From 35c1faf86566d716af4193f43cb6faf4d6e510fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 4 Sep 2026 15:56:37 +0200 Subject: [PATCH 1/3] Preserve less-than tokens in getChildren --- packages/typescript/src/ast/astnav.ts | 10 +++++-- packages/typescript/test/sync/ast.test.ts | 32 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/typescript/src/ast/astnav.ts b/packages/typescript/src/ast/astnav.ts index f401215034b9d..2ef8887e5a445 100644 --- a/packages/typescript/src/ast/astnav.ts +++ b/packages/typescript/src/ast/astnav.ts @@ -709,8 +709,14 @@ function addSyntheticNodes(children: Node[], pos: number, end: number, parent: N scanner.resetTokenState(pos); scanner.scan(); while (pos < end) { - const token = scanner.getToken(); - const tokenEnd = scanner.getTokenEnd(); + let token = scanner.getToken(); + let tokenEnd = scanner.getTokenEnd(); + if (tokenEnd > end) { + // The parser rescans `<<` as `<` when opening type arguments; mirror that split + // when the combined token crosses the next AST child's boundary. + token = scanner.reScanLessThanToken(); + tokenEnd = scanner.getTokenEnd(); + } if (tokenEnd <= end) { // An identifier should never appear as trivia between AST children; skip defensively. if (token !== SyntaxKind.Identifier) { diff --git a/packages/typescript/test/sync/ast.test.ts b/packages/typescript/test/sync/ast.test.ts index 1783da4ac3185..7e56227d63f99 100644 --- a/packages/typescript/test/sync/ast.test.ts +++ b/packages/typescript/test/sync/ast.test.ts @@ -1072,6 +1072,38 @@ describe("RemoteNode + child/token getters", () => { }); }); + test("public getChildren API preserves leaf tokens when type arguments begin with less-than", () => { + for ( + const source of [ + "type X = ReturnType<(x: T) => number>;", + "type X = ReturnType <(x: T) => number>;", + "type X = ReturnType/* c */ <(x: T) => number>;", + "const x = foo<(x: T) => T>();", + ] + ) { + withFirstStatement(source, (stmt, sf) => { + const node = findFirstOfKind(stmt, SyntaxKind.CallExpression) + ?? findFirstOfKind(stmt, SyntaxKind.TypeReference)!; + const leafTokens: Node[] = []; + const collectLeafTokens = (current: Node): void => { + const children = current.getChildren(sf); + if (children.length === 0) { + leafTokens.push(current); + } + else { + children.forEach(collectLeafTokens); + } + }; + collectLeafTokens(node); + + const firstLessThan = leafTokens.find(child => child.kind === SyntaxKind.LessThanToken); + assert.ok(firstLessThan, "expected the public API to expose the opening less-than token"); + assert.strictEqual(firstLessThan.getStart(sf), source.indexOf("<<")); + assert.strictEqual(leafTokens.map(child => child.getFullText(sf)).join(""), node.getFullText(sf)); + }); + } + }); + test("getChildCount and getChildAt agree with getChildren", () => { withFirstStatement("if (x) {}", stmt => { const children = stmt.getChildren(); From 4c8f992c977a5e0469a4ee2654694cd34efad62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 4 Sep 2026 16:22:27 +0200 Subject: [PATCH 2/3] Tighten less-than token rescan conditions --- packages/typescript/src/ast/astnav.ts | 2 +- packages/typescript/test/sync/ast.test.ts | 25 +++++++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/typescript/src/ast/astnav.ts b/packages/typescript/src/ast/astnav.ts index 2ef8887e5a445..0b823adb6c5f8 100644 --- a/packages/typescript/src/ast/astnav.ts +++ b/packages/typescript/src/ast/astnav.ts @@ -711,7 +711,7 @@ function addSyntheticNodes(children: Node[], pos: number, end: number, parent: N while (pos < end) { let token = scanner.getToken(); let tokenEnd = scanner.getTokenEnd(); - if (tokenEnd > end) { + if (token === SyntaxKind.LessThanLessThanToken && scanner.getTokenStart() < end && tokenEnd > end) { // The parser rescans `<<` as `<` when opening type arguments; mirror that split // when the combined token crosses the next AST child's boundary. token = scanner.reScanLessThanToken(); diff --git a/packages/typescript/test/sync/ast.test.ts b/packages/typescript/test/sync/ast.test.ts index 7e56227d63f99..9acc1d5318080 100644 --- a/packages/typescript/test/sync/ast.test.ts +++ b/packages/typescript/test/sync/ast.test.ts @@ -1065,6 +1065,11 @@ describe("RemoteNode + child/token getters", () => { return found; } + function getLeafTokens(node: Node, sourceFile: SourceFile): Node[] { + const children = node.getChildren(sourceFile); + return children.length === 0 ? [node] : children.flatMap(child => getLeafTokens(child, sourceFile)); + } + test("getChildren materializes the punctuation/keyword tokens the AST omits", () => { withFirstStatement("if (x) {}", stmt => { const texts = stmt.getChildren().map(c => c.getText()); @@ -1084,17 +1089,7 @@ describe("RemoteNode + child/token getters", () => { withFirstStatement(source, (stmt, sf) => { const node = findFirstOfKind(stmt, SyntaxKind.CallExpression) ?? findFirstOfKind(stmt, SyntaxKind.TypeReference)!; - const leafTokens: Node[] = []; - const collectLeafTokens = (current: Node): void => { - const children = current.getChildren(sf); - if (children.length === 0) { - leafTokens.push(current); - } - else { - children.forEach(collectLeafTokens); - } - }; - collectLeafTokens(node); + const leafTokens = getLeafTokens(node, sf); const firstLessThan = leafTokens.find(child => child.kind === SyntaxKind.LessThanToken); assert.ok(firstLessThan, "expected the public API to expose the opening less-than token"); @@ -1104,6 +1099,14 @@ describe("RemoteNode + child/token getters", () => { } }); + test("public getChildren API preserves ordinary left-shift tokens", () => { + withFirstStatement("const x = a << b;", (stmt, sf) => { + const leafTokens = getLeafTokens(stmt, sf); + assert.strictEqual(leafTokens.filter(child => child.kind === SyntaxKind.LessThanLessThanToken).length, 1); + assert.strictEqual(leafTokens.filter(child => child.kind === SyntaxKind.LessThanToken).length, 0); + }); + }); + test("getChildCount and getChildAt agree with getChildren", () => { withFirstStatement("if (x) {}", stmt => { const children = stmt.getChildren(); From f7cee855ca718cf0c25cea6167643c59f076e683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 4 Sep 2026 16:27:20 +0200 Subject: [PATCH 3/3] Reuse child invariants in token tests --- packages/typescript/test/sync/ast.test.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/typescript/test/sync/ast.test.ts b/packages/typescript/test/sync/ast.test.ts index 9acc1d5318080..0ccd1a4dd82eb 100644 --- a/packages/typescript/test/sync/ast.test.ts +++ b/packages/typescript/test/sync/ast.test.ts @@ -1065,11 +1065,6 @@ describe("RemoteNode + child/token getters", () => { return found; } - function getLeafTokens(node: Node, sourceFile: SourceFile): Node[] { - const children = node.getChildren(sourceFile); - return children.length === 0 ? [node] : children.flatMap(child => getLeafTokens(child, sourceFile)); - } - test("getChildren materializes the punctuation/keyword tokens the AST omits", () => { withFirstStatement("if (x) {}", stmt => { const texts = stmt.getChildren().map(c => c.getText()); @@ -1089,21 +1084,23 @@ describe("RemoteNode + child/token getters", () => { withFirstStatement(source, (stmt, sf) => { const node = findFirstOfKind(stmt, SyntaxKind.CallExpression) ?? findFirstOfKind(stmt, SyntaxKind.TypeReference)!; - const leafTokens = getLeafTokens(node, sf); + const children = node.getChildren(sf); + assertChildInvariants(node, sf); - const firstLessThan = leafTokens.find(child => child.kind === SyntaxKind.LessThanToken); + const firstLessThan = children.find(child => child.kind === SyntaxKind.LessThanToken); assert.ok(firstLessThan, "expected the public API to expose the opening less-than token"); assert.strictEqual(firstLessThan.getStart(sf), source.indexOf("<<")); - assert.strictEqual(leafTokens.map(child => child.getFullText(sf)).join(""), node.getFullText(sf)); + assert.strictEqual(children.map(child => child.getFullText(sf)).join(""), node.getFullText(sf)); }); } }); test("public getChildren API preserves ordinary left-shift tokens", () => { withFirstStatement("const x = a << b;", (stmt, sf) => { - const leafTokens = getLeafTokens(stmt, sf); - assert.strictEqual(leafTokens.filter(child => child.kind === SyntaxKind.LessThanLessThanToken).length, 1); - assert.strictEqual(leafTokens.filter(child => child.kind === SyntaxKind.LessThanToken).length, 0); + const binary = findFirstOfKind(stmt, SyntaxKind.BinaryExpression)!; + const children = binary.getChildren(sf); + assert.strictEqual(children.filter(child => child.kind === SyntaxKind.LessThanLessThanToken).length, 1); + assert.strictEqual(children.filter(child => child.kind === SyntaxKind.LessThanToken).length, 0); }); });