optimize(go-scope-capture): thread captured nodes to kill O(n^2) findNodeAtRange re-walks

emitGoScopeCaptures re-derived each match's AST node via findNodeAtRange from
the tree root on every query match, giving O(matches x rootChildren) ~ O(n^2)
behaviour (the #1848 root cause: a 250-struct generated DAO took ~10.8s, 800
structs ~100s+ — long enough to trip the worker sub-batch idle timeout and get
quarantined). Thread the query-captured SyntaxNode (c.node) through a parallel
tag->node map and use it directly (or via a bounded local parent walk for the
import_declaration ancestor case) instead of re-walking from root.

Output is byte-identical (capture fingerprint over the DAO file + all 89 go-*
fixtures unchanged; capture_groups=13501). 250 entities: 10835ms -> 114ms (95x).
800 entities: ~100s -> 384ms. Go resolver + scope-resolution suites: 165/165 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-05-30 07:09:25 +00:00
parent 5d1695f66a
commit eaf0a3052a

View file

@ -1,10 +1,5 @@
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import {
findNodeAtRange,
nodeToCapture,
syntheticCapture,
type SyntaxNode,
} from '../../utils/ast-helpers.js';
import { nodeToCapture, syntheticCapture, type SyntaxNode } from '../../utils/ast-helpers.js';
import { getGoParser, getGoScopeQuery } from './query.js';
import { recordGoCacheHit, recordGoCacheMiss } from './cache-stats.js';
import { computeGoCallArity, computeGoDeclarationArity } from './arity-metadata.js';
@ -34,18 +29,29 @@ export function emitGoScopeCaptures(
for (const m of rawMatches) {
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map. The tree-sitter query already
// hands us the matched node as `c.node`; keeping it here lets us derive the
// anchor/relative node by walking LOCALLY (parent chain / own subtree)
// instead of re-walking from tree.rootNode (the O(matches x rootChildren)
// hotpath that made #1848's 250-struct DAO file take ~10s). The captured
// node either IS the node the old findNodeAtRange re-derived, or is a close
// relative reachable by a bounded local walk.
const nodeMap: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
if (tag.startsWith('@_')) continue; // skip anonymous captures
grouped[tag] = nodeToCapture(tag, c.node);
nodeMap[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
if (grouped['@import.statement'] !== undefined) {
const anchor = grouped['@import.statement']!;
const importNode =
findNodeAtRange(tree.rootNode, anchor.range, 'import_declaration') ??
findNodeAtRange(tree.rootNode, anchor.range, 'import_spec');
// The captured node is the `import_spec`; the original code preferred its
// enclosing `import_declaration` ONLY when that ancestor shares the exact
// same range (which never happens — the declaration always includes the
// `import` keyword prefix — so it falls back to the import_spec itself).
// Replicate that exactly via a local ancestor walk, never from root.
const importNode = resolveImportNode(nodeMap['@import.statement']!);
if (importNode !== null) {
out.push(...splitGoImportStatement(importNode));
continue;
@ -53,23 +59,33 @@ export function emitGoScopeCaptures(
}
if (grouped['@scope.function'] !== undefined) {
const scopeCap = grouped['@scope.function']!;
// @scope.function captures function_declaration | method_declaration |
// func_literal. The original looked for a function_declaration or
// method_declaration at the captured range; the captured node IS that
// node for the first two, and a func_literal never coincides in range
// with either, so the lookup yields null for func_literal.
const scopeNode = nodeMap['@scope.function']!;
const fnNode =
findNodeAtRange(tree.rootNode, scopeCap.range, 'function_declaration') ??
findNodeAtRange(tree.rootNode, scopeCap.range, 'method_declaration');
scopeNode.type === 'function_declaration' || scopeNode.type === 'method_declaration'
? scopeNode
: null;
if (fnNode !== null) {
const receiver = synthesizeGoReceiverBinding(fnNode);
if (receiver !== null) out.push(receiver);
}
}
if (isRawMultiAssignTypeBinding(tree.rootNode, grouped)) continue;
if (isRawMultiAssignTypeBinding(nodeMap)) continue;
const declAnchor = grouped['@declaration.function'] ?? grouped['@declaration.method'];
if (declAnchor !== undefined) {
const declAnchorNode = nodeMap['@declaration.function'] ?? nodeMap['@declaration.method'];
if (declAnchorNode !== undefined) {
// @declaration.function / @declaration.method are captured directly on
// the function_declaration / method_declaration node.
const fnNode =
findNodeAtRange(tree.rootNode, declAnchor.range, 'function_declaration') ??
findNodeAtRange(tree.rootNode, declAnchor.range, 'method_declaration');
declAnchorNode.type === 'function_declaration' ||
declAnchorNode.type === 'method_declaration'
? declAnchorNode
: null;
if (fnNode !== null) {
const arity = computeGoDeclarationArity(fnNode);
if (arity.parameterCount !== undefined) {
@ -98,15 +114,15 @@ export function emitGoScopeCaptures(
continue;
}
const callAnchor =
grouped['@reference.call.free'] ??
grouped['@reference.call.member'] ??
grouped['@reference.call.constructor'];
if (callAnchor !== undefined && grouped['@reference.arity'] === undefined) {
const callNode =
findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
findNodeAtRange(tree.rootNode, callAnchor.range, 'composite_literal');
if (callNode !== null) {
// @reference.call.free / .member are captured on the call_expression;
// @reference.call.constructor on the composite_literal. The captured node
// IS the node the old findNodeAtRange re-derived for each, so use it.
const callNode =
nodeMap['@reference.call.free'] ??
nodeMap['@reference.call.member'] ??
nodeMap['@reference.call.constructor'];
if (callNode !== undefined && grouped['@reference.arity'] === undefined) {
if (callNode.type === 'call_expression' || callNode.type === 'composite_literal') {
grouped['@reference.arity'] = syntheticCapture(
'@reference.arity',
callNode,
@ -146,18 +162,53 @@ export function emitGoScopeCaptures(
return out;
}
function isRawMultiAssignTypeBinding(
rootNode: SyntaxNode,
grouped: Record<string, Capture>,
): boolean {
/**
* Resolve the node passed to `splitGoImportStatement` for an @import.statement
* match. The capture is on the `import_spec`; the original preferred an
* `import_declaration` at the SAME range, else the import_spec. An
* import_declaration always includes the `import` keyword and so never shares
* the spec's exact range the only candidate is an ancestor, and it can only
* match when ranges coincide. Walk the parent chain (bounded, local) for an
* import_declaration whose range equals the spec's; otherwise return the spec.
*/
function resolveImportNode(importSpec: SyntaxNode): SyntaxNode {
let current: SyntaxNode | null = importSpec.parent;
while (current !== null) {
if (current.type === 'import_declaration') {
if (nodeRangeEquals(current, importSpec)) return current;
break;
}
// import_spec is nested at most under import_declaration ->
// import_spec_list -> import_spec; stop once we leave the import subtree.
if (current.type !== 'import_spec_list') break;
current = current.parent;
}
return importSpec;
}
/** True iff two nodes occupy the exact same source range. */
function nodeRangeEquals(a: SyntaxNode, b: SyntaxNode): boolean {
return (
a.startPosition.row === b.startPosition.row &&
a.startPosition.column === b.startPosition.column &&
a.endPosition.row === b.endPosition.row &&
a.endPosition.column === b.endPosition.column
);
}
function isRawMultiAssignTypeBinding(nodeMap: Record<string, SyntaxNode>): boolean {
const anchor =
grouped['@type-binding.constructor'] ??
grouped['@type-binding.call-return'] ??
grouped['@type-binding.assertion'];
nodeMap['@type-binding.constructor'] ??
nodeMap['@type-binding.call-return'] ??
nodeMap['@type-binding.assertion'];
if (anchor === undefined) return false;
const node = findNodeAtRange(rootNode, anchor.range, 'short_var_declaration');
if (node === null) return false;
// Original looked up a short_var_declaration at the anchor's range; these
// tags are captured directly on the short_var_declaration (except the
// var_declaration form of @type-binding.assertion, which the old range+type
// lookup would have missed -> null -> false).
if (anchor.type !== 'short_var_declaration') return false;
const node = anchor;
const lhs = node.childForFieldName('left');
const rhs = node.childForFieldName('right');
if (lhs === null) return false;