Skip to content

feat: framework resolvers, synthesis, VB.NET and codegraph context - #338

Draft
sunerpy wants to merge 33 commits into
mainfrom
feat/v0.56-followups
Draft

sunerpy wants to merge 33 commits into
mainfrom
feat/v0.56-followups

Conversation

@sunerpy

@sunerpy sunerpy commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Summary

The v0.56.0 follow-up program: the work split out of v0.55 (OD-18) plus the
items that came after it. This PR stays a draft while the waves land; each
wave is pushed when its local gate passes.

Planned scope:

  • W0 foundations (this push)
  • W7 UI families and framework resolvers (F1-F12, synthesis stage, framework
    resolvers)
  • W8 VB.NET
  • W9 codegraph context, upstream #1617, and the v1.5.0 performance strategies
  • W10 documentation, upstream ledger and release preparation

W0 (pushed)

  • Extraction version 22 → 23 as the first commit, before any change in
    graph meaning, so users rebuild once. upgrade_from_v055 covers the
    v0.55 → v0.56 path (a v0.55 index is Outdated{22} and upgrades to a
    graph equal to a fresh init).
  • Refuse a schema newer than the binary (OD-19) without writing:
    StoreError::FutureSchema from every opening path (sync, daemon catch-up,
    MCP sessions, rebuild), probed WAL-aware before any writable open. The
    recovery text offers uninit only where uninit would succeed.
    uninit --force can still discard such an index.
  • uninit --force beside a leftover tombstone: a current index next to
    the uninit tombstone is now discarded (the tombstone stays, so only init
    rebuilds it), and a newer schema is reported before the tombstone.
  • Rust member-path std gate: no code needed; a probe pins that
    std-typed receivers (PathBuf, &Path) stay unresolved.
  • MCP engine test directories no longer collide on a reused pid.
  • Dependencies: the Cargo updates from build(deps): bump the cargo group across 1 directory with 6 updates #334 except tree-sitter-swift,
    which stays at 0.7.3 (0.7.4 regenerates the grammar; documented in
    docs/grammar-manifest.md).

Verification (W0, HEAD 5cb5f32)

  • make check: 239 suites, 6135 passed, 0 failed, 1 ignored.
  • make pre-ci (pre-push hook): passed, including the viewer bundle byte
    check and the release-archive smoke.
  • Extraction goldens unchanged (scripts/regen-goldens.sh --check); graph
    diffs over the precision corpora identical before and after the dependency
    updates.
  • Benchmark quick check (fd-small, tokio-medium; 32 and 4 threads; 6 runs)
    within budget.

Not in this PR yet

W7-W10. This description is updated as each wave is pushed.

v0.56 changes graph meaning (framework resolvers, synthesis, VB.NET,
grammar bumps), so the extraction version moves once, first, to 23. A
v0.55 index (schema 10, extraction version 22) is Outdated and upgrades
through the existing full migration; v0.55 reports a version-23 index as
Future and writes nothing.

- upgrade_from_v055 covers the paths upgrade_from_v054 covers (CLI sync,
  catch-up, the cold MCP session's daemon upgrade, an older uninit stays
  Uninitialized, a failed upgrade still refuses, interrupted builds,
  tombstones and missing databases are skipped) over a fixture the
  official v0.55.2 linux-musl binary wrote; checksum, attestation and
  commands are in its README. The shared helpers move to
  tests/upgrade_support unchanged.
- upgrade_from_schema_9 downgrades a fresh init of the mini corpus to
  schema 9 instead of planting rows from 3878d52, so its comparison with
  a fresh init checks only the migration. The release fixture keeps a
  third test that checks the in-place migration alone.
A database whose schema_versions records a version above this binary's
was written by a newer codegraph (OD-19). Reads, status, sync, MCP, the
daemon, init and index --force now refuse it before they change a byte,
and the error names both versions. ensure_schema_and_migrations returns
MigrationError::NewerSchema before it creates a table, migrates or
analyzes.

The newer version may sit only in an un-checkpointed write-ahead log.
The writers that tolerate a live log (an incremental sync, the daemon's
recovery of a dead owner's log) and every rebuild entry (before an
explicit init removes a copied project's state slots, before a lockless
root gets its lock, before building is published) decide from
schema_probe: the main file plus the log's committed frames, applied in
memory the way SQLite's recovery does, so no -wal or -shm is created or
folded. A file that is not SQLite is still replaced; a database whose
version cannot be read is refused, and the error says nothing changed.

uninit --force skips the check and stays the way to discard such an
index; the refusal offers it only for the states uninit accepts. status
reports the index as current with initialized false and both versions.
test_engine() returned an engine over /tmp/cg-mcp-engine-{pid}-{seq} and
never removed the directory, so a reused pid opened a stale index and
engine tests failed locally. TestEngine derefs to the engine, removes a
stale directory before it creates one, and on drop closes the engine
before removing its directory.
The member-path std gate deferred on 2026-10-03 needs no code: the v0.55
port of upstream #2169 already keeps a receiver whose declared type the
project does not declare (PathBuf, &Path, std::path::PathBuf) from
binding a project method by name. The probe pins it: index_root.join,
base.join, p.join and q.join stay unresolved and reach no
LineIndex::join, while env.set() still reaches EnvGuard::set.
Takes dependabot #334 except tree-sitter-swift: tree-sitter 0.27.1,
tree-sitter-language 0.1.9, erlang 0.21.0, php 0.25.1, scala =0.26.3,
cfml 0.26.43, and the windows-sys reselection. Every golden, the four
precision corpora and this repository index identically before and after.

tree-sitter-swift stays at =0.7.3 (OD-B4). 0.7.4 fixes the scanner line
behind upstream #2683 (`1UL << 32`, which MSVC folds to 0, so the Windows
build reads `try! Foo()` as a call of `! Foo`), but it also regenerates
the grammar: a call after an infix or prefix operator (`a + b.c()`,
`!a.b()`) comes out as another tree, which the walker does not read as a
member call. Upgrading it lost an exact-match edge in the swift golden and
turned two Alamofire callees into whole expressions; the upgrade waits for
that walker change.

