feat(c/c++): resolve function-pointer dispatch (#932) (#954)

C/C++ polymorphism is the function pointer: a struct fn-pointer field, concrete
functions registered into it through a table (`{"add", cmd_add}`), a designated
initializer (`.handler = on_open`), or an assignment, then dispatched indirectly
(`p->fn(argv)`). Static extraction captures neither the registration→field
binding nor the indirect call, so the dispatcher→handler edge was missing — git's
run_builtin looked like it called nothing, a vtable's implementations had no
callers, and the hook_demo.c in the issue was unreachable.

Add a resolution-layer synthesizer keyed by (struct type, fn-pointer field). It
reads source (the established Celery/Sidekiq/Spring pattern — C extraction has no
struct fields or indirect-call edges to build on) in passes: collect fn-pointer
typedefs, parse struct field layouts, collect registrations (positional matched
by field index, designated, and assignment), propagate field←field assignments
(so a generic hook slot reassigned from a registry — the hook_demo.c
`h->func = found->fn` shape — inherits the registry field's handlers), then link
each indirect dispatch site to the registered handlers. Receiver type resolves
from the enclosing function's params/locals, falling back to a field name unique
to one struct. Covers both the command-table idiom (git, redis) and the
ops-struct/vtable idiom (curl content-encoders, protocol handlers).

Pure edge synthesis (no node growth); high precision via the (struct, field) key.

Validated: git 502 edges (run_builtin→cmd_* plus git_hash_algo/archiver/reftable
vtables), redis 357 (dictType.hashFunction, connection + reply-object vtables),
curl 478 (Curl_cwtype.do_init → deflate/gzip/brotli/zstd); 0 non-function targets
on all three; node-stable; 0 on the lua control (its {name,fn} tables register
into the Lua VM, with no C indirect call to bridge). Full suite 1665 pass.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-22 13:34:11 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 826810f128
commit ba209d9489
7 changed files with 524 additions and 1 deletions
+1
View File
@@ -22,6 +22,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph_explore` now follows **Sidekiq** background-job dispatch in Ruby. A `DestroyUserWorker.perform_async(id)` (or `.perform_in` / `.perform_at`) call now links to that worker's `perform` method — usually in `app/workers/` away from the controller or model that enqueues it — so "what runs in the background here?" traces from the enqueue straight into the job body. Both the modern `include Sidekiq::Job` and the older `Sidekiq::Worker` are recognized, namespaced workers resolve to the right class even when several share a name (e.g. `Comments::NotifyWorker` vs `Articles::NotifyWorker`), and Rails ActiveJob's `perform_later` — a different mechanism — is intentionally left alone.
- `codegraph_explore` now follows **Laravel events** in PHP. An `event(new OrderShipped($order))` call now links to every listener that handles it — each listener's `handle()` method, usually a separate `app/Listeners/` class — so "what reacts to this event?" traces from the dispatch straight into the listener bodies. Listeners are found both ways Laravel registers them: by a typed `handle(OrderShipped $event)` (auto-discovery, including a `handle(A|B $event)` union that listens for two events) and by the `protected $listen` map in your `EventServiceProvider` (which also catches a listener whose `handle()` has no type-hint). One event fans out to all its listeners, and queued jobs — dispatched via `::dispatch()` rather than `event()` — are correctly left out.
- CodeGraph now understands **Lombok**-generated methods in Java. `@Getter`, `@Setter`, `@Data`, `@Value`, and `@Builder` generate getters, setters, `builder()`, `equals`/`hashCode`/`toString`, and the `@Slf4j` `log` field at compile time, so those methods never appear in the source — and a `user.getName()`, `User.builder()`, or `log.info(...)` call used to resolve to nothing, silently breaking call-chain analysis (the agent would conclude the method didn't exist and reconstruct it by hand). Those members are now indexed from the annotations and fields, so they appear in `codegraph search` and `codegraph_explore`/`codegraph_node`, and callers trace through them like any hand-written method. They're marked as Lombok-generated so they read as generated, not hand-written; a method you write yourself is never overridden, static fields get no accessor, and a class without Lombok is unaffected. Thanks @git87663849. (#912)
- `codegraph_explore` now follows **C and C++ function-pointer dispatch**. C does polymorphism with function pointers: a struct carries a function-pointer field, concrete functions are registered into it through a table (`static struct cmd commands[] = {{"add", cmd_add}, …}`), a designated initializer (`.handler = on_open`), or an assignment, and the code dispatches indirectly (`p->fn(argv)`). None of that was visible to analysis — the indirect call resolved to nothing, so `git`'s command runner looked like it called nothing and a vtable's implementations had no callers. CodeGraph now links the dispatch site to the registered handlers, keyed by the struct field, so "what runs when this dispatches?" traces from `p->fn(...)` into every function registered for that field. This covers the command-table idiom (git, redis) and the ops-struct/vtable idiom (curl's content-encoders, protocol handlers), including the case where a generic hook slot is reassigned from a registry (`h->func = found->fn`). It stays precise — distinct function-pointer fields don't cross-link, a plain data field is never treated as a dispatch, and a project without function-pointer dispatch is unaffected. (#932)
- `codegraph_explore` now surfaces the right code in large multi-layer projects. When you ask a backend-flow question in a repo that pairs an API server with a big frontend that mirrors the same domain words — say an `app/` admin UI sitting over an `api/` server — the server-side file that genuinely matches several of your query's terms is no longer pushed out of the results by the larger, more interconnected frontend layer. A file corroborated by two or more distinct query terms is now kept in the answer even when a denser unrelated layer would otherwise crowd it out, so "how does X read items / handle the request" returns the service or handler that does the work instead of a wall of frontend views. Single-layer projects are unaffected; set `CODEGRAPH_RANK_NO_MULTITERM=1` to revert to the previous ranking.
- Impact and blast-radius analysis for TypeScript, JavaScript, Go, Python, Rust, Ruby, C, Java, C#, PHP, Scala, Kotlin, Swift, Dart, and Pascal/Delphi now understands the readers of a constant. When you change a file-scope, package-level, module-level, or class-level constant — a config object, a lookup table, a shared constant — the other symbols in that file that read it now show up as affected, where before they were invisible (impact only followed calls, imports, and inheritance, so a constant's consumers looked like "nothing depends on this"). This makes `codegraph impact`, and the impact trail in `codegraph_explore`/`codegraph_node`, catch the "change this table, break its readers" class of change. It's on by default and adds no nodes to your graph; bundled/minified files and ambiguously-shadowed names are skipped to keep results precise. Set `CODEGRAPH_VALUE_REFS=0` to turn it off.