From 127e3100b0852516b7103e50d8ac3f1c61c0a7bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:40:57 +0000 Subject: [PATCH 1/5] Initial plan From 89d6c676d77d4dd142d27fe5a3dac3a075ca8e90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:53:54 +0000 Subject: [PATCH 2/5] Add test case to reproduce JSX comment duplication issue Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/emitter.ts | 8 +++---- .../jsxCommentDuplication(jsx=preserve).js | 11 ++++++++++ ...sxCommentDuplication(jsx=preserve).symbols | 11 ++++++++++ .../jsxCommentDuplication(jsx=preserve).types | 20 +++++++++++++++++ ...mmentDuplication(jsx=react-jsx).errors.txt | 9 ++++++++ .../jsxCommentDuplication(jsx=react-jsx).js | 14 ++++++++++++ ...xCommentDuplication(jsx=react-jsx).symbols | 11 ++++++++++ ...jsxCommentDuplication(jsx=react-jsx).types | 22 +++++++++++++++++++ .../cases/compiler/jsxCommentDuplication.tsx | 4 ++++ 9 files changed, 106 insertions(+), 4 deletions(-) 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-jsx).errors.txt create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).js create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).symbols create mode 100644 tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).types create mode 100644 tests/cases/compiler/jsxCommentDuplication.tsx diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index c60fd92787275..071ecb382aed6 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -4785,10 +4785,10 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri } } - // Emit this child. - if (shouldEmitInterveningComments) { - const commentRange = getCommentRange(child); - emitTrailingCommentsOfPosition(commentRange.pos); + // Emit this child. + if (shouldEmitInterveningComments) { + const commentRange = getCommentRange(child); + emitTrailingCommentsOfPosition(commentRange.pos); } else { shouldEmitInterveningComments = mayEmitInterveningComments; diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js new file mode 100644 index 0000000000000..476d4c989b883 --- /dev/null +++ b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js @@ -0,0 +1,11 @@ +//// [tests/cases/compiler/jsxCommentDuplication.tsx] //// + +//// [jsxCommentDuplication.tsx] +// Simple test case to reproduce JSX comment duplication +const x = 42; +const jsx =
/*pre*/{x}/*post*/
; + +//// [jsxCommentDuplication.jsx] +// Simple test case to reproduce JSX comment duplication +var x = 42; +var jsx =
/*pre*//*pre*/{x} /*post*//*post*/
; diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).symbols b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).symbols new file mode 100644 index 0000000000000..bb4f837dbfc9e --- /dev/null +++ b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).symbols @@ -0,0 +1,11 @@ +//// [tests/cases/compiler/jsxCommentDuplication.tsx] //// + +=== jsxCommentDuplication.tsx === +// Simple test case to reproduce JSX comment duplication +const x = 42; +>x : Symbol(x, Decl(jsxCommentDuplication.tsx, 1, 5)) + +const jsx =
/*pre*/{x}/*post*/
; +>jsx : Symbol(jsx, Decl(jsxCommentDuplication.tsx, 2, 5)) +>x : Symbol(x, Decl(jsxCommentDuplication.tsx, 1, 5)) + diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).types b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).types new file mode 100644 index 0000000000000..2c9cdbd68a0f4 --- /dev/null +++ b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).types @@ -0,0 +1,20 @@ +//// [tests/cases/compiler/jsxCommentDuplication.tsx] //// + +=== jsxCommentDuplication.tsx === +// Simple test case to reproduce JSX comment duplication +const x = 42; +>x : 42 +> : ^^ +>42 : 42 +> : ^^ + +const jsx =
/*pre*/{x}/*post*/
; +>jsx : error +>
/*pre*/{x}/*post*/
: error +>div : any +> : ^^^ +>x : 42 +> : ^^ +>div : any +> : ^^^ + diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).errors.txt b/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).errors.txt new file mode 100644 index 0000000000000..aefdad9e893ce --- /dev/null +++ b/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).errors.txt @@ -0,0 +1,9 @@ +jsxCommentDuplication.tsx(3,13): error TS2875: This JSX tag requires the module path 'react/jsx-runtime' to exist, but none could be found. Make sure you have types for the appropriate package installed. + + +==== jsxCommentDuplication.tsx (1 errors) ==== + // Simple test case to reproduce JSX comment duplication + const x = 42; + const jsx =
/*pre*/{x}/*post*/
; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2875: This JSX tag requires the module path 'react/jsx-runtime' to exist, but none could be found. Make sure you have types for the appropriate package installed. \ No newline at end of file diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).js b/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).js new file mode 100644 index 0000000000000..c663c49b539ec --- /dev/null +++ b/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).js @@ -0,0 +1,14 @@ +//// [tests/cases/compiler/jsxCommentDuplication.tsx] //// + +//// [jsxCommentDuplication.tsx] +// Simple test case to reproduce JSX comment duplication +const x = 42; +const jsx =
/*pre*/{x}/*post*/
; + +//// [jsxCommentDuplication.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var jsx_runtime_1 = require("react/jsx-runtime"); +// Simple test case to reproduce JSX comment duplication +var x = 42; +var jsx = (0, jsx_runtime_1.jsxs)("div", { children: ["/*pre*/", x, "/*post*/"] }); diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).symbols b/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).symbols new file mode 100644 index 0000000000000..bb4f837dbfc9e --- /dev/null +++ b/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).symbols @@ -0,0 +1,11 @@ +//// [tests/cases/compiler/jsxCommentDuplication.tsx] //// + +=== jsxCommentDuplication.tsx === +// Simple test case to reproduce JSX comment duplication +const x = 42; +>x : Symbol(x, Decl(jsxCommentDuplication.tsx, 1, 5)) + +const jsx =
/*pre*/{x}/*post*/
; +>jsx : Symbol(jsx, Decl(jsxCommentDuplication.tsx, 2, 5)) +>x : Symbol(x, Decl(jsxCommentDuplication.tsx, 1, 5)) + diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).types b/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).types new file mode 100644 index 0000000000000..0ee7b9eb8dcfa --- /dev/null +++ b/tests/baselines/reference/jsxCommentDuplication(jsx=react-jsx).types @@ -0,0 +1,22 @@ +//// [tests/cases/compiler/jsxCommentDuplication.tsx] //// + +=== jsxCommentDuplication.tsx === +// Simple test case to reproduce JSX comment duplication +const x = 42; +>x : 42 +> : ^^ +>42 : 42 +> : ^^ + +const jsx =
/*pre*/{x}/*post*/
; +>jsx : any +> : ^^^ +>
/*pre*/{x}/*post*/
: any +> : ^^^ +>div : any +> : ^^^ +>x : 42 +> : ^^ +>div : any +> : ^^^ + diff --git a/tests/cases/compiler/jsxCommentDuplication.tsx b/tests/cases/compiler/jsxCommentDuplication.tsx new file mode 100644 index 0000000000000..4ff65ddd631d6 --- /dev/null +++ b/tests/cases/compiler/jsxCommentDuplication.tsx @@ -0,0 +1,4 @@ +// @jsx: preserve,react-jsx +// Simple test case to reproduce JSX comment duplication +const x = 42; +const jsx =
/*pre*/{x}/*post*/
; \ No newline at end of file From 854b89ecde88515572e3f407b40505e76bc7a6c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:04:25 +0000 Subject: [PATCH 3/5] Investigate JSX comment duplication - identify issue is not in main list emission logic Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/emitter.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 071ecb382aed6..e195634d75ecd 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -3192,9 +3192,9 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri pos = writeTokenText(token, writer, pos); } - if (isSimilarNode && contextNode.end !== pos) { - const isJsxExprContext = contextNode.kind === SyntaxKind.JsxExpression; - emitTrailingCommentsOfPosition(pos, /*prefixSpace*/ !isJsxExprContext, /*forceNoNewline*/ isJsxExprContext); + if (isSimilarNode && contextNode.end !== pos) { + const isJsxExprContext = contextNode.kind === SyntaxKind.JsxExpression; + emitTrailingCommentsOfPosition(pos, /*prefixSpace*/ !isJsxExprContext, /*forceNoNewline*/ isJsxExprContext); } return pos; } @@ -4713,9 +4713,9 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri * * NOTE: You probably don't want to call this directly and should be using `emitList` or `emitExpressionList` instead. */ - function emitNodeListItems(emit: EmitFunction, parentNode: Node | undefined, children: readonly Child[], format: ListFormat, parenthesizerRule: ParenthesizerRuleOrSelector | undefined, start: number, count: number, hasTrailingComma: boolean, childrenTextRange: TextRange | undefined) { - // Write the opening line terminator or leading whitespace. - const mayEmitInterveningComments = (format & ListFormat.NoInterveningComments) === 0; + function emitNodeListItems(emit: EmitFunction, parentNode: Node | undefined, children: readonly Child[], format: ListFormat, parenthesizerRule: ParenthesizerRuleOrSelector | undefined, start: number, count: number, hasTrailingComma: boolean, childrenTextRange: TextRange | undefined) { + // Write the opening line terminator or leading whitespace. + const mayEmitInterveningComments = (format & ListFormat.NoInterveningComments) === 0; let shouldEmitInterveningComments = mayEmitInterveningComments; const leadingLineTerminatorCount = getLeadingLineTerminatorCount(parentNode, children[start], format); @@ -4772,9 +4772,9 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri shouldDecreaseIndentAfterEmit = true; } - if (shouldEmitInterveningComments && format & ListFormat.DelimitersMask && !positionIsSynthesized(child.pos)) { - const commentRange = getCommentRange(child); - emitTrailingCommentsOfPosition(commentRange.pos, /*prefixSpace*/ !!(format & ListFormat.SpaceBetweenSiblings), /*forceNoNewline*/ true); + if (shouldEmitInterveningComments && format & ListFormat.DelimitersMask && !positionIsSynthesized(child.pos)) { + const commentRange = getCommentRange(child); + emitTrailingCommentsOfPosition(commentRange.pos, /*prefixSpace*/ !!(format & ListFormat.SpaceBetweenSiblings), /*forceNoNewline*/ true); } writeLine(separatingLineTerminatorCount); @@ -6051,13 +6051,13 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri } } - function emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean, forceNoNewline?: boolean) { - if (commentsDisabled) { - return; - } - enterComment(); - forEachTrailingCommentToEmit(pos, prefixSpace ? emitTrailingComment : forceNoNewline ? emitTrailingCommentOfPositionNoNewline : emitTrailingCommentOfPosition); - exitComment(); + function emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean, forceNoNewline?: boolean) { + if (commentsDisabled) { + return; + } + enterComment(); + forEachTrailingCommentToEmit(pos, prefixSpace ? emitTrailingComment : forceNoNewline ? emitTrailingCommentOfPositionNoNewline : emitTrailingCommentOfPosition); + exitComment(); } function emitTrailingCommentOfPositionNoNewline(commentPos: number, commentEnd: number, kind: SyntaxKind) { From f6ff1596d64b01fb0aeb0325ca359f63b41c2345 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:11:56 +0000 Subject: [PATCH 4/5] Fix JSX comment duplication by filtering comments within JSX text Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- debug-ast.js | 28 ++++++++ src/compiler/emitter.ts | 67 +++++++++++++------ .../jsxCommentDuplication(jsx=preserve).js | 2 +- 3 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 debug-ast.js diff --git a/debug-ast.js b/debug-ast.js new file mode 100644 index 0000000000000..3dd31b071514a --- /dev/null +++ b/debug-ast.js @@ -0,0 +1,28 @@ +const ts = require("./built/local/typescript.js"); + +const sourceCode = `const jsx =
/*a*/{x}/*b*/
;`; +const sourceFile = ts.createSourceFile("test.tsx", sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); + +function printNode(node, depth = 0) { + const indent = " ".repeat(depth); + const kindName = ts.SyntaxKind[node.kind]; + const posInfo = `(${node.pos}-${node.end})`; + let extraInfo = ""; + + if (node.text !== undefined) { + extraInfo += ` text:"${node.text}"`; + } + + console.log(`${indent}${kindName} ${posInfo}${extraInfo}`); + + // Print the actual source text for this node range + if (node.pos >= 0 && node.end > node.pos) { + const nodeText = sourceCode.substring(node.pos, node.end); + console.log(`${indent} source: "${nodeText}"`); + } + + ts.forEachChild(node, child => printNode(child, depth + 1)); +} + +console.log("AST for:", sourceCode); +printNode(sourceFile); \ No newline at end of file diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index e195634d75ecd..210a6d47728b2 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -3939,20 +3939,20 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri return hasTrailingCommentsAtPosition(pos) || hasLeadingCommentsAtPosition(pos); } - function emitJsxExpression(node: JsxExpression) { - if (node.expression || (!commentsDisabled && !nodeIsSynthesized(node) && hasCommentsAtPosition(node.pos))) { // preserve empty expressions if they contain comments! - const isMultiline = currentSourceFile && !nodeIsSynthesized(node) && getLineAndCharacterOfPosition(currentSourceFile, node.pos).line !== getLineAndCharacterOfPosition(currentSourceFile, node.end).line; - if (isMultiline) { - writer.increaseIndent(); - } - const end = emitTokenWithComment(SyntaxKind.OpenBraceToken, node.pos, writePunctuation, node); - emit(node.dotDotDotToken); - emitExpression(node.expression); - emitTokenWithComment(SyntaxKind.CloseBraceToken, node.expression?.end || end, writePunctuation, node); - if (isMultiline) { - writer.decreaseIndent(); - } - } + function emitJsxExpression(node: JsxExpression) { + if (node.expression || (!commentsDisabled && !nodeIsSynthesized(node) && hasCommentsAtPosition(node.pos))) { // preserve empty expressions if they contain comments! + const isMultiline = currentSourceFile && !nodeIsSynthesized(node) && getLineAndCharacterOfPosition(currentSourceFile, node.pos).line !== getLineAndCharacterOfPosition(currentSourceFile, node.end).line; + if (isMultiline) { + writer.increaseIndent(); + } + const end = emitTokenWithComment(SyntaxKind.OpenBraceToken, node.pos, writePunctuation, node); + emit(node.dotDotDotToken); + emitExpression(node.expression); + emitTokenWithComment(SyntaxKind.CloseBraceToken, node.expression?.end || end, writePunctuation, node); + if (isMultiline) { + writer.decreaseIndent(); + } + } } function emitJsxNamespacedName(node: JsxNamespacedName) { @@ -6101,11 +6101,40 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri } } - function forEachTrailingCommentToEmit(end: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => void) { - // Emit the trailing comments only if the container's end doesn't match because the container should take care of emitting these comments - if (currentSourceFile && (containerEnd === -1 || (end !== containerEnd && end !== declarationListContainerEnd))) { - forEachTrailingCommentRange(currentSourceFile.text, end, cb); - } + function forEachTrailingCommentToEmit(end: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => void) { + // Emit the trailing comments only if the container's end doesn't match because the container should take care of emitting these comments + if (currentSourceFile && (containerEnd === -1 || (end !== containerEnd && end !== declarationListContainerEnd))) { + // Create a wrapper callback that filters out comments that are part of JSX text content + const filteredCb = (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => { + // Check if this comment is actually JSX text content + if (isCommentWithinJsxText(commentPos, commentEnd)) { + return; // Skip this comment emission + } + cb(commentPos, commentEnd, kind, hasTrailingNewLine); + }; + + forEachTrailingCommentRange(currentSourceFile.text, end, filteredCb); + } + } + + function isCommentWithinJsxText(commentPos: number, commentEnd: number): boolean { + if (!currentSourceFile) return false; + + // Find JSX text nodes that might contain this comment + function checkNode(node: Node): boolean { + if (node.kind === SyntaxKind.JsxText) { + const jsxText = node as JsxText; + // Check if the comment range is within the JSX text node range + if (commentPos >= jsxText.pos && commentEnd <= jsxText.end) { + return true; + } + } + + // Recursively check children + return !!forEachChild(node, checkNode); + } + + return checkNode(currentSourceFile); } function hasDetachedComments(pos: number) { diff --git a/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js index 476d4c989b883..e6e31288aa05d 100644 --- a/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js +++ b/tests/baselines/reference/jsxCommentDuplication(jsx=preserve).js @@ -8,4 +8,4 @@ const jsx =
/*pre*/{x}/*post*/
; //// [jsxCommentDuplication.jsx] // Simple test case to reproduce JSX comment duplication var x = 42; -var jsx =
/*pre*//*pre*/{x} /*post*//*post*/
; +var jsx =
/*pre*/{x}/*post*/
; From 98d98b9935eed840eb34fa888687a7bd7174c434 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:18:31 +0000 Subject: [PATCH 5/5] Clean up debug files and format code --- debug-ast.js | 28 --------- src/compiler/emitter.ts | 136 ++++++++++++++++++++-------------------- 2 files changed, 68 insertions(+), 96 deletions(-) delete mode 100644 debug-ast.js diff --git a/debug-ast.js b/debug-ast.js deleted file mode 100644 index 3dd31b071514a..0000000000000 --- a/debug-ast.js +++ /dev/null @@ -1,28 +0,0 @@ -const ts = require("./built/local/typescript.js"); - -const sourceCode = `const jsx =
/*a*/{x}/*b*/
;`; -const sourceFile = ts.createSourceFile("test.tsx", sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); - -function printNode(node, depth = 0) { - const indent = " ".repeat(depth); - const kindName = ts.SyntaxKind[node.kind]; - const posInfo = `(${node.pos}-${node.end})`; - let extraInfo = ""; - - if (node.text !== undefined) { - extraInfo += ` text:"${node.text}"`; - } - - console.log(`${indent}${kindName} ${posInfo}${extraInfo}`); - - // Print the actual source text for this node range - if (node.pos >= 0 && node.end > node.pos) { - const nodeText = sourceCode.substring(node.pos, node.end); - console.log(`${indent} source: "${nodeText}"`); - } - - ts.forEachChild(node, child => printNode(child, depth + 1)); -} - -console.log("AST for:", sourceCode); -printNode(sourceFile); \ No newline at end of file diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 210a6d47728b2..c907b2453090d 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -3192,9 +3192,9 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri pos = writeTokenText(token, writer, pos); } - if (isSimilarNode && contextNode.end !== pos) { - const isJsxExprContext = contextNode.kind === SyntaxKind.JsxExpression; - emitTrailingCommentsOfPosition(pos, /*prefixSpace*/ !isJsxExprContext, /*forceNoNewline*/ isJsxExprContext); + if (isSimilarNode && contextNode.end !== pos) { + const isJsxExprContext = contextNode.kind === SyntaxKind.JsxExpression; + emitTrailingCommentsOfPosition(pos, /*prefixSpace*/ !isJsxExprContext, /*forceNoNewline*/ isJsxExprContext); } return pos; } @@ -3939,20 +3939,20 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri return hasTrailingCommentsAtPosition(pos) || hasLeadingCommentsAtPosition(pos); } - function emitJsxExpression(node: JsxExpression) { - if (node.expression || (!commentsDisabled && !nodeIsSynthesized(node) && hasCommentsAtPosition(node.pos))) { // preserve empty expressions if they contain comments! - const isMultiline = currentSourceFile && !nodeIsSynthesized(node) && getLineAndCharacterOfPosition(currentSourceFile, node.pos).line !== getLineAndCharacterOfPosition(currentSourceFile, node.end).line; - if (isMultiline) { - writer.increaseIndent(); - } - const end = emitTokenWithComment(SyntaxKind.OpenBraceToken, node.pos, writePunctuation, node); - emit(node.dotDotDotToken); - emitExpression(node.expression); - emitTokenWithComment(SyntaxKind.CloseBraceToken, node.expression?.end || end, writePunctuation, node); - if (isMultiline) { - writer.decreaseIndent(); - } - } + function emitJsxExpression(node: JsxExpression) { + if (node.expression || (!commentsDisabled && !nodeIsSynthesized(node) && hasCommentsAtPosition(node.pos))) { // preserve empty expressions if they contain comments! + const isMultiline = currentSourceFile && !nodeIsSynthesized(node) && getLineAndCharacterOfPosition(currentSourceFile, node.pos).line !== getLineAndCharacterOfPosition(currentSourceFile, node.end).line; + if (isMultiline) { + writer.increaseIndent(); + } + const end = emitTokenWithComment(SyntaxKind.OpenBraceToken, node.pos, writePunctuation, node); + emit(node.dotDotDotToken); + emitExpression(node.expression); + emitTokenWithComment(SyntaxKind.CloseBraceToken, node.expression?.end || end, writePunctuation, node); + if (isMultiline) { + writer.decreaseIndent(); + } + } } function emitJsxNamespacedName(node: JsxNamespacedName) { @@ -4713,9 +4713,9 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri * * NOTE: You probably don't want to call this directly and should be using `emitList` or `emitExpressionList` instead. */ - function emitNodeListItems(emit: EmitFunction, parentNode: Node | undefined, children: readonly Child[], format: ListFormat, parenthesizerRule: ParenthesizerRuleOrSelector | undefined, start: number, count: number, hasTrailingComma: boolean, childrenTextRange: TextRange | undefined) { - // Write the opening line terminator or leading whitespace. - const mayEmitInterveningComments = (format & ListFormat.NoInterveningComments) === 0; + function emitNodeListItems(emit: EmitFunction, parentNode: Node | undefined, children: readonly Child[], format: ListFormat, parenthesizerRule: ParenthesizerRuleOrSelector | undefined, start: number, count: number, hasTrailingComma: boolean, childrenTextRange: TextRange | undefined) { + // Write the opening line terminator or leading whitespace. + const mayEmitInterveningComments = (format & ListFormat.NoInterveningComments) === 0; let shouldEmitInterveningComments = mayEmitInterveningComments; const leadingLineTerminatorCount = getLeadingLineTerminatorCount(parentNode, children[start], format); @@ -4772,9 +4772,9 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri shouldDecreaseIndentAfterEmit = true; } - if (shouldEmitInterveningComments && format & ListFormat.DelimitersMask && !positionIsSynthesized(child.pos)) { - const commentRange = getCommentRange(child); - emitTrailingCommentsOfPosition(commentRange.pos, /*prefixSpace*/ !!(format & ListFormat.SpaceBetweenSiblings), /*forceNoNewline*/ true); + if (shouldEmitInterveningComments && format & ListFormat.DelimitersMask && !positionIsSynthesized(child.pos)) { + const commentRange = getCommentRange(child); + emitTrailingCommentsOfPosition(commentRange.pos, /*prefixSpace*/ !!(format & ListFormat.SpaceBetweenSiblings), /*forceNoNewline*/ true); } writeLine(separatingLineTerminatorCount); @@ -4785,10 +4785,10 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri } } - // Emit this child. - if (shouldEmitInterveningComments) { - const commentRange = getCommentRange(child); - emitTrailingCommentsOfPosition(commentRange.pos); + // Emit this child. + if (shouldEmitInterveningComments) { + const commentRange = getCommentRange(child); + emitTrailingCommentsOfPosition(commentRange.pos); } else { shouldEmitInterveningComments = mayEmitInterveningComments; @@ -6051,13 +6051,13 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri } } - function emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean, forceNoNewline?: boolean) { - if (commentsDisabled) { - return; - } - enterComment(); - forEachTrailingCommentToEmit(pos, prefixSpace ? emitTrailingComment : forceNoNewline ? emitTrailingCommentOfPositionNoNewline : emitTrailingCommentOfPosition); - exitComment(); + function emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean, forceNoNewline?: boolean) { + if (commentsDisabled) { + return; + } + enterComment(); + forEachTrailingCommentToEmit(pos, prefixSpace ? emitTrailingComment : forceNoNewline ? emitTrailingCommentOfPositionNoNewline : emitTrailingCommentOfPosition); + exitComment(); } function emitTrailingCommentOfPositionNoNewline(commentPos: number, commentEnd: number, kind: SyntaxKind) { @@ -6101,40 +6101,40 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri } } - function forEachTrailingCommentToEmit(end: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => void) { - // Emit the trailing comments only if the container's end doesn't match because the container should take care of emitting these comments - if (currentSourceFile && (containerEnd === -1 || (end !== containerEnd && end !== declarationListContainerEnd))) { - // Create a wrapper callback that filters out comments that are part of JSX text content - const filteredCb = (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => { - // Check if this comment is actually JSX text content - if (isCommentWithinJsxText(commentPos, commentEnd)) { - return; // Skip this comment emission - } - cb(commentPos, commentEnd, kind, hasTrailingNewLine); - }; - - forEachTrailingCommentRange(currentSourceFile.text, end, filteredCb); - } - } - - function isCommentWithinJsxText(commentPos: number, commentEnd: number): boolean { - if (!currentSourceFile) return false; - - // Find JSX text nodes that might contain this comment - function checkNode(node: Node): boolean { - if (node.kind === SyntaxKind.JsxText) { - const jsxText = node as JsxText; - // Check if the comment range is within the JSX text node range - if (commentPos >= jsxText.pos && commentEnd <= jsxText.end) { - return true; - } - } - - // Recursively check children - return !!forEachChild(node, checkNode); - } - - return checkNode(currentSourceFile); + function forEachTrailingCommentToEmit(end: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => void) { + // Emit the trailing comments only if the container's end doesn't match because the container should take care of emitting these comments + if (currentSourceFile && (containerEnd === -1 || (end !== containerEnd && end !== declarationListContainerEnd))) { + // Create a wrapper callback that filters out comments that are part of JSX text content + const filteredCb = (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => { + // Check if this comment is actually JSX text content + if (isCommentWithinJsxText(commentPos, commentEnd)) { + return; // Skip this comment emission + } + cb(commentPos, commentEnd, kind, hasTrailingNewLine); + }; + + forEachTrailingCommentRange(currentSourceFile.text, end, filteredCb); + } + } + + function isCommentWithinJsxText(commentPos: number, commentEnd: number): boolean { + if (!currentSourceFile) return false; + + // Find JSX text nodes that might contain this comment + function checkNode(node: Node): boolean { + if (node.kind === SyntaxKind.JsxText) { + const jsxText = node as JsxText; + // Check if the comment range is within the JSX text node range + if (commentPos >= jsxText.pos && commentEnd <= jsxText.end) { + return true; + } + } + + // Recursively check children + return !!forEachChild(node, checkNode); + } + + return checkNode(currentSourceFile); } function hasDetachedComments(pos: number) {