Commit Graph

1371 Commits

Author SHA1 Message Date
Colby Mchenry 73797bf1e4 test(sync): compare synced graphs with an index rebuilt from scratch (#2411)
Three sync-convergence checks used `indexAll()` on the synced graph as
the "full re-index" side. Over an existing index that run skips every
file whose content is unchanged, so the edges the sync wrote survive it
and each check compared the synced graph with itself.

They now rebuild the index from an empty database, as `codegraph index`
does (`CodeGraph.recreate` + `indexAll`, the same oracle the CG-33
convergence suite uses):
- sync-import-retry: the imports of the unchanged importer, all 20
  languages.
- sync.test.ts "the synced graph matches a full re-index" and "rebinds
  the unchanged caller when the symbol moves to another file": every node
  and edge by natural key instead of node and edge counts, which a
  bidirectional difference can net out of. Both now index twice in the
  test body, so they get the 60s bound #1773 gave such tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:15:36 +00:00
Colby Mchenry 31c3328d33 fix(sync): a Liquid section or snippet reference links its file when the file appears later (#2403)
* fix(sync): a Liquid section or snippet reference links its file when the file appears later

A sync retries a parked failed reference by its name tail, matched against
the names of the nodes the changed files carry; a file's node is named for
the file. Liquid records `{% render 'price' %}`, `{% include %}`,
`{% section %}` and a JSON template's section `type` as references named
`snippets/price.liquid` / `sections/404.liquid`, and a failed one was parked
under the text after its last dot: `liquid`, the extension, which no node is
named. So a section or snippet added after the files naming it (or restored
after a delete) stayed unlinked until a referencing file changed or the
project was re-indexed, even in a repository with a single theme.

A `references` ref whose name is a path ending in a file name with an
extension is now parked under that file name (`price.liquid`), the key path
matching looks the file up by. The existing symbol-name retry then finds it
with no new query, since the changed files' node names already include each
file's own. Calls are left alone: their slashes sit in string arguments,
comments or divisions. Schema v13 rewrites the tails an older version
parked.

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

* test(liquid): compare the synced links with a fresh index of the same files

Indexing again over an existing index skips files whose content is
unchanged, so it kept the synced links of the files that name the new ones
instead of resolving them again.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:14:42 +00:00
Colby Mchenry 5ace306b7c fix(kernel): a Go value like wm.new or raft.None keeps its function ref (#2401)
* fix(kernel): a Go value like `wm.new` or `raft.None` keeps its function ref

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:08:27 +00:00
Colby Mchenry 4040eab975 fix(cpp): a base class links to the class C++ name lookup finds, not a namesake (#2407)
A C++ base class (`class DynamicMessage final : public Message`) was
resolved by its name alone, so a base named like a class of another
namespace, or of another language, bound to whichever namesake ranked
first. In protocolbuffers/protobuf, `DynamicMessage`, `MapEntry`,
`ZeroFieldsBase` and two arena_unittest classes derived from
`json_internal::ResolverPool::Message`; before #2397 they derived from the
PHP extension's C struct `Message`. The Go framework resolver claimed
those: protobuf ships Go examples, so the resolver is detected, its
`resolve()` runs for every language's refs (`languages` only gates
extraction), its PascalCase-struct rule found the C struct, and the
framework language gate lets C++ reach C. google/leveldb's and rocksdb's
iterators derived from an unrelated nested `Iterator` (`SkipList::Iterator`,
`WriteThread::WriteGroup::Iterator`), and leveldb's test comparators from a
`Comparator` struct that skiplist_test.cc declares for itself.

C and C++ inheritance refs now resolve in their own matcher
(cpp-supertypes.ts), ahead of every other strategy and with no
fallthrough, so no framework resolver, import strategy or name matcher sees
them, and only C and C++ declarations are candidates. The base is looked
up the way C++ looks it up:
- from the scope the deriving class is declared in, outwards, using the
  lookup cpp-type-aliases.ts (#2399) already has: a class scope with its
  bases, a qualified name a segment at a time, an alias followed to the
  class it names; `::Base` from the global scope only;
- then through what the file writes before the class: namespace aliases
  (`namespace _pbi = ::google::protobuf::internal;`), using-declarations
  and using-directives, and fmt's macro-opened namespaces;
- a class a .cc/.c file defines belongs to that translation unit;
- a template parameter, or a member of one, names no class: no edge.
When none of that finds the class, the only visible C/C++ class with that
qualified tail is taken, since the index loses some classes' namespaces
(`class PROTOBUF_EXPORT FieldDescriptor`, classes after a partial
specialization); several are a guess, so no edge.

The base is read as written, template arguments included, so an alias
template (`bool_constant<…>`) is found. An alias that leads to no project
class (one of `std::integral_constant`, one each `#if` arm declares
differently) is linked itself, as before.

Validated with site-keyed edge diffs on leveldb, fmt, protobuf and
rocksdb, and redis, SDWebImage and gson as controls (dumps
byte-identical): first against main 02d22aeb, then again on main
dd706469 after #2405, #2408 and #2400 landed, with identical site
diffs (#2402, merged after, changes only Go's implements synthesis).
Every removed edge was wrong: 9 protobuf
`suite_.ReportFailure` guesses that reached `ForkPipeRunner`, cpp-override
edges from the wrong bases, and two rocksdb bases that are template
parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:01:39 +00:00
Colby Mchenry fe9518357c fix(go): satisfying an interface counts the methods embedding brings in (#2402)
* fix(go): satisfying an interface counts the methods embedding brings in

goImplementsEdges (#584) built each interface's method set and each struct's
from their own `contains` edges only. Embedded interfaces are `extends` edges
since #2397, and embedded structs `extends` (or `implements` for an embedded
interface), so both sides now follow those declared edges, recursively and
around cycles: an interface asks for the methods of the interfaces it embeds,
and a struct has the methods promoted from the structs, interfaces and
defined types it embeds. Synthesized edges are never followed.

A struct that embeds the interface keeps its declared edge rather than gaining
a synthesized duplicate, and under the per-interface cap, structs that declare
every method themselves are linked before those that need promoted ones, so
the cap keeps the implementations interface-impl dispatch reaches. The
empty-interface skip and the cap are unchanged.

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

* test(go): the embedding test's supertype list reads declared edges only

Store in that fixture now satisfies other.Closer through the Close that
*storage.Base promotes, a go-implements edge synthesized from method names.
The assertion is about how declared embeddings resolve, as the rest of the
test already treats synthesized edges.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 02:39:46 +00:00
Colby Mchenry dd7064698c fix(react-router): read route tables another file hands the router (#2400)
A route table kept in a file of its own produced no routes: the ASP.NET
Core React template's AppRoutes array, which App.js maps into
<Route {...rest}>, a routes array imported into useRoutes(routes) or
createBrowserRouter(routes), and codedthemes' one-object-per-file
[MainRoutes, LoginRoutes]. Route objects were read only from a file that
itself names a data router, so these apps had no routes and their
navigate()/<Link to> navigation reached nothing.

The table's own file cannot tell it from a menu's { path, element } list,
so the evidence is the file that hands it over: a useRoutes /
create*Router argument, or a .map whose callback renders a <Route> from
the item's own fields ({...route}, {...rest}, path={route.path}). A new
FrameworkResolver.crossFileNodes hook keeps those routes, reconciled in
runPostExtract on every index and sync (removal-only syncs included):
new ones are inserted with pending references, ones no longer wanted are
removed with their incoming resolution edges resurrected, unchanged ones
keep their edges. The React pass starts from the files that import React
Router, follows the import to the table and the tables it names
(...ApiAuthorizationRoutes, children: x), each under its mount, and owns
its routes by id (route:<file>:<line>:table:<path>).

Route objects (tables and data routers alike): an index: true route is
the page at its parent's address; a route object around others is their
layout (a references edge marked layout: true, as Angular's), and is not
a page of its own where a child claims its address. A route renders past
a *Wrapper or *Route guard, and through a same-file
lazy(() => import(...)) to the module it loads, never a same-named
component in another app. useRoutes joins the file-level gate.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 02:27:44 +00:00
Colby Mchenry 787f75fbc9 fix(go): a type written bare links its own package's, not a namesake struct elsewhere (#2408)
* fix(go): a type written bare links its own package's, not a namesake struct elsewhere

The Go framework resolver's name heuristics (Patterns 1-4) picked a
same-named declaration by folder convention and fell back to the first
candidate anywhere, outranking name matching on a tie. Go reads a bare
name only from the reference's own package, so every cross-package pick
was wrong: promql/parser's `Node` parameters went to discovery/kubernetes's
struct, `apiv1.Node` to the same file's struct, and method calls through
an expression (`err[i].Error()`, a chained `.String()`) became
instantiations of structs named like the method.

- The patterns guess only for a name written bare, and only in its own
  package; Pattern 4 takes any type there, not only structs.
- isGoBareName is the one check for "Go reads this name from its own
  package"; a Go call through an expression never lands on a type.
- Go's if/switch/for/range join the bare-call keywords, so `if T(x)`
  conversions stay bare.
- goRefQualifier reads a variadic `...pkg.T` as written through `pkg`.

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

* docs(changelog): the Go type fix covers exported types; name the before-state precisely

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 02:27:30 +00:00
Colby Mchenry 9a515c3e64 fix(dead-code): a method a framework calls by decorator or by name is not dead (#2405)
The viewer's Dead code screen listed two kinds of framework-called methods.

A method whose decorator resolves outside the project (`@HostListener`,
`@Cron`, `@OnEvent`, `@Process`, `@BeforeInsert`, `@InitBinder`) has no
`decorates` edge and no `node.decorators`, only the resolver's unresolved
`decorates` row. That row now counts as a decorator, the same evidence the
opaque-ancestor rule reads for an `extends`/`implements` row, under the
existing `decorated` count.

A lifecycle hook on a class that skips the `implements` clause (`ngOnInit`
on a `@Component` without `implements OnInit`) had no evidence at all. A new
`hooks` rule drops a TypeScript method when its class carries a decorator and
an interface outside the index carries the name. What the interface carries is
read off the classes that write it out: every one has the method, declared or
inherited, at least two and more than half declare it, and more than half of
them carry a decorator. TypeScript only, because `implements` is erased there
and a framework can only be finding the hook by name.

The ancestor walk moves out of overrideCandidates into ancestorsOf, shared by
both rules. Declarations are read from the candidate list rather than searched
by name: a hook is never called from inside the index, and a name some
declaration of which is called is the ambiguous-name rule's anyway.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 02:27:27 +00:00
Colby Mchenry 02d22aebed fix(cpp): a receiver declared through a typedef or using alias calls the type it names (#2399)
google/leveldb's MemTable::Add calls `table_.Insert(buf)` on `Table table_;`,
declared next to `typedef SkipList<const char*, KeyComparator> Table;` in the
class. Receiver inference returned `Table`, no class `Table` has an `Insert`
(the one class `Table` is the unrelated leveldb::Table, and C++ type_alias
nodes record no aliased type), so the call fell through to the receiver-word
guess HandleTable::Insert. It now reaches SkipList::Insert.

cpp-type-aliases.ts reads the alias from source and looks it up the way C++
does: an alias declared earlier in the calling function, the caller's class
and its bases (read from the class's own base clause), then the namespaces
around it. A qualified name resolves a segment at a time (Table::Iterator is
SkipList::Iterator). Aliases of a template parameter, a dependent typename or
a decltype name no particular class. The constructor path uses the same
lookup, so `Table::Iterator iter(&table_);` constructs SkipList::Iterator and
an alias hides an outer class of its name.

When the aliased type lacks the method, a `.` call or a call through a
pointer on a receiver the caller itself declares gets no guessed edge. A `->`
through an iterator or smart pointer, a project class template (whose
specializations can add members) and declarations read from outside the
caller keep the previous behavior.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 01:02:35 +00:00
Colby Mchenry 9a274a4b47 fix(angular): a binding, interpolation or control-flow block links the member it calls (#2398)
The Angular template pass read own-member calls only in event bindings, so
a member a template calls from `[name]="icon()"`, `{{ label() }}`,
`*ngIf="isOpen()"` or `@if (loading())` had no incoming edge. With signals
that is the usual way a template reads state, and the dead-code derivation
listed such members as unused (CleanArchitecture's ThemeToggleComponent
icon and label).

Every render-time expression is now read: property bindings,
interpolations, structural-directive microsyntax, control-flow blocks and
@let. A call of one of the component's own members becomes an
`angular-binding` edge with the binding as `via` and no trigger. It runs at
render, and a trigger drew computed signals as user actions in Steps. A
read without a call counts only for a method-kind member: a getter is
called, a function handed to a child is a fnRef reference, and a field a
call filled is a plain reference. Property reads link nothing, as in
TypeScript. Pipes, object and microsyntax keys, members of other objects,
template locals, strings and comments link nothing either.

Validated on CleanArchitecture, angular-realworld, ngx-admin and Ghostfolio:
272 edges added, none removed, and every registeredAt line checked. The
proshop_mern and next-saas-starter dumps are byte-identical.

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:29:50 +00:00
Colby Mchenry f797b8cad2 fix(liquid): a Shopify theme's sections and snippets resolve only inside that theme (#2396)
A `{% render %}`, `{% section %}` or JSON template section `type` written in a
file inside a Shopify theme now resolves to that theme's own `sections/` or
`snippets/` file, or to nothing. It no longer reaches another theme's copy by
path tail (panoply/syncify: using-tailwind's templates linked straps/dusk's
sections), nor the root theme's copy over a folder theme's own.

The theme detector is shared with isShopifyLiquidJson (#2393):
shopifyThemeRoot in grammars.ts.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:26:15 +00:00
Colby Mchenry c513f1472a fix(frameworks): detection reads an app's package.json three or more levels down (#2395)
declaredDependencies read the root manifest and the ones one or two
directories down. An app whose package.json sits deeper was never read:
an ASP.NET solution's src/Web/ClientApp/, or prometheus's
web/ui/mantine-ui/ under a workspace root that declares only tooling.
Every resolver gated on dependsOn() then stayed off for it: Angular Router
and the Angular template pass, React Router's navigation, Vue Router,
SvelteKit and TanStack Router.

The existing pass is unchanged. When it leaves slots open, a second pass
probes the directories three or more levels down that hold indexed JS/TS
code, shallowest first and sorted within a level, at most 192 of them,
never inside node_modules.

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:27:58 +00:00
Colby Mchenry 0a670dc8cc fix(liquid): JSON under templates/ or sections/ is Liquid only inside a Shopify theme (#2393)
isShopifyLiquidJson matched every templates/**/*.json and sections/**/*.json
by path alone, so a .NET project template's template.json (jasontaylordev/
CleanArchitecture), schematics and CMS content were indexed as empty Liquid
files, and JSON listing `sections` could link to an unrelated Liquid file.

The rule now needs the directory holding templates/ or sections/ to also
hold layout/theme.liquid or config/settings_schema.json, looked up under the
project root. isSourceFile, detectLanguage and preloadLanguagesForFiles take
an optional rootDir, and every caller that decides what to index or how to
parse passes it: the scans, git change detection, scoped sync, the watcher,
indexAll, indexFileWithContent and CodeGraph.extractFromSource. Without a
root there is no theme to find. A sync removes JSON an older index stored
this way.

Shopify/dawn, expressjs/express and gofynd/Astra index byte-identically;
CleanArchitecture loses its 2 bogus Liquid files.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:18:41 +00:00
Colby Mchenry 3f5a9e2e1d fix(sync): an import links the file it names when that file appears later (#2392)
A sync retried parked failed refs by name_tail (#1240), but an import
names a file, a folder or a namespace, not a symbol, so the lookup never
found one: the tail of `package:app/b.dart` was `dart`, of `inc/db.php`
`php`, of `./req` `/req`. Most languages never parked theirs at all: an
import that named no file reached the importing file's own `import` node
through matchByQualifiedName (its qualified name is the module path) and
resolved there, so `./x` in TS/JS, `import pkg.mod`, `#include "a/b.h"`,
`<stdio.h>`, a C# `using` of an outside namespace, Go module paths and R,
Solidity, Svelte and Vue imports were consumed by an edge to themselves.
A Lua `require` bound the local it is assigned to. The import linked
only once the importer changed, or on a full index.

- resolveOne's target gate drops an `imports` ref's own import statement,
  so the ref parks as failed (#2388 did this for Dart alone).
- A Lua / Luau `require` resolves to a module file or nothing.
- An import written as a path is parked under its last segment's stem
  (`package:app/b.dart` -> `b`, `./z.js` -> `z`): src/db/reference-tail.ts.
- Sync retries the failed imports an ADDED file can satisfy: by its name,
  stem and folder (importPathKeys), as a failed import's tail or whole
  name, and by the changed files' namespace / module names as a whole
  name (C# `using Foo.Bar`, Java `import com.z.*`). Same per-key ceiling.
- Schema v12: two partial indexes over failed imports, which the planner
  needs to avoid reading every failed call that shares a tail; the
  migration rewrites the tails of path imports parked by an older version.

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

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:16:55 +00:00
Colby Mchenry 837a1866f1 fix(dart): a call through an import prefix reaches only what that import exports (#2389)
A Dart call written through an import prefix - `http.get(uri)` after
`import 'package:http/http.dart' as http;` - was resolved like a method call
on a receiver named `http`, so it linked to any project member that fit the
name: riverpod's docs' `http.get(...)` / `http.post(...)` calls went to a docs
example's `Http::get` / `Http::post` by the receiver's capitalized name, and
`fmt.describe(...)` to a class's `describe` method.

Now `p.name(...)` calls a top-level declaration a library imported with that
prefix exports (export chains, show / hide and parts followed through the
library model), or nothing when the prefixed library is a package outside the
repository. A constructor call (`p.Widget()`) instantiates the prefixed
library's class. The same holds where the prefix heads a chain -
`p.Box.create()`, `p.Report.empty()` - and for `p.Box<int>.named(...)`, which
arrives as `Box.named`: the type is the one the prefix brings in. A
parameter, local or member named like the prefix still hides it, and
`const p.Box.named()`, which the extractors record as `p.named`, links
nothing.

The library model's one prefix reader is `dartImportPrefixes`: Dart
annotations (#2390's `matchDartAnnotation`) read prefixes through it too, in
place of the `isDartImportPrefix` it had alongside.

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

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:27:41 +00:00
Colby Mchenry 631df0138e fix(dart): an import or export links the file its URI names, not a namesake (#2388)
A Dart `import` / `export` now resolves its URI through the library model
in src/resolution/dart-libraries.ts: `package:<name>/<path>` is
`<root>/lib/<path>` of the project package whose pubspec.yaml says
`name: <name>`, any other URI is a path from the importing file, and a
`dart:` library, a package from outside the project or a file the index
does not hold links nothing. The ref never reaches the name-matcher, which
took the URI's last segment for a file name (`package:flutter/widgets.dart`
reached riverpod_analyzer_utils' widgets.dart from 130 files, bloc_tools'
`package:args/command_runner.dart` its own command_runner.dart) or bound
it to the importing file's own `import` node.

When packages share a name and neither the importer's own package nor one
enclosing it settles which, the importing package's `path:` dependency of
that name (pubspec_overrides.yaml, then pubspec.yaml) does: bloc's two
example apps each keep an authentication_repository. Library visibility
reads the same answer.

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

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

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

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:29:29 +00:00
Colby Mchenry 0c860c00b3 fix(dart): a bare call or type name reaches only what its library can see (#2386)
A receiver-less Dart call, constructor call or type name now links only to
a top-level declaration its library can see: one in the library itself (the
file and its parts) or one exported by a library it imports without a
prefix, through export chains and show/hide. A type written through an
import prefix (`p.Report`) means what that import exports. It is a
candidate filter ahead of the ranking in matchByExactName and matchFuzzy,
so a visible namesake wins, and a library's own declaration shadows an
imported one. The library model (pubspec packages, package: and relative
URIs, part / part of, conditional imports) is src/resolution/dart-libraries.ts.

A bare call also never reaches another type's enum constant or static
constant, a generic `extension<T> on X` counts as unnamed, and an unnamed
extension applies in every part of its library.

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

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:40:12 +00:00
Colby Mchenry 5f5b8db620 fix(js,ts): find a store exported by an export { } list when its name starts or ends with $ (#2384)
TreeSitterExtractor.isExportedLater and its kernel mirror is_exported_later
bounded the name in the `export { ... }` branch with `\b`, which takes a `$`
for a separator. It never fires beside a leading or trailing `$`, so
`const items$ = create(...)` + `export { items$ };` (or `export { $items as
default };` for a `$items` store) never matched, and the store's actions
were not extracted on either engine. It also matched inside a longer name:
`export { useStore$ }` or `export { $useStore }` counted as exporting a
store named `useStore`.

The clause branch now bounds the name with a character that can't continue
an identifier. The regex crate has no look-around, so the bounds consume,
and both sides spell the class in ASCII, since the crate's `\w` is Unicode
while JS's is ASCII. Plain names decide as before apart from the two false
positives. A non-ASCII letter written against the name (`export {
éuseStore }`) used to split the engines (the kernel's Unicode `\b` said not
exported, the wasm path exported); both now give the wasm answer.

Adds kernel/wasm parity cases (ts/tsx/js/jsx) inside 'a store exported by a
later statement', and the same cases in store-exported-later.test.ts, for
`items$`, `$items as default` and both false-positive shapes. They fail with
the unfixed kernel and with the unfixed TS on the wasm path, and pass with
both fixes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:31:37 +00:00
Colby Mchenry d500e58a52 fix(dart): link a call chained on a generic lookup like BlocProvider.of<T>(…) (#750) (#2383)
When a generic call parses as a call (a named argument after a positional
one, as in `(context, listen: false)`, or a type argument like `<A?>`,
`<A, B>` or `<List<A>>`), the Dart extractor records a chain on it as
`BlocProvider.of().increment`. The resolver typed that receiver by what
`BlocProvider.of` is declared to return: `T` for `static T of<T>(…)`,
which names no type, and nothing at all when the lookup comes from a
package, as flutter_bloc and provider do in an app. So
`BlocProvider.of<CounterCubit>(context, listen: false).increment()` and
provider's `Provider.of<Cart>(context, listen: false).add(item)` linked
nothing.

The declared return type is still tried first, so `Foo.create().bar()`
with a concrete return type resolves as before. When it gives no edge,
the chain is read at the call site the way #2376 reads a later link of a
chain: the lookup's own type parameter is the type argument the call
gives, and an outside `of<T>(…)` lookup hands back its T. The edge stays
on the encoded ref, as in Go's fallback (the gin runaway).

The extractors are unchanged. A chain on a call parsed as two
comparisons keeps its bare name (#2375), and now reaches the same
method; the comments that gave the old reason are corrected.

Validated before/after with the native kernel at 3f96f80f, nothing
removed anywhere: felangel/bloc @b9be1e2 0 (its four such sites parse as
comparisons and already linked), rrousselGit/riverpod @4ba1be2 +6
(`X.family<…>(…).call(42)` to `ClassFamily::call`), flutter/samples
@63411527 +2 and brianegan/flutter_architecture_samples @d898d13 +6,
all `Provider.of<T>(context, listen: false).m()`. A bare-name fallback
(as Go does) added nothing on those repos, since the lookups are outside
the project, and linked 2 of the test's 8 chains wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:29:47 +00:00
Colby Mchenry 1b752de2c0 fix(dart): a member with no body keeps its dartdoc and annotations (#2382)
A Dart member written without a body - a constructor like `Foo._();` or
`Point.origin() : x = 0;`, a `const` constructor or redirecting factory
such as flutter_bloc's `const BlocProvider.value(...)`, an abstract or
`external` method - parses as a `declaration` wrapping its signature,
and its `///` dartdoc and `@annotation`s are siblings of that wrapper.
Both extractors looked for them before the signature, inside the
wrapper, and found nothing: the docstring was lost, and
`@visibleForTesting Foo._();` emitted no `decorates` reference.

Extraction (TS + kernel, at parity):
- A Dart-only `getDeclarationWrapper` hook returns the `declaration`
  when the signature is its first named child. `docstringFor` and
  extractDecoratorsFor's preceding-sibling scan start from it; the
  kernel's `declaration_wrapper` does the same for every docstring
  lookup and for the annotation scan.
- The shared DOCSTRING_WRAPPER_TYPES / `is_wrapper` list is untouched:
  C/C++ share it, and their `declaration` wraps declarators. All 35
  non-Dart parity fixtures extract byte-identically before and after.

Validated on felangel/bloc @b9be1e2 and rrousselGit/riverpod @4ba1be2
at main 3f96f80f with the kernel loaded: bloc +32 docstrings, riverpod
+126, none changed or removed, and 0 edges added or removed. The new
`decorates` refs (+10 / +186) all name SDK or package:meta annotations
and stay unresolved. Kernel parity sweeps show 0 diffs, and full-index
kernel and wasm dumps are byte-identical on both repos.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:25:55 +00:00
Colby Mchenry 3f96f80fcb fix(dart): link generic calls the parser reads as two comparisons (#2375)
tree-sitter-dart parses a generic call with arguments as two comparisons
about as often as it parses it as a call, depending on the code around it:
`ref.read<Repo>(p)` comes out as
`relational_expression(relational_expression(ref.read < Repo) > (p))`.
Generic constructors (`Provider<int>((ref) => 0)`), named arguments (which
then parse as a `record_literal`) and calls after `await` or a prefix
operator are hit too. Neither extractor recorded a call for that shape,
only a member read of `ref.read` that linked nothing, so providers,
repository reads and bloc lookups written with a type argument had no
callers. Across felangel/bloc, rrousselGit/riverpod and flutter/samples
there are 2,853 such calls, against 2,836 generic calls with arguments
that parse as calls.

- Both extractors (the TS walker and the native kernel) recover the call
  at its `<` when the type argument names a type and the code is laid out
  as a call: `<` against the callee and `(` against the `>`. A comparison
  such as `a < b` is left alone. The recovered call gets what a parsed one
  does: the call at the `<`, a reference to the type argument, and no
  member read of the callee or of a prefixed type argument.
- A call chained on a recovered call keeps its bare name. The
  `BlocProvider.of().increment` encoding a parsed chain gets resolves
  through what `of` returns, which for a generic factory is its type
  parameter, so it would drop the edge the bare name finds.
- When the grammar ends an arrow function in front of a generic call
  (`(ref) => ref.watch<int>(p)` as `((ref) => ref).watch<int>(p)`), the
  receiver is read from the end of the arrow's body, so the call is
  `ref.watch` rather than a bare `watch`.

Validated with the kernel loaded: bloc +85, riverpod +1,580 and
flutter/samples +54 edges, none lost; kernel/wasm parity has 0 diffs on
all three repos.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:57:23 +00:00
Colby Mchenry 652c36b72a fix(dart): index const constructors and redirecting factories; a call through a type stays on that type (#2380)
* fix(dart): index const constructors and redirecting factories; a call through a type stays on that type

Both Dart extractors (the TS walker and the native kernel) skipped
`const` constructors and redirecting factories (`const Foo.bar(...)`,
`const factory Foo.bar() = _Bar;`), which parse as signature kinds of
their own. A call such as flutter_bloc's `BlocProvider.value(...)` then
found no method on BlocProvider, and the resolver guessed another
type's `value`: on felangel/bloc, 89 calls linked to
`RepositoryProvider::value`.

Extraction (TS + kernel, at parity):
- Named const constructors and redirecting factories are methods named
  by the constructor, returning their class, with a parameters-only
  signature; the unnamed ones stay unindexed. The name is read only
  from before the parameter list, so a redirect target
  (`= _Impl.named`) never names it.
- A redirecting factory references the class it redirects to, not the
  target constructor's name or an import prefix.
- A constructor called with type arguments keeps its type:
  `BlocProvider<CounterCubit>.value(...)` is `BlocProvider.value`, as a
  statement and as an expression (`constructor_invocation`, which
  produced no call at all before).

Resolver:
- A Dart call through a type's name reaches only that type's own
  constructor, static member or static constant, or nothing: no guess
  by name, no inherited lookup, no same-file look-alike, no instance
  member of the same name. SDK calls like `Uri.parse` no longer land
  on a project `parse` past an `extension on Uri`.
- A named constructor is never reached by a bare name, the last link
  of a chain, a function value or an annotation (riverpod's 210
  `@internal` annotations).

Validated on felangel/bloc @b9be1e2 and rrousselGit/riverpod @4ba1be2
with the kernel loaded: bloc 18,338 -> 18,495 edges, riverpod 85,606
-> 85,854, every removed edge triaged. Afterwards every edge into a
named constructor is written `Owner.ctor`.

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

* fix(dart): read a type's static members one way for calls and chains

- A call through a type's name now finds its member with dartStaticMember,
  the lookup a chain's static link uses since #2376, so both pick the same
  member: the call site's own file first, then the nearest copy.
- isDartStaticMember's constructor test is isDartConstructor, anchored at
  the declaration: an instance getter declared just above a same-named
  factory (`Object? get error` over `const factory AsyncValue.error(...)`)
  read the factory from the next lines and counted as static.
- clearNameMatcherMemos drops DART_CONSTRUCTORS with the other
  source-derived Dart memos.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:48:06 +00:00
Colby Mchenry 4d6168a701 fix(dart): a bare call to a parameter or local links nothing (#2379)
A receiver-less Dart call, or a function passed by name, whose name a
parameter, local, loop or catch variable, or pattern variable around it
binds now links nothing, instead of a same-named top-level symbol in
another file or a member the class inherits; a local function declared
there still links.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:45:17 +00:00
Colby Mchenry 0571286907 fix(js,ts): link calls through an import whose name contains $ (#2378)
The ES import regex in extractJSImports captured default and namespace
bindings with \w+, which stops at a `$`. So `import items$ from './store'`,
`import $ from './dom'` and `import * as ns$ from './ns'` failed the whole
statement and produced no import mapping, and `items$.getState().inc()`
never reached the store action. The `as` split of named imports, the
`a: b` split of a CommonJS destructuring and the re-export checks had the
same \w-only shape. Some bound a cut-off name instead (`{ a as b$ }` bound
`b`, `{ default as items$ }` bound `items`), and `export { a$ } from` was
dropped from barrel chasing.

Capture every binding with the ASCII identifier class [A-Za-z_$][\w$]*,
keeping the `type`-modifier lookahead: `import type from './x'` still
binds `type`, and `import type $T` / `import type from$` now bind. A
default binding is never followed by `{`, so the `${` of a code
generator's template literal (`import ${x} from '${src}'`, as in qwik's
optimizer) is not taken for a binding.

importShadowedAt and hasParameterBinding bounded the name with \b, which
never fires beside a `$`, so a local `const items$ = ...` or an `items$`
parameter did not shadow the imported store. Named `$` imports already
got those wrong edges on main, and the mapping fix would have extended
them to default and namespace imports. Bound the name with (?<![\w$]) and
(?![\w$]) instead.

Resolution-only: the kernel has no import-mapping mirror. A store
exported by `export { items$ }` (the \b in isExportedLater's clause
branch, left out of #2366) is a separate extraction fix.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:41:06 +00:00
Colby Mchenry 5a11cd7ab7 fix(vbnet): link fields and properties read or written through a value (#2377)
VB.NET linked a member read only through a type or module name (#2355),
so a read or write through an instance -- a parameter `x.Normal = 3`, a
local `h.Normal`, a field `_h.Title`, `Me._h.Normal` -- produced no edge,
and `codegraph callers` on an instance field or property listed almost
none of its uses.

- Extraction (TS only; VB.NET is not kernel-routed): every member read or
  write is sent with its receiver as a path, whatever the receiver's case:
  `x.Normal`, `Me._h.Normal`, `MyBase.Count`, `.Value` in a `With` block
  (nested `With .Inner` included), `.Switch` in an object initializer
  `New BoolParam With {...}`, and `{T}.X` for a value whose type the code
  writes (`DirectCast(o, T).X`, `New T().X`). A `With` block's `.Run()`
  call, which recorded nothing, is sent the same way. The grammar splits
  `New System.Drawing.Size(...)` into members read through `New System`;
  those, anonymous types' `New With {.X = 1}`, and paths that start with
  System, Microsoft or My are not sent.
- Resolution (vbnet-receivers.ts): each link of the path is typed by the
  rules calls use since #2351 -- declarations, casts, `For Each` over a
  typed collection, call results, type-parameter constraints, Module
  variables, namespaces and Imports -- and the read links the member the
  type declares or inherits. A method named without parentheses is called
  (unless AddressOf or NameOf only names it), and `x.Items(0)`, an index
  that VB.NET writes as a call, reads `Items`. An outside, `Object`-typed,
  ambiguous or untyped receiver links nothing; nothing is guessed by name.
- "Color Color": a value named like its type (`theme As Theme`) now reads
  an instance member through the value, so only the member is linked; a
  Shared member, a Const (read from its declaration) or an Enum case still
  links the type as well, as #2355 did for every such read.
- membersNamed looks a type's members up in its parts' files rather than
  with one query per member name. The graph is identical on SCrawler; on
  staxrip, whose AutoCrop tool redeclares `ServerInfo`, 35 reads that
  main typed through the other project's field, and so left unlinked,
  now link (70 edges).

Verification: vbnet-instance-member-refs.test.ts (11 tests) fails 9/11 on
main and passes; one vbnet-shared-member-refs assertion now expects
`OtherSession::SessionId` from `Shadowed(appSession As OtherSession)`;
the VB.NET suites, tsc and the full suite pass (462 files, 5,859 tests;
22 kernel/platform files skipped). The issue's repro on the built CLI
links every read and write, and `codegraph callers` lists `Run` for
`SharedCache`, `Normal` and `Title`.

Validation, main vs this branch: nodes unchanged, self-loops unchanged.
SCrawler 17,437 -> 31,344 edges, 1 removed (same edge, new metadata).
staxrip 32,569 -> 71,362 edges, 585 removed: 318 same edge with new
metadata (Color Color reads of instance members, qualified-name ->
instance-method) and 267 class links of those reads, every one beside an
instance member. About 42k added two-link reads were re-derived from the
source text by a separate check; its 837 disagreements were all its own
blind spots (constraints, Imports aliases, sibling-block locals, call
results, staxrip's global `p`). No added edge crosses between staxrip
and its AutoCrop tool. Cost (5-run medians): CPU 16.1 -> 19.3 s and
20.2 -> 25.4 s, DB 28.1 -> 43.2 MB and 34.9 -> 59.0 MB; the DB growth is
the new edges plus reads through outside types kept as failed refs for
re-resolution (#1240).

Not covered: unqualified reads, reads through an index or a call result
(`list(0).X`), Handles clauses, and calls through a chained receiver
written directly (`g.MainForm.Update()`), which still resolve by name.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:39:26 +00:00
Colby Mchenry b7e05f3ccd fix(dart): link a chained call only through its receiver's declared types (#750) (#2376)
The Dart extractor keeps one receiver level, so the later links of a call
chain reach the resolver by their bare name (`Provider.autoDispose.family(…)`
→ `family`, `events.map(mapper).transform(…)` → `transform`). They then fell
to name matching and bound to whichever project method shared the name: on
felangel/bloc a Stream's `transform` went to angular_bloc's
`BlocPipe::transform`, and on rrousselGit/riverpod every
`X.autoDispose.family(…)` went to one builder's `family` whatever X was.

A chain link now reaches only the member of what the chain before it
evaluates to, typed from evidence: the head (a type, a constructor call, a
string, `this` / `super`, an import prefix, a local, parameter, field or
getter with a declared type) and each link's declared type (a method's or
getter's return type with the method's own type parameters substituted, a
field's or abstract getter's type, a static constant's initializer). A lookup
nothing declared answers for hands back its one type argument
(`context.read<LoginCubit>()`). Without evidence it links nothing.

Along the way: an annotated extension (`@internal extension X on Y`) is
recognized as one, so its members reach member reads and chains; `super.m()`
reaches the superclass's method instead of the caller itself; an extension
on a type parameter of its own applies to the project's types; a spread's
`...foo()` is a receiver-less call.

Validated before/after with the native kernel: bloc 337 name-only chain
edges on main → 123 kept (now typed), 106 retargeted, 106 removed (68 on a
typed receiver lacking the member), +75 `super.` calls; riverpod 1,254 →
171 kept, 383 retargeted, 696 removed (137 provably wrong), +263. Kernel and
wasm graphs are identical with the fix.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:36:56 +00:00
Colby Mchenry 6421644ef2 fix(dart): link calls in top-level and field initializers (#2367)
Both Dart extractors (the TS walker and the native kernel) skipped the
code in initializers. A `final`/`const` declaration minted its constant
and stopped, and a field's or `var`'s initializer only had its types
recorded (#2360). In Riverpod apps, where most wiring lives in provider
closures (`final repoProvider = Provider((ref) =>
Repository(ref.watch(dioProvider)));`), what a provider builds with had
no callers, and impact and explore flows stopped at the provider.

- A `final`/`const` initializer is walked like a body, for its constant:
  calls, instantiations, static and member reads, types, closures with
  block bodies and local functions, function values.
- A field's (instance, `static var`, typed, `late`) or a top-level
  `var`/`late final` initializer is walked for its class or the file,
  since neither mints a node. A local declaration's second variable
  (`for (var i = 0, j = n(); ...)`) stays its function's.
- Each initializer is walked once: a new `walkInitializer` context method
  marks it, and the dispatcher's function-as-value scan skips it. Other
  languages' hooks are unchanged.
- Resolver: a Dart call through a type's static constant
  (`FutureProvider.autoDispose(...)` on `static const autoDispose =
  ...Builder();`) links that constant, not another type's same-named
  method.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:55:25 +00:00
Colby Mchenry 5b73d0fd46 fix(js,ts): find a store exported on a later line when its name contains $ (#2366)
TreeSitterExtractor.isExportedLater put the binding name into its regex
unescaped. A `$` in the name (`items$`, `$store`, `a$b`) became an
end-of-line anchor, so `const items$ = create(...)` + `export default
items$;` never matched and the store's actions (e.g. `function inc`) were
not extracted, while the kernel mirror escapes the name with regex::escape
and extracts them. The same anchor also matched where it should not: a
store named `items$` read as exported when a multi-line `export { ... }`
ended a line with `items`.

The kernel routes TS/TSX/JS/JSX by default, so the wasm path is what files
with parse errors (deferred to wasm), Svelte/Vue/Astro script blocks and
runs without the kernel get. Escape the name the way regex::escape does;
the guard limits names to [A-Za-z0-9_$], so only `$` changes. `\b` stays
as is on both sides: next to a leading or trailing `$` it never matches
(`export { items$ }`), the same in both engines.

Adds a kernel/wasm parity case (ts/tsx/js/jsx) for `items$`, `$store` and
`a$b` plus the false-positive shape; all 16 fail with the name unescaped
and pass with the fix.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:54:08 +00:00
Colby Mchenry fe7542b6f6 fix(vbnet): index names that begin with a keyword (SharedCache, Dimension, Sub NewItem) (#2365)
The vendored grammar lexed the member modifiers as one prec-10 token and
`Sub New` as one prec-100 token. Neither went through keyword extraction,
and both outranked the prec(-1) identifier, so the lexer stopped at the end
of a modifier spelled at the start of a name: `Public SharedCache` indexed a
field `Cache`, `Public Dimension` a field `ension`, `Public Shared Shared1`
was lost to a parse error, and `Sub NewItem()` became a constructor. `Dim`
and `Const` were part of that token, which also kept the local-declaration
`Dim`/`Const` out of keyword extraction, so `ConstVBV.Value = False` and
`ConstructPath()` inside a method parsed as `Const` declarations.

The modifiers and `Sub New` are now plain keywords. The old tokens also
folded a newline into the keyword after it, which two parses relied on by
accident: `Option` lines under a comment banner may now follow newlines, and
upstream's top-level `file_attribute_section` is gone, so attribute lines
above a top-level declaration belong to it. `Protected Friend` and
`Private Protected` are two modifiers now; the extractor reads them as
protected and private.

Rebuilt with tree-sitter-cli 0.25.10 and emscripten 4.0.4. On SCrawler and
staxrip, 3 fields get their whole names, staxrip recovers one method, and
every other graph difference is an `imports` edge or a node start now on its
own line instead of the comment line above.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:51:51 +00:00
Colby Mchenry aeb8f95581 fix(kernel): find a store exported on a later line in CRLF TS/JS files (#2364)
The kernel's is_exported_later mirrors TreeSitterExtractor.isExportedLater's
multiline regex, but JS's `m` flag treats `\r` as a line end while the regex
crate's `(?m)` sees only `\n`. On a CRLF checkout (Windows, autocrlf) a line
like `export default useStore;\r\n` matched in wasm but not in the kernel, so
`const useStore = create(...)` + `export default useStore;` lost its actions
(e.g. `function inc`) on the default kernel route for TS/TSX/JS/JSX.

Compile it in CRLF mode, `(?mR)`: `^`/`$` then treat `\r` as a line end too,
and the pattern body stays byte-identical to the TS regex. It agrees with the
JS regex on CRLF, trailing whitespace and lone `\r` line ends, where an
optional `\r?$` would still miss a lone `\r`.

This was the only `(?m)` regex in the kernel. The other tsjs regexes are
whole-string anchors, and the minified-bundle check splits lines on `\n`
exactly as the TS does.

Adds a kernel/wasm parity case (ts/tsx/js/jsx, LF and CRLF) that fails on
the unfixed kernel for all four CRLF variants and passes with the fix.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 04:06:24 +00:00
Colby Mchenry f40db4b9cc fix(js,ts): index the functions in every named object literal, not only exported ones (#2300) (#2363)
Issue #2300: a function written inside an object literal became a symbol
only when the object was an `export const`. A plain `const api = {...}`,
an object declared inside an IIFE or a function, and a namespace hung on
the page (`window.WS = {...}`, `ns.mod = {...}`) produced no member nodes,
and no node for `WS` either. Calls made inside those members were credited
to the enclosing constant or lost, and calls into them resolved to
nothing. Script-tag JavaScript, written almost entirely this way, was
mostly missing from callers and impact.

Cause: the TS/JS extractor, and the kernel's tsjs mirror of it, minted
members only for exported object-of-functions; any other literal was
walked as one opaque initializer, and an assignment to a member path was
never a declaration. Resolution had no way to reach a member through
`App.init()`, `window.App.init()` or `App.utils.pad()`.

Fix (extraction, TS and kernel byte-identical): a named object literal
owns its function members (method shorthand, `key: function`, `key: () =>`,
generators; static keys only), whether it is declared at module scope,
in a function body or IIFE, assigned to a path (`window.App = {...}`,
`App.utils = {...}`) or assigned to a plain name at module level
(`dw_page = {...}`). Members are `function` nodes qualified under the owner
(`api::load`, `window.App::init`, `App.utils::pad`), the exported case
included; a path owner is named by its last link and qualified by the
path. Calls in a member are the member's; other values are walked where
they were before. CommonJS exports, prototypes, `this.x = {...}`,
call-argument literals, a name reassigned inside a function, and
generated or minified files keep the old shape. Minified files are now
also recognised by content, in both extractors, and the kernel's
`.min.js` pattern matches the TS one.

Fix (resolution): a member is reached through its object only:
`App.init()`, `window.App.init()`, `App.utils.pad()`, a sibling's
`this.render()`, a `const { init } = App` binding; never a bare `init()`.
A same-file holder is chosen by lexical block (an IIFE's own `App` first);
otherwise a global one (`window.App = {...}`, or a classic script's
top-level `App`) when the caller neither imports nor binds the name.
Arrow members keep the `this` of the method around the literal, in the
`this.x` resolvers too. A dotted-path holder is never reached by its last
name alone, and Svelte's `$store` rule now applies only in `.svelte`
components, so the new local holders are not taken for `$n` in plain
scripts. The lookups read each file once (destructuring patterns only
when the raw text has a `} =`) and keep only the last 32 files' scans.

Contributor PR #2310 (@danusha2345): adopted its model (owner-qualified
members exported or not, path owners, IIFE/local owners, static keys,
no bare-name reach, `<script setup>` contains edges, SFC languages, test
scenarios), re-implemented on current main. Not taken: the
EXTRACTION_VERSION bump (already 28), the extraction-time binding oracle
with its ref/edge metadata and kernel ref patching (replaced by
resolution-time lookups), naming the owner `window.WS`, rewriting
`window.X.m()` ref names, flipping exported members' isExported, and
unrelated bare-call changes.

Verification: new js-object-literal-members suite (native + wasm, 18
tests); the issue's shapes fail on main and the guard test fails without
the guards. kernel-tsjs-parity passes with CODEGRAPH_KERNEL_EXPECT=1
(object-literal owners LF/CRLF, minified bundles); kernel parity sweeps
show 0 diffs on DokuWiki, vue-realworld, TodoMVC, excalidraw and this repo.

Validation (kernel loaded, before = origin/main 023fc311):
- DokuWiki: nodes +85, calls +105; 17 removed edges = 13 moved to the
  member, 4 re-resolved from a wrong LinkWizard::init to the right init.
- TodoMVC: nodes +427, calls +148; 306 removed = 226 moved, 33
  re-resolved (14 fixed `this.render()`/`this.save()`/`this.track()`, 19
  wrong-to-wrong fallback guesses), 47 dropped wrong edges (43 to a
  helper local to jQuery Mobile's scrollstart setup).
- excalidraw: nodes +86, calls +75; 34 removed = 33 moved, 1 fuzzy 0.3
  guess dropped.
- vue-realworld: nodes +10, calls +11; 7 calls re-resolved from the
  ApiService constant to its members.
New self-loops are real recursion, bar one lazily redefined method that
calls itself as written. 81 sampled added edges, 80 correct
(the other is main's own window.open guess, re-attributed). Whole-run
index time, median of 3 interleaved: vue-realworld 1.2s -> 1.2s,
DokuWiki 8.6s -> 9.1s, excalidraw 10.8s -> 10.7s, TodoMVC 16.9s -> 16.9s
(within run-to-run noise).

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:45:43 +00:00
Colby Mchenry 023fc3111b fix(axum,actix): link a route to a handler written below its call (#2326) (#2359)
With rustfmt's wrapped form

    .route(
        "/multi",
        get(handlers::multi_line_handler),
    )

the route was never linked to `multi_line_handler`, even when it was the
only function of that name; the single-line form worked. Rustfmt wraps
every `.route(` past the line width, so most routes of a real router were
affected (31 of 364 handler references resolved in the reporter's router).

Cause: the Axum block of the Rust framework extractor recorded the handler
reference at the line of `.route(`, column 0. Every resolved reference then
passes the Rust scope gate (isRustNameInScope), which reads that one line
to see how the name is written (`handlers::f` names the `handlers` module).
On a line holding only `.route(` it found nothing and dropped the edge. The
same position also broke single-line routes whose handler name appears
elsewhere on the line: in `.route("/login", post(handlers::user::login))`
or `delete(handlers::user::delete)` the gate read the path string or the
method router as a bare use of the name. Actix's `web::resource(..)` chains
and App-level `.route("/p", web::get().to(h))` had the same weakness.

Fix (src/resolution/frameworks/rust.ts):
- The handler reference carries the line and column of the handler's own
  name (its last path segment), for Axum and the three Actix forms; the
  route node stays on the `.route(` / `web::resource(` line. The gate is
  unchanged and still reads one line, the one the handler is written on.
- Axum's method routers are the top-level chain of `.route(`'s second
  argument: a `get(` nested inside it is a call in a closure handler's body
  (`post(|h| async move { h.get(CONTENT_LENGTH) })`), not a route. With the
  reference moved, such a call would otherwise gain an edge.
- An Actix resource's method chain also ends at the `)` of the call it is
  an argument of, so in `.service(web::resource("/").to(index))
  .route("/count", web::get().to(get_count))` the resource keeps
  `.to(index)` instead of taking `/count`'s handler.

Adopted from #2329 by @danusha2345 (the fix and the tests are theirs),
without its EXTRACTION_VERSION bump (already 28 on main); added a case for
a single-line route whose handler name the line also writes elsewhere.

Verification: __tests__/rust-route-handler-line.test.ts: 10 of 13 fail on
main in both wasm and kernel modes; 13/13 pass with the fix in both modes.
The issue's repro now prints `GET /multi [references] src/main.rs:7`.

Validation (main -> branch, route nodes and route->handler edges keyed by
route name/file/line and target QN/file):
- rust-lang/crates.io: 1 -> 5 linked (+4 wrapped routes).
- atuinsh/atuin server: 7 -> 12; +5 newly linked and 2 wrong targets
  replaced (`GET /api/v0/me` user::get -> v0::me::get, `GET
  /api/v0/record/next` a TUI `Events::next` method -> v0::record::next).
- launchbadge/realworld-axum-sqlx: 18 -> 19 (+1 wrapped chain link); 11
  same-file edges now carry the handler's line.
- tokio-rs/axum (whole repo): 204 -> 204, 19 edges carry the handler's
  line; 2 bogus `GET /` route nodes from `headers.get(..)` in closure
  handlers in routing tests are gone.
- actix/examples: 116 -> 116; `ANY / -> index` x2 replaces two wrong `GET /`
  edges (websockets/chat took the next routes' handlers; basics/hello-world
  now records its real `.service(web::resource("/").to(index))`).
No other edge changed in any of the five repos.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:00:29 +00:00
Colby Mchenry ed1a89fb0f fix(rust): a variant path references the enum that declares it (#2328) (#2354)
Using an enum only through its variants (`mode::Mode::A` in an expression,
`mode::Mode::A => 1` / `Mode::B => ...` in a match, a `Mode::C(x)` or
`Mode::D { .. }` pattern) recorded no reference to the enum, so code that
builds or matches variants was missing from the enum's callers and impact.
Only a type annotation (`fn takes(_m: Mode)`) produced one.

Cause: Rust was not in the static-member pass, which turns `Enum.value` /
`Type::CONST` reads into references for Java, C#, Kotlin, Swift, Scala,
Dart, PHP and C++, and a path to a variant that is not a call or a struct
literal reaches no other extractor.

Fix:
- Extraction (wasm extractor and native kernel, mirrored): a path whose
  last segment is capitalized emits a `references` ref named by its
  receiver (the segment before it; `Self` read as the impl's type, nothing
  in a trait), positioned at the receiver, so a `mode::` prefix scopes it
  exactly as it scopes a type annotation. Covers value reads, match arms,
  `|` / `if let` patterns, tuple- and struct-variant patterns and a variant
  passed as a value. A call's callee (`Mode::C(1)`, `Foo::new()`), a struct
  literal, a longer path's prefix, a `use` tree and a lowercase receiver or
  member emit nothing.
- Resolution (isRustNameInScope): a reference written `Name::Variant` or
  `Self::Variant` lands only on an enum that declares that variant, after
  the usual Rust scoping (module path, `use`, glob, prelude, std and
  dependency imports). ripgrep's `Match::None` reaches `ignore`'s enum, not
  the matcher's `struct Match`; std variants (`Ordering::Less`,
  `Option::Some`) never reach a same-named project enum; associated consts
  and functions (`Limits::MAX`, `Mode::ALL`, `Foo::new()`) reference no
  type. `Self::X` is read as the impl's type, never through a path written
  elsewhere on its line.

Adapted from #2330 by @danusha2345: the extraction half (both walkers) and
most of the tests are theirs. Changed here:
- The resolver accepts a declared variant only. #2330 also accepted any type
  whose file holds a variable or const of the item's name; impl consts are
  file-level variables in the graph, so that cannot tell a type's associated
  const from another type's or a top-level one.
- The walkers check the node kind before looking at its parent, and the
  body walk hands the parent down: #2330 called `parent()`, which walks down
  from the tree's root, for every node of every body, which nearly doubled
  the kernel's Rust extraction time on tokio (2.1 s -> 3.9 s); now within
  noise of main.
- EXTRACTION_VERSION is not bumped again (already 28 on main).

Verification: __tests__/rust-enum-variant-reference.test.ts (10 tests):
6 fail on main in wasm mode, in kernel mode, and with this TypeScript over
main's kernel binary; 10/10 pass in both modes. The kernel parity case
covers every shape and asserts the refs exist; the kernel-parity sweep over
ripgrep, alacritty and tokio is 1004/1006 files byte-parity, 0 diffs (2
parse-error files defer to wasm by policy).

Validation (native kernel loaded, main -> branch, edges keyed by
QN/kind/file/line): ripgrep +822/-0, alacritty +917/-0, tokio +665/-0, all
`references -> enum`, node counts unchanged. Every added edge's source
reads `Name::Variant` / `Self::Variant` at its column and its target enum
declares that variant; no std enum targets. New symbol->enum pairs:
ripgrep +220, alacritty +253, tokio +254; enums with no referencer before
and one now: 31 / 13 / 31. One wrong target on tokio
(`oneshot::error::TryRecvError::Empty` -> mpsc's `TryRecvError`) comes
from the existing path rule that checks only the last module segment, which
type annotations share. Index time on tokio stays within noise of main
(interleaved runs; resolveOne time 27.7 s vs 31.2 s summed over workers).

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:59:11 +00:00
Colby Mchenry 7210d2fd4c perf(resolution): remove the JS indexing regressions since 1.6.2; the summary time covers the whole run (#2334) (#2362)
Indexing pretix took 2.2x as long since #2163 (issue #2334), and the
bundled minified d3 was what #2344 left of CPython's 1.6.2 slowdown. Two
JS/TS checks that first shipped in 1.6.2 redid whole-file work for every
reference:

- matchDestructuredCallResult (#2163) ran for every bare call in a file
  holding `const {` / `let {` / `var {`: per call it re-tested that
  pattern on the source, then stripped, blanked and brace-walked every
  line above the call and matched the binding regex over it.
- jsFunctionLocalScope (#2226) re-stripped the calling function's lines
  above each reference. In a minified script every function's text runs
  to the end of its one line, so each strip was the whole file.

CPU profiles, main -> this change: matchDestructuredCallResult 82.4 s
-> 1.7 s and jsFunctionLocalScope 12.3 s -> 5.2 s on pretix; 35.4 s ->
0.37 s and 23.3 s -> 1.0 s on CPython's Lib/profiling + Doc/_static.

Now:
- A file's `const { … } = f(…)` bindings are read once per resolution:
  the blanked code, each binding's end, callee and keys, the block it
  sits in and where that block closes, and the spans the blanker read as
  regex literals. A call looks up its own name; only a call through a
  destructured name scans the code between that binding and itself for
  shadowing, and only once the callee is found to return the key (a
  `require(…)` destructuring, for one, needs no scan).
- Blanking the text above a call on its own gives the file's blanked code
  up to the call: the stripper and blanker look past a character only for
  a comment opener's second character and for a regex literal's closing
  `/`. So the prefix's bindings are the file's that end at or before the
  call (a match never reads past its `(`, and one running past the call
  holds no `{`…`(` to start another), and its brace stack follows from
  where each block closes. For a call inside a span the file reads as a
  regex literal (a division, in minified code) the prefix is the code up
  to the `/` plus the short tail blanked on its own; where a binding
  statement could run across that `/` (inside its `{ … }` or `< … >`), and
  for out-of-range positions, the old per-call scan still runs.
- jsFunctionLocalScope strips a function's lines once and cuts at the
  reference's line end; functions that start on the same line share the
  regex answer by (file, first line, reference line, name).
- The new memos, and JS_FN_LOCAL_MEMO (never cleared before), drop with
  the other per-file memos in clearNameMatcherMemos.

The summary line printed orchestrator.indexAll's durationMs, which covers
extraction only, beside node and edge totals recomputed after resolution:
the issue's 19.9 s run printed "in 1.7s". CodeGraph.indexAll and sync now
report the whole run's wall time, which `codegraph init`, `index` and
`sync`, the MCP auto-sync log and the telemetry duration bucket read.

Verification: nodes, edges and unresolved_refs dumps keyed by
file|kind|qualified_name|start_line are byte-identical to origin/main
8998697f (with #2357's new destructuring and Vue template refs) on all
24 runs: pretix, mealie and CPython's Lib/profiling + Doc/_static, 8
each; and to deac7710 (#2358, #2360, #2361) on one more round of each. A differential harness ran old and new matchDestructuredCallResult
on every call position of mealie (10,330) and the CPython assets (9,170,
742 inside misread regex literals) and on 16,000 random token-soup
sources (1.33M positions), and old and new jsFunctionLocalScope on ~500K
(function, line, name) triples: no difference; it reports one for each
deliberate mutation of the new code that is not provably equivalent.

Medians of 4 interleaved runs on a loaded 16-thread Windows box, process
CPU / wall / peak RSS: pretix 160.2 s / 75.9 s / 2.47 GB -> 88.7 s /
43.5 s / 1.93 GB; CPython assets 75.1 s / 37.6 s / 1.23 GB -> 32.1 s /
20.4 s / 0.81 GB; mealie (no bundles, the control) 39.2 s -> 38.6 s CPU.

__tests__/js-resolution-work.test.ts counts strips: a file with a
destructured binding and 40 bare calls is stripped for the check once
(41 on main), a 40-line function once (40 on main); each count fails when
only its own half of the fix is reverted. full-pipeline.test.ts requires
indexAll's and sync's durationMs to cover a linking pass delayed 250 ms
past the orchestrator's time (on main: 218 < 468).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:52:39 +00:00
Colby Mchenry deac7710f2 fix(go): resolve unexported receivers and nested modules within the right package (#2323, #2322) (#2361)
Calls through a method receiver or parameter of an unexported type
(`func (s *server) Create() { s.service.AddItem() }`) had no edge: the
Go receiver inference only accepted PascalCase types (#2323). And only
the project-root go.mod was read, so a module in a subdirectory, or one
beside other modules as in etcd, treated imports of itself as
third-party (#2322).

Fixing either alone sends calls to the wrong package, because Go type
names are unique only per directory while types and methods were looked
up by name across the project. The receiver pattern alone sent 1,042 of
harbor's 1,998 new calls to another package's same-named type; reading
nested modules alone made harbor's caching wrappers call themselves and
put about 180 of etcd's type references on same-named methods. Both
share one fix:

- A lowercase receiver or parameter type is read only where a parameter
  list puts it (receivers, parameters, multi-line lists, var blocks).
- Receiver inference also reports the type as written. For Go,
  resolveMethodOnType takes the declaring package's directory (the
  caller's for a bare name, the import's for a qualified one) and counts
  only that package's methods, then the types its declaration embeds
  (struct and interface embedding, read from the source) instead of the
  name-based supertype union. The field-chain matcher scopes its struct
  and field types the same way.
- The resolver reads the nearest go.mod of every directory holding
  indexed Go files (no disk walk; a module under testdata/ or a _/.
  directory serves only its own files) and maps an import path to the
  module with the longest module path, the importing file's own module
  breaking a tie. getGoPackageDir replaces getGoModule.
- A bare name written through a package (`job.OPCommand`) resolves
  through the import into that package's directory, and a name-matched
  candidate outside the qualifier's package is rejected; a method reached
  through a variable that shadows an import is exempt.

Verification: new go-unexported-receiver (3) and go-nested-module (11)
tests: 12 fail on main (the 2 controls pass), all 14 pass. The issues'
repros go from 0 to 1 caller; the root-go.mod and `Server` variants are
unchanged.

Validation, main 26e8488a -> this branch, whole repos, Go->Go edges keyed
by source/target qualified name, file, line and column:
- etcd 6bb7e5e: 52,530 -> 66,284 (+13,827 / -73), calls 18,074 -> 25,265
- harbor 721c6d4: 62,423 -> 77,615 (+15,819 / -627), calls 16,308 -> 25,236
- prometheus 770ca8f: 90,970 -> 92,399 (+2,249 / -820), calls 40,050 -> 41,493
Of the 1,520 removed edges, 1,458 are the same call site or reference
now resolved to the right package's symbol; the other 62 were field
reads taken for method values, names whose real target is not indexed
(a `type T = string` alias), and a call to a method the receiver's type
does not have. Indexing time and CPU are unchanged.

Co-authored-by: Diao Shengjia <104132148+GoDiao@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:16:14 +00:00
Colby Mchenry 9f69e47836 fix(dart): link getter reads, enum-extension members and more type positions (#2338, #2327) (#2360)
Issue #2338: a Dart getter read (`x.area`, `s.label`) linked nothing, so
`codegraph callers area` was empty while `x.grow()` worked, and a member
an extension adds to an ENUM (`s.shout()` through `extension ShapeInfo on
Shape`) never resolved. Issue #2327: `callers Report` saw only parameter
and return types; the type an extension is `on`, field types, and types
named in bodies and initializers (`Future<Report?>.value(null)`, riverpod's
`final reportProvider = Family<Report?, String>()`) recorded nothing.

Causes:
- The Dart body walker only emitted calls (a selector with an argument
  list); a member read is a selector without one, so it produced no ref.
- For a typed receiver, resolveMethodOnType looks for `Type::member` and
  the type's supertype edges; an extension's member is `ShapeInfo::shout`.
  On a class receiver the call fell through to name matching, which found
  a unique `twice` by luck; on an enum the "type the project doesn't
  declare" guard (it only counts classes) returned null.
- Type references were taken from function and method signatures only.

Fix (TS extractor and the Rust kernel, mirrored):
- A member read `x.area` / `x?.area` / `Type.getter` whose receiver is a
  plain name, and that is not a call, is sent as a `references` ref named
  `x.area` on the member. The resolver (matchDartMemberRead, ahead of
  every other strategy) types the receiver — a static read through a
  project type, a parameter or local in scope, else a field of the
  enclosing class — and links the getter that type reaches: declared on
  it or a type it extends, mixes in or implements, else added by an
  extension on one of those (an unnamed extension only in its own
  library). The edge is `calls`: a Dart getter is a method node, reading
  it runs it, and `calls` is what callers, impact and explore's flow
  follow (as Ruby's paren-less `x.area` and VB.NET's paren-less calls
  through #2355 already are). A field read, a getter of a type outside the
  project and an untyped receiver link nothing; there is no name-only
  fallback. Kotlin/Swift/C#/TS link no instance property reads today.
- resolveMethodOnType, for Dart, falls back to a member an extension on
  the type (or a supertype) adds when no declaration of the type has it.
- `references` refs for the type an extension is `on`, a field's declared
  type and initializer (from its class — Dart fields mint no nodes), an
  enum field, a top-level variable's type (from the file), a constant's
  initializer (from the constant), and every type a function body names:
  locals, generic arguments, casts, type tests, catch clauses. Skipped: a
  `new`/`const` constructor's class (already instantiates/calls), an
  import prefix (`p` in `p.Foo`), built-ins, and lowercase names, which in
  Dart are built-ins (`num`, `dynamic`) or error-recovery artifacts around
  syntax the grammar predates (dot shorthands). A class whose field holds
  its own type does not get a self-edge.

Verification: __tests__/dart-getter-reads.test.ts (7 tests) and
__tests__/dart-type-positions.test.ts (3 tests) fail 8/10 on main (both
the TS path and main's kernel) and pass on both arms here; the 2 that pass
on main are the controls. New kernel parity fixture TortureReadsTypes.dart
(+CRLF); kernel-dart-parity passes with CODEGRAPH_KERNEL_EXPECT=1, and
scripts/kernel-parity.mjs over bloc/riverpod/flutter-samples is 0 diffs
(2,103 files byte-parity, 300 parse-error files defer as before). tsc and
build clean; the issues' repros now list c/a/b for area/label/shout and
Holder, ReportX, reportProvider and run for Report.

Validation (kernel loaded, main vs this branch): nodes unchanged, 0 edges
removed, no new self-loops.
- felangel/bloc: 16,372 -> 18,276 edges (+550 getter-read calls, +1,354
  type references)
- rrousselGit/riverpod: 76,432 -> 81,909 (+598 getter reads, +13
  extension-method calls, +4,866 type references)
- flutter/samples: 26,133 -> 26,943 (+142 getter reads, +668 type refs)
Spot-checked 16 getter reads and 16 type references against source.
Index time within noise on riverpod (median of 5 interleaved runs); reads
that link nothing are kept for re-resolution like failed calls (riverpod
db 92.9 -> 103.2 MB).

No EXTRACTION_VERSION bump: it already moved to 28 this release.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:12:24 +00:00
Colby Mchenry 1b56cb1992 fix(explore): find COBOL copybooks by name, with every include site (#2342) (#2358)
`codegraph explore MYCOPYBOOK`, the `codegraph_explore` MCP tool and
`codegraph context` answered "No relevant code found" for a COBOL copybook
named in the query, while `query`/`node` found its include at once.

Cause: the COBOL extractor records `COPY X` and `EXEC SQL INCLUDE X` as
`import` nodes, and findRelevantContext filters every channel by
HIGH_VALUE_NODE_KINDS, which leaves imports out on purpose (a JS/TS/Python
import statement is noise in explore). With the include out of reach the
text search fell through to its fuzzy fallback, so a member with a digit in
it (never extracted as a symbol) came back as an unrelated program one edit
away: CVACT01Y -> CBACT01C, CVCUS01Y -> CBCUS01C on CardDemo. Making COBOL
imports "high value" alone would not have helped either: explore's renderer
skips import nodes, and the import-to-definition step drops a COBOL include
because its `imports` edge leaves the enclosing scope, not the import node.

Change:
- src/graph/cobol-copybooks.ts (new, shared derivation): resolves the COBOL
  words of a query to copybooks - every COPY / EXEC SQL INCLUDE of the member
  (case-insensitive) and the indexed copybook file (.cpy, or a file an
  include resolved to; a same-named program is not one). Gated on the
  project having COBOL files at all (one seek on idx_files_language), so no
  other language's query does extra work or changes. Plain lowercase words
  in a multi-word query are English, not member names.
- findRelevantContext: a named copybook's top-level declarations, then its
  include statements, become entry points ahead of the kind-filtered
  channels, like an exact filename (at most half the slots when the query
  matched other things, include sites in those files first). They are not
  walked (BFS from a record layout spent the answer on whichever program
  MOVEs into it), the member is not re-searched as text (the fuzzy decoy),
  and an explicit nodeKinds filter without `import` is honored.
- codegraph_explore: the copybook file is pinned like a path in the query;
  an include statement that is an entry point is grouped and rendered (a
  window around the COPY line) instead of skipped; a new "COBOL copybook"
  section lists every include site (path:line, COPY vs EXEC SQL INCLUDE,
  25 lines then a count) or says the member has no indexed source (DB2
  DCLGEN, compiler-supplied such as SQLCA/DFHAID).
- QueryBuilder: three indexed lookups; CodeGraph.findNamedCopybooks exposes
  the derivation so explore and the context builder share one answer.
No extraction change; no re-index needed.

Verification: __tests__/explore-cobol-copybook.test.ts (8 tests, through
ToolHandler codegraph_explore and buildContext): with src/ reverted to
origin/main 6 fail (the 2 guard cases pass on both), 8/8 pass with the fix.
tsc clean, npm run build OK. Issue repro fixed on the built CLI (explore +
context) and through a real `serve --mcp` tools/call. Full suite on a heavily
loaded box: 5716 passed, 25 failed in 15 files; 12 of those files pass alone,
and the other 3 (function-ref, git-index-currency, worktree-detection) are
5 s / 60 s timeouts that fail on clean main as well in interleaved runs.

Validation (both builds on one index per repo; query-side change only):
- expressjs/express: 14 explore + 4 context queries byte-identical.
- aws-mainframe-modernization-carddemo: 10 non-copybook queries
  byte-identical; 15 copybook queries all now return the copybook (when
  indexed) and every include site - before: 9 "No relevant code found",
  4 unrelated programs, 2 without the copybook.
- cicsdev/cics-genapp: 6 non-copybook queries byte-identical; 6 copybook
  queries fixed (LGCMAREA's 25 sites incl. multi-line EXEC SQL INCLUDE,
  lowercase lgpolicy, SSMAP with no indexed source).
- In-process explore latency on express unchanged within noise.

Thanks @popolusiak for the report.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:39:03 +00:00
Colby Mchenry 8998697f1d fix(vue,ts): link calls in Vue templates and in top-level destructuring (#2340) (#2357)
`codegraph callers <composable>` missed most of the components using it in
a Nuxt app (Issue #2340), through two extraction gaps:

- A destructuring declaration at module or `<script setup>` scope
  (`const { a } = useFoo(1)`, `const [b] = useFoo(1)`) mints no symbol,
  and extractVariable skipped its declarator with `continue` before
  walking the initializer, so the call was never recorded (inside a
  function the body walker reached it). The declarator is now walked the
  way a function body walks it, with the enclosing scope on the stack:
  the file for module code, which a `<script setup>` block folds into its
  component. Defaults in the pattern and calls inside callbacks in the
  initializer count too. Mirrored in the native kernel
  (tsjs/extractors.rs), which extracts TS/JS in released builds.
- The Vue extractor parsed only `<script>`, so calls in the template
  (`{{ useBar(link) }}`, `:to="useBar(link.location)"`, `v-if`, `v-for`
  sources, `@click="save(item)"`) made no reference. The new
  vue-template-calls scanner reads the root `<template>` quote-aware,
  collects interpolations and directive values, and names each callee
  the way a script call is named (`useBar`, `store.fetchUsers`). It skips
  `v-for` aliases and slot props in their element's subtree, arrow and
  function parameters, `$`-helpers, Vue's template globals, strings,
  regex literals, `new X()`, object-method shorthand, `v-pre` subtrees,
  custom blocks and non-HTML templates. Each call is a `calls` reference
  from the component at its own line and column; a bare call to the
  component's own Options API method is linked at extraction, since
  resolution only binds those through `this.`.

Two resolution follow-ons, found validating on elk:

- The vue-handler synthesizer resolved the leading identifier of every
  `@event` value, so `@click="save(item)"` got a second, line-1 edge, and
  `@click="query = ''"`, a slot prop's `hide()` or a `refresh()`
  destructured from useAsyncData were bound to any same-named function in
  another file. It now resolves only a bare handler name (`@click="save"`)
  and keeps its composable-destructure mapping.
- The Nuxt auto-import rule answered a call to a file's own `clearError`
  or `navigateTo` with an edge from the caller to itself. A same-file
  declaration of that name now shadows the auto-import.

Verification: new tests in vue-template-calls.test.ts (scanner shapes and
four indexed projects, incl. the issue's repro), extraction.test.ts
(module-scope destructuring in ts/tsx/js/jsx; script-setup + template
refs, LF and CRLF) and kernel-tsjs-parity.test.ts (destructuring, kernel
vs wasm). On main (src + kernel reverted) the 10 indexed/extraction tests
fail and the 4 parity cases fail with the main kernel; all pass with the
fix (CODEGRAPH_KERNEL_EXPECT=1). Issue repro on the built CLI with the kernel:
callers useFoo 2 -> 5 (A.vue, B.vue, F.vue, G.ts, D.ts), callers useBar
0 -> 2 (H.vue, I.vue). kernel-parity.mjs over elk, nuxt/movies and
fastify: 467/471 files byte-identical, 0 diffs, 4 deferred.

Validation (kernel loaded, main vs branch, edges by natural key):
elk 6282 -> 6633 (+443 / -92), vue-element-admin 2337 -> 2372 (+60 /
-25), nuxt/movies 939 -> 959 (+26 / -6), fastify (plain JS control) no
edge change. Every removed edge is a line-1 vue-handler edge whose pair
is now linked at the handler's line (101), a name-collision guess (8),
or an auto-import self-loop now resolved to the local function or route
(14). Self-loops: elk 44 -> 30, others unchanged.

Builds on #2341 by @drakeo338 (walk the destructuring initializer) and
the template scan in #2073 by @L0garithmic.

Co-authored-by: drakeo338 <paranoyouz@gmail.com>
Co-authored-by: L0garithmic <L0garithmic@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:17:51 +00:00
Colby Mchenry a192df40f9 fix(telemetry): count late index uploads, treat usage as live ingest, purge before catch-up (#2333) (#2356)
Three bugs in the self-hosted telemetry services (telemetry-worker/ and
telemetry-dashboard/), reported against 6560052a and all still on main.

1. An index run that uploads late never counted toward activation. The
   dashboard's funnel reads machine_first_seen.first_index_day, which only the
   nightly rollup wrote, and the rollup re-rolls just the last three days plus
   days with no daily_machines row. Ingest accepts timestamps up to 30 days old
   and the client keeps an event's original timestamp when it re-queues it, so
   an index event 4+ days late landed on a day nothing revisited and
   first_index_day stayed NULL. The ingest upsert of machine_first_seen now
   lowers first_index_day the way it lowers first_day:
   coalesce(min(old, new), old, new), because SQLite's min() is NULL if either
   side is. It is the same statement and row, with no new index and no
   migration (the column exists since 0002). The rollup still re-derives it
   from raw events, which covers events stored before this change.

2. ingest_stalled read only max(day) FROM events, but usage counters have
   lived in usage_daily since 0003, so a day with usage and no lifecycle events
   read as "The ingest worker ... is not storing anything". /api/meta now also
   reads max(day) FROM usage_daily (a primary-key lookup) and returns
   latest_usage_day and latest_ingest_day. Ingest counts as stalled only when
   there is no lifecycle event from yesterday or later and no usage counter
   from the day before yesterday or later, because clients upload a day's
   counters only after that day ends. The banner names latest_ingest_day.

3. runNightly rolled up the 3 regular days, then up to 31 missed days, and ran
   purgeOldEvents last. Each day first folds its legacy usage_rollup rows,
   which takes minutes on a heavy day, so a backlog could run past Cloudflare's
   15-minute Cron Trigger limit and skip the purge and the summary line night
   after night. The purge now runs first: it is bounded and touches only days
   past the window, which no rollup reads. After that, rollups stop starting
   new days or fold chunks 10 minutes in (NIGHTLY_BUDGET_MS). A day stopped
   mid-fold gets no rollup at all, so it stays a missed day and the next night
   continues from its last committed chunk. The summary line counts those days
   as `deferred`, and `caught_up` now counts missed days actually rolled up.

Verification: __tests__/telemetry-services.test.ts (new; the root vitest
config picks it up) runs the worker's fetch/cron code and the dashboard API
against the checked-in migrations in in-memory node:sqlite through a small
D1-shaped adapter. With the source fix reversed, all 5 tests fail (activation
0, first_index_day null, ingest_stalled true, and "nightly rollup incomplete:
4 day(s) failed, purge failed" once statements pass the 15-minute mark). With
the fix, all 5 pass. `npm run check` is clean in both packages. Against
wrangler dev, run locally only: smoke:rollup 71/71, smoke:cutover 62/62,
smoke:api 122/122 and render-check 87/87. smoke (ingest) is 46/47; the one
failure is the 70 KB oversized-body curl argument hitting the Windows
command-line limit, and main fails it the same way. The new smoke assertions
fail on main.

Takes effect only once both workers are redeployed; no migration.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:06:42 +00:00
Colby Mchenry 6c61dd74ca fix(vbnet): link Shared fields and properties read through their class (#2305) (#2355)
VB.NET emitted a reference for calls only, so a value read or write
through a type name -- `AppSession.SessionId`,
`AppSession.CurrentUser = "demo"`, `Logger.Level` -- linked nothing, and
`codegraph callers` on the field, the property and the class came back
empty. The reporter's central session class showed 6 callers against
~460 referencing files.

- Extraction (TS only; VB.NET is not kernel-routed): the static-member
  pass now covers VB.NET. A value read through a Capitalized name is sent
  as one `references` ref, `Name.Member`, the receiver kept as a call's
  is. Namespace roots (`System`, `Microsoft`, `My`, `Global`) and the
  built-in type keywords (`String.Empty`) are not sent.
- Resolution (vbnet-receivers.ts matchVbMemberRead, ahead of the
  framework, import and name strategies): the receiver is looked up with
  #2351's VB.NET scoping, case-insensitively. A local, parameter, field,
  property or Module member of that name holds a value and links nothing;
  one typed as the type of its own name ("Color Color") reads that type.
  Otherwise the class, Module, Structure, Interface or Enum the
  namespaces, Imports and aliases around the read see is the type: the
  read links the member it declares or inherits (field, property,
  constant, event, Enum value, nested type; Shared before a same-named
  instance member, never the member the read is in) and, from outside
  the type, the type itself. A method named this way is a call made
  without parentheses (`calls`) unless AddressOf / NameOf only names it.
- `AppSession.Items(0)`, an index into a Shared field that VB.NET writes
  as a call, now reads the field and the type instead of linking nothing.

Contributor PR #2321 (@ChrisPrapas) had the right idea -- enable the
static-member pass for VB.NET with a member-level ref -- and its test
scenarios are adopted. Its resolution is replaced: it name-matched the
receiver's bare name and checked a member only against the receiver's
spelling. Ported onto main it passes its own tests, but binds a
parameter `appSession As OtherSession` to AppSession's members, misses
import aliases, and adds 8,652 / 13,554 edges on SCrawler / staxrip,
about 2,000 per repo binding a receiver to another class's same-named
member (a plugin form's local `Dim CONTAINER_MAIN` went to another
form's control field).

Verification: new vbnet-shared-member-refs.test.ts (7 tests) fails 6/7
on main and passes; the other VB.NET suites pass; tsc and build clean;
the issue's repro lists Consumer.Run and Consumer2.Describe for
SessionId, CurrentUser and AppSession.

Validation, main vs this branch: nodes unchanged, 0 edges removed, no
self-loops. SCrawler 14,893 -> 17,437 edges, staxrip 26,647 -> 32,568:
Enum values 803 / 1,448, Shared fields and properties 363 / 1,459, the
type read through 1,374 / 2,879, Shared functions called without
parentheses 3 / 126, AddressOf / NameOf 1 / 9. Spot-checked against
source: per-site SiteSettings, import aliases, escaped `[Date]` /
`[New]`, nested enums, "Color Color" fields; designer `Point.Empty` and
staxrip's AutoCrop duplicates stay unlinked. Index time within noise;
value reads that link nothing are kept for re-resolution like failed
calls (db 22.6 -> 28.9 MB, 28.3 -> 35.8 MB).

Not covered: reads through Me / MyBase, unqualified reads, instance
reads through a typed variable, Module variables as receivers,
namespace-qualified receivers and With blocks.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: ChrisPrapas <129202926+ChrisPrapas@users.noreply.github.com>
2026-10-05 20:05:18 +00:00
Colby Mchenry 69ee3b9a85 fix(ui): fall back past a port Windows refuses with EACCES, and say why (#2299) (#2353)
On Windows, a port another program holds exclusively (SO_EXCLUSIVEADDRUSE)
or one inside an excluded port range fails listen() with EACCES rather than
EADDRINUSE. The `codegraph ui` port fallback only moved on after EADDRINUSE,
so it stopped at the first such port. The error then blamed the POSIX
privileged-port rule ("Ports below 1024 usually need elevated privileges")
for ports like 4747 or 49912. Confirmed on Windows 11: a .NET listener on
0.0.0.0:P with ExclusiveAddressUse makes Node's listen(P, '127.0.0.1') fail
with EACCES.

Fix:
- shouldTryNextPort moves past EADDRINUSE on every platform. It moves past
  EACCES at any port on Windows, which has no privileged ports, and from
  1024 up elsewhere.
- Below 1024 off Windows, EACCES is the privileged-port rule, and the next
  port falls under it too. The walk stops there with the message it always
  gave, rather than trying 20 privileged ports.
- describeBindFailure takes the platform:
  - Windows names the exclusive hold or reserved range and how to list the
    ranges (netsh int ipv4 show excludedportrange protocol=tcp).
  - POSIX keeps the privileged-port text below 1024. Above it, it says the
    system refused permission.
  - A walk that ran out says "in use or reserved" (Windows) or "in use or
    not allowed" when any port was refused rather than in use.
- Both helpers take the platform as a parameter, like browserOpenCommand,
  so the tests check every platform's answer on any host.

This builds on PR #2319 by @sx4im, which advanced on EACCES, used the
Windows wording and added the issue's listen-spy test. That PR advanced on
EACCES on every platform and printed "Windows refused port N ... netsh" on
Linux and macOS too. On POSIX it would also walk 20 privileged ports and
end with "Ports 80-99 are all in use or reserved". This change restricts
both to where they are true. The changelog entry goes in the viewer-launch
file, because the viewer is not released yet.

Verification:
- __tests__/ui-server.test.ts gains three kinds of test:
  - The issue's end-to-end test: port N is taken and N+1 is refused through
    a listen spy, so the server must land past N+1 and serve 200.
  - A pinned-port message test.
  - Unit tests of the decision and the messages for win32, linux and darwin.
- Results on __tests__/ui-server.test.ts:
  - Main source: 9 failed / 40 passed. The walk fails with "Not allowed to
    listen on port 57423. Ports below 1024 usually need elevated privileges".
  - With the fix: 49 passed / 2 skipped (existing POSIX-gated tests).
- Built CLI on Windows, with 0.0.0.0:<port> held exclusively:
  - `codegraph ui` while 4747 is held: main exits 1 with the privileged-port
    message. With the fix it serves on 4748 (GET / 200).
  - `codegraph ui --port <held>`: prints the Windows message.
  - A 3-port walk over held ports reports "in use or reserved".
  - Re-listening on the same server after a real EACCES works on Node 22.22
    and 24.16.
- tsc --noEmit is clean and npm run build succeeds. The 20 viewer test
  files pass (439 passed, 5 skipped).
- Full suite: 5732 passed, 10 failed, 319 skipped. The 10 failures are
  timeouts and EBUSY teardown under load in function-ref,
  git-index-currency, index-daemon-rebuild and rust-self-owner. All four
  files pass when run alone.

Issue #2299 reported by @ijbranch.

Co-authored-by: Saim Shafique <msaimshafique4664@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:03:03 +00:00
Colby Mchenry 26e8488aed fix(windows): the kernel-parity sweep loads the engine as file:// URLs (#2352)
scripts/kernel-parity.mjs handed import() a path from path.join(). On
Windows that is `C:\...`, which Node's ESM loader rejects with
ERR_UNSUPPORTED_ESM_URL_SCHEME, so the sweep could not run there at all
and contributors kept patched scratch copies. The three engine imports
now go through pathToFileURL(...).href, as the agent-eval probe scripts
already do. On macOS and Linux an ordinary path resolves to the same
file:// URL as before.

Checked on Windows 11 after npm ci, npm run build and
scripts/build-kernel.sh: the unmodified script threw at its first import;
with the fix, the C# fixtures (3/3), src/search (4/4), all of src/ as
TypeScript (261/263, 2 deferred to wasm) and every fixture language
(42/42) run to the summary line and exit 0.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:37:39 +00:00
Colby Mchenry c9944147bb fix(vbnet): resolve receiver calls by declared type; guard .NET names, bare calls and ties (#2351)
VB.NET's resolver guessed a receiver call's target from the receiver's
name whenever no class matched it, without any of the guards C# has.
Indexing Structure members (#2345) gave those guesses new targets: 23
SCrawler `xxxFile.Delete()` calls on PersonalUtilities' SFile went to a
nested struct's `TempFileConversion::Delete`, and staxrip's main app
sent 90 calls to its AutoCrop tool's duplicate `ColorHSL`.

- Declared types (new src/resolution/vbnet-receivers.ts): a receiver's
  `As` type decides the call (local, parameter, field, property,
  `For Each`, `As New`, ...). The candidates are that type's member, one
  it inherits, or an extension method written for it. A type from
  outside the project gets no link.
- .NET standard method names (`Add`, `Contains`, `Clear`, `Dispose`,
  `ToString`, ...) on an untyped receiver need a receiver named after
  the owner, as in C#. VB.NET matches them case-insensitively.
- A bare call, or one on `Me`, reaches the enclosing class, what it
  inherits, or a Module. A nested type is matched by its bare name only
  from inside its owner. Types resolve through enclosing namespaces,
  `Imports` (aliases included) and the caller's project. A tie between
  equally good guesses goes to the caller's file, then its project, then
  the nearer directory, or gets no link.
- vbReceiverOf reads the name at or after the reference's column, so
  `Me.Size = New System.Drawing.Size(...)` is read through
  `System.Drawing`, not `Me`.

Before/after on main (nodes unchanged): SCrawler 15,092 -> 14,893
edges, staxrip 28,002 -> 26,647. Of the non-containment edges removed,
855 / 2,802 are the same link with new resolver metadata. The rest:

- .NET names on untyped receivers no longer guessed: 196 / 1,378.
  731 of staxrip's are StringBuilder `sb.Append` calls that had gone to
  `LogBuilder::Append`.
- Other receiver guesses dropped: 86 / 103, including the SFile.Delete
  calls.
- Bare calls out of scope: 38 / 834. 590 of staxrip's are
  `New Point(...)` that had gone to a class nested in `ButtonEx`; others
  are WinForms `Refresh()`, `Focus()` and `Activate()`.
- Retargeted: 227 / 532, e.g. 208 bare `Add(...)` calls in encoder
  classes now reach the inherited `CommandLineParams::Add`.

121 / 958 call sites resolve that did not before, e.g. `cms.Add` ->
ContextMenuStripEx and `td.AddButton` -> TaskDialog. Designer -> Size
links fall from 5 / 1 to 0; designer -> Add stays 0.

Known trade-off, shared with C#'s guard: about 31 staxrip calls like
`switches.Join(BR)` that may be the project's own `MiscExtensions`
extension methods are no longer linked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:32:53 +00:00
Colby Mchenry 355afc70b1 fix(csharp): walk field and property initializers; a constant never names a type (#2337) (#2348)
* fix(csharp): walk field and property initializers as the member

Field declarators and property `= initializer` values were skipped by
both extractors, so `private readonly ILogger _log =
LogManager.GetLogger(typeof(X));` linked nothing, and a method or class
used only from an initializer looked unused.

- tree-sitter.ts / csharp.rs: walk each C# field declarator whole with
  its field on the stack, and every property `value:` (not only
  `=> expr`) with the property on the stack. Calls, instantiations,
  static reads, lambda bodies and fn-ref candidates (varinit included)
  belong to the member; the fn-ref scan skips the walked subtrees, so
  each candidate is captured once. Attributes stay unwalked.
- A target-typed `new()` that is the initializer instantiates the
  declared type (`List<Foo> _items = new();` -> List). In bodies it
  stays invisible, as before.
- name-matcher.ts: a .NET type position never names a `constant` (the
  kind `const` / `static readonly` fields get). Walking initializers
  exposed `new Version(5, 18)` binding to a `const string Version` and
  `static readonly Meter Meter = new(...)` instantiating itself.

Kernel/wasm parity: 0 diffs on serilog, Newtonsoft.Json and jellyfin,
and full-index dumps byte-identical between the two arms. No
EXTRACTION_VERSION bump: #2345 already moved it to 28 this release.

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

* docs(changelog): credit the report and contributor behind the constant change

The type-position change here is what issue #2337 reports (`new Station
{ … }` unresolved beside a `private const string Station`) and the same
one-line change as @drakeo338's #2339. Verified on the issue's own
three-file repro: the `instantiates` edge to `Demo.Core::Station`, missing
on main, is present with this branch.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:20:15 +00:00