Commit Graph
191 Commits
Author SHA1 Message Date
danusha2345andClaude Fable 5.1 80f6b50504 fix(sync): a tsconfig, jsconfig or go.mod change re-resolves the imports it governs
The resolver read tsconfig / jsconfig `paths` and `baseUrl` and go.mod's
module path once and kept the maps for its lifetime ("callers re-create the
resolver if config changes", which none did), and the watcher follows source
files only. So after an edit to one of them a long-running MCP server kept
resolving imports by the old mapping, and so did later processes: a sync
re-resolves only the changed files, and a config-only edit changes none.

The index now carries a snapshot of the config files resolution read: every
tsconfig / jsconfig the alias loaders opened, `extends` chains included,
every go.mod, and the root's candidates whether present or not. Every sync
reads them again and, when one differs, drops the resolver's alias and
module maps with the memos resolved through them, re-opens every import the
file governs (the ES-module family for a tsconfig / jsconfig, Go for a
go.mod) for the sweep, as a moved workspace map does, and marks synthesis
for a rebuild. An index an older engine stamped records a snapshot without
re-opening anything.

Review changes: each file's digest covers what resolution reads from it, a
go.mod's module path and a tsconfig / jsconfig's `extends`, `baseUrl` and
`paths`, not its bytes. A dependency bump in go.mod or a `strict` flag in
tsconfig had re-opened every import of the language for the same graph
(caddy: 48,945 refs, sync 3.8 s against 0.9 s; excalidraw: 88,860 refs,
9.2 s against 1.1 s). Plus a test for that, the ReferenceResolver class doc
comment back on its class, and the CHANGELOG entry beside the workspace-map
one.

Validation: 7 tests; on main's source the 5 behaviour tests fail, and the
new one fails on the byte digest. 11 fixture edits, each compared with a
fresh index of the same tree, in a later process and in the same process:
main diverged in 7 and 9, this in none. Real repos, sync against a fresh
index over nodes, edges, refs and files: excalidraw with `@excalidraw/math`
retargeted was 27 edges off on main and is identical here; caddy with its
module path renamed was 6,511 edges off and is identical here. Fresh indexes
are identical to main's. A one-file sync on excalidraw, median of 5
interleaved runs: main 3,834 ms, this 3,269 ms.

Original PR #2984 by @danusha2345.
Fixes #2977

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:57:07 +00:00
Colby MchenryandClaude Opus 5.5 b700a3eec2 fix(python): a parameter or local named like an imported module is not that module (#3046)
A Python call through a name the file imports as a module, like
other.update(...) after `from pkg import other`, linked to the module's
function whatever the calling function bound `other` to: a parameter, a
local, a with/except/loop target, a walrus, a case capture, a lambda or
comprehension variable. resolveViaImport read the file's import mappings
alone (its only scope check, pythonFunctionModuleAlias from PR #2600,
covers a function-local `import a.b as X`), and isPythonProjectModule,
which lets a builtin-named call like update/get/write past the builtin
filter for a project module, asked the same file-wide question. The
mappings also list from-imports at any depth ahead of plain imports, so a
function-local `from pkg import third as other` became every function's
`other`.

pythonReceiverBinding reads how the code around a reference binds the
name: the reference's own statement (lambda parameters, comprehension
targets), then each enclosing function, innermost first, through its
parameters and own statements (assignment, annotation, for, as, walrus,
del, nested def/class, case captures, imports; global keeps the module's
binding), memoized per function. pythonImportsInScope gives
resolveViaImport's Python branch and isPythonProjectModule only the
receiver's mappings that apply there.

Validation:
- __tests__/python-receiver-rebinding.test.ts, 80 tests (default and
  wasm, LF and CRLF): 48 fail on the base main (ten rebinding shapes,
  the local from-import, the cross-function leak), 32 pass (look-alikes:
  keyword argument, string, comment, comprehension over the module, lambda
  beside the call, global, keyword pattern, same-module local import);
  all pass with the fix. The 26 Python-related test files pass.
- A/B, main vs fix: django's own package (930 files, kernel path) changes
  2 edges' confidence (0.85 to 0.8, same targets: a closure over a
  parameter `apps` and a local `apps = model._meta.apps`); flask drops
  one cli.group() call and lowers 4 to 0.7 with the same targets (a
  nested `def cli` and a pytest fixture parameter `app` named like
  imports); requests, click, mealie and fastapi (package plus tests), and
  the express and gin controls are byte-identical.
- django package index time, median of 3 interleaved: 11.3 s main,
  10.7 s fix.

Reimplements PR #2632 by BGamboa13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 06:22:47 +00:00
Colby MchenryandClaude Opus 5.5 787eafbffa fix(jvm): a call through an import reaches the Java or Kotlin class it names (#3023)
A Java or Kotlin import was matched against a file path written with the
calling file's own extension (com/example/Foo.java from Java, .kt from
Kotlin), so an import across the two languages never bound: Kotlin's
JUtil.make() on an imported Java class went to a same-named class in
another package, JUtil.Nested.deep() and Outer.Inner() linked nothing,
and Java's KUtil.INSTANCE.kmake() went to another package's object.

