Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/typescript/src/ast/astnav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should a <<=, even if it's syntactically invalid, also needs to be accounted for?

// 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) {
Expand Down
32 changes: 32 additions & 0 deletions packages/typescript/test/sync/ast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<<T>(x: T) => number>;",
"type X = ReturnType <<T>(x: T) => number>;",
"type X = ReturnType/* c */ <<T>(x: T) => number>;",
"const x = foo<<T>(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();
Expand Down