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;