Commit Graph
124 Commits
Author SHA1 Message Date
danusha2345andClaude Opus 5.5 0d46dba08f fix(js,ts): a call on a new C() receiver resolves through C
A TypeScript or JavaScript call on a constructed object, new C(args).m(),
reached the resolver as the bare method name in both engines, so exact
matching bound it to any project method called m: new RegExp(p).exec(s)
to a database wrapper's exec, new Array(n).fill(0) to a text class's
fill, new TextEncoder().encode(s) to a project encode function.

Both engines now emit new C().m (new ns.C().m for a namespaced class, and
nothing for a constructor that is not a plain name or member chain). The
resolver looks m up on C with resolveMethodOnType, inherited methods on
the conformance pass once extends edges exist, an aliased named import
mapped back to its class, and a JS built-in (RegExp, Map, Date...) never
a project class unless the file imports or declares one. An ES5
constructor function keeps its prototype method.

Our changes: the lookup passes the file the call's file imports the class
from (tsImportedTypeFile), so of two classes sharing a name the imported
one is the one constructed (the PR as written took the first indexed copy,
at 0.9); new this.constructor() and a static new this() keep the bare
method name, as a call on this does (the PR dropped three.js's clone
idiom, 29 links to a class's own copy); rebased onto PR #2852, which
edits the same keepsBareTsJsReceiver and test lists; resolved the import
list conflict with rustChainedCallByName from PR #2846; CHANGELOG bullet
moved next to the JS/TS receiver bullets.

Validation: the new tests fail 4 of 6 on main 449eefa7 with both engines
and pass here. A/B against main, kernel per arm: flask, gin and vite
byte-identical. codegraph 48 links added (new ToolHandler(cg).execute), 2
retargeted and 14 typed, all right, 58 removed, 57 wrong (RegExp exec and
test, Array fill, Set add) and 1 right one lost to the grammar's parse of
new nodeSqlite!.DatabaseSync(). excalidraw 22 wrong removed (TextEncoder,
TextDecoder, URLSearchParams, Array fill), 3 typed. pixijs 14 retargeted
and 278 typed, all right, 7 wrong removed. pdf.js 5 added, 47 retargeted
and 68 typed, all right, 76 wrong removed (Array keys and fill, typed
arrays, Response). three.js 33 added, 404 retargeted (Capsule::set to
Matrix4::set and Matrix3::set, NURBSCurve::copy to the class's own copy)
and 429 typed, all right; 135 removed, 133 wrong (UINumber onChange, which
three.js attaches at run time, TextDecoder, typed arrays, URL) and 2 right
only by name (Loader methods inherited through a base imported from the
three package). No default-imported class under another name lost an edge.
Kernel parity holds on all six TS/JS repos and this repository, 0 diffs.
Index CPU time unchanged or lower: codegraph 81.5 s to 79.5 s, three.js
72.0 s to 71.2 s (medians of 3, interleaved).

Original PR #1945 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:34:25 +00:00
Eric MinishandClaude Opus 5.5 449eefa7da fix(js,ts): a call on an element of a class field reaches the element's type
A TypeScript or JavaScript method called on an element of a class field,
like this.handlers[type].handle() or this.items[0].save(), kept the bare
method name: a member chain rooted at this collapses to it so the resolver
can read the owner off the enclosing class. A subscript in the chain leaves
the resolver nothing to read, so the bare name exact-matched any project
method of that name, often another class's (this.listeners[type].push(h)
reached an unrelated UndoStack.push).

Both engines now emit this.<field>[k].<m>() as this.<field>[].<m>, and
this['svc'].m() as this.svc.m; any other subscript in a this-rooted chain,
or under window, emits nothing, as arr[0].run() already did. The resolver
reads the element type the field's declaration writes (X[], Array<X>,
ReadonlyArray<X>, Record<K, X>), on the calling class or a base, and
resolves the method on it or leaves the call unresolved: a type parameter
of the declaring class, a union with another type, an array of arrays or a
non-plain element hold no one type. The class type-parameter reader now
reads from the keyword to the list's own >, over several lines and through
braces in constraints.

Our changes: kept escapeRegExp from PR #2769 in the conflict; the
type-parameter reader also accepts the type keyword, since the T['p'] path
passes type-alias owners and the PR as written read State<Item>['current']
as the project class Item (new case in ts-array-annotated-receivers);
CHANGELOG covers JavaScript and asks for a re-index.

Validation: the element tests fail 10 of 11 on main with both engines; the
type-alias case fails on the PR as written and its brace half on main. A/B
against main, kernel built per arm: flask and gin byte-identical. TS:
pixijs 7 links added, 5 retargeted and 5 typed, all right, 15 removed (9
wrong, 6 right only by name: a cast, a mixin's this, a T[] parameter, a
merged mixin member, an index-signature type, an any field); typeorm 3
wrong removed, 1 right lost, 2 retargeted to the declared interface;
excalidraw 1 wrong removed; vscode's base and editor 19 added, 24
retargeted and 35 typed, all right, 45 removed (37 wrong, 8 right only by
name: a getter, an index-signature field, deep chains, a T[] parameter, a
union alias). JS: three.js, playcanvas and pdf.js
lose 288 bare-name guesses, about 230 of them wrong (array, Map and DOM
calls on project classes). Kernel parity holds on every repo with
deferrals unchanged. Index CPU time within noise (pixijs 32.4 s to 33.3 s,
three.js 64.7 s to 67.9 s, medians of 3 interleaved).