The import is now read by qualified name in either language (the
package is some prefix of the name, so import p.Outer.Inner is
p::Outer::Inner), and the rest of the reference walks the imported
type's nested types: a Kotlin object's INSTANCE and a class's Companion,
as Java writes them, step onto the type, whose companion members the
index keeps on the class. Java's UtilsKt.helper() reaches the top-level
function of p/Utils.kt, and import p.UtilsKt links that file. A call
picks the member's method over a same-named field or nested class
(jextract's Index_h.clang_getRange() is the static method, not the
holder class of that name), the overload its argument count fits
(Minestom's NetworkBufferTemplate.template(a, g, b, h, ctor)), and, from
Kotlin, a nested Java class's own constructor. A Kotlin chain rooted at
an imported type is walked before the receiver-chain typing. Only an
imported type takes this path; an imported function or property is left
to the name strategies, which know a call's receiver.

A Kotlin extension declared in another package (fun Modes.hidden(),
indexed as Modes::hidden) is now a candidate only from a file that
imports it or its package, as Kotlin has it.

Validation:
- jvm-cross-language-imports.test.ts: 8 tests, all red on main, green
  here (cases from #1948's test and the T5 triage fixtures).
- A/B against main 86c85cff (the base, 8bd55fa7, gives identical graphs
  on jextract and nowinandroid, as 4955eac9 did there and on okhttp),
  sites added/removed/retargeted, with confidence raised at the same
  target:
  okhttp +172/-0/30 (167 raised), SystemUI +83/-0/25 (642), mihon
  +21/-26/28, nowinandroid +24/-0/5, jextract +96/-96/18 (calls that
  were instantiations of a same-named holder class now call the static
  method), spring-modulith 0/0/16, Minestom 0/0/843 (799 onto the
  overload their argument count fits, none against it), spring-ai
  +14/-12/73 (50 overloads; 22 onto the imported provider's nested
  class), playframework 0/0/37, kestra 0/0/302 (290 overloads); click
  and gin identical. Added: Java calls into Kotlin companions and objects
  (SystemUI's PhysicsAnimator.getInstance, okhttp's RequestBody.create),
  Kotlin calls into Java classes, type references across the two.
  Removed: guesses onto extensions a file cannot see (mihon's
  JsonObject.toByteArray on a String) and calls that become
  instantiations of a nested type.
- Cost (3 interleaved fresh indexes): mihon CPU 27.7 s on main vs
  27.5 s, wall 8.6 vs 8.7 s; okhttp CPU 31.5 s on both, wall 10.0 vs
  9.8 s.

Reimplements #1948 by @mixxer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:09:01 +00:00
4955eac958 fix(rust): use bindings follow Rust's scopes, and module paths walk the crate's modules (#3014)
A bare Rust name took the file's `use`s all at once: a `use` inside one
function (`fn first() { use crate::util::take; take() }`) bound `take` for
every other function too, and for the file's own `fn take`; a renamed one
(`use crate::util::take as consume; consume()`) linked nothing, since no
item carries the alias; a parameter or closure of the function's name
(`fn run(take: fn()) { take() }`, `let take = || {};`) linked to the
imported function; and `use crate::models::E::Good; Good(1)` took any
`E::Good`. Paths through modules (`resolveRustPathReference`) walked the
directories instead of the modules: `super::helper()` and `crate::config()`
with no module between linked nothing, `super` inside an inline
`mod tests { }` went to the parent directory's module, and an item a module
passes on (`pub use`, a glob) or another workspace crate's was not found.

The fix keeps one `use` reader, rustUsesOf:
- each `use` keeps the block it is written in (a function's body or a
  block; an inline `mod` and a macro's braces, `cfg_if!`'s nested ones
  included, are the module's), and std `use`s are kept by position;
- a reference reads the binding of its innermost scope (rustVisibleUse);
  isRustNameInScope uses it, so a block's `use` shadows the file's own item
  and a variant `use` names its enum's variant (rustUsePathItems' enum tail);
- matchRustUseBoundName, ahead of the pre-filter, resolves a rename to the
  item its path names, and a call to a name a local binds (rust-scopes'
  bindings, nearer than any `use`) to nothing, unless the local is a `let`
  holding a function by its path (serde's `let test = test::<i8>;`);
- resolveRustPathReference is rustWalkModules + rustModuleItems from the
  module the path is written in (rustPathTarget), with today's crate-root
  fallback; resolveRustModuleFile, collectRustUseBindings and
  rustItemPlacement are gone, and isBoundToOutOfRepoImport reads
  rustUsesOf's std `use`s;
- `use`s are read with strings blanked (blankRustCode), so a string's
  "use crate::x;" binds nothing;
- the crate graph reads a package's name by its section (axum-core writes
  `categories = [..]` first) and a `[lib]`/`[[bin]]` path of its own
  (ripgrep's `crates/core/main.rs`), which `crate::` then starts from; a
  file no manifest covers starts it from the nearest lib.rs or main.rs
  above it, as the directory walk did.

Validation:
- rust-use-call.test.ts (PR 2308's 15 cases), rust-use-scopes.test.ts,
  rust-crate-roots.test.ts and 5 new rust-module-path-call cases: on main's
  source 18 of 32 fail; all pass. Every rust-* suite passes.
- A/B on main 01e0ac3a, 7 repos, the same sites on the kernel and on wasm:
  tokio +2,434 sites (1,354 calls, 1,053 imports), 159 removed (152 locals
  called or passed as values, 6 `Result`s std's, 1 import through a path
  attribute module); ripgrep +248 / -149; axum +710 / -60; clap +730 / -112;
  serde +155 / -207 (192 calls to a closure named `test`); dioxus
  +2,950 / -272; gleam +1,573 / -359. Groups and samples inspected, every
  changed site's crate checked against the manifests' dependencies;
  express and flask byte-identical.
- Cost (kernel, medians of 3 interleaved): tokio CPU 34.5 -> 33.0 s, peak
  RSS 991 -> 1,007 MB; gleam 32.5 -> 30.6 s, 1,022 -> 1,012 MB; dioxus
  45.4 -> 45.4 s, 1,101 -> 1,107 MB.
- Original PR #2308 by @danusha2345; its module-path part landed as PR #2955.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:39:13 +00:00
danusha2345andClaude Opus 5.5 01e0ac3aa7 fix(kotlin): a call through a property or a receiver chain resolves on its declared type
Kotlin indexes a class property without a signature, and a
primary-constructor property (class A(private val repo: Repo)) is no
node at all, so a call through either (probe.close() on a lateinit var
probe: HttpProbe) found no receiver type and fell to the name-only
strategies: the interface's method, or any same-named method elsewhere.
A call through a chain (engine.pump.drain()) was emitted as its bare
method name, and a local bound to a qualified call (val lease =
HardwareLock.tryBegin()) was untyped. tree-sitter-kotlin also predates
fun interface: such a declaration parsed as a broken function and, with
a doc comment after it, its error recovery swallowed the next
declaration too.

The declared type is now read from the declaration itself: a property's
own lines, the class header for a constructor property, a callee's
declared return type for a local bound to a call, an enum entry's enum,
each segment of a receiver chain in turn (the chain is kept in the
reference name, by the wasm extractor and the native kernel alike), a
superclass's property, and an outer function's local inside an
anonymous object. A receiver whose type the project does not declare
(Regex, Properties, a JDK class) gets no edge instead of a name-only
guess, unless the project extends that type. The extractor's preParse
blanks the fun of a fun interface, offset-preserving, so the file parses
cleanly in both paths.

Validation:
- kotlin-property-receiver.test.ts: 17 tests, 12 red on main, all
  green here; kernel-kotlin-parity.test.ts pins the fun interface
  fixture.
- Kernel parity (kotlin), 0 diffs: okhttp 588/619 files byte-identical
  (main's kernel 578, the rest deferred to wasm on parse errors), mihon
  891/940 (887), nowinandroid 340/350 (339): the fun interface files
  no longer defer.
- A/B against main with #1942 (positions only: the extractor now keeps
  a chain in the reference name): okhttp +299/-257, 2,554 retargeted,
  2,158 of them test calls moving from the deprecated
  okhttp3.mockwebserver.MockWebServer (a 0.65 name guess) to the
  mockwebserver3.MockWebServer each file imports; mihon +82/-118, 337
  retargeted (Preference get/changes/set/register onto the declared
  Preference); nowinandroid -9, 23 retargeted (properties onto their
  concrete test doubles); SystemUI's Kotlin +1/-104, 7 retargeted;
  jextract, click and gin identical. The removed are name guesses on
  library types (okio sinks, collections' filter and clear, OkHttp
  bodies, MutableStateFlow.update, Kotlin's apply). 7 right edges are
  lost where a chain passes through a library wrapper (Lazy.value,
  StateFlow.value, Pair.first, a ViewBinding's view).
- Cost (mihon, median of 3 interleaved indexes): CPU 25.8 s on main
  with #1942 vs 26.7 s, wall 8.3 vs 8.4 s.

Original PR #1933 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 02:14:52 +00:00
Colby MchenryandClaude Opus 5.5 31ec908224 fix(sync): a long-running session picks up a framework added while it runs (#2941)
The MCP server's watcher holds one CodeGraph for the whole session, and
its framework detection was cached from the first time it ran: the
orchestrator reset it only in indexAll, and CodeGraph.sync never
re-initialized the resolver. A framework that arrived mid-session, like
Express in a new package.json, was missing from every file synced after
it, so a new route file got no route nodes until `codegraph index`. The
resolver's declared-dependencies cache was also keyed by the indexed file
COUNT (PR #2519): swapping one app for another of the same size, or
editing a manifest, kept the old dependency set, even through
reinitializeResolver().

Detection now records what it read: indexed source files, which a sync's
change lists cover, and every other path it read or probed, with a size
and mtime signature, including the per-app manifests the extractor gating
reads. Before a sync extracts files, it detects again only when files
were added or removed since, a source file detection read changed, or one
of those paths changed on disk. A scoped sync takes the file list from
the index plus the files it adds, so it never walks the project (the
first sync in a process used to run a full scan for its first detection).
What detection read is kept between detections and dropped by the same
change lists and signatures, so detecting again reads only what changed.
When the detected set differs from the resolver's, the sync
re-initializes the resolver. Its initialize() now clears its caches
before detecting, and clearCaches() forgets the declared-dependencies
cache with the other per-context memos.

Files indexed before a framework change are not re-extracted. A framework
like Express applies to every file of its language, and re-reading them
all inside a watcher's sync would cost what a full index does. Instead a
full index records the frameworks it used, a sync records how detection
moved since, and `codegraph status` (text, and --json with
reindexRecommended and frameworksChangedSinceIndex) recommends
`codegraph index` while they differ. Status only reads that record.

Validation:
- sync-framework-detection.test.ts (new, 8 cases): Express arriving with a
  new route file, by full and by scoped sync; a .svelte file added with no
  manifest change; an edited source file that detection read; a
  manifest-only edit picked up by the next sync, with the status record,
  cleared on revert and by a full re-index; a fresh process's scoped
  syncs detecting once, from the index, with no scan; the equal-size app
  swap; and the dependency cache forgetting on request. 7 of 8 fail on
  main's source (the 8th needs the new export). Ten mutations, one per
  mechanism, each fail a test.
- The triage probes: main detects [] after Express arrives and routes.js
  gets no route; the fix detects express and extracts GET /hello, by full
  and by scoped sync. For the swap, main's reinitializeResolver() gives
  [react] and the fix [express].
- Cost on excalidraw (737 files, scoped syncs in one long-lived
  instance): the detection step takes 8 ms on a sync where nothing it
  read changed and 8-13 ms when it detects again, against syncs of about
  260 ms (add) to 2 s (modify). The first sync in a process detects once
  (about 300 ms) and no longer scans the project.

Reported in PRs #2516 and #2519 by @nk33-dev. This reimplements both
fixes independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:31:26 +00:00
danusha2345andClaude Opus 5.5 3a93f57ae1 fix(java): a record is indexed as a class, with its components' fields and accessors
Neither engine treated record_declaration as a class type, so a Java record
left no node of its own. Its methods and explicit constructor became
package-level functions (p::norm, p::Point), a nested record's methods were
filed under the class around it, compact constructors, static fields and
records nested in a record were dropped, and calls on a record-typed value
resolved to nothing or to a same-named member of another type.

The Java extractor and the kernel now treat record_declaration as a class
and a compact constructor as a method. Each component becomes a private
field ("<Type> <name>") and a public accessor name() with the component's
type as its return type, unless the body declares it; they are synthesized
before Lombok's members, the same way.

An implicit accessor is reached only through what types its receiver: a
typed receiver, a method reference, a qualified call, or a call from inside
the record. The name-only strategies (the receiver-word guesses, exact-name
and fuzzy matching) no longer settle on one, so result.content() on a
library type stops linking to whichever record has a content component.

Records usually sit in sealed hierarchies, and that exposed a resolver gap:
the member-type reader took the head "sealed interface Blocks permits Block"
for a member named Block of type "permits", typed the receiver of
Block.fromKey(...) inside the record as a built-in and dropped the call.
"permits" and "throws" are now among the words that are not a type there.

Validation:
- java-records.test.ts, 5 cases on both engines (10 tests), all red on
  main; the sealed-permits and name-only-guess cases are each red without
  their resolver line.
- Kernel parity: TortureRecord.java in the torture and CRLF runs; real-repo
  sweeps at full parity with deferrals unchanged (jextract 218/218,
  spring-modulith 682/682, playframework 801/801, kestra 2997/2997,
  spring-ai 2491/2492, Minestom 1859/1894).
- A/B against main d02a88d0, every bucket triaged by sampling (edge sites
  added/removed): jextract +207/-1, spring-modulith +375/-1, playframework
  +80/-2, Minestom +17,963/-287, spring-ai +8,806/-471, kestra
  +10,077/-497; most retargets are a constructor function becoming its
  record. The accessor rule drops only name-only results (spring-ai 1,063,
  kestra 765, Minestom 408 sites at 0.65-0.8 confidence); no typed call,
  method reference or qualified call moves. click and gin byte-identical.
- Cost (process CPU, median of 3 interleaved indexes): Minestom
  47.2s -> 50.3s (+6.6%, for 17% more nodes and 23% more edges),
  spring-ai 93.5s -> 94.8s (+1.4%).

Original PR #1946 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 22:14:17 +00:00
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
danusha2345andClaude Opus 5.5 16f2d357bc fix(go): a project package named like a standard-library package is not the standard library
A Go reference written through a package qualifier was dropped as a
standard-library reference whenever the qualifier matched a name in
the built-in list (errors, log, types, parser, encoding, template, ...).
The check looked at the name alone, never at what the file imports
under that name, so a project with its own package of such a name lost
nearly every call into it: harbor's errors.New(...) through its
lib/errors, prometheus's parser.Inspect(...) through promql/parser.

What the file imports now decides before the name does. A qualifier
the file imports, by its last path segment, the name goimports assumes
for an unaliased import, or an alias, from a package of one of the
project's own modules is a project package, and the reference goes on
to normal resolution. A file that imports the standard library's
package of that name, and a qualifier the file doesn't import at all,
are skipped as before.

Validation: the new test file has 7 cases, 4 of which fail on main;
all pass here, with the native kernel and with CODEGRAPH_KERNEL=0.
A/B against main: harbor 296f304 (its Go tree, 1,611 Go files) +2,889
edges, prometheus a2fda0e +454, 0 removed and 0 retargeted on both,
nodes unchanged. Every added edge was checked against its source: the
line spells qualifier.Name and the file imports the target's package
directory under that qualifier from inside the module (3,343 of
3,343), and a sample of 10 was read by eye. gin and cobra, and the
controls express and flask, are byte-identical. Index CPU on harbor,
median of 3 interleaved: 40.0 s on main, 39.9 s here.

Original PR #2368 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:11:55 +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 d833827503 perf(resolution): skip receiver typing for JS/TS calls of a method no node is named (#2840)
A JS/TS call of a method that no node in the index is named, like
`list.push()`, `fs.writeFileSync()` or a test library's matcher, still
went through matchMethodCall's receiver-type inference, the TS binding
and import readers, and the class strategies. Every one of them ends in a
lookup by the method's name, so for such a name they can only answer
null. The one exception is the object-literal reader, which follows a
member to the binding the literal gives it: `const api = { alias:
renamed }` makes `api.alias()` a call of `renamed` (Issue #1932).

Root cause: matchMethodCall reads the calling file for the receiver's type
before anything tests the method's name.

Fix: the resolution context answers isAbsentName(name) from the set of
node names warmCaches already loads for the pre-filter. The answer is
exact, like getNodesByName, and false whenever the set isn't loaded. For a
JS-family call whose member is absent, matchMethodCall asks only the
object-literal reader, exactly as the full path would. When the reader
answers, the full path runs unchanged; otherwise the call returns null at
once. CodeGraph.indexFiles now drops the resolver's caches after it stores,
as indexAll and sync already do before they resolve, so a later resolution
never reads the name set from before the new files.

Validation:
- New __tests__/absent-member-calls.test.ts (6 cases). The skip reads
  nothing of the calling file (red on main). The name set answers only
  once loaded. Every path that links a member to a symbol of another name
  is pinned with the member asserted absent: a literal alias, a shorthand
  member bound to a renamed import, an `as const` literal, a holder in the
  same file and one imported, and a renamed re-export read through a
  namespace import. A method added through indexFiles after indexAll still
  links (main links it; the skip without the cache drop did not). A
  fixture indexes identically with and without the skip.
- Graphs byte-identical to main (scripts/dump-graph.mjs: nodes, edges with
  metadata, refs, files), kernel built per arm, on express,
  vue-element-admin, excalidraw and this repository (JS, TS, Vue, Svelte,
  Astro) and on the controls flask, gson, gin, ripgrep and slim (Python,
  Java, Go, Rust, PHP). Default mode (pooled on the two largest) for all
  nine, single-threaded for the JS four, and again after rebasing onto
  the current main. Main run twice is identical too. An incremental sync
  adding a file that declares formerly absent names (1,088 failed refs
  retried) is identical between the arms, with and without git, and
  matches a fresh index.
- Survey of main (instrumented build, nine repos, every language): no call
  whose member no node is named was resolved by matchMethodCall. The skip
  fires on exactly the calls the survey found (12,354 here).
- Profile counters, single-threaded: receiver inference runs 38,626 times
  instead of 50,980 on this repository, 6,460 instead of 9,306 on
  excalidraw, 2,488 instead of 3,586 on express. Outcome counts are equal.
- Cost, medians of 5 interleaved runs: the resolve loop on this repository
  4.54 to 4.41 s pooled and 16.85 to 16.29 s single-threaded, faster in 5
  of 5 paired rounds each; resolution phase -2.1% and -2.3%; whole index
  -1.3% and -2.2%. excalidraw's loop -1.6%, express's -5.0%; the five
  controls within noise (3 runs each).

Fixes #2829

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:48:07 +00:00
Colby MchenryandClaude Opus 5.5 f406fe0064 fix(cfml): createObject("component", path) links its component, and .init() on it (#2841)
`new a.b.Batch()` made an instantiates edge, but
`createObject("component", "a.b.Batch")`, the form older CFML uses most,
was extracted as a call to the engine function createObject, which never
resolves. A call chained onto either construction, `createObject(...).init()`
or `new a.b.Batch().init()`, was extracted as a bare `init`, which the
name matcher bound to whichever component's init came first (0.4), or to
none when there are many.

Fix:
- cfscript extraction (tag expressions included): a component
  createObject, with two arguments, one, or named ones, makes an
  instantiates reference under the dotted path as written, not a call.
  Java and other object types, and names built at runtime, are unchanged.
- A call on a construction is extracted as `<component>().<method>`.
- The resolver reads a dotted instantiates path with
  resolveCfmlComponentPath, the rules extends uses, and resolves a chained
  call to the method of that component or one it inherits (matched
  ignoring case; inheritance is wired before calls). A bare component name
  is the one in the calling file's folder, else the only one so named.
  Without both, the call gets no edge rather than a guess by name.
- resolveCfmlComponentPath also accepts a component at the project root
  for a path with one folder segment, its file named like it:
  `wheels.JobWorker` in Wheels' own source, where `wheels` maps onto the
  root. Without it Wheels' core lost calls like
  `new wheels.JobWorker().processNext()` that name matching had found. It
  ranks below any folder match; extends gains the same.

Validation:
- New cfml-create-object.test.ts (5 cases) and a root-component case in
  cfml-inheritance-resolution.test.ts: 5 of the 6 fail on main (the sixth
  pins the variable typing that already worked), all pass.
- A/B vs main d90f6abe on FW/1, BlogCFC, ColdBox, ContentBox, Preside,
  CFWheels and its vendor/wheels core indexed as its own project:
  1,022 edges removed: 563 resolved again to the same target by type,
  444 moved from a method guessed by name to the constructed component's
  own or inherited method, and 15 dropped, all name guesses for
  components outside the index, without the method, or tied. 1,342 added:
  641 instantiates, 38 calls, and in Wheels' core 653 extends and 10
  implements to root components. Oracles over the fix graphs: of the 983
  chained-construction call edges, 965 land in the file named like the
  component and 18 on a method its supertypes declare (each checked); all
  608 dotted instantiates and, in Wheels' core, all 786 dotted
  extends/implements edges target a file named like the path's last
  segment. One site is doubtful: Wheels' core public/docs/core.cfm names
  the application's app.models.Model, which that subtree lacks; one
  `models` folder corroborates a test asset's Model.cfc, as it would for
  extends. express and flask controls byte-identical.
- Index time, median of 4 interleaved runs, CPU with workers, main vs
  fix: Preside 58.7 / 54.5 s, Wheels core 32.2 / 33.0 s. Within noise.

Fixes #2652

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:49:44 +00:00
Colby MchenryandClaude Opus 5.5 d90f6abec7 fix(cfml): <cfinclude template> and cfinclude() link the code to the file they include (#2838)
Only cfscript's `include "x.cfm";`, written outside any function, linked
the including code to the template. CfmlExtractor never read
`<cfinclude template="...">`, which the grammar parses as a
cf_selfclose_tag whose name is only in the text after `<cf`, so the tag
form made nothing anywhere. cfscript's include hook read only the string
form, `cfinclude(template="x.cfm")` went through the call extractor as a
call to `cfinclude`, and the function-body walker never extracts import
statements, so an `include` in a function was dropped. Resolution went
through the generic pipeline: the file-name matcher links a relative path
like `../shared/helpers.cfm` only when no other file shares its name, and
past it the symbol strategies could take another template's import node
of the same path, or the including file itself.

Fix:
- `<cfinclude template="...">` is extracted like the script form: an
  import node in the scope it sits in (function, component, or the
  template's file) and an imports reference from that scope.
  `cfinclude(...)` and `include template="...";` make the same import and
  no call, and an `include` in a function body counts, from the function.
- A path built at runtime (`#dir#/x.cfm`) is no import, in every form.
- A CFML include of a template path resolves as CFML does: relative to the
  including file's directory, or from the project root for a leading `/`
  (less sure, 0.85), to the file at that path, ignoring case when one file
  matches. A path that names no file there goes to the file-name matcher,
  as before, but only ever to a file, and never to the including file.

Validation:
- New cfml-include.test.ts: the tag form at component level (same folder,
  ../, upper case, unquoted), in a function body inside <cfif>, in a
  template and from the web root; the script forms; runtime-built paths,
  cfinclude(template=variables.x) and <cfmodule> (no import); a relative
  path with a namesake elsewhere; an unmatched path that another template
  also includes, and a path whose only namesake is the includer. 5 of 7
  fail on main, all 7 pass.
- A/B vs main 51c4124d (CFML has no kernel path) on FW/1, BlogCFC,
  ColdBox, ContentBox, Preside, CFWheels and its vendor/wheels core as a
  project of its own: 127 new imports edges and 152 import nodes (36
  more resolved again to the same file, more surely); 9 import nodes of
  runtime-built paths gone; 1 edge gone, CFWheels' starter-app
  views/wheels/wheels.cfm including itself. No edge moved to another
  target. Every CFML imports edge in the fix graphs (225) targets a file
  named like the include path, and every relative path that stays inside
  the project reaches exactly that file but one: Wheels' Public.cfc
  including views/routetesterprocess.cfm, no file beside it, matched by
  name to public/views/routetesterprocess.cfm as before. express and flask
  controls byte-identical.
- Index time, median of 4 interleaved runs, CPU with workers, main vs
  fix: Preside 52.6 / 50.8 s, Wheels core 28.2 / 29.5 s. Within noise.

Refs #2651 (part 1; part 2 is a feature request and stays open)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:59:26 +00:00
Colby MchenryandClaude Opus 5.5 51c4124dad fix(cfml): a dotted extends finds its component when a test suite mirrors its path (#2835)
A CFML component that extends another by its dotted path, like
extends="contenscms.core.basecore", got no extends edge when a test
suite held a file of the same name in a folder of the same name, such as
a TestBox spec at tests/testbox/tests/specs/unit/core/basecore.cfc.
resolveCfmlComponentPath counts the path's folders that match a
candidate's, from the right. contenscms is a server mapping onto
contens/, so only core matched, for the component and the spec alike,
and a tie gives no edge. The rule the resolver applies elsewhere since
PR #2209, that production code never names a symbol in a test suite,
never ran on this path: it returns before the name matcher's visibility
gate.

Fix:
- A component in a test suite is no candidate for a reference from
  production code (isTestSuitePath, now exported from the name matcher).
  This also keeps application code from extending a test file that
  happens to match more folders.
- A test's tie between a component and a suite file that mirrors its
  path goes to the component outside the test suites. Two production
  components still tie.

Validation:
- cfml-inheritance-resolution.test.ts: 2 new cases (the issue's tree in
  script and tag form, a test extending the component, a mirror that
  matches more folders, a path only a test suite holds, a test naming its
  own suite) fail on main's resolver; all 12 pass with the fix.
- The issue's repro: page and tagpage now extend contens/core/basecore.cfc
  (qualified-name 0.9); on main both stayed unresolved.
- A/B vs main 8fdc8a1e (CFML has no kernel path): FW/1 (305 CFML files),
  BlogCFC (167), ColdBox (668), ContentBox (726), Preside (1,791) and
  CFWheels (the repo, and its vendor/wheels core of 1,528 CFML files as a
  project of its own): no node, edge, reference or file error changed.
  None has a component and a test-suite file that tie on a dotted path.
  express and flask controls: full dumps byte-identical (kernel path).
- Index time, median of 3 interleaved runs, CPU with workers, main vs
  fix: Preside 63.6 / 60.4 s, Wheels core 35.5 / 32.7 s. Within noise
  (main alone ranged 59-67 s and 35-38 s).

Fixes #2650

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:47:14 +00:00
Colby MchenryandClaude Opus 5.5 8fdc8a1efb fix(rust): calls on field chains, struct literals and outside-type paths link by type (#2831)
Issue #2614 reported four Rust calls that linked to a same-named project
method. Case 1 (a slice's contains) was fixed on main by the typed-receiver
work. The other three still reproduced on main in both engines:
vis.map.get("a") on a HashMap field went to Ext::get, J::from(i) under
`use serde_json::Value as J` to a local `impl From<io::Error> for Fail`, and
Xyz { x: 1.0 }.to_val() to Stat::to_val.

Root cause. The extractor keeps one receiver level on a call, so a deeper
field chain (vis.map.get, self.a.b.m, self.0.m) or a struct literal reaches
the resolver as the bare method name, and the typed-receiver reading only
ran for x.m and self.field.m refs. The name strategies then took the one
project get, or one of the two to_val impls. J::from matched no qualified
name, J names no project type, and a single capital letter is read as a
possible generic parameter, so Strategy 3 took the crate's one from. The
same family: under an unrenamed `use serde_json::Value`, Value::default()
linked by qualified name to a project Value's impl.

Fix.
- matchRustWrittenReceiverCall, an exclusive hook beside the as_mut one:
  for a bare-named call, read the receiver from the text at the ref's
  column. Only a field chain rooted at a local, a parameter or self, or a
  struct literal (alone or in parentheses) is read; it is typed with
  rustValueType and the call resolved with rustTypedMethodCall. Any other
  receiver, an unreadable one, or a type no node names (one a macro
  declares) is left to the name strategies. A provided method of a project
  trait the type derives counts (axum-extra's #[derive(TypedPath)]).
- rustFieldValue reads an element of a tuple-typed field (self.pair.0).
- isRustPathCallReachable in gateRustScope: a call through a type path
  whose type the file's use, or the written path, names from std or a
  dependency reaches only the project's impl of a trait for that type, or
  for a trait's path the project's impls of that trait. A path dependency
  the project holds is the project's.

Validation.
- New __tests__/rust-written-receivers.test.ts (12 tests: the issue's
  crate verbatim, chain/literal/derive/path variants, guards): 9 fail on
  main's sources, all pass with the fix. The 27 Rust test files plus
  resolution and cross-language resolution pass.
- A/B against main 5b47bc49 (kernel on), changed call sites triaged as
  removed wrong / added right / wrong to right / same target now by type:
  ripgrep 30 / 18 / 10 / 37 and tokio 281 / 58 / 54 / 141 (every site
  read), axum 18 / 7 / 0 / 7 (every site read), rust-analyzer without
  its parser test data (951 files) 737 / 186 / 434 / 843 (every path
  removal checked by its use; every group sampled). No right link lost.
  Two wrong links added on rust-analyzer: a block-scoped glob
  (`const _: () = { use syntax::ast::*; }`) is read file-wide by the
  existing glob lookup, so a `String` field there reads as ast::String;
  typed locals already misread it the same way on main. Nodes identical
  on every repo; controls gin and express byte-identical.
- Kernel parity (Rust): 1226/1226 files on ripgrep, tokio and axum.
  Resolver-only change, so the kernel needs no mirror.
- Cost: ripgrep median 6.7 s to 6.5 s, tokio 20.2 s to 20.8 s (interleaved,
  median of 3, noisy box); a CPU profile puts about 0.3 s of tokio's 57 s
  busy CPU in the new code and the typed reading it calls.

Fixes #2614

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 06:25:23 +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 58eb57a324 fix(nuxt): navigateTo('/x') links to the page it opens, as router.push does (#2826)
Nuxt's navigateTo was in the Vue resolver's list of Nuxt auto-imports,
which answers each with a sink: an edge from the calling node to itself
at confidence 1. The Vue resolver runs before Vue Router's, so the call's
destination was never read. Every navigateTo, in pages and route
middleware alike, became a self-pointing calls edge, and no navigates
edge was drawn; the sink also kept sync from revisiting the call when a
page appeared (a navigation waits as a failed reference).

Fix:
- navigateTo leaves the auto-import list, so Vue Router reads it.
- A new optional hook, FrameworkResolver.ownsReference: a reference a
  framework answers alone stops at the frameworks' answer, and no import
  or name strategy guesses past it. nuxtResolver owns a bare navigateTo
  that no declaration or import in the file binds. One whose destination
  is computed, names no page or is an external URL stays unresolved,
  rather than linked to a project function that only shares the name
  (elk declares a navigateTo of its own inside a plugin). A file that
  declares or imports its own navigateTo calls that one, as before.
- Vue Router's call reader keeps only destinations that start with "/",
  as its markup reader does: navigateTo(url, { external: true }) leaves
  the app, and the shared reader had matched an absolute URL's path.
- framework-coverage.md: the Vue Router / Nuxt row and a new trap 21.

Validation:
- nuxt-navigate-to.test.ts (6 tests) and a Nuxt case in
  sync-navigation-retry.test.ts (the page appears later; sync equals a
  fresh index). All 7 fail on main. Without the ownership check the
  unresolved case fails (a composable's call reaches a namesake); without
  the "/" rule the external URL navigates to "/".
- A/B against main 9ee6718a (and before that 628f69e2, with the same
  deltas), kernel on, natural-key dumps and a site-keyed edge diff. Every
  changed edge is a navigateTo call site; no node changed. Across
  atidone, elk, nuxt.com, mealie and nuxt/ui, 54 navigateTo calls
  (elk's plugin with its own navigateTo aside, unchanged) lost their
  self-loop; 41 now navigate (42 edges, each checked against its source
  line, all correct) and 13 are unresolved: 6 computed, an API endpoint
  opened with external: true, 4 pages of a Nuxt layer (its own route
  table), and 2 pages the matcher can't read yet (a catch-all and an
  optional parameter).
- Byte-identical: nuxt/movies; vue-vben-admin and vue-element-plus-admin,
  plain Vue apps that declare their own navigateTo; the controls
  svelte-realworld (Svelte) and gin (Go).
- Index time unchanged within noise, medians of interleaved runs, main
  then fix: nuxt.com 5.21 s and 5.24 s (5 runs), mealie 11.52 s and
  11.60 s, elk 3.38 s and 3.55 s (3 runs).

Fixes #2634

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 04:01:39 +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 d5fc1fbb21 fix(python): calls like reg.get() on a typed local or parameter reach the class's method (#2821)
In Python, a call of a method named like a builtin dict, list, string or
file method (get, update, items, pop, close and the rest of the
builtin-method list) was dropped by the builtin filter before any
strategy could read its receiver. Only a receiver whose capitalized name
is a project class, or an imported project module, got past it. So
`reg = SystemPackRegistry(); reg.get(k)`, `registry = load_registry()`
with `def load_registry() -> SystemPackRegistry`, and a parameter
`reg: SystemPackRegistry` gave no edge, while `self.reg.get()` linked
through the attribute reader of PR #1926.

A call the filter drops now gets one way back, by evidence only:
matchPythonTypedLocalCall. It reads the def that binds the receiver
where the call reads it (the calling def, or one it is nested in). It
types the name only when every binding there gives it the same project
class:
- a parameter gives what its annotation names ("T", Optional[T],
  T | None);
- an assignment gives its own annotation, or what its whole value
  makes: a constructor, or a factory typed by its return annotation or
  by its returns (the attribute reader's pythonValueEvidence and
  pythonFactoryType);
- `x = None` gives nothing either way;
- each class is the one the file's imports name (pythonClassNamed), so a
  class imported from outside the project is none of the project's;
- the method must be the class's own or a declared base's.

Everything else stays dropped: any other binding of the name (a loop,
with, except or comprehension target, a tuple or augmented assignment, a
walrus, an import, del, a nested def or class of the name, a nonlocal
write from a nested def), a value that names no class ({}, json.load(p),
Foo().data(), a union), a rebound callee, a module-level name. These are
the shapes the reporter's follow-up found making wrong edges with a
looser reader. Like an attribute call, the edge records the other files
its answer read (typeFrom), so a sync that edits the factory or a base
re-opens it.

Validation:
- New __tests__/python-typed-builtin-method-calls.test.ts, 20 tests:
  - the issue's three calls; annotated, quoted and Optional parameters;
    items, pop and close; a module-qualified constructor; typed, untyped
    and awaited factories; a None placeholder; a local annotation; a
    closure; typeFrom; two same-named classes; an inherited method;
  - the reporter's trap shapes, which stay unlinked;
  - three sync cases, each matching a fresh index.
  With main's source the 13 positive tests fail and the 7 trap tests
  pass.
- A/B against main 3f58e343. Both arms were built from their own sources
  with the kernel. Rebased onto e32021f6 (PR #2820, outside Python),
  the fix arm's graphs are byte-identical to before the rebase:
  - requests: +15 edges;
  - sqlalchemy: +52 edges;
  - django's 2,938 Python files: +73 edges;
  - 0 removed and no other change in any of them;
  - express and gin (controls): byte-identical.
  All 140 added edges were read at their source: each is a constructed
  local, a factory-typed local or an annotated parameter calling its own
  class's method, or one a declared base has. The issue's fixtures link
  all three calls, and the verification kit's nine trap call sites stay
  unresolved, kernel and wasm alike.
- Cost: a CPU profile puts the new reader at about 1.0 s on django and
  1.2 s on sqlalchemy, around 1% of index CPU. Index CPU medians of 3
  interleaved runs, main vs fix: sqlalchemy 98.6 s vs 98.5 s, django 97.9 s
  vs 99.1 s (single runs vary by several seconds on this shared
  machine).
- kernel-parity.mjs: 295/295 Python files at parity.

Fixes #2583

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:50:54 +00:00
Colby MchenryandClaude Opus 5.5 ab92c90a54 fix(js,ts): a super.method() call links to the method of the class's parent (#2818)
A TS/JS call written super.run() is recorded under its bare name, as
issue #2497 found. Inside an override of run every strategy found the
calling method itself, and the self-edge gate declined it, so the call
was unlinked; elsewhere it went to whatever method had the name, often
the class's own or a sibling class's in the same file, and, for a
parent outside the project like HTMLElement, Set or Promise, to a
project namesake.

A call written on super where it starts is now resolved through the
class's resolved extends edges, which the resolver persists before it
resolves calls: the nearest ancestor's method of that name, a static
one for a call in a static member, or nothing when no ancestor in the
project has it. The call may sit in a method, a class field, or an
arrow function inside either. One the index can't place in a class (a
class expression's method, an object literal's) keeps the old path, as
does one after a class keyword in the member, outside comments and
strings. The self-edge gate now reads a TS/JS call at its own column,
so a this.run() on the line of a super.run() keeps its edge.

The issue's first part, a method call on the receiver of an aliased
constructor, already resolves on main since PR #2661 and is unchanged.

Validation: the new test fails 3 of 4 on main 68fcf2f1 and passes 4 of
4 on the fix; the fourth pins the class-expression guard, and a
mutation of each guard fails a test. A/B against main on 22 repos (20
TS/JS, flask and gin): 660 call sites change, all super calls, checked
against each class's extends chain and sampled by hand. 643 gain an
edge to an ancestor's method (304 in three.js, like super.copy(source)
reaching Object3D.copy; 210 in webpack, 85 in lit); 2 move off the
class's own method to the parent's; 4 keep their target, now by the
parent; 11 lose a guess, where the parent is a builtin or a library's
class, like webpack's SortableSet extending Set, or a mixin's
parameter. Nodes and all other edges are
unchanged; 14 repos' dumps, the controls among them, are byte-identical.
A sync that moves a parent, swaps a parent or adds an override to an
intermediate class matches a fresh index. Index time, median of 3
interleaved runs on three.js, webpack, jest and insomnia: +1.7% total,
noise; 7 interleaved rounds on three.js alone: -1.5%.

Fixes #2497

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:11:29 +00:00
Colby MchenryandClaude Opus 5.5 f3fdba2955 fix(pascal): calls inside nested Delphi routines are indexed, each under its routine (#2810)
A Pascal routine may declare routines of its own between its header and
its begin. The extractor walked only a routine's body block, so a nested
routine's code was never read: what it called was missing, and a call to
it from the routine that declares it went by name to any routine
elsewhere with that name (in mORMot2's tests, Test, Check and One calls
landed on other classes' methods).

A nested routine is now a function node qualified under the routine it
is nested in (TFoo::Bar::Inner), contained by it and walked with itself
as the caller. It sees the parameters and locals of the routines around
it that are declared above it. Its name means it in the routine that
declares it, in the routines nested there after it and in its own body,
ahead of the class and the unit. A call resolved to a nested routine
from anywhere else is what the call's own scope declares, or nothing. A
nested routine's bare calls reach the class of the method it is nested
in first. Where a routine's tree has errors and its parse swallowed the
unit's routines after it (column 0, or after the body), those are walked
as the unit's own; of two bodies under {$IFDEF} the first is walked, as
before.

Validation against origin/main 7b50ca83, nodes and edges removed/added:
SynEdit +239, -6/+1127; HeidiSQL +404, -178/+2352; mORMot2 +574,
-956/+9565; DelphiMVCFramework +134, -279/+1049; DWScript +434,
-346/+3351; leveldb and SDWebImage byte-identical. Sampled: calls to
nested routines 38/38 right, calls made in them 44/59 (ordinary name
matching's misses), removed edges 30/30 wrong links. Index time within
about 5%, in proportion to the code now walked.

Covers the nested-routine part of #2550.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 17:50:35 +00:00
Colby MchenryandClaude Opus 5.5 7b50ca8321 fix(pascal): Delphi calls to compiler intrinsics and plain data no longer link namesakes (#2803)
A Pascal call to a name the compiler or the System unit gives meaning
to (SizeOf, TypeInfo, High, Low, Length, Inc, Dec, Assigned, Ord, Exit,
Include, Move, Assert, typecasts like Pointer(P)) went to name
matching, which linked any project member of that name: mORMot's
TRttiProp.TypeInfo, a record's length or high field, a property named
Pointer. The built-in name check compared case, so length(S) and dec(N)
got through too. And a call that resolved to a field, property or
variable linked whatever data had the name.

Now a bare call to a System name links only to a declaration its scope
reaches: the class and its ancestors, the unit, the units it uses last
first, skipping a unit's stand-in declared under {$IFDEF} or {$IF}. A
call resolved to data stands only when the data's declared type is a
routine type (written out, a project alias to one, generic or on two
lines, or an RTL procedural type by name), retargeted to what the scope
declares. A name the calling routine declares itself links nothing; a
name the scope doesn't declare at all (a with object's member, a
declaration the parse misplaced) links to the one routine-holding field
or variable of that name. Built-in names compare without case; the RTL
namespaces keep their spelling, except system. Scope lookups now see a
unit whose unit header the parser lost, by its file name (its indented,
lost class members aren't taken for its routines), and an include file
shares its unit's scope and uses clauses. A with body's bare call to a
System name is not extracted, since it may be the with object's.

Validation, edges removed/added against origin/main: SynEdit -243/+50,
HeidiSQL -858/+296, mORMot2 -9039/+987, DelphiMVCFramework -2420/+242,
DWScript -705/+222; leveldb and SDWebImage byte-identical. Sampled
precision of calls to data, before and after: 20/50 and 50/50; of calls
to System names: 2/50 and 47/47. Sampled removed edges: 40/40 wrong.

Follow-up to #2514.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:20:06 +00:00
Colby MchenryandClaude Opus 5.5 5d81d292fa fix(js,ts): a call or new never lands on an interface or a type alias (#2801)
A call or `new` that a strategy resolved by name or through an import could
target a TypeScript interface or type alias, which nothing can call or make
an instance of. memfs's fs-core `result.ts` declares `interface Ok` beside
`function Ok` (and `Err` beside `Err`), and `Ok(...)` through an import
reached the interface. A `new` of a global that a project only declares a
type for (`new Response()`, `new Worker()`, `new Event()`) was linked by
name to that type, and jest's `write()` parameter call to a `type write`.

Root cause: no rule on what a call may target. Whichever strategy found the
name picked among same-named nodes by order, and nothing stopped it on a
type.

Fix (gateTargetKind): for the JS family, a `calls` or `instantiates` result
whose target is an `interface` or a `type_alias` moves to the value the
target's file declares under the same name (a function, class, component,
constant or variable, first by position): `function Ok` beside
`interface Ok`, a class merged with an interface. With no such value the
result is dropped, and the reference stays unresolved.

Validation:
- __tests__/call-value-targets.test.ts (3 tests): all 3 fail on main and
  pass on the kernel. Each of 5 rules removed in turn fails at least one.
- Sync equals a fresh index (CLI) when the value beside a type is added or
  removed, and when a project type for a global or a parameter appears.
- A/B on 14 TS/JS repos and 2 controls: memfs's 11 Ok/Err calls move
  from the interfaces to the functions; 165 `new` of globals dropped (vite
  27, nuxt 57, webdriverio 26, insomnia 47, jest 7, memfs 1) and jest's
  `write()` call on `type write`. Every dropped `new` names a global
  (Worker, Event, Response, Request, Headers, ReadableStream, Node's Module,
  mocha's Context/Suite) or a local variable. vben, outline, social-app,
  t3-turbo, nitro, vue3-antdv-admin, pathe, c12, flask and gin are
  byte-identical.
- 91 related test files pass (2,178 tests).
- Cost: fresh index within noise (median of 3, interleaved): vite 9.07 s
  to 8.74 s, nuxt 11.71 s to 10.93 s, insomnia 15.26 s to 15.11 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:49:07 +00:00
Colby MchenryandClaude Opus 5.5 e8ddc4e2be fix(js,ts): imports of a repo's own packages follow their package.json entries (#2799)
Issue 2502: a pnpm (or npm, Yarn, Bun) workspace member that declares its
entry with `main`, `module`, `types` or `exports`, like
`"main": "./src/index.ts"`, has no index file at its root, so
`import { caseStatus } from '@acme/core'` failed and its calls fell back to
name matching, where a same-named private function nearer the caller took
them. Issue 2456: a sibling package another declares as a path dependency
(`"acme-shared": "file:../common"`), with no root `workspaces`, was not
known as a package at all.

Root cause: workspace-packages.ts mapped a member's name to its folder only
(its header said it did not yet honour `exports` or `main`), and the
resolver probed `<folder>/index.*`. Path dependencies were read only from
ohpm manifests.

Fix:
- workspace-packages.ts reads each package's declared entries: `exports`
  (subpaths, `*` patterns, fallback arrays, nested conditions, read with
  import/require/node/module/default first, other conditions next, types
  last) and the root's source/module/main/types/typings. It reads `file:`,
  `link:` and `portal:` dependencies from every package.json in the
  existing manifest walk: the root manifest's for every file, any other's
  for the files under it, as npm installs them.
- The resolver takes the first declared target, `exports` before the
  main-style fields, that the index holds or that is build output the
  index lacks, read as the source it is built from (dist/, build/, lib/,
  out/, es/, esm/, cjs/ mirrored under src/, then without a format folder).
  Then the package's folder, as before, and a path into its build output
  (`@acme/utils/lib/path`) read as its source. A file a local build left on
  disk never wins over an indexed source, and no mapping links nothing.
- Sync: a full reconcile reads the manifests again and, when their digest
  moved, re-opens every JavaScript-family reference, as a moved Python root
  set does. A file added inside a package re-checks the imports of that
  package that landed elsewhere in it. A full index drops the cached map.

Validation:
- __tests__/workspace-package-entries.test.ts (26 tests): 25 fail on main,
  the control passes; all pass on the kernel and with CODEGRAPH_KERNEL=0.
  Each of 17 rules removed in turn fails at least one test.
- Sync equals a fresh index in 15 CLI scenarios (entries, subpaths, a
  member renamed, added or leaving the globs, file: deps added, removed or
  retargeted, an entry's file appearing). On real repos: two vben manifest
  edits (237 edge rows move) and a vite playground file: edit, synced graph
  identical to a fresh index.
- A/B on 14 TS/JS repos and 2 controls: vben +2,955 edges, insomnia +2,222,
  webdriverio +2,472, jest +1,700, nuxt +706, memfs +470, vite +251,
  t3-turbo +47, social-app +40, vue3-antdv-admin +8, with name-matched
  namesakes corrected; nitro, outline, pathe, c12, flask and gin are
  byte-identical.
- Cost: fresh index within 3% (median of 3, interleaved); a full sync reads
  the manifests, +30 to 160 ms on these monorepos.

Fixes #2502
Fixes #2456

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:54:48 +00:00
Colby MchenryandClaude Opus 5.5 407efb1c65 fix(pascal): Delphi calls written without parentheses link to the routine (#2797)
Delphi calls a routine that takes no arguments without parentheses
(`Before;`, `X := FProvider.Value + 1`, `if Ready then`), and only a
statement-level `Obj.Method;` was recorded, so most call sites were
missing from callers, callees and impact. Bare names in arguments and
assignments were captured as function values and matched by name alone,
which linked locals and parameters to other classes' methods.

The extractor now records each name written without parentheses with
how it is used (a statement, a value, an event's method, a receiver),
the routine's class, and the declared type of a receiver the routine
declares. It leaves out what the routine declares, data the class and
the unit declare, names the class's code assigns or passes to Inc/Dec,
names Delphi's conventions give to data, and anything in a `with` body
but an event's method. A new resolver links such a name only to a method
or function Delphi's scope rules reach: the class and its ancestors, the
unit, then the units it uses, last first, or a method of a receiver's
declared type. Data of that name links nothing. A function runs (calls);
a procedure read as a value, an event's method, and a routine that needs
arguments written without them are method pointers (references). Where
the tree has errors, the class and declarations come from the source
text, and a routine's own header is no call. `class sealed (TBase)` and
`class abstract (TBase)` heads now name their ancestors. Pascal function
values capture only `@Handler`.

A/B against main 8073637d (SynEdit, HeidiSQL, mORMot2, DWScript,
DelphiMVCFramework): added edges 3011/4626/6501/5092/6776; main's
name-matched function values now linked by scope 2387/2727/948/726/712,
114/281/554/342/586 of them as calls; removed 85/139/802/333/707,
triaged as main's false positives (locals and parameters linked by
name, other classes' methods, routine headers misparsed in broken
trees) and own-class retargets, less deliberate losses (reads in `with`
bodies, lowercase or type-like names read as values). Sampled precision
of added edges: 196/196. leveldb and SDWebImage byte-identical. CPU,
median of 3 interleaved: +15.2%, +16.2%, +8.7%, +12.9%, and +0.8% for
DelphiMVCFramework (one run each).

Fixes #2514

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:10:10 +00:00
Colby MchenryandClaude Opus 5.5 e69546e4a3 fix(js,ts): code outside tests resolves as if Jest manual mocks were not there (#2790)
Issue 2455: when production code imports a workspace package the resolver
cannot follow (`import { formatLabel } from 'acme-shared'`), its names fall
back to name matching, and a Jest manual mock beside the code
(`__mocks__/acme-shared.ts`) is the nearest namesake. So the call, the
import's names, type references, constructions and `extends` all linked to
the mock, and the real function lost its callers. The visibility gate keeps
test suites from production code, but `__mocks__/` is not one of them.
jsx-render, Vue templates and the event-emitter pass pick by name with
their own lookups, which no gate covers.

A manual mock stands in for a module only while Jest or Vitest runs, and it
re-declares that module's names by design. Rejecting it after the ranking
picked it (what the gate does for `__tests__/`) leaves the call unlinked even
where the real function is the only other candidate. It also leaves sync
behind a fresh index: deleting the mock re-opens nothing, while a fresh
index then links the real function (main has this today for `__tests__/`).

Fix:
- ReferenceResolver.resolveOne resolves a reference from code that is not a
  test (isTestPath) on a view of the graph without manual mocks, when the
  project has any. Every lookup by name, lower-case name, qualified name,
  kind and method answers as if no `__mocks__/` file were indexed. Only
  names a mock declares are filtered, and those answers are cached. The
  view is swapped in as this.context for the call, so every strategy
  (frameworks, imports, name matching, fuzzy, function refs) reads it, and
  clearCaches drops it with the memos keyed by it. Test code keeps the
  whole graph.
- jsxChild, the Vue template resolver and the event-emitter handler lookup
  skip a mock's component or function for a non-test file
  (isManualMockHiddenFrom).
- The synthesized-edge merge drops any edge from non-test code into a node a
  manual mock declares, whatever pass made it.
- An import that names the mock's own path (`import x from
  './__mocks__/uuid'` in a vitest setup file) still links. Jest's
  `moduleNameMapper` and automock are not read: they act only while Jest
  runs, so they never change what production code calls.

Validation:
- __tests__/jest-manual-mocks.test.ts: 5 of 6 tests fail with main's
  sources. Each of five rules fails a test when removed: the view (three
  tests, the in-process sync case among them, which adds and deletes a
  mock), the JSX, Vue and event lookups, and the merge guard.
- Sync equals a fresh index (CLI) when the mock or the real module arrives
  later, when the production file is edited, and when the mock is deleted.
- A/B on 8 TS/JS repos plus 2 controls, both arms with the kernel. Edges
  from production code into mocks: webdriverio 23 to 0, outline 21 to 0,
  social-app 3 to 0, insomnia 19 to 13 (the 13 left are setup-vitest.ts
  importing mocks by path). webdriverio gains 23 right edges and 142
  confidence raises that the mocks had blocked. jest, nuxt, flask and gin
  are byte-identical. On webdriverio, production code's edges equal main's
  on a copy without the mocks, 20,885 of 20,885 with metadata. insomnia
  matches the same way, apart from the 13 path imports.
- Cost: fresh index, median of 3 interleaved: webdriverio 12.67 s main vs
  12.71 s, insomnia 12.84 s vs 12.60 s.

Fixes #2455

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:10:10 +00:00
Colby MchenryandClaude Opus 5.5 0953084bc5 fix(pascal): the receiver calls of a chained Delphi call are indexed (#2787)
A Pascal call whose receiver is itself a call recorded only its outer
method: in `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` the
receiver call `MD5FileSized` and the `TMeta.ExtendedPath` in its
arguments were dropped, as were paren'd calls deeper in a chain
(`A(1).B.C`) or in an index (`List[GetIndex(x)].Free`). The extractor
read a receiver call only for a `TFoo.GetInstance()` factory chain.

The extractor now walks a call's receiver chain and records each paren'd
call in it, with the calls in its arguments and indexes. A paren-less
link stays a read. A link inside a chain is recorded only when it can be
named precisely (`Obj.Method`, a bare routine, a `TFoo.Factory().X`
chain), never as the bare method name a chain like
`Client.New.BaseURL(…)` leaves, which matched namesakes. The calls main
already read keep their names.

Receivers are very often typecasts (`TButton(Sender).Caption`), which
read like calls. Delphi builds objects only through a constructor
(`TFoo.Create`), so a bare-name Pascal call that resolves to a type is a
cast: resolution now gives it a `references` edge to the type instead of
`instantiates` or `calls`. That also corrects the edges main already
made for value casts (`F := TFoo(Obj)`).

Validation:
- New __tests__/pascal-chained-calls.test.ts (7 tests, the issue's unit
  included): all 7 fail with main's sources. Without only the resolver
  rule the typecast test fails; without only the chain-link gate the
  fluent-chain test fails.
- A/B against main 88ebefa1, both arms built from their own sources,
  diffed by natural keys:
  - Every removed edge is a cast changing kind at the same site, with
    the same source and target (SynEdit 491, HeidiSQL 1,262, mORMot2
    4,832, DWScript 3,115, DelphiMVCFramework 2,761), apart from 10
    function references that moved to an earlier use in the same
    routine.
  - New edges: SynEdit 55, HeidiSQL 249, mORMot2 1,196, DWScript 690,
    DelphiMVCFramework 2,727. Mostly casts read as references to their
    type (14 of 14 sampled right) and receiver calls.
  - 178 new call edges sampled: 140 right. instance-method 30/30,
    qualified-name 10/10, bare functions 26/30, bare methods 70/92,
    calls landing on a property, field or variable 0/12. The wrong ones
    follow rules main applies to every call: the TypeInfo intrinsic
    (main already links 1,709 such calls to a project method), calls
    through local procedural variables, and calls now read inside a
    receiver's arguments, named as main names them.
- Controls leveldb (C++, kernel) and SDWebImage (Objective-C, wasm) are
  byte-identical in dump-graph.
- Cost within noise: index time medians of 3 interleaved rounds,
  HeidiSQL 7.10 to 7.30 s, mORMot2 18.80 to 18.90 s, DWScript 20.00 to
  19.40 s wall.

Fixes #2547

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:17:14 +00:00
Colby MchenryandClaude Opus 5.5 800e582d55 fix(go): conversions, external test packages and build-tag twins link as Go builds them (#2784)
Three Go shapes recorded edges the source doesn't make. A conversion
like Category(s), (*Pair)(p) or model.Kind(3) became a calls edge to
the type, or an instantiates edge to a struct. A bare call in a
package's external tests (package a_test) could bind to a same-named
helper of package a in the same directory, which a_test can't see. And
a name a package defines once per build target, like sep in sep_unix.go
and sep_other.go, linked to whichever definition came first, so a Linux
caller reached the !unix file.

A Go call whose target is a type is now a references edge. Two files of
one directory under different package clauses see each other's
top-level names only through an import: qualified, or bare after a dot
import and exported. A reference whose definition has build-constrained
twins in its package reaches the one that builds for the most typical
target its own file builds for: linux/amd64, then darwin/arm64,
windows/amd64 and the rest. Constraints are read from the file name's
GOOS/GOARCH suffix and the //go:build or // +build lines, with tags as
go build sets them by default.

Validation: 9 new tests in three files, all failing on main, and each
part switched off fails its own file; the Go tests pass. A/B against
main 0c3e12dd, edges changed as conversions / twin retargets: gin 56/0,
etcd 556/89, prometheus 812/5270, sonic 49/0, x/sys 550/3072, gopsutil
31/179, fsnotify 3/7, kubernetes 16039/328. Nodes and refs are
unchanged, and express and flask dumps are byte-identical. Every kind
change sits on a conversion-shaped call, and the twin retargets read
right by their files' build lines (prometheus labels to the default
stringlabels file, x/sys syscalls to syscall_unix_gc.go). Indexing
cost, wall/CPU seconds, median of 3: x/sys 6.7/20.5 -> 7.1/21.7, etcd
9.6/29.9 -> 9.8/30.0, prometheus 12.5/49.2 -> 12.9/50.2; kubernetes, 2
runs each, 108.1/385.8 -> 110.4/390.0.

Fixes #2581

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:19:32 +00:00
Colby MchenryandClaude Opus 5.5 0c3e12ddbb fix(go): a call further along a chain links the method of the type the chain reaches (#2782)
A Go method called on a receiver the extractor keeps no name for, such as
a fluent chain's later link (model.WithContext(0).Where(1).First()), a
call on what a function returns (time.Now().UTC().Format(...)) or a
composite literal (GitOracle{}.Worktree()), reached the resolver as its
bare name at the column where the chain starts. Name matching then took
a project method of that name by proximity, or the only one: the issue's
GORM-style Where and First went to another query type, the standard
library's Format to the one project Format, and a literal's method to a
namesake type.

A new Go resolver step reads the chain at that column and types the
receiver link by link: a call's result, or the one a multi-value
assignment takes; a conversion's or a composite literal's type; a
generic call given its type arguments; an embedded field by its type's
name; a package-level variable from the call it is set to. A result
written with type parameters keeps them as parameters, read from the
declaration, since the indexed signature leaves them out, and the helper
that reads them now includes a one-line function's only line. The method
is the one Go selects on that type. A type from outside the project, or
one that can't be read, links nothing instead of a guess. When a chain
calls one name twice, the bare ref is the later call.

Validation: 8 new tests; 7 fail on main, and the repeated-name case
fails under a first-link rule; the 43 Go test files pass. A/B against
main c326b6be, edges removed/added: gin 80/72, etcd 1726/1327,
prometheus 2249/1265, sonic (gorm/gen) 910/877, kubernetes 88288/84331.
Every removed edge was a name-match guess except 173 in kubernetes: 171
are the same edge now owned by another ref of its chain, and 2 wrong
links of metav1.Now().Time.Unix(). Sampled retargets, drops and
additions read correct; express and flask dumps are byte-identical.
Indexing cost is within noise, median of 3 interleaved runs, wall/CPU
seconds main -> fix: etcd 9.5/28.6 -> 8.6/25.4, prometheus 10.5/42.1 ->
10.5/42.6, kubernetes 121.9/413.7 -> 121.3/416.8.

Fixes #2543

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 04:53:59 +00:00
Colby MchenryandClaude Opus 5.5 efff745783 fix(go): a field typed by a dot-imported package resolves in that package (#2764)
A Go type written without a package qualifier was looked up only in the
file's own package. In a file that dot-imports a project package, like
harbor's config tests (`import . ".../controller/config"`, then a
`controller Controller` field), the type isn't there, so the call
`c.controller.AllConfigs()` fell back to name matching. Name matching
took the same-named type of a package the file doesn't import, the one
that sorts first: matchGoFieldChainCall used the struct's own directory
for a bare field type, resolveGoMethodInPackage found no such type there,
and resolveMethodOnType picked the first of every
`Controller::AllConfigs`.

The same gap affected calls through parameters, locals and package-level
variables of such a type (one set by a conversion), through an alias of
it, and through methods promoted from an embedded field. It also affected
method values, type references, and the embedding edge itself where the
namesake was as near.

The field chain, the typed-receiver and method-value package lookup,
embedded types, alias targets, package-level values and Go type positions
now ask goBareTypeDir. It reads a bare name as Go does: the file's own
package first, then a project package the file dot-imports that declares
it. An exported name that only a dot import of a package outside the
project's modules can supply (`. "github.com/onsi/gomega"`) names none
of the project's types. The exception is a type that one of the file's
functions declares, which the index holds no node for: name matching
still handles it, as before. A bare embedding is retargeted only when a
dot import decides, and files without dot imports resolve exactly as
before.

Tests: go-dot-import-field-type.test.ts has 10 cases on both the kernel
and wasm backends. 18 tests fail on main and 2 guards pass. All
go-*, goframe, gin-*, frameworks*, resolution*, sync* and dead-code test
files pass (65 files, 1,140 tests). tsc and the build are clean.

Validation compares main bf6f1fa6 with this commit, each built with a
kernel from its own sources. Each repo was indexed from scratch and
diffed by call site:
- gomega 820ad4d5: 30 calls retargeted from internal.Gomega's Expect to
  types.Gomega's, the type the dot-imported root package's `Gomega`
  alias names. All 30 were checked against source.
- ginkgo 7ff96d7d: 135 calls added and 1,022 confirmed (same target,
  0.7 to 0.9), all through package-level vars typed by the dot-imported
  internal/test_helpers. 9 retargeted (8 composite literals to the root
  package's SemVerConstraints aliases, 1 Status call to the fake
  handler). None removed.
- harbor src/ 4e07c69c and kubernetes d2c4b85b: byte-identical dumps.
  Their dot-imported types' methods have no namesakes, so main already
  linked them.
- Controls gin 0f09c3a9 and zustand d7a5583c (TypeScript): byte-identical.
- Index time and DB size are within noise. On kubernetes, the median of
  3 interleaved rounds was 151.5 s on main and 148.7 s with the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:46:24 +00:00
Colby MchenryandClaude Opus 5.5 a5fe6e1a27 fix(go): a call on an indexed element reaches its element type's method (#2755)
A Go call on an indexed element (kubectl's `schemas[ix].ValidateBytes(data)`,
deepcopy-gen's `(*in)[i].DeepCopyInto(&(*out)[i])`, go-to-protobuf's
`m.Items[len(m.Items)-1].Unmarshal(...)`) reaches the resolver by its bare
name, at the column where the receiver starts, and nothing typed the
receiver: a method one type declares was guessed by name, often onto a
namesake, and a common one like `DeepCopyInto` got nothing.

matchGoIndexedCall, an early hook beside matchGoAssertedCall, reads the
receiver chain with goChainLinks and types it as Go does: the head from the
declaration in scope (goLocalDecl; readGoScopes now records where each `:=`
or `var` value starts and which name it is) or a package-level variable;
values (composite literals, make, new, append, conversions, fields,
elements, addresses, range elements); struct fields, promoted ones too; and
the element type of a slice, array, array pointer, map or named slice type,
with `(*p)` dereferenced. The method is the element type's own, its
interface's or one embedding promotes, and a type parameter's comes from its
constraint. An element type from outside the project, a predeclared one, a
type literal or an outside alias links nothing, and so does a method the type
declares in a file of its package the index couldn't read (kubernetes'
core/v1 generated.pb.go), which is read from that file's text. What can't be
typed (a call's result) is left to the other strategies.

On kubernetes 4c05e667 the indexed-element calls go from 760 resolved and
2,284 unresolved to 2,666 resolved and 378 unresolved; every changed site in
kubernetes, prometheus and etcd is such a call, nodes are identical, and the
express and flask controls are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:56:19 +00:00
Colby MchenryandClaude Opus 5.5 553d5d101d fix(sync): an import a name match took follows its module once it resolves another way (#2752)
A named import that a name match linked to a declaration elsewhere while
its module did not export the name kept that link after an edit gave the
module the name. A barrel's `export { EmptyState } from './EmptyState'`
declares nothing, so no definition delta, failed-ref retry or barrel walk
saw it (the "retargets" PR #2744 left). A name match into the module
itself, a class exported only as its default and imported by name, kept
its link after the default was dropped, where a fresh index finds nothing.

reopenNameMatchedImports (module-importers.ts): for each module the sync
modified, its importers' binding edges that resolved some other way than
their import (getImportBindingsResolvedElsewhere, an indexed range per
importer), whose import statement names this module, are resolved again
against the graph as it is (ReferenceResolver.resolveNow, writing
nothing). When the answer differs, the importer's edges through the name
and into the members of what the name match took go back for the sweep.

Regression test sync-name-matched-imports.test.ts: 4 cases comparing the
synced graph with a fresh index of the final tree. Fresh indexes are
byte-identical on 8 repos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:23:05 +00:00
Colby MchenryandClaude Opus 5.5 fc293f0081 fix(sync): a default import follows the module's default when an edit moves it (#2749)
An edit that changes which declaration a module exports as its default
(`export default A` becoming `export default B` while A stays, or the
default dropped) left every reference through a default import of it on
the old declaration. A sync moves other files' edges into a changed file
onto the node that replaces each target, and an `export default` statement
is no node, so no definition delta, failed-ref retry or importer walk saw
the change. A fresh index links the new default, or nothing.

- ReferenceResolver.changedDefaultExports: for each modified JS/TS module
  that unchanged files import by default or as a namespace (an imports edge
  into its file node named for the binding), compare what the whole-name
  references through those bindings resolved to with the module's default
  now (moduleDefaultExport, the lookup resolveViaImport makes). No file is
  read; a module forwarding its default samples 8 importers' edges.
- reopenReferencesThroughChangedDefaults (module-importers.ts): when one
  moved, the edges bound before this sync resolved anything go back for
  the sweep: through every binding that reads the default (importersOf
  seeded with `default`: default imports, { default as Y },
  require('./x').default, modules passing it on), the routers'
  `layout:<binding>` references, and into the old default's own members
  (typed receivers, inherited members).

Regression test sync-default-export-change.test.ts: 6 cases comparing the
synced graph with a fresh index of the final tree; all fail on main.
Fresh indexes are byte-identical on 8 repos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:22:36 +00:00
Colby MchenryandClaude Opus 5.5 ada1445458 fix(ts): a qualified type's last name is a member of what its qualifier names, not any declaration of that name (#2739)
Both extractors record the last name of a qualified type alone, so
`React.ReactNode` reached name matching as `ReactNode` and bound by word:
outline's 197 `React.ReactNode` annotations linked to its own editor
`class ReactNode`, excalidraw's `TChat.ChatMessage` to a `ChatMessage`
component, signoz's `EQueryType.CLICKHOUSE` to another enum's member, and
outline's `UnfurlResourceType.Group` to the `Group` model the file imports.
PR #2736 left that name to name matching on purpose.

A TypeScript `references` ref written after a qualifier now resolves
through the qualifier, ahead of the framework, import and name strategies:
- an import: the module's export (`import * as T`, through barrels and the
  repository's own `declare module` blocks), or the member of the enum,
  class or namespace a named or default import binds;
- a declaration of the file's own, in the innermost namespace around the
  reference that declares it, else at the top level: an enum's or a
  class's `::` member, or a member of a namespace block;
- otherwise a namespace the project declares global (`Express.User` in a
  `declare global` block).
A package from outside the repository, which no manifest of the importing
file's declares as its own and no `declare module` block declares, and a
global the project doesn't declare, link nothing. An import this resolver
can't follow but no manifest declares as a dependency (zod's own tests'
`import * as z from "zod/v3"`, through the package's `exports`), an
`import X = ns.Y` alias, and a name written after an expression
(`import('./x').T`) keep the old strategies.

The index holds no node for a TypeScript namespace, and its members are
indexed by their bare names, so ambient-modules gains scanNamespaceBlocks:
every `namespace A.B { … }` block with its full name and whether it is
global, read from the same tokens as the module blocks.

Validated on 18 TypeScript repos (13 React apps plus zod, vben, mealie,
prometheus's web UI and fastapi-template), main 2c4a16d0 against this fix,
every changed site checked with the TypeScript checker in a program over
each file's tsconfig and the repository's declaration files, without
node_modules:
- 300 qualified types move from a wrong declaration to the member
  TypeScript binds (zod 224, outline 39, excalidraw 23, signoz 10,
  mastodon 3, prometheus 1);
- 297 lose a wrong link and link nothing, where TypeScript binds nothing
  of the project, a package's member (outline 254, signoz 32, bbb 8,
  mastodon 3);
- 43 unresolved ones now link the member (outline 28, zod 9, signoz 6);
- 1966 keep their target, now resolved through the qualifier;
- next-saas, proshop, t3-turbo, minimal-kit, matx, berry, mantis,
  bulletproof, fastapi-template and mealie are byte-identical;
- index time is unchanged (125 s before, 120 s after, all 18 repos).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 09:48:45 +00:00
Colby MchenryandClaude Opus 5.5 73f68e188c fix(ts): a call to a parameter of any type links nothing elsewhere (#2732)
A JS/TS name the calling function binds as a parameter is that parameter,
never a same-named function in another file, as the file's own were made
in #2226. The parameter reader took a type or default to be anything up to
a comma or a parenthesis, so a parameter typed as a function, defaulted to
one, or after one was never seen: bitwarden's isAvailable(f) on a callback
typed (f: string) => boolean linked to the napi isAvailable, and vscode's
merge editor's compare(a, b) on a comparator parameter to strings.ts's.

A type or default may now hold balanced parentheses, braces, brackets and
generics, a parameter property's modifiers are read, and an earlier
destructured parameter is passed over; each alternative still has one
parse. A name ends where no identifier character follows, so a const
syncState$ no longer declares syncState.

The parameter also shadows the file's own import of the name, which the
import resolver answered before the check ran, but only where it is in
scope: in the body of the function, arrow or catch clause that binds it,
or the block or for statement of a const, let, var, function or class,
read from the function's text up to the reference. A lone arrow
parameter counts, and an object type a function returns is no body. A
Promise executor's resolve is the calls inside it, never a resolve(file)
after it, and a const X = require('./x').default is the import itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:08:54 +00:00
Colby MchenryandClaude Opus 5.5 accf600e47 fix(ts): a declaration in a declare module block is reached through its module (#2728)
A declaration file's declare module 'x' { ... } declares the members of
x, which no other file names by a bare name: an import of x reaches them.
The functions of such a block have been held to that since #2718, but its
other declarations, the types, classes, constants and enums, still took
bare-name matches from any file. On vscode, Node's
import { readFileSync } from 'fs' linked to the fs that vscode.d.ts's
workspace namespace declares, electron's clipboard and shell to env's, a
ProcessEnv to the proposed API's, and an LSP TextDocument re-exported
through a project barrel to the API's own.

Every block-level declaration now takes a match from another file only
when the reference is written in a block declaring the same module (the
proposed API files merge into vscode.d.ts), is a type position in a file
that imports the module (vscode.TextDocument under a namespace import,
which the extractor records by its last name), or names what the file
imports from a project file that imports the module and re-exports it
(copilot's vscodeTypes.ts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 05:01:49 +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 6fe9dc1266 fix(python): resolve imports from the package roots the project has (#1921)
Python imports now resolve the way the interpreter finds modules: from
the package roots the project has, not by any file whose path merely
ends in the module's name. A project root is the repository, each
service with its own pyproject.toml, setup.cfg or setup.py, its src/,
and the package directories its build config declares; a local root
answers only importers within its reach, so a test fixture's copy of a
package never becomes the application's. Relative imports climb from
the importer's own package.

Before, `import json` or `from datetime import date` could link to a
project file or function of that name, and a src-layout package like
flask's was found through a test fixture named flask.py. On flask,
fastapi, mealie and django, relative imports now resolve (about 2,200
links added), stdlib and third-party names stop linking to project
namesakes (about 1,300 wrong links removed), and imports of a module's
function land on the function rather than its file. A sync re-opens
what an added or removed module, an edited __init__.py or a changed
package root can move. Indexing CPU time is unchanged.

Merged with main: a function-local `import a.b as X` (PR 2607) now uses
the root-aware lookup, the outside-module receiver check (PR 2606)
passes the importer, and plain imports are read at module level only,
since the mappings apply to every function of the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:07:07 +00:00
Eric MinishandClaude Opus 5.5 4f222a3d65 fix(python): the module of a from-import no longer links a same-named variable (#2603)
The module of a Python `from X import ...` names a module or package,
never a symbol, so `from app import email` no longer links the
importing file to a project variable named app, like a FastAPI or
Flask app = FastAPI(). A from-import module that resolves to anything
but a file or module links nothing. On five Python repos this removes
12 wrong import links and nothing else.

Fixes issue 2584.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:02:50 +00:00
Colby MchenryandClaude Opus 5.5 60b2f1e7bb fix(rust): a call reaches a project impl on a std type only as Rust would pick it (#2697)
A crate can't declare a method on String, Vec, Arc or NonNull, only
implement a trait for one, and name matching took those impls for any
call of that name. Every String::from("...") in dioxus's CLI went to its
impl From<IDI> for String, tokio's NonNull::from(waiter) calls to its
impl From<&Handle> for NonNull<Entry>, its tests' files.iter() to an
internal Storage trait's impl for Vec, and clap's String::from calls
to its impl From<Str> for OsString.

A call into a method of a project impl on a std type now stands only
when Rust could pick it. A std trait (From, PartialEq, ...) is
implemented for the crate's own type argument, so the call must pass
one: built from it (IDI::Install), self in an impl of it, or a name
the enclosing function declares with it (fn from(name: Str)). A trait
of the project's must be in scope in the calling file: declared,
implemented or imported there, or brought in by a glob that may carry
it. Otherwise the call is left unresolved, since what it calls is the
standard library's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:37:17 +00:00
Colby MchenryandClaude Opus 5.5 437b5c329f fix(sync): an import links an added file whose name a popular import shares (#2666)
Sync retries a failed import by what an added file can be imported as (its
name, its stem, its folder) and skips a key more than 500 failed imports
share, as noise one file cannot answer. A path import is parked under its
path's stem, though, and a package's imports share that stem with every file
of the name: `utils` is the stem of every `jotai/utils` and `@scope/pkg/utils`
as much as of a project's own `./utils`, and `assert` the name of every binding
of `import * as assert from 'assert'`. In a large project those crowded the key
past the ceiling, so a project's own `utils.ts` added after its importers were
indexed, or restored after a delete, never linked them: folo's
`modules/settings/utils.ts` and `components/ui/modal/stacked/hooks.ts`, and
vscode's `base/common/assert.ts`.

A key past the ceiling is now looked through for the imports that could name
an added file, and those are retried. A JavaScript or TypeScript import names
a file by its module (the path branch of resolveViaImport, shared with it as
jsModuleFile), by a framework alias, or by the file's own name. What that
accepts is no noise, so it is held to the module ceiling, as the specifiers
that resolve to an added file already are: bitwarden imports its
account.service from 724 files. An import in another language is not
accepted, so a crowded key there is skipped as before; a key within the
ceiling is retried whole, as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:58:56 +00:00
Colby MchenryandClaude Opus 5.5 6692e05107 fix(go): an unexported name links its own package's declaration, and a local binding shadows the package (#2658)
Go keeps an unexported name inside its package, yet prometheus tsdb's
`[]*memSeries` in head_append.go linked to nothing. Name matching scored
head.go's `memSeries` and `tsdb/agent`'s the same, because directory
proximity counts shared leading directories (and caps at 80), so a child
or deep sibling package ties with the package's own directory. The first
one met, `tsdb/agent`'s, was then vetoed as invisible. Parameters,
results, struct fields, composite literals and calls all failed this way:
prometheus's `memSeries` and `sample{…}` (1,185 literals), and in
kubernetes client-go's per-version informer types and many test helpers.

- matchByExactName drops a Go candidate the reference cannot see before
  the ranking (isGoNameVisible, shared with isVisibleAcrossFiles' Go rule),
  as it already did for C#, Java, PHP and Scala, so the package's own
  declaration wins.
- That lets a name with more namesakes than the ambiguity ceiling resolve
  to its package's declaration. It also exposed bare calls to a local
  closure named like a package function: client-go's `init := func(…)`
  then `init(t)` bound to `func init()`. gateTargetKind now leaves a bare
  Go call or function value unresolved when the function around it binds
  the name itself (isGoLocallyBound: a parameter, `:=`, `var`), as Go's
  scoping says. The same rule drops existing wrong links: a test's
  `volumeSpec = volumeSpec(…)` var passed on, or a local `errors` map that
  linked to kubectl's `errors` method.

A/B on main a944ec35:
- gin: unchanged.
- etcd: +2 / -25.
- prometheus: +1251 / -112.
- kubernetes: +670 / -542.
Every added link is the package's own declaration, or the right method
for the 5 calls written through a value. Every dropped one is a name the
enclosing function binds (checked by text, the rest by hand). The other
changes are the same targets at confidence 0.9 instead of 0.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:13:17 +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 de3eb71df6 fix(js,ts): an import written from a jsconfig or tsconfig baseUrl links the file it names (#2649)
CRA and Vite admin templates (codedthemes' berry and mantis, uilibrary's
matx) set `"baseUrl": "src"` or `"paths": { "*": ["./src/*"] }` in a
jsconfig and write every cross-file import as `layout/MainLayout`. The
import resolver took each one for an npm package: loadProjectAliases
returned null for a config with a baseUrl and no paths, and
isExternalImport classifies a bare specifier from the root config alone,
before a nested app's config is read. The imports stayed unresolved,
their names linked by bare-name matching (vite components rendering the
remix app's namesakes), and React Router routes loaded through
`lazy(() => import('views/x'))` bound to the local Loadable value.

- path-aliases: a config with only `baseUrl` loads, marked
  `declaresBaseUrl`; aliasAndBaseUrlCandidates lists the most specific
  `paths` pattern's targets, then the specifier read from a declared
  baseUrl (paths alone never reads bare specifiers, as in TS 4.1+).
- import-resolver: a specifier isExternalImport takes for a package is
  read through the importing file's own config (nearest, else the root's)
  and binds only to a file the index holds (`fileExists` also answers for
  folders and node_modules). A package the file's package.json or an
  enclosing one declares, or a Node built-in, never binds to a project
  module that way, only to a `.d.ts` the project keeps for it (#2437's
  `"*": ["./typings/*"]`). A root catch-all `"*"` no longer marks every
  specifier local in isExternalImport's default mode either.
- The module ref of a one-segment import (`'config'`) links the file its
  specifier resolves to, as a path-shaped one already does (#2528).
- isBoundToOutOfRepoImport keeps the `extends` edge of a class whose base
  is imported through baseUrl.
- importBindingTail parks a binding under its module when the app's
  config maps the specifier to an indexed file, so a sync that edits the
  module relinks `import Layout from 'layout/MainLayout'` and
  `{ Foo as Bar }` (#2450's rule for relative imports). An undeclared name
  that maps to nothing (`express` under `"baseUrl": "."`) and a declared
  package keep their own tail, which root-"*" repos had lost.
- The TypeScript and TSX extension lists probe `/index.jsx` last, as tsc
  does: a route resolves its lazy module as TypeScript.

Validated against main 6071c9e4 on 21 repos (wasm path): every new
import-resolved edge lands in the file its specifier names; failed
imports naming a project file go 194/190/96 -> 0 on berry/mantis/matx;
their routes bind their lazily loaded pages and layouts (matx 7 -> 21
routes); 37 + 45 cross-app JSX renders move to the app's own components.
proshop, next-saas-starter, create-t3-turbo, excalidraw and six more are
byte-identical; root-"*" repos change only parked tails plus module
links. Same deltas with stubbed node_modules. Sync after late adds and
edits equals a fresh index, except one pre-existing barrel-edit gap that
relative imports share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:36:56 +00:00
Colby MchenryandClaude Opus 5.5 98127e54d0 fix(c): a header's definition for a flag nothing sets, like lua-cjson's #ifdef USE_INTERNAL_FPCONV fpconv_init, gives way to the source file the build compiles (#2615)
lua-cjson's fpconv.h defines `static inline void fpconv_init()` only under
`#ifdef USE_INTERNAL_FPCONV` and otherwise declares it `extern` for fpconv.c
to define. Nothing in redis's tree sets the flag, so its build calls
fpconv.c's, but #2613's rule (the file's own or an included header's
definition first) took the header's.

c-feature-variants.ts decides whether a line is compiled only by a build
that sets a flag nothing sets. It reads the conditionals around the line
(cpp-macro-visibility's directiveLines, now exported): `#ifdef`/`#ifndef`,
and `#if` with defined, !, &&, ||, parentheses, integers and bare flags,
then `#elif` and `#else`. A flag is set nowhere when the line's own file
doesn't define it, no build file from the project root down to the
definition or the call names it, and no C/C++ file of their components
(the tree under the nearest directory with a build file) defines it.
Reserved and platform names stay unknown.

cppFunctionOverload's C branch then takes a non-static, non-variant
definition outside the file's tier when one exists, when every definition
the file sees is such a variant, and when the call itself is compiled by
the default build. A branch around both the definition and the call decides
nothing: redis's quicklist.c tests call their own `static mstime` inside
`#ifdef REDIS_TEST`.

A/B against main dc4ccf6b: leveldb, fmt, rocksdb and protobuf identical;
redis 1 call retargeted, lua_cjson.c's fpconv_init() from fpconv.h to
fpconv.c. Indexing time within noise (redis 10.5-11.9 s on main, 10.2-12.3 s
here).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 05:47:57 +00:00
Colby MchenryandClaude Opus 5.5 dc4ccf6b14 fix(c): a C call, like redis's mstime(), reaches the function its own file can see, past another file's static copy (#2613)
A C call now links to the function of its name the call's own file can see:
its own, or one a header it includes defines, else one with external
linkage, the best placed of them (cppFunctionOverload's C branch). Before,
the name strategies took the function nearest the call; when that was
another source file's `static`, isVisibleAcrossFiles dropped it and the
call was left unlinked, so no call reached redis's `server.c` `mstime()`
or `ustime()`.

- A source file's `static` function is visible to a file that `#include`s
  that source file: hiredis's async.c includes dict.c, redis's ae.c its
  event-loop backend.
- The coordinator's `resolveImport` hook falls back to matchByFilePath for
  a C/C++ `#include` the import resolver can't place, as resolveOneInner
  already does, so cppIncluders sees includes only the end of their path
  names: protobuf's `google/protobuf/descriptor.upb.h`, which two generated
  copies end in, and jemalloc's `jemalloc/internal/*.h`. Standard headers
  are left out: otherwise `#include <math.h>` makes jemalloc's
  test/math.h visible to its own include through isIncludedCppTestSuite.

A/B on the cgstd kit against main 4a591aee: leveldb, fmt and rocksdb
identical; redis +194 calls linked and 115 retargeted (114 to hiredis
alloc.h's POSIX `static inline`s, 1 known miss: lua-cjson's fpconv_init to
an inline only `#ifdef USE_INTERNAL_FPCONV` builds use); protobuf 412 upb
calls retargeted from the unincluded php-upb.h amalgamation to the headers
the calling file includes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:34:50 +00:00
Colby MchenryandClaude Opus 5.5 4f3eaf33ae fix(resolution): component and scoped-alias imports link the module file (#2528)
An import statement's own module reference linked nothing in two cases,
even when its bindings resolved through the file: a Vue, Svelte or Astro
component's import (`isJsPathImportRef` covered only the script languages),
and a bare scoped package that a `tsconfig` path or a workspace package maps
to a project file (`isJsPathSpecifier` excluded `@scope/pkg`). So a barrel or
a side-effect module showed no dependency from those files, and sync's
importer discovery, which follows the module edge into a changed file,
never found them when a barrel they import through was deleted or added.

Both now resolve through `resolveImportPath`, as a script's path import
does; an outside scoped package still names no file. Component imports
written with an extension, which a basename match linked before, now link
through the resolver too. When a sync adds a file, it also retries the
failed imports that now resolve to it but wait under no key its path gives:
a scoped package parked under its last segment (`common` for
`@excalidraw/common`, mapped to `packages/common/src/index.ts`, whose keys
are `src` and `index`), under the module ceiling, and an import of the
importer's own folder or the one above (`.`, `..`), parked under no tail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:23:41 +00:00
Colby MchenryandClaude Opus 5.5 8a5ba3d02c fix(go): a call through a parameter or local named like a standard-library package reaches the project's method (#2489)
isBuiltInOrExternal dropped every dotted Go ref whose first segment is in
GO_STDLIB_PACKAGES before any resolution, so a call through a parameter or
local of that name linked to nothing: gin's `context.AbortWithError(…)` in
`func(context *Context)`, harbor's `context.IsAuthenticated()` after
`context := NewSecurityContext(…)` and `log.Errorf(…)` after
`log := log.G(ctx)`, etcd's `printer.DBHashKV(…)` and `trace.Step(…)`.

Such a call now gets past the package list when #2448's scope reader finds
the name declared in the function around it, unless the declaration says it
holds no project method: a type from outside the project or a predeclared
one, or what an outside package's function hands out
(`scanner := bufio.NewScanner(r)`, `url, err := url.Parse(raw)`). #2478's
exemption for locals bound from a type assertion stays beside it.

matchMethodCall resolves these calls by the declaration alone, never by the
receiver's name, which fits dozens of a big tree's types (kubernetes'
`printer := NewTablePrinter(…)` is no kubeadm `Printer`):
- a written or asserted project type (goWrittenType) calls its own method,
  an interface's or a promoted one, or nothing;
- a value a project function hands out calls a method of that function's
  package: the type named like the variable, or the only one of the name;
- anything else calls only a method no other type declares, never one with
  a standard-library method name.
The scope reader also records `x := T{…}` / `new(T)` types and the
own-package function a `:=` calls. GO_STDLIB_PACKAGES moves to name-matcher.

A/B against main 1d3619d6, natural-key edge diffs with identical nodes and
no edge removed: gin +2, prometheus 0, etcd +44, harbor +60, kubernetes
+508, every one triaged correct. The first version, which let these calls
reach name matching, added 811 on kubernetes, 346 of them guesses by the
receiver's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:50:55 +00:00
Colby MchenryandClaude Opus 5.5 16bc05a208 fix(go): a call through a local bound from a type assertion reaches the asserted type's method (#2478)
Receiver inference read `v := T{}`, `var v T` and parameters, never a
local bound from a type assertion. So `flusher.Flush()` after
`flusher, ok := w.(http.Flusher)` went to whichever project method name
matching picked, and calls like etcd's `fi.Action()` after
`fi := s.kvindex.(*fakeIndex)` linked to nothing.

A Go local whose declaration in scope at the call binds the value of a
type assertion (`v := x.(T)`, `v, ok := x.(T)`, `var v, _ = x.(T)`) now
has the asserted type, found where Go finds it, as #2444 does for a call
made through the assertion itself (`x.(T).M()`). Calls, method values and
field chains through the local (`wr.job.Start()`) reach the type's own
method, its interface's, or one embedding promotes into it. The binding is
the one the scope reader from #2448 finds in scope at the call: an `if`
header's ends with its statement, and a nearer declaration in a block that
has ended does not hide it. A type switch's variable and a value that only
starts with an assertion (`x.(T).field`) resolve as before.

A type from outside the project, a predeclared type or an alias of an
outside type links nothing, as for the assertion itself. A type the index
can't tell is left to name matching, as before: a type literal
(`interface{ GetDeadline() time.Time }`) or a type declared inside a
function (grpc-go's `internalInfo` and `grpcstatus`). A local bound from an
assertion and named like a standard-library package
(`parser, ok := builder.(balancer.ConfigParser)`) is no longer dropped as
a call into that package.

On etcd, harbor, kratos and grpc-go, 358 call sites change. 216 gain a
link, 38 move to the asserted type's method and 89 keep their target, now
resolved through the asserted type. 15 lose a link that went through a
type from outside the project, or from a field read to a same-named
function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:15:20 +00:00