Port FindNextToken and FindPrecedingToken from Go to TypeScript#2963
Port FindNextToken and FindPrecedingToken from Go to TypeScript#2963
Conversation
|
@copilot also do FindPrecedingToken |
Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Done in 8cf4ca2. Added |
|
@copilot |
Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Ports AST token-navigation helpers from the Go implementation into the TypeScript @typescript/ast package so API consumers can navigate tokens forward/backward consistently.
Changes:
- Added
findNextTokento_packages/ast/src/astnav.ts(intended port of Go’sFindNextToken). - Exported
findPrecedingTokenfrom_packages/ast/src/astnav.ts(wrapper over existing impl). - Added async tests and regenerated sync tests covering both helpers.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
_packages/ast/src/astnav.ts |
Adds findNextToken implementation and exports findPrecedingToken. |
_packages/api/test/async/astnav.test.ts |
Adds new tests for findNextToken/findPrecedingToken. |
_packages/api/test/sync/astnav.test.ts |
Auto-generated sync copy of the async tests. |
| // Visit JSDoc children first (mirrors Go's VisitEachChildAndJSDoc). | ||
| if (n.jsDoc) { | ||
| for (const jsdoc of n.jsDoc) { | ||
| visitChild(jsdoc); | ||
| } | ||
| } | ||
|
|
||
| n.forEachChild( | ||
| visitChild, | ||
| nodes => { | ||
| if (nodes.length > 0 && foundNode === undefined) { | ||
| for (const node of nodes) { | ||
| if (node.flags & NodeFlags.Reparsed) continue; | ||
| if (node.pos > previousToken.end) break; | ||
| if (node.end > previousToken.end) { | ||
| foundNode = node; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| return undefined; | ||
| }, | ||
| ); |
There was a problem hiding this comment.
findNextToken doesn’t apply the JSDoc single-comment NodeList skipping that other navigation code in this file uses (see the isJSDocSingleCommentNodeList/isJSDocCommentChildKind helpers used by getTokenAtPositionImpl). The Go VisitEachChildAndJSDoc path also explicitly skips these. Without the same skip here, the descent target can differ (especially inside JSDoc), which can change results vs the intended port.
_packages/ast/src/astnav.ts
Outdated
| const flags = scanner.getTokenFlags(); | ||
| if (tokenStart === previousToken.end) { | ||
| return getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, n, flags); | ||
| } |
There was a problem hiding this comment.
In the scanner fallback, if startPos is within the node but tokenStart !== previousToken.end, the Go implementation treats this as an invariant violation (panics). Here it silently returns undefined, which can mask inconsistent node ranges and make the port behave differently. Consider throwing/asserting in this branch (or otherwise documenting/encoding the intended deviation).
| } | |
| } | |
| // Invariant violation: startPos is within the node, but the scanner token does not start at previousToken.end. | |
| // The Go implementation treats this as a panic; we mirror that here to avoid masking inconsistent node ranges. | |
| throw new Error( | |
| `findNextToken: inconsistent node ranges: previousToken.end=${previousToken.end}, tokenStart=${tokenStart}, ` + | |
| `node=[${n.pos}, ${n.end}), startPos=${startPos}`, | |
| ); |
| nodes => { | ||
| if (nodes.length > 0 && foundNode === undefined) { | ||
| for (const node of nodes) { | ||
| if (node.flags & NodeFlags.Reparsed) continue; | ||
| if (node.pos > previousToken.end) break; | ||
| if (node.end > previousToken.end) { | ||
| foundNode = node; | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
In the NodeArray path, the selection predicate differs from the Go port: it only considers nodes with node.end > previousToken.end. The Go implementation’s binary search matches any node overlapping the previousToken span (node.pos <= previousToken.End && node.end > previousToken.Pos), which matters when callers pass a non-token node (the Go API allows this) or when the relevant child ends exactly at previousToken.end. Consider mirroring the Go overlap predicate (and ideally the binary-search approach) to avoid missing the correct subtree.
There was a problem hiding this comment.
This is the only difference I see as well. Rest all looks good.
| for (const node of nodes) { | ||
| if (node.flags & NodeFlags.Reparsed) continue; | ||
| if (node.pos > previousToken.end) break; | ||
| if (node.end > previousToken.end) { |
There was a problem hiding this comment.
This looks a little different from Corsa. I believe this should be if (node.end > previousToken.pos) instead.
There was a problem hiding this comment.
After careful analysis, the linear scan predicate node.end > previousToken.end is actually correct for the scanner-based approach. Changing it to node.end > previousToken.pos would cause nodes with the same span as previousToken to be selected, preventing the scanner fallback from firing and breaking comma-finding (e.g. findNextToken(Block_identifier, ...) would return nil instead of ,). The real fix was in the scanner fallback itself — using tokenFullStart instead of tokenStart — committed in a4777eb.
|
@copilot see review feedback, failing tests, and carefully compare your implementation against tokens.go. Run tests and format before committing. |
…er handling Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Fixed in a4777eb. Changes made:
|
|
@copilot the Go version of astnav.GetTokenAtPosition produces baselines that the TS version of getTokenAtPosition uses to guarantee identical behavior. Add baseline generation to the Go tests for FindNextToken and FindPrecedingToken and replace the manual TS tests with ones that check their behavior against the Go baselines. |
Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
|
@copilot you were interrupted by an error but pushed your work in progress. Please continue. |
…or for JSDoc nodes Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Completed in 1acd2b3. The baseline-based tests are now passing (9772 positions checked for both
|
Ports
findNextTokenandfindPrecedingTokenAST token-navigation helpers from the Go implementation into the TypeScript@typescript/astpackage, with behavior validated against Go-generated baselines.Changes Made
findNextToken: Added to_packages/ast/src/astnav.ts, porting Go'sFindNextToken. Uses a scanner fallback withtokenFullStart(nottokenStart) so tokens with leading trivia (like{afterimport) are found correctly.findPrecedingToken: Exported from_packages/ast/src/astnav.tsas a public API, wrapping the internalfindPrecedingTokenImpl. Key correctness fixes:foundChild(start >= position && position >= foundChild.pos), mirrors Go's logic: checks for JSDoc nodes of the parent precedingfoundChild, recurses into JSDoc ifposition < jsDoc.end, otherwise usesjsDoc.endas the scan boundary.findRightmostValidTokenskips single-comment JSDoc NodeLists (e.g.JSDocTextinJSDoc.comment) using theskipSingleCommentChildrenflag pattern — necessary becauseRemoteNode.forEachChildalways calls bothvisitListandvisitNodefor NodeList children, so guarding only in thenodesCallbackis insufficient.findRightmostValidTokenso syntactic tokens ({,,) not stored in the AST are found correctly.getTokenPosOfNodecalled withincludeJSDoc=trueso position checks correctly account for JSDoc leading nodes.Go baseline tests: Added
TestFindPrecedingTokenandTestFindNextTokenininternal/astnav/tokens_test.gousingbaselineGoTokensJSON, generatingFindPrecedingToken.mapCode.ts.baseline.jsonandFindNextToken.mapCode.ts.baseline.json.TypeScript tests: Replaced manual hand-written tests with baseline-comparison tests (matching the
getTokenAtPositionpattern), checking all 9,772 positions against the Go-generated baselines.💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.