You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Extend indirect_call to remaining dispatch patterns (dict tables, assignments, other languages) #1566
#1565 plus the cross-file follow-up (commit cf747ab) capture a function passed by name as a call argument, same-file and cross-file (submit(fn), Thread(target=fn), map(fn, xs)). That covers the most common indirect-dispatch shape but not all of them. graphify affected (blast radius) still misses callers that reference a function through the patterns below.
All of these are detectable from the AST with the same guards already in place (resolves to a callable def, not shadowed by a param/local, single-definition cross-file), and all stay quarantined in the INFERRED indirect_call relation so strict calls queries are untouched.
Proposed slices, ranked by value vs. false-edge risk
Dispatch tables (collection literals). A function referenced as a value inside a dict, list, tuple, or set literal:
getattr by string literal.getattr(obj, "method")() where the argument is a constant string. Literal-string case only. Niche, lower priority.
Decorator-registry dispatch.@registry.register style, where the decorated function becomes a callback. Framework-specific, higher false-edge risk, deferred unless requested.
Other languages.indirect_call is Python-only today. JS/TS is the next ecosystem for callback-passing (arr.map(fn), setTimeout(fn), addEventListener("x", fn)); the cross-file resolver already supports JS/TS direct calls, so it could reuse the same path.
Plan
Start with slice 1 (dispatch tables), same test rigor: positive cases plus negatives for shadowing and non-callable values. Land it, then decide on 2 and 5. Slices 3 and 4 stay deferred unless a concrete request comes in.
Non-goals
No dataflow analysis (resolve names, not values). No influence on calls. No change to how affected traverses, only what edges feed it.
Slice 1 (dispatch tables) landed in 8288829 on v8.
A function listed as a value in a dict/list/set/tuple literal now emits an indirect_call edge:
Module-level tables (ROUTES = {"create": create_user}) attribute to the file node; function-scoped tables attribute to the enclosing function.
Same-file and cross-file (an imported handler in a table routes through the cross-file resolver).
Guards: callable-target-only, dict KEYS excluded (only values are references), and the name must not be shadowed by a param/local or rebound at module scope.
7 new tests, full suite 2717 passed. Shared the resolve-and-emit logic with the call-argument path so both stay consistent.
Remaining (still open): slice 2 (assignment/return references), slice 5 (JS/TS and other languages), and the deferred slices 3 (getattr-by-string) and 4 (decorator-registry).
Yeah, jumped on it — #1567 has slice 1 (the dispatch tables). dict/list/set/tuple at both module and function scope, built on your cf747ab guards so the callable-def + param/local shadow checks carry straight over. Added the shadow and non-callable negatives up front this time too. Suite's green.
Can take slice 2 (assignment/return refs) next if you'd like — that's the trickier one since the shadow logic inverts, so I'll be careful it doesn't reintroduce the false edges you just fixed.
Arrow-const functions (const cb = () => {}) count as callable targets. Guards mirror Python (shadowing, callable-only, keys/data excluded, inline arrows are direct defs not references, single-def god-node guard cross-file). Two cross-file fixes the JS import model surfaced: import-resolved foreign symbols now defer to the cross-file resolver, and a benign imports edge no longer suppresses the indirect_call. 9 new tests, full suite 2726 passed.
Remaining: slice 2 (assignment/return references), and the deferred slices 3 (getattr-by-string) and 4 (decorator-registry). The other tree-sitter languages could reuse this same path on request.
@safishamsi Looks like slices 1, 2, and 5 are all landed now (8288829, 311e63a, 6dc1cb0). Slices 3 (getattr-by-string) and 4 (decorator-registry) are still open but marked deferred unless there's a concrete request.
Would you want either of these picked up? Happy to take slice 3 (getattr-by-string) first since it's lower-risk/self-contained — can hold off on slice 4 (decorator-registry) given the higher false-edge risk you flagged, unless you'd rather scope that one too. Let me know and I'll open a PR.
Hi @safishamsi, slices 1, 2 and 5 have landed. I'd like to pick up the remaining two:
Slice 3 (getattr(obj, "name")): emit an indirect_call edge when the attribute name is a string literal that resolves to a known callable, using the same callable-def and shadowing guards as slice 1.
Slice 4 (decorator registry): capture functions registered through decorators like @app.route(...) and @register, linking the registry to the decorated function.
A concrete use case is Flask/Click-style apps, where most entry points are reachable only through decorators, so they look like dead code in the graph right now. Would you accept these? If so, I'll start with slice 3 as a small PR.
Activity
feat(extract): capture dispatch tables as indirect_call edges (#1566)
safishamsi commentedon Jun 30, 2026
Slice 1 (dispatch tables) landed in
8288829onv8.A function listed as a value in a dict/list/set/tuple literal now emits an
indirect_calledge:ROUTES = {"create": create_user}) attribute to the file node; function-scoped tables attribute to the enclosing function.7 new tests, full suite 2717 passed. Shared the resolve-and-emit logic with the call-argument path so both stay consistent.
Remaining (still open): slice 2 (assignment/return references), slice 5 (JS/TS and other languages), and the deferred slices 3 (getattr-by-string) and 4 (decorator-registry).
sheik-hiiobd commentedon Jun 30, 2026
Yeah, jumped on it — #1567 has slice 1 (the dispatch tables). dict/list/set/tuple at both module and function scope, built on your
cf747abguards so the callable-def + param/local shadow checks carry straight over. Added the shadow and non-callable negatives up front this time too. Suite's green.Can take slice 2 (assignment/return refs) next if you'd like — that's the trickier one since the shadow logic inverts, so I'll be careful it doesn't reintroduce the false edges you just fixed.
feat(extract): capture indirect dispatch for JS/TS (#1566 slice 5)
safishamsi commentedon Jun 30, 2026
Slice 5 (JS/TS) landed in
6dc1cb0onv8.The
indirect_callmodel now covers JavaScript and TypeScript:arr.map(fn),setTimeout(fn),el.addEventListener("x", fn))app.get("/", handler),emitter.on("e", handler), timers)const ROUTES = { create: handler },const HOOKS = [onStart, onStop], shorthand{ handler })Arrow-const functions (
const cb = () => {}) count as callable targets. Guards mirror Python (shadowing, callable-only, keys/data excluded, inline arrows are direct defs not references, single-def god-node guard cross-file). Two cross-file fixes the JS import model surfaced: import-resolved foreign symbols now defer to the cross-file resolver, and a benignimportsedge no longer suppresses theindirect_call. 9 new tests, full suite 2726 passed.Remaining: slice 2 (assignment/return references), and the deferred slices 3 (getattr-by-string) and 4 (decorator-registry). The other tree-sitter languages could reuse this same path on request.
feat(extract): capture assignment/return indirect_call edges (#1566 s…
kedarvartak commentedon Jul 1, 2026
@safishamsi Looks like slices 1, 2, and 5 are all landed now (8288829, 311e63a, 6dc1cb0). Slices 3 (getattr-by-string) and 4 (decorator-registry) are still open but marked deferred unless there's a concrete request.
Would you want either of these picked up? Happy to take slice 3 (getattr-by-string) first since it's lower-risk/self-contained — can hold off on slice 4 (decorator-registry) given the higher false-edge risk you flagged, unless you'd rather scope that one too. Let me know and I'll open a PR.
feat(extract): capture getattr(obj, "name") indirect_call edges (#1566 …
feat(extract): capture dispatch tables as indirect_call edges (Graphi…
feat(extract): capture indirect dispatch for JS/TS (Graphify-Labs#1566 …
feat(extract): capture assignment/return indirect_call edges (Graphif…
feat(extract): capture getattr(obj, "name") indirect_call edges (Grap…
iakshkhurana commentedon Oct 1, 2026
Hi @safishamsi, slices 1, 2 and 5 have landed. I'd like to pick up the remaining two:
Slice 3 (getattr(obj, "name")): emit an indirect_call edge when the attribute name is a string literal that resolves to a known callable, using the same callable-def and shadowing guards as slice 1.
Slice 4 (decorator registry): capture functions registered through decorators like @app.route(...) and @register, linking the registry to the decorated function.
A concrete use case is Flask/Click-style apps, where most entry points are reachable only through decorators, so they look like dead code in the graph right now. Would you accept these? If so, I'll start with slice 3 as a small PR.
omsenjalia commentedon Oct 18, 2026
This looks fixed on the
v8branch by commits 8fdbf501 (feat(extract): capture getattr(obj, "name") indirect_call edges (#1566 slice 3)), 311e63a7 (feat(extract): capture assignment/return indirect_call edges (#1566 slice 2)), 6dc1cb0d (feat(extract): capture indirect dispatch for JS/TS (#1566 slice 5)). Could a maintainer confirm and close?