The #2683 probe pins calls after try!, try? and try. It is ignored on
Windows until the upgrade; with the scanner line changed to what MSVC
compiles it to, it fails on Linux the way upstream observed on Windows.
No real Windows host was used. The grammar manifest records the held
version, and languages.md the Windows limitation.
A Current index beside the uninit tombstone (an init that published
Current but could not remove it) refused uninit --force with
CurrentTombstoned. With a schema a newer codegraph wrote, init refuses
that index and names uninit --force, so the refusal pointed at a command
that then failed.

Uninit's corroboration now accepts the tombstone: it discards the index
like any other Current one and keeps the tombstone, so only init
rebuilds it. Covered with and without a newer schema, in the store and
through the CLI.
A Current index beside the uninit tombstone was reported as tombstoned
before its schema was read, so status named `codegraph init` as the
recovery even when a newer codegraph wrote the database, and init then
refused it.

When the tombstone is present, the corroboration now reads the schema
version first (the write-ahead-log-aware probe, which touches no file);
a positively newer version is FutureSchema, which offers uninit --force.
Any other outcome is still CurrentTombstoned, so the same-version
contract and its init recovery are unchanged.
A refusal of a newer codegraph's index offered `uninit --force` for every
state uninit accepts, but on a Current namespace uninit first corroborates
the database's extraction stamp. With the stamp missing, malformed or not
this binary's, the offered command then failed.

The offer now runs the same read-only corroboration uninit runs before
its first change, for the tombstone precedence in the read gate and for
the rebuild prologue. A newer schema with a bad stamp, tombstoned or not,
is refused without the offer.
status's human output for an index a newer codegraph wrote always named
`uninit --force`, even when the refusal it reports does not offer it
because uninit would refuse the index (a missing or foreign extraction
stamp). It now follows the refusal: upgrade, plus the uninit recovery
only where uninit would discard the index.
Moving the shared helpers to tests/upgrade_support left
upgrade_from_v054 and upgrade_from_v055 using ExtractionStatus and Store
only in their unix-only tests, so the Windows Clippy job failed on
unused imports. The import is now cfg(unix).
@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.02620% with 522 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/codegraph-graph/src/branch_guards.rs 90.30% 178 Missing ⚠️
...es/codegraph-resolve/src/frameworks/lazy_loader.rs 85.94% 146 Missing ⚠️
...tes/codegraph-resolve/src/frameworks/route_href.rs 94.38% 51 Missing ⚠️
...s/codegraph-resolve/src/frameworks/react_tables.rs 95.34% 41 Missing ⚠️
...s/codegraph-resolve/src/frameworks/package_deps.rs 94.21% 29 Missing ⚠️
crates/codegraph-resolve/src/frameworks/nestjs.rs 98.29% 17 Missing ⚠️
...odegraph-resolve/src/frameworks/tanstack_router.rs 97.41% 16 Missing ⚠️
crates/codegraph-graph/src/synthesized_hop.rs 95.45% 12 Missing ⚠️
crates/codegraph-resolve/src/frameworks/js_scan.rs 97.80% 10 Missing ⚠️
crates/codegraph-mcp/src/engine.rs 97.92% 4 Missing ⚠️
... and 8 more

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #338      +/-   ##
==========================================
+ Coverage   95.59%   95.63%   +0.04%     
==========================================
  Files         280      297      +17     
  Lines      166519   185044   +18525     
