From 7363ac935d0f9287044b6642a14981453bc7dca6 Mon Sep 17 00:00:00 2001 From: Trevor Florence Date: Sun, 11 Oct 2026 12:16:45 -0700 Subject: [PATCH 1/4] Copy synthetic comments in SetOriginal and emit them once in TS transforms In the TypeScript 6 compiler, `setOriginalNode` merges the original node's emit node into the new node (`mergeEmitNode`), including its synthetic leading and trailing comments. In tsgo, `emitNode.copyFrom` copies comment and source map ranges, emit flags, and helpers, but not synthetic comments. So a synthetic comment attached to a declaration is dropped as soon as a transform replaces that declaration. For example, with a synthetic `/*leading*/` comment on `enum E { A }`, the TS transforms emit `var E; (function (E) { ... })(E || (E = {}));` with no comment at all. This PR: - copies synthetic comments in `copyFrom`, prepending them to any the destination already has, as `mergeEmitNode` does; - clears them on the `VariableDeclaration` that `addVarForDeclaration` creates for an enum or namespace, as Strada does (this was the `// !!! synthetic comments` TODO), so they are emitted once, on the `var` statement; - clears them on the class expression, its variable declaration, and the `__decorate` statement in the legacy decorators transform, so they are emitted once, on the `let` statement. The last item goes beyond a port. Strada calls `setOriginalNode` on all of those nodes without clearing anything, and TypeScript 6.0.3 prints a class's synthetic comment four times under `experimentalDecorators`: ```js /*synthetic*/ let /*synthetic*/ C = /*synthetic*/ class C { }; /*synthetic*/ C = __decorate([ dec ], C); ``` That is #54742 (labeled Bug and Help Wanted). Its fix, #54750, was closed unmerged in the post-6.0 freeze. Copying synthetic comments without also clearing them here would carry that bug into tsgo. One difference from Strada remains: a trailing synthetic comment on an enum or namespace is emitted after the closing `})(E || (E = {}));`, not after `var E;`. tsgo's printer treats `EFNoTrailingComments` on the `var` statement as also suppressing synthetic trailing comments. The new position matches where source trailing comments for these declarations already go. The new test `TestSyntheticCommentsAreEmittedOnce` covers an enum, a namespace, and a decorated class, with a leading and a trailing synthetic comment on each. Without this change, all three cases emit no comment. --- tsc/internal/printer/emitcontext.go | 8 ++ .../tstransforms/legacydecorators.go | 7 ++ .../tstransforms/runtimesyntax.go | 3 +- .../tstransforms/syntheticcomments_test.go | 79 +++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 tsc/internal/transformers/tstransforms/syntheticcomments_test.go 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/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..617b64cc7106c 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. diff --git a/tsc/internal/transformers/tstransforms/syntheticcomments_test.go b/tsc/internal/transformers/tstransforms/syntheticcomments_test.go new file mode 100644 index 0000000000000..53108b42d5581 --- /dev/null +++ b/tsc/internal/transformers/tstransforms/syntheticcomments_test.go @@ -0,0 +1,79 @@ +package tstransforms_test + +import ( + "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" +) + +const decorateHelper = `var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +` + +// Synthetic comments attached to a declaration are copied by SetOriginal to every node created from +// it, so each transform must keep them on exactly one of the emitted nodes. +func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { + t.Parallel() + data := []struct { + title string + input string + output string + newTx func(opts *transformers.TransformOptions) *transformers.Transformer + }{ + { + title: "EnumDeclaration", + input: "enum E { A }", + output: "/*leading*/ var E;\n(function (E) {\n E[E[\"A\"] = 0] = \"A\";\n})(E || (E = {})); /*trailing*/", + newTx: tstransforms.NewRuntimeSyntaxTransformer, + }, + { + title: "DecoratedClass", + input: "declare function dec(c: any): any;\n@dec class C {}", + output: decorateHelper + "/*leading*/ let C = class C {\n}; /*trailing*/\nC = __decorate([\n dec\n], C);", + newTx: tstransforms.NewLegacyDecoratorsTransformer, + }, + { + title: "ModuleDeclaration", + input: "namespace N { export const x = 1; }", + output: "/*leading*/ var N;\n(function (N) {\n N.x = 1;\n})(N || (N = {})); /*trailing*/", + newTx: tstransforms.NewRuntimeSyntaxTransformer, + }, + } + + 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} + 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) + emittestutil.CheckEmit(t, emitContext, file, rec.output) + }) + } +} From c74815f7410b86b05a1233d1b78a45b5f33b83e1 Mon Sep 17 00:00:00 2001 From: Trevor Florence Date: Sun, 11 Oct 2026 13:42:11 -0700 Subject: [PATCH 2/4] Clear synthetic comments on enum and namespace statements when a var is added Strada clears them on the enum and namespace statements when it emits a leading `var` (ts.ts, `visitEnumDeclaration` and `visitModuleDeclaration`), so the comments are emitted once, on the `var` statement. tsgo relied on `EFNoLeadingComments` suppressing synthesized comments instead, which #64453 reports as a divergence from Strada and #64488 would remove. --- .../transformers/tstransforms/runtimesyntax.go | 10 ++++++++++ .../tstransforms/syntheticcomments_test.go | 8 ++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tsc/internal/transformers/tstransforms/runtimesyntax.go b/tsc/internal/transformers/tstransforms/runtimesyntax.go index 617b64cc7106c..6b2c615550945 100644 --- a/tsc/internal/transformers/tstransforms/runtimesyntax.go +++ b/tsc/internal/transformers/tstransforms/runtimesyntax.go @@ -338,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)) @@ -480,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 index 53108b42d5581..1ae14b08457bb 100644 --- a/tsc/internal/transformers/tstransforms/syntheticcomments_test.go +++ b/tsc/internal/transformers/tstransforms/syntheticcomments_test.go @@ -23,6 +23,10 @@ const decorateHelper = `var __decorate = (this && this.__decorate) || function ( // Synthetic comments attached to a declaration are copied by SetOriginal to every node created 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 { @@ -34,7 +38,7 @@ func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { { title: "EnumDeclaration", input: "enum E { A }", - output: "/*leading*/ var E;\n(function (E) {\n E[E[\"A\"] = 0] = \"A\";\n})(E || (E = {})); /*trailing*/", + output: "/*leading*/ var E;\n(function (E) {\n E[E[\"A\"] = 0] = \"A\";\n})(E || (E = {}));", newTx: tstransforms.NewRuntimeSyntaxTransformer, }, { @@ -46,7 +50,7 @@ func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { { title: "ModuleDeclaration", input: "namespace N { export const x = 1; }", - output: "/*leading*/ var N;\n(function (N) {\n N.x = 1;\n})(N || (N = {})); /*trailing*/", + output: "/*leading*/ var N;\n(function (N) {\n N.x = 1;\n})(N || (N = {}));", newTx: tstransforms.NewRuntimeSyntaxTransformer, }, } From c17251e8c8a27bea96163a2240dbc15567fe5808 Mon Sep 17 00:00:00 2001 From: Trevor Florence Date: Sun, 11 Oct 2026 14:05:54 -0700 Subject: [PATCH 3/4] Clear synthetic comments on the static block initializer statement When a static property initializer becomes `static { this.x = ...; }`, the property's comments are emitted on the static block. Strada clears them on the initializer statement (classFields.ts, `transformPropertyOrClassStaticBlock` caller in `visitPropertyDeclaration`). tsgo only set `EFNoComments`, which stops suppressing synthetic comments once #64488 lands, so the comments would be printed twice. --- .../transformers/estransforms/classfields.go | 3 +++ .../tstransforms/syntheticcomments_test.go | 22 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) 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/tstransforms/syntheticcomments_test.go b/tsc/internal/transformers/tstransforms/syntheticcomments_test.go index 1ae14b08457bb..32f3b7ce2ee63 100644 --- a/tsc/internal/transformers/tstransforms/syntheticcomments_test.go +++ b/tsc/internal/transformers/tstransforms/syntheticcomments_test.go @@ -10,6 +10,7 @@ import ( "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" "github.com/microsoft/TypeScript/tsc/internal/transformers/tstransforms" ) @@ -34,6 +35,10 @@ func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { input string output string newTx func(opts *transformers.TransformOptions) *transformers.Transformer + // The node to attach the comments to; defaults to the last statement. + declaration func(file *ast.SourceFile) *ast.Node + // Defaults to ESNext. + target core.ScriptTarget }{ { title: "EnumDeclaration", @@ -47,6 +52,14 @@ func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { output: decorateHelper + "/*leading*/ let C = class C {\n}; /*trailing*/\nC = __decorate([\n dec\n], C);", newTx: tstransforms.NewLegacyDecoratorsTransformer, }, + { + title: "StaticPropertyDeclaration", + input: "class C { static x = 1; }", + output: "class C {\n /*leading*/ static { this.x = 1; } /*trailing*/\n}", + newTx: estransforms.GetESTransformer, + declaration: func(file *ast.SourceFile) *ast.Node { return file.Statements.Nodes[0].Members()[0] }, + target: core.ScriptTargetES2022, + }, { title: "ModuleDeclaration", input: "namespace N { export const x = 1; }", @@ -60,7 +73,11 @@ func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { t.Parallel() file := parsetestutil.ParseTypeScript(rec.input, false /*jsx*/) parsetestutil.CheckDiagnostics(t, file) - compilerOptions := &core.CompilerOptions{ExperimentalDecorators: core.TSTrue} + target := rec.target + if target == core.ScriptTargetNone { + target = core.ScriptTargetESNext + } + compilerOptions := &core.CompilerOptions{ExperimentalDecorators: core.TSTrue, Target: target, UseDefineForClassFields: core.TSFalse} esnext := func(ast.HasFileName) core.ModuleKind { return core.ModuleKindESNext } c, _ := checker.NewChecker(&fakeProgram{ singleThreaded: true, @@ -72,6 +89,9 @@ func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { emitContext := printer.NewEmitContext() emitResolver := c.NewEmitResolver(emitContext) declaration := file.Statements.Nodes[len(file.Statements.Nodes)-1] + if rec.declaration != nil { + declaration = rec.declaration(file) + } 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} From 2c4d14acd38812a577e02926421d34dead819211 Mon Sep 17 00:00:00 2001 From: Trevor Florence Date: Mon, 12 Oct 2026 13:55:28 -0700 Subject: [PATCH 4/4] Test synthetic comment propagation per package and count emitted comments Split the synthetic comment tests by package instead of testing several packages from tstransforms: - printer: SetOriginal copying synthetic comments is tested next to the other emit context tests. - tstransforms: the enum, namespace and legacy decorator cases. - estransforms: the static property moved into a static block. The transform tests now count how often each comment is emitted instead of matching the whole output, which removes the duplicated __decorate helper string. emittestutil gains an Emit helper that returns the printed output so tests can make such assertions. A compiler baseline cannot observe this change: in a normal emit, the only synthetic comments in JS output come from the const enum inliner, which runs after these transforms. --- tsc/internal/printer/emitcontext_test.go | 35 ++++++++++ .../testutil/emittestutil/emittestutil.go | 9 ++- .../estransforms/classfields_test.go | 35 ++++++++++ .../tstransforms/syntheticcomments_test.go | 67 ++++--------------- 4 files changed, 91 insertions(+), 55 deletions(-) create mode 100644 tsc/internal/printer/emitcontext_test.go create mode 100644 tsc/internal/transformers/estransforms/classfields_test.go 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_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/syntheticcomments_test.go b/tsc/internal/transformers/tstransforms/syntheticcomments_test.go index 32f3b7ce2ee63..b590fef6cdcca 100644 --- a/tsc/internal/transformers/tstransforms/syntheticcomments_test.go +++ b/tsc/internal/transformers/tstransforms/syntheticcomments_test.go @@ -1,6 +1,7 @@ package tstransforms_test import ( + "strings" "testing" "github.com/microsoft/TypeScript/tsc/internal/ast" @@ -10,19 +11,11 @@ import ( "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" "github.com/microsoft/TypeScript/tsc/internal/transformers/tstransforms" + "gotest.tools/v3/assert" ) -const decorateHelper = `var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -` - -// Synthetic comments attached to a declaration are copied by SetOriginal to every node created from +// 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 @@ -31,41 +24,14 @@ const decorateHelper = `var __decorate = (this && this.__decorate) || function ( func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { t.Parallel() data := []struct { - title string - input string - output string - newTx func(opts *transformers.TransformOptions) *transformers.Transformer - // The node to attach the comments to; defaults to the last statement. - declaration func(file *ast.SourceFile) *ast.Node - // Defaults to ESNext. - target core.ScriptTarget + title string + input string + newTx func(opts *transformers.TransformOptions) *transformers.Transformer + trailingCount int }{ - { - title: "EnumDeclaration", - input: "enum E { A }", - output: "/*leading*/ var E;\n(function (E) {\n E[E[\"A\"] = 0] = \"A\";\n})(E || (E = {}));", - newTx: tstransforms.NewRuntimeSyntaxTransformer, - }, - { - title: "DecoratedClass", - input: "declare function dec(c: any): any;\n@dec class C {}", - output: decorateHelper + "/*leading*/ let C = class C {\n}; /*trailing*/\nC = __decorate([\n dec\n], C);", - newTx: tstransforms.NewLegacyDecoratorsTransformer, - }, - { - title: "StaticPropertyDeclaration", - input: "class C { static x = 1; }", - output: "class C {\n /*leading*/ static { this.x = 1; } /*trailing*/\n}", - newTx: estransforms.GetESTransformer, - declaration: func(file *ast.SourceFile) *ast.Node { return file.Statements.Nodes[0].Members()[0] }, - target: core.ScriptTargetES2022, - }, - { - title: "ModuleDeclaration", - input: "namespace N { export const x = 1; }", - output: "/*leading*/ var N;\n(function (N) {\n N.x = 1;\n})(N || (N = {}));", - newTx: tstransforms.NewRuntimeSyntaxTransformer, - }, + {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 { @@ -73,11 +39,7 @@ func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { t.Parallel() file := parsetestutil.ParseTypeScript(rec.input, false /*jsx*/) parsetestutil.CheckDiagnostics(t, file) - target := rec.target - if target == core.ScriptTargetNone { - target = core.ScriptTargetESNext - } - compilerOptions := &core.CompilerOptions{ExperimentalDecorators: core.TSTrue, Target: target, UseDefineForClassFields: core.TSFalse} + compilerOptions := &core.CompilerOptions{ExperimentalDecorators: core.TSTrue, Target: core.ScriptTargetESNext} esnext := func(ast.HasFileName) core.ModuleKind { return core.ModuleKindESNext } c, _ := checker.NewChecker(&fakeProgram{ singleThreaded: true, @@ -89,15 +51,14 @@ func TestSyntheticCommentsAreEmittedOnce(t *testing.T) { emitContext := printer.NewEmitContext() emitResolver := c.NewEmitResolver(emitContext) declaration := file.Statements.Nodes[len(file.Statements.Nodes)-1] - if rec.declaration != nil { - declaration = rec.declaration(file) - } 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) - emittestutil.CheckEmit(t, emitContext, file, rec.output) + 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) }) } }