From 45314e4558c243270a973db95cc1416491229522 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Jun 2025 00:26:40 +0000 Subject: [PATCH 1/6] Initial plan for issue From 3e616fbc16991695bc0aface159ce411d59223f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:34:25 +0000 Subject: [PATCH 2/6] Initial plan for TypeScript PR #60303 template string escaping fix Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- internal/printer/template_escaping_test.go | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 internal/printer/template_escaping_test.go diff --git a/internal/printer/template_escaping_test.go b/internal/printer/template_escaping_test.go new file mode 100644 index 0000000000..f7bf9b0617 --- /dev/null +++ b/internal/printer/template_escaping_test.go @@ -0,0 +1,78 @@ +package printer_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "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" +) + +func TestTemplateStringEscaping(t *testing.T) { + t.Parallel() + + t.Run("ParsedTemplateLiteral", func(t *testing.T) { + // Test case that parses template literal from source + file := parsetestutil.ParseTypeScript("`\n`", false) + emittestutil.CheckEmit(t, nil, file, "`\n`;") + }) + + t.Run("SyntheticTemplateLiteral", func(t *testing.T) { + // This is the test case from TypeScript PR #60303 + // It should NOT escape the newline character in template literals + var factory ast.NodeFactory + + // Create a template literal with just a newline character + templateLiteral := factory.NewNoSubstitutionTemplateLiteral("\n") + + // Create a synthetic source file containing the template literal + file := factory.NewSourceFile("/test.ts", "/test.ts", "/test.ts", factory.NewNodeList([]*ast.Node{ + factory.NewExpressionStatement(templateLiteral), + })) + ast.SetParentInChildren(file) + parsetestutil.MarkSyntheticRecursive(file) + + // The expected result should NOT escape the newline + expected := "`\n`;" + emittestutil.CheckEmit(t, nil, file.AsSourceFile(), expected) + }) + + t.Run("SyntheticTemplateLiteralExplicitTest", func(t *testing.T) { + // More explicit test to see what's really happening + var factory ast.NodeFactory + + templateLiteral := factory.NewNoSubstitutionTemplateLiteral("\n") + + file := factory.NewSourceFile("/test.ts", "/test.ts", "/test.ts", factory.NewNodeList([]*ast.Node{ + factory.NewExpressionStatement(templateLiteral), + })) + ast.SetParentInChildren(file) + parsetestutil.MarkSyntheticRecursive(file) + + // Print the template literal directly using the same setup as emittestutil + p := printer.NewPrinter( + printer.PrinterOptions{ + NewLine: core.NewLineKindLF, + }, + printer.PrintHandlers{}, + nil, // emitContext + ) + result := p.EmitSourceFile(file.AsSourceFile()) + + t.Logf("Full file result: %q", result) + + // Check if the newline is properly preserved (not escaped) + expected1 := "`\n`;\n" // What we should get + expected2 := "`\\n`;\n" // What would indicate the bug + + if result == expected1 { + t.Log("✓ Newline is correctly preserved (not escaped)") + } else if result == expected2 { + t.Error("✗ Bug confirmed: newline is incorrectly escaped") + } else { + t.Errorf("✗ Unexpected result: %q", result) + } + }) +} \ No newline at end of file From 1d3732bed856b5f3f0678d8beea154db66362365 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:43:53 +0000 Subject: [PATCH 3/6] Fix template string escaping per TypeScript PR #60303 Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- internal/printer/printer_test.go | 1 + internal/printer/template_escaping_test.go | 78 -------------------- internal/printer/template_string_fix_test.go | 40 ++++++++++ internal/printer/utilities.go | 14 +++- 4 files changed, 53 insertions(+), 80 deletions(-) delete mode 100644 internal/printer/template_escaping_test.go create mode 100644 internal/printer/template_string_fix_test.go diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index 1bd17cc6c6..31f14a6145 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -28,6 +28,7 @@ func TestEmit(t *testing.T) { {title: "BooleanLiteral#1", input: `true`, output: `true;`}, {title: "BooleanLiteral#2", input: `false`, output: `false;`}, {title: "NoSubstitutionTemplateLiteral", input: "``", output: "``;"}, + {title: "NoSubstitutionTemplateLiteral#2", input: "`\n`", output: "`\n`;"}, {title: "RegularExpressionLiteral#1", input: `/a/`, output: `/a/;`}, {title: "RegularExpressionLiteral#2", input: `/a/g`, output: `/a/g;`}, {title: "NullLiteral", input: `null`, output: `null;`}, diff --git a/internal/printer/template_escaping_test.go b/internal/printer/template_escaping_test.go deleted file mode 100644 index f7bf9b0617..0000000000 --- a/internal/printer/template_escaping_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package printer_test - -import ( - "testing" - - "github.com/microsoft/typescript-go/internal/ast" - "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" -) - -func TestTemplateStringEscaping(t *testing.T) { - t.Parallel() - - t.Run("ParsedTemplateLiteral", func(t *testing.T) { - // Test case that parses template literal from source - file := parsetestutil.ParseTypeScript("`\n`", false) - emittestutil.CheckEmit(t, nil, file, "`\n`;") - }) - - t.Run("SyntheticTemplateLiteral", func(t *testing.T) { - // This is the test case from TypeScript PR #60303 - // It should NOT escape the newline character in template literals - var factory ast.NodeFactory - - // Create a template literal with just a newline character - templateLiteral := factory.NewNoSubstitutionTemplateLiteral("\n") - - // Create a synthetic source file containing the template literal - file := factory.NewSourceFile("/test.ts", "/test.ts", "/test.ts", factory.NewNodeList([]*ast.Node{ - factory.NewExpressionStatement(templateLiteral), - })) - ast.SetParentInChildren(file) - parsetestutil.MarkSyntheticRecursive(file) - - // The expected result should NOT escape the newline - expected := "`\n`;" - emittestutil.CheckEmit(t, nil, file.AsSourceFile(), expected) - }) - - t.Run("SyntheticTemplateLiteralExplicitTest", func(t *testing.T) { - // More explicit test to see what's really happening - var factory ast.NodeFactory - - templateLiteral := factory.NewNoSubstitutionTemplateLiteral("\n") - - file := factory.NewSourceFile("/test.ts", "/test.ts", "/test.ts", factory.NewNodeList([]*ast.Node{ - factory.NewExpressionStatement(templateLiteral), - })) - ast.SetParentInChildren(file) - parsetestutil.MarkSyntheticRecursive(file) - - // Print the template literal directly using the same setup as emittestutil - p := printer.NewPrinter( - printer.PrinterOptions{ - NewLine: core.NewLineKindLF, - }, - printer.PrintHandlers{}, - nil, // emitContext - ) - result := p.EmitSourceFile(file.AsSourceFile()) - - t.Logf("Full file result: %q", result) - - // Check if the newline is properly preserved (not escaped) - expected1 := "`\n`;\n" // What we should get - expected2 := "`\\n`;\n" // What would indicate the bug - - if result == expected1 { - t.Log("✓ Newline is correctly preserved (not escaped)") - } else if result == expected2 { - t.Error("✗ Bug confirmed: newline is incorrectly escaped") - } else { - t.Errorf("✗ Unexpected result: %q", result) - } - }) -} \ No newline at end of file diff --git a/internal/printer/template_string_fix_test.go b/internal/printer/template_string_fix_test.go new file mode 100644 index 0000000000..c7c25b9e26 --- /dev/null +++ b/internal/printer/template_string_fix_test.go @@ -0,0 +1,40 @@ +package printer_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/testutil/emittestutil" + "github.com/microsoft/typescript-go/internal/testutil/parsetestutil" +) + +// TestTemplateStringEscapingFix tests the fix for TypeScript issue #59150 +// This test mirrors the test case added in TypeScript PR #60303 +func TestTemplateStringEscapingFix(t *testing.T) { + t.Parallel() + + // https://github.com/microsoft/TypeScript/issues/59150 + // Replicates: printsCorrectly("template string", {}, printer => + // printer.printNode( + // ts.EmitHint.Unspecified, + // ts.factory.createNoSubstitutionTemplateLiteral("\n"), + // ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext), + // )); + + var factory ast.NodeFactory + + // Create a synthetic NoSubstitutionTemplateLiteral with just a newline character + templateLiteral := factory.NewNoSubstitutionTemplateLiteral("\n") + + // Create a synthetic source file + file := factory.NewSourceFile("/source.ts", "/source.ts", "/source.ts", factory.NewNodeList([]*ast.Node{ + factory.NewExpressionStatement(templateLiteral), + })) + ast.SetParentInChildren(file) + parsetestutil.MarkSyntheticRecursive(file) + + // The fix ensures that LF newlines are NOT escaped in template literals + // Expected: `\n` (with literal newline character, not escaped) + // Bug would produce: `\\n` (with escaped newline) + emittestutil.CheckEmit(t, nil, file.AsSourceFile(), "`\n`;") +} \ No newline at end of file diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 6561a5a95a..4b26558d3b 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -103,8 +103,18 @@ func escapeStringWorker(s string, quoteChar QuoteChar, flags getLiteralTextFlags escape = true } default: - if ch < '\u001f' || flags&getLiteralTextFlagsNeverAsciiEscape == 0 && ch > '\u007f' { - escape = true + // For template strings, exclude \u000a (LF/newline) from the character range that gets escaped + // This matches the TypeScript fix in PR #60303 that changed the regex from \u0000-\u001f to \u0000-\u0009\u000b-\u001f + if quoteChar == QuoteCharBacktick { + // For template strings: escape \u0000-\u0009 and \u000b-\u001f (excluding \u000a which is LF) + if (ch >= '\u0000' && ch <= '\u0009') || (ch >= '\u000b' && ch <= '\u001f') || flags&getLiteralTextFlagsNeverAsciiEscape == 0 && ch > '\u007f' { + escape = true + } + } else { + // For regular strings: escape \u0000-\u001f as before + if ch < '\u001f' || flags&getLiteralTextFlagsNeverAsciiEscape == 0 && ch > '\u007f' { + escape = true + } } } From 942edc6d20685e5906c39177f8b4e07d84a09132 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:23:59 +0000 Subject: [PATCH 4/6] Fix template string escaping: change ch < '\u001f' to ch <= '\u001f' - Addresses TypeScript PR #60303 by ensuring \u001f is properly escaped - \n was already handled correctly by existing case '\n': logic - Removed separate test file and added comprehensive tests to utilities_test.go - Fixed the minimal issue: \u001f (Unit Separator) was not being escaped due to < vs <= condition Addresses feedback from code review. Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- internal/printer/template_string_fix_test.go | 40 -------------------- internal/printer/utilities.go | 14 +------ internal/printer/utilities_test.go | 33 ++++++++++++++++ 3 files changed, 35 insertions(+), 52 deletions(-) delete mode 100644 internal/printer/template_string_fix_test.go diff --git a/internal/printer/template_string_fix_test.go b/internal/printer/template_string_fix_test.go deleted file mode 100644 index c7c25b9e26..0000000000 --- a/internal/printer/template_string_fix_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package printer_test - -import ( - "testing" - - "github.com/microsoft/typescript-go/internal/ast" - "github.com/microsoft/typescript-go/internal/testutil/emittestutil" - "github.com/microsoft/typescript-go/internal/testutil/parsetestutil" -) - -// TestTemplateStringEscapingFix tests the fix for TypeScript issue #59150 -// This test mirrors the test case added in TypeScript PR #60303 -func TestTemplateStringEscapingFix(t *testing.T) { - t.Parallel() - - // https://github.com/microsoft/TypeScript/issues/59150 - // Replicates: printsCorrectly("template string", {}, printer => - // printer.printNode( - // ts.EmitHint.Unspecified, - // ts.factory.createNoSubstitutionTemplateLiteral("\n"), - // ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext), - // )); - - var factory ast.NodeFactory - - // Create a synthetic NoSubstitutionTemplateLiteral with just a newline character - templateLiteral := factory.NewNoSubstitutionTemplateLiteral("\n") - - // Create a synthetic source file - file := factory.NewSourceFile("/source.ts", "/source.ts", "/source.ts", factory.NewNodeList([]*ast.Node{ - factory.NewExpressionStatement(templateLiteral), - })) - ast.SetParentInChildren(file) - parsetestutil.MarkSyntheticRecursive(file) - - // The fix ensures that LF newlines are NOT escaped in template literals - // Expected: `\n` (with literal newline character, not escaped) - // Bug would produce: `\\n` (with escaped newline) - emittestutil.CheckEmit(t, nil, file.AsSourceFile(), "`\n`;") -} \ No newline at end of file diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 4b26558d3b..9eac7f1b89 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -103,18 +103,8 @@ func escapeStringWorker(s string, quoteChar QuoteChar, flags getLiteralTextFlags escape = true } default: - // For template strings, exclude \u000a (LF/newline) from the character range that gets escaped - // This matches the TypeScript fix in PR #60303 that changed the regex from \u0000-\u001f to \u0000-\u0009\u000b-\u001f - if quoteChar == QuoteCharBacktick { - // For template strings: escape \u0000-\u0009 and \u000b-\u001f (excluding \u000a which is LF) - if (ch >= '\u0000' && ch <= '\u0009') || (ch >= '\u000b' && ch <= '\u001f') || flags&getLiteralTextFlagsNeverAsciiEscape == 0 && ch > '\u007f' { - escape = true - } - } else { - // For regular strings: escape \u0000-\u001f as before - if ch < '\u001f' || flags&getLiteralTextFlagsNeverAsciiEscape == 0 && ch > '\u007f' { - escape = true - } + if ch <= '\u001f' || flags&getLiteralTextFlagsNeverAsciiEscape == 0 && ch > '\u007f' { + escape = true } } diff --git a/internal/printer/utilities_test.go b/internal/printer/utilities_test.go index cbe034f22d..70a9593d28 100644 --- a/internal/printer/utilities_test.go +++ b/internal/printer/utilities_test.go @@ -90,6 +90,39 @@ func TestEscapeJsxAttributeString(t *testing.T) { } } +func TestEscapeStringTemplateStrings(t *testing.T) { + t.Parallel() + data := []struct { + s string + quoteChar QuoteChar + expected string + desc string + }{ + // Test newline character (\u000a) - should NOT be escaped in template strings + {s: "\n", quoteChar: QuoteCharBacktick, expected: "\n", desc: "newline in template string"}, + {s: "\n", quoteChar: QuoteCharDoubleQuote, expected: "\\n", desc: "newline in double quote string"}, + {s: "\n", quoteChar: QuoteCharSingleQuote, expected: "\\n", desc: "newline in single quote string"}, + + // Test \u001f (Unit Separator) - should be escaped in template strings according to TypeScript PR #60303 + {s: "\u001f", quoteChar: QuoteCharBacktick, expected: "\\u001F", desc: "\\u001f in template string"}, + {s: "\u001f", quoteChar: QuoteCharDoubleQuote, expected: "\\u001F", desc: "\\u001f in double quote string"}, + {s: "\u001f", quoteChar: QuoteCharSingleQuote, expected: "\\u001F", desc: "\\u001f in single quote string"}, + + // Test other control characters that should be escaped + {s: "\u0000", quoteChar: QuoteCharBacktick, expected: "\\0", desc: "\\u0000 in template string"}, + {s: "\u0009", quoteChar: QuoteCharBacktick, expected: "\\t", desc: "\\u0009 (tab) in template string"}, + {s: "\u000b", quoteChar: QuoteCharBacktick, expected: "\\v", desc: "\\u000b (vtab) in template string"}, + {s: "\u001e", quoteChar: QuoteCharBacktick, expected: "\\u001E", desc: "\\u001e in template string"}, + } + for i, rec := range data { + t.Run(fmt.Sprintf("[%d] %s", i, rec.desc), func(t *testing.T) { + t.Parallel() + actual := EscapeString(rec.s, rec.quoteChar) + assert.Equal(t, actual, rec.expected) + }) + } +} + func TestIsRecognizedTripleSlashComment(t *testing.T) { t.Parallel() data := []struct { From 9f87b5f74abf70fa3b226bb42e992601203ee23e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:42:08 +0000 Subject: [PATCH 5/6] Remove TestEscapeStringTemplateStrings test as it duplicates existing coverage Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/printer/utilities_test.go | 33 ------------------------------ 1 file changed, 33 deletions(-) diff --git a/internal/printer/utilities_test.go b/internal/printer/utilities_test.go index 70a9593d28..cbe034f22d 100644 --- a/internal/printer/utilities_test.go +++ b/internal/printer/utilities_test.go @@ -90,39 +90,6 @@ func TestEscapeJsxAttributeString(t *testing.T) { } } -func TestEscapeStringTemplateStrings(t *testing.T) { - t.Parallel() - data := []struct { - s string - quoteChar QuoteChar - expected string - desc string - }{ - // Test newline character (\u000a) - should NOT be escaped in template strings - {s: "\n", quoteChar: QuoteCharBacktick, expected: "\n", desc: "newline in template string"}, - {s: "\n", quoteChar: QuoteCharDoubleQuote, expected: "\\n", desc: "newline in double quote string"}, - {s: "\n", quoteChar: QuoteCharSingleQuote, expected: "\\n", desc: "newline in single quote string"}, - - // Test \u001f (Unit Separator) - should be escaped in template strings according to TypeScript PR #60303 - {s: "\u001f", quoteChar: QuoteCharBacktick, expected: "\\u001F", desc: "\\u001f in template string"}, - {s: "\u001f", quoteChar: QuoteCharDoubleQuote, expected: "\\u001F", desc: "\\u001f in double quote string"}, - {s: "\u001f", quoteChar: QuoteCharSingleQuote, expected: "\\u001F", desc: "\\u001f in single quote string"}, - - // Test other control characters that should be escaped - {s: "\u0000", quoteChar: QuoteCharBacktick, expected: "\\0", desc: "\\u0000 in template string"}, - {s: "\u0009", quoteChar: QuoteCharBacktick, expected: "\\t", desc: "\\u0009 (tab) in template string"}, - {s: "\u000b", quoteChar: QuoteCharBacktick, expected: "\\v", desc: "\\u000b (vtab) in template string"}, - {s: "\u001e", quoteChar: QuoteCharBacktick, expected: "\\u001E", desc: "\\u001e in template string"}, - } - for i, rec := range data { - t.Run(fmt.Sprintf("[%d] %s", i, rec.desc), func(t *testing.T) { - t.Parallel() - actual := EscapeString(rec.s, rec.quoteChar) - assert.Equal(t, actual, rec.expected) - }) - } -} - func TestIsRecognizedTripleSlashComment(t *testing.T) { t.Parallel() data := []struct { From fadf2219ef2e40d809c2d90f8e497f35301f174b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Jun 2025 17:01:47 +0000 Subject: [PATCH 6/6] Add test for template string escaping fix and force escaping for control characters Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/printer/printer_test.go | 1 + internal/printer/utilities.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index 31f14a6145..2e2c2f4600 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -29,6 +29,7 @@ func TestEmit(t *testing.T) { {title: "BooleanLiteral#2", input: `false`, output: `false;`}, {title: "NoSubstitutionTemplateLiteral", input: "``", output: "``;"}, {title: "NoSubstitutionTemplateLiteral#2", input: "`\n`", output: "`\n`;"}, + {title: "NoSubstitutionTemplateLiteral#3", input: "`\u001f`", output: "`\\u001F`;"}, {title: "RegularExpressionLiteral#1", input: `/a/`, output: `/a/;`}, {title: "RegularExpressionLiteral#2", input: `/a/g`, output: `/a/g;`}, {title: "NullLiteral", input: `null`, output: `null;`}, diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 9eac7f1b89..2e8838c1f6 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -205,6 +205,21 @@ func canUseOriginalText(node *ast.LiteralLikeNode, flags getLiteralTextFlags) bo } } + // For template literals, check if they contain characters that need escaping + if node.Kind == ast.KindNoSubstitutionTemplateLiteral || + node.Kind == ast.KindTemplateHead || + node.Kind == ast.KindTemplateMiddle || + node.Kind == ast.KindTemplateTail { + text := node.TemplateLiteralLikeData().Text + for _, ch := range text { + // Check if this character needs escaping according to the TypeScript PR #60303 fix + // Characters in range \u0000-\u001f (excluding \u000a which is handled separately) should be escaped + if ch <= '\u001f' && ch != '\n' { + return false // Force escaping path + } + } + } + // Finally, we do not use the original text of a BigInt literal // TODO(rbuckton): The reason as to why we do not use the original text for bigints is not mentioned in the // original compiler source. It could be that this is no longer necessary, in which case bigint literals should