Skip to content

Extend indirect_call to remaining dispatch patterns (dict tables, assignments, other languages) #1566

Open

Description

@safishamsi

Context

#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

  1. Dispatch tables (collection literals). A function referenced as a value inside a dict, list, tuple, or set literal:

    ROUTES = {"create": create_user, "delete": delete_user}
    HANDLERS = [on_start, on_stop]

    Very common registry idiom. Same guards as the argument case. Low risk, highest payoff.

  2. Assignment and return references. A function bound to a name and used later, or returned from a factory:

    cb = handler          # cb() called later
    def make(): return handler

    Medium risk: the shadow logic has to be inverted carefully (here the local binding is the reference we want), so it must not re-introduce the false edges feat(extract): capture indirect dispatch as indirect_call edges #1565 fixed.

  3. getattr by string literal. getattr(obj, "method")() where the argument is a constant string. Literal-string case only. Niche, lower priority.

  4. Decorator-registry dispatch. @registry.register style, where the decorated function becomes a callback. Framework-specific, higher false-edge risk, deferred unless requested.

  5. 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.

Activity

  1. safishamsi commented on Jun 30, 2026

    @safishamsi
    MemberAuthor

    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).

  2. sheik-hiiobd commented on Jun 30, 2026

    @sheik-hiiobd
    Contributor

    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.

  3. safishamsi commented on Jun 30, 2026

    @safishamsi
    MemberAuthor

    Slice 5 (JS/TS) landed in 6dc1cb0 on v8.

    The indirect_call model now covers JavaScript and TypeScript:

    • callbacks passed by name as call arguments (arr.map(fn), setTimeout(fn), el.addEventListener("x", fn))
    • module-level callback registration, which is idiomatic in JS (Express app.get("/", handler), emitter.on("e", handler), timers)
    • object/array dispatch tables (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 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.

  4. kedarvartak commented on Jul 1, 2026

    @kedarvartak

    @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.

  5. iakshkhurana commented on Oct 1, 2026

    @iakshkhurana

    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.

  6. omsenjalia commented on Oct 18, 2026

    @omsenjalia
    Contributor

    This looks fixed on the v8 branch 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions