From 58ad447a2e75b1595404b6b9e66d6237fd0881ad Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 13 Aug 2024 13:02:27 -0400 Subject: [PATCH] Include missing transpiled comments (#1278) * Transpile comments above annotations for common statement types. * Fix some missing transpiled comments for namespaced calls * Fix lint issue --- src/files/BrsFile.spec.ts | 144 ++++++++++++++++++++++++++++++++ src/parser/BrsTranspileState.ts | 10 +++ src/parser/Expression.ts | 12 ++- src/parser/Statement.ts | 28 +++++-- src/parser/TranspileState.ts | 20 ++++- 5 files changed, 201 insertions(+), 13 deletions(-) diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index d43ed2c38..a1c7ca736 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -2509,6 +2509,150 @@ describe('BrsFile', () => { `, 'trim', 'source/main.bs'); }); + it('includes annotation comments for class', async () => { + await testTranspile(` + 'comment1 + @annotation + 'comment2 + @annotation() + 'comment3 + class Beta + end class + `, ` + function __Beta_builder() + instance = {} + instance.new = sub() + end sub + return instance + end function + 'comment1 + 'comment2 + 'comment3 + function Beta() + instance = __Beta_builder() + instance.new() + return instance + end function + `, undefined, 'source/main.bs'); + }); + + it('includes annotation comments for function', async () => { + await testTranspile(` + 'comment1 + @annotation + 'comment2 + @annotation() + 'comment3 + function alpha() + end function + `, ` + 'comment1 + 'comment2 + 'comment3 + function alpha() + end function + `, undefined, 'source/main.bs'); + }); + + it('includes annotation comments for enum', async () => { + await testTranspile(` + 'comment1 + @annotation + 'comment2 + @annotation() + 'comment3 + enum Direction + up = "up" + end enum + `, ` + 'comment1 + 'comment2 + 'comment3 + `, undefined, 'source/main.bs'); + }); + + it('includes annotation comments for const', async () => { + await testTranspile(` + 'comment1 + @annotation + 'comment2 + @annotation() + 'comment3 + const direction = "up" + `, ` + 'comment1 + 'comment2 + 'comment3 + `, undefined, 'source/main.bs'); + }); + + it('includes annotation comments for empty namespaces', async () => { + await testTranspile(` + 'comment1 + @annotation + 'comment2 + @annotation() + 'comment3 + namespace alpha + 'comment4 + @annotation + 'comment5 + @annotation() + 'comment6 + namespace beta + end namespace + end namespace + `, ` + 'comment1 + 'comment2 + 'comment3 + 'comment4 + 'comment5 + 'comment6 + `, undefined, 'source/main.bs'); + }); + + it('includes comments above namespaced method call', async () => { + await testTranspile(` + sub main() + 'do nothing + utils.noop() + end sub + namespace utils + sub noop() + end sub + end namespace + `, ` + sub main() + 'do nothing + utils_noop() + end sub + sub utils_noop() + end sub + `, undefined, 'source/main.bs'); + }); + + it('includes comments above inferred namespace function call', async () => { + await testTranspile(` + namespace utils + sub test() + 'do nothing + noop() + end sub + sub noop() + end sub + end namespace + `, ` + sub utils_test() + 'do nothing + utils_noop() + end sub + + sub utils_noop() + end sub + `, undefined, 'source/main.bs'); + }); + it('keeps end-of-line comments with their line', async () => { await testTranspile(` function DoSomething() 'comment 1 diff --git a/src/parser/BrsTranspileState.ts b/src/parser/BrsTranspileState.ts index b17eef607..294238cda 100644 --- a/src/parser/BrsTranspileState.ts +++ b/src/parser/BrsTranspileState.ts @@ -3,6 +3,7 @@ import { Editor } from '../astUtils/Editor'; import type { BrsFile } from '../files/BrsFile'; import type { ClassStatement, ConditionalCompileStatement } from './Statement'; import { TranspileState } from './TranspileState'; +import type { Statement } from './AstNode'; export class BrsTranspileState extends TranspileState { public constructor( @@ -45,4 +46,13 @@ export class BrsTranspileState extends TranspileState { * Do not transpile leading comments */ public skipLeadingComments = false; + + /** + * Transpile all leading trivia for a given statement, including comments mixed between annotations + */ + public transpileAnnotations(node: Statement) { + return (node?.annotations ?? []).map(x => { + return x.transpile(this); + }); + } } diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 9ec07b240..84386fb64 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -146,7 +146,11 @@ export class CallExpression extends Expression { //transpile the name if (nameOverride) { - result.push(state.sourceNode(this.callee, nameOverride)); + result.push( + //transpile leading comments since we're bypassing callee.transpile (which would normally do this) + ...state.transpileLeadingCommentsForAstNode(this), + state.sourceNode(this.callee, nameOverride) + ); } else { result.push(...this.callee.transpile(state)); } @@ -553,6 +557,7 @@ export class DottedGetExpression extends Expression { //if the callee starts with a namespace name, transpile the name if (state.file.calleeStartsWithNamespace(this)) { return [ + ...state.transpileLeadingCommentsForAstNode(this), state.sourceNode(this, this.getName(ParseMode.BrightScript)) ]; } else { @@ -1184,6 +1189,8 @@ export class VariableExpression extends Expression { //if the callee is the name of a known namespace function if (namespace && state.file.calleeIsKnownNamespaceFunction(this, namespace.getName(ParseMode.BrighterScript))) { result.push( + //transpile leading comments since the token isn't being transpiled directly + ...state.transpileLeadingCommentsForAstNode(this), state.sourceNode(this, [ namespace.getName(ParseMode.BrightScript), '_', @@ -1828,7 +1835,8 @@ export class AnnotationExpression extends Expression { } transpile(state: BrsTranspileState) { - return []; + //transpile only our leading comments + return state.transpileComments(this.leadingTrivia); } walk(visitor: WalkVisitor, options: WalkOptions) { diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 047372ff5..1b1689c17 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -72,7 +72,7 @@ export class Body extends Statement implements TypedefProvider { } transpile(state: BrsTranspileState) { - let result = [] as TranspileResult; + let result: TranspileResult = state.transpileAnnotations(this); for (let i = 0; i < this.statements.length; i++) { let statement = this.statements[i]; let previousStatement = this.statements[i - 1]; @@ -419,7 +419,10 @@ export class ExpressionStatement extends Statement { public readonly location: Location | undefined; transpile(state: BrsTranspileState) { - return this.expression.transpile(state); + return [ + state.transpileAnnotations(this), + this.expression.transpile(state) + ]; } walk(visitor: WalkVisitor, options: WalkOptions) { @@ -542,14 +545,17 @@ export class FunctionStatement extends Statement implements TypedefProvider { return this.func.leadingTrivia; } - transpile(state: BrsTranspileState) { + transpile(state: BrsTranspileState): TranspileResult { //create a fake token using the full transpiled name let nameToken = { ...this.tokens.name, text: this.getName(ParseMode.BrightScript) }; - return this.func.transpile(state, nameToken); + return [ + ...state.transpileAnnotations(this), + ...this.func.transpile(state, nameToken) + ]; } getTypedef(state: BrsTranspileState) { @@ -1686,6 +1692,7 @@ export class NamespaceStatement extends Statement implements TypedefProvider { transpile(state: BrsTranspileState) { //namespaces don't actually have any real content, so just transpile their bodies return [ + state.transpileAnnotations(this), state.transpileLeadingComments(this.tokens.namespace), this.body.transpile(state), state.transpileLeadingComments(this.tokens.endNamespace) @@ -2625,11 +2632,12 @@ export class ClassStatement extends Statement implements TypedefProvider { * This invokes the builder, gets an instance of the class, then invokes the "new" function on that class. */ private getTranspiledClassFunction(state: BrsTranspileState) { - let result = [] as TranspileResult; + let result: TranspileResult = state.transpileAnnotations(this); const constructorFunction = this.getConstructorFunction(); const constructorParams = constructorFunction ? constructorFunction.func.parameters : []; result.push( + state.transpileLeadingComments(this.tokens.class), state.sourceNode(this.tokens.class, 'function'), state.sourceNode(this.tokens.class, ' '), state.sourceNode(this.tokens.name, this.getName(ParseMode.BrightScript)), @@ -3346,8 +3354,9 @@ export class EnumStatement extends Statement implements TypedefProvider { } transpile(state: BrsTranspileState) { - //enum declarations do not exist at runtime, so don't transpile anything... + //enum declarations don't exist at runtime, so just transpile comments and trivia return [ + state.transpileAnnotations(this), state.transpileLeadingComments(this.tokens.enum) ]; } @@ -3551,8 +3560,11 @@ export class ConstStatement extends Statement implements TypedefProvider { } public transpile(state: BrsTranspileState): TranspileResult { - //const declarations don't exist at runtime, so just transpile empty - return [state.transpileLeadingComments(this.tokens.const)]; + //const declarations don't exist at runtime, so just transpile comments and trivia + return [ + state.transpileAnnotations(this), + state.transpileLeadingComments(this.tokens.const) + ]; } getTypedef(state: BrsTranspileState): TranspileResult { diff --git a/src/parser/TranspileState.ts b/src/parser/TranspileState.ts index ae669f779..74c9a2ef8 100644 --- a/src/parser/TranspileState.ts +++ b/src/parser/TranspileState.ts @@ -2,12 +2,11 @@ import { SourceNode } from 'source-map'; import type { Location } from 'vscode-languageserver'; import type { BsConfig } from '../BsConfig'; import { TokenKind } from '../lexer/TokenKind'; -import type { Token } from '../lexer/Token'; +import { type Token } from '../lexer/Token'; import type { RangeLike } from '../util'; import { util } from '../util'; import type { TranspileResult } from '../interfaces'; - interface TranspileToken { location?: Location; text: string; @@ -113,6 +112,17 @@ export class TranspileState { ); } + public transpileLeadingCommentsForAstNode(node: { leadingTrivia?: Token[] }) { + const leadingTrivia = node?.leadingTrivia ?? []; + const leadingCommentsSourceNodes = this.transpileComments(leadingTrivia); + if (leadingCommentsSourceNodes.length > 0) { + // indent in preparation for next text + leadingCommentsSourceNodes.push(this.indent()); + } + + return leadingCommentsSourceNodes; + } + public transpileLeadingComments(token: TranspileToken) { const leadingTrivia = (token?.leadingTrivia ?? []); const leadingCommentsSourceNodes = this.transpileComments(leadingTrivia); @@ -124,7 +134,7 @@ export class TranspileState { return leadingCommentsSourceNodes; } - public transpileComments(tokens: TranspileToken[]) { + public transpileComments(tokens: TranspileToken[], prepNextLine = false): Array { const leadingCommentsSourceNodes = []; const justComments = tokens.filter(t => t.kind === TokenKind.Comment || t.kind === TokenKind.Newline); let newLinesSinceComment = 0; @@ -150,6 +160,10 @@ export class TranspileState { } transpiledCommentAlready = true; } + //if we should prepare for the next line, add an indent (only if applicable) + if (prepNextLine && transpiledCommentAlready) { + leadingCommentsSourceNodes.push(this.indent()); + } return leadingCommentsSourceNodes; }