diff --git a/tsc/internal/printer/emitcontext.go b/tsc/internal/printer/emitcontext.go index 7953212bd40e7..33d263c2dade7 100644 --- a/tsc/internal/printer/emitcontext.go +++ b/tsc/internal/printer/emitcontext.go @@ -550,6 +550,14 @@ func (e *emitNode) copyFrom(source *emitNode) { snippetElement := *source.snippetElement e.snippetElement = &snippetElement } + // Synthetic comments are prepended to any existing comments on the destination, + // as in `mergeEmitNode` in the TypeScript 6 compiler. + if len(source.leadingComments) > 0 { + e.leadingComments = append(slices.Clone(source.leadingComments), e.leadingComments...) + } + if len(source.trailingComments) > 0 { + e.trailingComments = append(slices.Clone(source.trailingComments), e.trailingComments...) + } } func (c *EmitContext) EmitFlags(node *ast.Node) EmitFlags { diff --git a/tsc/internal/printer/emitcontext_test.go b/tsc/internal/printer/emitcontext_test.go new file mode 100644 index 0000000000000..5f9630327902f --- /dev/null +++ b/tsc/internal/printer/emitcontext_test.go @@ -0,0 +1,35 @@ +package printer_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/printer" + "gotest.tools/v3/assert" +) + +func syntheticCommentTexts(comments []printer.SynthesizedComment) []string { + texts := make([]string, len(comments)) + for i, comment := range comments { + texts[i] = comment.Text + } + return texts +} + +func TestSetOriginalCopiesSyntheticComments(t *testing.T) { + t.Parallel() + emitContext := printer.NewEmitContext() + factory := emitContext.Factory + + original := factory.NewIdentifier("a") + emitContext.AddSyntheticLeadingComment(original, ast.KindMultiLineCommentTrivia, "leading", false /*hasTrailingNewLine*/) + emitContext.AddSyntheticTrailingComment(original, ast.KindMultiLineCommentTrivia, "trailing", false /*hasTrailingNewLine*/) + + node := factory.NewIdentifier("b") + emitContext.AddSyntheticLeadingComment(node, ast.KindMultiLineCommentTrivia, "existing", false /*hasTrailingNewLine*/) + emitContext.SetOriginal(node, original) + + // The original node's comments are prepended to the ones the node already has. + assert.DeepEqual(t, syntheticCommentTexts(emitContext.GetSyntheticLeadingComments(node)), []string{"leading", "existing"}) + assert.DeepEqual(t, syntheticCommentTexts(emitContext.GetSyntheticTrailingComments(node)), []string{"trailing"}) +} diff --git a/tsc/internal/testutil/emittestutil/emittestutil.go b/tsc/internal/testutil/emittestutil/emittestutil.go index 93800e2dd64f9..a6e09eec6846d 100644 --- a/tsc/internal/testutil/emittestutil/emittestutil.go +++ b/tsc/internal/testutil/emittestutil/emittestutil.go @@ -13,6 +13,12 @@ import ( // Checks that pretty-printing the given file matches the expected output. func CheckEmit(t *testing.T, emitContext *printer.EmitContext, file *ast.SourceFile, expected string) { + t.Helper() + assert.Equal(t, expected, Emit(t, emitContext, file)) +} + +// Pretty-prints the given file and checks that the output parses without errors. +func Emit(t *testing.T, emitContext *printer.EmitContext, file *ast.SourceFile) string { t.Helper() printer := printer.NewPrinter( printer.PrinterOptions{ @@ -22,8 +28,7 @@ func CheckEmit(t *testing.T, emitContext *printer.EmitContext, file *ast.SourceF emitContext, ) text := printer.EmitSourceFile(file) - actual := strings.TrimSuffix(text, "\n") - assert.Equal(t, expected, actual) file2 := parsetestutil.ParseTypeScript(text, file.LanguageVariant == core.LanguageVariantJSX) parsetestutil.CheckDiagnosticsMessage(t, file2, "error on reparse: ") + return strings.TrimSuffix(text, "\n") } diff --git a/tsc/internal/transformers/estransforms/classfields.go b/tsc/internal/transformers/estransforms/classfields.go index bf1b79fc3225f..80179bbe0373b 100644 --- a/tsc/internal/transformers/estransforms/classfields.go +++ b/tsc/internal/transformers/estransforms/classfields.go @@ -972,7 +972,10 @@ func (tx *classFieldsTransformer) transformPublicFieldInitializer(node *ast.Prop tx.EmitContext().SetOriginal(staticBlock, node.AsNode()) tx.EmitContext().SetCommentRange(staticBlock, node.Loc) + // Drop comments from the statement to avoid printing them twice. tx.EmitContext().AddEmitFlags(initializerStatement, printer.EFNoComments) + tx.EmitContext().SetSyntheticLeadingComments(initializerStatement, nil) + tx.EmitContext().SetSyntheticTrailingComments(initializerStatement, nil) return staticBlock } } diff --git a/tsc/internal/transformers/estransforms/classfields_test.go b/tsc/internal/transformers/estransforms/classfields_test.go new file mode 100644 index 0000000000000..f54dc0653cf0e --- /dev/null +++ b/tsc/internal/transformers/estransforms/classfields_test.go @@ -0,0 +1,35 @@ +package estransforms_test + +import ( + "strings" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/printer" + "github.com/microsoft/TypeScript/tsc/internal/testutil/emittestutil" + "github.com/microsoft/TypeScript/tsc/internal/testutil/parsetestutil" + "github.com/microsoft/TypeScript/tsc/internal/transformers" + "github.com/microsoft/TypeScript/tsc/internal/transformers/estransforms" + "gotest.tools/v3/assert" +) + +// When a static property initializer is moved into a `static {}` block, SetOriginal copies the +// property's synthetic comments to both the block and the statement inside it, so the transform +// must keep them on the block only. +func TestStaticPropertySyntheticCommentsAreEmittedOnce(t *testing.T) { + t.Parallel() + file := parsetestutil.ParseTypeScript("class C { static x = 1; }", false /*jsx*/) + parsetestutil.CheckDiagnostics(t, file) + property := file.Statements.Nodes[0].Members()[0] + emitContext := printer.NewEmitContext() + emitContext.AddSyntheticLeadingComment(property, ast.KindMultiLineCommentTrivia, "leading", false /*hasTrailingNewLine*/) + emitContext.AddSyntheticTrailingComment(property, ast.KindMultiLineCommentTrivia, "trailing", false /*hasTrailingNewLine*/) + compilerOptions := &core.CompilerOptions{Target: core.ScriptTargetES2022, UseDefineForClassFields: core.TSFalse} + opts := &transformers.TransformOptions{CompilerOptions: compilerOptions, Context: emitContext} + file = estransforms.GetESTransformer(opts).TransformSourceFile(file) + output := emittestutil.Emit(t, emitContext, file) + assert.Assert(t, strings.Contains(output, "static {"), output) + assert.Equal(t, strings.Count(output, "/*leading*/"), 1, output) + assert.Equal(t, strings.Count(output, "/*trailing*/"), 1, output) +} diff --git a/tsc/internal/transformers/tstransforms/legacydecorators.go b/tsc/internal/transformers/tstransforms/legacydecorators.go index 8eaa45e5071ad..48c14c62d1973 100644 --- a/tsc/internal/transformers/tstransforms/legacydecorators.go +++ b/tsc/internal/transformers/tstransforms/legacydecorators.go @@ -468,6 +468,9 @@ func (tx *LegacyDecoratorsTransformer) transformClassDeclarationWithClassDecorat tx.EmitContext().SetOriginal(classExpression, node.AsNode()) classExpression.Loc = location + // The class's synthetic comments are emitted once, on the variable statement. + tx.EmitContext().SetSyntheticLeadingComments(classExpression, nil) + tx.EmitContext().SetSyntheticTrailingComments(classExpression, nil) // let ${name} = ${classExpression} where name is either declaredName if the class doesn't contain self-reference // or decoratedClassAlias if the class contain self-reference. @@ -482,6 +485,8 @@ func (tx *LegacyDecoratorsTransformer) transformClassDeclarationWithClassDecorat varInitializer, ) tx.EmitContext().SetOriginal(varDecl, node.AsNode()) + tx.EmitContext().SetSyntheticLeadingComments(varDecl, nil) + tx.EmitContext().SetSyntheticTrailingComments(varDecl, nil) varDeclList := tx.Factory().NewVariableDeclarationList(tx.Factory().NewNodeList([]*ast.Node{varDecl}), ast.NodeFlagsLet) varStatement := tx.Factory().NewVariableStatement(nil, varDeclList) @@ -562,6 +567,8 @@ func (tx *LegacyDecoratorsTransformer) getConstructorDecorationStatement(node *a if expression != nil { result := tx.Factory().NewExpressionStatement(expression) tx.EmitContext().SetOriginal(result, node.AsNode()) + tx.EmitContext().SetSyntheticLeadingComments(result, nil) + tx.EmitContext().SetSyntheticTrailingComments(result, nil) return result } return nil diff --git a/tsc/internal/transformers/tstransforms/runtimesyntax.go b/tsc/internal/transformers/tstransforms/runtimesyntax.go index 35c29262606b0..6b2c615550945 100644 --- a/tsc/internal/transformers/tstransforms/runtimesyntax.go +++ b/tsc/internal/transformers/tstransforms/runtimesyntax.go @@ -258,7 +258,8 @@ func (tx *RuntimeSyntaxTransformer) addVarForDeclaration(statements []*ast.State varStatement := tx.Factory().NewVariableStatement(modifiers, varDecls) tx.EmitContext().SetOriginal(varDecl, node) - // !!! synthetic comments + tx.EmitContext().SetSyntheticLeadingComments(varDecl, nil) + tx.EmitContext().SetSyntheticTrailingComments(varDecl, nil) tx.EmitContext().SetOriginal(varStatement, node) // Adjust the source map emit to match the old emitter. @@ -337,6 +338,11 @@ func (tx *RuntimeSyntaxTransformer) visitEnumDeclaration(node *ast.EnumDeclarati enumCall := tx.Factory().NewCallExpression(tx.Factory().NewParenthesizedExpression(enumFunc), nil, nil, tx.Factory().NewNodeList([]*ast.Node{enumArg}), ast.NodeFlagsNone) enumStatement := tx.Factory().NewExpressionStatement(enumCall) tx.EmitContext().SetOriginal(enumStatement, node.AsNode()) + if varAdded { + // If a variable was added, synthetic comments are emitted on it, not on the enumStatement. + tx.EmitContext().SetSyntheticLeadingComments(enumStatement, nil) + tx.EmitContext().SetSyntheticTrailingComments(enumStatement, nil) + } tx.EmitContext().AssignCommentAndSourceMapRanges(enumStatement, node.AsNode()) tx.EmitContext().AddEmitFlags(enumStatement, emitFlags) return tx.Factory().NewSyntaxList(append(statements, enumStatement)) @@ -479,6 +485,11 @@ func (tx *RuntimeSyntaxTransformer) visitModuleDeclaration(node *ast.ModuleDecla moduleCall := tx.Factory().NewCallExpression(tx.Factory().NewParenthesizedExpression(moduleFunc), nil, nil, tx.Factory().NewNodeList([]*ast.Node{moduleArg}), ast.NodeFlagsNone) moduleStatement := tx.Factory().NewExpressionStatement(moduleCall) tx.EmitContext().SetOriginal(moduleStatement, node.AsNode()) + if varAdded { + // If a variable was added, synthetic comments are emitted on it, not on the moduleStatement. + tx.EmitContext().SetSyntheticLeadingComments(moduleStatement, nil) + tx.EmitContext().SetSyntheticTrailingComments(moduleStatement, nil) + } tx.EmitContext().AssignCommentAndSourceMapRanges(moduleStatement, node.AsNode()) tx.EmitContext().AddEmitFlags(moduleStatement, emitFlags) return tx.Factory().NewSyntaxList(append(statements, moduleStatement)) diff --git a/tsc/internal/transformers/tstransforms/syntheticcomments_test.go b/tsc/internal/transformers/tstransforms/syntheticcomments_test.go new file mode 100644 index 0000000000000..b590fef6cdcca --- /dev/null +++ b/tsc/internal/transformers/tstransforms/syntheticcomments_test.go @@ -0,0 +1,64 @@ +package tstransforms_test + +import ( + "strings" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/checker" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/printer" + "github.com/microsoft/TypeScript/tsc/internal/testutil/emittestutil" + "github.com/microsoft/TypeScript/tsc/internal/testutil/parsetestutil" + "github.com/microsoft/TypeScript/tsc/internal/transformers" + "github.com/microsoft/TypeScript/tsc/internal/transformers/tstransforms" + "gotest.tools/v3/assert" +) + +// SetOriginal copies the synthetic comments of a declaration to every node a transform creates from +// it, so each transform must keep them on exactly one of the emitted nodes. +// +// For an enum or namespace, both comments belong on the `var` statement, as in TypeScript 6. The +// trailing one is currently missing there because EFNoTrailingComments on that statement also +// suppresses synthetic comments (#64453). +func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { + t.Parallel() + data := []struct { + title string + input string + newTx func(opts *transformers.TransformOptions) *transformers.Transformer + trailingCount int + }{ + {title: "EnumDeclaration", input: "enum E { A }", newTx: tstransforms.NewRuntimeSyntaxTransformer, trailingCount: 0}, + {title: "ModuleDeclaration", input: "namespace N { export const x = 1; }", newTx: tstransforms.NewRuntimeSyntaxTransformer, trailingCount: 0}, + {title: "DecoratedClass", input: "declare function dec(c: any): any;\n@dec class C {}", newTx: tstransforms.NewLegacyDecoratorsTransformer, trailingCount: 1}, + } + + for _, rec := range data { + t.Run(rec.title, func(t *testing.T) { + t.Parallel() + file := parsetestutil.ParseTypeScript(rec.input, false /*jsx*/) + parsetestutil.CheckDiagnostics(t, file) + compilerOptions := &core.CompilerOptions{ExperimentalDecorators: core.TSTrue, Target: core.ScriptTargetESNext} + esnext := func(ast.HasFileName) core.ModuleKind { return core.ModuleKindESNext } + c, _ := checker.NewChecker(&fakeProgram{ + singleThreaded: true, + compilerOptions: compilerOptions, + files: []*ast.SourceFile{file}, + getEmitModuleFormatOfFile: esnext, + getImpliedNodeFormatForEmit: esnext, + }, nil) + emitContext := printer.NewEmitContext() + emitResolver := c.NewEmitResolver(emitContext) + declaration := file.Statements.Nodes[len(file.Statements.Nodes)-1] + emitContext.AddSyntheticLeadingComment(declaration, ast.KindMultiLineCommentTrivia, "leading", false /*hasTrailingNewLine*/) + emitContext.AddSyntheticTrailingComment(declaration, ast.KindMultiLineCommentTrivia, "trailing", false /*hasTrailingNewLine*/) + opts := &transformers.TransformOptions{CompilerOptions: compilerOptions, Context: emitContext, EmitResolver: emitResolver, Resolver: emitResolver} + file = tstransforms.NewTypeEraserTransformer(opts).TransformSourceFile(file) + file = rec.newTx(opts).TransformSourceFile(file) + output := emittestutil.Emit(t, emitContext, file) + assert.Equal(t, strings.Count(output, "/*leading*/"), 1, output) + assert.Equal(t, strings.Count(output, "/*trailing*/"), rec.trailingCount, output) + }) + } +}