Skip to content
Open
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
8 changes: 8 additions & 0 deletions tsc/internal/printer/emitcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Comment thread
trevorade marked this conversation as resolved.
}
}

func (c *EmitContext) EmitFlags(node *ast.Node) EmitFlags {
Expand Down
35 changes: 35 additions & 0 deletions tsc/internal/printer/emitcontext_test.go
Original file line number Diff line number Diff line change
@@ -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"})
}
9 changes: 7 additions & 2 deletions tsc/internal/testutil/emittestutil/emittestutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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")
}
3 changes: 3 additions & 0 deletions tsc/internal/transformers/estransforms/classfields.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
35 changes: 35 additions & 0 deletions tsc/internal/transformers/estransforms/classfields_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
7 changes: 7 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
// 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.
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion tsc/internal/transformers/tstransforms/runtimesyntax.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
64 changes: 64 additions & 0 deletions tsc/internal/transformers/tstransforms/syntheticcomments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package tstransforms_test

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this file a little weird. We typically don't do unit tests for these, and this duplicates a helper string, and tests multiple packages ad-hoc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair points. I looked at doing this as a compiler baseline test instead, but I don't think one can see it: synthetic comments only reach these transforms when an API user adds them (that's how we hit it, via a custom transformer). In a normal emit, the only synthetic comments in JS output come from the const enum inliner, which runs after these transforms, so a baseline passes with or without the change.

I trimmed the test down instead in 2c4d14a. It now just checks that each comment is emitted once rather than matching the whole output, so the duplicated __decorate helper is gone. It's also split by package: the SetOriginal copy is tested next to copyFrom in printer, the enum/namespace/decorator cases stay in tstransforms, and the static field case moved to estransforms.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we have to do something annoying here because this is all Go API related. Just not mega satisfied with these ad hoc tests. Perhaps it's fine.


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)
})
}
}