Commit Graph
1609 Commits
Author SHA1 Message Date
Colby MchenryandClaude Opus 5.5 5a6917b437 fix(c): a function whose return type is a macro call is indexed (#2773)
In C, a function declared with its return type written as a macro call,
like cJSON's `CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)` or
libgit2's `GIT_EXTERN(int) git_blob_dup(...);`, had no node.
blankCppAnnotationMacroCalls blanks a line-leading ALL-CAPS macro call
that a declaration follows, the markup shape of Unreal's `UPROPERTY(...)`.
Since the C/C++ kernel port, C's pre-parse runs it as well, to set aside
redis's `REDIS_NO_SANITIZE("bounds")` and similar. In C the same shape is
often the return type. Blanked together with its argument, it left
`cJSON_Print(...)` and a block, which parse as a call statement. The
function was lost, the calls in its body went to the file, its locals
and parameters became file-level variables, and a header's prototype
read as a call from the header.

C now passes keepCReturnTypes. With it, a macro is kept, and tree-sitter
reads it as a macro_type_specifier, when all of these hold:
- it is in column 0;
- its one argument reads as a type;
- only a function declarator follows: a name, its parameter list,
  `__attribute__` or ALL-CAPS attributes, then `{` or `;`;
- the line right above leaves no declaration open. jemalloc writes
  `size_t JEMALLOC_NOTHROW` there, above `JEMALLOC_ATTR(pure)`.
C++ is unchanged.

Validation:
- New __tests__/c-macro-return-types.test.ts (3 tests: the issue's
  reproduction plus GNU-style, prototype-with-attribute, ATTR_FORMAT,
  REDIS_NO_SANITIZE and jemalloc shapes; a libgit2-style header; the
  blank itself). All 3 fail with main's c-cpp.ts and pass on the kernel
  and with CODEGRAPH_KERNEL=0.
- Pre-parse output compared file by file on 12 repos. The only changes
  are macro text kept where main blanked it; no file's parse-error count
  got worse. Better: cJSON 4 files (cJSON.c 131 -> 6 errors), expat 1,
  APR 69, libgit2 82. Byte-identical pre-parse, and so graphs, on jq,
  redis (with jemalloc), sqlite and curl, and on the C++ controls fmt,
  glm, json and abseil-cpp.
- A/B on main 6bd217a2, graph dumps:
  - cJSON: +92 functions; 7 functions named `(void)` and 100 phantom
    variables and constants gone; edges 2,882 -> 3,750.
  - libgit2: +403 functions; 3 `(void)` functions and 293 phantoms
    gone; 37 calls that went to a phantom parameter gone; edges 81,984
    -> 85,834.
  - APR: +1,551 functions; 7 bogus ones and 1,723 phantoms gone; edges
    16,163 -> 23,278. 39 calls now reach the real APR_DECLARE definition
    instead of its EBCDIC stub. 283 portable calls now see the
    OS/2 and Windows copies beside the Unix one: 278 go to OS/2 at 0.4
    (was Unix at 0.9), and 5 from win32 code go to win32.
  - expat: 22 phantom parameters gone.
- kernel-parity: 0 diffs on both arms; deferred files fall from 33 to
  31 (cJSON), 218 to 161 (APR) and 208 to 128 (libgit2).
- Cost: medians of 3 interleaved init runs: cJSON -1.6%, expat -4.3%,
  libgit2 -6.9%, APR +18.4%. APR's graph is 44% larger. The pre-parse
  itself is unchanged (APR 262 vs 257 ms, libgit2 598 vs 599 ms).

Fixes #2470

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 02:16:31 +00:00
Colby MchenryandClaude Opus 5.5 0de94978fc fix(installer): the Codex table is found however its config.toml writes it (#2771)
Root cause: the TOML helpers behind Codex's install, refresh and uninstall
found CodeGraph's table with two literal searches, `[mcp_servers.codegraph]`
at the start of the file or right after a newline, and detection used a
plain substring check. Some valid ways of writing that table went unseen:
- an indented header, which formatters that indent sub-tables write;
- spaces inside the brackets or around the dot;
- quoted or escaped keys.
Install then appended a second `[mcp_servers.codegraph]`. A table defined
twice is invalid TOML, so Codex loaded none of its config. Install and
refresh also appended a table after a definition written with dotted keys
or as an inline table, or under an `mcp_servers` that is an inline table or
an array of tables (both invalid too). The literal search also matched
header-shaped text inside a multiline string and spliced into the string.
Uninstall left the user's original table, a `[mcp_servers.codegraph.env]`
sub-table (a server with no command), and dotted or inline definitions in
place. CRLF files came back with mixed line endings.

Fix: a small structure scan of the file finds every statement (table
header, array-of-tables header, key/value). It decodes key paths the way
TOML reads them (bare, "basic" with escapes, 'literal', spaces around
dots), tracks the table each key sits in, and folds the lines of a value
that spans several into the key that opened it. On top of that scan:
- The `[mcp_servers.codegraph]` table is found however its header line is
  written. Install and refresh replace the first one in place and remove
  any further copies, so a file the old bug broke parses again.
  Sub-tables such as `.env` are kept.
- A block ends at its last key, so a comment heading the next table stays
  with that table.
- Uninstall removes the table, every copy, its sub-tables, and
  definitions written with dotted keys or as an inline table, as whole
  statements.
- An entry the user wrote with dotted keys or as an inline table is left as
  written, with a note.
- Where a table can't be added (`mcp_servers` set as an inline table, a
  value or an array of tables), install refuses with a clear error and
  changes nothing.
- An entry inside an inline `mcp_servers` table is reported by uninstall,
  never edited. The uninstaller now prints such notes.
- Everything is written in the file's own line breaks, and the existing
  block is compared regardless of them.

Validation:
- 25 new tests in installer-targets.test.ts:
  - 20 on the TOML helpers: 8 header forms, CRLF, the old bug's duplicate,
    look-alikes (other names, header-shaped text inside strings and
    arrays), sub-tables, comments, 4 dotted/inline/array definitions, 2
    shapes where a table can't be added, an entry inside an inline table;
  - 5 on Codex install, re-install, refresh and uninstall end to end.
  With main's toml.ts, codex.ts and installer index.ts put back, all 25
  fail: 24 on the behavior itself, and one, a header with a trailing
  comment, only on the new detection helper, since main already handled
  that form. The 13 existing TOML tests pass on both.
- The real CLI on this Windows machine against private homes: `install
  --yes`, a second `install --yes`, `install --refresh` and `uninstall
  --yes --keep-cli`, for 23 config.toml shapes, each result parsed with
  Python's strict tomllib.
  - main (0abae119): 46 of 92 results were invalid TOML (15 of 23 right
    after install), the entry was still there after uninstall in 16
    shapes, and 8 results had mixed line endings.
  - The fix: 0 invalid and no mixed line endings. The entry remains after
    uninstall only inside an inline `mcp_servers` table, which uninstall
    reports. Install refuses with exit 1 and an unchanged file for the two
    shapes where a table can't be added.

Fixes #2562

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:59:51 +00:00
Colby MchenryandClaude Opus 5.5 6bd217a263 fix(cpp): code that calls operator++ by its name no longer stops an index or sync (#2769)
Indexing or syncing a C++ project stopped at "Resolving refs" with
"Invalid regular expression: /\boperator++\s*(?:<[^<>()]*>)?\s*\(/:
Nothing to repeat" as soon as its code called an overloaded operator by
name, as `this->operator++();` inside `operator++(int)` does. Choosing a
method's overload reads the argument list after the name, and
cppParenListIn and cppOverloadParameters wrote the name into that
pattern with only `~` escaped. `operator++` did not compile. `operator()`
matched the name's own empty parentheses as the arguments, `operator[]`
never matched, and `operator+=` matched the next `operator=`. The same
helper serves every language that overloads by arity, so Scala `++`
overloads and Kotlin names in backticks that hold a `(` failed the same
way.

The fix adds one escape helper, escapeRegExp in src/utils.ts, the same
body as the private copies elsewhere. It is used at every place in src/
where a node name, a reference name, a piece of one, or a word read
from source goes into a RegExp unchecked: 114 interpolations in 13
files. 79 were raw, 31 escaped only `$`, 1 `$` and `\`, 1 only `.`, and
the 2 overload readers only `~`. An AST pass over all 490 dynamic
RegExp constructions followed each interpolated value to its sources.
The 72 it still flags were each read by hand. They are
identifier-checked first, built from escaped names, constants, or the
CLI/MCP glob converters. The kernel's one dynamic regex already
escapes.

With operator arguments now read, nlohmann/json's `p->operator[](0)`
reached the argument-type scorer and picked `template <typename T>
operator[](T* key)`: a `size_type` parameter scored as an unknown
class, a `0` as a null pointer. `size_type` and `difference_type` now
count as numbers, as `size_t` does.

Validation:
- New __tests__/operator-method-names.test.ts (5 tests: init and sync of
  an operator fixture, operator() and operator+= overload choice, the
  json operator[](0) shape, Scala and Kotlin). With main's sources,
  4 fail (3 throw "Nothing to repeat"); the json shape fails only with
  size_type taken out of the number types. All pass on the kernel and
  with CODEGRAPH_KERNEL=0.
- Census with a preload that logs every throwing RegExp: main threw
  only in cppParenListIn (CTRE 27, folly 3); the fix threw 0 times in
  all 12 repos below.
- A/B on main efff7457, graph dumps:
  - fmt, glm, rocksdb, protobuf: byte-identical.
  - json: 9 calls retargeted, all right (`j.at("name")` to the key
    overload, `v.at(i)` to size_type).
  - abseil-cpp: 3 retargeted, all right (operator() self-call to the
    two-argument overload, assign(3, 10) to the count overload).
  - CTRE and folly crash on main; both index now. CTRE: 10 of 12 calls
    retargeted against main's census run are right; 2 single-header
    calls reach the identical copy in include/. folly: 4 of 6 are right;
    2 small_vector assign(int, int) calls link the count overload where
    C++ takes the unconstrained template.
  - Controls gin (Go), bulletproof-react (TS), ktlint (Kotlin, 86
    backtick names with pattern characters) and cats (Scala symbolic
    methods): byte-identical.
- Cost: none measurable. Medians of 3 interleaved init runs: fmt -1.5%,
  json +2.1%, glm +0.2%, abseil -2.5%, gin -2.1%, bulletproof-react
  -0.3%, rocksdb +0.4% (45.2 s vs 45.3 s).
- kernel-parity on fmt + json (cpp/c): 458/669 at parity, 0 diffs,
  both arms.

Fixes #2457
Fixes #2482
Fixes #2636

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:39:08 +00:00
Colby MchenryandClaude Opus 5.5 1ef8866aae test(explore): the cross-call dedup test passes with LF and CRLF checkouts alike (#2772)
explore-cross-call-dedup's "never re-sends a line it already sent" case
has failed on every LF checkout (Linux, macOS, CI) since PR #2655. It
passed on Windows only because core.autocrlf writes the fixture with CRLF.

Root cause. Since PR #2655 the payroll fixture's RunResult struct
(cycle.go lines 31-37) references Payslip through a field, so call 1
renders it. Call 2 renders the file's head, and that 7-line held run comes
back inside it: dedupeRange keeps a covered run shorter than
MIN_COVERED_LINES (8) in the emit set on purpose, rule 3 of the range
algebra, which its own unit test pins. The handler case counted any
repeated line as a duplicate. On a CRLF checkout explore's per-file
character budget also counts each line's carriage return, so cycle.go
rendered fewer lines and the case never met the short run.

Fix, test only:
- The case groups the repeated lines into consecutive runs and fails on a
  run of MIN_COVERED_LINES lines or more, read from EXPLORE_DEDUP. A
  shorter run is the algebra's deliberate re-send. What agents receive is
  unchanged.
- The suite gives its copy of the fixture LF line endings, so every
  checkout indexes and renders the same bytes.

Validation on Windows, with the kernel and with CODEGRAPH_KERNEL=0:
- main's test fails with an LF fixture ("call 2 re-sent
  internal/usecase/payroll/cycle.go lines 31,32,33,34,35") and passes with
  the CRLF one. The new test passes 26/26 with both.
- With the normalisation and main's assertion, a CRLF checkout fails the
  way an LF one does (lines 31-37): both checkouts now test one scenario.
- Teeth: an algebra mutated to re-send held runs of 8-23 lines fails the
  new case ("call 2 re-sent internal/usecase/payroll/cycle.go lines 42-51
  (10 lines)").
- tsc on the test file is clean.

Contributor PR #2758 by @danusha2345 reached the same diagnosis. This
change was written separately and also normalises the fixture.

Fixes #2756

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:38:45 +00:00
Colby MchenryandClaude Opus 5.5 0abae11959 fix(installer): a symlinked instructions or config file is written through, not replaced (#2768)
Root cause: every installer write goes through one helper that writes
`<path>.tmp.<pid>` and renames it onto the path. A rename replaces a
symbolic link itself, not the file it points to. So a `~/.codex/AGENTS.md`
or `~/.claude.json` linked from a dotfiles repo became a detached regular
copy on install: the shared file never got CodeGraph's settings, and later
edits to it no longer reached the agent. This held for every target (Claude
Code, Codex, opencode, Cursor, Gemini, Antigravity, Kiro, Hermes and the
Copilot family) and for install, the refresh that `codegraph upgrade` runs,
and uninstall. Uninstall also deleted a link whose file it had emptied,
leaving CodeGraph's entry behind in the shared file.

Fix:
- The write helper resolves a link to the file at the end of its chain and
  writes and renames there, so the link stays exactly as it was, relative
  or absolute.
- A link to a file that doesn't exist yet gets that file created, but only
  in a folder that already exists, as writing through it in a shell would.
  No folder is ever created where a link points.
- A dangling chain is followed hop by hop from the real folder holding each
  link, so a relative link under a linked folder lands where the OS would
  put it.
- When the linked file can't be written (a read-only file, a folder that
  doesn't exist), the error names the link and its target and nothing
  changes.
- Where uninstall used to delete a user's file it had emptied (an
  instructions file, Codex's config.toml, Copilot CLI's mcp-config.json,
  the legacy project .claude.json), a link is kept and the emptied content
  is written through it.
- The refresh sweep catches a failure per agent, reports it, still
  refreshes the others, and exits non-zero, so the upgrade warns.

Decided: links to CodeGraph's own legacy files (Cursor's codegraph.mdc,
Kiro's steering doc), which install only ever deletes, are still removed.

Validation:
- 28 new tests. 25 are in installer-targets.test.ts:
  - every target and location: install and uninstall keep each of its
    files that is a link (18 cases);
  - the shared files' content through install and uninstall for Claude
    Code, Codex, opencode (two agents sharing one AGENTS.md) and Cursor;
  - relative links under a linked folder;
  - a link into a missing folder;
  - a link to a read-only file;
  - uninstall emptying through links;
  - a failing link in a refresh;
  - a folder link guard.
  The other 3:
  - 1 in upgrade.test.ts, where the refresh runs the built CLI;
  - 2 in a new installer-links-simulated.test.ts, which simulates the
    link with spied lstat/realpath/readlink, so it runs on machines that
    can't create file symlinks.
  With main's sources put back, 27 fail; the folder-link guard passes on
  both. All pass with the fix. The real-link tests are skipped, with the
  reason in their names, where file symlinks can't be created (Windows
  without Developer Mode or admin).
- This Windows machine has Developer Mode on, so the real-link tests ran
  for real.
- The real CLI against a private home (`install --yes`, `install
  --refresh`, `uninstall --yes --keep-cli`), 10 linked files for five
  agents plus relative, dangling and read-only links:
  - on main, all 29 link checks found the link replaced by a copy;
  - with the fix, none did, every link's text was unchanged, the shared
    files got CodeGraph's settings and lost them again on uninstall, and
    the missing-folder and read-only cases exited 1 with the new message
    and changed nothing.

Fixes #2721

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:06:12 +00:00
Colby MchenryandClaude Opus 5.5 d6e8c91e0a fix(installer): upgrade keeps everything around CodeGraph's section in instructions files (#2765)
Root cause: the write that refreshes CodeGraph's marker-fenced section in
an agent's instructions file (CLAUDE.md, AGENTS.md, GEMINI.md) located the
section with two independent searches: the first start marker anywhere in
the file and the first end marker anywhere in the file. Everything between
them was treated as CodeGraph's and replaced. A stray start marker above
the real section (what is left when part of an older section is deleted by
hand, or the marker quoted in the user's own notes) therefore stretched the
"section" over the user's text, and when that marker was the first line
the file came back as just the block, which is the report. The
`codegraph upgrade` refresh (`codegraph install --refresh`) and plain
`codegraph install` both go through this write, for the Claude Code, Codex,
Gemini and opencode targets. The same pairing made a lone end marker look
like "no section", so every run appended another block, and left a second
block stale. Uninstall (and the npm preuninstall hook) and the Cursor
rules-file cleanup used the same pairing to delete the same span.

Fix: one parser finds the well-formed sections. Each end marker closes the
nearest start marker before it, and a marker counts only when it is alone
on its line, as every block codegraph has written puts it. A stray start
or end marker is left as plain text. The refresh rewrites only the bytes
between the first section's markers, using the line breaks that section
already has, so a CRLF file stays CRLF and a current CRLF block is
unchanged. Further sections are duplicates and are removed, keeping the
text around them. With no section yet the block is appended after the
existing bytes, which are no longer trimmed. Uninstall and the Cursor
cleanup strip only well-formed sections.

Validation:
- 18 new tests (17 in installer-targets.test.ts: exact bytes for a stray
  start marker on line 1, a stray start marker with no section after it, a
  marker quoted in prose, a lone end marker, two sections, CRLF, no final
  newline, appending, a BOM, uninstall; a refresh plus uninstall per
  target for Claude Code, Codex, Gemini and opencode; the Cursor rules
  file. 1 in upgrade.test.ts: an npm upgrade whose refresh runs the built
  CLI against a private home). 16 fail on main's sources (the other 2 are
  guards that hold on both), all pass with the fix.
- The real CLI on this Windows machine against private temp homes:
  `codegraph install --refresh`, 14 file shapes x 4 targets x 2 runs. Main
  had 28 bad results of 112 (the user's text lost in 3 shapes, a block
  added on every run after a lone end marker, a stale second block kept),
  the fix 0. With the fix, the bytes outside the section are identical in
  every single-section shape, and the input stays a byte prefix in every
  append.
- `codegraph install --yes` and `codegraph uninstall --yes --keep-cli` on
  the reported shape: main kept none of the notes (install left only the
  802-byte block, uninstall left 19 bytes); the fix kept them all.

Fixes #2506

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:33:16 +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 bf6f1fa67e fix(dart): a typedef references the types it aliases (#2763)
In

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:46:20 +00:00
Colby MchenryandClaude Opus 5.5 dbb6aa9ba9 fix(vbnet): link reads through a namespace or a bare member name, and run the parameterless overload (#2762)
Three kinds of VB.NET member read that #2355 and #2377 left unlinked:

- A method named without parentheses (`Me.Load`, `Calc.GetTargetDAR`,
  a constructor's `Me.New` / `MyBase.New`) went to the first overload
  declared, so `Me.Load` reached `Load(path)` instead of
  `Load(Optional force = False)`, and a `Me.New` whose first overload was
  the calling constructor itself linked nothing.
- A read whose first name is a namespace (`Billing.Invoice.MaxLines`,
  `StaxRip.UI.ButtonEx.ButtonSymbol.Delete` with the root namespace in front,
  a Module's `RedGifs.RedGifsSiteKey`) linked nothing: the receiver was
  looked up as a value or a type only.
- A field, property or Module variable named bare before the dot
  (`Button1.Text`, `Settings.Plugins`, staxrip's global `p.TargetWidth`) was
  not counted as read: a bare name is no member access, so only what is read
  through it could link, and nothing when that is .NET's (`Text`).

Fix (src/resolution/vbnet-receivers.ts; resolution only, extraction output
unchanged):

- A method a reference runs without parentheses resolves among its type's
  overloads to one that takes no arguments (none, or only Optional and
  ParamArray parameters, read from the declaration), never the member the
  reference is in. AddressOf / NameOf keep the first overload, as before.
- A receiver name that is neither a value nor a type is looked up as a
  namespace the way VB.NET does: nested in a namespace around the read,
  outward to the root namespaces, then in the file's and project's Imports.
  Each further link is a type declared in it, a nested namespace, or a
  member of one of its Modules. The read links the type (`Billing.Invoice`)
  or the member (`Assets.Logo`), as #2355's `Outer.Mode` reads do.
- A bare name before the dot that VB.NET binds to a field, property or
  Module variable (not a local or parameter, which hide it) is linked as a
  read alongside what is read through it. Not for a `With` block's `.Text`,
  a method run through the name (the call is the method's alone, as with
  parentheses), another project's Module, or a member declared with an
  outside type of its own name (`Responser As Responser`), whose Shared
  members and nested types VB.NET reads through the type; a project type of
  its own name follows #2377's Color Color rule.
- membersNamed looked a type's members up in every project's same-named
  type and memoised the list by qualified name alone, so SCrawler's two
  plugin `UserData` classes shared one list ordered by whichever was asked
  first. It now keeps to the type's own project. Alone this leaves main's
  graph unchanged on SCrawler and staxrip; without it, 17 of the new
  bare-name reads in one plugin linked the other plugin's members.

Dropped from the 2026-10-04 test set: a method named through its class
also linking the class (#2355 treats it as a call, and its test pins that);
`AppSession.SessionId` through a property typed OtherSession linking
nothing (#2377 links OtherSession's field, pinned the same way);
`SCrawler.My.Resources...` reads (#2355/#2377 send no path that starts with
`My` or `Global`); and same-named classes picked by namespace, Imports and
project, which main already does once the fixture declares its
RootNamespace.

Verification: new vbnet-member-access-scope.test.ts (9 tests) fails 6/9 on
main and passes; a mutation that restores main's membersNamed fails the
own-project test. The other VB.NET, C#, extraction, resolution, sync and
framework suites pass; tsc and build clean.

Validation, main vs this branch (indexes from each arm's own build; nodes
unchanged; self-loops unchanged; serilog and gin dumps byte-identical):

- SCrawler 31,201 -> 36,378 edges. 1 removed: `COLOR_PICKER.ColorsGet`
  re-pointed from the two-argument overload to `ColorsGet() As DataColor`.
  Added: 4,973 bare-name reads (3,731 own class, 957 Module variables,
  285 inherited), 192 namespace reads, 12 reads through a Module member
  reached by namespace, and that 1 overload.
- staxrip 70,864 -> 83,909 edges, 0 removed. Added: 12,983 bare-name reads
  (8,991 own class, 3,820 Module variables, 172 inherited), 61 reads with
  the root namespace in front, and `box.Show` reaching
  `Function Show() As DialogResult` from its Shared overload.
- Checked against source: every bare-name read's name starts the access,
  has no local of that name, and binds to its class, an ancestor (23
  through a qualified `Inherits Instagram.UserData` the graph's own extends
  edge misses) or a Module of its project; every namespace read's path ends
  its target's full name and the target's project is the reader's or one it
  references; no edge crosses between staxrip and its AutoCrop tool; 48
  sampled edges hand-checked, all correct.
- Cost (5 interleaved rounds, medians, quiet machine): SCrawler wall
  5.60 -> 5.75 s, CPU 19.5 -> 20.3 s, DB 43.4 -> 45.1 MB; staxrip wall
  7.23 -> 7.11 s (noise), CPU 25.7 -> 26.3 s, DB 59.0 -> 64.4 MB, the
  growth being the new edges (unresolved refs fall 47,074 -> 43,607 and
  38,051 -> 36,165).

Not covered: a call through a bare name (`Button1.Focus()`) or a function
named before the dot, reads in a `With Button1` block, and `My.` /
`Global.` paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 19:46:36 +00:00
Colby MchenryandClaude Opus 5.5 8b5d1d543d chore(scripts): regenerate the Go standard-library method table from Go's source (#2760)
go-stdlib-methods.ts lists the methods of 130 standard-library types with
their parameter and result counts. Its generator lived in a validation kit.
scripts/gen-go-stdlib-methods.mjs reads a Go source tree with the engine's
Go grammar, counts with the engine's own goSignatureArity, and prints (or
--write's) the table for the types it lists, so a later Go release's
methods are one command away. Against go1.27.3 it reproduces the table byte
for byte.

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:13:54 +00:00
Colby MchenryandClaude Opus 5.5 1f3a274c53 fix(go): a method a type declares in a file the index couldn't read is not its embedded type's (#2757)
kubernetes' core/v1 generated.pb.go is 1.7 MB, over the index's 1 MB
limit, so every API type's own String, Marshal, Unmarshal, Size and
MarshalToSizedBuffer is missing from the graph. Resolving `pod.String()` on
a `*v1.Pod`, or `m.Template.Size()` on a `v1.PodTemplateSpec`, the walk over
embedded types found the method the embedded TypeMeta or ObjectMeta has and
linked that, a method the call never runs.

resolveGoMethodInPackage, which every typed Go receiver goes through
(parameters, locals, fields, assertions, package-level values, indexed
elements), now reads the method declarations of the files of the type's
package that the index couldn't read, from their text, and links nothing
when the type declares the method there. matchMethodCall's typed branch and
matchGoPackageValueCall keep that null instead of falling through to name
matching, which found the same promoted method. A method the type really
gets from an embedded type, like GetName from ObjectMeta, still links. The
check that #2755 added for indexed elements moves into the shared path.

On kubernetes 4c05e667, 63 calls lose that link and nothing else changes;
etcd, prometheus, express and flask are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 17:44:33 +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 bbf987e13b fix(explore): the named symbols take turns in the dynamic-dispatch links list (#2751)
Links of the same rank were listed in edge order, so one named symbol's
long fan-out could fill the 6-line list before another named symbol's
first link. prometheus's `refresh.Discovery.refresh` hands its func
field to 28 discovery packages, and in `refresh.Discovery.refresh
hetzner.refresher.refresh hcloudDiscovery.refresh` those links filled
the list before `refresher.refresh` → `robotDiscovery.refresh`.

Each link now has a turn: how many links of its rank the named symbol
it hangs from already has. Links sort by rank as before, then by turn,
then by edge order, so links of the same rank alternate between the
named symbols. A token that names two interfaces gets both: prometheus's
`Appender.Append` matches storage.Appender and chunkenc.Appender, and
storage's implementations no longer wait behind six of chunkenc's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:07:51 +00:00
Colby MchenryandClaude Opus 5.5 f5dfa21fef fix(explore): a symbol written with its package resolves, like refresh.Discovery.refresh (#2750)
Explore's token resolution, codegraph_node's lookup and the callers,
callees and impact lookups dropped every Go symbol written the way Go
code names it: `refresh.Discovery.refresh`, `storage.Queryable.Querier`
and `gin.Context.Next` resolved to nothing, and so did `binding.Default`.
Three gaps:

- A qualified name was matched against the qualifiedName, which holds
  the type and not the package, or entirely against the path, which
  holds the package and not the type, never as a package followed by a
  type. A new matcher stage reads `pkg.Type.method` as leading
  qualifiers that name the package by path segment and trailing ones
  that end the node's scope. A Python `pkg.mod.Class.method` and a Rust
  `module::Type::method` read the same way.
- A module's root package has no directory: gin's `context.go` is
  `package gin`. A Go package qualifier now also matches the package the
  file declares, read from its `package` clause (src/graph/go-package.ts).
  That covers versioned `v2/` directories and generated packages like
  prometheus's `io_prometheus_client` as well. The clause is read last
  and only where it can decide, a top-level function or a node whose
  scope already ends with the written type, so a common name's hundreds
  of definitions cost no file reads.
- Explore and codegraph_node took a qualified token's matches from the
  full-text search's top 50 only, and for gin's `binding.Default` those
  were three `binding/default_validator*.go` files. Through one shared
  helper they now fall back to the exact-name index when the search has
  no match; the CLI's lookup already started there.

The new stage runs only after the existing ones fail, and the search
keeps its order, so a name that resolved before keeps its result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:50:38 +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 3cfd24a46d fix(explore): every dynamic-dispatch link gets its own line, the ones the query names first (#2748)
codegraph_explore's "Dynamic-dispatch links among your symbols" section
deduped its lines by `source name > target name`. Every interface →
implementation link of one method reads the same way (`TrySet →
TrySet`), so an interface method with several implementations printed
one line, and which implementation it kept was edge order, not the
query. On gin, `tryToSetValue setter.TrySet multipartRequest.TrySet
setByForm` printed only `TrySet → TrySet @binding/header.go:35` and hid
formSource's link.

Lines are now deduped by the edge's endpoints (node ids), and the
6-line cap stays. When more links exist than fit:
- a link that reaches a test or example file through an end the query
  didn't name goes last, as explore's answer leaves those files out;
  the predicate is explore's own `isLowValue`, moved to module scope so
  both uses share it;
- otherwise links go by how pointedly the query named their two ends.
  A symbol a token picks out (at most three definitions, the same
  notion as the boundary scan's agent-specific symbols) or a named
  non-callable endpoint weighs 3, a candidate of an ambiguous token 1,
  an unnamed symbol 0. An implementation the query names always comes
  before one it doesn't. The weights also keep an ambiguous token's
  candidates from crowding out a precise one: prometheus's
  `Discovery.refresh` matches a `Discovery` in each discovery package,
  and in `Discovery.refresh fetchApps Decoder.Decode` their links to
  each other filled the cap and hid the one link into `fetchApps`;
- edge order breaks ties.

The section takes the rendered Flow chain itself. It leaves out an edge
between two of the chain's nodes, as before, and also a line that reads
the same as one of the chain's synthesized hops or as a line already
listed. gin declares its `Binding` and `StructValidator` interfaces
once per build tag (binding.go, binding_nomsgpack.go), so the twin
declaration's link printed exactly like the Flow's hop.

A same-name link names each end by its owner, `setter.TrySet →
formSource.TrySet`, the way an agent writes it in a query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:08:44 +00:00
Colby MchenryandClaude Opus 5.5 e450ae3a63 fix(go): an unexported interface method is satisfied only from its own package (#2746)
An unexported Go method name belongs to the package that declares it.
goImplementsEdges matched method names only, so an interface asking for
an unexported method was linked to every type in any package with a
method of that name: each of prometheus's `refresher { refresh(...) }`
interfaces (one per discovery package) went to about 37 providers in
other packages, such as discovery/aws.EC2Discovery, none of which Go
lets satisfy it.

Method sets now hold an unexported name together with the directory of
the file that declares the method (goMethodKey), on both sides. Where
the method is declared is what counts, not where the type is: etcd's
gRPC servers outside api/etcdserverpb keep their KVServer edges,
because the UnsafeKVServer or UnimplementedKVServer they embed declares
mustEmbedUnimplementedKVServer in the interface's package.

The interface-dispatch bridge uses the same key, so a call through the
interface reaches the method of the interface's package: not the
type's own namesake from another package, and not one Go's selector
would skip at a shallower embedding depth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 13:31:48 +00:00
Colby MchenryandClaude Opus 5.5 6e24711590 fix(go): a call through a func-typed struct field reaches the functions put in it (#2745)
A Go struct can hold a function in a field and call it through the field.
Prometheus' service discovery runs every provider that way: refresh.Discovery
calls d.refreshf(ctx), NewDiscovery copies refreshf from Options.RefreshF,
and each provider passes its own method (RefreshF: d.refresh). Nothing
answered d.refreshf, so a flow from Discovery.Run stopped at the field; #2430
rightly dropped the interface-impl edges that had stood in for it.

goFuncFieldEdges, a Go-only synthesis pass, links a call x.F(...) through a
func field of a struct to each function or method value put in that field:
by a composite literal (keyed or positional, found through its instantiates
edge), by an assignment x.F = v, or by one level of field-to-field copy
(refreshf: opts.RefreshF). A value is what the resolver already linked it
to, else x's method m for x.m, an interface's method for an interface value.
Receivers are typed through the resolver's own Go scope reader
(goLocalDeclarationAt over goLocalDecl, #2706), or by a local's composite
literal, new(T), type assertion or resolved call result. Channels are keyed
by package, struct and field, so a generic field name never crosses between
unrelated structs; one package's test wiring links no other package's test;
an edge cites a function's production wiring; generated files are not read.

Edges are calls, provenance heuristic, synthesizedBy go-func-field, with
field, via (package-qualified when the copied field is another package's)
and registeredAt; explore, the node trail and ContextBuilder read them as
"func field `Discovery.refreshf` (set from `Options.RefreshF`)". The pass
sits after the Go framework passes, with a pool cost hint, and every Go file
now counts as a synthesis input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 13:04:48 +00:00
Colby MchenryandClaude Opus 5.5 e24827011e fix(sync): a name imported through a barrel links when an edit gives the barrel its re-export (#2744)
Sync and the file watcher now link `import { useInvocation } from './context'`,
and every use of the name, when an edit gives the barrel context/index.ts
that re-export after the importing file was indexed. Before, the import,
its calls and its JSX or template tags stayed unlinked until the importing
file changed or the project was re-indexed.

The barrel declares none of the names it passes on, so sync's retry by name
never reached the importers' failed references. The edit adds no file and
changes no definition, so neither the importer re-open through added files
(PR #2512) nor the barrel walks (PR #2560, PR #2665) run.

importBindingTail now parks the whole name of a plain named import under
its module's key (`module:context`), as PR #2450 does for default, namespace
and aliased bindings and PR #2507 for Python's unaliased imports. It does so
only while the import resolves to a project file that carries that key and
that module doesn't give the name yet; the by-name lookup still finds the
reference by its own name.

Three versions were measured on 18 repos, and all fix the same barrel-edit
cases. This narrowest one parks 2,727 references on a fresh index, against
11,061 for parking every plain named import, and keeps the retries per
changed file within +1% to +3% of main on folo, vben and milo-ui. Fresh
indexes are identical in nodes, edges and files in all 18 repos, and on
vben's gut scenario sync's difference from a fresh index falls from 2,337
lines to 194.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:01:53 +00:00
Colby MchenryandClaude Opus 5.5 aa01692620 fix(js,ts): an import from the app's root maps through the app's own tsconfig paths (#2740)
bigbluebutton's client imports nearly everything from its root,
`import Icon from '/imports/ui/components/common/icon/component'`, which its
tsconfig maps with `"paths": { "/*": ["*"] }`. tsc maps a rooted name through
`paths` (only `./` and `../` count as relative there) and never reads it
from `baseUrl`. ownConfigCandidates refused every specifier starting with
`/`, so all 2,375 of these imports stayed unresolved and every name they
import linked by bare-name matching, often to the wrong one of several
same-named components.

A rooted specifier now goes through the nearest config's `paths` patterns
only; `baseUrl` is still never applied to it, and a protocol-relative
`//host/x` stays out. On bigbluebutton the unresolved refs fall from 26,189
to 22,565 and its rooted imports from 2,375 unresolved to 1 (a module that
isn't in the repository); 13 control repos are byte-identical.

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 09:29:05 +00:00
Colby MchenryandClaude Opus 5.5 7b9c356dd7 fix(react): a type annotation in a .tsx file names a type, never a same-named component (#2737)
React's Pattern 1 ran its component lookup on every PascalCase name in a
.tsx or .jsx file, type annotations included, and its candidate could
outrank name matching, so an annotation linked to a component: outline's
InputSelect.tsx declares `type Option`, and its annotations went to the
`Option` component of Export/ExportDialog.tsx; mastodon's `<Item>` type
parameters went to the `Item` class of media_gallery.jsx, and bulletproof's
`TableProps<Entry>` to another app's `function Entry`.

A type annotation is now name matching's, which scopes a type as TypeScript
does since PR #2736: the file's own, an import's or a global one. Pattern 1
decides only a type alias or interface the file declares beside a value or
a component of the same name, both of them the file's own: the annotation is
the type. That covers bigbluebutton's `type ModalRegistration` beside its
render-prop component, mastodon's `type ShortNumberRenderer` and `interface
AccountList` beside their functions, and a type beside a value that PR #2420
reads through its wrapper or loader. Every other reference keeps PR #2420's
order: a value's loaded or wrapped component, then a function, class or
component the file declares, then name matching for any other declaration
of the file's, and the component lookup only for a name the file doesn't
declare.

Validated on 13 React repos, main 458ad519 against this fix, every changed
site checked against the TypeScript checker and the source:
- 11 annotations move from a component to the type their file declares
  (bbb 5, outline 3, mastodon 3);
- 15 type parameters and qualified types lose a wrong component and link
  nothing (mastodon 8, bulletproof 3, bbb 3, outline 1);
- 220 last names of qualified types, like `React.ReactNode`, `Y.Doc` and
  `SectionKind.Legend`, now come from name matching, wrong before and after:
  219 keep their target and 1 moves to another wrong one (outline 205,
  excalidraw 9, signoz 3, mastodon 3); PR #2736 left that name to name
  matching on purpose;
- next-saas, proshop, t3-turbo, minimal-kit, matx, berry and mantis are
  byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 09:02:31 +00:00
Colby MchenryandClaude Opus 5.5 458ad51921 fix(ts): a type a module doesn't import is never another module's (#2736)
A TypeScript module reaches another module's type only through an import,
so a type it names without importing it is its own or a global one: a
script's top-level declaration or one in a declare global block. #2726
held a supertype to that. A type annotation, a type argument and a type
alias's value went to the name matcher like any other name, which handed
them a project type of that name: vscode's Omit<...> annotations reached
quickInput.ts's export type Omit instead of TypeScript's own, and type
parameters and mapped keys, like excalidraw's <Point extends ...> and
frontend's <T>, reached other modules' types and enum members.

Both name-match strategies now leave out another module's non-global type
for a TS or JS module's type reference to a name the file doesn't import,
ahead of the ranking, so a declare global type still wins over a nearer
module's namesake. Left as they were: the last name of a qualified type,
vscode.TextDocument, written after a dot; a name inside a declare module
'x' block, which is module x's own; and a name an import X = ns.X alias
binds, which the file's import list doesn't hold.

Globals come from declaration files and declare global blocks. A script
.ts's top level is a sample's or a fixture's, global to its own program
only (vscode's colorize fixtures declare type T), and a test's or
fixture's globals reach its tests only, as its declare module blocks do.
That holds for supertypes too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:34:49 +00:00
Colby MchenryandClaude Opus 5.5 0c28480576 fix(cpp): -> on a value of one of the project's classes follows its operator->, like rocksdb's FileSystemPtr and MultiGetContext::Range::Iterator (#2735)
C++ reads `x->m` on a class value as `x.operator->()->m`, and applies
`->` again to a class it returns. Only a library holder's `->` and a
container iterator's (`std::vector<T>::iterator`) were read. A call
through a project iterator or smart-pointer wrapper was guessed by the
method's name:
- rocksdb's `fs_->NewWritableFile(…)` on a `FileSystemPtr`, whose
  `operator->` returns a `std::shared_ptr<FileSystem>`, reached
  `FaultInjectionTestFS`;
- `writable_file_->Truncate(…)` on an `FSWritableFilePtr` reached the
  legacy `WritableFile::Truncate`;
- `iter->lkey->user_key()` through `MultiGetContext::Range::Iterator`,
  whose `KeyContext* operator->()` is a `KeyContext*`, reached
  `BlobCountingIterator::user_key`.

Both a member chain's `->` and a `->` call on a declared receiver end in
cppIteratorTarget. For a type that is no container's iterator, it now
falls back to cppOperatorTarget:
- what the class's `operator->` returns, through the pointer it is, or
  with `->` applied again to a class it is: a library holder, or another
  of these;
- else what its `operator*` returns;
- the return type is read where the class declares it, and the class's
  own template parameters are bound to the type's written arguments
  (cppTemplateBound): `UnownedPtr<DB>`'s `T* operator->()` is a `DB*`;
- a parameter the type doesn't bind names no particular class.

A log-only census on main bdf288a7 found 304 rocksdb and 42 protobuf
dereferences of a project class with an `operator->` that gave no
element. Among the rocksdb calls, 77 were bare-name guesses and 56 were
unlinked.

Kit A/B against bdf288a7:

| repo | removed | added | retargeted | same target, now typed |
|---|---|---|---|---|
| rocksdb | 2 | 49 | 158 | 89 |
| protobuf | 0 | 0 | 7 | 3 |
| leveldb, fmt, redis | identical | | | |

- Retargeted:
  - FaultInjectionTestFS → FileSystem (57), WritableFile → FSWritableFile
    (32), and TruncatedRangeDelIterator through UnownedPtr (16);
  - LookupKey and GetContext through MultiGetContext's iterator;
  - SequentialFile and RandomAccessFile → FSSequentialFile and
    FSRandomAccessFile;
  - protobuf's `map_field_->` calls from WeakRepeatedPtrField to the map
    field bases;
  - overloads (DB::Put, PinnableSlice) chosen on the now-known class.
- Removed: `iter->timestamp->assign(…)` on a `std::string*`, which had
  reached `autovector::assign`.
- Added: Status::PermitUncheckedError, FileSystem::OptimizeFor…,
  Cleanable::RegisterCleanup, and gtest's ParamGeneratorInterface through
  linked_ptr.
- About 40 changed sites read by hand were all right.

Mutations: dropping the fallback, the operator* fallback, the second
`->` or the template binding each fail the new tests. Accepting an
inexact class, found only as the sole one of its name, survives; the
guard stays, as in the member-chain matcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:16:26 +00:00
Colby MchenryandClaude Opus 5.5 4e35e0cb78 test(python): the module-resolution tests remove their temp dir after indexing (#2734)
python-module-resolution.test.ts removed a test's temp dir only when the
test never opened a CodeGraph (`if (cg) cg.destroy(); else if ...`), and
destroy() closes the database without deleting anything. So every test
that indexed left its codegraph-pymod-* dir behind, 47 per full run,
passing or not, since PR #1921 added the file. The dev box's %TEMP% held
1,739 of them by 2026-10-21.

The dir is now removed after destroy(), as the other test files do.

Checked with a private TEMP: main's file leaves 47 codegraph-pymod-* dirs,
this one none, and its 50 tests pass both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:34:41 +00:00
Colby MchenryandClaude Opus 5.5 bdf288a76f fix(cpp): a pimpl's Rep, which a header names and its source file defines, is found for the code in that file (#2733)
A C++ name is looked up from where it is written, and a class that a
source file defines belongs to that file's translation unit, so only that
file sees it (#2538). A header that declares `struct Rep;` and
`std::unique_ptr<Rep> rep_;` writes the name for a `Table::Rep` that its
source file defines, and code in that file reads the member through it.
The lookup ran from the header, saw no source file's classes and found
nothing. The call was then guessed by the method's name, often onto the
calling class's own method:
- leveldb's `rep_->index_block->NewIterator(…)` reached
  `Table::NewIterator`;
- rocksdb's `rep->write_batch.GetDataSize()` reached
  `WriteBatchWithIndex::GetDataSize`;
- rocksdb's `level_iters_[i]->status()` reached `ForwardIterator::status`.

cppClassWritten's lookup now also sees the classes of the file the
reference is in, the translation unit that includes the header
(visibleFrom(file, unit)). classNode filters by that same visibility
instead of recomputing the header's.

A member-chain diagnostic on main 81fd0078 found 99 rocksdb and 7 leveldb
chain calls through a `Rep` whose class couldn't be placed.

Kit A/B against main 5994edb3:

| repo | removed | added | retargeted | same target, now typed |
|---|---|---|---|---|
| rocksdb | 34 | 40 | 43 | 56 |
| fmt | 0 | 27 | 0 | 1 |
| protobuf | 25 | 1 | 2 | 2 |
| leveldb | 0 | 1 | 3 | 3 |
| redis | identical | | | |

- Retargeted: from the calling class's own method or a namesake to the
  member's class:
  - rocksdb: WriteBatch, ForwardLevelIterator, IndexBuilder and
    PartitionedIndexBuilder, RangeDelBlockBuilder, Env, MemFile;
  - leveldb: Block::NewIterator;
  - protobuf: DescriptorPool::Tables::FindSymbol.
- Removed: calls on library members reached this way, which had linked to
  namesakes:
  - `std::stack::pop` (BinaryHeap::pop);
  - `std::vector::size` (Block::size);
  - `std::thread::join` (WindowsThread::join);
  - absl::flat_hash_set's `clear` (Map::clear);
  - `std::optional`'s `has_value` and `reset` (Option,
    ScopedPythonPtr).
- Added:
  - WriteBatch's Put, Delete, Merge and the rest, through
    WriteBatchWithIndex's `rep->write_batch`;
  - gtest's UnitTestImpl through `impl()`, in fmt's and rocksdb's vendored
    gtest;
  - leveldb's FilterPolicy::Name.
- About 35 changed sites read by hand were all right.

Mutations: dropping the unit from visibleFrom, or from the lookup's call,
fails the new tests. classNode's own filter only chooses among several
declarations of one class, and nothing on the kit or in the tests has two,
so dropping it survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:31:38 +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 5994edb34e fix(cpp): a member declared in an anonymous union, like protobuf's union { Impl_ _impl_; };, is found as a member of the class around it (#2731)
A class's data members are read from its own lines, skipping those of
the functions and classes nested in it. The index keeps an anonymous
union or struct as a nested `<anonymous>` node, so its lines were skipped
too, though C++ looks its members up as the enclosing class's
([class.union.anon]).

Every generated protobuf message keeps its fields in `union { Impl_
_impl_; };`, so a call through `_impl_` was guessed by the method's name:
- `this_._impl_.type_url_.Destroy()` reached `Arena::Destroy`;
- `_impl_._cached_size_.Get()` reached `FieldArenaRep::Get`;
- `_impl_._has_bits_.Clear()` reached the message's own `Clear`.

fmt's `value` keeps its argument in a union of `string`, `custom` and the
rest in the same way.

cppNestedSpans now leaves out an anonymous union or struct with no name
and no declarator (isCppAnonymousMembers):
- a bare `union {` or `struct {` head;
- `};` after the closing brace.
A union with a declarator, `union { … } u;`, keeps its members to itself,
and so does a partial specialization, which the index also keeps as
`<anonymous>` but whose head names its template.

A member-chain diagnostic on main 81fd0078 found the anonymous union
behind most of protobuf's untyped chains. Kit A/B against 81fd0078:

| repo | removed | added | retargeted | same target, now typed |
|---|---|---|---|---|
| protobuf | 2 | 54 | 349 | 231 |
| fmt | 0 | 0 | 1 | 1 |
| rocksdb | 0 | 0 | 2 | 0 |
| leveldb, redis | identical | | | |

- Retargets go from bare-name guesses to the member's class:
  ArenaStringPtr (Set, Get, IsDefault, Destroy), CachedSize::Get,
  HasBits::Clear, RepeatedPtrField, SourceContext and MessageLite.
- The 2 removals are `ptr.string_value->clear()` on a `std::string*`, which
  had reached `Map::clear`.
- fmt's and rocksdb's retargets pick the overload their argument fits now
  that a union member has a type: `map(value_.uint128_value)` takes
  `native_uint128`, and `Add(…, props.oldest_ancestor_time)` the `uint64_t`
  overload.
- About 45 changed sites read by hand were all right.

Mutations: dropping the nested-span exclusion, the declarator check or
the head check each fail the new tests. Dropping the name check survives,
because the head check covers named types; it stays as a cheap early exit.
A matching change to the caller's-declaration check left the kit A/B
byte-identical, so it was not kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 06:55:34 +00:00
Colby MchenryandClaude Opus 5.5 81fd007850 fix(cpp): a call through a namespace links no class's method by its name (#2730)
A C++ call written through a namespace, `x::m()`, reached matchMethodCall's
name strategies when no declaration of `x::m` was found by qualified name.
They can only return a method of some class, which a namespace never holds:
the capitalized-receiver class (`internal` → a class `Internal`), the one
method of the name in the project, or the best receiver-word guess. So
protocolbuffers/protobuf's `absl::StripPrefix(name, …)` linked to its Rust
generator's `MultiCasePrefixStripper::StripPrefix` and `absl::Status(…)` to
`upb::Status`'s constructor. fmtlib/fmt's `fmt::ptr(p)` linked to a test
scanner's `ptr()` method, and `fmt::day(…)` to a test's `date::day`, ahead of
the step that reads the namespaces a macro opens and finds fmt's own `ptr`
function and `day` alias in `FMT_BEGIN_NAMESPACE`.

Those strategies now return nothing when the qualifier is a namespace
(isCppNamespaceQualifier): a lowercase name that no C or C++ type or alias
node has, and that the calling file declares as neither a template parameter
nor an alias (cppNamesBroughtIn, now split into type names and namespace
names, since a namespace the file opens confirms it is one). rocksdb's
`hash_table::Insert(…)` through `typedef HashTable<…> hash_table;` keeps its
edge. So does the constructor of a class the call names in that namespace
(isCppConstructorThrough): protobuf's `internal::SourceWrapper<absl::Cord>(&data)`
constructs `struct SourceWrapper<absl::Cord>`, a specialization the index
holds without its name, whose template is only declared. A method of
another template's specialization is no constructor: fmt's
`fmt::scan<int>(…)` is fmt's `scan` function, not `scanner<num>::scan`.
The check runs only where one of those strategies would return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 05:56:24 +00:00
Colby MchenryandClaude Opus 5.5 009e4377ff fix(cpp): a call through a class the index doesn't hold gets no guess by a shared word (#2729)
A C++ call written through a class, `X::m()`, whose class has no node in the
index fell through to the last strategy of matchMethodCall, which scores
every method named `m` by the words its class shares with `X` and picks one.
protocolbuffers/protobuf declares its descriptor messages in descriptor.pb.h,
which is over the indexer's size cap, so `FeatureSet::default_instance()`
linked to `UnknownFieldSet::default_instance`, `FeatureSetDefaults::GetDescriptor()`
to `JavaFeatures_NestInFileClassFeature::GetDescriptor`, and the options
messages' `default_instance()` to `pb::enumvalue::JsonEnumValueOptions`'. A
class whose name only shares a word with `X` is another class: one named `X`
was looked for first, by qualified name and by class name.

That guess is now dropped when `X` is a capitalized name (isUndeclaredTypeName)
that no C or C++ class, struct, union, enum, alias or namespace node has, and
that the calling file makes nothing of itself (cppNamesBroughtIn): not a
template parameter of any `template <…>` head in it, not an alias it declares
(a function-local `using FD = FieldDescriptor;`, a typedef, or one a macro it
expands declares, like rocksdb's IMPORT_RIBBON_IMPL_TYPES), not a namespace it
opens, aliases or uses, and not googletest's TypeParam or TestFixture in a
typed test. The one method of a name in the project is still taken as before.

The scans read the file once (cppTypeNamesDeclared in cpp-type-aliases.ts,
shared with the template-head reader) and the project's headers once for
macro bodies, only when a guess would otherwise be returned.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 03:44:24 +00:00
Colby MchenryandClaude Opus 5.5 226ac930de fix(cpp): a call through a written class template specialization, like formatter<int>::format(…), never reaches another specialization's member (#2725)
The index names a class template specialization's out-of-line member
after the template: fmt's test-only `auto
fmt::formatter<incomplete_type>::format(…)` is `fmt::formatter::format`.
So `formatter<int>::format(42, ctx)` in fmt's tests reached that member,
defined in another file, and `formatter<std::tm, Char>::parse(ctx)`,
where the template declares no such member, fell through to a guess by
the receiver's name: `native_formatter::parse`.

- A C++ call whose qualifier is written with template arguments keeps a
  candidate defined out of line for a full specialization of its class
  only when that specialization's arguments are the call's. A template's
  own members, which have a `template <…>` header with parameters,
  partial specializations' members and members defined in their class
  stay. This applies beside the existing specialization pool in the exact
  and partial qualified-name matches and in cppMethodOf's own members.
- When the class strategy finds no member for a call through a project
  class template's written specialization, matchMethodCall stops: a
  method picked by the receiver's name would be another class's.

Measured on main f6d8f19b: fmt 17 wrong edges removed; json's 16
changed sites all point at a phantom node (a noexcept clause read as a
declaration, filed separately); rocksdb, protobuf, leveldb, abseil,
redis and seven controls in other languages are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 03:10:30 +00:00
Colby MchenryandClaude Opus 5.5 f6d8f19b29 fix(rust): a call through a trait-bounded generic, like Key::get(…) with Key: MapKey, reaches the bound trait's method (#2724)
A Rust call through a generic type parameter is a call of the method its
bounds bring in: protobuf's `Key::get(…)` in `fn map_get<Key: MapKey>`,
with `trait MapKey: … + FfiMapKey`, is `FfiMapKey::get`. No strategy
read the bounds for such a path. These calls linked only when a
qualified name happened to end with the written text, which the scope
boundary of the previous fix rightly stopped, and a parameter bounded
only by an outside trait (`T::default()` under `T: Default`) could reach
a project type's unrelated `default` by name.

matchRustGenericPath, right after the `Self::x` branch of
matchMethodCall, reads the bounds the function and the impl or trait
around the call put on the parameter (in `<…>` or a `where` clause) and
resolves the method among those traits and the traits they extend,
through the helpers typed receivers already use. Outside-only bounds,
markers, and two traits declaring the method give no edge. The ref is
recorded as typed, so a trait method's missing `pub` doesn't hide it
from another module.

Measured on main ac9f96f8: protobuf +47, tokio +43, axum +14, clap +10,
serde +7, nothing removed; ripgrep and eight controls in other languages
are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 02:25:32 +00:00
Colby MchenryandClaude Opus 5.5 ac9f96f8a3 fix(cpp,rust): a qualified call like Status::OK() links the class it names, not one whose name ends the same way (#2723)
The partial qualified-name match took any declaration whose qualified
name ended with the written text, with no scope boundary. In
facebook/rocksdb about 4,100 calls to `Status::OK()`,
`Status::InvalidArgument(…)` and their siblings went to `IOStatus`'s
methods of those names; base-class calls from an override, like
`return FileSystemWrapper::NewWritableFile(…)` or leveldb's
`return DB::Put(…)`, went to a namesake subclass or to the calling
method itself; and ripgrep's `Path::new(…)` on std's `Path` went to its
own `PrinterPath::new`.

- The written name must start a scope of the candidate's: the
  candidate's qualified name is the name, or ends with it right after
  `::` or `.`.
- Among those candidates, the ones in the call's own language come
  first. protobuf's C++ `DescriptorPool::generated_pool()` met a Ruby
  namesake first, which the language gate then dropped.
- A C++ name qualified by a template parameter, like
  `CacheShard::ComputeHash` in `template <class CacheShard> class
  ShardedCache`, gets no edge from any strategy, except to a member of
  the parameter's default (`typename Parser = TcParser`). The boundary
  alone moved rocksdb's call to the receiver-word guess, with the same
  wrong target.
- The C++ template-head reader names each parameter by its last word,
  `int N`, `detail::fixed_string S`, `const PrefetchOpts& kOpts`, and
  keeps its default. A `<…>` after a C++ declaration's name is a
  specialization's arguments, not parameters. Before, `WireFormatLite::
  FieldType kKeyFieldType` made `WireFormatLite` a type parameter.

Measured on main bd62d9c0 and re-checked on 17f1453a: rocksdb 4,219
retargeted, 83 added, 30 removed; protobuf 24, 47, 40; fmt 7, 0, 21;
leveldb 2, 0, 0; ripgrep 0, 0, 38. Nine Ruby, Java, C#, C, JS, Python
and Go controls are byte-identical. Only C++, Rust and Ruby reach this
match; the file-path suffix match is already anchored and is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:53:51 +00:00
Colby MchenryandClaude Opus 5.5 17f1453a6c fix(rust): a value's declared type is its binding's in scope at the call (#2722)
The type a Rust value is written with, read for `reader.as_mut().m()`
and calls through a `Pin`, came from a backward line scan for `reader:`
in the calling function. That took a `let reader: …` in a block the call
is outside of, or a struct literal's `reader: …` field, for the
declaration; it also stopped at a `let entry = entry.as_ref()…` on the
call's own line, whose new binding is not yet in scope there. It is now
the binding rustBindingAt finds in scope at the call, through the
block-scoped reader the typed-receiver path already uses.

A/B against main with fresh indexes: clap, dioxus, axum, serde, ripgrep
and gin are byte-identical; tokio has one call read more precisely, the
same target at a higher confidence (`entry.as_ref().handle()` on the
parameter `entry: NonNull<TimerShared>`). Two regression tests, both
failing on main. Full suite on the tested tree: 8,039 passed, 0 failed.

Follow-up 3 of the Rust receiver series (PR 2719's open list). Follow-up
2, an impl's type parameter in a return type read from the receiver's type
arguments, was measured and dropped: 84 value reads across six Rust repos
reach the case, most substituting a generic for a generic, an associated
type, or a head type already named, so about 10 to 20 calls would gain.

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:54:30 +00:00
Colby MchenryandClaude Opus 5.5 7324faf69f fix(rust): a call on a field of self reaches the method of the field's declared type (#2719)
A Rust `self.field.m()` call was resolved by the last segment of the
field's type name alone. `inner: multer::Field` linked to axum's own
`Field::name`, `std: std::time::Instant` to tokio's `Instant` (the calling
method itself), `inner: std::sync::Mutex<…>` to tokio's loom mock or async
`Mutex::lock`, and a field generic over the impl's `S: Store` to nothing.

The field's type is now read the way field values already are: from the
struct's declaration, through the wrappers, by the `use`s of the
struct's file. A struct's own parameter is known by the bounds that the
impl around the call gives the parameter it is instantiated with
(`struct Worker<S>` under `impl<S: Store> Worker<S>`). The method is the
one the typed receiver path picks. The plain-name reading stays only
where the field's type can't be read.

A call on a std or dependency value reaches a project trait's impl for
its type only when the impl is for that type. tokio's `impl Kill for
Child` in process/windows.rs is for its own `struct Child`, not
`std::process::Child`. The impl's type is read as the impl writes it:
gleam's `impl LowestVersion for pubgrub::Range<Version>` is for pubgrub's
`Range`.

A/B with fresh indexes against main on axum, serde, ripgrep, clap, dioxus,
tokio and gleam, every changed edge triaged: 361 calls linked that were
not (fields bounded by the impl's traits, dioxus's signal fields, trait
objects), 104 removed (calls on std and dependency fields, and 5 on tokio
values whose methods sit inside `cfg_*!` macros), 59 moved to the right
type of a shared name, 2,844 kept at a higher confidence; 62
fewer self-loops and none added. gin, serilog, flask and express are
byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:04:17 +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
Colby MchenryandClaude Opus 5.5 4286083a84 fix(rust): a for-loop variable or a tuple pattern's name has the type it is bound from (#2717)
A Rust `for` loop's variable, and a name a tuple pattern binds (`for (k,
v) in &map`, `let (tx, rx) = mpsc::channel(8)`), had no type the typed
receiver path could read, so calls on them fell to the older receiver
inference and the name strategies. tokio's 198 `tx.send(…)` calls on
channel senders went to the internal `chan::Tx::send`, and 100 calls on
the receivers to `chan::Rx`.

The scope reader now records what a `for` loop iterates, and gives the
plain names of a top-level tuple pattern their place in it. A loop's
variable is an item of what it iterates: a std sequence's element, a std
map's `(K, V)`, a slice's, a std iterator's or an `impl Iterator<Item =
T>`'s item, through `&` and the pointers that deref to what they hold.
The std adapters that keep the items (`.iter()`, `.rev()`, `.skip(n)`)
or project them (`.keys()`, `.values()`, `.enumerate()`) are looked
through only where what they are called on is read, so a project method
of one of those names is read from its own signature. A tuple pattern's
name is the matched tuple's field at its place. A field access
(`self.items`, `opts.config`, `pair.0`) has the type the receiver's
struct declares for the field.

A free function called through a module that a `use … as` renames was
looked up by the alias: tokio's loom tests' `oneshot::channel()` under
`use crate::runtime::tests::loom_oneshot as oneshot` read the return type
of `sync::oneshot::channel`. The path's head now goes through its `use`,
and the project's modules are walked along it.

A/B with fresh indexes against main on axum, serde, ripgrep, clap, dioxus,
tokio and gleam, every changed edge triaged: 394 calls linked that were
not, 86 removed (calls on std or dependency values, tokio values whose
read and write methods come from macro-declared extension traits, derived
`Clone`s, fields that hold functions), 404 moved to the declared type's
method, 625 kept at a higher confidence. gin, serilog, flask and express
are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:33:36 +00:00
Eric MinishandClaude Opus 5.5 e043efbc84 fix(resolution): a call through a class name reaches that class, not one whose name contains it (#1935)
A call through a class name, `Logger.log()`, or through a receiver named
after one, `logger.log()`, now binds to that class itself: its own
method, else the nearest one a declared supertype has. Before, the
method was any one in the class's file whose qualified name contained
the class's name, so a `FileLogger` declared first took every
`Logger.log()`, and Kotlin's `Foo.create()` went to `FooBar`'s.

Reworked from the exact-owner rule alone, which lost inherited methods
(Django's `form.is_valid()` on `Form(BaseForm)`, Alamofire's protocol
methods). Also in this change:

- A class declared inside a function is named only there: a call from
  anywhere else no longer reaches a class a test declares in its body,
  through the class name, the receiver's name, or the method-name
  fallback. Those methods were never in the running, and the one method
  such a filter leaves is still only a guess by the receiver's name.
- Production code never names a test suite's class, as the method-name
  fallback already held: `manager.check()` in Django's
  `Model._check_managers` is no test app's `Manager` model.
- A Python base built by a class's factory, Django's
  `class Manager(BaseManager.from_queryset(QuerySet))`, names that class
  as the supertype, besides the factory's arguments.

A/B against main with fresh indexes, every changed edge triaged. Django:
67 links added (model instances reaching `Model.save`, inherited lookups
and expressions), 40 removed (links into test-local classes from outside
their function, and one polymorphic manager call), 263 retargeted:
about 170 to the right class (`user.has_perm` to `PermissionsMixin`
rather than `AnonymousUser`, `parser.parse` to the template `Parser`
rather than `MultiPartParser`, `field.get_lookup` to
`RegisterLookupMixin`), about 90 from one guess to another, and about
20 worse, where a receiver named after a model holds something else
(`session.save()` on a session store now guesses the `Session` model).
flask: one fix (`app.get` to `Scaffold.get`), 3 wrong links removed.
Alamofire: one fix and 3 protocol-typed locals that now guess the
`Adapter` class. zod, excalidraw and spring-petclinic: higher
confidence on the same targets. gin, cobra, express, eShopOnWeb and
jekyll are byte-identical. Full suite on the merged tree: 7,908 passed,
0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:55:52 +00:00
Colby MchenryandClaude Opus 5.5 f76021db22 fix(rust): a local holding a method call's value has the type the method returns (#2716)
A Rust local bound to a method call (`let server = b.port(80).build()`,
`let s = cfg.load()?`, `let conn = c.connect().await`, `if let Some(c) =
arg.get::<Completer>()`) had no type the typed receiver path could read,
so calls on it fell to the name strategies. Those strategies did not read
`?` on a returned `Result`, a `self` method's value or a turbofish, and
picked a same-named method of another type.

The value is now the return type of the method the call reaches on its
receiver's type, read from that method's signature. A receiver is a local,
`self`, or another such call. `.await` hands out an `async fn`'s return
type (read off the source: the extractor's async flag is never set for
Rust) or an `impl Future`'s `Output`, and an `async fn` not awaited is a
future, left unread. A method's own type parameter is the call's turbofish
argument. A parameter the call doesn't write stays opaque: `Standard<W>`
is a `Standard`, but a value that is `W` itself is unread rather than its
bound. A trait's provided method returning `Self` gives the receiver's
own type.

A type with two impls of one trait, `Encoder<Bytes>` and `Encoder<&[u8]>`
or `Service<IncomingStream>` and `Service<Request<B>>`, was ambiguous for
a typed call. The impl whose trait argument is the call's first
argument's type now wins.

A/B with fresh indexes against main on axum, serde, ripgrep, clap, dioxus,
tokio and gleam, every changed edge triaged: 536 calls linked that were
not, 69 removed (calls on std or dependency values a method returns, and
tokio values whose read and write methods come from macro-declared
extension traits), 50 moved to the declared type's method, 565 kept at a
higher confidence. gin, serilog, flask and express are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:41:15 +00:00
Colby MchenryandClaude Opus 5.5 23b5ca1b43 fix(rust): a call on a local reaches the method of the type it is declared with (#2715)
A Rust method call on a local variable or a parameter (`opts.build()`) was
matched by the method's name whenever the receiver's declaration wasn't one
of two unscoped line patterns. dioxus's `Commands::Build(opts) =>
opts.build()` lost its link once the build module's `BuildRequest::build`
shared the name, and calls on std or dependency values linked to project
namesakes: `cx.waker()` on a `Context` to tokio's `CachedParkThread::waker`.

The receiver is now typed by the binding the call sees. The new
src/resolution/rust-scopes.ts reads each file's parameters, closure
parameters, lets, if-let / while-let / let-else, for and match-arm bindings
with block scope. A binding's type is its written one, the value it is
built from, or the variant or struct field its pattern takes. The method is
the one Rust picks on that type: inherent first, then a trait impl whose
trait is in scope, then a trait's provided method (a blanket impl only where
its bounds are met), then Deref. `&`, Box, Rc, Arc, Pin, Cow and the lock
guards are looked through, their own methods kept (one list with the
self-call path's), and a project impl on the wrapper itself answers before
what the wrapper holds, as it does for `self`. `impl Trait`, `dyn Trait` and
bounded generics reach the trait's method or a supertrait's. Among types
sharing a name, the one its path names through the project's modules wins,
the path written or bound by a block-scoped `use`, else the one that has
the method. A std, dependency or primitive type reaches only a project trait
impl in scope, and not one that hands the call on to the type's own method
of that name.

A binding that writes no type leaves the call to the name strategies, which
no longer take a project type from another declaration of the name. A
trait's own methods carry no `pub`: isVisibleAcrossFiles lets them through
for the calls this path resolved, and name guesses stay out as before.

A/B with fresh indexes against main on axum, serde, ripgrep, clap, dioxus,
tokio and gleam, every changed edge triaged: 4,645 calls linked that were
not, 887 removed (calls on std or dependency values, project types without
the method, a wrapper's own methods), 459 moved to the declared type's
method, 3,669 kept at a higher confidence. gin, serilog, flask and express
are byte-identical. The new code costs 0.6 to 0.9 s of CPU per full index
of tokio, dioxus or gleam.

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