Commit Graph
56 Commits
Author SHA1 Message Date
Colby MchenryandGitHub 41c10750e0 fix(erlang): give same-name different-arity functions separate arity-qualified nodes (#1610) (#1615)
Fixes #1610. Also fixes #1358 (the `<<binary>>` arity miscount in behaviour dispatch, reported separately and hit by the same code path).

## Problem

Arity is part of an Erlang function's identity — `f/1` and `f/2` are unrelated top-level definitions — but the extractor merged consecutive same-name `fun_decl`s regardless of arity. Reproduced on main exactly as reported:

- adjacent `f(X) -> …. f(X, Y) -> ….` → **one** node spanning both, with the first definition's signature;
- interleaved `f/1, g/0, f/2` → two nodes with **identical** `qualified_name`;
- `cowboy_req`'s `header(Name, Req) -> header(Name, Req, undefined).` → a **self-loop** `header → header`, with the `-spec` for `/3` swallowed by the merged span;
- `-export([f/1])` marked every arity exported.

## Fix

- **One node per (name, arity).** Clauses of the same name+arity still merge (that part of the old behavior was correct); a different arity starts a new node. `qualifiedName` carries the canonical spelling — `mod::f/1` — while the node **name stays bare** so search and bare-name matching are unchanged.
- **`-export` and `-spec` are per-arity.** `-export([f/1])` exports exactly `f/1`; a spec sitting between two arities attaches to the arity its signature names.
- **Refs carry the call-site arity** wherever it's statically known: local `f/1`, remote `mod::f/2`, `fun f/1` / `fun mod:f/1` values, `gen_server` dispatch (`handle_call/3`, `handle_cast/2`), and spawn/apply MFA lists (`spawn_link(?MODULE, work, [A, B])` → `work/2`).
- **The matcher resolves only to the named arity** — same file first (a local call targets its own module) — and when no definition of that arity exists it resolves to **nothing** rather than a sibling arity: silent beats wrong. An arity-less dynamic-MFA ref resolves only when the module defines exactly one arity of that name.
- **Behaviour dispatch** selects the implementer node of the site's arity, and the arity counter now skips `<<1,2,3>>` binary-literal commas per its own docstring (#1358) — `Mod:decode(<<1,2,3>>, Opts)` counts 2, not 4.
- **`codegraph_explore` / `codegraph_node`** accept the written `mod:fn/3` spelling against the new arity-qualified names (the issue's measured `cowboy_stream_h:request_process/3` shape).

## Validation

Minimal fixtures (all three reported shapes) now index as `gap::f/1` + `gap::f/2`, distinct `inter::f/1`/`inter::f/2`, and a real `deleg::header/2 → deleg::header/3` edge with no self-loop.

Cowboy (fresh `--depth 1` clone, this build vs unmodified main build):

| | main | this PR |
|---|---|---|
| nodes | 3,668 | 3,748 (+80 — the arity splits; no explosion) |
| erlang function nodes | 2,850 | 2,930 |
| behaviour dispatch edges | 38 | **44** |
| `cowboy_req::header` | one node, span 420–425, /3's spec lost | `header/2` (420–421, its own spec) + `header/3` (424–425, its spec) |
| delegation | self-loop | `header/2 → header/3` |

`calls` edges drop 6,059 → 5,656: a sample of every removed pair shows the false-positive class the issue predicted — out-of-repo/BIF calls (`length/1`, `error/1`, `quicer:*`) that previously name-matched onto unrelated same-named in-repo functions now stay unresolved.

Tests: new arity coverage in extraction + a new arity-resolution integration suite + a #1358 binary-literal behaviour test; updated existing Erlang expectations to the arity-carrying spellings. Full suite: **3,018 passed, 0 failed**.

No migration: an existing Erlang index picks the new shape up on its next re-index (`codegraph sync` / re-`init`).

Erlang is wasm-only (not in the native kernel), so there is no kernel-parity surface.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
2026-08-26 10:39:15 -05:00
Colby MchenryandGitHub 278a8edc35 fix(resolution): resolve calls to object-literal namespace members (#1573) (#1597)
Fixes #1573. Thanks @IAliceBobI — the report had the root cause exactly right, and the fix sits one layer up from the suggested spot (resolution rather than the container-kind set), for the reason below.

## What was wrong

Methods of an exported object-literal constant — `export const api = { call() {…}, get: () => {…} }` used as a module's API surface — never received a call edge from `api.call()`, same-file or through an import. The members are extracted as plain functions with **bare** qualified names (`call`, not `api::call`) sitting inside the constant's source extent, so:

- the `Container::member` lookup the class-shaped kinds use (#825) bails on kind `constant`, and even with `constant` added to that set there is no `api::call` to find;
- the declared-type inference for imported singleton instances (#1292) finds no type in a literal and falls back to the constant edge;
- the same-file strategies only consider classes and `method` kinds, so the call resolved to nothing at all.

Net effect: `callers` / impact reported zero for methods called from everywhere, with no boundary warning because nothing about `obj.method()` looks dynamic.

## What this does

Adds one helper that resolves a member **by containment** — a node named `member` whose source range lies inside the value's range, in the value's own file — and uses it from both halves:

- **Import path**: when the imported value is a constant/variable, the literal member is tried right after the `Container::member` lookup and before the #1292 instance inference, so the cross-file edge lands on the method instead of the constant.
- **Same-file path**: a same-file constant/variable receiver (TS/JS family only) is checked before the class-name strategies.

Precision rules, all tested: calls accept callable kinds only; a declaration nested inside another member's body is not a member; nothing outside the value's range can donate a match — a same-named top-level function, or a method returned by a factory the value merely holds — so those cases keep today's behavior rather than guessing. Class statics (`C.s()`) and non-literal values are untouched.

Extraction and qualified names are deliberately left alone: changing how literal members are named would have to be mirrored in the native kernel byte-for-byte, and the resolver-side lookup is contained and language-gated.

## Tests

- The issue's repro end-to-end: `sameFileCallers` and `crossFileCaller` are both callers of `m`; a decoy `m` in a third file gets none; the `C.s()` static control resolves exactly as before; `crossFileCaller` no longer has a `calls` edge to the constant.
- Arrow-property and method members both resolve; a `function call()` nested inside `get`'s body is never taken for `api.call()`.
- A value holding a factory's result (`const obj = makeObj()`) with a same-named top-level `m` in the file: no false attribution, existing behavior kept.
- The two positive tests fail on `main`; the control passes both ways, as a guard should.
- Full suite: 189 files, 3181 passed / 9 skipped.

With the built CLI on the issue's `a.ts`/`b.ts`: `codegraph callers m` → 2 callers (`sameFileCallers`, `crossFileCaller`); `callers s` unchanged; edges `sameFileCallers -> m` (0.85) and `crossFileCaller -> m` (import, 0.9), none to `obj`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
2026-08-26 10:38:13 -05:00
Colby MchenryandGitHub 7963672689 fix(rust): resolve self.field.method() on the field's declared type instead of a same-named method (#1585) (#1599)
Fixes #1585. **Stacked on #1596** (the base branch is `fix/1588-rust-impl-type-qualification`; this PR's own diff is the second commit). Merge #1596 first, then retarget/merge this one.

## What was wrong

```rust
impl Outer {
    pub fn run(&mut self) {
        self.inner.run();      // inner: Inner
    }
}
```

produced `Outer::run -> Outer::run` — recursion the source doesn't contain. The extractor collapsed every `self.<field>.<method>()` receiver to the bare method name (`run`), so the resolver only ever saw `run` and exact-matched the nearest same-named method — the calling method itself, or a method of an unrelated type. Nothing marked the edge as a guess, and no row stayed in `unresolved_refs`, so a consumer had no way to tell.

The same happened when the field's type isn't a project type at all (`its: std::vec::IntoIter<_>` → `self.its.next()`, `matcher: Regex` → `self.matcher.is_match()`): the bare `next` / `is_match` attached to whatever local method shared the name. ripgrep had 279 self-edges on `main`; the issue lists three sites, all of this shape.

(The issue's C++ control — "`Outer::run -> Inner::run` resolves correctly" — doesn't actually hold on `main`: `inner.h` is classified as C by the `.h` heuristic, so `Inner::run` never exists and the C++ repro self-edges too. That's #1592, fixed separately.)

## What this does

Rust struct fields are not graph nodes, so the field's type can only come from the struct's declaration text. This follows the Go 2-hop precedent exactly (`matchGoFieldChainCall`, #1276), including its exclusivity rule:

1. **Extraction (TS walker + native kernel, identical, parity-tested):** a call whose receiver is `self.<field>` keeps the owner-field shape — `self.inner.run()` is emitted as `self.inner.run`. Deeper chains (`self.a.b.m()`), call receivers (`self.f().m()`), parenthesized receivers and bare `self` keep the bare name, exactly as before.
2. **Resolution (`matchRustSelfFieldCall`):** owner type = the calling method's qualified-name prefix (`Outer::run` → `Outer`); the field's declared type is read from the owner struct's **own declaration lines** (comment-stripped, line by line — same discipline as the Go helper); the method is resolved **and validated** on that type by `resolveMethodOnType` (confidence 0.85, `instance-method`).
3. **Exclusive:** when the field is declared with an external type, a generic parameter (`T`), a container that doesn't auto-deref (`Option`/`Vec`/`Mutex`/…), or can't be found, the ref **stays unresolved** — it never falls through to the bare-name strategies. That is the safe behaviour the issue asks for, and it is what #1276 already chose for Go.

`rustFieldTypeName` looks through exactly the layers Rust's method-call auto-deref looks through: references (`&`, `&'a mut`) and the owning smart pointers `Box`/`Rc`/`Arc`. `Box<dyn Source>` yields the trait, whose method node the interface-impl synthesizer then fans out to every implementation. `Option<Inner>` is left alone — `self.inner.take()` is Option's method and must not become `Inner::take`.

Why it stacks on #1596: the owner is taken from the method's qualified name, which for a generic/lifetime impl was the trait's name before that fix.

## Measured on ripgrep (110 `.rs` files, #1596 build vs this branch)

| | #1596 | this PR |
|---|---|---|
| nodes | 4029 | 4029 |
| `calls` self-edges | 279 | **146** (none of the `self.<field>` shape remain — 116 bare-receiver, 30 other dotted) |
| `self.<field>.m()` calls resolved through a validated field type | — | **292** (`DecompressionMatcher::command -> GlobSet::matches`, `Parser::find_long -> FlagMap::find`, `Haystack::path -> DirEntry::path`, …) |
| `self.<field>.m()` calls left unresolved | — | **417** — every sampled one is a std/container method: `self.commands.push`, `self.child.wait`, `self.pre.is_some`, `self.colors.clone`, `self.path_terminator.unwrap_or` |
| `calls` edges total | 9150 | 8878 (the 272 removed are the former bare-name guesses for those 417) |

The issue's three sites: `walk.rs:824` now resolves to `IgnoreBuilder::add_custom_ignore_filename` (was a self-edge); `walk.rs:1195` (`self.its.next`, `IntoIter`) and `globset/lib.rs:983` (`self.matcher.is_match`, `Regex`) are parked as unresolved instead of guessed.

The issue's repro gives `Outer::run -> Inner::run` (`instance-method`, confidence 0.85) on both the kernel path and `CODEGRAPH_KERNEL=0`.

## Tests

- `__tests__/extraction.test.ts`: only the single-hop `self.<field>.<method>()` call keeps the prefix; deeper / call / parenthesized / bare-`self` receivers and a local receiver are unchanged.
- `__tests__/resolution.test.ts` (end-to-end, Cargo layout): the issue's repro → `Outer::run -> Inner::run`, no self-edge; an external field type (`std::vec::IntoIter`) with a local `next` decoy → no edge at all; `Box<Inner>` and `&'a mut Inner` resolve, `Option<Inner>` does not (even though `Inner` declares the method); a generic `T` field → no edge; genuine `self.run()` recursion keeps its self-edge; the #1588 repro's `UsesFile::go` / `UsesBuf::go` resolve to `FileSource::read` / `BufSource::read`, and a `Box<dyn Source>` field lands on `Source::read` with the synthesizer fanning out to both impls.
- `__tests__/fixtures/kernel-parity/torture.rs` grows the receiver shapes; all 15 kernel parity suites pass against the rebuilt kernel (147 tests).
- Full `npm test` on this branch: 189 files, 3187 passed, 9 skipped, 0 failed.

Re-index after upgrading.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
2026-08-26 10:37:50 -05:00
ctype_lab e922563e05 fix(resolution): recognize union instantiation 2026-08-06 17:11:26 +09:00
ctype_lab 8e3cde6994 feat(kernel): preserve union nodes across extraction 2026-08-06 17:11:26 +09:00
38580e0b04 fix(python): bare class references produce references edges to classes (#1478) (#1493)
Python's class-as-value idioms (return SomeClass, x = SomeClass, registry
dicts, classes passed as arguments) produced no references edges, so
callers/impact on a Django/DRF serializer missed the views that consume it.
Three gates dropped them:

- return_statement was never dispatched by PYTHON_SPEC (kernel mirrored)
- the extraction gate (definedHere) collected function/method names only
- resolution accepted function/method targets only (matchFunctionRef +
  the function_ref import fast path)

Capture return_statement for Python (single expression; tuple returns not
descended), admit same-file CLASS names to the gate, and accept class
targets for Python bare identifiers — scoped to Python so the TS/JS KIND
FILTER contract is untouched. The docopt false-positive mechanism behind
the function-only rule (lowercase locals vs same-named methods) doesn't
transfer: methods stay excluded for bare ids, and the same-file/import
gate + unique-or-drop rules still apply.

Probed on django-rest-framework (~250 files): 559 new references→class
edges, 10/10 sampled genuine (serializer_class = AuthTokenSerializer, the
ModelSerializer field-mapping registry, aliases, ctor args, isinstance).
EXTRACTION_VERSION 24 → 25.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 01:36:07 -05:00
157c8e735d perf(resolution): generation-tagged supertype memo + method owner index — Swift compiler 185→98s, byte-identical (#1395)
The swiftc =2/nm:mc-* attribution located the wall: the
getSupertypes conformance walk ran 971,200 times (565s of combined
worker time, 581µs each) — every resolveMethodOnType miss re-queried
implements/extends edges for every same-named type node, recursing
depth-4 through Swift's protocol landscape with no memoization, and
post-inference resolveMethodOnType averaged 1,912µs per call.

Fix 1 — generation-tagged getSupertypes memo. Supertype edges GROW
during the resolution loop (batch k persists its edges BEFORE batch
k+1 fans out — the #1320 ordering), so a plain cache would freeze an
early batch's emptier answer. Within a batch the edge state is fixed
by that same ordering, so memo entries carry a generation that
advances at every batch entry point (resolveBatchYielding /
resolveListForAdmission — covering the sequential loop, pool workers,
sync admission, and the conformance pass); a stale-gen entry
recomputes. Behavior-identical to no memo at every point in time;
walk invocation counts match the unmemoized run exactly (971,200 /
24,336 / 76,415).

Fix 2 — per-(language, method-name) owner index in getMethodMatches:
candidates bucket once by their qualifiedName's last two segments
(exactly the span the match predicate tests), so a (type, method)
query is a map lookup instead of an O(candidates) scan per
methodMatchCache miss. ObjC selectors and multi-segment typeNames
keep the legacy linear path. Also ships nm:mc-rmot / nm:rmot-supers
=2 attribution rows.

swiftc: settle 100.2→31.3s, resolveMethodOnType 1,912→202µs, wall
183.5→97.8s (was 185s at the head-to-head; cbm's same-box number is
119.1s). Gates: swiftc old-vs-new dump byte-identical (1,837,235
rows), swiftc pooled-vs-CODEGRAPH_NO_PARALLEL_RESOLVE=1 identical
(the generation-semantics risk surface), dubbo old-vs-new identical
(49k Java instance-method hits share both paths), Alamofire
identical; suite 2,689 ×2 with CODEGRAPH_KERNEL_EXPECT=1.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 10:56:40 -05:00
974e6c8b95 perf(resolution): incremental receiver-inference scan memo + compiled-pattern memo — kong −8% more (−23% cumulative), byte-identical (#1392)
The kong/tokio matcher-chain residue attributed (nm:mc-* sub-stage rows,
shipped here too): matchMethodCall's cost is ~entirely
inferLocalReceiverType — 61µs per miss on kong, 99% miss rate (39k `self:`
calls hunting a local declaration Lua never writes), re-scanning the same
scope lines for every ref.

Two pure memos, both semantics-preserving by construction:
- Compiled-pattern memo: localReceiverTypePatterns/phpPropertyTypePatterns
  built 2-4 fresh RegExp objects per call; patterns are a pure function of
  (language, receiver) and non-global, so instances are shared via a
  FIFO-capped map (no per-get mutation — the §7a.6 LRU-churn lesson).
- Incremental scan memo: refs for the same (file, scope, receiver) arrive
  in ~ascending line order and the backward declaration scan is a pure
  function of immutable file lines — a per-context watermark scans each
  line once per key (query(c) = highest match in [start..c]; monotonic
  calls extend the watermark over (hi..c]; non-monotonic calls fall back
  to the plain bounded scan). componentScoped (CFML/PHP whole-file sweep)
  is keyed out. States drop with the context's file caches via
  clearNameMatcherMemos, wired into ReferenceResolver.clearCaches.

kong mc-infer misses 61→20µs (2.4s→0.8s combined); fresh index 3.43 →
3.03-3.20s (−8%; 4.07 → 3.14 cumulative with #1391). tokio unchanged
(tight scopes). Gates: dubbo (49k Java instance-method HITS ride this
scan), kong, tokio, Fusion dumps all byte-identical; suite 2,689 ×2 with
CODEGRAPH_KERNEL_EXPECT=1.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 00:20:03 -05:00
082ea65f3a perf(synthesis): provably-empty pass gates + prefilters — render/expo/rn/mybatis stop scanning repos they can't match; iface memo (#1389)
Store-arc round 2 (#1388 follow-up). The synthesis pool barrier on dubbo
carried ~1.4s of passes that provably could not emit an edge for the
project: reactRenderEdges fanned out over every class before checking for
a render method (now: one indexed name lookup bounds candidates — not a
language gate, Java Litho-style render+setState still matches);
expo/rn cross-platform pairing streamed every method row without the
languages their edges require (now registry-gated: expo needs swift AND
kotlin file-languages, rn needs a JS-family caller for isBridge);
mybatis built its full java-method index before discovering there were no
mapper-XML methods (now collects the XML side first). ifaceEdges — real
work — stops re-fetching a hub interface's methods once per implementer
and skips supertype-less classes before any per-class lookup.

dubbo warm wall 8.49-8.79 → 8.14-8.24s (n=3/arm, caffeinated); barrier
784→435ms; the full removed pass work lands on low-core envelopes where
synthesis runs sequentially. Dumps byte-identical: dubbo old-vs-new,
pooled-vs-sequential, kernel-vs-wasm (441,270 rows) + excalidraw JSX-live
control (89,903 rows, 46 react-render edges reproduced). Suite 2,689 ×2
with CODEGRAPH_KERNEL_EXPECT=1.

Also ships the diagnostics that located the round (zero cost when off):
CODEGRAPH_RESOLVE_PROFILE=2 attributes per-ref time to resolveOne's
strategies (stage:*) and the name-matcher's sub-matchers (nm:*);
CODEGRAPH_SYNTH_TIMINGS now prints the store worker's decode-vs-SQL
split. Killed by measurement, recorded in the PR: import-failure negative
cache (both-outcome names exist — static imports resolve via
instance-method on jvm-miss), jvm-miss early return (1,939 later-strategy
edges), jsxEdges language gate (Java generics text produces jsx edges),
and §4d buffer→bind on Spring repos (extract() hook forces the decoded
path — kernel=0 bundles measured).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 21:15:58 -05:00
c472cfb52e fix(resolution): literal-receiver builtins and nested locals stop fabricating call edges (#1317)
", ".join(sorted(x)) resolved by bare name to a project function named
join — one nested inside a DIFFERENT function, so scope alone rules the
edge out. Both defects from #1230, fixed independently:

1. Extraction: a member call on a LITERAL receiver (string, number,
   collection, regex — across grammars) emits no call ref at all. A
   literal's methods are the language's builtins, never project
   symbols; the bare-name fallback let them exact-match any same-named
   project function. Silent miss, never a wrong edge.

2. Resolution: matchByExactName filters out candidates nested inside a
   same-file FUNCTION container unless the ref originates within that
   container's line range. Class members (parent is a class-like node),
   top-level symbols, and C++ namespace prefixes (no parent node) are
   untouched.

requests re-index: byte-identical (813 calls edges). excalidraw: -27
edges, all literal-receiver refs by construction. The issue's repro is
pinned: join has exactly one caller (format_fields), report_missing
has zero project callees.

Fixes #1230

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:55:38 -05:00
41c2029798 fix(go): field-chain calls resolve via validated type inference, never bare-name guessing (#1316)
target.conn.Exec("insert") with `conn *sql.DB` emitted a BARE `Exec`
ref (the receiver chain was dropped for non-identifier receivers), and
exact-match then bound it to the only local `Exec` — an unrelated
interface's method — fabricating an internal dependency (#1276).

Extraction now keeps Go 2-hop selector chains (`base.field.Method`),
and a dedicated matcher resolves them EXCLUSIVELY via two inference
hops: base's type from the enclosing scope (#1108 machinery), field's
declared type from the struct's own declaration lines (comment-
stripped, per-line — chi's "the tree router" doc comment otherwise
donates a phantom type). resolveMethodOnType validates the target.
Package-qualified field types are followed only when the package is
in-module — `handler http.Handler` must not bind a same-named local
decoy. Failure at any hop leaves the ref unresolved: chained Go
receivers never fall through to the bare-name strategies (they were
never emitted before, so no prior recall depends on that path).

chi before/after: node count stable (1,181); 8 correct field-chain
edges gained (mx.tree.FindRoute/InsertRoute/routes, validated,
including the unexported `node` type); the removed edges are the
prior bare-name guesses on external receivers.

Fixes #1276

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:49:24 -05:00
2ec877b08c fix(resolution): calls through an imported singleton resolve to the method (#1315)
reproStore.notifyJoinGuildStatus() after `import { reproStore }`
resolved its calls edge to the exported CONSTANT (resolvedBy:'import'),
while the identical same-file call resolved to the method via
local-variable receiver inference (#1108) — so `callers <method>`
missed every cross-file use and a widely-used method could look
unused (#1292).

resolveViaImport's member-descend now handles imported VALUES alongside
the #825 static-member case: when the base resolves to a
constant/variable, the value's type is inferred from ITS OWN
declaration lines in the exporting file (the shared #1108 pattern
table: `= new T(...)` initializers and type annotations) and the member
is resolved AND VALIDATED on that type via resolveMethodOnType. A
failed inference or validation keeps the existing constant edge —
never a fabricated one. Calls only; plain member reads still reference
the value.

excalidraw control: byte-identical graph (10,653 nodes / 19,483 calls
edges before and after).

Fixes #1292

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:36:33 -05:00
e871c49a31 fix(resolution): clean up processed refs by row id so batch boundaries can't drop sibling call sites (#1269) (#1270)
Post-batch cleanup deleted resolved refs (and parked failed ones) by
(from_node_id, reference_name, reference_kind) — no line/col. When one
caller had several call sites to the same callee and a batch boundary
split them, the first batch's cleanup removed every row with that key,
including later-batch siblings that were never attempted — their edges
were silently never created. On nlohmann/json this ate 422 real call
edges (write_cbor's 38 to_char_type calls indexed as 11).

Refs loaded from unresolved_refs now carry their row id through
resolution, and all three persist paths (sync resolveAndPersist, the
yielding retry pass, the batched drain loop) delete / mark-failed by
exactly that id. The key-tuple methods remain only as the fallback for
hand-built refs from the public API. Failed-parking gains the same
precision: outcome can differ per call site (receiver inference reads
the ref's line), so a sibling must not inherit another row's failure.

Also untracks the zz-scratch local test files that slipped into #1268
and gitignores the pattern.

Validation: red-green regression test (5 sites, batch size 2 — old code
kept 2 edges, fix keeps 5); nlohmann/json re-index is a strict superset
of the previous edge set (0 lost, 422 recovered, spot-checked against
source).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:09:03 -05:00
6103f5e228 fix(cpp): resolve explicit operator calls (a.operator+(b)) to the operator method (#1268)
* fix(cpp): resolve explicit operator calls (a.operator+(b)) to the operator method (#1247)

tree-sitter-cpp can't parse an operator_name in field position: the
call_expression carries `function: <receiver>` plus an ERROR child
wrapping the operator_name instead of a field_expression callee, so the
extractor emitted a calls ref named just the receiver (`a`) and the edge
never resolved — while the operator method itself indexed fine.

Two-part fix, scoped to the explicit call form (infix `a + b` / `a[i]`
need receiver type inference and are tracked in #1258):

- extraction: recover the operator_name from the ERROR child and emit
  `<receiver>.operator+` (`->` receivers normalized, `this->` emits the
  bare name), like any other member call
- resolution: matchMethodCall's dot pattern now admits an operator
  method part (cpp-gated; symbol chars failed the \w match), so
  receiver-type inference + resolveMethodOnType validate the target —
  a same-named operator on an unrelated class can't capture the edge

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cpp): harden explicit operator-call recovery against real-world shapes (#1247)

Validated on nlohmann/json (dozens of explicit operator[] / operator* /
operator< call sites). Two refinements the synthetic fixtures missed:

- normalize spaced call-site operator names (`it.operator * ()`,
  `other.operator < (*this)`) to the compact form definitions index as
- drop the ref for a complex receiver (`obj()->operator+`, member chains
  ending in a call) instead of emitting a bare operator name: exact-name
  fallback GUESSED among unrelated same-named operators (linked a
  std::map operator[] call to an in-repo operator[]) — silent miss,
  never a wrong edge

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:56:35 -05:00
70b1be6a21 fix(php): resolve method calls through $this-> properties on their declared type (#1220) (#1251)
Carries #1221 by @w0lan plus a hardening pass: property-receiver typing consults property-shaped declarations only (typed property / promoted ctor param / pseudoconstructor assignment / assignment-followed classic ctor and typed setter), so same-named locals and parameters can never mistype a property.

Co-authored-by: Roman Wolan <roman.wolan@morizon-gratka.pl>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:47:57 -05:00
625b4fe921 fix(resolution): stop per-call config-key scan that made large Java/Kotlin (Spring) indexes take ~1h (#1180) (#1210)
On a large Java/Kotlin Spring monorepo, reference resolution — not extraction —
dominated a full index (Spring Boot's ~9,650-file tree: extraction 62s,
resolution ~26min). The Spring framework resolver ran an uncached
getNodesByKind('constant') full scan + canonicalConfigKey() filter for EVERY
dotted `calls` ref (every list.add(), builder.build(), receiver.method()),
because the config-key branch gated only on "dotted java/kotlin", not on ref
kind. With ~1,100 constant nodes × ~200k dotted calls that is ~200M wasted
row-fetches/allocations.

Fixes, one theme — config-key constants bind config `references`, never `calls`:
- frameworks/java.ts: gate the Spring config-key branch on
  referenceKind === 'references' (what @Value/@ConfigurationProperties emit) so
  the `calls` flood skips the scan.
- name-matcher.ts: a `calls` ref no longer resolves to a yaml/properties config
  node via matchByQualifiedName (service.process() vs the yaml key
  service.process) — a wrong edge that also hid the real callee; it now falls
  through to method resolution.
- resolution/index.ts: cache getNodesByKind in the resolver context (same
  lifetime as nameCache). Fixes the same uncached-per-ref scan in the Drupal
  hook_ resolver and is defense-in-depth for the Spring :prefix branch.

Measured (Spring Boot): resolution 269s→16.5s on a 4.3k-file module (16×) and
~26min→44.7s on the full 9.6k-file tree (~35×); graph byte-identical, full suite
passes. Adds a regression test (same key, two ref kinds, opposite outcomes).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:17:16 -05:00
99152212a9 feat(extraction): add ArkTS language support with ArkUI dispatch bridges (#396, #512, #890 via #648) (#1186)
Adds ArkTS (.ets, HarmonyOS/OpenHarmony) as a first-class language:
full TypeScript-grade extraction via the harmony-contrib tree-sitter
grammar (MIT, vendored byte-identical from the tree-sitter-arkts 0.2.0
npm tarball), plus the ArkUI constructs that make HarmonyOS apps
traceable:

- @Component/@ComponentV2 structs with decorators from both grammar
  positions; members extract as class members with qualified names.
- build() component trees: child instantiation edges via
  arkui_component_expression, no synthesizer needed.
- Attribute chains emitted dot-prefixed and resolved ONLY against
  @Extend/@Styles/@AnimatableExtend/@Builder helpers (unique-or-drop) —
  bare-name fallthrough produced 36,840 wrong edges (17% of calls) on
  the OpenHarmony samples monorepo. All four grammar chain shapes
  handled, including the detached-chain forms.
- .onClick(this.handler) method-reference bindings.
- ohpm workspace modules: bare imports follow oh-package.json5 file:
  deps (ambiguous names dropped), honoring each module's main entry —
  which also lets .ts consumers resolve .ets modules.
- ArkUI dynamic-dispatch bridges, all provenance:'heuristic' with
  wiring-site metadata: assignment-gated state->build() re-render
  (V1 @State family + V2 @Local/@Provider/@Consumer),
  @ohos.events.emitter emit->subscriber pairing on static event keys
  (numeric ids same-file, named constants same-module, fan-out capped),
  and router.pushUrl literal urls -> the target page's @Entry struct.
- $r/$rawfile resource intrinsics treated as built-ins; arkts joins the
  web language family, value-reference edges, re-export chase, and the
  other TS-applicable gates.

Also ships a language-agnostic index-completeness guard: indexAll
stamps index_state (indexing -> complete/partial/failed), reconciles
discovered vs accounted files (a loaded run silently dropped 37 files),
and codegraph status surfaces truncated/partial indexes in human and
--json output.

Validated on HarmoneyOpenEye (82 files), CoolMallArkTS (528, modular
ohpm + ArkUI V2), and openharmony/applications_app_samples (11,693
files, 202,890 nodes stable across re-index, attribute false-positive
audit 36,840 -> 588 residual all-plausible). Supersedes PRs #656 and
#988 with credit — both informed this implementation.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:07:15 -05:00
a0208feaac feat(extraction): index Erlang escripts and OTP app resource files (#635, #648) (#1169)
escripts (.escript) index like any module — the ELP grammar has a
first-class shebang node, so no source transform is needed; main/1 and its
helpers get full function/call extraction.

OTP application resource files (<app>.app.src and compiled <app>.app) join
the graph as Erlang terms the grammar parses natively. They route by full
suffix (their last-dot extension, .src, is far too generic for the
extension map). The application tuple yields structure: {mod, {Mod, _}}
links the app to its callback module — the app's entry point — and
{applications, [...]} / {included_applications, [...]} connect umbrella
sibling apps, resolving through the OTP app-name == module-name convention;
kernel/stdlib and other out-of-repo apps stay unresolved.

App-file refs resolve only ever to MODULES: validation on emqx caught the
ssl OTP-app dependency resolving to a test helper FUNCTION named ssl (the
same defect class as the earlier -behaviour gate), so the matchReference
module-only gate now covers every ref an .app/.app.src file emits.

Validated on emqx: 2 app.src + 6 escripts indexed, entry-module and
umbrella-dependency edges all namespace-targeted post-gate, escript
functions extracted; a stray legacy/module.src stays unknown.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:32:59 -05:00
6511722250 feat(extraction): add Erlang language support (.erl/.hrl) (#635, #648) (#1165)
Vendored WhatsApp/tree-sitter-erlang 0.19 (the ELP grammar, ABI 14) with an
Erlang-shaped extractor: multi-clause/multi-arity functions merged into one
symbol, -spec signatures, records with fields, -type/-opaque aliases, -define
macros, -include/-include_lib file edges, and -export-driven visibility.

Modules wrap in a namespace so remote mod:fn(...) calls resolve through the
existing qualified-name matcher as mod::fn with zero resolver changes.
-behaviour declarations link to the behaviour module — gated to namespace
targets only (bare-name fallthrough linked -behaviour(supervisor) to an
unrelated macro constant on emqx). OTP indirection with static targets is
followed: spawn/apply/proc_lib/timer/rpc MFA-argument callees, and
gen_server:call/cast(?MODULE | ?SERVER) to the module's own
handle_call/handle_cast. Var-module dispatch and message sends stay
deliberately unlinked. codegraph_explore also normalizes Erlang-native query
spelling (mod:fn/3, init/2) so named symbols resolve as typed.

Benchmarked on cowboy (189 files), ejabberd (414), emqx (2,447): extraction
PASS on all three; with-codegraph arms reached 2/2/0 file Reads vs 10/5+/19
without, fastest on the largest repo.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:32:20 -05:00
7d624ecfac feat(resolution): CFML receiver-type inference for locals, typed args, and component properties (#1155)
CFML joins the #1108 receiver-inference family: new/createObject/typed-arg/property(inject) declarations type the receiver, variables./this. fields scan whole-file, method QNs re-scoped to Class::member in all three extraction paths. 1,649 typed edges on fw1/ColdBox/CFWheels, 1,649/1,649 audit-consistent, inherited methods resolve via #1152 extends edges.

Co-authored-by: ghedwards <125586+ghedwards@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:02:16 -05:00
81cb59a86e fix(resolution): yield per ref and cache hot per-ref work so the watchdog can't kill a valid index (#1122) (#1137)
The #850 liveness watchdog was killing valid `codegraph init`/`index` runs
at "Resolving refs 0-2%" on large collision-heavy repos (18-25K-file Java
monorepos on slower hardware). #1105's cooperative yielding assumed a
500-ref sub-chunk is always cheap, but per-ref cost is unbounded: a
colliding method name (`execute`, `process`, ...) whose candidate set
misses the 5,000-entry name LRU re-fetches every same-named row
(unbounded SELECT + materialization, measured 8.8ms at just 4K collisions
on an M4 — linear in collision count), and receiver-type inference
re-split the whole source file per ref (~20% of total index CPU). A dense
pocket multiplied that past the 60s window and the heartbeat starved.

Three guards, no behavior change:
- resolveBatchYielding checkpoints after EVERY ref (maybeYield is a ~ns
  time check when under budget), so a slow pocket can never run more than
  one ref past the yield budget.
- resolveMethodOnType's ref-independent candidate filter is memoized per
  (language, Type::method) on the resolver context; per-ref
  disambiguation (import FQN #314, call-site file #1079) stays outside
  the memo.
- Receiver inference reads lines through a per-file LRU (shared and C++
  inferrers), and skips generated/minified lines >10K chars instead of
  regex-scanning them per ref.

Measured on a 4,028-file synthetic Java bank repo (392K refs): mid-loop
max event-loop stall 1528ms -> 546ms under cache thrash, total init
250.9s -> 96.8s at default config.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:34:56 -05:00
e53968cae8 fix(resolution): gate the Lua/Luau annotation pattern against method-call self-match (#1124) (#1131)
Lua method-call syntax (lg:Log()) is byte-identical to the Luau type-annotation
shape (lg: Logger), and the receiver-type scan starts on the call's own line —
so any PascalCase method call self-matched as "type = Log" before the scan
reached the real declaration, silently dropping the calls edge whenever two or
more classes shared a method name.

The annotation pattern now rejects a capture followed by any of Lua's three
call forms; its leading [\w.] lookahead alternative prevents backtracking from
shrinking the capture to dodge the gate. Gated rather than dropped: the pattern
is the only type source for Luau typed params and annotated locals whose
initializer isn't T.new().

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:35:33 -05:00
cf86fe8198 fix(resolution): extend typed-parameter receiver inference to Rust/Go/Dart/PHP (#1125) (#1130)
Completes the #1125 fix. The same typed-parameter gap fixed for TS/JS existed
in every other language whose localReceiverTypePatterns only matched
keyword-anchored locals (let/var/:=/= new) and never the bare parameter form:

- Rust: the `:`-annotation pattern required `let`, so `fn use(lg: &Logger)`
  didn't match. Dropped the `let` anchor (still covers `let lg: Logger`),
  keeping the `&?mut?` handling — now covers params and closures `|lg: T|`.
- Go: only `lg := T{}` / `var lg T` matched; a parameter/method-receiver
  `func use(lg Logger)` / `func (l Logger) M()` (name-before-type, no keyword)
  didn't. Added a PascalCase-guarded `ident Type` pattern — the guard plus the
  existing enclosing-scope bound (excludes package-level struct fields) keep
  the keyword-free shape from matching unrelated pairs.
- Dart: the type-before-name pattern's trailing `[=;]` missed a parameter's
  `)`/`,`. Widened to `[=;,)]`, mirroring Java/C#.
- PHP: only `$lg = new T` matched; a typed param `function use(Logger $lg)`
  (also `?Logger`, `\App\Logger`, `&$lg`, `catch (E $e)`) didn't. Added a
  type-before-$var pattern. Reserved words can't be class names, so the
  looser lowercase-allowing capture yields no wrong edges.

Every pattern still relies on resolveMethodOnType validating the inferred type
actually declares the method (no edge on a mis-inference) — the same safety
net the already-covered languages use. Verified with a deterministic probe:
all four now disambiguate two same-named methods via the typed param (Java +
Kotlin as passing controls), full suite green (1930), no regressions.

Adds a parameterized regression test (Rust/Go/Dart/PHP), associating method to
type by qualifiedName so it holds where the method sits outside the type's
line range (Rust impl, Go decl).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 12:17:23 -05:00
385001398b fix(resolution): infer typed-parameter receivers in TS/JS (#1125) (#1129)
The local-variable receiver-type inference from #1108/#1110 covered typed
parameters for every language except TypeScript/JavaScript (+ TSX/JSX). The
TS/JS `:`-annotation pattern required a leading `const|let|var`, so it only
matched a local's own annotation (`const lg: Logger`) and never a bare
parameter (`function use(lg: Logger)` / `(lg: Logger) =>`). With a second
class sharing the method name — the case where a same-name fallback can't
paper over it — `lg.log()` resolved to no edge, dropping it from callers and
impact/blast-radius. TS/JS is the most common language pair in the userbase,
so this was a real precision gap.

Replace the keyword-anchored pattern with the keyword-free
`\b${r}\b\s*:\s*([A-Z][\w.$]*)`, mirroring Kotlin/Swift/Scala. It's a strict
superset (still matches `const lg: Logger`) plus the typed-parameter case,
and the capture stops at `<` so a generic-typed param
(`repo: Repository<User>`) still yields `Repository`. resolveMethodOnType
already validates the inferred type declares the method, so the looser match
produces no edge on a mis-inference — the same safety net the other
languages rely on; Swift already ships this identical bare-colon pattern with
the same theoretical ternary/dict-literal exposure.

Adds a regression test using two ambiguous classes + typed params, asserting
each call routes to its OWN class's method (verified to fail without the fix
and pass with it — a single-class version would pass either way via the
same-name fallback, which is why the collision is load-bearing).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 12:07:25 -05:00
358f400c40 feat(resolution): local-variable method calls in Lua, Luau, R, Pascal (#1112) (#1113)
Extends the local-variable receiver-type inference (#1108/#1110) to the
remaining supported languages with object-method calls. An empirical
sweep found Objective-C, Svelte, Vue, and Astro already resolved
`localVar.method()` (ObjC via message-send handling; the template langs
ride the TypeScript path), leaving Lua, Luau, R, and Pascal.

Lua/Luau/R were a resolution gap, not extraction: the call ref IS
extracted (`lg:log`, `lg$log`), but (1) the resolver's fast pre-filter
`hasAnyPossibleMatch` only understood `.`/`::` separators, so a `:`/`$`
ref was dropped before any strategy ran, and (2) matchMethodCall only
parsed `.`/`::` receivers with no local-var inference for these langs.
Fixes: pre-filter now checks the member/receiver around `:` and `$`;
matchMethodCall recognizes `lg:log` / `lg$log` and routes them through
the same inference + validated resolveMethodOnType path; and inference
patterns are added for Lua/Luau (`local x = T.new()` / `T()` / `x: T`),
R (`x <- T$new()`), and Pascal (`var x: T` / `x := T.Create`).

Pascal statement-form calls (`obj.Method;`) now resolve via the new
inference pattern. The assignment-RHS parameterless form
(`x := obj.Method`) is deliberately left as a field read by the existing
Pascal extractor — an intentional field-vs-call ambiguity tradeoff — so
it stays out of scope.

Validated with single-file and two-file same-name repros per language
(resolves to the right method; two-file is same-file-correct, #1079).
Adds all four to the local-variable inference test matrix. Full suite
green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:59:06 -05:00
ed64db08b4 feat(resolution): infer local-variable receiver types across languages (#1108) (#1109)
Instance calls through a local variable — `const lg = new Logger();
lg.log();` — only resolved to the method in C++. Every other language
produced no `calls` edge, because the resolver had no way to learn the
receiver variable's type, so such calls were missing from callers,
impact/blast-radius, and explore flow traces.

Local variables aren't indexed as nodes (node-explosion), so — like the
existing C++ inferrer — this reads the enclosing function's source and
matches the receiver's declaration/initializer to recover its type, then
hands it to resolveMethodOnType. That validates the method actually
exists on the inferred type, so a mis-inference yields no edge, which is
what lets the per-language patterns stay simple. The scan is bounded to
the enclosing scope so a same-named variable in another function can't
leak in.

Generalizes the C++-only path in matchMethodCall into a language dispatch:
C++ keeps its dedicated header-aware inferrer; a new shared
inferLocalReceiverType covers TypeScript, JavaScript, Python, Java, C#,
Kotlin, Swift, Go, Rust, Dart, Scala, and PHP, matching each language's
declaration shapes (`= new T`, `= T(...)`, `= T.new`, `let x = T{}`,
`x := T{}`, `T x = ...`, `x: T`, etc.). For Java/Kotlin an import FQN
still pins which same-named class is meant (#314); other languages fall
back to the call-site's own file (#1079).

Ruby is not covered: its extractor emits no `receiver.method()` call
reference in the first place, so there is nothing for resolution to
resolve — a separate extraction-layer gap.

Adds a parameterized end-to-end test covering all twelve languages. Full
suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:12:22 -05:00
63bc0fd037 fix(resolution): resolve same-named methods to the call site's own file (#1079) (#1107)
When two files each declared a same-named class with a same-named method
(e.g. `class Logger { void log(); }`), a call resolved to whichever
definition was indexed first — so a call in `b/svc` wrongly targeted
`a/svc`, mixing up that method's callers and blast radius.

The reported case was C++ instance calls, but the underlying pattern —
"multiple same-named candidates, pick the first-indexed, ignore the call
site's file" — lived in three resolution paths, each firing for a
different call shape and affecting different languages:

  - `obj.log()`     instance        -> resolveMethodOnType (C++)
  - `Logger.log()`  class receiver  -> matchMethodCall Strategy 1/2/3
                                       (Python, TypeScript, Java, C#)
  - `Logger::log()` qualified       -> matchByQualifiedName (C++, Rust)

All five sites now share one helper, `preferCallSiteFile`, that prefers
a candidate declared in the call site's own file when a name is
ambiguous. It runs after the `preferredFqn` block in resolveMethodOnType,
so Java/Kotlin import disambiguation (#314) — whose target is
intentionally in another file — is unaffected. The helper is a no-op
when there are fewer than two candidates or none share the call site's
file, so the common single-definition case is unchanged.

Adds 8 tests under `Same-name method disambiguation (#1079)`: the
`preferCallSiteFile` contract, resolveMethodOnType precedence (including
a guard that an import FQN still beats the same-file preference),
`matchByQualifiedName` disambiguation, and end-to-end index tests for the
C++ instance, TypeScript static, and C++ qualified call shapes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:55:41 -05:00
45d3293c6a fix(resolution): stop "Resolving refs" wedge on theme-vendoring repos; add exclude config + index watchdogs (#999) (#1009)
Three fixes for a repo that commits a large JS/TS theme/SDK (Metronic under
static/, ~1,600 tracked files):

1. A SECOND "Resolving refs" quadratic that #915 didn't cover. #915 capped
   import-name collisions; this caps method-name collisions (init/update/render
   re-declared on every widget), which flow through matchMethodCall Strategy 3
   and findBestMatch instead. New AMBIGUOUS_NAME_CEILING (default 500, env
   CODEGRAPH_AMBIGUOUS_NAME_CEILING): above it the fuzzy strategies decline
   rather than score K candidates — no proximity score can pick the one true
   target among thousands anyway. Resolving drops from O(K^2) to linear in refs
   (e.g. 900-file synthetic: 28.7s -> 3.4s), edge counts unchanged, and the cap
   never fires on normal repos (max real method-collision ~40).

2. A new `exclude` array in codegraph.json keeps git-TRACKED paths out of the
   index, which .gitignore can't do (enumeration is `git ls-files`). Mirrors the
   existing includeIgnored plumbing across the git, sync, and non-git-walk
   paths.

3. `index`/`init` now install the #850 liveness + #277 ppid watchdogs (which
   were serve-only), so a wedged or orphaned indexer self-terminates instead of
   pinning a core. The --liftoff-only relaunch's spawnSync can't forward
   signals, so killing the parent shim used to orphan the worker.

Tests: ubiquitous-name ceiling, exclude (incl. tracked-file exclusion on git +
non-git), orphan self-termination (POSIX), and ppid-parser units. Shared the
ppid parsers out of mcp/index.ts into mcp/ppid-watchdog.ts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:25:47 -05:00
0a91d0f512 perf(resolution): fix O(K²) import-node blowup in "Resolving refs" (#915) (#965)
* perf(resolution): resolve imports to definitions, not sibling import nodes (#915)

"Resolving refs" crawled (tens of minutes) on large projects — most painfully
ones mixing a big front-end and back-end. An external package or module imported
across hundreds/thousands of files (react, a shared UI package, Python
logging/typing) is re-declared as an `import` node in every importing file, so
its unresolved import ref fell through to the exact-name matcher, which scored
all K same-named import nodes via findBestMatch — K refs x K candidates = O(K^2)
per package, producing only meaningless import->import edges.

Fix: exclude `import`-kind nodes as name-match targets (they're statements, not
definitions; real import->definition resolution is the import resolver's job).
Plus two safe constant-factor wins in findBestMatch: hoist the per-candidate
ref.filePath split, and skip cross-language candidates when a same-language one
exists (provably the same winner — same-language scores >=50, cross-language
maxes at 35).

Measured: superset (Py+TS) candidates scored 7.5M -> 833K (9x), non-import edges
preserved (+1618 now resolve to real defs), ~22K useless import->import edges
removed; kubernetes (Go) computePathProximity 37.2s -> 5.0s; synthetic 8k-file
mixed repo (K=4000) resolution 16.0s -> 1.7s. Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: correct stale better-sqlite3/wasm references to node:sqlite

The SQLite backend has been Node's built-in node:sqlite (real SQLite, WAL + FTS5,
from the bundled runtime) for a while — there is no native build step and no
node-sqlite3-wasm fallback. README and the docs site were already updated; this
catches the stragglers:

- CLAUDE.md: the src/db/ backend description and the sqlite-backend test note.
- src/db/index.ts, src/mcp/tools.ts: two code comments that still blamed "the
  wasm backend" for non-WAL behavior (reworded to "when WAL isn't in effect").

Leaves tree-sitter grammar wasm (web-tree-sitter / --liftoff-only) untouched —
that's a different, still-current use of wasm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(telemetry): drop the dead sqlite_backend field (schema v2)

node:sqlite is now the only backend, so the `index` event's `sqlite_backend`
field was a constant ("native") carrying no signal — and the `install` event
never actually sent it. Remove the field and the backendKind() helper, bump the
telemetry SCHEMA_VERSION 1 -> 2, and update TELEMETRY.md + docs/design/telemetry.md.

The ingest worker is deliberately left tolerant: `index` doesn't require the
field and schema_version validates as nonNegInt(99), so v2 events ingest fine and
old clients still sending v1 + sqlite_backend keep validating too. Added a legacy
comment there explaining it's safe to drop once old-client share is negligible.

telemetry.test.ts: the assertion pinning schema_version and a stale-claim fixture
line updated 1 -> 2. All telemetry tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:26:23 -05:00
1f15f93feb feat(extraction): PHP string/array callables + Ruby lifecycle-hook symbols (#811)
The last two deferred callback-registration shapes from #756, each scoped
to positions where the reference is trustworthy:

PHP — a string is a callable ONLY in a known callable position:
  - string args of core HOFs (usort, array_map, array_filter,
    call_user_func*, preg_replace_callback, spl_autoload_register,
    set_error_handler, … — PHP_CALLABLE_HOFS): ungated (PHP globals are
    referenced cross-file without imports) + resolution unique-or-drop,
    function-kind only ('Cls::m' strings resolve qualified)
  - array callables anywhere in call args: [$this, 'method'] routes through
    the class-scoped this. resolver (parents included); [Foo::class,
    'method'] resolves qualified
  - strings to arbitrary functions: deliberately nothing

Ruby — hook-DSL symbols name a method of the enclosing class:
  (skip_)?(before|after|around)_* / validate / set_callback /
  helper_method / rescue_from(with:) symbols → class-scoped this.<sym>,
  riding the supertype pass so `before_action :authenticate` in a
  controller resolves to ApplicationController's method. `validates`
  (plural) excluded — its symbols name ATTRIBUTES. Class-body-level hooks
  attribute to the CLASS node (the scoped resolvers now accept class-like
  from-nodes).

Also hardened while validating: the this.X supertype pass is now
NODE-anchored — file-anchored class node → implements/extends edge targets
→ contains-anchored member lookup — replacing the name-keyed
getSupertypes walk, which unioned every same-named class's parents (rails
has a dozen `Engine`s) and produced a cross-class wrong edge.

A/B vs main: WordPress +556 (14/14 sampled genuine — [$this,'m'] wiring,
array_map('absint',…), sodium polyfill call_user_func_array dispatch);
rails/rails +385 after the node-anchored fix (16/16 sampled genuine, incl.
inherited hooks across real extends edges); controls byte-stable
(excalidraw 0-delta, redis identical, typeorm keeps its +4 inherited
getters). The only calls-edge deltas anywhere are pre-existing
minified-bundle resolution jitter (wp-tinymce.js single-letter symbols).

Full suite 1391 passed. EXTRACTION_VERSION 21 → 22 (re-index to benefit).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:30:29 -05:00
38095aa95b feat(resolution): inherited this.X, Java/Kotlin cross-file method refs, Swift type scoping (#810)
Three callback-registration shapes deferred from #756/#808, one arc:

1. INHERITED this.X (TS/JS + every this.-routed language): a `this.<member>`
   registration whose member isn't on the enclosing class defers to a second
   pass (resolveDeferredThisMemberRefs — in-memory like deferredChainRefs,
   runs after implements/extends edges persist, same lifecycle as the #750
   conformance pass) and resolves up the supertype chain, depth-capped BFS,
   validated targets only. `bus.on("submit", this.handleSubmit)` in a
   subclass links to FormBase::handleSubmit; same-named methods on unrelated
   classes never match. this.-prefixed candidates skip the extraction name
   gate (an inherited member can't be in definedHere).

2. JAVA/KOTLIN qualified method refs: `Handlers::onMessage` /
   `OtherClass::handle` emit QUALIFIED names resolved by the scoped
   suffix-matcher — cross-file capable, gated on the scope name being a
   same-file type or an imported name (dotted JVM imports now contribute
   their last segment). `this::m` and `super::m` route through the
   class-scoped resolver (super rides the supertype pass). References
   through a VARIABLE (`subscriber::onNext`) deliberately produce nothing —
   receiver type is unknowable; RxJava's baseline bare capture was resolving
   these to same-named same-file methods (a test method "registering" an
   anonymous class's onNext) — the rework drops 18 such wrong edges and
   keeps the 7 genuine Type::method refs RxJava's main tree actually has.

3. SWIFT enclosing-type scoping (implicit self): bare callback names match
   methods only of the from-symbol's own type (extension/nested scopes
   reconciled by suffix), and top-level code never matches methods.
   Alamofire: −44 wrong edges (parameters like `request`/`data`/`retrier`
   resolving to same-named methods on unrelated protocols), all verified;
   the same-class param collision (`task`) remains and is documented.

New ResolutionContext.getNodeById lets matchers derive the from-symbol's
class scope. Controls: redis/fmt fnref edges byte-identical; excalidraw
stable; typeorm +4 genuine inherited-getter dependencies; zero calls edges
changed on any of 7 A/B repos; nodes identical everywhere. Kotlin
companion-object members extract unqualified (pre-existing) so
`Type::companionFn` stays silent rather than guessing — documented.

Full suite 1389 passed. EXTRACTION_VERSION 20 → 21 (re-index to benefit).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:09:01 -05:00
38eb4e688c fix(extraction): classify TS/JS class fields by value — properties, not methods (#808) (#809)
Every TS `public_field_definition` / JS `field_definition` extracted as a
method-kind node, so a plain field (`public fonts: Fonts;`) was reported
as callable: class shape was misrepresented, kind-based filtering was
defeated, and bare-name call resolution landed on data fields — typeorm's
boolean `ColumnMetadata::isArray` field was soaking up Array.isArray(...)
call edges (685 such wrong edges on typeorm alone).

Classification now follows the VALUE (classifyMethodNode hook, mirroring
resolveBody's callable detection): arrow-function / function-expression
fields and HOF-wrapped ones (`onScroll = throttle(() => {…})`) stay
methods with their bodies walked; everything else becomes a property that
keeps its type-annotation references edge, visibility, static-ness, and
decorators. Field initializers are now walked too (`history =
createHistory()` attributes the call to the property — previously
invisible), and JS class fields — whose name lives in the grammar's
`property` field, so they never extracted a symbol at all — now appear in
the graph (resolveName on the JS extractor).

With fields correctly kinded, `this.X` callback registration is re-enabled
for TS/JS (removed in #807 because field pseudo-methods made it mostly
wrong): `this.<member>` candidates resolve CLASS-SCOPED
(resolveThisMemberFnRef) — the target must be a function/method sharing
the from-symbol's qualified-name class prefix, same file, no fallback —
so `addEventListener("online", this.onOfflineStatusToggle)` and API-object
wiring (`{ mutateElement: this.mutateElement }`) produce registration
edges to the enclosing class's own method, while `this.fonts` (a
property) and inherited/unknown members yield no edge.

A/B (baseline = #807 main): excalidraw / typeorm / express — node counts
identical on all three; kinds shift method→property only (typeorm: exactly
7,406 swapped; excalidraw also corrects 5 anonymous-class mock fields that
were function-kind); every one of the 736 dropped call edges targeted a
node that is now a property (calls into data fields — verified 100%);
gains are retargets to real callables, initializer-call attributions, and
+74/+7 class-scoped this.X registration edges (sampled: addEventListener/
removeEventListener wiring, imperative-API method maps). Full suite green
(1386).

EXTRACTION_VERSION 19 → 20 (re-index to benefit).

Closes #808

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:48:11 -05:00
8a114ba53c feat(extraction): capture function-as-value — callback registration sites in callers/impact (#756) (#807)
A function name used as a VALUE — passed as an argument
(signal(SIGINT, handler), qsort(..., compare)), assigned to a function
pointer or field (ops->recv_cb = my_cb, OnClick := Handler), or placed in
a struct initializer / handler table ({ .recv_cb = my_cb },
{ "get", getCommand }) — produced no edge in ANY of the 19 tree-sitter
languages, so registered callbacks looked dead and their registration
sites were invisible to callers/impact.

This adds table-driven function-as-value capture across all 19 languages
(plus the wrapper forms: &fn, &Cls::method, Java Class::m, Kotlin ::f,
Swift #selector, ObjC @selector, Ruby method(:sym), Scala eta, Pascal
@Handler), gated at extraction (same-file definitions + imported
bindings; C-family file-scope initializers are constant-expression
contexts and skip the gate, which is how redis-style cross-file command
tables resolve), and resolved by a dedicated strategy: function/method
targets only, same-file first, unique-or-drop cross-file, no fuzzy
fallback ever. Edges persist as kind 'references' with metadata.fnRef,
so getCallers/getImpactRadius surface them with zero graph-layer
changes; MCP callers/callees label them "via callback registration".

Precision rules bought by real-repo false positives (full A/B record in
docs/design/function-ref-capture.md): C++ is &-explicit outside
file-scope tables (fmt's begin/out/size collisions; out-of-line member
defs are function-kind); TS/JS/Python bare ids resolve to functions only
(TS class fields extract as method-kind — pre-existing quirk); Swift
refuses same-file method overload-families; param-forward shapes
(this.x = x, value: value) and destructuring are skipped; minified
bundles (*.min.js) produce no candidates.

Validated on 17 public OSS repos (redis, excalidraw, gin, bytes, okhttp,
okio, Alamofire, flask, sinatra, Newtonsoft.Json, scopt, provider,
busted, Fusion, AFNetworking, PascalCoin, fmt): node counts identical,
zero calls edges lost or gained, references strictly additive
(+3,200 registration edges total), precision spot-checked by reading
sampled source lines (redis 30/30, flask 8/8). Deliberately NOT covered:
indirect-dispatch resolution (o->cb(x) → impl) — that needs data-flow
through struct fields, and a wrong edge is worse than none.

EXTRACTION_VERSION 18 → 19 (re-index to benefit).

Closes #756

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:20:27 -05:00
af56f3539d fix(pascal): resolve chained factory calls TFoo.GetInstance().DoIt() (#750) (#791)
Ports the #645/#608 chained-receiver mechanism to Pascal/Delphi — which I'd
previously mis-scoped as blocked. The paren'd chained form extracts fine; it just
hit the chained-call gap like the others (with a decoy, `TFoo.GetInstance().DoIt()`
mis-resolved to a same-named method on an unrelated class).

- pascal.ts: getReturnType reads the method's `typeref` (a `function GetInstance:
  TBar` returns TBar; an interface return `IFoo` is captured too).
- tree-sitter.ts: extractPascalCall now re-encodes a chained call `TFoo.GetInstance().DoIt`
  (the exprDot's receiver is an exprCall) instead of collapsing it to bare `DoIt`.
  Gated on the Delphi type-naming convention (`TFoo`/`IFoo`) so a capitalized
  VARIABLE chain (Pascal capitalizes locals too — `Curve.X().Y()`, `Self.X().Y()`)
  stays bare and keeps its existing bare-name resolution.
- name-matcher.ts: `pascal` joins the dotted-chain gate + CHAIN_LANGUAGES +
  CONSTRUCTS_VIA_BARE_CALL (a `TFoo(x)` typecast yields a TFoo). When the factory's
  return type wasn't captured (a `constructor Create` has no `: TBar` but returns
  its class), resolve the method on the factory class itself. resolveMethodOnType
  validates, so a wrong inference yields no edge.

Validation: 4 synthetic tests (factory+decoy, constructor chain, typecast chain,
absent-method safety). Real-repo A/B on PascalCoin (772 files): +19 / -18 — 15 of
the -18 are correct class→interface retargets (`GetInstance(): IAsn1OctetString`
resolves `.GetOctets` on the declared interface, not baseline's concrete-class
guess); 3 are negligible drops (0.02%). EXTRACTION_VERSION 15->16. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 08:37:04 -04:00
d21d2dfa50 fix(objc): resolve chained message-send calls [[Foo create] doIt] (#750) (#786)
Ports the #645/#608 chained-receiver mechanism to Objective-C. A message send
whose receiver is itself a message send — `[[Foo create] doIt]` — used to drop
the receiver, so `doIt` name-matched a same-named method on an unrelated class
(commonly a test helper's `init` or an Apple-SDK method).

- objc.ts: getReturnType reads the method's `method_type`, SKIPPING nullability /
  ARC qualifiers (`nonnull instancetype` must yield instancetype, not `nonnull`).
- tree-sitter.ts: the message_expression branch now re-encodes a chained send
  `[[Foo create] doIt]` as `Foo.create().doIt` when the inner receiver is a
  capitalized class and the outer selector is unary.
- name-matcher.ts: `objc` joins the dotted-chain gate + CHAIN_LANGUAGES. A
  class-message factory returns an instance of the RECEIVER class by convention
  (`instancetype`), so when the factory's own return type isn't recoverable
  (`alloc`/`new`/`shared…` return instancetype, or aren't user nodes), the
  receiver's type is the class itself — this resolves the ubiquitous
  `[[X alloc] init]` and singleton chains. resolveMethodOnType validates against
  the class and its supertypes, so a wrong inference yields no edge.

Validation: 4 synthetic tests (factory+decoy, superclass conformance, absent-method
safety, the nonnull-instancetype singleton). Real-repo A/B on SDWebImage (208 files):
+35 / -75 — all corrections (the -75 are wrong `init` mis-matches to a test helper /
wrong class, retargeted to the right class's init in the +35, plus 2 Apple-SDK chains
on unindexed classes). db stable, no node explosion. EXTRACTION_VERSION 14->15.
Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:35:49 -04:00
16c73e2b0e fix(dart): resolve chained static-factory / constructor calls Foo.create().bar() (#750) (#762)
Ports the #645/#608 chained-receiver mechanism to Dart, plus makes Dart factory
and named constructors first-class so their chains can resolve at all. A call
whose receiver is itself a call — `Foo.create().bar()` (static factory or
factory/named constructor) — used to drop the receiver to a bare `bar`, which
name-matched a same-named method on an unrelated type (commonly a stdlib
`Option`/`Iterator` `.map`/`.where` mis-tied to the project's own class).

- dart.ts: extractBareCall now re-encodes `Foo.create().bar` when the chain
  starts with a capitalized type; getReturnType captures the return type (generic
  `List<Foo>` → `List`); factory (`factory Foo.create()`) and named (`Foo._()`)
  constructors are indexed as `Foo::create` / `Foo::_` with return type = the
  class (via resolveName + getReturnType + constructor_signature in methodTypes).
- The UNNAMED ctor `Foo()` is deliberately NOT extracted (isMisparsedFunction),
  so plain construction stays an `instantiates` edge to the class rather than a
  call to a phantom `Foo::Foo` method.
- dartCtorInfo validates a "constructor" against the enclosing class name, so a
  method tree-sitter MISPARSES as a constructor — `@override (A, B) m()`, where
  the annotation swallows the record return type and `m()` looks like a one-id
  constructor_signature — is still extracted as the method it is (regression
  found on localsend; covered by a new test).
- name-matcher.ts / index.ts: `dart` joins the dotted-chain gate,
  CONSTRUCTS_VIA_BARE_CALL (case construction), and CHAIN_LANGUAGES (conformance
  for superclass/mixin methods). resolveMethodOnType validates, so a wrong
  inference yields no edge.

Validation: 7 synthetic tests (static factory, factory/named ctor, construction,
conformance, absent-method safety, the misparse regression, instantiation-not-
hijacked). Real-repo A/B on localsend (368 Dart files): hand-written +17/-10 — all
corrections (the -10 = 7 wrong stdlib/extension misattributions removed + 3 ctor
source-renames), plus additive factory/named-ctor call resolution. Instantiation
preserved; no node explosion. EXTRACTION_VERSION 13->14. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:53:04 -04:00
2f96f58cbb fix(scala): resolve chained static-factory/apply calls Foo.create().bar() (#750) (#761)
Ports the #645 (C++) / #608 (PHP) chained-receiver mechanism to Scala. A call
whose receiver is itself a call — `Foo.create().bar()` (companion factory),
`Builder(cfg).bar()` (case-class apply), or a fluent chain — used to drop the
receiver to a bare `bar`, which name-matched a same-named method on an unrelated
type. The most common wrong edge was a stdlib `Option`/`Iterator` `.map`/`.flatMap`/
`.foreach` mis-attributed onto the project's own same-named class.

- scala.ts: `getReturnType` reads the `return_type` field — generic `List[Foo]`
  → container `List`, qualified `pkg.Foo` → `Foo`, `this.type` left undefined.
- tree-sitter.ts: re-encode `Foo.create().bar` when the inner call's receiver chain
  starts with a capital (companion factory / case-class apply); instance chains
  (`list.map().filter()`) stay bare.
- name-matcher.ts: `scala` joins the dotted-chain gate + CONSTRUCTS_VIA_BARE_CALL
  (case-class `apply` constructs the class); resolveMethodOnType validates, so a
  non-conventional `apply` returning another type yields no edge, not a wrong one.
- index.ts: `scala` joins CHAIN_LANGUAGES so trait-inherited methods resolve via
  the conformance second pass.

Validation: 4 synthetic tests (factory+decoy, case-class apply, trait conformance,
absent-method safety). Real-repo A/B on gatling (750 Scala files): +14 / -59 unique
edges — all corrections. The +14 are retargets (e.g. `HttpProtocolBuilder(cfg).baseUrl`
now resolves to HttpProtocolBuilder::baseUrl, not the same-named private BaseUrlSupport
helper); the -59 are wrong edges removed (stdlib Option/Iterator monad calls
mis-tied to the project's Validation::*, self-loops, decoy collisions) — zero genuine
factory chains dropped (verified: gatling has no real Validation.success().map() chains).
db stable at 40 MB. EXTRACTION_VERSION 12→13. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:09:33 -04:00
ccced9e358 fix(go): resolve chained factory-function calls New().Method() (#750) (#760)
* fix(go): resolve chained factory-function calls New().Method() (#750)

A Go call through a chained factory function — `New().Method()`,
`With(cfg).Build()` — dropped the receiver to a bare method name, which then
attached to a same-named method on an unrelated type (a wrong edge) or didn't
resolve. Ports the #645/#608 mechanism for Go's bare-factory receivers:

- Part 1: capture Go return types; a pointer `*Foo` -> `Foo`, a multi-return
  `(*Foo, error)` -> its first result, qualified `pkg.Foo` -> `Foo`.
- Part 2: encode a bare-factory chain (`New().Method`), gated to an `identifier`
  receiver so instance chains (`obj.Method().Other()`) keep bare-name.
- Part 3: matchDottedCallChain bare-inner Go branch looks up the FUNCTION's
  return type, then resolves+validates the method on it. Wired into the
  conformance pass so a method promoted from an embedded struct (`type Widget
  struct{ Base }` -> the existing `extends` edge) resolves. FALLBACK: when the
  inner isn't a resolvable function (a package-level VARIABLE holding a function
  value, e.g. gin's `engine()`), fall back to bare-name so the edge isn't dropped.

Validated: synthetic decoy + args + multi-return + embedded-conformance + absent
safety tests (4/4); full suite green. Real-repo A/B on gin (99 .go): pre-fallback
-40 = 25 wrong self-loops removed (good) + 15 correct `Engine::ServeHTTP` dropped
(gin's ginS variable-factory `engine()`); the fallback recovers the 15. gin A/B
re-confirm with the fallback is PENDING (local index flakiness, not a code issue).
EXTRACTION_VERSION 11 -> 12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(go): stop the chained-call fallback from looping the batched resolver

The Go variable-inner fallback (for chains like `engine().ServeHTTP()` whose
inner is a package-level var, not a factory function) resolved the method via
a synthetic bare-name ref and propagated THAT ref as `.original`. Its
`referenceName` was the bare `ServeHTTP`, not the stored `engine().ServeHTTP`,
so `resolveAndPersistBatched`'s keyed `deleteSpecificResolvedReferences` no-oped,
the offset-0 batch never drained, and the loop re-resolved + re-inserted the
same rows forever — a runaway that grew a 99-file repo (gin) to 5,050,206 edges
/ 1.4 GB before filling the disk.

- name-matcher.ts: tie the bare-name match back to the original `ref` so the
  batch-cleanup delete matches the stored row and the loop drains.
- index.ts: add a non-progress guard to resolveAndPersistBatched — if the
  unresolved_refs table doesn't shrink after a batch, stop instead of growing
  the graph without bound (defense-in-depth for any future keyed-delete mismatch).
- resolution.test.ts: regression test for the variable-inner chain — asserts the
  fallback edge resolves AND the edge count stays bounded (no explosion).

gin A/B (post-fix): db 5.8 MB / 3,699 calls edges; net-zero unique-edge diff vs
main (the fallback recovers the dropped edges, adds no wrong ones). Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:31:24 -04:00
5805f01957 fix(rust): resolve chained associated-function calls Foo::new().bar() (#750) (#757)
A Rust call through a chained associated function — `Foo::new().bar()`,
`Foo::with(cfg).build()` — dropped the receiver to a bare method name, which
then attached to a same-named method on an unrelated type (a wrong edge) or
didn't resolve. Ports the #645/#608 mechanism for Rust's `::` receivers:

- Part 1: capture Rust return types; `-> Self` yields the `self` marker (resolved
  to the impl's own type, like PHP), references/generics are unwrapped/reduced.
- Part 2: encode an associated-function chain (`Foo::new().bar`), gated to a
  scoped_identifier receiver so instance chains (`x.foo().bar()`) keep bare-name.
- Part 3: resolve via matchScopedCallChain (PHP's `::` resolver, generalized),
  validated by resolveMethodOnType. Wire Rust into the conformance second pass
  (matchScopedCallChain variant) so a chained method provided by a trait the type
  implements (`impl Trait for Type` → existing implements edges) resolves too.

Validated: synthetic decoy + args + Self + trait-default-conformance + absent
safety tests; full suite green (lone failure is the known-flaky #662 daemon test,
passes in isolation). Real-repo A/B vs main: clap (329 .rs) a net precision win —
**+937 added (96% correct builder methods), 622 wrong->right retargets**
(`Command::new().arg()` was mis-resolving to `ArgGroup::arg`, now `Command::arg`),
+162 net unique edges; the pure-drops are largely wrong bare-name edges the fix
correctly stops emitting. tokio-rs/bytes 0/0 (no regression). Known limit: the
single-hop mechanism re-encodes only the first hop of a chain (deeper hops keep
bare-name) — clap's unusually deep builder chains are partly covered.
EXTRACTION_VERSION 10 -> 11.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 02:41:59 -04:00
7c7f0dd56f fix(swift): resolve chained static-factory/fluent calls + nested-extension naming (#750) (#755)
Completes Swift in the #750 chained-call series (after Java #751, Kotlin #752,
C# #753, conformance #754). Two parts:

1. Swift chained-call resolution (the #645/#608 mechanism): capture Swift return
   types (positional, member types -> last segment), encode capitalized-receiver
   chains `Foo.make().draw()` / `Foo(args).draw()`, resolve+validate via the
   shared matchDottedCallChain (+ constructor branch). Fixes the decoy wrong-edge
   bug where a chained method dropped to a bare name and attached to a same-named
   method on an unrelated class.

2. Nested-type extension naming fix: `extension KF.Builder: KFOptionSetter` parsed
   as a class_declaration named `KF.Builder` (dot) — inconsistent with the type's
   own declaration `KF::Builder` (name `Builder`) — so the extension's conformances
   and members were invisible to a chained call on the type. A Swift resolveName
   now names a nested-type extension by its last segment (`Builder`), so its
   `implements`/`extends` edges and methods are found by the supertype walk
   (conformance #754) and the simple-name method match.

Validated: synthetic decoy + args + constructor + absent-method tests; full suite
green; nested-extension repro (`KF.url().onSuccess()` resolves via conformance to
the protocol method). Real-repo A/B vs main (conformance) — Alamofire and
Kingfisher both **0 added / 0 removed, node count unchanged**: NEUTRAL and SAFE.
The prior -168 Kingfisher regression (from the naming inconsistency) is eliminated;
Swift's unique-named fluent methods already resolved by bare name, so the chain
path lands the same edges — the value here is decoy-collision correctness, the
nested-extension naming fix, and consistency with the other four languages.
EXTRACTION_VERSION 9 -> 10.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:54:12 -04:00
48d4654e8d feat(resolution): conformance-aware chained-method resolution (#750) (#754)
* feat(resolution): conformance-aware chained-method resolution (#750)

A chained static-factory/fluent call whose method lives on a SUPERTYPE the
receiver conforms to — a protocol-extension method (Swift), an interface default
method, or an inherited superclass method — now resolves. resolveMethodOnType
falls back to walking the return type's implements/extends edges (via the new
context.getSupertypes) when the method isn't a direct member. Because those edges
don't exist during the single-pass resolution, a second pass
(resolveChainedCallsViaConformance) re-resolves the deferred chained refs after
edges are built. Still validated, so a wrong inference yields no edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): conformance-aware chained-method resolution (#750)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:38:37 -04:00
aa07dc59d4 fix(csharp): resolve chained static-factory calls Foo.Create().Bar() (#750) (#753)
A C# method called through a static factory or fluent chain —
`Foo.Create().Bar()`, `JObject.Parse(s).Property(...)`,
`Instant.FromUtc(...).InZone(zone)` — lost the receiver's type, so the chained
method didn't resolve and the call was invisible to callers/impact/trace. Ports
the #645/#608 mechanism to C# (additive, like Java #751):

- Part 1: capture C# return types in the extractor, reading the `returns` field
  (`static Foo Create()` -> `Foo`); predefined/array/generic/nullable/namespaced
  types are normalized or skipped.
- Part 2: encode a chained `member_access_expression` receiver
  (`Foo.Create(args).Bar()`) as `inner().Bar` with normalized empty parens, so
  factory calls that take arguments still split. Non-chained member calls keep
  their existing `recv.Method` text.
- Part 3: resolve via the shared matchDottedCallChain (now Java/Kotlin/C#),
  validated by resolveMethodOnType so a wrong inference yields NO edge.

Known limitation (safe): C# extension-method chains don't resolve, since the
method lives on the extension class, not the receiver's type — no edge, never a
wrong one.

Validated: synthetic decoy + args + absent-method safety tests; full suite green;
real-repo A/B on Newtonsoft.Json (945 .cs: +3, 0 lost) and nodatime (488 .cs:
+73, 0 lost) — node count identical (no explosion), 0 edges lost, precision
spot-checked verbatim (Instant.FromUtc().InZone(), Offset.FromHoursAndMinutes().Plus(),
OffsetDateTimePattern.CreateWithInvariantCulture().WithTwoDigitYearMax()).
EXTRACTION_VERSION 7 -> 8.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:30:03 -04:00
3e04650850 fix(kotlin): resolve chained companion-factory calls Foo.getInstance().bar() (#750) (#752)
A Kotlin method called through a companion-object factory, fluent chain, or
constructor — `Foo.getInstance().bar()`, `Config.create(opts).build()`,
`STMTransaction(f).commit()` — dropped the receiver to a BARE method name, which
then name-matched a same-named method on an unrelated class (a wrong edge) or
failed to resolve. Ports the #645/#608 mechanism to Kotlin:

- Part 1: capture Kotlin return types in the extractor. tree-sitter-kotlin
  exposes no field names, so the return type is read positionally (the type node
  after function_value_parameters); inferred/Unit/Nothing returns yield none.
- Part 2: encode a CLASS/companion-factory call-receiver chain as `inner().method`.
  Gated to a capitalized receiver (`Foo.getInstance()` / `Foo(args)`) so instance
  chains (`list.filter{}.map{}`) keep their bare-name behavior — re-encoding those
  would only drop the edge, regressing recall in fluent codebases.
- Part 3: generalize matchJavaCallChain -> matchDottedCallChain (shared by the JVM
  dot-notation languages); resolve the method on the factory's return type, or on
  the constructed class for a Kotlin `Foo(args).method()` receiver. Validated via
  resolveMethodOnType, so a wrong inference yields NO edge.

Validated: synthetic decoy + args + absent-method safety tests; full suite green;
real-repo A/B on arrow-kt/arrow (734 .kt) — node count identical (no explosion),
+49 validated-correct chained edges, and the removed edges are wrong bare-name
guesses the fix correctly stops emitting (419/438 from test/doc files; the 18
from product code are stdlib `.apply{}`, self-loops, and bare-name mismatches) —
a net precision improvement, ~0 correct product edges lost. Java path unchanged
(constructor branch is Kotlin-gated). EXTRACTION_VERSION 6 -> 7.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:12:37 -04:00
7f6bdf7ad1 fix(java): resolve chained static-factory calls Foo.getInstance().bar() (#750) (#751)
A Java method called through a static factory or fluent chain — `Foo.getInstance().bar()`,
`Config.create(opts).build()` — lost the receiver's type, so the chained method either
didn't resolve at all or (when a same-named method existed on an unrelated class) attached
to whichever class was indexed first. Ports the #645 (C++) / #608 (PHP) 3-part mechanism:

- Part 1: capture Java return types in the extractor (skip void/primitives/arrays,
  unwrap generics, strip package qualifier).
- Part 2: encode a chained-call receiver as `inner().method` with normalized empty
  parens, so factory calls that take arguments still split.
- Part 3: matchJavaCallChain resolves the chained method on the factory's return type,
  validated via resolveMethodOnType so a wrong inference yields NO edge (never a wrong one).

Validated: synthetic decoy + absent-method safety tests; real-repo A/B on google/guava
(3,227 files) — node count identical (no explosion), 0 edges lost, +1,507 unique chained
edges recovered, precision spot-checked verbatim (Splitter.on().split(),
CacheBuilder.newBuilder().recordStats(), GraphBuilder.directed().build(), nested
MultimapBuilder.linkedHashKeys().arrayListValues()). EXTRACTION_VERSION 5 -> 6.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:43:17 -04:00
eb5960b535 fix(php): resolve chained static-factory calls Cls::for($x)->method() (#608) (#749)
A method called through a PHP fluent static factory — `ApiClient::for($c)->createOrder()`,
the canonical Laravel per-credential/per-tenant client idiom — produced no
`calls` edge: the receiver of `->createOrder` is the `Cls::for(...)` static
call, whose result type was never recovered, so the edge was dropped and
`codegraph_callers` returned nothing.

Same shape as the C++ singleton/factory fix (#645), reusing its return_type
column + the chained-call mechanism:
- Capture PHP return types (getReturnType): `: self` / `: static` / `$this`
  stored as the `self` marker, a concrete `: Type` as its short name,
  primitives/unions dropped.
- Encode the chained scoped-call receiver as `Cls::for().method` so the
  resolver can split it (PHP-gated, in extractCall).
- New matchPhpCallChain: look up the factory's return type (`self` → the
  factory's own class; concrete → that class), then resolve AND validate the
  method on it — a wrong inference yields no edge, never a wrong one.

EXTRACTION_VERSION 4->5 (re-index to populate PHP return types + chained edges).

Validated on koel (1383 PHP files): node count identical (no explosion),
0 edges lost, +80 chained-call edges recovered; synthetic tests cover the
self-factory, concrete-return, namespace, decoy, and absent-method cases.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:10:01 -04:00
fd03f31b2c fix(cpp): resolve calls through singletons/factories/chained getters (#645) (#742)
A C++ method call whose receiver is another call's result — `Foo::instance().bar()`,
`WidgetFactory::create().draw()`, `openSession()->run()`, or the same stored in an
`auto` local first — lost the receiver's type during extraction. The callee degraded
to a bare method name, so when two classes shared a method name the call silently
resolved to whichever was indexed first (or not at all), corrupting callers / impact /
trace with a plausible-but-wrong edge.

Three parts:
- Capture C++ return types (new nodes.return_type column, schema v5): the
  function_definition's `type` field, normalized — smart-pointer pointee unwrapped,
  void/primitives dropped.
- Preserve the inner-call receiver in extraction: a C/C++ field_expression whose
  receiver is itself a call is encoded `inner().method` instead of dropping to the
  bare name. Other languages keep the existing behavior.
- New resolution strategy (matchCppCallChain): infer the receiver's class from the
  inner call's return type, then resolve AND validate the method on it. Handles
  singletons/accessors, factories returning a different type, free-function
  factories, make_unique/make_shared/new/direct construction, single-level member
  chains, and namespace-qualified inner calls. A wrong inference yields no edge,
  never a wrong one.

EXTRACTION_VERSION 2->3 (re-index to populate return types).

Validated on the issue repro + spdlog: node count stable (no explosion),
deterministic, and ~100 pre-existing wrong `.size()`-style edges removed.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:18:17 -04:00
07af3db6c7 feat(impact): cross-language blast-radius coverage (22 languages + 14 frameworks) (#708)
Completes the cross-file dependency graph behind impact / affected / explore across all 22 supported languages and 14 web frameworks, validated on real-world repos (measured fair-coverage table added to the README). Per-language resolution + framework resolvers/synthesizers (Lua/Luau require, Shopify OS 2.0 Liquid sections, Delphi forms, Rust cross-module + Rocket macros, Swift Fluent, SvelteKit/Nuxt loader/component conventions, RN/Expo bridges). 0 cross-family false edges, full suite green (1187 passed). See #708.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:02:59 -04:00
Colby MchenryandGitHub 8c69001289 fix(resolution): Java/Kotlin imports disambiguate same-name classes (#314) (#472)
A Maven multi-module project where `dao/converter/FooConverter` and
`service/converter/FooConverter` both expose a `convert` method used to
resolve by file-path proximity — picking whichever class was closer to
the caller, which is wrong any time the caller lives in an equidistant
cross-cutting module. `extractImportMappings` had no Java branch at all,
so the FQN signal Java imports carry — `import
com.example.dao.converter.FooConverter;` — was thrown away.

- `extractJavaImports` parses regular and `import static` directives;
  wildcard imports (`*`) are intentionally skipped.
- `resolveViaImport` has a new Java/Kotlin cross-file branch that
  converts the imported FQN to a file-path suffix
  (`com/example/dao/converter/FooConverter.java`, or `.kt`) and
  resolves the symbol against the file whose path matches by suffix.
- For the field-receiver pattern (`@Autowired private FooConverter
  fooConverter; fooConverter.convert(...)`), `matchMethodCall` now
  looks up the receiver's inferred type in the caller file's imports
  and threads the resulting FQN through to `resolveMethodOnType`.
  When two `FooConverter::convert` candidates exist, the import — not
  iteration order — picks the right one.

Validated with a synthetic 3-module repro: swapping only the import
line on the caller swaps the resolved target between dao and service.

spring-petclinic (47 .java files): +15 newly import-resolved edges,
+2 references, no regression elsewhere.

Closes #314.
2026-05-26 17:42:14 -05:00
2543ae565a feat(java): trace Spring/MyBatis enterprise flow end-to-end (#389) (#468)
Closes three gaps that broke `trace(controller, mapper-xml)` on real Spring +
MyBatis projects:

1. **Field-injected concrete-bean trace.** Java `this.<field>.method()` is
   unwrapped at extraction (was surfaced as `this.<field>.method` and dropped
   through every name-matcher strategy). The receiver name is then looked up
   in the enclosing class's field declarations to get the declared type and
   resolve the method on it. Closes the controller→bean hop when the field
   name doesn't capitalize to the type (`userbo` → `UserBO`). General Java
   fix, not Spring-specific.

2. **MyBatis XML mapper as a first-class language.** New extractor parses
   `<mapper namespace="..."><select|insert|update|delete|sql id="X">` and
   emits method-shaped nodes qualified as `<namespace>::<id>`, plus
   `<include refid="X"/>` references to `<sql>` fragments. Non-mapper XML
   (pom, log4j, web.xml) → file node only. A new synthesizer
   (`mybatisJavaXmlEdges`) joins Java mapper methods to XML statements by
   suffix-matching qualified names. Ambiguous simple-name collisions dropped
   for precision.

3. **Spring `@Value`/`@ConfigurationProperties` → application config.**
   `application.{yml,yaml,properties}` + profile variants parse on the
   framework path; each leaf key becomes a `constant` node qualified by its
   dotted path. `@Value("${k}")` / `@Value("${k:default}")` and
   `@ConfigurationProperties(prefix="X")` emit binding nodes that resolve
   with Spring's relaxed binding (kebab↔camel↔snake).

Validated on macrozheng/mall-tiny: full chain
`UmsRoleController.listResource → UmsRoleService.listResource → impl →
UmsResourceMapper.getResourceListByRoleId → XML <select>` connects across 5
hops via static + synthesized edges. 11/11 @Value annotations resolved
(incl. `@ConfigurationProperties(prefix="secure.ignored")`); 6/6 custom-SQL
mapper methods bridge to XML.

Tests: 4 new integration tests in frameworks-integration.test.ts. Full
suite: 1005 passed.

Docs: CHANGELOG `[Unreleased]` entry + dynamic-dispatch-coverage-playbook
narrative + matrix row.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:34:30 -05:00
c0cf9c1e7d fix(cpp): resolve callers for typed pointer method calls (#445)
Resolves typed member-pointer method calls like `m_cpAlg->Processing()` so `codegraph callers CDetect::Processing` returns the expected callers.

- Extract C/C++ `field_expression` member calls as receiver-qualified references, so `ptr->method()` is preserved as a receiver-aware reference.
- Surface out-of-line C++ method definitions (`int CDetect::Processing() {...}` in `.cpp` with class in `.hpp`) as proper method nodes with the correct qualified identity.
- C++ receiver-type inference: declarator regex requires a terminator after the receiver (rules out matching `return m_cpAlg->...`), handles `Type*x`/`Type *x`/`Type* x` uniformly, and rejects C++ keywords as a final guard.
- `resolveMethodOnType` matches by `Class::method` qualified-name suffix, so out-of-line definitions across files resolve (typical `.hpp`/`.cpp` split).

Validated on bitcoin-core (1306 .cpp files): 38,180 → 40,503 cpp method incoming-call edges (+6.1%), deterministic across re-indexes. Regression test added for the ambiguous-name + `return ptr->m()` / `Type x = ptr->m()` patterns.

Closes #445

Co-authored-by: chenyuxuan <458254969@qq.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:17:07 -05:00