Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions tsc/internal/printer/emitcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
66 changes: 66 additions & 0 deletions tsc/internal/printer/emitcontext_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
9 changes: 9 additions & 0 deletions tsc/internal/transformers/tstransforms/legacydecorators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions tsc/internal/transformers/tstransforms/legacydecorators_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}