From d0a5223252fbe6a33fed1a0276e1ed256d6fa407 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Jun 2025 20:39:38 +0000
Subject: [PATCH 1/5] Initial plan
From d857f4a749d78320e286d18d8bf60a92ec1ba9a7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Jun 2025 20:53:25 +0000
Subject: [PATCH 2/5] Fix JSX comment duplication in React mode, investigate
preserve mode issue
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
---
src/compiler/transformers/jsx.ts | 47 ++++++++++++-------
.../cases/compiler/jsxCommentDuplication.tsx | 5 ++
2 files changed, 36 insertions(+), 16 deletions(-)
create mode 100644 tests/cases/compiler/jsxCommentDuplication.tsx
diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts
index 885f4c5d67ad6..4c25359e68fc8 100644
--- a/src/compiler/transformers/jsx.ts
+++ b/src/compiler/transformers/jsx.ts
@@ -554,22 +554,37 @@ export function transformJsx(context: TransformationContext): (x: SourceFile | B
return fixed === undefined ? undefined : factory.createStringLiteral(fixed);
}
- /**
- * JSX trims whitespace at the end and beginning of lines, except that the
- * start/end of a tag is considered a start/end of a line only if that line is
- * on the same line as the closing tag. See examples in
- * tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
- * See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
- *
- * An equivalent algorithm would be:
- * - If there is only one line, return it.
- * - If there is only whitespace (but multiple lines), return `undefined`.
- * - Split the text into lines.
- * - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
- * - Decode entities on each line (individually).
- * - Remove empty lines and join the rest with " ".
- */
- function fixupWhitespaceAndDecodeEntities(text: string): string | undefined {
+ /**
+ * Remove comments from JSX text content.
+ * Comments like slash-star comment star-slash should not be treated as text content in JSX.
+ */
+ function stripCommentsFromJsxText(text: string): string {
+ // Only strip comments when not in preserve mode
+ if (compilerOptions.jsx === JsxEmit.Preserve) {
+ return text;
+ }
+ // Remove /* ... */ style comments from JSX text
+ return text.replace(/\/\*[\s\S]*?\*\//g, '');
+ }
+
+ /**
+ * JSX trims whitespace at the end and beginning of lines, except that the
+ * start/end of a tag is considered a start/end of a line only if that line is
+ * on the same line as the closing tag. See examples in
+ * tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
+ * See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
+ *
+ * An equivalent algorithm would be:
+ * - If there is only one line, return it.
+ * - If there is only whitespace (but multiple lines), return `undefined`.
+ * - Split the text into lines.
+ * - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
+ * - Decode entities on each line (individually).
+ * - Remove empty lines and join the rest with " ".
+ */
+ function fixupWhitespaceAndDecodeEntities(text: string): string | undefined {
+ // First, strip comments from the text
+ text = stripCommentsFromJsxText(text);
let acc: string | undefined;
// First non-whitespace character on this line.
let firstNonWhitespace = 0;
diff --git a/tests/cases/compiler/jsxCommentDuplication.tsx b/tests/cases/compiler/jsxCommentDuplication.tsx
new file mode 100644
index 0000000000000..217e4e29ebd0e
--- /dev/null
+++ b/tests/cases/compiler/jsxCommentDuplication.tsx
@@ -0,0 +1,5 @@
+// @jsx: react,preserve
+// @module: commonjs
+
+function App() {}
+const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;
\ No newline at end of file
From 39190fd0c2660fbebb976593204033859cb6acf3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Jun 2025 21:01:40 +0000
Subject: [PATCH 3/5] Complete fix for JSX comment duplication in both React
and preserve modes
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
---
debug-ast.mjs | 21 +++++++++++++++++++++
src/compiler/emitter.ts | 7 +++++--
2 files changed, 26 insertions(+), 2 deletions(-)
create mode 100644 debug-ast.mjs
diff --git a/debug-ast.mjs b/debug-ast.mjs
new file mode 100644
index 0000000000000..7c170495cdc9d
--- /dev/null
+++ b/debug-ast.mjs
@@ -0,0 +1,21 @@
+import * as ts from "./built/local/typescript.js";
+
+const sourceCode = `function App() {}
+const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;`;
+
+const sourceFile = ts.createSourceFile("test.tsx", sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
+
+function printAST(node, depth = 0) {
+ const indent = " ".repeat(depth);
+ console.log(`${indent}${ts.SyntaxKind[node.kind]}: ${node.text || ""}`);
+
+ if (node.kind === ts.SyntaxKind.JsxText) {
+ console.log(`${indent} JSX Text content: "${node.text}"`);
+ console.log(`${indent} Contains only whitespace: ${node.containsOnlyTriviaWhiteSpaces}`);
+ }
+
+ ts.forEachChild(node, child => printAST(child, depth + 1));
+}
+
+console.log("=== AST Structure ===");
+printAST(sourceFile);
\ No newline at end of file
diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts
index c60fd92787275..bd64a83022d2c 100644
--- a/src/compiler/emitter.ts
+++ b/src/compiler/emitter.ts
@@ -3896,8 +3896,11 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
writePunctuation(">");
}
- function emitJsxText(node: JsxText) {
- writer.writeLiteral(node.text);
+ function emitJsxText(node: JsxText) {
+ // Strip comments from JSX text content to prevent duplication
+ // Comments like /* comment */ should not be treated as text content in JSX
+ const textWithoutComments = node.text.replace(/\/\*[\s\S]*?\*\//g, '');
+ writer.writeLiteral(textWithoutComments);
}
function emitJsxClosingElementOrFragment(node: JsxClosingElement | JsxClosingFragment) {
From 24295d3793cb34ae2aeb5d9ca829c4acca6b090a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Jun 2025 21:02:50 +0000
Subject: [PATCH 4/5] Add test baselines for JSX comment duplication fix
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
---
debug-ast.mjs | 21 -------------------
.../jsxCommentDuplication(jsx=preserve).js | 9 ++++++++
...sxCommentDuplication(jsx=preserve).symbols | 11 ++++++++++
.../jsxCommentDuplication(jsx=preserve).types | 17 +++++++++++++++
...sxCommentDuplication(jsx=react).errors.txt | 8 +++++++
.../jsxCommentDuplication(jsx=react).js | 12 +++++++++++
.../jsxCommentDuplication(jsx=react).symbols | 11 ++++++++++
.../jsxCommentDuplication(jsx=react).types | 19 +++++++++++++++++
8 files changed, 87 insertions(+), 21 deletions(-)
delete mode 100644 debug-ast.mjs
create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js
create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=preserve).symbols
create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=preserve).types
create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=react).errors.txt
create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=react).js
create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=react).symbols
create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=react).types
diff --git a/debug-ast.mjs b/debug-ast.mjs
deleted file mode 100644
index 7c170495cdc9d..0000000000000
--- a/debug-ast.mjs
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as ts from "./built/local/typescript.js";
-
-const sourceCode = `function App() {}
-const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;`;
-
-const sourceFile = ts.createSourceFile("test.tsx", sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
-
-function printAST(node, depth = 0) {
- const indent = " ".repeat(depth);
- console.log(`${indent}${ts.SyntaxKind[node.kind]}: ${node.text || ""}`);
-
- if (node.kind === ts.SyntaxKind.JsxText) {
- console.log(`${indent} JSX Text content: "${node.text}"`);
- console.log(`${indent} Contains only whitespace: ${node.containsOnlyTriviaWhiteSpaces}`);
- }
-
- ts.forEachChild(node, child => printAST(child, depth + 1));
-}
-
-console.log("=== AST Structure ===");
-printAST(sourceFile);
\ No newline at end of file
diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js
new file mode 100644
index 0000000000000..e28a8263a5c52
--- /dev/null
+++ b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js
@@ -0,0 +1,9 @@
+//// [tests/cases/compiler/jsxCommentDuplication.tsx] ////
+
+//// [jsxCommentDuplication.tsx]
+function App() {}
+const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;
+
+//// [jsxCommentDuplication.jsx]
+function App() { }
+var jsx = /* no */{/* 1 */123 /* 2 */} /* no */;
diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).symbols b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).symbols
new file mode 100644
index 0000000000000..328bd5542845d
--- /dev/null
+++ b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).symbols
@@ -0,0 +1,11 @@
+//// [tests/cases/compiler/jsxCommentDuplication.tsx] ////
+
+=== jsxCommentDuplication.tsx ===
+function App() {}
+>App : Symbol(App, Decl(jsxCommentDuplication.tsx, 0, 0))
+
+const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;
+>jsx : Symbol(jsx, Decl(jsxCommentDuplication.tsx, 1, 5))
+>App : Symbol(App, Decl(jsxCommentDuplication.tsx, 0, 0))
+>App : Symbol(App, Decl(jsxCommentDuplication.tsx, 0, 0))
+
diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).types b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).types
new file mode 100644
index 0000000000000..5a4fbfe5078c4
--- /dev/null
+++ b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).types
@@ -0,0 +1,17 @@
+//// [tests/cases/compiler/jsxCommentDuplication.tsx] ////
+
+=== jsxCommentDuplication.tsx ===
+function App() {}
+>App : () => void
+> : ^^^^^^^^^^
+
+const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;
+>jsx : error
+>/* no */{/* 1 */ 123 /* 2 */}/* no */ : error
+>App : () => void
+> : ^^^^^^^^^^
+>123 : 123
+> : ^^^
+>App : () => void
+> : ^^^^^^^^^^
+
diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=react).errors.txt b/tests/baselines/reference/jsxCommentDuplication(jsx=react).errors.txt
new file mode 100644
index 0000000000000..5902bb8f12f9e
--- /dev/null
+++ b/tests/baselines/reference/jsxCommentDuplication(jsx=react).errors.txt
@@ -0,0 +1,8 @@
+jsxCommentDuplication.tsx(2,14): error TS2874: This JSX tag requires 'React' to be in scope, but it could not be found.
+
+
+==== jsxCommentDuplication.tsx (1 errors) ====
+ function App() {}
+ const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;
+ ~~~
+!!! error TS2874: This JSX tag requires 'React' to be in scope, but it could not be found.
\ No newline at end of file
diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=react).js b/tests/baselines/reference/jsxCommentDuplication(jsx=react).js
new file mode 100644
index 0000000000000..c2706df39d2cf
--- /dev/null
+++ b/tests/baselines/reference/jsxCommentDuplication(jsx=react).js
@@ -0,0 +1,12 @@
+//// [tests/cases/compiler/jsxCommentDuplication.tsx] ////
+
+//// [jsxCommentDuplication.tsx]
+function App() {}
+const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;
+
+//// [jsxCommentDuplication.js]
+function App() { }
+var jsx = React.createElement(App, null,
+ "", /* 1 */
+ 123 /* 2 */,
+ "");
diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=react).symbols b/tests/baselines/reference/jsxCommentDuplication(jsx=react).symbols
new file mode 100644
index 0000000000000..328bd5542845d
--- /dev/null
+++ b/tests/baselines/reference/jsxCommentDuplication(jsx=react).symbols
@@ -0,0 +1,11 @@
+//// [tests/cases/compiler/jsxCommentDuplication.tsx] ////
+
+=== jsxCommentDuplication.tsx ===
+function App() {}
+>App : Symbol(App, Decl(jsxCommentDuplication.tsx, 0, 0))
+
+const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;
+>jsx : Symbol(jsx, Decl(jsxCommentDuplication.tsx, 1, 5))
+>App : Symbol(App, Decl(jsxCommentDuplication.tsx, 0, 0))
+>App : Symbol(App, Decl(jsxCommentDuplication.tsx, 0, 0))
+
diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=react).types b/tests/baselines/reference/jsxCommentDuplication(jsx=react).types
new file mode 100644
index 0000000000000..d0333134fdd1c
--- /dev/null
+++ b/tests/baselines/reference/jsxCommentDuplication(jsx=react).types
@@ -0,0 +1,19 @@
+//// [tests/cases/compiler/jsxCommentDuplication.tsx] ////
+
+=== jsxCommentDuplication.tsx ===
+function App() {}
+>App : () => void
+> : ^^^^^^^^^^
+
+const jsx = /* no */{/* 1 */ 123 /* 2 */}/* no */;
+>jsx : any
+> : ^^^
+>/* no */{/* 1 */ 123 /* 2 */}/* no */ : any
+> : ^^^
+>App : () => void
+> : ^^^^^^^^^^
+>123 : 123
+> : ^^^
+>App : () => void
+> : ^^^^^^^^^^
+
From d6d87853eb996cc3d31e634950b9191dd7c0b1d9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Jun 2025 21:43:24 +0000
Subject: [PATCH 5/5] Fix JSX comment duplication by properly handling comments
in JSX text scanning
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
---
src/compiler/emitter.ts | 7 ++---
src/compiler/scanner.ts | 25 +++++++++++++++--
src/compiler/transformers/jsx.ts | 47 +++++++++++---------------------
3 files changed, 41 insertions(+), 38 deletions(-)
diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts
index bd64a83022d2c..c60fd92787275 100644
--- a/src/compiler/emitter.ts
+++ b/src/compiler/emitter.ts
@@ -3896,11 +3896,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
writePunctuation(">");
}
- function emitJsxText(node: JsxText) {
- // Strip comments from JSX text content to prevent duplication
- // Comments like /* comment */ should not be treated as text content in JSX
- const textWithoutComments = node.text.replace(/\/\*[\s\S]*?\*\//g, '');
- writer.writeLiteral(textWithoutComments);
+ function emitJsxText(node: JsxText) {
+ writer.writeLiteral(node.text);
}
function emitJsxClosingElementOrFragment(node: JsxClosingElement | JsxClosingFragment) {
diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts
index 343df39ad9cbc..5b283b7929b21 100644
--- a/src/compiler/scanner.ts
+++ b/src/compiler/scanner.ts
@@ -3720,6 +3720,9 @@ export function createScanner(
// These initial values are special because the first line is:
// firstNonWhitespace = 0 to indicate that we want leading whitespace,
+ // Build the text content excluding comments
+ let textContent = "";
+
while (pos < end) {
char = charCodeUnchecked(pos);
if (char === CharacterCodes.openBrace) {
@@ -3739,6 +3742,21 @@ export function createScanner(
error(Diagnostics.Unexpected_token_Did_you_mean_or_rbrace, pos, 1);
}
+ // Handle comments - skip /* ... */ style comments
+ if (char === CharacterCodes.slash && charCodeUnchecked(pos + 1) === CharacterCodes.asterisk) {
+ pos += 2; // Skip /*
+ // Find the end of the comment
+ while (pos < end) {
+ if (charCodeUnchecked(pos) === CharacterCodes.asterisk && charCodeUnchecked(pos + 1) === CharacterCodes.slash) {
+ pos += 2; // Skip */
+ break;
+ }
+ pos++;
+ }
+ // Continue without adding comment content to textContent
+ continue;
+ }
+
// FirstNonWhitespace is 0, then we only see whitespaces so far. If we see a linebreak, we want to ignore that whitespaces.
// i.e (- : whitespace)
//
----
@@ -3754,13 +3772,16 @@ export function createScanner(
break;
}
else if (!isWhiteSpaceLike(char)) {
- firstNonWhitespace = pos;
+ if (firstNonWhitespace === 0) {
+ firstNonWhitespace = textContent.length;
+ }
}
+ textContent += String.fromCharCode(char);
pos++;
}
- tokenValue = text.substring(fullStartPos, pos);
+ tokenValue = textContent;
return firstNonWhitespace === -1 ? SyntaxKind.JsxTextAllWhiteSpaces : SyntaxKind.JsxText;
}
diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts
index 4c25359e68fc8..885f4c5d67ad6 100644
--- a/src/compiler/transformers/jsx.ts
+++ b/src/compiler/transformers/jsx.ts
@@ -554,37 +554,22 @@ export function transformJsx(context: TransformationContext): (x: SourceFile | B
return fixed === undefined ? undefined : factory.createStringLiteral(fixed);
}
- /**
- * Remove comments from JSX text content.
- * Comments like slash-star comment star-slash should not be treated as text content in JSX.
- */
- function stripCommentsFromJsxText(text: string): string {
- // Only strip comments when not in preserve mode
- if (compilerOptions.jsx === JsxEmit.Preserve) {
- return text;
- }
- // Remove /* ... */ style comments from JSX text
- return text.replace(/\/\*[\s\S]*?\*\//g, '');
- }
-
- /**
- * JSX trims whitespace at the end and beginning of lines, except that the
- * start/end of a tag is considered a start/end of a line only if that line is
- * on the same line as the closing tag. See examples in
- * tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
- * See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
- *
- * An equivalent algorithm would be:
- * - If there is only one line, return it.
- * - If there is only whitespace (but multiple lines), return `undefined`.
- * - Split the text into lines.
- * - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
- * - Decode entities on each line (individually).
- * - Remove empty lines and join the rest with " ".
- */
- function fixupWhitespaceAndDecodeEntities(text: string): string | undefined {
- // First, strip comments from the text
- text = stripCommentsFromJsxText(text);
+ /**
+ * JSX trims whitespace at the end and beginning of lines, except that the
+ * start/end of a tag is considered a start/end of a line only if that line is
+ * on the same line as the closing tag. See examples in
+ * tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
+ * See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
+ *
+ * An equivalent algorithm would be:
+ * - If there is only one line, return it.
+ * - If there is only whitespace (but multiple lines), return `undefined`.
+ * - Split the text into lines.
+ * - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
+ * - Decode entities on each line (individually).
+ * - Remove empty lines and join the rest with " ".
+ */
+ function fixupWhitespaceAndDecodeEntities(text: string): string | undefined {
let acc: string | undefined;
// First non-whitespace character on this line.
let firstNonWhitespace = 0;