From d2b1e1fcb787c043de46eef9a16c912f984212e1 Mon Sep 17 00:00:00 2001 From: Javier Zapien Date: Sat, 12 Sep 2026 07:14:07 +0000 Subject: [PATCH] Remove duplicated comments on legacy decorators --- tsc/internal/printer/emitcontext.go | 6 + tsc/internal/printer/emitcontext_test.go | 66 ++++++++++ .../tstransforms/legacydecorators.go | 9 ++ .../tstransforms/legacydecorators_test.go | 116 ++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 tsc/internal/printer/emitcontext_test.go create mode 100644 tsc/internal/transformers/tstransforms/legacydecorators_test.go diff --git a/tsc/internal/printer/emitcontext.go b/tsc/internal/printer/emitcontext.go index ee483873f0d58..eb60ffb048b6e 100644 --- a/tsc/internal/printer/emitcontext.go +++ b/tsc/internal/printer/emitcontext.go @@ -571,6 +571,12 @@ func (e *emitNode) copyFrom(source *emitNode) { snippetElement := *source.snippetElement e.snippetElement = &snippetElement } + 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..62d2f06848f35 --- /dev/null +++ b/tsc/internal/printer/emitcontext_test.go @@ -0,0 +1,66 @@ +package printer_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/printer" + "gotest.tools/v3/assert" +) + +func TestEmitContext_CopyFrom_PreservesSyntheticComments(t *testing.T) { + t.Parallel() + ctx := printer.NewEmitContext() + orig := &ast.Node{Kind: ast.KindIdentifier} + ctx.SetSyntheticLeadingComments(orig, []printer.SynthesizedComment{ + {Text: "/*leading*/"}, + }) + ctx.SetSyntheticTrailingComments(orig, []printer.SynthesizedComment{ + {Text: "/*trailing*/"}, + }) + + newNode := &ast.Node{Kind: ast.KindIdentifier} + ctx.SetOriginal(newNode, orig) + + leading := ctx.GetSyntheticLeadingComments(newNode) + trailing := ctx.GetSyntheticTrailingComments(newNode) + assert.Assert(t, len(leading) == 1) + assert.Equal(t, "/*leading*/", leading[0].Text) + assert.Assert(t, len(trailing) == 1) + assert.Equal(t, "/*trailing*/", trailing[0].Text) + + // Verify independent slice clone semantics: mutating newNode comments doesn't mutate orig + ctx.SetSyntheticLeadingComments(newNode, nil) + assert.Assert(t, len(ctx.GetSyntheticLeadingComments(orig)) == 1) +} + +func TestEmitContext_CopyFrom_MergesWithExistingComments(t *testing.T) { + t.Parallel() + ctx := printer.NewEmitContext() + orig := &ast.Node{Kind: ast.KindIdentifier} + ctx.SetSyntheticLeadingComments(orig, []printer.SynthesizedComment{ + {Text: "/*source-leading*/"}, + }) + ctx.SetSyntheticTrailingComments(orig, []printer.SynthesizedComment{ + {Text: "/*source-trailing*/"}, + }) + + newNode := &ast.Node{Kind: ast.KindIdentifier} + ctx.SetSyntheticLeadingComments(newNode, []printer.SynthesizedComment{ + {Text: "/*dest-leading*/"}, + }) + ctx.SetSyntheticTrailingComments(newNode, []printer.SynthesizedComment{ + {Text: "/*dest-trailing*/"}, + }) + + ctx.SetOriginal(newNode, orig) + + leading := ctx.GetSyntheticLeadingComments(newNode) + trailing := ctx.GetSyntheticTrailingComments(newNode) + assert.Assert(t, len(leading) == 2) + assert.Equal(t, "/*source-leading*/", leading[0].Text) + assert.Equal(t, "/*dest-leading*/", leading[1].Text) + assert.Assert(t, len(trailing) == 2) + assert.Equal(t, "/*source-trailing*/", trailing[0].Text) + assert.Equal(t, "/*dest-trailing*/", trailing[1].Text) +} diff --git a/tsc/internal/transformers/tstransforms/legacydecorators.go b/tsc/internal/transformers/tstransforms/legacydecorators.go index 8eaa45e5071ad..eda094f6c68e6 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 + // Drop synthetic comments to avoid duplicating them on the inner class expression (fixes #54742). + 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,9 @@ func (tx *LegacyDecoratorsTransformer) transformClassDeclarationWithClassDecorat varInitializer, ) tx.EmitContext().SetOriginal(varDecl, node.AsNode()) + // Drop synthetic comments to avoid duplicating them on the variable declaration (fixes #54742). + 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 +568,9 @@ func (tx *LegacyDecoratorsTransformer) getConstructorDecorationStatement(node *a if expression != nil { result := tx.Factory().NewExpressionStatement(expression) tx.EmitContext().SetOriginal(result, node.AsNode()) + // Drop synthetic comments to avoid duplicating them on the decorator invocation statement (fixes #54742). + tx.EmitContext().SetSyntheticLeadingComments(result, nil) + tx.EmitContext().SetSyntheticTrailingComments(result, nil) return result } return nil diff --git a/tsc/internal/transformers/tstransforms/legacydecorators_test.go b/tsc/internal/transformers/tstransforms/legacydecorators_test.go new file mode 100644 index 0000000000000..637e54dc3b33c --- /dev/null +++ b/tsc/internal/transformers/tstransforms/legacydecorators_test.go @@ -0,0 +1,116 @@ +package tstransforms_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/binder" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/printer" + "github.com/microsoft/typescript-go/internal/testutil/emittestutil" + "github.com/microsoft/typescript-go/internal/testutil/parsetestutil" + "github.com/microsoft/typescript-go/internal/transformers" + "github.com/microsoft/typescript-go/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; +};` + +func TestLegacyDecorators_SyntheticComments(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + leading []printer.SynthesizedComment + trailing []printer.SynthesizedComment + expected string + }{ + { + name: "leading comment on decorated class", + input: "@decorate()\nclass Foo {}", + leading: []printer.SynthesizedComment{ + {Kind: ast.KindMultiLineCommentTrivia, Text: "leading", Loc: core.NewTextRange(-1, -1)}, + }, + expected: decorateHelper + "\n/*leading*/ let Foo = class Foo {\n};\nFoo = __decorate([\n decorate()\n], Foo);", + }, + { + name: "trailing comment on decorated class", + input: "@decorate()\nclass Foo {}", + trailing: []printer.SynthesizedComment{ + {Kind: ast.KindSingleLineCommentTrivia, Text: " trailing", Loc: core.NewTextRange(-1, -1), HasTrailingNewLine: true}, + }, + expected: decorateHelper + "\nlet Foo = class Foo {\n}; // trailing\nFoo = __decorate([\n decorate()\n], Foo);", + }, + { + name: "leading and trailing comments", + input: "@decorate()\nclass Foo {}", + leading: []printer.SynthesizedComment{ + {Kind: ast.KindMultiLineCommentTrivia, Text: "leading", Loc: core.NewTextRange(-1, -1)}, + }, + trailing: []printer.SynthesizedComment{ + {Kind: ast.KindSingleLineCommentTrivia, Text: " trailing", Loc: core.NewTextRange(-1, -1), HasTrailingNewLine: true}, + }, + expected: decorateHelper + "\n/*leading*/ let Foo = class Foo {\n}; // trailing\nFoo = __decorate([\n decorate()\n], Foo);", + }, + { + name: "exported class comments", + input: "@decorate()\nexport class Foo {}", + leading: []printer.SynthesizedComment{ + {Kind: ast.KindMultiLineCommentTrivia, Text: "exported", Loc: core.NewTextRange(-1, -1)}, + }, + expected: decorateHelper + "\n/*exported*/ let Foo = class Foo {\n};\nFoo = __decorate([\n decorate()\n], Foo);\nexport { Foo };", + }, + { + name: "class with static property in script", + input: "@decorate()\nclass Foo { static instance = new Foo(); }", + leading: []printer.SynthesizedComment{ + {Kind: ast.KindMultiLineCommentTrivia, Text: "static-prop", Loc: core.NewTextRange(-1, -1)}, + }, + expected: decorateHelper + "\n/*static-prop*/ let Foo = class Foo {\n static instance = new Foo();\n};\nFoo = __decorate([\n decorate()\n], Foo);", + }, + { + name: "class with self-reference alias in module", + input: "@decorate()\nexport class Foo { static instance = new Foo(); }", + leading: []printer.SynthesizedComment{ + {Kind: ast.KindMultiLineCommentTrivia, Text: "aliased", Loc: core.NewTextRange(-1, -1)}, + }, + expected: decorateHelper + "\nvar Foo_1;\n/*aliased*/ let Foo = class Foo {\n static { Foo_1 = this; }\n static instance = new Foo_1();\n};\nFoo = Foo_1 = __decorate([\n decorate()\n], Foo);\nexport { Foo };", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + file := parsetestutil.ParseTypeScript(tc.input, false) + parsetestutil.CheckDiagnostics(t, file) + binder.BindSourceFile(file) + + emitContext := printer.NewEmitContext() + classDecl := file.Statements.Nodes[0] + if tc.leading != nil { + emitContext.SetSyntheticLeadingComments(classDecl, tc.leading) + } + if tc.trailing != nil { + emitContext.SetSyntheticTrailingComments(classDecl, tc.trailing) + } + + compilerOptions := &core.CompilerOptions{ + ExperimentalDecorators: core.TSTrue, + Target: core.ScriptTargetESNext, + } + opts := &transformers.TransformOptions{ + CompilerOptions: compilerOptions, + Context: emitContext, + Resolver: binder.NewReferenceResolver(compilerOptions, binder.ReferenceResolverHooks{}), + } + + transformed := tstransforms.NewLegacyDecoratorsTransformer(opts).TransformSourceFile(file) + emittestutil.CheckEmit(t, emitContext, transformed, tc.expected) + }) + } +}