==========================================
+ Hits       159181   176968   +17787     
- Misses       7338     8076     +738     
Files with missing lines Coverage Δ
crates/codegraph-core/src/types.rs 100.00% <100.00%> (ø)
crates/codegraph-graph/src/flow_boundary.rs 93.87% <100.00%> (+0.12%) ⬆️
crates/codegraph-graph/src/graph/mod.rs 94.67% <100.00%> (+<0.01%) ⬆️
crates/codegraph-graph/src/named_symbol_flow.rs 86.50% <100.00%> (+1.52%) ⬆️
crates/codegraph-resolve/src/cfml_scope.rs 97.60% <100.00%> (ø)
crates/codegraph-resolve/src/context.rs 99.35% <100.00%> (+0.04%) ⬆️
crates/codegraph-resolve/src/cpp_namespaces.rs 96.25% <100.00%> (+0.01%) ⬆️
crates/codegraph-resolve/src/cpp_supertypes.rs 86.22% <100.00%> (+0.04%) ⬆️
crates/codegraph-resolve/src/csharp_extensions.rs 95.36% <100.00%> (ø)
crates/codegraph-resolve/src/dart_annotations.rs 92.88% <100.00%> (-0.04%) ⬇️
... and 59 more

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The groundwork the routers of the next commits draw their edges with
(F1, upstream #2129 and #2395). No registered resolver uses the new
pieces yet, so every golden and the express, typescript-large and
self-index graphs are unchanged.

- EdgeKind::Navigates, stored as text like every edge kind. Callers,
  callees, the named-symbol flow and a flow's boundary follow it, as
  upstream's traversal, FLOW_EDGE_KINDS and CONTINUATION_KINDS do.
- A ResolvedRef can carry an EdgeShape (upstream edgeKind, metadata and
  alsoTargets): the edges' kind, the metadata on the target's edge, and
  more targets, each its own edge under the same kind and confidence.
  ResolvedRef::new builds the plain resolution every resolver returned
  before.
- frameworks/package_deps reads the dependencies a project declares in
  its root package.json and up to 24 nested ones: one or two directories
  down, then deeper above JavaScript-family code, never inside
  node_modules. React and NestJS detect through it, so an app whose
  manifest is in frontend/, apps/web/ or src/Web/ClientApp/ is detected
  without its fallback evidence. Upstream package-deps.ts is unchanged
  between b635dd46 and e8ddc4e2.
- A framework can name its app dependencies (appDependencies). When a
  manifest declares one, its extractor runs only on files whose nearest
  package.json naming such a framework names it. The frameworks row a
  sync compares adds a digest of where each gated framework runs, so a
  manifest edit that moves an app re-extracts every file, as a framework
  that came or went does. The JS control inputs digest every manifest
  detection may read, so a sync notices one appearing beside no script.
- The trait gains cross_file_nodes and navigation (upstream #2400 and
  #2425), both None by default; nothing calls them until a router
  implements them.

The sync matrix gains two cases: React declared in an app's manifest,
and in a manifest one directory down above no script. The gating sync
case waits for the first gated framework (W7-07).
An edge a framework resolver made now names that resolver in
metadata.framework (upstream #2159), as a synthesized edge names its
pass in synthesizedBy: a Tauri invoke('greet') says `tauri`, a Godot
autoload call `godot`. The orchestrator adds it to every result a
framework's resolve returns, before the confidence race, so a lower
confidence framework result that wins later carries it too. When a
resolution reaches several targets, only the edge to the first carries
it, as upstream's does; the others keep the metadata the resolver gave
each.

Goldens: godot's four autoload calls and routers' one Vue-resolved call
gain the key; no edge, node or reference comes or goes. The same holds
for this repository (2 edges) and typescript-large (590 edges, all
`react`).
Framework detection reads the manifests one or two directories down from
every indexed file, whatever its language (upstream #2395), and the JS
control-input row digests every such candidate. The watcher re-read that
row only when a JS-family file came or went, so a file of another language
arriving in a new directory left the row behind a fresh index: 39 cases of
the sync matrix (CFML, C, C++, C#, Dart, Go, Java, Kotlin, Liquid, Lua,
Objective-C, PHP, Python, Rust and Solidity edits) failed under the watch
driver. The graph itself converged, since a sync with changes re-detects
the frameworks anyway.

The control-input check now counts any file that comes or goes as touching
the JS kind. typescript_react_dependency_beside_an_added_script covers it
directly: React declared in frontend/package.json is detected once
frontend/build.py is indexed, and the sync records that manifest as a fresh
index does.
…ts; typed styled components

React resolution now follows upstream b635dd46 (#2120's React half,
#2240, #2429, #841, #2475's static half and #2420's wrapper half):

- A component, hook or context the file imports is the import's: a
  UI kit's `<Button>` or @tanstack/react-query's `useQuery` no longer
  binds a project namesake, and a project module's is import
  resolution's to find (#2120, #2475).
- A hook nested inside another function is never a target, and the
  file's own hook comes first (#2120). In a JS/TS module another file's
  hook or context is reached only through an import, never by bare name
  (#2240), and `f?.()` is a bare call, so an optional call no longer
  binds a class member or a property.
- A PascalCase name a .tsx/.jsx file declares as a module-level value
  is that declaration wherever the file uses it: the function the value
  wraps (`observer(function X() {…})`, `forwardRef(Inner) as …`), else
  the declaration, never another app's same-named component (#2420).
  The lazy loader half needs W7-09's reader.
- A PascalCase const declared through forwardRef/memo (bare or `React.`)
  or a styled-components/emotion tag (`styled.x`…``, `styled(Base)`…``,
  typed `styled.x<P>`…``, #2429) is a `component` node, and an inline
  render function's calls are the component's (#841). React's extract no
  longer mints regex component/hook duplicates of tree-sitter nodes; it
  emits routes only, now from .tsx/.jsx files too.

js_scan ports the bracket and string scanner upstream reads declarations
with, iteratively, so deeply nested source cannot exhaust the stack.

Goldens: routers gains the four .tsx page and data-router routes and one
route -> Layout edge; synthesis loses the regex `useCartStore` hook node
(log row in docs/equivalence.md). Precision: express-medium identical;
this repository +4 routes, -8 regex hook duplicates, and a Svelte
`onclose?.()` is now unresolved, as upstream; typescript-large and
LibreChat sampled 20 + 20 rows each, no wrong edge introduced.

LibreChat loses 919 reference sites in the interim: 857 behind the
`~/` alias its client/tsconfig.json declares (Rust reads only the root
tsconfig; upstream #2165's nearest-config aliases come with W7-09) and
62 rendering a module whose default export is `memo(X)` (#2563's
static half, W7-09). Upstream landed #2120 before #2165 the same way.

The sync matrix gains tsx_route_element_body_edit and
tsx_wrapper_value_body_edit; the react_dependency cases observe a data
router's route instead of a regex hook node. Upstream's later react.ts
rows #2737 (PORT) and #2738 (PARTIAL) are left for the next round.
…sioning

In a TS/JS module the provider convention (`*Service`, `*Controller`, …)
now takes only the reference's own class or the convention file beside
it (users.controller.ts -> users.service.ts); anything farther is import
resolution's or name matching's (upstream #2246). Nest's own specs'
`TransientService` no longer reaches an integration app's, and a
decorator call `@Controller('users')` no longer instantiates a PHP class
named `Controller`.

A NestJS route is named by the path a request takes to it (upstream
#2117). post_extract reads each `NestFactory.create` bootstrap and
renames the routes of its app's controllers in NestJS's order,
`/{prefix}/{v}{version}/{RouterModule prefix}/{path}`:

- `exclude` paths (plain, `{ path, method }`, `{/*wildcard}`, `(.*)`,
  `*`) skip the prefix and keep their version;
- a route's version is its own `@Version` (found past decorators with
  object arguments), else its class's `@Version` or `@Controller({
  version })`, else `defaultVersion`; `VERSION_NEUTRAL` has none, and
  header or media-type versioning adds no segment;
- a computed prefix is not guessed; each app takes the bootstrap with
  the longest root above it, a seed script that creates the app without
  serving it does not displace the one that configures it, and an app
  that sets nothing keeps its routes as written;
- only a route whose decorator sits in a `@Controller` is renamed.

A route keeps its id and qualified name. One an earlier pass renamed
that nothing renames any more (a removed global prefix, version or
RouterModule entry) gets the name its file gives it back, so a sync
converges with a fresh index; before, a removed RouterModule entry left
the old name, as colby's sync does. The decorator regexes are compiled
once, since the pass now scans controller files on every sync.

Goldens: servers' NestJS route becomes `GET /api/v1/users/:id` (log row
in docs/equivalence.md). Precision: express-medium and typescript-large
identical; this repository renames that route and drops the PHP
`Controller` guess; nestjs/nest's samples and specs move 43 provider
references from another app's class to the spec's own and resolve 14
more. Its 302 routes without a bootstrap of their own take the `/api` of
the one root-level e2e spec that sets one, by upstream's bootstrapFor.
A sync of typescript-large declaring @nestjs/core takes 2.8% longer
(median of three).

The sync matrix gains four NestJS cases and checks node names. The
Express and cross-tier halves of upstream's test come with W7-19a and
W7-20.
…component

Nuxt's file routes move from the Vue resolver to their own `nuxt`
resolver (upstream #2130), detected by a `nuxt`, `nuxt3` or `@nuxt/kit`
dependency or a `nuxt.config.*` file, and gated to the apps whose
nearest manifest declares one (#2129). A plain Vue app keeps its views
in a `pages/` folder just as often, and those are components a router
config names, not addresses: halo's console no longer makes a route of
each of them. Every path gets a leading `/` before the `pages/`,
`server/api/` and `middleware/` searches, so a Nuxt app at the
repository root finds its endpoints and middleware as a nested one does;
a top-level `pages/index.vue` is `/`, and a `server/api/` file takes a
page's param syntax (`[id]` -> `:id`, `[...slug]` -> `*slug`; `index`
is the folder's path, `/api` at the top).

A page route `calls` the component its own file is (#2267, new
`frameworks/page_component.rs`): the route says so by the file's
basename, and the resolver lands it on that file's component only,
never on a same-named one elsewhere (every `index.vue` is a component
named `index`). The viewer's routes list counts a component as a
route's handler, so a Nuxt page is listed with the component that
serves it.

Goldens: routers' Nuxt index route becomes `/` (its id moves) and its
two pages gain a `framework` edge each (log row in docs/equivalence.md).
Precision: express-medium and typescript-large identical; elk's 59 pages
each reach their own component, its root route is `/` and its four
`server/api/` routes appear; halo's console loses 12 routes that were
components.

The sync matrix gains three Nuxt cases: a `nuxt.config.ts` appearing,
an app's manifest coming to declare Nuxt, and a page's template edit.

The Astro half of #2267 comes with W7-19a, and #2130's Vue Router
admin-template half with W7-11. Upstream's later change to vue.ts
(#2633, 986de95c: `@/`/`~/` imports left to the import resolver) is
left for the next round.
Explore opens with the call path among the symbols the query names
(upstream's Flow section, #365): when two or more named callables are
connected by a chain of three or more, one numbered line per symbol and
one `↓` line per hop come before the header. The search is
named_symbol_flow's, the one the viewer's Flow rides, so of two equally
long routes the Flow takes the one through the symbols the query names
(#2453's explore half).

A hop names the conditions its call runs under (#1655). The new
codegraph-graph branch_guards module reads them from the caller's
current source with the extractor's grammars: the if/else and ternary
arms, switch cases, `&&` / `||` short circuits and catch blocks around
the call site up to the enclosing function, and the early exits before
it, negated, for JavaScript, TypeScript, Swift, Python, Java, Kotlin,
C#, Go, C, C++ and Objective-C callers: `↓ calls (when !busy && ready)`.
A caller file that changed since it was indexed, or one above 256 KiB,
gets no condition rather than one read at a moved line. Upstream's rules
read fwcd's Kotlin grammar and an older Go grammar; this port reads
kotlin-ng's tree (arms without control_structure_body,
return_expression, a bare break or continue as an identifier) and
tree-sitter-go 0.25's statement_list for the same labels, and passes
every guard label of upstream's branch-guards tests. codegraph-graph now
depends on codegraph-extract for the grammars.

A synthesized hop reads as the dynamic dispatch it is, in the Flow and
in the codegraph_node trail: `dynamic: interface → impl
@src/pdf-exporter.ts:5` (new synthesized_hop module, upstream's
synthesized-hop.ts at cd97bb37, #2428). The index has no synthesized
edges until the synthesis stage (W7-14), so only the engine test shows
the tag for now.

Not in this commit: the dynamic-dispatch links among the named symbols
(W7-26), boundary and polymorphic-dispatch announcements for a flow that
does not connect (the port keeps its boundary scan over the explore
seeds), and the spine's weight in file ranking and body windows (W7-25).
Upstream's interface cases of #2453 need interface-impl edges and come
with W7-14. Upstream rows left for the next round: #2750 (f5dfa21f,
PORT, named-symbol-flow.ts), #2769's branch-guards hunk (6bd217a2,
PARTIAL, the Steps status reader this commit does not port), and the
explore rows #2783 (1890b10c, PARTIAL), #2789 (214ef50e, PORT) and #2557
(5da4a41d, PARTIAL).

Goldens and the MCP fixtures are unchanged: the mini fixture's query
`Counter add runDemo` connects no chain of three, because runDemo calls
add directly and the walk reaches it first.
A `NestFactory.create` in a test file (an e2e spec) with no `src/`
segment above it now belongs to the directory above its test folder
(`integration/security/` for `integration/security/e2e/csrf.spec.ts`),
or to the directory holding it. Within one app root, the bootstraps of
the app's entry points decide over a test's: an e2e spec's
`setGlobalPrefix('api')` does not rename the routes `src/main.ts`
serves.

W7-02 ported upstream b635dd46's appRootOf and bootstrapFor as they
are. They put every such spec at the repository root and prefer the
bootstrap that configures the app, so on nestjs/nest one integration
app's spec gave its `/api` to 306 routes of ten other integration apps
and packages/core's specs. With this change only
integration/security's own routes and sample/24's keep `/api`.
express-medium and typescript-large are identical, and no golden
changes. This is a deliberate difference from upstream (KEEP-RUST,
recorded in KNOWN_DIFFS.md). It answers the W7a sub-wave gate's first
round.
W7-04 (9ef9f69) names the resolver on the edges a framework resolution
draws (`metadata.framework`, upstream #2159) without adding a sync case
of its own. The plan requires one for every commit that changes
crates/codegraph-resolve/src.

The matrix harness gains `edge_metadata_after`, which requires an edge
of the fresh index to hold a value under a metadata key. Each sync is
already compared with the fresh index by every metadata key. Two cases
use it:

- tauri_command_body_edit_keeps_the_invoke_edges_resolver_name: an edit
  to a Tauri command's body re-resolves the TypeScript `invoke('greet')`
  at its site, and the edge keeps `framework: "tauri"`.
- tsx_framework_edge_keeps_its_resolvers_name: a `.tsx` file is
  re-extracted, and the React-resolved edge to the function a component
  value wraps keeps `framework: "react"`.

Both cases fail if W7-04's naming is removed (the fresh index lacks the
key). They also fail if the incremental resolution drops the name (the
sync differs from the fresh index). Only test files change.
Next.js gets a resolver of its own, as upstream F3 (9c6bc23b) split
nextjs.ts out of react.ts. React's old rule read any `pages/` or `app/`
file with a default export as a page, kept route groups in the path
(`/(commonLayout)/tools/custom`) and drew no edge.

- A page is a route named by the path a request takes: a `(group)` is no
  segment, `[slug]` is `:slug`, `[...all]` and `[[...all]]` are `:all*`.
  It `calls` its default export. A `route.ts` handler is one
  `METHOD /path` endpoint per exported method, referencing it; a
  `pages/api/` file is `ANY /path`. Layouts, `_app`, configs, tests and
  parallel or intercepting routes are no route.
- Next is detected by a `next` dependency, or a `next.config.*` beside a
  route file, and names `next` as its app dependency (#2129): in a
  monorepo it reads only the apps whose manifest declares it.
- `router.push` / `replace` / `prefetch`, `redirect`,
  `permanentRedirect` and `NextResponse.redirect(new URL(...))` under a
  Next app become a `navigates` edge to the page their href names, with
  the `href` and `navMethod` in its metadata; each arm of a conditional
  is an edge. The href readers and the matcher are upstream's Expo Router
  ones, in a new shared `route_href` module: a string, a template whose
  holes fill parameters, a `{ pathname }` object, a local `const href`.
  A computed href, a relative one or a path no page serves stays
  unresolved.
- When several pages serve one path (two apps of a monorepo with
  `/login`), a call goes to its own app's page; several or none there
  is nothing. Upstream keeps the last page in node order, which a sync
  changes (KEEP-RUST, recorded in KNOWN_DIFFS.md).

The routers golden is rewritten (log row in docs/equivalence.md): the
three Next page routes take upstream's ids and gain their `calls` edges,
`GET /api/auth` appears, and the Dashboard's `router.push('/')` becomes
a `navigates` edge. On Dify's Next.js web the 31 page routes lose their
route-group segments, each calls its own file's component, and 27
navigation calls reach the pages they name; express-medium,
typescript-large and LibreChat are identical, and halo loses the
`/module` route React's rule made of a Vue console file.

Not in this commit: `<Link href>` and `<a href>` (a synthesizer, not a
call), the Screens and Steps builders that read these edges, and the
sync retry for a navigation call whose page appears later in another
file (upstream #2425).
Expo Router gets a resolver of its own (upstream F2, expo-router.ts at
b635dd46, with #2135's API routes).

- A file under `app/` or `src/app/` is a screen named by its path: a
  `(group)` is no segment, `[id]` and `[...rest]` stay as written, an
  `index` is its directory, a platform suffix (`.ios.tsx`) is dropped.
  The route `calls` the file's default export. Layouts and the other `_`
  and `+` files are no screen, except `+not-found`. A `+api` file is one
  `GET /path`-style endpoint per exported method, referencing it (#2135).
- Expo Router is detected by an `expo-router` dependency, or an
  `app/_layout` beside an `app.config.*`, and names `expo-router` as its
  app dependency (#2129).
- `router.push`, `replace`, `navigate` and `dismissTo`, and a project's
  own wrapper named for the verb (`safePush`), become a `navigates` edge
  to the screen their href names, through the shared href readers; a
  relative href is read from the calling screen's directory, and a
  wrapper is remembered as `via`.
- A call matches only the screens of its own Expo app, the longest
  directory above an `app/` that holds it; a file under no Expo app is
  not read, and a path two screens of that app serve stays unresolved
  (KEEP-RUST, recorded in KNOWN_DIFFS.md). Upstream keeps one table for
  the project and reads every file: in the routers corpus, the React
  Router app's `navigate('/')` reached the Expo app's `/` screen.

The Next.js resolver's duplicate-path rule moves into route_href as a
shared `ExactTable`, and the routers' unit tests share one test context.

The routers golden is rewritten (log row in docs/equivalence.md): the
Expo app's `/` and `/settings` screens and its `GET /api/hello`
endpoint appear, and the index screen's `router.push('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/settings')`
becomes a `navigates` edge.
A monorepo app declares its own `paths` (`@/*`, `~/*`) in a tsconfig.json
or jsconfig.json below the repository root, and only the root's aliases
were read: LibreChat's `~/hooks`, bulletproof-react's per-app `@/lib/auth`
stayed unresolved, and since imported names wait for import resolution
(#2120) their calls, tags and imports lost their edges.

- An import tries the aliases of the config nearest the importing file
  first (walking up its directories, the root excluded; a directory
  counts only when its tsconfig.json or jsconfig.json yields aliases, so
  a lone tsconfig.base.json does not), then the root's. Upstream #2165
  (getNearestAliases). Both resolution contexts cache the lookup per
  directory.
- An importing path that is absolute or climbs with `..` never walks.
- Sync re-resolves the JS class when an ancestor directory's
  tsconfig.json or jsconfig.json appears, changes or goes away, or a
  config one of them extends changes.

Precision against the previous commit: express, typescript-large, this
repository, adifyweb and mantis identical; bulletproof-react and LibreChat
lose no site and gain 898 and 6665, each in its own app or package.
React Router routes were read by a v1.0 regex scan that named a nested
route by its own segment (`login`, `c/:conversationId?`) and never read a
route object whose path is a constant or whose page loads lazily, and a
navigation call (`navigate('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/login')`, `history.push(…)`) resolved to
nothing.

- A structural scanner, ported from upstream's `scanRoutes` (the part
  that reads one file), reads `<Route>` elements and the route objects of
  a file that makes a router: nested paths compose, an index route is
  its parent's address, a route around others is the layout they render
  inside (`references` with `layout: true`), `lazy: () => import(…)`
  renders the module's default or `Component` export, an `<Outlet />`
  route renders nothing of its own, and `path={paths.x}` is a route the
  file does not spell out. A path built from a constant
  (`paths.app.root.path`) is named in `post_extract`, which now runs
  before resolution in index, migration, sync and the orphan sweep, as
  upstream's does, so a fresh index and a sync agree.
- A `react-router` resolver turns `history.push`/`replace`,
  `navigate(…)`, `router.navigate(…)` and `redirect(…)` into `navigates`
  edges carrying the href, matched against the routes of the caller's own
  app: a literal, a template with holes, a `{ pathname }` object, a local
  `const href`, `paths.x.getHref(id)`, every arm of a conditional.
  Upstream F4 (#2165, #2187, #2400, #2423, #2436, #2558, #2670).
- KEEP-RUST: a path two routes of one app serve stays unresolved rather
  than taking the first in node order, and a route file nested past 256
  levels yields no routes rather than running the stack out.

Precision against the previous commit: express, typescript-large,
adifyweb and mantis identical; this repository changes only the routers
fixture, as its golden; bulletproof-react gains ten constant-path routes
and three navigation edges; LibreChat's 14 relative routes become 16
composed ones and seven navigation calls resolve. Upstream's first-wins
table would resolve one more call (bulletproof's `navigate('/app')`).
A data-router `lazy` loader that picks the export it renders (`const {
TestTab } = await import('./tabs'); return { Component: TestTab }`) was
read for its first import, so the route rendered the module's default or
`Component` export, and a barrel of tabs has neither. A module-level value
a lazy wrapper loads (`Loadable(lazy(() => import('./Register')))`) was
bound by name, so a tag naming it reached another app's same-named page.

- The loader readers, ported from upstream (#2480), follow what a loader
  hands back: a destructured or `.then`-picked export, `(await
  import('./x')).Page`, a module binding, `Promise.all`, an `async lazy()`
  method, React Router 7's object of lazy properties, the page an
  `element` shows past a guard, or a component of the route file. The
  route's reference names the pick (`import:./x#Page`), which the React
  resolver finds through barrels, only for a React route's reference. A
  loader that hands back `Outlet` renders nothing of its own (#2670); one
  that hands over only a `loader` makes no page.
- A PascalCase value the file declares is, wherever the file uses it, the
  component of the module its lazy wrapper loads, through `Loadable`,
  `dynamic(…, options)`, a loader option or either branch of a
  conditional (#2420, #2452's route half).
- KEEP-RUST: a loader nested past 256 levels links nothing rather than
  running the stack out.

Precision against the previous commit: express, typescript-large, this
repository, adifyweb, bulletproof-react and mantis identical; LibreChat's
two tags naming a same-file `lazy()` value (`<AccountSettings>`,
`<BookmarkNav>`) reach the component of the module it loads instead of the
value itself.
A route table written in one file and handed to the router in another
(`useRoutes(routes)`, `createBrowserRouter([MainRoutes, LoginRoutes])`,
`AppRoutes.map((route) => <Route {...route} />)`) made no routes: the
scanner read only the route objects of a file that makes a router
itself.

- The table pass, ported from upstream (#2400, #2423, #2668, #2672),
  starts from every non-test file that imports React Router and hands a
  table over, finds the table where it is declared, imported or
  re-exported, or as what a function a value calls returns (through
  `useMemo` too), and makes its route objects, and the tables they name,
  routes under the path they are mounted at. They render inside the
  layouts around the mount only when the table is written in the same
  file. A navigation call binds to these routes as to any other.
- The routes are cross-file nodes (`route:<file>:<line>:table:<path>`):
  a pass after the constant naming compares the routes the tables give
  now with the stored ones, before anything resolves, in an index, a
  migration, every sync and the orphan sweep. The edges that reached a
  removed route are resolved again; a sync withdraws its markers only
  when the pass changes the store, and a re-extraction of a file keeps
  the references of the routes in it.
- KEEP-RUST: a route whose name or references changed is deleted and
  inserted again with its new references, where upstream renames it in
  place and keeps the old ones; a file nested past the scan bound hands
  over no table either.
- The table readers blank comments byte for byte, so an offset found
  past a comment holding non-ASCII text still indexes the file.

Precision against the previous commit: express, typescript-large, this
repository, adifyweb and bulletproof-react identical; LibreChat gains
the three routes of `client/src/routes/Dashboard.tsx`, which its router
names among another table's children, and mantis the eight routes of
its `MainRoutes.jsx` and `LoginRoutes.jsx`; nothing removed.
A tag whose name the file imports from a module of the project kept
import resolution's or name matching's target: the module's first
exported function (`getToolIconType` for `<ToolIcon>`), a memo
wrapper's bodyless node, a type alias, or a same-named declaration
elsewhere, and nothing when the module's default export wraps the
component (`export default memo(HoverButtons)`) or an index forwards it
(`export { default as X } from './X'`).

- React binds such a tag, ported from upstream (#2563), to the
  component the module exports under the imported name: through an
  index that forwards it, past a wrapper around it, or through a value
  holding another module's component. Where import resolution already
  reaches that component, its edge stays.
- KEEP-RUST: where the module exports no component under the name, the
  tag keeps import resolution's edge; upstream's `jsx-render` pass
  draws nothing there.
- The module surface a dependent re-resolves on covers the imports a
  module's own exports forward, read the way React reads them (each
  declarator of a statement, past a type annotation; row format 3), so
  an index that re-points what it forwards moves the tags behind it on
  sync, as a fresh index does.

Precision against the previous commit: express, typescript-large, this
repository and mantis identical. Every change is a tsx tag's
`references` edge, now the module's (`framework`, 0.9): LibreChat 181
sites (75 bound where they were unresolved, 47 retargeted, 59 the same
target), adifyweb 1038 (944, 90, 4), bulletproof-react 434 (the same
target); no tag lost its edge.
A TanStack Router app had no routes in the graph: its
`createFileRoute('/posts/$postId')` pages and `createRoute({
getParentRoute, path })` trees were plain calls, and its
`navigate({ to })` and `throw redirect({ to })` calls stayed unresolved.

- A resolver, ported from upstream, extracts one route per address.
  File routes take their path from the literal (`$x` is `:x`, a
  trailing `$` a splat; pathless `_layout` segments and `(group)`
  folders are not in the URL); an index route wins its address over
  the layout around it, and a layout file (`*.route.tsx`, or one that
  renders `<Outlet />`) is no page. Code routes are composed through
  `getParentRoute` within their file. Each route calls its component.
- `navigate`, `redirect`, `router.navigate` and `Route.navigate` bind
  to the route of their own app that the `to` pattern names, as a
  `navigates` edge with `href` and `navMethod`. The resolver is
  detected by any of the five TanStack Router and Start packages and
  runs after React Router and before Next.js, as upstream orders the
  routers.
- KEEP-RUST: a pattern two routes of one app serve stays unresolved,
  as React Router's does; upstream keeps the first in node order, which
  a sync changes.
- KEEP-RUST: a framework result inside a module that exports nothing is
  dropped like a name match's, so a route tree in a `main.tsx` that only
  renders is reached only from that file.

Precision against the previous commit: express, typescript-large,
LibreChat, adifyweb, bulletproof-react and mantis identical; this
repository gains its routers fixture's two TanStack routes. Five
TanStack Router examples gain 27 routes, 26 route-to-component edges
and 3 navigations. One component edge picks another file's same-named
function in the directory, as upstream's component rule does.
A Vue app's routes table was plain data:
`createRouter({ routes: [...] })` and an admin template's
`constantRoutes` named no route, and `router.push({ name: 'login' })`,
the way Vue apps navigate, stayed unresolved.

- A resolver, ported from upstream, reads the routes tables of a file
  that builds a router, imports `vue-router`, writes a `routes` array
  or sits in a `router/` directory: one route per entry, a child's
  path joined onto its parent's (`children` read only from an array
  written in place). A route references its parents' components as
  layouts; a parent that a child's path claims or that redirects is a
  frame, not a page.
- A route calls the component its import takes: a named import the
  module's exported node of that name, through export lists and
  re-exports; a default import or a lazy `() => import(...)` the
  module's default export, a `.vue` file's own component. The module
  is the one the router file's import resolves to, or, through an
  alias import resolution misses, the one file of the app whose path
  ends the same way. KEEP-RUST: the route's reference to an identifier
  is `component:<name>`, which only this resolver answers, so an import
  it cannot follow binds no namesake; upstream takes the module's first
  component-like function and falls back to any component of the name.
- `router.push`, `router.replace` and `this.$router.push` bind by name
  to the route of their own app that the table names so, else by path
  to a config route or a Nuxt page, as a `navigates` edge with `href`
  and `navMethod` (and `by: name`). KEEP-RUST: a destination is read
  only when it is whole (a string, a template whose holes fill whole
  segments, a conditional of them, a local initialized to one, an
  object's own `name` or `path`), so `'/redirect' + fullPath` reaches
  nothing; and a name or a path that more than one entry of an app
  carries stays unresolved, where upstream keeps the first.
- The resolver is detected by `vue-router`, `nuxt` or `nuxt3` and runs
  after Next.js and before Expo Router, as upstream orders the routers.
  A bare `navigateTo` now passes the name filter as a navigation call,
  and the Vue resolver answers it first as a Nuxt auto-import, an edge
  to its own caller, as upstream does at the audited revision.

Precision against the previous commit: express, typescript-large,
LibreChat, adifyweb, bulletproof-react and mantis identical; this
repository gains its routers fixture's Vue routes. Across six Vue apps
and the fixture: 92 routes, 92 route-to-view edges and 62 layout
references, each to the component its entry names, and 15 navigations.
A SvelteKit app's pages were plain components:
`src/routes/+page.svelte` and `src/routes/blog/[slug]/+page.svelte`
named no route, `goto('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/login')` and `redirect(303, '/')` stayed
unresolved, and some `$lib/...` imports reached nothing.

- A `svelte` resolver, ported from upstream, makes each `+page.svelte`
  under a `routes/` directory a route named by its directory: a
  `(group)` folder is no part of the address and a parameter's
  `=matcher` no part of its name (upstream #2131), `[[lang]]` is
  `:lang?` and `[...rest]` is `*rest`. A layout, an error page, an
  endpoint or a loader is no route.
- It resolves what Svelte's and SvelteKit's conventions name: a rune is
  the compiler's, `$app/*` and `$env/*` are SvelteKit's own modules,
  `$lib/x` is the file `src/lib/x` of the importing file's app, and a
  capitalized call names a component. KEEP-RUST: `$lib` is read from
  the importing file's app rather than the repository root, a store
  subscription binds only the store its own component declares, and a
  component call binds only the one component of its name in the
  calling file's directory or, failing that, in the project.
- A `sveltekit-router` resolver binds `goto` (destination first) and
  `redirect` (destination after the status) to the page of their own
  app that serves the path, as a `navigates` edge with `href` and
  `navMethod`. A page whose last segment is `[[optional]]` also serves
  the shorter path unless another page serves it. KEEP-RUST: a
  destination is read only when it is whole, as Vue Router reads it, an
  absolute URL reaches nothing, and a path two pages of one app serve
  stays unresolved, where upstream keeps the first page in node order.
- Both are detected by their dependencies (Svelte also by any `.svelte`
  file) and run after Next.js and before Vue Router, as upstream orders
  them. `<a href>` links and a page's join with its loader are synthesis
  passes, which come later.

Precision against the previous commit: express, typescript-large,
LibreChat, adifyweb, bulletproof-react and mantis identical; this
repository gains its routers fixture's SvelteKit routes. Across three
SvelteKit apps (sveltejs/realworld, shadcn-svelte, svelte.dev): 45
routes, 17 navigations each to the page its destination names, and 48
`$lib` imports that reached nothing now reach their app's file.
An Angular app's routes were plain arrays: `{ path: 'login', component:
AuthComponent }` named no route, and a lazily loaded routes file's
paths were never seen under the path that loads it.

- An `angular-router` resolver, ported from upstream (#2113, #2133),
  reads the routes arrays of each TypeScript file that imports
  `@angular/router`: an array typed `Routes` or `Route[]`, handed to
  `RouterModule.forRoot/forChild` or `provideRouter`, a routes file's
  `export default [...]`, or a `const` that says `as Routes`. Each
  screen is a route that references the component it renders (a
  `component` class through import resolution, the `@Component` class
  a `loadComponent` import picks) and the layouts around it in its file
  (`layout: true`). A route with `children` is a layout, a screen only
  where no child claims its address; a redirect, a `**` path, a custom
  `matcher` and a computed path are none.
- After extraction, each file's routes are renamed under the path that
  lazy-loads the file: a `loadChildren` import, followed through
  barrels (an Nx library's `src/index.ts`) and an NgModule's routing
  modules, with constant segments (`internalRoutes.account.path`, an
  enum member, a class's static field, `$localize`) read from their
  declaration. A file mounted from two places sits under the first in
  path order, as in upstream. The renaming runs in every sync, so an
  edited mount or constant renames routes in files the edit did not
  touch.
- KEEP-RUST: `children` and a constant's nested object are read only
  where written in place, a cycle of mounts settles on each file's own
  chain, and a chain of local aliases is followed at most eight hops.
- Navigation calls, template links and the viewer's Angular parts come
  in later commits.

Precision against the previous commit: express, typescript-large,
LibreChat, adifyweb, bulletproof-react and mantis identical; this
repository gains its routers fixture's two Angular routes. Across five
Angular apps (angular-realworld-example-app, ngx-admin,
jira-clone-angular, angular-spotify, ghostfolio): 168 routes, each
checked against its routes file, and nothing removed.
An Angular app's `this.router.navigate(['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/login'])` reached nothing:
its routes were nodes since the previous commit, but no call was bound
to them.

- The `angular-router` resolver, ported from upstream (#2113, #2133),
  binds `navigate`, `navigateByUrl`, `createUrlTree` and `parseUrl` on
  the router to the route of the caller's app its destination names, a
  `navigates` edge with the `href` and `navMethod`. The destination is
  a command array, a static string, `'/editor/' + slug`, a route
  constant or one whose arrow function builds the commands, a property
  of the calling class, or any of those `.concat(…)`. A computed
  command is a hole, and a first command without a leading `/` is read
  from the root unless it starts with `.`; a call with `relativeTo`
  reaches nothing.
- An app is the directory of an `angular.json` or `nx.json` above a
  routes file, else its conventional app root, and a file under no app
  uses the only app's routes; an app whose routes all hold unread
  constants still counts. A redirect gives its path the route it leads
  to, and `/` that nothing serves takes the root `**` redirect's.
- KEEP-RUST: the table does not depend on node or file order. A path
  two routes of an app serve stays unresolved, a redirect is followed
  through the others at most four hops, and a path whose redirects
  lead to two routes reaches neither; upstream keeps the first in node
  order. `'/editor/' + slug` is read only when the literal ends with
  `/` and one operand follows, a conditional or template is read whole,
  a relative arm or one that leaves the site (`//host/…`) drops out
  before matching, and a route or redirect with a constant the
  cross-file pass could not read is in no table.
- Five sync shapes still diverge from a fresh index (a competitor route
  or redirect in another file, an edited route constant, a route that
  appears where a parked call points, a workspace marker that appears);
  they are kept as seeds for the sync relinking commit.

Precision against the previous commit: express, typescript-large,
LibreChat, adifyweb, bulletproof-react and mantis identical; this
repository's routers fixture gains its Angular navigation. Across five
Angular apps: 40 navigations (angular-realworld-example-app 10,
jira-clone-angular 3, ghostfolio 27), each checked against its call.
A per-piece mutation run of React Router's routes and navigation found
four pieces whose removal left every test green: a navigation needs a
call, a route answers its own app's calls, and both a sync and a
migration name a route a constant spells before anything resolves.

- A `references` reference at a `history.push('/shipping')` site
  resolves to nothing; the same site as a call navigates.
- A route of `frontend/src/App.js` answers the calls of `frontend/`,
  where the existing case had only the repository's root app.
- A sync case edits a routes file whose route a constant names and
  whose `redirect('/app')` names the route: the sync renames the
  re-extracted route before it resolves the call, as a fresh index does.
- An outdated-index migration keeps that call's `navigates` edge and
  equals `index --force`.

Each new assertion fails when its piece is removed and passes on the
code as it is.
The v0.56.0 regeneration log in docs/equivalence.md had no command for
three routers rows (React Router, Vue Router, Svelte and SvelteKit),
and two rows named their commit by a shorter title than the commit's.
The rows now carry `scripts/regen-goldens.sh --write routers` and the
exact titles.

These commits took b635dd46's behavior for an upstream row, or ported
part of a change and left the rest to a later commit, without saying so
in their own message:

- `feat(resolve): React Router routes and navigation` takes React's
  routing at e8ddc4e2 without #2737 (PORT: Pattern 1's
  `ownDeclarations` and `namesType`) and #2738 (PARTIAL:
  `wrappedComponentName`, the nested `React.memo(forwardRef(…))`
  wrapper), and writes #2769's five escapes (PARTIAL) as b635dd46 did.
  The three rows are left for the next round, and with #2738 the one
  `React.memo(forwardRef(…))` site that `fix(resolve): an import alias
  is the nearest tsconfig's` measured. The sync halves of #2560 and
  #2452 go to the commit that relinks routes on sync, and #2132 with
  #2187's Link-tag fix to the link synthesis pass.
- `feat(resolve): Vue Router routes and navigation` keeps the audited
  revision's bare `navigateTo` in a Nuxt app. Upstream #2826
  (58eb57a3, after the audited range) makes it a navigation and adds an
  `ownsReference` hook; the next audit takes it.
- `feat(resolve): Angular routes and the components they render` and
  `feat(resolve): Angular navigation` port angular-router.ts only.
  #2113's template synthesizer and its callback-synthesizer change,
  and #2114 and #2398, go to the template synthesis pass; the viewer
  parts of #2113 (route roots, Screens) and of #2114 and #2398 (Steps)
  to the viewer commit.

The navigation hook's documentation in framework.rs named only Next.js
and Expo Router; every router resolver answers it now.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant