Repository navigation
Description
Summary
Function-valued consts are indexed at module scope but not inside a function
body. In a React codebase that means most event handlers are absent from the
graph, and callers / callees / impact answer Symbol not found for them
— which reads identically to "this symbol has no callers".
The boundary is scope, not syntax: a top-level const fn = () => {} indexes
fine, and the same declaration inside a component does not. useCallback is
not involved — a bare arrow inside the component is missing too.
Reproduction
Two files, no dependencies, codegraph init:
src/widget.jsx
import { useCallback } from "react";
export function formatLabel(value) { // 1. top-level declaration
return String(value).trim();
}
export const parseLabel = (raw) => raw.split(":")[1]; // 2. top-level arrow const
export class Formatter { // 3. class + method
render(value) { return formatLabel(value); }
}
export default function Widget({ items, onPick }) {
const handleSelect = useCallback((item) => { // 4. const in a component, via hook
onPick(formatLabel(item), parseLabel(item));
}, [onPick]);
const handleClear = () => { // 5. plain const in a component
onPick(null, null);
};
return items.map((i) => (
<button key={i} onClick={() => handleSelect(i)} onDoubleClick={handleClear}>
{formatLabel(i)}
</button>
));
}src/page.jsx
import Widget, { formatLabel, parseLabel } from "./widget";
export default function Page() {
const label = formatLabel("a:b");
const parsed = parseLabel("a:b");
return <Widget items={[label, parsed]} onPick={() => {}} />;
}Result
codegraph init reports 10 nodes. Every node in the index:
file page.jsx src/page.jsx:1
import ./widget src/page.jsx:1
function Page src/page.jsx:3
file widget.jsx src/widget.jsx:1
import react src/widget.jsx:1
function formatLabel src/widget.jsx:4
function parseLabel src/widget.jsx:9
class Formatter src/widget.jsx:12
method render src/widget.jsx:13
function Widget src/widget.jsx:18
handleSelect (line 19) and handleClear (line 23) are absent.
codegraph callers formatLabel → 4 callers
codegraph callers parseLabel → 3 callers
codegraph callers Widget → 1 caller
codegraph callers handleSelect → Symbol "handleSelect" not found
codegraph callers handleClear → Symbol "handleClear" not found
Note line 9: parseLabel is an arrow function and indexes correctly. Only the
two declared inside Widget are missing.
Expected
handleSelect and handleClear are declared functions with a name, a body and
call sites, and should appear as nodes the same way parseLabel does.
Why it matters
This is the dominant shape for handlers in React, so on a real front end a
large share of the functions an agent needs are invisible. The failure is also
silent in the worst direction: Symbol not found is indistinguishable from
"nothing calls this", so an agent asked to update every call site of a handler
gets an empty answer and concludes there are none. Grep finds them, which is
the fallback CodeGraph is meant to replace.
The same gap presumably affects const x = function () {}, object-literal
methods, and IIFE-scoped helpers in plain JS/TS, though I only measured the
React case.
Environment
- codegraph 1.6.0 (npm
@colbymchenry/codegraph) - node v26.7.0
- macOS 26.4.1, arm64
Possibly related
#1656 (route callers/callees/impact through one symbol resolver) looks adjacent
— though this appears to be extraction rather than resolution, since the nodes
never reach the database at all.
Activity
inth3shadows commentedon Sep 4, 2026
Reproduced on 1.6.0 (npm latest), and narrowed to an exact boundary — it is not "function-valued consts" in general, it is arrow/function expressions bound inside a function body.
Observed nodes for that file
Store/get/set/hasouterinnertopLevelArrowhandleClickis absent. So three cases that look alike behave differently:const x = () => {}→ indexed (the documentedexport const useAuthcase)const x = function name() {}→ indexed, under the expression's own nameconst x = () => {}→ no node at allThis is why React handlers disappear: they are almost always the third form.
Cause
Naming is not the problem.
extractFunctionalready resolves an arrow's name from its parentvariable_declarator(src/extraction/tree-sitter.ts, around theexport const useAuth = () => { ... }comment) — that path is simply never reached for an in-body arrow.Function bodies are walked by
visitFunctionBody→visitForCallsAndStructure(src/extraction/tree-sitter.ts:5166). That walker dispatches oncallTypes,INSTANTIATION_KINDS,macro_invocationand friends, and firesmaybeCaptureFnRefs, but it has no branch that treats avariable_declaratorwhose value is anarrow_function/function_expressionas a function declaration.innersurvives only because it is a named function expression and is picked up as a function node in its own right.Fix direction
In
visitForCallsAndStructure, before the call/instantiation dispatch, add the declarator case and recurse into it as a function:Two things worth deciding while implementing, which is why I am not sending this as a PR:
scanFnRefSubtree(:603) already halts at nested function definitions with adepth > 12guard; the same containment probably wants to apply here so a deeply-curried factory does not explode the node count.functionor a distinct kind. An in-body arrow is lexically unreachable from outside its enclosing function, so indexing it as a plain top-level-lookingfunctioninvites exactly the cross-scope name collisions [python] matching built-ins #1230/fix(resolution): literal-receiver builtins and nested locals stop fabricating call edges #1317 fixed for nested locals. Emitting it with the enclosing function as its scope would avoid re-opening that.Environment
1.6.0 (also 1.5.0), Linux, fresh
codegraph init, verified via direct SQL against.codegraph/codegraph.db.danusha2345 commentedon Sep 4, 2026
Thanks for narrowing it — that is exactly the boundary. #1679 implements the declarator case in both walkers (wasm
visitForCallsAndStructureand the Rust kernel's mirror), so an in-bodyconst x = () => {}/= function () {}is extracted through the existingextractFunctionname resolution.On your two open points:
Widget::handleClear, acontainsedge fromWidget), the same shape theuseCallbackhandlers already have on main, so it does not reopen the [python] matching built-ins #1230/fix(resolution): literal-receiver builtins and nested locals stop fabricating call edges #1317 cross-scope collisions.fix(extraction): index const-bound functions inside a body as symbols (…
colbymchenry commentedon Sep 8, 2026
Fixed in #1774 (lands/rebases #1679 onto current main).
Cause: the JS/TS body walker only promoted nested named function decls and React hook-bound arrows (
useCallback/ …). A plainconst handleClear = () => {…}inside a component fell through, so the handler never became a symbol andcallers/impactanswered "Symbol not found".Fix: wasm + kernel treat an anonymous arrow/function expression that is the whole value of a plain-identifier
variable_declaratoras a function of the enclosing one (same namingextractFunctionalready uses at module scope). Destructuring / inline JSX arrows / non-function values unchanged.Verified on Linux:
handleClearabsent; post-fix:Widget::handleClearpresent with its own calls/containment.callers handleClear→ Widget; query lists both handlers.Re-index after upgrading.
fix(extraction): index const-bound functions inside a body as symbols (…
perf(resolution): remove the indexing regressions since v1.6.0 (vscod…