fix(extraction): index const-bound functions inside a body as symbols (#1669) (#1774)

`const handleClear = () => {…}` inside a component — every React handler
that skips useCallback — was never a symbol: the body walker only named
nested function declarations and hook-bound arrows, so the handler was
absent from callers/impact ("Symbol not found", indistinguishable from
"no callers") and its calls attributed to the component. Bind the arrow
or function expression to its declarator the way module scope already
does, in both the wasm walker and the kernel.

A navigation such a handler makes is now the handler's own edge and a hop
in the Screens `via` chain — the shape a useCallback handler already has —
so the react-router and expo-router expectations follow that convention.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 10:07:46 -05:00
committed by GitHub
co-authored by danusha2345 Colby McHenry
parent 85550eb2ce
commit 8733c2880f
7 changed files with 173 additions and 7 deletions
+34
View File
@@ -5325,6 +5325,28 @@ export class TreeSitterExtractor {
targets.add(target);
}
/**
* Whether an anonymous function is the whole value of a `variable_declarator`
* with a plain identifier name `const NAME = () => {…}` / `= function () {…}`.
* JS-family only.
*/
private declaratorBoundFunction(node: SyntaxNode): boolean {
if (
this.language !== 'typescript' &&
this.language !== 'javascript' &&
this.language !== 'tsx' &&
this.language !== 'jsx'
) {
return false;
}
if (node.type !== 'arrow_function' && node.type !== 'function_expression') return false;
const declarator = node.parent;
if (!declarator || declarator.type !== 'variable_declarator') return false;
const value = getChildByField(declarator, 'value');
if (!value || value.startIndex !== node.startIndex || value.endIndex !== node.endIndex) return false;
return getChildByField(declarator, 'name')?.type === 'identifier';
}
/**
* The property a CommonJS export assignment binds a function to
* `exports.NAME = <node>` or `module.exports.NAME = <node>` or null for
@@ -5519,6 +5541,18 @@ export class TreeSitterExtractor {
this.extractFunction(node, hookBound);
return;
}
// `const handleClear = () => {…}` inside a body (#1669) — the same
// binding that names a function at module scope names one here, and in
// a React component it is how every handler that skips `useCallback`
// is written. Without a node the handler is absent from callers /
// impact ("Symbol not found" reads like "no callers") and its calls
// attribute to the component. extractFunction resolves the name from
// the declarator; a destructuring or otherwise unnamed binding stays
// anonymous and falls through.
if (this.declaratorBoundFunction(node)) {
this.extractFunction(node);
return;
}
}
// Extract structural nodes found inside function bodies.