Original PR #2766 by @inth3shadows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:50:28 +00:00
Colby MchenryandClaude Opus 5.5 9c4794d694 fix(rust): a call keeps its whole receiver chain, and links through method calls in it (#2846)
A Rust method call made through more than one field, or on what another
method returns, was recorded by its bare method name in both extractors:
self.services.store.put("a") became `put`, as did self.store().put("m").
Only a single field kept its receiver (self.store.put). The edge's
refName then said nothing of which value the call goes through, and a
call on a method call's value was never typed, so self.store().put("m")
stayed unresolved. PR #2831 had already made the field-chain calls link
by reading the receiver from the source text, but their names stayed
bare.

Both extractors now keep a receiver that is a chain of fields and method
calls on `self` or on a name, a call written `name()` with its arguments
and turbofish dropped, up to 16 links: self.services.store.put,
self.store().put, vis.map.get, self.0.len, x.get().put. The shapes that
already kept a receiver (x.m, self.m, self.<field>.m, Foo::new().m) are
unchanged; a parenthesized receiver, `?`, `.await`, an index and a longer
chain rooted at a path keep the bare name. The kernel's receiver_chain
mirrors the wasm extractor's rustReceiverChain byte for byte.

Resolution reads such a call as the bare-named call was read before
(rustChainedCallByName, at the top of resolveOneIn), so the frameworks,
the strategies and the scope gates see what they saw, and the edge keeps
the call as written. The chain goes along: the written-receiver hook
reads the receiver the chain names where the call is written
(rustChainedReceiver), so a method call's value is typed from that
method's signature, and a later link that calls the same name again is
told apart instead of being left to a guess by the name.

Validation:
- New __tests__/rust-receiver-chains.test.ts (10 tests): the issue's
  workspace verbatim plus self.store().put("m"), chains whose links call
  the same name through self, a tuple field and a parameter, a call on a
  method's std return value, the recorded shapes and the 16-link cap,
  and a sync that adds the missing method. Red on main's sources, 7 of
  10 failing on both the wasm and the kernel path; green with the fix.
  The Rust kernel parity suite gains a chain case. The extraction test
  that pinned the old collapse now pins the chains.
- A/B against main 865ddf4a (same groups against d8338275), both arms
  with the native kernel, every changed call edge read. Per repo: edges
  that only gained their receiver in refName / same target now by type /
  wrong removed / right added / wrong to right:
  ripgrep 3fce3b5b: 159 / 45 / 6 / 6 / 15
  axum 86aa6153: 115 / 25 / 0 / 14 / 7
  tokio 8a2408ed: 1039 / 124 / 36 / 29 / 27, and 1 right link lost
  lemmy 92e077b5: 190 / 24 / 3 / 1 / 3, 1 right lost, 1 wrong added
  The two lost links and the wrong one follow rules the typed-receiver
  reading already applies to typed locals on main: a function-local glob
  `use` does not shadow a file-level named `use` (tokio), a call on a
  dependency's type reaches only impls of project traits, and `x.into()`
  takes the type's explicit `impl Into` (lemmy). Controls gin and
  express: byte-identical dumps. Nodes are identical on every repo.
- Kernel parity (Rust), ripgrep, axum, tokio, lemmy: 1752/1752 files at
  byte parity, 0 deferred.
- Index cost, interleaved median of 3 (wall): ripgrep 3.15 s -> 3.15 s,
  axum 3.39 s -> 3.42 s, tokio 10.48 s -> 10.54 s, lemmy 5.14 s ->
  5.15 s; CPU within noise.

Fixes #2836

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:07:45 +00:00
Colby MchenryandClaude Opus 5.5 16a8e77c46 fix(ruby): a call reaches the class method or the instance method its receiver calls (#2834)
A Ruby class method (`def self.build`, `def Widget.build`, or a def inside
`class << self`) was indexed exactly like an instance method: is_static 0,
under the same qualified name as an instance method of that name
(Widget::build twice). So every call took whichever of the two the class
declared first. Widget.build could reach `def build`, and widget.build on an
instance could reach `def self.build`. In the repro for issue 2503,
`card = Billing::ChargeCard.new(...); card.call` linked to `def self.call`
rather than `def call`.

Fix:
- Extraction, both engines: the Ruby isStatic hook (isRubyClassMethod in
  languages/ruby.ts, is_class_method in codegraph-kernel/src/ruby.rs) marks a
  singleton_method, or a method whose parent past one body_statement is a
  singleton_class, as static. Every other def is marked not static, the same
  way on both, so the graph only changes for class methods.
- Resolution, Ruby only, where the call says which kind it is:
  rubyMethodsOfKindFirst orders a call's candidates with the wanted kind
  first. A call on a constant (Strategy 1, its inherited walk, and the
  namespaced path in ruby-constant-paths.ts) wants the class method. A
  receiver typed by `x = Widget.new` (resolveMethodOnType) wants the
  instance method. A bare call splits only a real pair, one qualified name
  among its candidates as both kinds, keeping the caller's kind: a block
  may run on another object, so nothing else moves. A receiver known only
  by its name (Strategies 2 and 3) says nothing of its kind and is left as
  it was: rubocop's `cop.cop_name` runs over cop classes. With only one
  kind present, every path picks what it did before.

Validation:
- New __tests__/ruby-singleton-methods.test.ts (6 cases, each on the
  kernel and on wasm) and a parity case in kernel-ruby-parity.test.ts. On
  main 9 of the 18 tests in the two files fail, every one that checks the
  new behaviour; the guards and the old parity cases pass. All pass with
  the fix.
- A/B against main f7820b58 (kernel path): nodes are unchanged except
  is_static going 0 to 1 on the class methods, 167 on sinatra, 358 on
  rubocop and 266 on mastodon's Ruby tree. Edges: none added or removed.
  20 changed their target or confidence: sinatra 10, rubocop 0, mastodon
  10. 19 are right: 13 moved from the wrong twin to the right one, such as
  InlineRenderer.render to def self.render and sinatra's instance methods'
  force_encoding to def force_encoding, and 6 kept their target with
  higher confidence. One kept a target wrong in both arms: a call inside
  sinatra's define_method block. Controls express and flask are
  byte-identical.
- The A/B narrowed two rules. Preferring instance methods for a receiver
  known only by its name moved rubocop's 19 right cop.cop_name edges, and
  dropping other-kind methods from any bare call moved block-DSL calls on
  sinatra. Both are gone: only a real pair is split.
- kernel-parity.mjs --lang ruby is the same on both arms: sinatra 147/147,
  rubocop 1799/1801 (2 deferred), mastodon 3333/3333, 0 diffs. The fix
  arm's kernel and wasm graphs are byte-identical on sinatra and rubocop.
- Index time, median of 3 interleaved runs: sinatra -3.7%, rubocop +0.5%,
  mastodon +0.7%, controls express -2.7% and flask +1.0%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:22:15 +00:00
Colby MchenryandClaude Opus 5.5 dcb985f1d2 fix(ruby): a call on a namespaced class links its caller to the class and its method (#2827)
A Ruby call on a constant written with its namespace, like
Billing::ChargeCard.call(...) or ::Foo.call, recorded no edge. Both
extractors (the kernel's ruby.rs and the wasm one) emitted the receiver's
references ref only for a bare `constant` receiver, so a scope_resolution
receiver got none. The call ref itself, Billing::ChargeCard.call, matched
no resolver strategy either: the method-call patterns reject a `::` in the
receiver. A class used only through such calls showed no callers and no
dependents.

Fix:
- Both engines treat a constant path (a constant, or a scope_resolution of
  constants, a leading `::` included) as a constant receiver and emit the
  references ref under the receiver as written. A path through a value
  (obj::Thing, self.class::LIMIT) still emits none, and .new stays an
  instantiation.
- A new resolver step, src/resolution/ruby-constant-paths.ts, resolves a
  Ruby references ref written as such a path, and a calls ref on one, the
  way Ruby looks the constant up: the class or module around the
  reference, then each namespace outside it, then the top level; a
  `::`-rooted path at the top level only. A call reaches the class's or
  module's own method, else the first one its ancestry declares
  (rubyAncestry, now exported). The step runs before the pre-filter,
  which dropped `::`-rooted names, and it is exclusive: a path the project
  doesn't declare (ActiveRecord::Base) or a class without the method links
  nothing, never a namesake by its last name, and production code never
  reaches a class only a test declares.

Validation:
- New __tests__/ruby-namespaced-receiver.test.ts (8 cases, each on the
  kernel and on wasm, plus a sync case) and a parity case in
  kernel-ruby-parity.test.ts. On main 16 of the 22 tests in the two files
  fail (the rest are guards and the old parity cases); all pass with the
  fix. Reverting only the resolver step, or only the wasm extractor,
  turns 13 and 10 of them red.
- A/B against main (kernel path), every edge triaged: sinatra +23 edges,
  rubocop +168, mastodon's Ruby tree (4,363 files) +676; 0 removed, nodes
  identical. 866 of 867 are right: references by the path as written or
  under an enclosing namespace (each such case checked against the call
  site's own nesting), calls to a class method, a module_function method,
  or an inherited class method. 1 is wrong: Paperclip::Transcoder.make
  reaches the instance method `make`, since a singleton method and an
  instance method share one qualified name today; a bare Transcoder.make
  does the same on main. Controls express and flask are byte-identical.
- kernel-parity.mjs --lang ruby: sinatra 147/147, rubocop 1799/1801 (the
  same 2 deferred on main), mastodon 3333/3333, 0 diffs. The fix arm's
  kernel and wasm graphs are byte-identical on sinatra and rubocop.
- Index time, median of 3 interleaved runs: +0.4% to +1.7%, the same as
  the controls (+0.8%, +1.3%).

Fixes #2503

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:20:21 +00:00
Colby MchenryandClaude Opus 5.5 013b91c1be fix(js,ts): a named class expression is a class with its own members (#2825)
A named class expression, like register(class Widget extends Base {})
or module.exports = class Router {}, was not indexed as a class, as
issue #2499 found. Its methods became free functions, its fields and
supertypes were dropped, and calls in it had no class to start from,
so its this-calls, super calls and own static calls went unresolved or
to a by-name guess, and calls elsewhere guessed into its methods.

Both engines now index a named class expression as a class, with its
methods, properties, extends and implements. An anonymous one is still
passed over. Its head (class Widget extends Base) is its signature,
which marks it as an expression, and it is never exported by itself.

JavaScript binds the name only inside the expression, and the resolver
now scopes it the same way. A reference written outside every class
expression resolves in a view of the graph that hides class
expressions and their members, so new Widget() elsewhere links
nothing, and a same-named class, function or binding keeps its links.
A name only class expressions carry is unknown there. A reference
inside one sees the whole graph, unless it names another class
expression. A class a CommonJS module exports, module.exports = class
Router {} or exports.Store = class Store {}, is the module's value,
which require hands to every importer, so it is not scoped: it reads
as a declaration does, and a require of it reaches its static methods
and its instances' methods. The reader of CommonJS named exports takes
exports.X = class Y {} too. Super calls in a class expression now
resolve through the super-call matcher.

Validation: the new test fails 11 of 13 on main d5fc1fbb and passes 13
of 13 on the fix, and a mutation of each of 7 parts (the view, the
inside check, the name rule, the expression marker, the class node,
the CommonJS export rule, the named-export reader) fails a test.
A/B against main d5fc1fbb on 22 repos (20 TS/JS, flask and gin): 193
classes are added in 10 repos with their members, and 801 edge sites
change in 7 repos, each bucketed and sampled by hand. 563 are jest's
calls to its global test(), 0.4 guesses that move from one wrong
namesake to another: the old one, a class expression's test() in an
e2e fixture, is now a method. Of the rest, 85 are new extends,
implements and type references of class expressions; 74 calls, news
and references move to the member they are written in; 65 calls and
news gain or change a target, inside class expressions (this-calls,
own static calls) and through require (static calls, typed instance
calls, new); and 14 guesses are removed, among them 3 super calls
whose parent the index can't name. 12 repos' dumps, the controls among
them, are byte-identical. Kernel parity holds on 22 repos. Three sync
scenarios match a fresh index. Index time, median of 5 interleaved
runs: lit +0.8%, webpack +2.7%, jest +0.6%; insomnia +12.8%, though it
indexes byte-identically, holds no class expression, and its resolver
time is the same on both arms, so that gap is other jobs' load.

Fixes #2499

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:16:07 +00:00
Colby MchenryandClaude Opus 5.5 68fcf2f1c7 fix(js,ts): static and instance methods override only their own kind (#2813)
The override-dispatch pass (interface-impl) linked a base class's
method to every subclass method of the same name, so a static method
counted as overriding an instance one, or the reverse, though a call to
one never runs the other (issue #2761). TypeScript nodes recorded
static already, so there the pass alone was wrong. JavaScript nodes
never did: the extractor had no isStatic hook and the kernel answered
nothing for the JS variants, so a JS class's static s() and s() were
both stored as instance methods.

JavaScript members now record their own static keyword with the hook
TypeScript uses (hasStaticModifier, which also keeps a method named
static() an instance one), and the kernel gives the JS variants the
same answer, so both engines agree. The pass skips a JS/TS base and
override whose static flags differ; a static method still overrides a
static one, and an instance method an instance one.

Validation: the new test fails 5 of 10 on main 08478eea (JS and JSX
static flags in both engines, and the override pairs) and passes 10 of
10 on the fix. A/B against main on 20 repos: webpack's lib loses 15
edges, every one a cross-kind pair, such as Module's instance
deserialize(context) to the static deserialize(context) factories of
NormalModule and six other modules, or Module's static
getSourceBasicTypes(module) to RuntimeModule's instance
getSourceBasicTypes(); the same-kind pairs stay and no edge is added.
No other repo's edges change. Nodes change only in is_static, 0 to 1,
on 315 JavaScript static members, none in TypeScript or another
language; the flask and gin control dumps are byte-identical. Kernel
parity over the 20 TS/JS repos: 16,612 of 16,873 files byte-parity,
none with diffs, the rest deferred to wasm as before. Index time,
median of 3 interleaved runs on vite, nuxt, webdriverio, insomnia and
webpack: -1.1% total, within noise.

Fixes #2761

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:08:41 +00:00
Colby MchenryandClaude Opus 5.5 d82a4adc9d fix(js,ts): a call or new in a parameter's default value is the function's call (#2802)
A default runs inside the function each time a call leaves the argument
out, but both extractors pushed the function and walked only its body,
so `function renderWithNote(note = buildNote())` recorded nothing for
the call: no edge and no unresolved reference. Arrows, methods,
constructor parameter properties, class-field arrows, object-literal
methods and destructured parameters lost theirs the same way. Only an
anonymous arrow inside a body was walked whole, params included.

The TS extractor (visitParameterDefaults) and the kernel's TS/JS walker
(visit_parameter_defaults) now hand each default value, and a computed
key in a destructured parameter, to the body walker, with the scope the
body gets on the stack: the function for a named function, method or
component, the enclosing scope for an anonymous function or an
object-literal method. It runs before the body walk at every site that
walks only a body, including a class field's arrow, a forwardRef/memo
render function and a styled .attrs() function. Patterns are recursed
through; the names a parameter binds, its type annotation and a bodiless
signature are not walked. TS/JS only: a Python default runs once, when
the def does, and is left as it was.

Validation: the new test fails 13 of 14 on main 5d81d292 and passes on
both engines, LF and CRLF; the kernel parity fixtures gained a defaults
section. A/B against main on 14 TS/JS repos: 118 edges added, none
removed or retargeted, each a call or `new` written in a default (nuxt
45, mostly kit utilities' `nuxt: Nuxt = useNuxt()`, webdriverio 30,
memfs 17, jest 7, vben 5, social-app 5, vite 4, outline 3, nitro 2;
insomnia, t3-turbo, vue3-antdv-admin, pathe and c12 none). Seven ride
resolution the body already gets: six Vue/Nuxt auto-import self-links
and one member call on memfs's `export default createProcess()`. The
flask and gin control dumps are byte-identical. kernel-parity.mjs over
the 14 repos: 15,408 files byte-parity, 0 with diffs. Index time,
median of 8 interleaved runs on vite, nuxt, webdriverio and insomnia:
+1.6% total, within noise (the last 5 rounds alone: -1.7%). A CLI sync
that adds defaults, adds a file and gives an unchanged file's default
its callee matches a fresh index.

Fixes #2588

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:52:16 +00:00
Colby MchenryandClaude Opus 5.5 c326b6bec1 fix(go): a function value keeps its link past a local of the same name (#2776)
In Go, a function used as a value, like `apply(run)`, was recorded once
per function and name, at its first use. Since PR #2658 the resolver drops
a use that a local or parameter of that name shadows. When a function's
first use was the shadowed one and a later use, outside the local's scope,
was the package function, that later use had no reference left, and the
function lost its references edge to the package function (an inner-block
`run := 2`, a closure's `func(run int)`, a case clause, an if header or a
range variable, then `apply(run)`). Issue #2596 predicted this.

Fix, in both extractors (function-ref.ts with tree-sitter.ts's
flushFnRefCandidates, and the kernel's go.rs flush_fn_ref_candidates):
- A Go function that binds the name itself records one use per scope
  instead of one per function. Bindings are a func literal's parameters,
  :=, var, const, range and type-switch variables and a select case's
  value. Scopes are Go's: a block, an if/for/switch/select statement's
  implicit block and each case clause. Within one scope a local declared
  before the first use also covers every later use, so the first use
  still speaks for the scope.
- The function's own receiver, parameters and results keep a single
  reference: they cover the whole body.
- A func literal at file scope is its own unit.
- Every other function is unchanged. The scope and enclosing function are
  read from the tree only for names a function uses at least twice, after
  the name gate (reading them for every container made kernel extraction
  of prometheus 2.4x slower).

Validation:
- go-shadowed-function-value.test.ts: 14 cases, kernel and wasm, LF and
  CRLF. On main's sources and kernel 10 fail; with the fix 14 pass.
  Per-scope keys without the binding check, and dropping range
  variables, case clauses or func literal parameters, each fail 3.
- Kernel parity, Go: gin 99/99, etcd 1065/1105 (40 deferred, as main),
  prometheus 746/746, kubernetes 17779/17952 (173 deferred, as main),
  0 files with diffs.
- A/B against 6bd217a2 (fresh indexes, each arm with its own kernel):
  gin 0f09c3a9, etcd bf9cadf0, prometheus 017fcf2e and kubernetes
  006cdf87 keep identical nodes and edges. The only change is 0, 4, 13
  and 163 more function_ref rows, all failed: per-scope uses of a local
  (all 17 on etcd and prometheus and 36 on kubernetes read by hand).
  express and flask dump byte-identically.
- Cost: kernel extraction of prometheus 2.90 s vs 2.90 s, wasm 6.06 vs
  6.07 s; kubernetes 34.9 vs 34.9 s; prometheus index medians
  12.35 s / 49.1 s CPU vs 12.45 s / 48.3 s over 4 interleaved rounds.

Fixes #2596

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 02:48:37 +00:00
Colby MchenryandClaude Opus 5.5 bf6f1fa67e fix(dart): a typedef references the types it aliases (#2763)
In

  class Report { final int score; const Report(this.score); }
  typedef ReportCallback = void Function(Report r);

`codegraph callers Report` did not list ReportCallback. Both extractors
minted the typedef's node and nothing else. tree-sitter-dart gives
`type_alias` no fields, so extractTypeAlias found no `value` to read and
had no Dart branch, and the kernel's extract_type_alias mirrored that.
Impact on a model type missed every callback or collection type built
on it. The legacy form with a return type, `typedef Report
LegacyMaker(int n);`, was also named after that return type: with no
`name` field the name fell back to the first type name in the
declaration, so a `type_alias Report` (or `Widget`, `Future`, `bool`)
stood beside the real type and took references meant for it.

Both extractors now name a typedef by the last type name before its
`=`, its type parameters or its parameter list, and add a `references`
ref from the typedef, on each type's name, for every type its aliased
type names: a function type's return and parameter types, generic
arguments, record fields, a type parameter's bound, and the legacy
form's return and parameter types. Annotations are not part of it, an
import prefix names no type, and a type parameter (the typedef's own,
or one a generic function type or an old-style generic function-typed
parameter declares) is skipped unless written through a prefix. Kernel
and wasm output are identical; a new parity fixture,
TortureTypedefs.dart, pins every form in LF and CRLF.

Validation, kernel path, arms built from dbb6aa9b and from this change,
each with its own kernel:
- felangel/bloc b9be1e25: 17,875 -> 17,884 edges (+9, 0 removed).
- rrousselGit/riverpod 4ded4f01: 84,726 -> 84,761 (+35, 0 removed).
- jonataslaw/getx e3c8fa04: 8,436 -> 8,456 (+20, 0 removed).
- alibaba/flutter-go a41b2124 (9 legacy typedefs, 6 with a return
  type): 7,994 -> 7,968; 39 wrong links removed (34 `Widget` and 5
  `Future` uses that had gone to typedefs misnamed `Widget` and
  `Future`), 13 added (4 from typedefs, 9 fields typed by the renamed
  ones).
- peng8350/flutter_pulltorefresh 0be096c1 (8 legacy, 5 with a return
  type): 3,186 -> 2,976; 221 wrong `Widget` links removed, 11 added.
All 88 added non-contains edges were checked against the source; every
removed edge pointed at a typedef misnamed after its return type. The
other new typedef refs name SDK or Flutter types and stay unresolved.
gin and zustand dumps are byte-identical across the arms, kernel and
wasm dumps are byte-identical on bloc, riverpod and pulltorefresh, and
the parity sweep over the five Dart repos is 2,330/2,633 files at
parity, 0 diffs, 303 deferred on both arms. Index time is unchanged
within noise (median of 5 interleaved runs: riverpod 12.6 s vs 11.9 s,
bloc 5.0 s vs 5.0 s, getx 2.8 s vs 2.8 s), and the DB grows by 8 to
24 KB (pulltorefresh shrinks by 64 KB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:46:20 +00:00
Colby MchenryandClaude Opus 5.5 847f1df77d fix(go): an interface or struct embedding a standard-library type counts its methods (#2759)
goImplementsEdges (#584) follows declared embeddings since #2402, but an
embedding of a type outside the index resolves to nothing, so it added no
methods: prometheus's TBRun (testing.TB plus Run) went to a struct with only
a Run, QueryLogger (slog.Handler plus io.Closer) had no implementations,
kapacitor's Diagnostic (a third-party Logger plus Log) took every struct with
a Log, and the predeclared `error` was not recorded at all.

- go-stdlib-methods.ts lists the methods of 130 standard-library types, each
  as `name:params/results`, generated from go1.27.3's source and counted the
  way goSignatureArity counts a stored signature.
- Both extractors record an embedded `error`, which stays unresolved. The
  predeclared-type set is unchanged, so alias and defined-type references
  still skip it.
- goImplementsEdges reads each Go type's failed `extends` rows and reads the
  package back off the line (goRefQualification, now exported). A
  standard-library type's methods join the method set, keyed as goMethodKey
  keys the project's (`std:<import path>` for an unexported one), and the
  arity gate holds them to the table's counts on both sides: a `sync.Mutex`
  makes a struct hugo's `Locker`, not etcd's gRPC `LockClient`. Any other
  outside type is met by a type embedding the same one, or by the only type
  with every other method the interface names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:13:54 +00:00
Colby MchenryandClaude Opus 5.5 51be28d15b fix(extraction): a function in an initializer is referenced once, by what the initializer declares (#2743)
PRs #693 and #1802 walk a declaration's initializer under the symbol it declares,
so `const routes = { home }` references `home` from `routes`. The
dispatcher's declaration-wide function-as-value scan then took the same
values again for the enclosing file (or class). Flush-time dedup keys on
the source, so both stayed: a second reference to the same function from
the same spot.

Each initializer walk now records what it walked, and the scan skips it.
PR #2738 added that mechanism for component wrappers, and C#, Scala and Dart
already had their own. The newly covered paths:
- TS/JS module-level constants and variables, owned object literals
  included (#2300), and TS/JS class properties;
- Python assignments, Go var/const specs, Rust const/static items;
- Java fields, and Kotlin properties (walkInitializer takes the property's
  id, so their walk is unchanged).
Mirrored in the kernel: tsjs, python, go, rustlang, java and kotlin.

References no initializer walk takes are unchanged: a module-level call
argument `register(fn)`, C-family tables, Rust's `static CB: fn() = handler`
(the declaration's own value, which the scan still reads).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:41:45 +00:00
Colby MchenryandClaude Opus 5.5 ae0fc631e0 fix(js,ts): a callee that names nothing records no reference, and a wrapped callee is the plain call (#2742)
A TS/JS call whose callee was no name and no member access (an IIFE, a
curried `connect(mapState)(View)`, `handlers[type]()`, `(0, fn)()`, a
tagged template on a call) stored the callee's whole source text as the
reference's name. Nothing can answer such a name, so each one parked as a
failed reference, an IIFE's entire body among them. extractCall now reads
the callee past the wrappers that keep it the same callee (parentheses,
`!`, `as` / `satisfies`, `await`, type arguments) and records nothing when
that is not a name or a dotted chain; the calls inside are visited on
their own. Mirrored in the kernel (tsjs/extractors.rs callee_name_of).

The reference of a wrapped callee sits at its name, where a plain call's
does, and the resolver's bare-call check reads `fn!(`, `(fn)(`,
`(fn as F)(` and `(fn satisfies F)(` as it reads `fn(`: SigNoz's
`(callback as FrameRequestCallback)(0)` and `(callback)(0)` name the
arrow's own parameter, and from the `(` they bound a generated interface's
`callback` property.

tree-sitter-typescript parses a prefix operator over a generic call as it
parses `await f<T>()` (#2586): `!settings.get<boolean>('X')` and
`void apiRequest<T>()` are calls whose callee is `!settings.get` /
`void apiRequest`. The callee peel that handled `await` now peels a prefix
operator too (peelTsPrefixedCallee; the kernel's peel_await_callee), so the
call is to `settings.get`, and `void apiRequest<T>().then()` encodes as
`apiRequest().then`, not `voidapiRequest().then`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:01:53 +00:00
Colby MchenryandClaude Opus 5.5 2c4a16d091 fix(react): a wrapper component renders the component it wraps, and owns the calls in its styles (#2738)
A component declared through a wrapper with no render function of its own
(`styled.div`…``, `styled(ActionButton).attrs(…)<Props>`…``, MUI's
`styled(Box)(({ theme }) => …)`, `memo(Button)`) was a bodyless component
node: nothing walked its initializer, so the calls in its styles went
nowhere, and nothing linked it to the component it wraps, so a render flow
stopped at it. On outline, explore printed no Flow for
"OverflowMenuButton NudeButton ActionButton performAction".

Extraction (TS and the native kernel) now walks the initializer with the
component on the stack: the template's interpolations, the .attrs() /
.withConfig() / options arguments, a style function and a memo comparator
are the component's code. The wrapper's own chain (styled, styled.div,
.attrs, memo) names the library and is not recorded; a typed tag's type
operands are not walked; and the wrapped component is no function-as-value
reference, from the component or from the file (the declaration-wide scan
skips the walked initializer). A component is also a value-ref reader, as
a function is.

The jsx-render synthesizer links a wrapper to the component it wraps
(`wrappedComponentName` in frameworks/react.ts, beside `wrappedName`),
bound as a tag of that name in the file would be, but only where the file
binds the name by a declaration or an import of a project module, so a
package's styled(Link), a Meteor /imports/… path and a destructured name
render nothing rather than a namesake. Files that declare such wrappers are
synthesis inputs, so a sync redraws the link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 09:29:05 +00:00
Colby MchenryandClaude Opus 5.5 f66d064279 fix(cpp): a function is named by its declarator, never by its noexcept, throw or requires clause (#2727)
Both C++ extractors name a function by the first qualified identifier
found breadth-first in its declarator, which is how an out-of-line
`Foo::bar` is named. The walk skipped only the parameter list and the
trailing return type, so a qualified name in the clauses after them
became the name of a function whose own name is plain. nlohmann/json's
constructor `basic_json(CompatibleType&& val) noexcept(noexcept(
JSONSerializer<U>::to_json(…)))` was indexed as a method
`JSONSerializer::to_json` that doesn't exist, with no node for the
constructor; `throw(std::bad_alloc)` named a method `std::bad_alloc`,
and a trailing `requires std::integral<T>` a function `std::integral<T>`.

The walk now also skips `noexcept`, `throw_specifier` and
`requires_clause`, in the TS extractor and the kernel alike; the parity
fixture covers all three.

Measured on main 226ac930 with fresh indexes, and byte-identical again
after syncing onto e97012eb: json 42, abseil 28 and
protobuf 1 phantom nodes become the real functions at the same lines;
fmt, leveldb, rocksdb, redis and three controls in other languages are
byte-identical. Kernel parity sweeps on json, fmt, abseil, leveldb and
rocksdb find no file with diffs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 04:20:15 +00:00
Colby MchenryandClaude Opus 5.5 e97012eb31 fix(ts): an interface links the interfaces it extends (#2726)
tree-sitter-typescript holds an interface's supertypes in an
extends_type_clause, which neither extractInheritance (wasm) nor the
kernel's extract_inheritance read: a class's extends and implements
became edges, an interface's extends nothing. So an interface had no
place in the type hierarchy, a change to it reached none of the
interfaces built on it, and a call through a composite interface whose
head the receiver typing could not read fell to name matching
(home-assistant/frontend's hass.formatEntityName linked to a namesake
in an unrelated file).

Both engines now emit one extends reference per type of the clause: a
name, a qualified name as written (shapes.Shape), or a generic type by
its name field (Repo for Repo<User>). They resolve as a class's do, so
a package's type stays unresolved, and a namespace import or a module
the repository declares resolves.

A supertype a TS/JS module names without importing it is now its own
declaration or a global one, at a script's top level or in declare
global, never another module's or a namespace's: vscode's interfaces
extending Omit<...> linked to quickInput.ts's export type Omit, and
extends Function to terminal-suggest's Fig.Function.

A call on a TS/JS type whose declaration is known now takes the method
from that declaration's own supertypes. The walk by name, which unions
the supertypes of every same-named type and looks the method up by name
again, follows only classes' supertypes, as before interfaces had any.
Otherwise vscode's receiver.dispose() on an ILocalOtlpHttpReceiver
extends IDisposable reached copilot's own IDisposable, one of eleven,
and stream.on() on a NodeJS.ReadableStream the project's
ReadableStreamEvents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 03:44:24 +00:00
Colby MchenryandClaude Opus 5.5 bd62d9c061 fix(rust): a turbofish call reaches the function the turbofish wraps (#2720)
A Rust call written with a turbofish (`s.get::<u32>()`, `helper::<u8>()`,
`Pool::<u8>::new()`) kept its type arguments in the callee's name
(`s.get::<u32>`), which no resolution strategy reads, so every such call
was left unresolved. Both engines now name the function the turbofish
wraps and drop a path's type arguments (`Pool::<u8>::new` is
`Pool::new`), so the call resolves as it would without the turbofish: on
a typed receiver, on `self` or a field of `self`, and through an
associated-function chain. The kernel and wasm extractors emit the same
bytes; the parity fixture covers nested type arguments and a `->` inside
them.

A/B against main with fresh indexes, sites matched by position: clap
197 calls linked, dioxus 251 linked and 1 removed, axum 69, tokio 143,
serde 48, ripgrep 3; gin and express are byte-identical. About 680 of the
new links are right: typed method calls (clap's
`matches.get_one::<String>()` to `ArgMatches::get_one`, dioxus'
`self.query.new_query::<T>()` to `QueryEngine::new_query`) and generic
free functions (tokio's `channel::<T>()`, dioxus' `consume_context::<T>()`,
serde's `assert::<T>()`). About 22 are weak guesses at confidence 0.5 or
below, where an instance chain's method becomes a bare name, as it is for
the same call without a turbofish (dioxus' `.collect::<Vec<_>>()` to
`ServerFunction::collect`; main already makes 23 of those for
`.collect()`). The one removed link was wrong on main:
`serde_json::from_str::<T>(...).context(...)` is no longer guessed as
`DiffState::context`. The 54 sites reported as retargeted are positions a
chain's inner and outer calls share, where an edge was added beside the
existing one. Full suite on the tested tree: 8,037 passed, 0 failed.

Follow-up 1 of the Rust receiver series (PR 2719's open list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:54:30 +00:00
Colby MchenryandClaude Opus 5.5 a33040f786 fix(js,ts): an import of a package the repository declares itself links to that declaration (#2718)
A `declare module 'mammoth' { ... }` in a script .d.ts declares the module
outright, and TypeScript takes it before any file the specifier resolves to.
Nothing mapped a package name to a file in the repository, so an import of
such a package linked nothing, or, where the out-of-repo guard did not fire
(VS Code's extensions list no `vscode` dependency), a project namesake. A
bodiless function in an ambient context produced no node either, so
`convertToHtml` had nothing to link to.

- ambient-modules.ts: a token scanner for `declare module` blocks, a
  per-context index of the project's declaration files, and the lookup the
  import resolver makes: named, default (`export default`, `export =`),
  export-clause and namespace-member imports, by position in the block.
- The import resolver consults a script declaration before a file a path
  alias maps the specifier to (copilot's `paths` shim), and the import's
  module reference and namespace or default binding link the declaring file.
- Extraction, both engines: a function_signature under `declare`, in
  `declare global`, at a declaration file's top level or directly in its
  `declare module` body is a function; never in a namespace, nor in an
  ordinary module file's `declare module`, an augmentation.
- A module's function is reached from another file only through an import.
- A type merged across the files declaring one module is no single
  declaration when a receiver is typed.
- Sync re-opens the importers of a package a changed declaration file
  declares.

Augmentations, wildcard patterns, shorthands and test-fixture declarations
are left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:35:02 +00:00
Eric MinishandClaude Opus 5.5 642c7bb3a5 fix(python): attribute calls follow the type the class body, a factory or a base gives the attribute (#1926)
Python calls through an attribute reach the method the code gives that
attribute: `self.sink.stop()` follows `self.sink = Sink()`, a `sink: Sink`
annotation, a typed `__init__` parameter, a factory's return type or what
every `return` in it builds, and a base class's `__init__` in another file.
Calls straight on a factory's result (`Client.from_env().send()`) resolve
the same way, and a sync re-opens these answers when the factory or base
they were read from changes. The native Python extractor now keeps a call's
receiver text as the wasm one does, so `self.data.append(1)` is never a bare
`append` that matches an unrelated project function. This includes all of
PR 1704.

Reworked from evidence-or-silence, which dropped about 3,000 correct Django
links (`self.client.get()`, `self.engine.render_to_string()`). The reader
now has three answers. A project class that has the method: that method.
Evidence that rules project methods out: no edge, for a builtin, a class
from outside the project built in a method or named by an annotation, an
attribute only an outside base can give (Django's `TestCase` in an app), or
a class without the method whose bases are all known. Nothing typed: the
call is guessed by the name of its last link, read as written, as any
receiver is, but only for an owner whose name holds the link's head noun
(`self.session` for `SessionBase`, not `database.group_shopping_lists` for
`GroupRepositoryAIProvider`) and never as the one method of a name; the
guess records the files the reader looked at, so a sync re-opens it too. A
class-body field an outside class builds (`author = models.ForeignKey(Author)`)
may be a descriptor, so it is no evidence. A chain through an imported
module (`os.path.exists()`) is the module's. In the shared scoring, a
receiver that spells an owner's whole name (`client` for `Client`)
outscores one that only shares its word (`RedisCacheClient`). A call
through an imported value's attribute chain (`app.jinja_env.filters.keys()`)
no longer links to the value's `jinja_env`.

A/B against main with fresh indexes, every changed edge triaged, sites
matched by position since the extractor now names these calls by their
whole chain. Django: 3,317 calls linked that were not
(`self.client.force_login()` to `ClientMixin`, `User.objects.create_user()`
to `UserManager`, `self.stdout.write()` to `OutputWrapper`), 127 moved,
nearly all to a more precise target (`self.site.register()` to
`AdminSite.register` rather than the `register` decorator), and 134
removed: main's bare-name links on builtin values (`value.lower()` to the
template filter, `.encode()` to a test password hasher) and calls on
expressions such as `(q1 | q2).order_by()`, which now go unresolved as they
do in the wasm engine. flask: 32 linked, 4 moved to `JSONProvider`.
fastapi: 58 linked. mealie: 740 linked. Guesses still wrong, as main makes
them for a plain receiver of the same name: fastapi's
`request.headers.get()` to a telemetry `_HeadersGetter`, and about 20 of
mealie's repository attributes to a controller. Go, C#, Ruby, Swift,
Kotlin, Java and TS repositories are byte-identical, except excalidraw's
5 `props.onChange()` calls, which move between two unrelated `Props`
interfaces. Indexing CPU: Django +7%, mealie +5%, fastapi flat; wall time
unchanged within noise. Full suite on the merged tree: 7,999 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:00:13 +00:00
Eric MinishandClaude Opus 5.5 8afb970a23 fix(ts): an awaited call with type arguments links to its callee (#2595)
tree-sitter-typescript parses `await f<T>(x)` as a call whose callee is
`await f`, so the reference was named `await f` and matched nothing.
Both engines now peel the await, skipping a comment after it, and the
reference sits where the callee starts, as for `await f(x)`: on the
call node, which starts at await, the checks that read the source at a
call's position missed the callee, and a package-imported
`await get<T>()` linked to a project class's static get.

A/B: excalidraw +2 (compressData, decompressData), vben +6, zod and
bulletproof-react unchanged.

Fixes issue 2586.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:11:12 +00:00
Colby MchenryandClaude Opus 5.5 b5553e238a fix(rust): an impl's associated type or const no longer takes a bare name or a module path (#2707)
A `type` or `const` written inside an `impl` belongs to the type: Rust
reaches it only through a path like `Self::Err`, `<T as FromStr>::Err` or
`T::Err`, never by its name alone, and no module path or `use` names it.
Both extractors put it at file scope under its bare name, so the resolver
could not tell it from a module's own alias:
- a same-file `Err(…)` call linked to a `FromStr` impl's `type Err`, and
  serde's `Ok(…)` calls to a serializer's `type Ok`;
- axum's routing tests' `Response` linked to the `type Response` of
  `routing/mod.rs`'s `impl Service`, a private item the modules inside
  it can see;
- `io::Error` and `std::error::Error` linked to the `type Error` of an
  impl in the same file, and serde's `use crate::de::Error` to one in
  `de/mod.rs`.

Fix:
- Both extractors (rustImplItemOwner, and the kernel's impl_item_owner)
  qualify an item written directly in an impl by the impl's type, as they
  already qualify its methods: `ValueHint::Err`, `Limits::MAX`. A type no
  single name stands for keeps the impl's spelling, whitespace collapsed:
  `(A, B)::First`, `[u8]::Out`, `u64::WIDTH`.
- isRustAssociatedItem: a Rust type alias or const with a qualified name.
  Nothing else of those kinds gets one: an inline module adds no prefix,
  a function body's items aren't indexed, and a trait's default `type`
  is already its trait's.
- isRustNameInScope: such an item never matches a bare name, and matches a
  path only as `Self::X` in its own impl or as `<…>::X` (unchanged).
- resolveRustPathReference never picks one for a `crate::m::X` path.

Tests: rust-associated-item-scope.test.ts, 6 tests, on both engines; 5
fail on origin/main. The kernel parity torture fixture gains associated
items on a tuple, a slice reference and a primitive.

A/B on fresh indexes, origin/main 5530676f vs this change. Every changed
edge was triaged; no right link was lost:
- axum: 8 wrong links gone, 14 moved to axum-core's `pub type Response`.
- tokio: 46 wrong links gone.
- clap: 12 wrong links gone, 7 of them `Err(…)` calls.
- serde: 98 wrong links gone; `First<…>`/`Second<…>` now reach the
  module's aliases.
- ripgrep: 54 wrong links gone; `impl Captures for RegexCaptures` (3)
  now implements grep-matcher's `Captures` trait.
- gleam: 5 wrong links gone, 10 moved to the right target, 1 moved from
  one wrong target to another.
- dioxus: 71 wrong links gone, 40 moved to the right target, 3 moved from
  one wrong target to another.
- Controls (gin, serilog, flask, express) byte-identical; indexing time
  unchanged within noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:57:03 +00:00
Colby MchenryandClaude Opus 5.5 3273f20f47 fix(objc): a doc comment inside an @implementation documents the method below it (#2696)
Inside an Objective-C @implementation, tree-sitter-objc wraps each
method, function and declaration in an implementation_definition of its
own, and the comment above it is the wrapper's sibling, not the method's.
getPrecedingDocstring looked only at the method's own siblings, so no
method, function or static written in an @implementation had its doc
comment, while the same comment above an @interface property was read.

- tree-sitter-helpers.ts: implementation_definition joins
  DOCSTRING_WRAPPER_TYPES. Each wraps exactly one item, so the climb
  cannot hand a comment to a neighbour: a comment above an @synthesize
  stays its own.
- codegraph-kernel docstring.rs: the faithful port's wrapper list gains
  it too. The kernel does not walk Objective-C, so the entry only keeps
  the two lists alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:16:23 +00:00
Colby MchenryandClaude Opus 5.5 65d734236e fix(rust): a private item, or one another crate holds, no longer takes a name a file imports (#2692)
A Rust type alias declared without pub recorded no visibility in either
engine, so name matching took dioxus's private type Result in
src/build/patch.rs for the Result<...> a file means through
use crate::Result. Type aliases now record it, as functions and structs
already do, and a private module item is out of scope outside its own
module ahead of the ranking. The pipeline's veto already rejected such
a pick after the ranking, which left the reference unresolved: that is
what kept tokio's timeout(...), split(...) and sleep(...) calls from
linking, since a private namesake outranked the function they import.

With the private items gone, a runner-up from an unrelated crate could
win instead: tokio's tests' spawn(...) calls, under
use tokio_test::task::spawn, went to the examples crate's pub fn spawn.
So a name a use binds is now an item of the crate its path starts from,
or of a project crate that one's [dependencies] name, in turn, which a
facade's re-exports need: clap::Command is clap_builder's. crate::,
self:: and super:: start from the file's own crate; a named root counts
when the file's own manifest lists it. Dependencies are found by path
(their own, or the workspace's for workspace = true), so renamed keys
and crates outside the members land, and dotted keys such as
helper.workspace = true are read. Dev and build dependencies are not
re-exported, so they don't count: tokio's own use crate::spawn, whose
spawn sits in a macro the index doesn't see, stays unresolved instead
of going to tokio-test's. A file in no crate the manifests name, or a
path from anything else, is matched as before.

reference-target-kind's sibling-crate fixture re-exported pupil_core
without depending on it, which Cargo rejects; it now declares it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 00:44:52 +00:00
Colby MchenryandClaude Opus 5.5 3308cff071 fix(swift): a call through a prefix operator, try or await keeps its callee and receiver (#2686)
tree-sitter-swift binds a prefix operator to the leftmost operand:
`!isReady()` parses as `(!isReady)()`, and `!store.contains(id)` as
`(!store).contains(id)`. `try` and `await` before a chain bind to the part
before its last call: `try! Decoder().decode(d)` parses as
`(try! Decoder()).decode(d)`. Both engines read the callee or receiver node
as written, so the call was named `!isReady`, which never resolves, or it
lost its receiver and name-matched whichever `contains` or `decode` came
first; a type path behind a `!` lost its path too.

Both walkers now strip these wrappers (a prefix operator other than an
implicit member's `.`, `try` and `await`) off a call's callee and receiver.
They read a receiver chain's text from its innermost wrapped operand on, for
the chained-call encoding and the type-path receiver, and look through a
prefix operator for the type a member read references: `!Config.shared.isOn`
references `Config`. An operator applied to a parenthesized value, `!(x)` or
`-(a + b)`, parses as a call of the operator and now emits no call.

swift-wrapped-call-receivers.test.ts checks the edges, and
TortureWrappedCalls.swift pins both engines' refs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:31:31 +00:00
Colby MchenryandClaude Opus 5.5 c58f0417c6 fix(swift): a call written after try! links what it calls on Windows (#2683)
The Windows kernels read the `!` of every `try!` as a logical not, so
`try! Message2(data: d)` became a call of `! Message2` that never resolves,
and a call through a receiver lost it: `try! Settings.Defaults.load()` was a
bare `load`.

The cause is the grammar's C, not the walker. tree-sitter-swift 0.7.3's
external scanner steps aside for the `!` of `try!` through a 64-bit mask
whose entry is built as `1UL << FAKE_TRY_BANG`, bit 32. `unsigned long` is 32
bits on Windows and MSVC folds the overflowing shift to 0 (warning C4293),
so the scanner never stepped aside: `try` parsed alone, then a prefix `!` on
the callee. macOS and Linux have 64-bit longs, and clang's wasm32 build folds
the shift to bit 31 (`_directive_endif`, valid at every expression start),
so the wasm and the other kernels have always read `try!` right.

The kernel now compiles the crate tarball's src/ itself, from
codegraph-kernel/grammars/swift, with upstream 0.7.4's one-line fix (`1ULL`),
as it already does for the Kotlin, Lua, Scala and Dart grammars. The patch and
its provenance are in docs/grammars/tree-sitter-swift.patch and .md. parser.c
is unchanged, so the node-kind and field tables still match the wasm, which
is untouched.

TortureTry.swift pins calls after try!, try? and try on both engines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:49:17 +00:00
Colby MchenryandClaude Opus 5.5 22e9f7b337 fix(go): variables declared in a var ( … ) block are indexed (#2682)
tree-sitter-go wraps the specs of a var group in a var_spec_list node,
unlike a const group's, which sit directly in the declaration. Both
extractors read only the specs directly inside a declaration, so every
variable declared in a var ( ... ) block was missing from the graph: no
node, no call or construction recorded from its initializer, and nothing
for code that reads it, or for a call made through it, to point at.

Each member of a var group is now a variable, as one declared on its
own already was, with its own doc comment or the group's, and its
initializer walked under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:23:07 +00:00
Colby MchenryandClaude Opus 5.5 6e68a11beb fix(go): a constant in a const ( … ) group keeps its own doc comment (#2680)
Both extractors read every member's docstring from the comment above
`const (`, which documents the group. A constant documented inside the
group, like gin's ErrorTypeBind under "ErrorTypeBind is used when
Context.Bind() fails.", stored no docstring, or the group's comment
instead of its own.

A member's own doc comment, read with Go's doc comment rules, is now its
docstring. A member without one keeps the group's comment, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:01:42 +00:00
Colby MchenryandClaude Opus 5.5 f43432c86a fix(go): a comment directive like //go:embed is not part of a declaration's doc (#2679)
go doc leaves comment directives out of a doc comment's text: a // comment
with no space after the slashes that is //line, //extern or //export
followed by text, or that opens with a lowercase word, a colon and a
lowercase letter or digit, like //go:embed static or //nolint:errcheck
(go/ast's isDirective). Both extractors kept them, so prometheus's
testsFs had the docstring "go:embed testdata", etcd's store.restore
"nolint:unparam", and a doc comment with a directive under it ended in
that directive.

Go now drops directives from the docstring's text through a Go-only
extractor option (docstringSkipsDirectives in TS; the kernel's Go
docstring helper becomes preceding_go_doc_comment, which applies all
three Go rules). A directive still counts as a comment when deciding
where the doc comment starts, as in go/parser, so a declaration whose
only comment is a directive has no docstring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:45:40 +00:00
Colby MchenryandClaude Opus 5.5 23f728fac2 fix(go): a comment a blank line separates from a declaration is not its doc (#2678)
Go's doc comment, the one go doc shows (go/parser's lead comment), is the
comment group that ends on the line right above a declaration: comments
on consecutive lines form one group, and a blank line ends it. Both
extractors joined every comment above a Go declaration instead, so a
section banner, a TODO or commented-out code above a blank line became
part of the declaration's docstring, or its whole docstring when the
declaration had no doc of its own.

Go now keeps only that group, through a Go-only extractor option beside
the trailing-comment one (docstringSkipsDetachedComments in TS,
preceding_docstring_skipping_trailing_and_detached in the kernel). A
comment that ends on the declaration's own line documents nothing, as
in go doc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:28:12 +00:00
Colby MchenryandClaude Opus 5.5 383c2cd9e3 fix(go): an interface's methods link the types their signatures name (#2659)
A Go interface's methods are `method` nodes, but their signatures named
no types: etcd's `KV.Put(ctx context.Context, key, val string, opts
...OpOption) (*PutResponse, error)` had no edge to `OpOption` or
`PutResponse`. A call through the interface reaches that method, so
impact on those types missed the interface method and the callers that go
through it.

TS extractor and Rust kernel, mirrored:
- extractGoInterfaceMethods / extract_go_interface_methods give each
  method a `references` ref per type name in its parameters and results
  (goInterfaceMethodTypeNames / interface_method_type_names, on #2655's
  collectGoTypeNames).
- Skipped: the predeclared types and, written bare, the interface's own
  type parameters (goTypeParameterNames / type_parameter_names, split out
  of goDeclaredNames). The interface's own name is a type its method names
  (`Children() []Node`), so it is kept.

A/B on main 6692e051: gin +34, etcd +775, prometheus +294, kubernetes
+11079 edges; every one judged against the real package clause (bare →
own package, `pkg.T` → the import's package, kubernetes' one dot import
included); 0 dropped/retargeted/metadata-only. Kernel and wasm indexes
are identical. Explore keeps every named body on 29 flow queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:38:55 +00:00
Colby MchenryandClaude Opus 5.5 a944ec3579 fix(go): a struct links the types its fields are declared with (#2655)
A Go struct referenced nothing through its fields: etcd's `type
PutResponse struct { Header *ResponseHeader }` or gin's `type Context
struct { handlers HandlersChain }` had no `references` edge to those
types, so impact on `ResponseHeader` or `HandlersChain` missed every
struct holding one, and their users. Main had 0 such edges on gin, etcd,
prometheus and kubernetes; only embedded fields were read (`extends`).

TS extractor and Rust kernel, mirrored:
- goStructFieldTypeNames / struct_field_type_names walk each named
  field's type and push a `references` ref per type name from the
  struct, on the name. Fields mint no nodes: impact never climbs
  `contains` upward (#536), so a field node would stop impact short of
  the struct; Dart attributes field types to the class too (#2327).
- #2441's alias walk is factored into collectGoTypeNames /
  collect_type_names + goDeclaredNames / declared_names, so struct
  fields skip what aliases skip: predeclared types and, written bare,
  the declaration's own type parameters and its own name (no self-edge,
  no failed row for `next *node`).
- An embedded field stays `extends`; an embedded generic's type
  arguments are references (client-go's
  `listers.ResourceIndexer[*v1.Pod]`: 651 such fields in kubernetes).

Resolver: gateTargetKind's unknown-qualifier rule (#2417) now covers
every Go type declaration (GO_TYPE_KINDS). Without it, a field
`op clientv3.Op` under an unaliased go.etcd.io/etcd/client/v3 import
bound to the same file's own `Op`.

Explore: a `references` edge whose ends are both type declarations no
longer makes either end connected to an entry. With the new edges alone
the #1500 CG-10 gate failed: payroll's 8-line `RunResult` (holding a
`[]Payslip`) became connected and displaced the 51-line
runPayrollCycleAll body. The rule alone changes no rendered symbol on
gin, etcd, prometheus, kubernetes, bloc, bulletproof or refine-examples.

Validation (main de3eb71d, site-keyed A/B, every added edge judged
against real package clauses): gin +41, etcd +1006, prometheus +1637,
kubernetes +20203, all correct; 0 dropped/retargeted/metadata-only; no
node changes or self-loops; kernel == wasm. Index time within noise,
DB +0.4-1.4%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:35:01 +00:00
Colby MchenryandClaude Opus 5.5 c7b50382af fix(ts): index abstract methods as methods of their class (#2653)
Neither the TS extractor nor the kernel read `abstract_method_signature`,
so an abstract class's `abstract send(path: string): Promise<unknown>;`
had no node. An abstract class used as a dependency-injection token, like
bitwarden's `abstract class ApiService` in `libs/common/src/abstractions`,
had none of its methods in the graph, and a call through a field of that
type linked to whichever same-named class declared the method. Abstract
methods are now indexed as method signatures are: a method of the class
that declares them, never a free function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:16:52 +00:00
Colby MchenryandClaude Opus 5.5 627018eb25 fix(cpp): an explicit specialization, like protobuf's WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>, is named after its template (#2609)
An explicit specialization of a function or member function template kept
its template arguments in its name: protobuf's `template <> bool
WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>(…)` was
named `TYPE_INT32>` and qualified `…::ReadPrimitive::TYPE_INT32>`, the
qualified id split at the `::` inside its arguments; rocksdb's `template <>
int64_t Deserialize<int64_t>(…)` was `Deserialize<int64_t>`. A call writes
the bare name, so nothing linked to them.

Both extractors (wasm and the native kernel) now name a specialization
after its template, from the template-id's own name, and take its receiver
from the text before the declarator's name. That text must end in a real
`::`: tree-sitter recovers Metal's `vertex VertexOut vertexShader(…)` with
a missing, zero-width one.

A specialization now meets its template among a name's candidates, so the
resolver chooses between them wherever it picks among C++ candidates (a
class's methods, the implicit-this walk and its bases, exact, qualified,
global, fuzzy and macro-namespace matching):
 - a call that writes template arguments reaches the specialization
   written with the same ones (spacing aside, or qualified further on one
   side), else the template itself or an ordinary overload, else nothing:
   fmt's `num_bits<unsigned short>()` stays on `num_bits<T>()` instead of
   the nearer `num_bits<uint128>()`; protobuf's `Arena::Create<T>` keeps
   calling `DefaultConstruct<T>`, not the tests' `DefaultConstruct<
   DispatcherTestProto>` hook; a dependent `ReadPrimitive<CType,
   DeclaredType>(…)` links nowhere rather than to the first specialization.
 - a call that writes none, or `<>`, reaches a specialization only when
   nothing else has the name.
 - a class's own member template, specialized but none for these
   arguments, ends the lookup there instead of falling to a base's
   namesake.
The `cpp-override` pass leaves specializations out on both sides: a member
template is never virtual.

Validated on the cgstd kit against main (f7951a13): leveldb and redis
byte-identical; fmt: 8 specializations renamed, every call target as on
main; rocksdb: the cassandra `Serialize<T>`/`Deserialize<T>` calls (25 in
format.cc, 75 in its tests, which main sent to `ColumnBase` methods) each
reach the specialization their argument names (100 of 100 agree);
protobuf: `MergeFrom<MessageLite>` (3 calls), `ReadPrimitive<int,
TYPE_ENUM>` (2) and a test's `ReadPackedPrimitive<uint32_t, TYPE_FIXED32>`
reach their specializations; 5 dependent calls like
`GetRepeatedFieldInternal<T>`, and one `MutableRepeatedFieldInternal<int>`
(the macro writes `<int32_t>`), no longer link to the first specialization
a macro defines. Kernel and wasm dumps identical (but for protobuf's known
Swift line). Full suite: 7179 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:40:57 +00:00
Colby MchenryandClaude Opus 5.5 3f714bcc48 fix(jsx): a .jsx component links to the file-scope values it reads, as in .js and .tsx (#2561)
Value-reference edges (a reader symbol -> the file-scope const/var it
reads, which impact follows) ran for typescript, tsx and javascript but
not jsx. A .jsx file is its own language, parsed with the JavaScript
grammar, so the same component had no edge to the lazy page it renders
(`<LoginPage />` under `const LoginPage = Loadable(lazy(...))`), the route
table or the config it maps over as .jsx, and all of them as .js or .tsx.
Every other JS-family gate in the extractor lists jsx, and nothing in the
history says this one was left out on purpose: the kernel mirrored the gap
with an explicit "NOT jsx" check for parity.

Add jsx to VALUE_REF_LANGS and drop the kernel's jsx exclusion.

On mantis, matx, berry, mastodon and bigbluebutton-html5 the index only
gains edges (35, 56, 15, 28 and 266): no node, ref or file changes and no
edge removed. Every added edge is a same-file read of a file-scope value
that is the only binding of its name in the file. A repo with no .jsx
files indexes byte-identically, and kernel-parity.mjs shows 0 diffs over
those repos with the same deferrals as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:23:21 +00:00
Colby MchenryandClaude Opus 5.5 34dd3d2fb4 fix(cpp): a nested class defined out of line is indexed under its name, and a construction makes the class C++ finds (#2542)
A class, struct or union whose declaration writes its name in a scope —
leveldb's `struct SkipList<Key, Comparator>::Node { … }`, rocksdb's
`struct BlockBasedTable::Rep { … }` — was indexed as `<anonymous>`, so its
members belonged to no class anyone names and a call on a `Node*` or `Rep*`
was guessed by the method's name. Such a class now takes its own name,
qualified under the written scope the way an out-of-line method is, and its
members are qualified under it, in the TS extractor and the kernel alike. A
specialization (`struct fmt::formatter<Answer>`) stays unnamed: named after
its template, it stood in for the template and every other specialization.

Constructions are recorded by the type's last name, so with `DBImpl::Writer`
named, leveldb's `new log::Writer(…)` in `DBImpl` methods went to it. A C++
construction now makes the class C++ lookup finds for the type as written at
the construction (matchCppConstruction): read with its template arguments
(`new Outer<int>::Inner(…)` makes an `Outer::Inner`), at 0.9, or 0.7 when it
is only the class of that name the file can see; an enum lookup finds; through
an alias the calling function declares, the class it names; and nothing for a
template parameter, a member of a type from outside the project or a local
alias of one. Of several classes or aliases the file declares under one
qualified name (the local classes of separate gtest TEST_F bodies, the members
of unnamed specializations), the one declared around the construction. A `<`
or `>` in parentheses inside template arguments is now read as a comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:47:02 +00:00
Colby MchenryandClaude Opus 5.5 041db10146 fix(ts): a re-export imports its module, and sync follows barrels of barrels (#2526)
An `export … from './x'` statement emitted one `imports` reference per
re-exported name and nothing for the module itself; `export * from` emitted
nothing at all. So a barrel made only of `export *` lines had no dependency
on what it re-exports (no dependent for `affected`), and sync could not find
an `index.ts` that re-exports another barrel: when the inner one was deleted
or added, the outer one's importers kept their stale or name-matched edges.

A re-export now imports its module the way `import … from` does, in both
extractors (TS and the Rust kernel): an import node named by the specifier
and a module reference that links the barrel to the file it names. Sync's
importer discovery follows such modules to their own importers, carrying
the names each passes on (`export *`, a named or renamed re-export, an
`export * as ns`), as deep as the resolver chases a re-export; an ES
namespace import reads whatever its module passes on. A re-export's import
node does not count as a symbol for the "parse produced no symbols" warning,
so a barrel with an unparseable line still reports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:56:50 +00:00
Colby MchenryandClaude Opus 5.5 b635dd467f fix(go): a type declared on its own keeps its doc comment (#2438)
tree-sitter-go puts the comment above `type Foo struct{}` before the
`type_declaration`, but the node is made from the spec inside it, so both
extractors found no docstring for an ungrouped struct, interface, defined
type or alias. A type inside a `type ( ... )` group kept its own comment,
which sits beside it in the parentheses.

Go's docstring is now read from the declaration when the declaration
holds one spec and nothing precedes that spec inside it. The leading
comment of a group with several members stays the group's.

Reading from the declaration exposed comments that trail the line above:
`const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto
package` would have become `type MetricType int32`'s doc. For every Go
declaration, a comment that begins after code on its line, and one that
begins on the line such a comment ends, is now left out, as go/parser
reads them. Seven function docstrings on etcd and prometheus lose one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:07:02 +00:00
Colby MchenryandClaude Opus 5.5 78f5c8893a fix(react): a typed styled component is a component, not a constant (#2429)
`const Wrapper = styled.div<WrapperProps>` + a template literal came out as a
`constant`, so the JSX-render synthesizer skipped it: `<Wrapper>` got no
render edge, or bound to a same-named component in another file.
tree-sitter's tagged-template call takes no type arguments, so when the type
argument also reads as an expression the initializer parses as comparisons,
`(styled.div < WrapperProps) > template`, not as a call. (A type argument
that can't be an expression, `<{ open: boolean }>`, error-recovers into a
call on the tag, which #841 already took.)

reactComponentHoc now also takes that comparison chain: the leftmost operand
is the `styled` tag against a `<`, the rightmost is the template against a
`>` (`>>` when the type argument ends in its own `<...>`), and operators in
between belong to the type argument (`<A & B>`). Mirrored in the kernel's
TS/JS walker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:48:58 +00:00
Colby MchenryandClaude Opus 5.5 235387ec42 fix(c,cpp): a struct, class, union or enum defined in a variable declaration is indexed (#2446)
`struct Foo { … } foo;`, `class Bar { … } bar;` and `static struct { … } SPT;`
outside any function or class body went to the variable extractor, which
skipped the declaration's children, so the type, its methods and its
enumerators were never indexed in either extraction engine.

Both engines now walk such a type before the variables: in C always, and in
C++ when the tree has no errors or the file is walked in brace scopes. This
converges with #2426's walk for erroring C++ files; a C++ file whose braces
don't balance is still left alone, since error recovery can run a class past
its own `}` there. The declaration's function-as-value scan skips the walked
type, which captured its own. An unnamed struct, union or enum takes the name
of the first variable its declaration declares (also inside function bodies,
where it was `<anonymous>`), as `typedef struct { … } Name;` already takes the
typedef name, and the comment above the declaration is the type's docstring.
cDeclaratorIdentifier moves to languages/c-cpp.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:35:08 +00:00
Colby MchenryandClaude Opus 5.5 51000e3a92 fix(go): a defined type links the types it is defined from (#2441)
A Go defined type (`type WatchChan <-chan WatchResponse`, gin's
`type HandlerFunc func(*Context)` and `type HandlersChain []HandlerFunc`)
is a `type_alias` node, and it referenced nothing: only an `=` alias
(#2417) named the types on its right-hand side. Impact on
`WatchResponse` or gin's `Context` missed the declarations built from
them, and everything that uses those.

Fix (TS extractor and the Rust kernel, mirrored): goAliasTypeNames /
alias_type_names walk the `type` field of every type_alias node, a
defined type's as well as an alias's, so each type it names becomes a
`references` ref on that name, where resolution reads the package
qualifier back. Still skipped: the declaration's own type parameters,
Go's predeclared types, and now its own name written bare, which in a
recursive type (prometheus' `type stateFn func(*Lexer) stateFn`) is the
declaration itself: no self-edge, and no failed row that would make
dead code treat every namesake as referenced. A qualified name is never
skipped (`type PutResponse pb.PutResponse` names pb's).

Resolution is unchanged. A defined type is a new type: goAliasTarget
still follows `=` aliases only, so a method called on a defined type
is not looked up on its underlying type. The gateTargetKind rule that
leaves a type_alias's reference written through a package the index
doesn't know unresolved now covers defined types too; without it, every
such name bound to the same-file declaration of its name (`type Op
clientv3.Op` linked itself).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:20:33 +00:00
Colby MchenryandClaude Opus 5.5 ed199e60d8 fix(go): a type alias type A = B is indexed and links to the type it names (#2417)
tree-sitter-go parses `type Event = mvccpb.Event` as a `type_alias`, beside
the `type_spec` of a defined type, and both extractors read `type_spec`
only. etcd's client/v3 `Event` had no node; a use through its package
(`clientv3.Event`, prometheus's `storage.AOptions{…}`) linked to nothing,
and a use inside it linked by name to any type so named, another package's
included.

- Extraction (TS and kernel): an alias is a `type_alias` node referencing
  each type its right-hand side names, at the name so resolution reads the
  package back; an alias of a struct or interface literal is a struct or
  interface. The alias's own type parameters and Go's predeclared types are
  skipped. A generic alias, which tree-sitter-go 0.23 parses as a
  `type_spec` around an error, reads as an alias on wasm; the kernel defers
  its file.
- A method called on an alias-typed value is the aliased type's: an alias
  of a project type is followed in its own package, through pointers and
  further aliases; one of an outside type resolves by name as before.
- An alias's target written through a package that is none of the file's
  imports as indexed stays unresolved, as an embedded type's does, so the
  alias never links to itself or a namesake.
- With Go defined types as implementers (#2419), an alias that owns
  methods written with it as the receiver implements what they satisfy,
  in place of the type it names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:16:45 +00:00
Colby MchenryandClaude Opus 5.5 9d07864e65 fix(cpp): a class keeps its namespaces and enclosing class past code the parser misreads (#2426)
tree-sitter recovers from a construct it can't parse by inserting the token
it expected, and inside a class body that is often a `}`: an unknown macro
in front of a member (`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD absl::string_view
name() const`) or in a class head (`struct ALIGN_AS(64U) HandleImpl`) closed
the class there, and every `}` after it closed the scope one level out.
Recovery also ran the other way, keeping a scope open past its own `}` once
an ERROR swallowed it. The walker scoped declarations by tree nesting, so
qualified names lost their namespace and class segments, or gained ones they
don't belong to: protobuf's `FieldDescriptor`, `MapFieldBase` and
`MutableRepeatedFieldProxyImpl`, fmt's `detail::` buffers (and
`fmt::format_int` put in `detail`), rocksdb's clock_cache classes and the
`Opts` structs nested in them, gtest's `PrettyUnitTestResultPrinter`.

Every such file parses with errors, and the kernel defers those to wasm, so
the wasm walker produced all of them; the kernel's walker has the same
tree-nested stack and reaches erroring trees only under the
CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT sweep hatch.

For a C++ file whose tree has errors, the walker now takes each
declaration's namespaces, and at declaration level its enclosing classes,
from the source's braces (languages/cpp-brace-scopes.ts: comments,
literals, raw strings, digit separators and preprocessor lines skipped,
each #if branch read from the braces open at the #if). A class-like node
ends at its body's `}`, and a class the tree glued into a namespace-level
declaration's type is walked as a class. A file whose braces don't balance,
and every file that parses cleanly, keeps the tree's scopes. Ported to the
kernel (ccpp/brace_scopes.rs) so the hatch stays parity-true: the same
erroring files diverge before and after (UTF-8/UTF-16 recovery), every
other one is byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:06:36 +00:00
Colby MchenryandClaude Opus 5.5 e0bbb6620f fix(dead-code): a type that declare module or declare global merges outward is not dead (#2418)
The viewer's Dead code screen listed TypeScript interfaces that augment a
type declared outside the project: chart.js's `PluginOptionsByType` in
`declare module 'chart.js' { … }` (ghostfolio), `interface Window` in
`declare global { … }` (angular-realworld). The library or the runtime
reads them through the type they merge into, so nothing in the repository
names them, and the index recorded them as unexported file-level
interfaces.

TypeScript exports what such a body declares without the keyword. The
binder makes an ambient module body an export context unless it holds an
export declaration (`export {}`, `export =`, `export default x`), and a
namespace nested in one is ambient too. Both extractors now set isExported
that way: isAmbientExport in languages/typescript.ts, shared by TSX and
ArkTS, and is_ambient_export in the kernel's tsjs walker. The exported rule
then drops them, counted under `exported`, and that count's label now
names them. `declare namespace X` is unchanged: in a module file X belongs
to that file.

The resolver sees them as exported too. A file holding `declare global`
was already treated as exporting, so nothing moves there. A `declare
module` member becomes reachable across files by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:21:08 +00:00
Colby MchenryandClaude Opus 5.5 5ace306b7c fix(kernel): a Go value like wm.new or raft.None keeps its function ref (#2401)
* fix(kernel): a Go value like `wm.new` or `raft.None` keeps its function ref

The kernel's Go fn-ref capture checked a selector value's field name
against the name stoplist, so `raft.None`, `compression.None`, cgo's
`C.NULL` and a method value like `sync.Pool{New: wm.new}` were dropped.
The wasm extractor, the reference, applies NAME_STOPLIST to the
normalized name (`raft.None`), which the stoplist never matches. It also
builds that name from the operand and the field, so a line break or
comment beside the dot keeps the candidate; the kernel validated the
selector's whole source text and dropped it.

Build the name from operand and field as normalizeSpecial does, and drop
the field-name check. No other walker has the pattern: each applies the
stoplist to the same name as its TS spec.

kernel-parity.mjs --lang go: prometheus 202 -> 0 function_ref diffs,
etcd 6 -> 0, golang/go 35 -> 0, kapacitor 2 -> 0, plus 0 on gin, grpc-go,
hugo, caddy and go-ethereum. Indexed with both kernels, edges are
unchanged on prometheus, etcd, gin and hugo; kapacitor gains the one
missing edge, NewExecutor -> workerMaker::new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kernel): note that a comment before the dot keeps a Go selector value too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:08:27 +00:00
Colby MchenryandClaude Opus 5.5 24501fa66c fix(go): an interface links the interfaces it embeds (#2397)
extractInheritance's Go interface-embedding branch matched a
`constraint_elem`, a node type neither the vendored tree-sitter-go wasm
nor the kernel's tree-sitter-go 0.23 crate has: both parse an embedded
interface as a `type_elem` holding one type. So no Go interface ever
extended anything, in either engine; gin's IRouter never reached IRoutes.

Read an embedded type the same way in an interface and in a struct:
`T`, `*T`, `pkg.T` and `T[X]` all embed `T`, recorded at the name so
resolution reads the package back from the source (#2322). A type_elem
holding a union, a `~T` term or a lone predeclared type is a type-set
constraint and records nothing, and predeclared names (`error`, `any`)
are skipped in structs too. Qualified and generic struct embeddings,
skipped until now, link the same way.

Resolution keeps an embedded type in its package. The Go framework
resolver's naming heuristics no longer take extends/implements refs: its
PascalCase-model pattern saw structs only, so an embedded interface went
to another package's same-named struct (promql/parser's Node, etcd's
Lease), as #1827 did for Spring. And a Go supertype written through a
package that is none of the file's imports as indexed (`yaml` under an
unaliased go.yaml.in/yaml/v3, registered as `v3`) stays unresolved
instead of binding by its bare name.

Validated on gin, prometheus and etcd (before/after edge diffs, every
added supertype edge checked against its package; kernel/wasm parity
sweeps unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:29:50 +00:00
Colby MchenryandClaude Opus 5.5 ac1f430d3e fix(c,cpp): a pointer, array or function member is not a supertype (#2394)
extractInheritance carried Go's struct-embedding check (a field_declaration
with no direct field_identifier names an embedded type) into every grammar
with a field_declaration_list. tree-sitter-c/cpp, and Objective-C's C
structs, nest a member's field_identifier inside its pointer/array/
function/reference declarator, so `SharedState* shared;`, `jv elements[];`
and `virtual Status Put(...) = 0;` all became `extends` edges to the
member's type: false supertypes in the type hierarchy, blast radius and
codegraph_explore, and false cpp-override call edges built on them
(google/leveldb: 246 of 302 extends edges).

Gate the branch to Go in the TS extractor, drop the mirrored arm from the
C/C++ kernel port (and the "kept for parity" quirk note), and drop the
inert copy from the Rust-language port. C++ base_class_clause, Objective-C
superclasses and Go embedding are unchanged.

Validated on leveldb, jq, llm.c and redis: every removed extends edge is a
member site, no base-clause edge is lost; gin, SDWebImage and ripgrep
dumps are byte-identical; kernel/wasm parity sweeps show 0 diffs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:27:58 +00:00
Colby MchenryandClaude Opus 5.5 83f54a1853 fix(dart): a library links the files its part directives name (#2391)
A Dart `part 'x.g.dart';` now mints an `import` node (name = the URI,
signature = the directive) and an `imports` ref from the library's file
node, in both extractors. The ref resolves through `dartDirectiveFile` by
the rules an import or export follows, so the library's file links the
part's file. A library and its parts are one library, so a change to a
part, generated or hand-written, now reaches the library and what imports
it in `codegraph affected` and the files listed as using the part. Before,
neither `part` nor `part of` left a trace, and the two files were joined
only by whatever symbol edges happened to cross between them: riverpod's
framework.dart, which holds nothing but `part` lines, by none.

`part of` still records nothing: the library's edge already joins the two
files, and a part -> library edge would list every pair on both import
rails of both files and make each a file 2-cycle.

The kernel's directive arm now goes on into the directive's children, as
the TS ladder does. Its `import_or_export` arm used to return first, so an
annotated directive's arguments (`@Tag(f) import 'a.dart';`) were fn-ref
candidates on the wasm path only.

Validation on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2
with the kernel loaded, base 837a1866:
- bloc: +53 import nodes, +53 contains, +53 imports (library -> part).
- riverpod: +294 import nodes, +294 contains, +258 imports, +36 refs left
  unresolved (generated parts that are not committed).
- Nothing removed; kernel and wasm dumps byte-identical; parity sweeps
  0 diffs. Every added edge sits on a `part` line whose URI names the
  target, and the target's `part of` names the library back (311/311).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:16:55 +00:00
Colby MchenryandClaude Opus 5.5 d2fb323e51 fix(dart): an annotation links the constant or constructor it names, even behind a comment (#2390)
A Dart annotation is a constant expression: a const variable (`@riverpod`,
`@meta.immutable`, a type's static constant or enum value) or a call of a
const constructor (`@Riverpod(keepAlive: true)`, `@Foo.named(…)`). It was
ranked like a Python decorator, which favours a function or method of that
name over a class and scores a constant not at all, so riverpod's 506
`@riverpod` annotations went to riverpod_analyzer_utils' extension getter
`riverpod` (0.4) instead of riverpod_annotation's `const riverpod = Riverpod();`.

matchDartAnnotation (name-matcher.ts) now takes every Dart `decorates` ref
before any other strategy, and nothing falls through. It reads the
annotation as written from its `@` and links a constant or (called) a class
the file's library can see, through the import prefix it is written with
(isDartImportPrefix, dart-libraries.ts); a type's own static constant, enum
value or (called) named constructor (isDartConstructor); or, on a member, a
static constant of the type around it. Never a method, getter or function.

The decorator scan also stopped at the first sibling that was not an
annotation, so a comment between an annotation and its declaration
(`@override` `// ignore: must_call_super` `void f()`, riverpod_lint's
`@riverpod` `// expect_lint: …` fixtures) hid the annotation. Dart's
decoratorStepOverTypes lists its two comment kinds, which the scan now
steps over, and the kernel's extract_decorators_for steps over the same.

Validated on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2 with
the kernel loaded: riverpod's 506 `@riverpod` edges move to `const riverpod`
and 16 annotations behind a comment add an edge each, with nothing removed;
bloc gains 7 `@override` refs and no edge changes. Kernel and wasm full-index
dumps are byte-identical, and the parity sweeps show 0 diffs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:27:41 +00:00
Colby MchenryandClaude Opus 5.5 9924684899 fix(dart): a member keeps the dartdoc written above its annotations (#2387)
A Dart member's annotations are siblings that stand between it and the
dartdoc written above them:

    /// Builds the widget.
    @override
    Widget build(BuildContext context) { ... }

Both extractors read a docstring from the comments directly before the
declaration (or its `declaration` wrapper) and stopped at the first node
that was not a comment, so the annotation ended the run before the `///`
was reached. Members behind `@override`, `@protected`, `@mustCallSuper`,
`@pragma(...)`, `@internal` and the like lost their documentation, and
searching for words in it didn't find them.

Extraction (TS + kernel, at parity):
- getPrecedingDocstring takes an optional list of sibling types to step
  over, and Dart sets `docstringStepOverTypes: ['annotation']`. Stacked,
  multi-line and inline annotations are passed, and comments on either
  side of one still join (`/// a` `@x` `// b` gives "a" and "b" on two
  lines), as adjacent comments already did. Any other node still ends
  the walk: the previous member's body, a field, a top-level variable,
  an import or another declaration.
- The kernel's shared docstring.rs gains preceding_docstring_stepping_over.
  preceding_docstring passes an empty list, so every other language is
  unchanged; only Dart's docstring_of passes ["annotation"].
- Class-like declarations needed nothing: a class, mixin, extension,
  extension type, enum or typedef node starts at its first annotation,
  so the dartdoc above it is already the node's previous sibling.

Validated on felangel/bloc @b9be1e2 and rrousselGit/riverpod @4ba1be2 at
main 249d9a86 (and at 1b752de2) with the kernel loaded: docstrings bloc
+26, riverpod +1,072 with 9 changed and none removed. Nodes otherwise,
edges, refs and files are byte-identical, full-index kernel and wasm
dumps are byte-identical, and the kernel parity sweeps show 0 diffs.

Not changed: a comment between an annotation and the declaration still
hides the annotation from the decorator scan. Recording those would only
add wrong edges until Dart `decorates` resolution stops preferring
methods: riverpod's `@riverpod` edges land on an analyzer getter rather
than `const riverpod`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:29:29 +00:00
Colby MchenryandClaude Opus 5.5 249d9a860e fix(dart): a comment inside a member chain no longer loses a call or a read (#2385)
tree-sitter-dart keeps a comment as a named node wherever it is written,
so in a member chain broken over lines (`tester //`, then `.state(…)` on
the next line: the empty `//` keeps dart format from joining the lines)
the comment, not `tester`, is the sibling right before the `.state`
selector. Both extractors (the TS walker and the native kernel) read
that sibling as the receiver or the callee:

- A call with a comment between the member and its arguments
  (`box.grow /* by */ (3)`) was lost, and recorded as a read of
  `box.grow`.
- A getter read (`box //` + `.area`) or a static access (`Config //` +
  `.instance`) written after a comment was lost.
- A call after a comment kept only its bare name. Since #2376 the
  resolver reads such a chain link back past comments, so those calls
  already linked correctly; they now carry the receiver in the name.

Every sibling step along a member chain now skips `comment` and
`documentation_comment` nodes: the callee before an argument part,
dartReceiverOf / receiver_of, dartCalleeOfArgPart, the type in front of
a constructor's type arguments, and the member read's receiver and next
selector. The misparsed-generic-call gate keeps reading raw siblings,
and now rejects a comment against the `<` (`ref.read /* c */<Repo>(p)`),
so that shape stays a comparison instead of losing its read.

Validated with the kernel loaded on felangel/bloc @b9be1e2,
rrousselGit/riverpod @4ba1be2 and flutter/samples @63411527. No edge is
added or removed: the 14 comment-split chains there are all in riverpod,
and its 5 calls among them gain their receiver and stay unlinked.
Kernel/wasm parity has 0 diffs on all three.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:40:12 +00:00