Commit Graph
79 Commits
Author SHA1 Message Date
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
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
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 MchenryandClaude Opus 5.5 911fcf6033 fix(vapor): a closure route's calls are the route's (#2273)
A Vapor route whose handler is a trailing closure had no handler symbol and
linked to nothing (vapor's own repo: 76 of 80 routes bound to nothing in the
viewer browser pass). The closure is the handler: its body's calls, each once
with its receiver, are referenced from the route, as an Express inline
handler's are.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:52:43 +00:00
Colby MchenryandClaude Opus 5.5 104b8de3e6 fix(laravel): a string controller resolves without a Controller suffix (#2271)
Only `XController@action` was claimed and resolved, and the written
namespace was stripped, so akaunting's 'Common\\Uploads@inline' (and a
Route::resource with a string controller plus options) bound 304 of 313
routes to nothing. Any `Class@action` is claimed; the namespace path under
App\\Http\\Controllers is kept and picks between same-named controllers;
a resource reads its first argument and resolves its controller class in
the Laravel resolver (no `use` brings a string-named class into scope).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:40:38 +00:00
Colby MchenryandClaude Opus 5.5 e12f5bbb92 fix(nuxt, astro): a file-routed page is bound to its component (#2267)
A Nuxt pages/x.vue or Astro src/pages/x.astro route linked to nothing, so
the viewer's Steps drew a page alone and the routes list never named what
serves it. Each page route now references its file's own component (never a
same-named one — every index.vue is a component named index), an Astro
endpoint its exported verb handlers, and the routing manifest accepts a
component as what serves a route, which also lists Vue Router screens.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:33:41 +00:00
Colby MchenryandClaude Opus 5.5 c0d3285b55 fix(rails): only:/except: are read as %i[], %w(), a string or :only => (#2269)
Only a bracketed list (and the scoped reader's bare :sym) was read, so
maybe's `resources :family_exports, only: %i[new create index]` drew all
seven RESTful routes — four to actions the controller lacks, listed by the
viewer as routes bound to nothing. Both route readers now share one option
reader for every form Rails accepts.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:31:06 +00:00
Colby MchenryandClaude Opus 5.5 d45edca3fd fix(express): a function-expression handler is the route's body, like an arrow (#2261)
An Express handler written inline as `function (req, res) {…}` — the form
Express's own examples use — fell to the named-handler branch, so the route
linked to nothing and Steps drew a lone box for `POST /login`. The handler
is now the registration's LAST argument, inline as an arrow or a function
expression, bare or inside a wrapper call, past a trailing comma; an inline
middleware before a named handler no longer stands in for it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:11:26 +00:00
Colby MchenryandClaude Opus 5.5 c38aa9c973 fix(nestjs): the provider convention stays in the reference's feature (#2246)
In a TS/JS module the *Service/*Controller convention picks the file's own
class or the convention file beside it (users.controller.ts → users.service.ts),
never a same-named class elsewhere preferred for its file name; anything
farther is the import resolver's or name matching's.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 04:15:09 +00:00
Colby MchenryandClaude Opus 5.5 b2afa75f08 fix(astro): the component name heuristic applies to .astro files only (#2245)
Astro components are used in Astro markup; a .ts / .d.ts type name sharing a
component's name (Playwright's Page, a declaration file's Image) is no
component.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 04:05:02 +00:00
Colby MchenryandClaude Opus 5.5 000c986276 fix(express): a middleware candidate is a declaration, never an import node (#2244)
#2243 put the calling file's candidates first, which let resolveMiddleware
pick the file's own `import cors from 'cors'` node; candidates are now
functions, methods, values and classes only.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 03:56:54 +00:00
Colby MchenryandClaude Opus 5.5 3343646446 fix(express): name patterns pick only the calling file's own declaration (#2243)
The middleware/controller/service heuristics took the first same-named node
anywhere; a JS/TS module reaches another file's only through an import or
require (the import resolver's), so they now keep a same-file pick only.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 03:52:28 +00:00
Colby MchenryandClaude Opus 5.5 8ca4433502 fix(objc,swift): a class conforming to its namesake protocol implements the protocol (#2242)
The SwiftUI/UIKit/Vapor resolvers apply to Swift refs only; the shared name
pick never returns the declaring type for an inheritance ref; and the
supertype re-choice prefers the declaring file, then a protocol/interface for
`implements`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 03:45:49 +00:00
Colby MchenryandClaude Opus 5.5 f976a050a0 fix(react): hook and context heuristics pick only the file's own in JS/TS (#2240)
A JS/TS module reaches another file's hook or context only through an import
(already the import resolver's), so resolveHook/resolveContext pick from the
reference's own file alone there; an optional call `f?.(…)` is a bare call.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 03:31:22 +00:00
Colby MchenryandClaude Opus 5.5 179939055c fix(frameworks): name heuristics pick only what the reference can see, own scope first (#2225)
One shared pick behind the Django/FastAPI/Flask, ASP.NET, Gin, SwiftUI/Vapor,
Spring and Rust name heuristics: never a function-local declaration, a type
nested in another file's type, or a node the language keeps out of the
reference's file; the reference's own file first, then its directory (not
Rust), then a conventional folder.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:45:56 +00:00
Colby MchenryandClaude Opus 5.5 7365427d4f fix(rust): framework name heuristics never reach a function-local item (#2224)
The Rust resolver's handler/service/struct heuristics took the first
same-named item and then checked scope; they now drop items declared inside
a function body and out-of-scope names before choosing, and take the file's
own item first.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:34:57 +00:00
Colby MchenryandClaude Opus 5.5 d6f68648cb fix(spring): name heuristics start from the reference's own file and package (#2222)
The Spring resolver's entity/service/controller name heuristics took the first
same-named class under a conventional folder; they now prefer the ref's own
file, then its package, and never reach another file's nested class by its
bare name.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:16:23 +00:00
Colby MchenryandClaude Opus 5.5 fa8d7284c5 fix(rust): a bare name is in scope only through its module or a use (#2214)
The Rust scope rule (#2124) only checked prelude names; every other bare
name could mean any same-named project item, and a name after any path
(`io::Result`) could mean any at all. The Rust framework resolver's
PascalCase / handler / service patterns bound names with no scope check at
0.7-0.8. tokio's 1,221 `io::Result<...>` went to a private
`runtime::task::Result` alias, 642 `Context<'_>` (from `use
std::task::{Context, Poll}`) to `runtime::context::Context`.

- A type or function from another file is in scope only through a `use`
  that binds it (its leaf, `as` alias, or `{self}`) or a glob over its
  module; methods and fields (reached through a value), import refs and
  module / file targets are unaffected.
- A name a file imports from outside the project - `std` / `core` /
  `alloc`, or a crate its Cargo manifests declare as a dependency - is that
  outside item. (An inline `mod support { pub mod panic; }` is the
  project's.)
- A path names its module: `seg::Name` reaches an item of module `seg`;
  a path rooted at std (`std::io::Error`) or at an outside import (`io::`
  under `use std::io`) reaches none; `crate::` / `self::` / `super::` and a
  project crate's name (`clap::Command`, re-exported) look it up as before.
  The path is read anywhere on the line, not only at the reference's column.
- The framework resolver's patterns apply the same check.

tokio -3008 +610, serde -792 +650, ripgrep -602 +247, axum -453 +186, clap
-342 +32, bat -38 +9. Sampled losses: std / core / futures / syn / http /
anyhow imports and paths, associated types (`S::Ok`, `S::Error`, `V::Value`,
`Fut::Output`); gains are same-module items the old first match hid.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:54:08 +00:00
Colby MchenryandClaude Opus 5.5 5c01f017cf fix(react): the React resolver only resolves a script's references (#2211)
The React resolver's `languages` gates its route extraction, not its
resolve(): framework resolvers see every language's references. Its
component rule was already JSX-only, but its hook (`use...`) and context
(`...Context` / `...Provider`) rules were not: in halo - a Spring app whose
UI package makes React detected - 209 Java import edges (`SecurityContext`,
`ApplicationContext`, `ObjectProvider`, `DirtiesContext`...) were resolved by
it onto import nodes. It now answers only JavaScript / TypeScript / TSX /
JSX references.

halo -209 +3 (a Java test's `responseContext()` helper now resolves to
itself); trpc, excalidraw, react-redux-realworld, bulletproof-react,
react-native-reusables unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:10:14 +00:00
Colby MchenryandClaude Opus 5.5 d7cdb0c09b fix(vue): the Vue resolver only resolves a Vue app's scripts (#2210)
Framework resolvers see every reference of every language, and the Vue
resolver had no language guard: its PascalCase-component rule took
mealie's Python `QueryFilterBuilder(...)` (an unresolved import) to the
`QueryFilterBuilder.vue` component at 0.8, and its macro / auto-import rules
matched any language's `ref(...)` or `computed(...)`. It now answers only
refs from vue / javascript / typescript / tsx / jsx files.

mealie: 6 Python calls reroute to the Python class; halo, elk, element-plus
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:01:39 +00:00
Colby MchenryandClaude Opus 5.5 7a1186471e fix(routes): inline handler calls keep their receivers (#2200)
The Express-style route extractor attributed an inline arrow handler's calls
to the route by bare name - `c.text('Hello')` became `text` - so each
matched whatever same-named symbol the project had: hono's 419 `c.text(...)`
routes called its client's `ClientResponse.text`, `c.html(...)` its `html`
helper, `c.req.parseBody()` a utility `parseBody`. A member call now keeps
its receiver (`c.text`, `userService.lookup`) and resolves like any member
call; a member of an expression (`a.b().c(`) names nothing to follow. Both
the plain and the `router.route(...).get(...)` forms share the scanner.

hono -538 +71 (`Context::text` / `Context::html` where the receiver
resolves; `c.req.*` 3-link chains now stay unresolved like any other
untyped chain); express, react-redux-realworld, trpc, kit, BookStack
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:28:25 +00:00
Colby MchenryandClaude Opus 5.5 1f64e15886 fix(react-router): read links through a route-config object's getHref (#2187)
bulletproof-react writes every link as `<Link to={paths.app.discussion.getHref(id)}>`
and every redirect as `navigate(paths.auth.login.getHref(...))`, so none of its
navigation reached a route. The href reader now follows the config object to
where it is declared (through imports and tsconfig aliases), takes the string or
template the helper returns, keeps a whole-segment `${id}` as a parameter and
drops a hole glued onto a segment (a `?redirectTo=` suffix).

The Link tag pattern also stopped at the `>` of an arrow attribute written
before `to` (`onMouseEnter={() => prefetch(id)}`), which hid those links.

bulletproof-react: 0 -> 11 navigates edges; jira_clone, ecommerce-react,
react-redux-realworld, create-t3-turbo and excalidraw unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:17:06 +00:00
Colby MchenryandClaude Opus 5.5 37bcd5c0ae fix(react-router): nested, lazy and constant routes; per-app tsconfig aliases (#2165)
- Route tables compose nested paths: a child's `path` is relative to the
  path-bearing routes around it (`children` of a data router, nested
  `<Route>` elements), absolute ones reset. `lazy: () => import('./x')`
  (object and JSX) renders the module's default export (`Component` export
  as fallback) via a claimed `lazy-import:` ref. `path: paths.app.root.path`
  keeps its parts on the node's signature and is named in postExtract from
  the constant's object literal (found through the file's import). The
  route keeps its extracted id; `isReactRouterRoute` accepts it. An element
  wrapped in parentheses or a guard (`<ProtectedRoute>`, `<Suspense>`,
  `*Provider`, `*Guard`) renders the first element inside.
- Import aliases: the tsconfig / jsconfig nearest the importing file that
  declares `paths` is tried before the root's, so each monorepo app resolves
  its own `@/*` / `~/*`.

A/B (edges removed / added): bulletproof-react -43/+367 (0 → 9 named,
linked routes; cross-app imports fixed), react-native-reusables -103/+193,
kit -184/+218, trpc -45/+71 (examples' `~/…` now their own), create-t3-turbo
-1/+13; excalidraw, halo, obytes, realworld byte-identical. #1348's nested
fixtures now expect composed paths (`/dashboard/settings`, `/data/prefs`).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:34:11 +00:00
Colby MchenryandClaude Opus 5.5 9269f92dd5 fix(aspnet): FastEndpoints routes, and minimal API paths get a leading slash (#2158)
- FastEndpoints: an endpoint class (`: Endpoint<…>`, `EndpointWithoutRequest`,
  `Ep.…`) declares `Get(…)` / `Post(…)` in `Configure()`; each becomes a route
  linked to the class's own HandleAsync / ExecuteAsync (the bare-member scope
  from #2151 keeps two `List` endpoints apart). A constant path
  (`CreateContributorRequest.Route`) is read in postExtract from wherever the
  class declares it; `$"/{nameof(Project)}s"` reads as `/Projects`.
- `app.MapGet("api/todos", …)` is named `/api/todos`.

A/B: ardalis/CleanArchitecture 0 → 24 routes (+24 edges); eShopOnWeb and
eShop only re-key routes whose path gained a leading slash; realworld and
jasontaylordev/CleanArchitecture byte-identical.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:30:56 +00:00
Colby MchenryandClaude Opus 5.5 3151d5c469 fix(vapor): closure, webSocket and on(…) routes are routes (#2157)
Only `<builder>.METHOD(…, use: handler)` registrations were read. Now also:
- trailing-closure routes, `app.get("hello", ":name") { req in … }`, and
  `app.webSocket("chat") { req, ws in … }` (method WS), `routes.on(.GET, "x")
  { … }` — statement-start only, so `if let v = req.parameters.get("x") {`
  and an HTTP client's `req.client.get("https://…") { … }` don't count;
- `routes.on(.POST, "x", body: .collect(…), use: handler)`, with only
  unlabeled string arguments as path segments.

Routes main → this branch: SwiftPackageIndex-Server 13→34, vapor 6→265
(its tests), swiftchat 0→1, SteamPress 27→27, penny-bot 0→0.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:23:58 +00:00
Colby MchenryandClaude Opus 5.5 a5b0160e5b fix(play): a Play project in a subdirectory is still Play (#2156)
Detection only looked at the root (`build.sbt`, `conf/routes`,
`conf/application.conf`); playframework/play-samples keeps each project's
`conf/routes` in its own directory and had no routes. Any indexed Play routes
file now counts. play-samples 0 → 139 routes (+108 edges); root-level Play
repos are unaffected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:16:46 +00:00
Colby MchenryandClaude Opus 5.5 110208973f fix(rails): routes are read with their namespaces, scopes and nesting (#2155)
- Route files (`routes.draw`, or under config/routes/) are read as a block
  structure: `namespace` / `scope` add path + controller module, nested
  `resources … do` sit under `/:parent_id`, `member` / `collection` add
  `/:id/x` / `/x` actions, `root` and `resource` / `resources` options
  (`only:` / `except:` across lines, `path:`, `controller:`) are honored;
  `do` and `if` / `unless` / `case` / `begin` open frames an `end` closes.
- Detection accepts `gem "rails"` (double quotes), railties, and a Rails
  engine's `config/routes.rb` anywhere (solidus).
- A namespaced `controller#action` only reaches controllers of its own
  module path when any exist — an admin controller that inherits `index` is
  not the API's `ZonesController#index`.

Route→action edges main → this branch: mastodon 221→578, solidus 0→319,
rubygems.org 108→192, maybe 127→189, lobsters 141→196; no non-route edge
lost except solidus's CamelCase refs now resolved by the Rails model
resolver (`Order` → Spree::Order model, was a state-machine module).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:12:36 +00:00
Colby MchenryandClaude Opus 5.5 11cc3d9647 fix(flask): add_url_rule and view_func registrations are routes (#2154)
`bp.add_url_rule('/path', 'name', view_func | positional view, methods=[…])`
and a project's own wrapper that hands a list of paths and a `view_func=`
over (flaskbb's `register_view(auth, routes=[…], view_func=X.as_view(…))`)
now produce route nodes linked to the view. A class-based view is ANY unless
`methods=` says; a function defaults to GET. A call whose path isn't a
string literal (the wrapper's own `add_url_rule(route, …)`) adds nothing.

flaskbb 0 → 103 routes (+104 edges); flask's own tests +27; CTFd, microblog,
bluelog byte-identical.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:03:06 +00:00
Colby MchenryandClaude Opus 5.5 6f798dce6b fix(react-native): Paper requireNativeComponent views reach native; JSX via default imports (#2137)
- `requireNativeComponent('X')` (Paper) now emits the same JS `component`
  node a Codegen spec does, so the Fabric pass links it to the native view /
  manager classes; the extractor also runs on `.js` / `.jsx` spec modules
  (segmented-control's spec is Flow `.js`). react-native-maps' `AIRMap`
  gains its iOS implementation.
- A JSX tag that names no node is the file's DEFAULT import of a module's
  one component: `<RNCSegmentedControlNativeComponent>`, and element-plus's
  test `<Autocomplete>` for `autocomplete.vue` (whose component node is
  `autocomplete`). A named import never takes a barrel's one component.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:49:17 +00:00
Colby MchenryandClaude Opus 5.5 72a9346b12 fix(expo-router): +api files are endpoints; the nearest router manifest decides (#2135)
- `app/blog/og-image/[post]+api.ts` was a screen named
  `/blog/og-image/[post]+api` (evanbacon.dev). An Expo API route is now one
  endpoint per exported HTTP method (`GET /blog/og-image/:post`), bound to
  its handler, the way Next.js `route.ts` files are.
- The per-app framework gate (#2129) took ANY enclosing manifest's
  declaration: react-native-true-sheet's root declares expo-router for its
  example app, so its `docs/` Next.js app got 16 Expo screens (`/layout`,
  `/page`, `/api/search/route`, …). Walking up from a file, the first
  manifest that names any gated framework now decides.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:34:14 +00:00
Colby MchenryandClaude Opus 5.5 67744468d2 fix(resolution): Expo module bindings, imported receivers, TS type-only imports (#2134)
- Expo Modules: a JS call on a `requireNativeModule('N')` binding — typed
  (`<CameraNativeModule>`) or not, local or imported (expo-camera's
  `import CameraManager from './ExpoCameraManager'`) — resolves to module
  N's declared function, Swift first and Kotlin beside it, else to the
  member on the binding's declared type. It went to the same-named static
  method making the call.
- A JS/TS member call on an import binding never picks a method declared in
  the calling file, and ruling those out never manufactures a unique guess.
- `import type { X }` was parsed as a default import named `type` (every
  type-only import bound `type`), and `{ a, type B }` bound `type B`.
- A name the file binds from an out-of-repo package names nothing in the
  project, for every reference kind, and JS/TS fuzzy matching is exact-case:
  trpc's 402 `Record<…>` references went to a `record` property, element-plus's
  121 `mount` imports from @vue/test-utils to a test helper.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:28:28 +00:00
Colby MchenryandClaude Opus 5.5 cec041d241 fix(angular): Nx workspaces, barrels, class-constant paths and root-relative navigate (#2133)
From the router sweep:
- angular-spotify had 12 routes all named `/` and no navigation. Its lazy
  routes are `async () => (await import('@ws/home')).HomeModule` through an
  Nx library's `src/index.ts` barrel, which the mount pass never looked
  through; and every lib is its own `src/`, so each routes file got its own
  route table and no template's `routerLink` found the screen it names.
- jira-clone had 3 routes and no navigation: its issue route is
  `issue/:${ProjectConst.IssueId}` (a class `static readonly`), its root
  redirect lives in `app.routes.ts`, which only mounts, and
  `navigate(['project', 'issue', id])` has no leading `/`.

Now: lazy mounts follow `export … from` barrels (and an NgModule's routing
imports); `(await import(x)).M` is read; route constants may be class
statics (strings or objects), enum members, or live behind a barrel, and a
template-literal path takes constant holes; a routes file that only mounts
contributes its redirects at its mount prefix; the route table is keyed by
the `angular.json` / `nx.json` workspace; and `router.navigate` /
`createUrlTree` without `relativeTo` resolve a bare first command from the
root, as Angular does (template `routerLink` stays relative).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:06:23 +00:00
Colby MchenryandClaude Opus 5.5 b62da73550 fix(sveltekit): route names drop (group) folders and parameter matchers (#2131)
A `(group)` directory shares a layout and is never part of the URL, and a
parameter's `=matcher` checks its value. Kept in the name, shadcn-svelte's
`src/routes/(app)/(layout)/blocks/+page.svelte` was `/(app)/(layout)/blocks`,
so no `goto('/blocks')` or `<a href="/blocks">` ever reached it and its
Screens showed no navigation at all.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 06:50:41 +00:00
Colby MchenryandClaude Opus 5.5 54a1698caa fix(vue-router): admin-template route tables, children and layouts; Nuxt routes only in Nuxt (#2130)
vue-element-admin, vue-admin-template and vben had no routes: their tables
are named arrays (`export const constantRoutes = [...]`, `const routes:
RouteRecordRaw[] = [...]`) or per-module objects (`const tableRouter = {…}`)
handed to `new Router(...)`, and most of their screens are `children`.

The Vue Router reader now reads those tables in any file that builds a
router, imports vue-router or lives in a router/ directory; joins children
onto their parent's path (a parent is a screen only when no child claims
its address and it doesn't redirect); binds a lazy view by the FILE it
imports (all of vue-element-admin's are `…/index.vue`), through an alias the
resolver can't follow by the one file in the app with that path; links each
screen to its parents' components as layouts; and takes `this.$router.push`.

Nuxt's file routes move into their own `nuxt` resolver, detected only in a
Nuxt app: halo's plain-Vue console got 30 made-up screens from its `pages/`
folders. A Nuxt app at the repository root now gets its routes, and a
top-level `pages/index.vue` is `/`, not `/index`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 06:46:26 +00:00
Colby MchenryandClaude Opus 5.5 92e14eda40 fix(extraction): a file-based router reads only its own app in a monorepo (#2129)
create-t3-turbo, tamagui's starter and react-native-reusables keep an Expo
app beside a Next.js app. Expo Router read the Next app's `app/` folder as
its own, so `layout.tsx` became a `/layout` screen, `page.tsx` a `/page`
one, `_components/posts.tsx` and `api/auth/[...all]/route.ts` screens too,
and a shared `packages/app/` library's files became screens as well.

A resolver can now name `appDependencies`; its extractor runs on a file
only when that file's package.json or an enclosing one declares one of
them. When no manifest in the project declares any, detection found the
framework by other evidence and it runs everywhere, as before. Expo Router
names `expo-router`, Next.js `next`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 06:32:33 +00:00
Colby MchenryandClaude Opus 5.5 7a51969fb0 fix(resolution): JS/TS calls never pick a class member by name alone when the code rules it out (#2120)
Found by the README-wide sweep: the "most depended on" lists of popular
repos were topped by members that nothing actually calls.

- A member call on a NAMESPACE import (`import * as z from "zod/v4"`) calls
  one of the module's exports, never some class's method; a receiver
  imported from a package outside the repository names nothing in it.
  matchMethodCall's name/word-overlap strategies no longer run for either.
  zod: `z.string()` had bound 2,640 calls to a test helper's `string`
  getter (Mocker::string topped the hub list with 3,209), `z.toJSONSchema`
  371 to ZodType::toJSONSchema; trpc `z.record` to a generated SDK class.
- A receiver-less JS/TS or Go call cannot reach a PROPERTY, FIELD or enum
  case either (it already excluded methods, #1714/#1857): typeorm's mocha
  `it(…)` bound 2,879 calls to an interface's `it` property and `describe(…)`
  1,368 to a command class's `describe` string field.
- A bare call to a name the file imports from outside the repository
  (`import { useQuery } from '@tanstack/react-query'`, CommonJS
  `require('supertest')`, vitest's `test`) gets no name-matched candidate.
- The React resolver leaves an imported component/hook/context to import
  resolution (framework resolution runs first and picked any same-named
  project hook — trpc tests' `useQuery` landed on a hook nested inside one of
  trpc's factories), and never targets a nested hook; same-file hook first.
- "Outside the repository" is the coordinator's `isOutOfRepoImport`:
  isExternalImport (aliases, workspace members) AND not the repo's own package
  name AND unresolvable by resolveImportPath — so axios's own tests'
  `import { getAdapter } from 'axios'` now resolve to lib/adapters.js instead
  of the .d.ts interface member.

A/B vs main (edge diffs, every change classified): zod −5,228, typeorm
−4,318, express −700 (calls onto the `var request = require('supertest')`
binding), trpc −350/+17, excalidraw −233/+3 (props callbacks onto Props
interface members), hono −150, axios −52/+23 (the self-import upgrade),
bulletproof-react: 156 sites that pointed at ANOTHER app's component/hook in
the three-app monorepo now point at the importing app's own, 16 wrong
cross-app links dropped, 8 imports the React heuristic had guessed right lost
(per-app tsconfig paths don't resolve — separate follow-up); gin/cobra
byte-identical, react-redux-realworld byte-identical.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 04:39:30 +00:00
Colby MchenryandClaude Opus 5.5 e845b1cd10 fix(nestjs): routes carry the app's global prefix and URI version (#2117)
`app.setGlobalPrefix('api')` and `app.enableVersioning({ type:
VersioningType.URI, defaultVersion: '1' })` put every controller under
`/api/v1`, but routes were named by the controller path alone (`GET /user`).
A front end's `this.http.get('/api/v1/user')` then linked to nothing:
Ghostfolio's Angular client had no cross-tier edge into its own NestJS API.

postExtract now reads each `NestFactory.create` bootstrap and renames the
routes of controllers in that app, in NestJS's order:
`/{prefix unless excluded}/{v}{version}/{RouterModule prefix}/{path}`.

- `exclude` paths (plain, `{ path, method }`, `{/*wildcard}`, `(.*)`, `*`)
  skip the prefix and keep their version, as NestJS does.
- A route's version: its own `@Version` (found past decorators with object
  arguments), else the class's `@Version` or `@Controller({ version })`,
  else `defaultVersion`. `VERSION_NEUTRAL` has none; header/media-type
  versioning adds no segment.
- A computed prefix (`config.get(...)`) is not guessed.
- Each app uses its own bootstrap (the longest root above it). A seed script
  that creates the same app without serving it doesn't displace the one that
  configures it, and an app with no config doesn't borrow another app's.
- Only routes whose decorator sits in a `@Controller` are touched, so an
  Express route in the same repo keeps its name.

Ghostfolio: 0 → 103 cross-tier edges (82 of 85 client call sites), no
mismatches. koel, proshop, IceCubes and nest-samples are byte-identical to main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 03:17:35 +00:00
Colby MchenryandClaude Opus 5.5 761611c18e feat(angular): routes, navigation and the component tree for Angular apps (#2113)
An Angular app had no route nodes, no navigates edges and no link from a
template to what it renders — the viewer's Steps tab said "No screens or
endpoints" and there was no Screens tab.

Routes (frameworks/angular-router.ts): every `Routes` array — typed
`Routes` / `Route[]`, `RouterModule.forRoot/forChild`, `provideRouter`, a
routes file's `export default [...]` / `as|satisfies Routes` — walked as
objects. `component` binds by `references` (route-roots takes a class as
a named handler); a lazy `loadComponent` binds to the file's `@Component`
class. `children` join their parent's path; a lazily loaded file's routes
are put under the path that loads them in postExtract (following an
NgModule `loadChildren` to the routing module it imports), idempotently
from the in-file path kept in the qualified name. A route with children
is a layout: a screen only where no child claims its address, and linked
from each screen inside it (`references`, `layout: true`). Paths written
as route constants (`internalRoutes.account.path`, through destructured
locals and `const x + '/:id'`) and `$localize` strings are read from the
constant object. `redirectTo` aliases an address to the screen it sends
the user to; a root `**` redirect answers `/` only. A `matcher` route and
a `**` catch-all are not screens.

Navigation: `router.navigate([...])`, `navigateByUrl`, a guard's
`createUrlTree` / `parseUrl` — a command array (non-literal elements are
holes), a static string, a route constant, a component property holding
one, a function constant called with arguments, `.concat(id)`. A
`relativeTo` navigation, a query-only `navigate([])` and a destination
made only of holes (it would match any route of its length) are left
unresolved.

Templates (angular-template-synthesizer.ts): a component's `templateUrl`
file or inline `template:` is read at synthesis time. `<app-foo>` by
element selector (same app first; two candidates = none) is a `calls`
edge from parent to child (`angular-template`), the edge a navigation in
a child component rides to its screen. `routerLink` / `[routerLink]` and
a `routerLink:` field written in the class (a tab bar's config) are
navigates edges. Gated on an `@angular/core` dependency.

Measured:
- angular-realworld: 10 routes all bound; 11/11 navigation sites
  accounted for (10 resolved, 1 query-only); 31/31 routerLink sites
  resolved; 18 renders.
- Ghostfolio: 84 routes all bound, 0 unresolved constant paths; 27
  navigate resolved, 23 correctly not (8 relative, 9 query-only, 2
  computed, 4 aimed at a matcher route); 162 routerLink edges, every one
  checked against its site; 170 renders.
- ngx-admin (NgModule): 55 routes, 47 bound (8 are @nebular/auth's); no
  navigation calls in the source.
- Controls byte-identical vs main (nodes and edges incl. metadata):
  koel, IceCubesApp, spring-petclinic, proshop. Ghostfolio index time at
  parity.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:43:58 +00:00
Colby MchenryandClaude Opus 5.5 7639c787e4 fix(swift): what a SwiftUI app's viewer showed that was not so (#2109)
Three things IceCubesApp showed in `codegraph ui`:

Steps labelled calls that leave the index by other stacks' names:
`rawValue.data(using: .utf8)` network (25 boxes), Foundation's `Timer`
telemetry, `Calendar` and a SiriKit `intent` device, the app's own
`Notifications` endpoint enum device (expo-notifications' row), and
`viewModel.votes.firstIndex(of:)` a database read (the `\w*Model` row).
Now: a Swift `.data/.upload/.download/.bytes` is network only on a
`*session` receiver; Timer/Calendar/intent are no effect in Swift; a
`viewModel` is not a table (any language); a keychain receiver is
storage ahead of Swift's generic `x.delete` database row. A call whose
receiver is a type the Swift project declares — not an `extension X {}`,
which declares nothing (read through the site reader) — no longer
matches the library-name rows; the model and reply rows, written for
project types, still do.

"Files that run something" listed every view with a `#Preview { … }`:
the macro sits at a file's top level, so its constructions were
module-level edges. Entry points now subtract calls inside a Swift
file's top-level previews (trailing closures included — WidgetKit's
`} timeline: {`), drop a file left with none, and re-rank.

Every `struct X: View` had a one-line `component` twin, and `@main`
apps, view controllers and UIView subclasses a `class` twin, from the
SwiftUI/UIKit resolvers' regex extract(): 188 nodes on IceCubesApp, 184
on mastodon-ios, zero edges into any of them — dead ends in search and
the symbol view. The two extract()s are removed.

IceCubesApp's effect sweep is now URLSession-only network, no
telemetry, no database, keychain storage; its entry files drop from
eleven preview files to its one real script. SwiftPackageIndex's Fluent
database and reply steps are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:05:38 +00:00
Colby MchenryandClaude Opus 5.5 48b1a19b19 fix(vapor): bind a route to the handler on the type it names (#2106)
`use: SearchController.show` kept only `show`, and name matching bound
every controller's `show` route to whichever `show` came first — on
SwiftPackageIndex-Server 7 of 13 routes pointed at another endpoint's
code (Search, HealthCheck, ValidateSPIManifest and ReadyForSwift6's
`show` all at AuthorController.show).

The route's handler ref now keeps the type: `Type@method` (dotted for
nested types, `API.PackageController@get`), `@method` for `self.x` and a
bare `x`, and a `let todos = TodoController()` receiver's type. The Vapor
resolver claims these and resolves on the owner's qualified name:
nested types, handlers declared in `extension Type` in another file, and
`extension API.PackageController` (whose node carries only the last
segment, so its declaration line decides). `self.x` / bare `x` take the
method of the type the route is written in. Of one type's overloads, the
one declared to take a `Request` wins. Two types, or none, leave the
ref unresolved rather than guess.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:50:24 +00:00
Colby MchenryandClaude Opus 5.5 ffced05e3c fix(laravel): name routes by the path a request takes — leading slash, prefix groups, the /api mount (#2099)
Routes were named by the bare literal in the routes file, so BookStack's
api.php showed `GET pages` for `/api/pages` and koel's `Route::prefix('api')`
group showed `GET ping` for `/api/ping`. Beyond the label, a front-end
`fetch('/api/pages')` could never pair with a route recorded as `pages`.

- extract(): `Route::prefix()` chains and `Route::group(['prefix' => …])`
  scope their routes, nested ones in order; a computed prefix is kept as a
  `{prefix}` segment. Paths are joined the way Laravel joins them (leading
  slash, no doubled or trailing slashes). qualifiedName now holds that
  in-file path.
- postExtract(): finds where each routes file is mounted — a group whose
  argument is the file, a require / include / loadRoutesFrom inside a
  group, Laravel 11's `withRouting(api:, web:, apiPrefix:)` — composes the
  prefixes down the include tree, and names every Laravel route from its
  qualifiedName plus its file's prefix (idempotent; a removed mount is
  undone). routes/api.php defaults to `/api` unless something mounts it or
  a `withRouting(using:)` loader might. A file mounted at two prefixes stays
  at its in-file path. Only files in routes/ or naming `routes/` /
  `withRouting(` are read.

On BookStack all 80 api.php routes become `/api/…` (from its
RouteServiceProvider group); on koel all 118 become `/api/…`; no route is
left without a leading slash.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:03:02 +00:00
Colby MchenryandClaude Opus 5.5 fef37765c1 perf(resolution): remove the indexing regressions since v1.6.0 (vscode stall, per-ref rescans) — graph byte-identical (#2072)
* perf(resolution): read JS/TS parameter lists without exponential backtracking

isLocallyBoundJsName (#1759, cd4e65b5) scans a file for a parameter that
binds a bare-called name. Its per-parameter pattern `(type)?(default)?\s*`
let one `name: T = value` text split several ways, so when the name was
absent the search backtracked through every combination. On vscode's
markersModel.test.ts, whose helper takes nine typed, defaulted parameters,
each `suite`/`test` scan took 30-40s — one pool worker held a whole
resolution batch while the rest idled, a 70-400s stall per run.

Each parameter now has exactly one parse: its first non-space character
after the identifier picks the type, default or bare alternative. The
alternatives match exactly the strings the old optional groups did, so the
result is identical for every input (4M generated cases and 5.6k real
vscode file/name pairs agree); the scan is 0.1-0.3ms on that file.

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

* perf(resolution): memoize Python module-file lookups per resolution pass

#2034 (5eaa6fed) sent every Python named import whose path the import
resolver cannot place — stdlib and third-party modules such as
`from unittest import mock` — through findPythonModuleFile, which scans
every `__init__.py` file node for a path suffix. It ran once per ref:
on django that scan alone went from 0.6s to 1.4s of resolution CPU.

The candidates for a module depend only on the module path, so they are
now collected once per resolution context, in the same name-lookup order;
the importing file is still excluded per call, so the first survivor is
the node the scan returned. The memo drops with the other import-resolver
memos. Resolution output on django is hash-identical; single-threaded
resolution CPU there goes from 6.5-6.8s to 4.9s (v1.6.0: 4.5s).

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

* perf(resolution): test sealed-module candidates cheapest-first

isSealedModule (#1719, bffd50e4) masks a candidate file's comments and
strings before asking whether it is a module that exports nothing. Import
refs call it for every same-named candidate across the tree, so on vscode
it masked nearly every TypeScript file once per resolver: 3.3s of masking
in a single-threaded pass, paid again in each pool worker.

The verdict is a pure conjunction, so it now asks the cheap questions
first: the source contains `import`, no node in the file is exported, and
there is no CommonJS export. Masking runs only for files that pass all
three, which in practice are the few that can be sealed. The masker blanks
text and never deletes it, so a file without `import` in its source has
none in its masked code either. Resolution output is hash-identical on
vscode and django.

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

* perf(resolution): scan a class for a field's declared type once per field

matchTsThisFieldCall (#1496, cece0720) resolves `this.<field>.<method>()`
by finding the field's declaration in the enclosing class. It re-split the
whole file, stripped comments line by line and compiled three patterns for
every such call, although the answer depends only on the class and the
field: 16.5s of a 148s single-threaded resolution pass on vscode.

The first declaring line — `: typeof X`, `: X` or `= new X`, in that order
per line — is now memoized per (class node, field), and the last few
classes' stripped lines are kept, since calls arrive file by file. Owners
are still tried in the same order and everything after the declaration
lookup is unchanged. Resolution output is hash-identical on vscode and
django.

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

* perf(synthesis): start cross-tier receiver chains only at a word's start

The cross-tier pass (b1f40c57) scans every non-test JS/TS file whose text
has a `.add(`, `.get(` or `.on(` — nearly all of vscode — with receiver-
chain patterns such as `a.b.c.add('job')`. A chain could begin at any
letter of an identifier, so each letter re-read the dotted chain behind
it; on vscode the pass ran 35s on one pool worker and set the synthesis
phase's wall.

A match that starts mid-word implies one from the word's first letter, and
every match of these patterns ends on a non-word character, so a scan
never resumes mid-word: a lookbehind that rejects those starts skips only
attempts bound to fail. Over all 14,135 vscode JS/TS files the five
patterns return identical matches (positions and groups) before and
after, in 8.8s instead of 32.8s; the pass itself drops from ~9.8s to
~5.6s CPU standalone.

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

* perf(resolution): filter same-named candidates in one pass, cheap checks first

Interface members (#1638) and nested const-bound functions (#1669) made
common names — `editor`, `model`, `dispose` — carry thousands of nodes.
matchByExactName ran ten chained filters over that list for every
reference, copying it once per rule before the source-reading checks ran,
and the method-call strategies copied it again to put the call site's file
first before looking at only the class-like or same-file entries.

Each rule judges one candidate on its own, so the exact-name rules now run
as a single pass with the kind and language checks ahead of the lexical,
sealed-module and local-binding checks; the class strategies narrow to
owner kinds before ordering by file (the same nodes in the same order),
and the object-literal strategy, which keeps same-file holders only,
skips the reordering altogether. Resolution output is hash-identical on
vscode and django.

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

* perf(synthesis): scope language-specific node scans in SQL

Eleven synthesis passes stream every node of a kind and keep only their
language's (Go, Java/Kotlin, XML, native, ArkTS, Nix, Erlang). On a
TypeScript monorepo the Go passes materialized all 113k vscode methods to
find two Go ones — goMethodContains alone went from 0.5s to ~2.9s on the
main thread after interface members (#1638) grew the method count and the
canonical ORDER BY (#1988) landed.

iterateNodesByKindIn adds the language predicate to the same query. Its
ORDER BY (file_path, start_line, id) is a total order, so it yields
exactly the nodes the callers kept, in the same sequence.

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

* perf(synthesis): run the synthesis-site index and source-gate scan beside the passes

Two serial main-thread steps landed with the incremental synthesis refresh
(#1988, #2038). The bulk-edge window rebuilt idx_edges_synthesis_site
before synthesis — its partial predicate runs json_valid over the
metadata of every edge (~1.5M rows, 2.7s on vscode) — and after the merged
insert synthesis re-read every file in the project to record its source
gates (~1.9s). Neither result is read until the index is finished: only
sync consults the index, and the gates depend only on the files.

The window's end now leaves that one index for later, and the resolver
builds it while the pool computes the passes, when the main thread was
only waiting; the source-gate scan runs in the same window. Both still
happen before resolution returns, whatever path synthesis takes, and a
crash in between is healed on the next open as before. On vscode
edge-index-recreate drops from 8.3s to 3.6s of serial time; the graph,
synthesis_inputs and the final index set are identical.

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

* perf(resolution): stop re-reading resolved targets and re-probing missing files

Every resolved reference now re-reads its target node several times —
the kind gate (#1796), the cross-family language gate (#1986), alias
following (#1482) and edge creation — through the query layer's
1,000-entry node cache, while targets recur ~5x (1.46M vscode edges onto
293k nodes). The language gate alone spent ~6s of single-threaded
resolution on vscode fetching whole nodes to read one field, and
fileExists re-probed the same missing candidate paths (every extension of
a `.js` specifier) from every importing file, now under a path.resolve
containment check (#1631).

The resolver keeps its own id → node cache and a memo of filesystem
probes, both in the stable window of its other caches and dropped by
clearCaches; the language gate returns early when the reference's
language has no code family (no target can cross one) and otherwise
memoizes each target's language. Resolution output is hash-identical on
vscode and django.

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

* perf(resolution): cheaper sealed-module and receiver-less call checks

isSealedModule read and decoded a candidate file — its source and every
node — to learn whether anything is exported, though nearly every module
does. It now asks one indexed EXISTS probe first and reads the source only
for files that export nothing (the conjunction is unchanged).

isReceiverLessCall (#1759, #1857) compiled a pattern per call and
end-anchored two more over the line's prefix, which V8 scans from the
start. It now matches the name literally with a sticky `\s*[(<]`, and reads
the prefix backwards: the last non-space character decides whether a
receiver precedes the call, and the word run ending there decides the
keyword exception — exactly what `[.\w$\])]\s*$` and
`\b(?:return|...)\s*$` accepted. Resolution output is hash-identical on
vscode and django.

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

* perf(synthesis): pull passes onto workers heaviest-first; cheaper registry and tier scans

The pooled fan-out handed every pass out at once, so each worker held a
count-based share and a heavy pass finished only with its worker's whole
share (vscode's registry pass ended ~16s in). Workers now pull one pass at
a time, heaviest first; edges still merge by registry index, so the output
is unchanged. vscode synthesis: 14.5s vs 19.6-24.4s.

The registry pass retried its greedy name at every character of every
identifier; both scans now start a name only at an identifier's first
character or a `this.` (a mid-identifier resume after a `.method` dispatch
falls back to the unguarded pattern), and dispatch lines come from a
newline-offset lookup instead of splitting the prefix per match. Checked
match-for-match against the old patterns on every vscode JS/TS file plus
300k fuzzed strings. The tier pass reads a file's nodes only when a site
needs them. Standalone on vscode: registry 5.6s -> 4.5s, tier 5.1s -> 4.4s.

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

* perf(resolution): lean export indexes; no per-name regex compiles in field and local-binding checks

Three per-lookup costs the resolver pool paid once per worker:

- A file's export index decoded every node in the file and read its source,
  for each file an import reached, in every worker. It now reads only the
  exported rows (same `start_line, id` order); the default-export binding
  and names a local `export { a as b }` clause introduces are read on first
  use, from name-targeted rows. findExportedSymbol across vscode's workers:
  12.5s -> 3.7s.
- The `this.<field>` declaration scan compiled three patterns per field of
  every class and walked every class line. Each pattern needs the field as
  a whole `[\w$#]` token, so a class's lines are indexed by token once and
  three sticky patterns, compiled once, are tried at the field's token
  occurrences. Identical on 721k vscode (file, field) pairs; 56.7s -> 1.1s
  standalone.
- isLocallyBoundJsName searched the whole file with four patterns per name.
  Each can only start at a `const`/`let`/`var` or `function`/`class`
  keyword, an `=>`, or the `(` opening a list before the name, so the
  patterns (compiled once per name, sticky) are tried there. Identical on
  198k vscode (file, name) pairs; 68.1s -> 10.7s standalone.

Replaying every vscode (2.19M) and django (270k) reference single-threaded
gives the same result hash before and after.

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

* perf(resolution): prepare kind-filtered edge reads once; memoize the inherited this-member walk

- getOutgoingEdges / getIncomingEdges with a kind filter prepared a fresh
  statement on every call, a third of the read's cost; they are issued per
  node by supertype walks and the member-lookup synthesis passes. Each SQL
  shape is now prepared once per connection (dropped on rebind).
- The post-loop `this.<member>` pass walked the same supertypes for every
  reference, reading each one's contains edges and decoding members one by
  one until a name matched. Nothing is written until the pass ends, so each
  type's supertype edges and each supertype's callable members (by name, in
  edge order) are read once. vscode: 4.1s -> 1.0s.
- Express's mount pass decoded every JS/TS file to look for `.use(`; it now
  checks the raw bytes first (an ASCII needle survives UTF-8 decoding
  unchanged) and decodes only files that have it.

A full vscode init dumps the identical graph before and after.

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

* perf(resolution): fan the next batch out before inserting edges resolution never reads

The batch loop inserted every batch's edges before fanning out the next
one, so the pool sat idle through each insert. That ordering exists for
one reason: later batches walk supertype chains over the `extends` /
`implements` edges earlier batches resolved. Those are the only edges
resolution reads, and the prerequisite phase drains every such ref first,
so after it no batch makes one. A batch without them now fans the next
batch out first and inserts while it resolves: the same writes in the same
order, and nothing the workers read differs. The kinds live in one
constant, shared with the supertype walk.

vscode resolution, three interleaved rounds against the parent commit:
50.3-51.0s vs 56.8-59.1s wall (the pool's wait per batch halves; the
overlapped inserts cost ~7s more CPU). Full vscode and django inits dump
identical graphs before and after, and identical to main.

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

* perf(db): prepare the edge-endpoint existence check once

Every edge insert checks its endpoints with a 500-id IN query, a chunk at
a time, and prepared that statement afresh for each chunk. The batch loop
now runs these inserts on the main thread while the pool resolves, so the
full-size statement is prepared once; only a final partial chunk prepares
ad hoc (as deleteReferencesByRowIds already does). Same SQL, same result.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:57:18 +00:00
Colby MchenryandClaude Opus 5.5 9a96b52991 fix(react-router): bound route scans to their declarations (#1348) (#2011)
* fix(react-router): bound route scans to their declarations (#1348)

Fixed scan windows borrowed paths and components from neighboring or nested routes.
Scan JSX opening tags and data-router objects structurally, pairing only their own attributes and properties.
Add persisted-graph regressions across JavaScript and TypeScript route declarations.

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

* fix(react-router): bound route scans to their declarations (#1348)

Fixed scan windows borrowed paths and components from neighboring or nested routes.
Scan JSX opening tags and data-router objects structurally, pairing only their own attributes and properties.
Add persisted-graph regressions across JavaScript and TypeScript route declarations.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:06:08 +00:00
htarnackiandHubert Tarnacki c3faef260b fix(spring): gate DI convention patterns to Java/Kotlin non-inheritance refs (#1827)
In a polyglot repo the Spring resolver is detected globally (a sibling
Java module with spring-boot in its pom is enough), and its resolve()
then ran on references from every language. Pattern 4's bare-name
fallback ('any [A-Z][a-zA-Z]+ name, prefer /model/ dirs, else ANY
same-named class') could therefore hijack a Scala 'extends ExtCustomer'
to an unrelated same-named test class, corrupting the inheritance graph
that impact analysis walks.

Two gates in front of the DI/convention patterns (1-5):
- skip refs whose language is not java/kotlin - Spring DI conventions
  say nothing about other languages
- skip extends/implements refs entirely - inheriting from a class is
  never a DI injection point; those must resolve via imports/name
  matching

The Spring config-key resolution above the gates is untouched (it
already gates itself). Regression tests cover the hijack case and the
still-working Java DI path.

Co-authored-by: Hubert Tarnacki <hubert.tarnacki@comarch.com>
2026-09-27 12:22:09 +00:00
Colby MchenryandColby McHenry 4453310eef fix(spring): index multi-path mappings and same-file constant paths (#1461) (#1811)
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 20:20:48 -05:00
Colby McHenry 6f4887db80 feat(steps): draw all arms of conditional navigations as separate edges
Adds multi-arm navigation support: when a destination is produced by a conditional, every arm is now drawn as its own edge. Introduces helpers (hrefArms, destinationsForHref) and updates framework resolvers and edge creation to emit multiple navigates edges (via alsoTargets) instead of a single one. Also introduces per-app rooted route tables to avoid cross-app crossings, and updates various resolvers (React Router, TanStack Router, Vue Router, SvelteKit, Vue, and SvelteKit’s linker) and the UI to reflect multiple possible destinations. Tests and docs updated to reflect the new behavior, ensuring the Screens tab shows all possible navigation paths from conditional destinations. This makes navigation visualization more accurate for forked destinations.
2026-08-31 11:34:26 -05:00
Colby McHenryandClaude Fable 5 02430ccc32 feat(steps): a reply that sets no status is a 200; inline Express handlers keep their replies
- effects.ts implicitResponseStatus: a body-sending reply with no status in its chain (res.json / send / render, reply.send, c.json, NextResponse.json, JSONResponse / jsonify / render_template, Rails render, Laravel response()->json) is a 200; a variable status, end, sendStatus and redirects stay as they were
- branch-guards callSiteInTree: a status set by the statement just before the reply (`res.status(202); res.json(user)`) is that reply's — looked back within the block, only a statement that IS the status call counts
- steps.ts: explicit chain/args → set-before → implicit 200
- express.ts: an inline handler's reply calls (`res.status(404).json(…)`, `res.json(user)`) are references at their own line and column instead of framework noise, so the route's own reply box exists
- tests: servers fixture (inline route's 200 beside the service's 404; a 202 set before), ui-effects
- CHANGELOG

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 12:00:04 -05:00
Colby McHenryandClaude Fable 5 bc45e9071d feat(routes): FastAPI prefixes, ASP.NET endpoint groups, wrapped server actions on the Screens tab
- python.ts postExtract: APIRouter(prefix=) and literal include_router(prefix=) composed down the include tree (module import, alias, local); a computed prefix leaves that mount alone; full-stack-fastapi-template 23 routes named by path
- csharp.ts: handler-first MapPost(Handler[, "path"]) under the endpoint-group class, the app's $"/api/{groupName}" head read in postExtract, RoutePrefix honoured; detection covers Endpoints/ files; CleanArchitecture 10 routes
- tier-synthesizer: a type argument between a client call and its parentheses (useSWR<T>(…), ky.get<T>(…)); recorded callee without it
- screens.ts: a file-scope navigation attributed to the value spanning it; a value nothing calls attributed to the functions mentioning it in importing files (request-time source read, bounded); steps.ts lends navigates edges to a value root
- tests: servers fixture (FastAPI prefixed routers, ASP.NET endpoint group end to end), frameworks.test.ts (group form, RoutePrefix), cross-tier (generic useSWR)
- docs: CHANGELOG, plan, playbook rows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-28 14:52:40 -05:00
Colby McHenryandClaude Fable 5 9c6bc23b21 feat(screens): Next.js as a Screens app — pages, route handlers, links and redirects
- frameworks/nextjs.ts (split out of react.ts): App Router app/**/page.tsx and Pages Router pages → routes named by path ((group) stripped, [slug] → :slug, [...all] → :all*), bound to the default export; app/**/route.ts exports → METHOD /api/… endpoints referencing their functions; pages/api → ANY; resolve() claims router.push/replace/prefetch, redirect/permanentRedirect and NextResponse.redirect(new URL(…)) into navigates edges via the Expo href readers, against a Next-only route table gated on the app's root
- next-router-synthesizer.ts: <Link href> and internal <a href> → dashed navigates edges from the component (next-link, registeredAt)
- expo-router.ts: href readers exported; matcher accepts :param / :all* segments
- steps.ts: a Next page's own work fires from page load; a Next page makes the project a web app; {status: 201} read off the call site (branch-guards CallSiteText.status) for response rows
- frameworks/package-deps.ts: nested package.json files probed on disk (getAllFiles lists only sources); Express/React/Expo/Nest detectors use it; routing manifest names constant handlers
- tests: nextjs.test.ts (file→route rules, extract, verbs, end to end with Screens and Steps); frameworks.test.ts Next cases moved to the Next resolver
- docs: CHANGELOG, spec §3.12 frameworks paragraph, CLAUDE.md, synthesis doc, plan P4 built, playbook rows for Next / MERN / Nest channels

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-28 14:40:14 -05:00
Colby McHenryandClaude Fable 5 b1f40c57dd feat(steps): cross-tier channels — a client's fetch onto its own route, queue jobs onto consumers, bus and socket events onto handlers
- resolution/tier-synthesizer.ts: http-client (literal fetch/axios/ky/got/$fetch paths, axios.create baseURL instances, template holes as :params, base-URL holes by a two-segment tail; unique match only), queue-job (BullMQ/Bull add ↔ @Process/@Processor, WorkerHost process, new Worker, queue.process), event-bus (EventEmitter2 emit ↔ @OnEvent with globs; socket emit ↔ @SubscribeMessage / socket.on both ways with tier); channel, tier, callee, registeredAt on every edge; generic transport events never pair; test and generated files never sources; registered before the emitter pass
- steps.ts: crossing() reads tier/channel before languages; an endpoint reached across a tier is a bridge box and a boundary like a screen (through=1 enters it); a channel's call is not also an effect; sites read as written; a Next 'use server' action is a crossing by its directive (when.ts directive); a function-valued constant handler (asyncHandler(...)) is a route root and borrows the file-scope calls and refs within its lines
- express.ts: app.use('/prefix', router) mounts composed onto route names in postExtract (nested, by import or require); chained router.route('/x').get(h).put(h2) extracted, across lines
- frameworks/package-deps.ts: dependencies read from workspace package.json files too (Express, React, Expo Router, NestJS detect)
- routing manifest names constant handlers; e2e/ is a test directory; explore's Flow section labels the new channels
- tests: ui-steps-cross-tier (monorepo fixture: Next client + Express/Nest API), servers test updated for the queue landing
- docs: CHANGELOG, spec §3.13 cross-tier paragraph, CLAUDE.md, callback-edge-synthesis.md, plan P3 built

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-28 14:31:01 -05:00