diff --git a/packages/typescript/src/ast/astnav.ts b/packages/typescript/src/ast/astnav.ts index f401215034b9d..0b823adb6c5f8 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 (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(); + 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..0ccd1a4dd82eb 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 children = node.getChildren(sf); + assertChildInvariants(node, sf); + + 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(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 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); + }); + }); + test("getChildCount and getChildAt agree with getChildren", () => { withFirstStatement("if (x) {}", stmt => { const children = stmt.getChildren();