mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* fix(rust): resolve module-qualified calls against the module tree (#2730) A Rust call written with a path (`tools::dispatch(..)`) was captured with only its tail identifier, making it indistinguishable from a bare `dispatch(..)`. The scope-chain walk then resolved the bare name lexically and bound it to whatever `dispatch` was nearest — which, for the common wrapper idiom fn dispatch(..) -> ToolOutcome { tools::dispatch(..) } is the wrapper itself. The graph gained a self-loop, the real cross-module edge never existed, and `impact` reported the callee as unreached: the issue's repository showed its central tool dispatcher as `risk: LOW` with 0 affected processes and both "callers" being `#[cfg(test)]` functions, while still labelling the result `epistemic: "exact"`. Resolve paths the way rustc does, over the module tree rather than the filesystem: - `mod_item` now emits `@declaration.namespace`, so a Rust module is a named definition rather than an anonymous scope region. This mirrors the existing C++ `namespace_definition` capture and lets the shared `tagNamespacePrefixes` pass stamp members with their enclosing module path — that pass needed no changes to start working for Rust. - `module-path.ts` reconstructs the other half of the tree: crate roots are directories holding `main.rs`/`lib.rs`, and a file's module path is its location below that root. A definition's module is its file's module plus any enclosing `mod` blocks. - `crate::`, `self::` and `super::` are prefix transforms on the calling module, not reasons to stop resolving. - The final path segment is looked up as a member of the resolved module, including members it only re-exports. A `pub use` creates no binding on the re-exporting module's own scope, so re-exports are followed through that module's import edges. Resolution runs ahead of the implicit-`this` and scope-chain tiers, so an explicit path outranks a lexical shadow, and returns undefined on an unknown module, a missing member or a tie — leaving the existing chain untouched. The new `ScopeResolver.resolveQualifiedFreeCall` hook is optional and unset for every other language, so this is additive. Fixes the reported case (direct callers 2 -> 3, impacted 2 -> 6, the Agent module now visible) plus multi-segment paths, `super::` paths and `pub use` facades, each of which previously produced a wrong edge. Known limitation, pre-existing and unchanged by this commit: an inline `mod inner { fn dispatch }` and a crate-root `fn dispatch` in the same file collapse to one graph node, because node identity is `<file>:<qualifiedName>` and does not carry the module path. That is a separate defect requiring module-path-qualified node ids and an incremental-schema migration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * test(rust): rebaseline the scope-capture fingerprint for the module-tree captures `mod_item` now emits `@declaration.namespace` and scoped call sites carry `@reference.qualified-name`. Both are additive, so every bench fixture holding a `mod` block or a `Foo::bar()` call gains capture groups, and the corpus grew by the three `rust-2730-*` fixtures. Only the Rust fingerprint moves. The other 14 languages are byte-identical, which is the intended blast radius for a language-local capture change. Scaling stays linear at 1.043, well inside the 1.5 budget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): carry crate identity in qualified module paths (#2741 review H1) A module was identified by its path segments below a crate root, so `crates/alpha/src/tools.rs` and `crates/beta/src/tools.rs` were the same module. A cargo workspace routinely gives several members the same internal module name — `util`, `error`, `config`, `types` are near-universal — and that made qualified resolution do one of two wrong things: - where only one member defined the called name, the call bound ACROSS crates; - where both defined it, the lookup saw two candidates, refused, and handed the site back to the lexical walk that emits the same-name self-loop. The fix for #2730 therefore switched itself off in exactly the workspace layouts it was written for, and #2730's own reported reproduction repository is multi-crate. A module is now `{ crateRoot, segments }` and `sameModule` compares both. Rust has no implicit cross-crate paths — reaching another crate requires naming it — so two modules in different crates are never the same module. Anchored paths (`crate::`, `self::`, `super::`) resolve inside the caller's own crate and inherit its root. Covered by a two-member workspace fixture where both crates define `tools::dispatch` behind a same-name wrapper, plus unit tests for the path arithmetic itself, including the branches no fixture reaches (a file under no crate root, a `super::` chain walking above the crate root). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): count only module members when resolving a qualified call (#2741 review H3) Module membership was inferred from the file path alone, so any callable in the right file counted as a member of the module. A `fn` nested inside another `fn` has the same `filePath`, the same bare `qualifiedName` and no owner, making it indistinguishable from a module-level item: pub fn dispatch() -> usize { 3 } // the real member pub fn wrapper() -> usize { fn dispatch() -> usize { 99 } // counted as a second member dispatch() } Two candidates tie, the lookup refuses, and the call falls back to the lexical walk that emits the same-name self-loop — so an unrelated local helper anywhere in a module silently reinstated #2730 for every qualified call into it. The scope model already draws the line exactly: a module-level item is bound with `origin: 'local'` in its module's own scope, a function-local item binds in the enclosing Block, and an `impl`/trait method binds in the Class scope. Membership is now that binding lookup rather than a path comparison. Inline-`mod` members bind in their Namespace scope rather than the file's Module scope, and reaching it would mean walking every child scope — faulting them back in from disk on the out-of-core path. They keep being identified by the `namespacePrefix` the shared tagging pass stamps on them, which a file-module member never carries. The documented residual is a `fn` nested inside a `fn` inside an inline `mod`, which inherits that prefix; that is strictly smaller than before and costs a refusal, never a wrong edge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): require a use-binding to name a module, not a type (#2741 review H2) Import resolution deliberately strips a trailing symbol segment when probing for a file — "the last segment might be a symbol (function, struct, etc.), not a module. Strip it and try again" (import-resolvers/rust.ts). So `use crate::client::ClientBuilder;` also resolves to `client/mod.rs`. The qualified-call resolver took that at face value and treated the imported TYPE as the module `client`. Rust impl methods carry a bare `qualifiedName`, so `ClientBuilder::new()` was then looked up among `client`'s module members and bound to an unrelated module-level `new` — turning an unresolved site into a false edge, which the module's own contract calls the worse outcome. A binding now has to name the module it resolved to. The edge's `targetExportedName` is the tail of the written path, so comparing it against the resolved module's own tail separates the cases exactly: use crate::tools; tail `tools` module ['tools'] accept use crate:🅰️:b as tools; tail `b` module ['a','b'] accept use crate::tools::{self, Ctx}; tail `tools` module ['tools'] accept use crate::client::ClientBuilder; tail `ClientBuilder` module ['client'] reject Covered by a fixture where `client/mod.rs` deliberately holds both `impl ClientBuilder { fn new }` and a module-level `fn new`, so a regression re-binds to the wrong one, plus a control asserting a genuine `client::new()` module qualifier still resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): give src/bin targets their own crate root (#2741 review) Cargo auto-discovers a binary target for every `src/bin/<name>.rs`. Each is a separate crate with its own `crate::` root, and its submodules live under `src/bin/<name>/`. Only `main.rs` and `lib.rs` established a crate root, so those entry files were folded into the surrounding library and given the invented module path `bin::<name>`. That made `crate::helper()` inside a binary resolve into the LIBRARY's `helper` — and unlike the other findings in this review, this one downgraded an edge the lexical walk had previously resolved correctly, so it made existing output worse rather than merely failing to improve it. `src/bin/<name>.rs` is now its own crate root (as is the `src/bin/<name>/main.rs` directory form), so a binary's modules and the library's modules of the same name are no longer the same module. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): only try a submodule candidate the caller actually declares (#2741 review) The first candidate module was `callerModule ++ qualifier`, yielded before the `use` channel and never checked against anything. That let file layout outrank a real import: with `use crate::b;` in `src/a/mod.rs` and an undeclared — or `cfg`-gated — `src/a/b.rs` present on disk, `b::f()` bound to the sibling file, where rustc resolves it to `crate::b`. A `mod` declaration, inline or file-backed, emits a `Namespace` def bound locally in the declaring scope, so the candidate is now gated on that binding rather than assumed. When the caller does not declare the submodule the candidate is skipped and the `use` and crate-root channels still run, so this only removes guesses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): follow only real re-exports, and refuse on an ambiguous one (#2741 review) Two problems in the re-export channel. A private `use` was followed as though it re-exported. `use crate::tools::helper;` makes `helper` visible INSIDE the module; it does not put it on the module's public surface, so `facade::helper()` does not compile. Only `pub use` does, and finalize already distinguishes them — `reexport` for `pub use`, `named` for a private one. The `alias` kind is now accepted alongside `reexport`, because `pub use x::y as name` is a re-export that was previously ignored entirely. The lookup also took the first matching edge in file-iteration order, which is parse-pool order. Two `cfg`-exclusive facades re-exporting the same name are indistinguishable at this layer, so picking one baked a coin flip into the graph. It now refuses on a genuine tie, consistent with how member lookup already behaves. The pre-existing limitation that only FILE modules are reachable — a `pub use` inside an inline `mod facade { … }` has no `moduleScopeByFile` entry — is now stated in the code. Reaching those would mean walking every child scope and faulting the scope tree back in from disk, which is the cost that index exists to avoid; a miss falls through to the unchanged chain rather than guessing. The regression test deliberately makes the re-exported name globally ambiguous. Without that, the pre-existing unique-global free-call fallback resolves the call on its own and the assertion passes whatever this channel does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * perf(rust): stop type-qualified calls paying for module resolution (#2741 review) The capture carrying `rawQualifiedName` matches every `scoped_identifier` callee, so this hook was reached by `Vec::new()`, `String::from()`, `Self::method()` and every other type-qualified call — the overwhelming majority of `::` calls in real Rust, none of which name a module. Each one ran the full candidate search before returning undefined, and every candidate that missed then walked all of `workspaceIndex.moduleScopeByFile`. Total cost grew as `qualified-call-sites x files`; two independent measurements put per-site cost at 0.117 -> 0.428 ms across 301 -> 1201 files, i.e. linear in workspace size. Two changes: - The module index now carries a flat set of every module segment name in the workspace, and a qualifier whose head matches none of them is rejected before any candidate work. Measured at 0.02 us per rejected call and flat in file count (500 -> 8000 files), against a previously linear per-site cost. - Module scopes are indexed by module identity once per pass rather than rediscovered by scanning every file per candidate. On the out-of-core scope index that scan was worse than CPU: `moduleScopeByFile` fetches through `scopeTree.getScope`, so a full sweep could fault every module scope back in from disk — the pattern `workspace-index.ts` added `exportedCallableByName` to avoid. Given the #2649 and #1871 history this mattered before merge. The captures golden is regenerated for the fixture files added earlier in this series; `emitRustScopeCaptures` itself is unchanged by this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(storage): bump schema versions so the #2730 fix reaches existing indexes Neither invalidation constant was bumped, so the fix did not reach the users who reported the bug. `INCREMENTAL_SCHEMA_VERSION` 22 -> 23. The incremental write set only covers CHANGED files, so a top-up against a pre-v23 index keeps the wrong self-loop — and keeps reporting the callee as unreached — for every unchanged Rust file. The constant's own doc block states this rule, and the precedent is exact: v11 is the same file (`rust/query.ts`) gaining a capture that changes CALLS edges, with the same "force a full re-analyze" contract, and v12 is a second Rust instance. `SCHEMA_BUMP` 30 -> 31. `@declaration.namespace` and `@reference.qualified-name` are parse-time captures, so a warm parse cache replays the old capture set verbatim: `rawQualifiedName` comes back undefined and no Namespace def exists to hang a module prefix on, turning the entire resolution tier into a no-op on unchanged files. `PARSE_CACHE_VERSION` folds in the package version, so a tagged release would have invalidated eventually — but source, dev and CI builds at the same version would not, and the v29 note already warns that relying on someone else's bump is how a change ships with no invalidation at all. Re-checked against origin/main at commit time, as that note instructs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(scope-resolution): let a language opt out of the already-namespaced guard (#2741 review) `tagNamespacePrefixes` skips a def whose `qualifiedName` already equals, or is prefixed by, its enclosing namespace path. That is right for C++ and C#, where the qualified name genuinely carries the namespace. Rust qualified names never do, so the guard fired on a coincidence: in `mod a { pub fn a() }` the member's name equals its module's name, the prefix was skipped, and `moduleOfDef` then reported the member as belonging to the PARENT module. `crate:🅰️:a()` refused, and the def became indistinguishable from a crate-root `fn a` for the module matcher. The guard is now conditional on a `qualifiedNamesCarryNamespace` option that defaults to the existing behaviour, and Rust opts out. The shared pass stays language-neutral — the decision lives with the provider that knows what its own qualified names contain. C++ and C# resolver suites pass unchanged alongside the Rust ones (600 tests). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): refuse a leading :: path instead of reading it as relative (#2741 review) A leading `::` anchors at the extern prelude: `::tools::dispatch()` names the CRATE `tools`, not a module of the current one. The path split filtered the empty leading segment away, which silently reinterpreted the path as relative and let it resolve against a local module that happens to share the name. Extern crates are outside the workspace module tree, so the qualified tier now refuses and leaves the site to the unchanged chain. The regression test asserts the tier does not bind into the local `tools` module, rather than asserting no edge at all: the lexical tier still resolves the bare tail on its own, and that behaviour is not what this change governs. Asserting an empty edge list would have been testing a different tier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * refactor(rust): reuse the canonical callable predicate and drop dead re-exports (#2741 review) `CALLABLE_TYPES` was a local copy of the set behind `isOverloadableCallable` in `utils/callable-labels.ts`. Two copies of the same set drift: extending the canonical one with a new callable kind would silently leave qualified calls of that kind unresolved here, with nothing to catch it. Use the shared predicate. The trailing `export { moduleOfFile, moduleOfDef }` and `export type { ScopeResolutionIndexes }` were commented as being "for the resolver's unit tests". No test imports them: the only importer of this module anywhere in src or test is `rust/scope-resolver.ts`, which takes just `resolveRustQualifiedFreeCall`. Both functions are already exported from `module-path.ts` (where the new unit tests take them from), and `ScopeResolutionIndexes` is canonically exported from `model/scope-resolution-indexes.ts`. Removed rather than left as surface that implies a contract it does not have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * test(rust): rebaseline the scope-capture fingerprint with the correct prior hash The rebaseline note added with the original fix cited `Prior 655aed01…`, which was two rebaselines stale — it predates both #2604 and #2714. The true pre-PR value on the base commit is `7f1240b3…`. CI could not catch it: the gate compares the live fingerprint against the stored one and never reads the prose, so the audit chain these notes exist to provide was broken with nothing to flag it. The note now carries the correct prior value, and the fingerprint is regenerated for the fixtures this review series added. Scaling 1.061, well inside the 1.5 budget; fixture_count 196; the other 14 languages remain byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * test: move the schema-version pin to 23 `call-summary-schema-version.test.ts` asserts the exact value of `INCREMENTAL_SCHEMA_VERSION` and enumerates which stamped versions the incremental reuse gate accepts. It moves with every bump by design — that pin is what stops an id- or edge-changing commit shipping without invalidation. Updated for the bump to 23, with the pre-v23 case added to the reuse-gate table: a v22 index predates Rust module-qualified call resolution, so every unchanged Rust file would keep the same-name self-loop and keep reporting the real callee as unreached. Caught by CI rather than locally, because the earlier sweeps in this series covered `test/integration/resolvers/` and `test/unit/scope-resolution/` only — the pin lives outside both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
79ff44dfa9
commit
df06529950
45 changed files with 1764 additions and 139 deletions
|
|
@ -47,7 +47,7 @@
|
|||
"_rebaselined_2563_instance_ownership": "#2563: csharp-using-static adds same-file ownership, local-function, overload, partial-class, and cross-namespace same-name coverage. Prior 75cf380209fa7d1a8a3ec873be1a9424b4e5173be0b08234c2291e8521a9b3c1 -> e05dc27456bde8175948586c9e7689033a378fa40e9ca4ce78cce41fbea0f2f8; scaling 1.058 < 1.5."
|
||||
},
|
||||
"rust": {
|
||||
"fingerprint": "7f1240b38457468f06b7931e0c2c578f218f922774d0dc7e2ee6ef3b08d4d689",
|
||||
"fingerprint": "90fda086a4e13aa069a5981f63ed58ab1c71f1ed3da5e1480a080e1992b0d3e5",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_dyn_trait_object_2604": "#2604: RUST_SCOPE_QUERY now captures function_signature_item (abstract trait methods, no body) as a scope + declaration, so a &dyn Trait receiver can dispatch a CALLS edge to the trait's own method. Additive capture shift across every bench fixture with a required trait method. Prior df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29 -> f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846; scaling 1.033 < 1.5.",
|
||||
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c -> df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29; scaling 1.065 < 1.5.",
|
||||
|
|
@ -55,7 +55,8 @@
|
|||
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) \u2014 legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical. | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
|
||||
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED \u2014 @declaration.macro/@reference.macro + MacroRegistry \u2192 USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures \u2014 pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f.",
|
||||
"_rebaselined_import_disambiguation_2514": "#2514: added rust-import-* and rust-dup-* fixtures under lang-resolution for the range-binding ambiguity latch + import-disambiguated resolution (for-loops / struct destructuring across explicit/aliased/glob use imports). emitRustScopeCaptures is unchanged; the corpus fingerprint shifts purely because the fixture set grew (130 -> 174). Prior f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846 -> 655aed01cf1b6b84fa0c64d48dfb2526ecb67f47d90f0a91edabacd269a212db; scaling 1.06 < 1.5.",
|
||||
"_rebaselined_self_type_binding_2714": "#2714: a Rust `Self` type binding now records the enclosing impl's type instead of the literal 'Self'. `let fresh = Self { .. }` inside `impl User` binds `fresh: User`; recorded verbatim it bound `fresh: Self`, which resolves to nothing. The type-env channel already substituted this (type-extractors/rust.ts findEnclosingImplType); the scope-resolution channel did not, so the two disagreed. The gap was invisible while lookupCore Step 1 still walked the lexical chain for NAMED receivers \u2014 the impl scope binds the method by name, so fresh.validate() resolved by accident \u2014 and became a lost CALLS edge when #2714 stopped that walk. Only the rust fingerprint moves; the other 14 languages are byte-identical."
|
||||
"_rebaselined_self_type_binding_2714": "#2714: a Rust `Self` type binding now records the enclosing impl's type instead of the literal 'Self'. `let fresh = Self { .. }` inside `impl User` binds `fresh: User`; recorded verbatim it bound `fresh: Self`, which resolves to nothing. The type-env channel already substituted this (type-extractors/rust.ts findEnclosingImplType); the scope-resolution channel did not, so the two disagreed. The gap was invisible while lookupCore Step 1 still walked the lexical chain for NAMED receivers \u2014 the impl scope binds the method by name, so fresh.validate() resolved by accident \u2014 and became a lost CALLS edge when #2714 stopped that walk. Only the rust fingerprint moves; the other 14 languages are byte-identical.",
|
||||
"_rebaselined_module_tree_2730": "#2730 + #2741 review: RUST_SCOPE_QUERY captures mod_item as @declaration.namespace (a Rust module is an item, mirroring the C++ namespace_definition capture) and tags scoped call sites with @reference.qualified-name so the written path survives to resolution. Both are additive captures: every bench fixture holding a mod block or a Foo::bar() call gains groups, and the corpus also grew by the rust-2730-* fixtures added for the fix and its review (workspace-crates, type-qualified, gaps, samename-wrapper, crate-layout). Prior 7f1240b38457468f06b7931e0c2c578f218f922774d0dc7e2ee6ef3b08d4d689 -> 90fda086a4e13aa069a5981f63ed58ab1c71f1ed3da5e1480a080e1992b0d3e5; scaling 1.061 < 1.5; fixture_count 196. Only the rust fingerprint moves; the other 14 languages are byte-identical. The earlier revision of this note cited 655aed01... as the prior value, which was two rebaselines stale (it predates #2604 and #2714); the CI gate compares live fingerprints, not this prose, so nothing caught it."
|
||||
},
|
||||
"php": {
|
||||
"fingerprint": "4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
export function resolveRustImportInternal(
|
||||
currentFile: string,
|
||||
importPath: string,
|
||||
allFiles: Set<string>,
|
||||
allFiles: ReadonlySet<string>,
|
||||
): string | null {
|
||||
let rustPath: string;
|
||||
|
||||
|
|
@ -63,7 +63,10 @@ export function resolveRustImportInternal(
|
|||
* Tries: path.rs, path/mod.rs, and with the last segment stripped
|
||||
* (last segment might be a symbol name, not a module).
|
||||
*/
|
||||
export function tryRustModulePath(modulePath: string, allFiles: Set<string>): string | null {
|
||||
export function tryRustModulePath(
|
||||
modulePath: string,
|
||||
allFiles: ReadonlySet<string>,
|
||||
): string | null {
|
||||
// Try direct: path.rs
|
||||
if (allFiles.has(modulePath + '.rs')) return modulePath + '.rs';
|
||||
// Try directory: path/mod.rs
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { isClassLike, populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import {
|
||||
isClassLike,
|
||||
populateClassOwnedMembers,
|
||||
tagNamespacePrefixes,
|
||||
} from '../../scope-resolution/scope/walkers.js';
|
||||
|
||||
/**
|
||||
* Populate `ownerId` on Rust method defs.
|
||||
|
|
@ -21,6 +25,16 @@ import { isClassLike, populateClassOwnedMembers } from '../../scope-resolution/s
|
|||
export function populateRustOwners(parsed: ParsedFile): void {
|
||||
populateClassOwnedMembers(parsed);
|
||||
populateRustImplOwners(parsed);
|
||||
// #2730: tag defs declared inside `mod` blocks with their enclosing module
|
||||
// path (`inner`, `a.b`) on the `namespacePrefix` sidecar. Rust modules are
|
||||
// Namespace scopes owning a Namespace def (see the `mod_item` capture), so
|
||||
// the shared C++-era pass applies unchanged. Qualified call resolution reads
|
||||
// this to tell `inner::dispatch` apart from a same-named crate-root `fn`.
|
||||
// Rust `qualifiedName`s never encode the enclosing module, so the shared
|
||||
// "already namespaced" guard must be off: with it on, `mod a { pub fn a() }`
|
||||
// matched by coincidence and the member was left looking like it belonged to
|
||||
// the parent module (#2741 review).
|
||||
tagNamespacePrefixes(parsed, { qualifiedNamesCarryNamespace: false });
|
||||
}
|
||||
|
||||
function populateRustImplOwners(parsed: ParsedFile): void {
|
||||
|
|
|
|||
249
gitnexus/src/core/ingestion/languages/rust/module-path.ts
Normal file
249
gitnexus/src/core/ingestion/languages/rust/module-path.ts
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
/**
|
||||
* Rust module paths — the module tree, expressed over the scope model.
|
||||
*
|
||||
* rustc resolves `a::b::dispatch` against a MODULE TREE, not a file tree: `mod`
|
||||
* is an item with its own `DefId`, and a path's leading segments name modules in
|
||||
* the *type* namespace (which is why a same-named `fn` can never shadow them —
|
||||
* functions live in the value namespace). GitNexus models the same tree in two
|
||||
* halves, and this module joins them:
|
||||
*
|
||||
* - **Inline modules** (`mod inner { … }`) are `Namespace` scopes owning a
|
||||
* `Namespace` def, so the shared `tagNamespacePrefixes` pass stamps members
|
||||
* with `namespacePrefix = 'inner'` / `'outer.inner'`.
|
||||
* - **File modules** (`mod tools;` loading `tools.rs` or `tools/mod.rs`) leave
|
||||
* no in-file marker at all — the module path lives in the FILE PATH relative
|
||||
* to the crate root. That half is reconstructed here.
|
||||
*
|
||||
* ## Module identity carries its crate
|
||||
*
|
||||
* A module is `{ crateRoot, segments }`, never bare segments. A cargo workspace
|
||||
* routinely gives several members the same internal module name — `util`,
|
||||
* `error`, `config`, `types` are near-universal — and identity by segments alone
|
||||
* makes `crates/a/src/tools.rs` and `crates/b/src/tools.rs` the same module. That
|
||||
* mis-binds a call across crates when only one defines the member, and (worse)
|
||||
* ties the lookup when both do, so qualified resolution refuses and the call
|
||||
* falls back to the lexical walk that #2730 exists to prevent. Rust has no
|
||||
* implicit cross-crate paths: reaching another crate requires naming it, so two
|
||||
* modules in different crates are never the same module.
|
||||
*
|
||||
* Everything here is pure path arithmetic over the workspace file set; no I/O.
|
||||
*/
|
||||
|
||||
/** Files that make their directory a crate root (`crate::` anchors here). */
|
||||
const CRATE_ROOT_FILES = new Set(['main.rs', 'lib.rs']);
|
||||
|
||||
/** A module file whose name does NOT contribute a path segment. */
|
||||
const MODULE_DIR_FILE = 'mod.rs';
|
||||
|
||||
/**
|
||||
* A module's full identity: the crate it belongs to, plus its `::`-path inside
|
||||
* that crate. `segments` is empty for the crate-root module itself.
|
||||
*/
|
||||
export interface RustModule {
|
||||
/** Crate-root directory (`src`, `crates/noob/src`, or `''` at repo root). */
|
||||
readonly crateRoot: string;
|
||||
readonly segments: readonly string[];
|
||||
}
|
||||
|
||||
export interface RustModuleIndex {
|
||||
/** Crate-root directories, longest first, so nested crates win. */
|
||||
readonly crateRoots: readonly string[];
|
||||
/**
|
||||
* Every module name that exists anywhere in the workspace, as a flat set of
|
||||
* single segments. Used only as a fast negative filter: a qualifier whose head
|
||||
* is not in here cannot name a workspace module, so type-qualified calls
|
||||
* (`Vec::new()`, `String::from()`) are rejected before any candidate search.
|
||||
*/
|
||||
readonly moduleNames: ReadonlySet<string>;
|
||||
/**
|
||||
* Entry files that are a crate root in their own right rather than a module of
|
||||
* the surrounding crate — `src/bin/<name>.rs` auto-discovered binary targets.
|
||||
* Maps the entry file to the directory its own submodules live under.
|
||||
*/
|
||||
readonly standaloneRootFiles: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index the workspace's crate roots: every directory holding a `main.rs` or
|
||||
* `lib.rs`. A cargo workspace has one per member (`crates/noob/src`), a plain
|
||||
* package exactly one (`src`). Longest-first ordering makes `moduleOfFile` pick
|
||||
* the innermost enclosing crate for nested layouts.
|
||||
*
|
||||
* Cargo also auto-discovers a binary target per `src/bin/<name>.rs`. Each is a
|
||||
* separate crate with its own `crate::` root, and its submodules live under
|
||||
* `src/bin/<name>/`. Treating those files as ordinary modules of the library
|
||||
* invented the module path `bin::<name>`, which made `crate::helper()` inside a
|
||||
* binary resolve into the LIBRARY — downgrading an edge the lexical walk had
|
||||
* previously got right (#2741 review).
|
||||
*/
|
||||
export function buildRustModuleIndex(allFilePaths: ReadonlySet<string>): RustModuleIndex {
|
||||
const roots = new Set<string>();
|
||||
for (const filePath of allFilePaths) {
|
||||
const slash = filePath.lastIndexOf('/');
|
||||
const base = slash === -1 ? filePath : filePath.slice(slash + 1);
|
||||
if (!CRATE_ROOT_FILES.has(base)) continue;
|
||||
roots.add(slash === -1 ? '' : filePath.slice(0, slash));
|
||||
}
|
||||
|
||||
// Auto-discovered binary targets: `<crateRoot>/bin/<name>.rs`, and the
|
||||
// directory form `<crateRoot>/bin/<name>/main.rs` (already a root above).
|
||||
const standaloneRootFiles = new Map<string, string>();
|
||||
for (const filePath of allFilePaths) {
|
||||
if (!filePath.endsWith('.rs')) continue;
|
||||
const slash = filePath.lastIndexOf('/');
|
||||
if (slash === -1) continue;
|
||||
const dir = filePath.slice(0, slash);
|
||||
if (!dir.endsWith('/bin')) continue;
|
||||
const parent = dir.slice(0, -'/bin'.length);
|
||||
if (!roots.has(parent)) continue;
|
||||
const own = filePath.slice(0, -'.rs'.length);
|
||||
standaloneRootFiles.set(filePath, own);
|
||||
roots.add(own);
|
||||
}
|
||||
|
||||
const crateRoots = [...roots].sort((a, b) => b.length - a.length);
|
||||
|
||||
// Flat set of every module segment name in the workspace, for the negative
|
||||
// filter. Built from the same single pass over file paths.
|
||||
const moduleNames = new Set<string>();
|
||||
const index: RustModuleIndex = { crateRoots, standaloneRootFiles, moduleNames: new Set() };
|
||||
for (const filePath of allFilePaths) {
|
||||
const module = moduleOfFile(filePath, index);
|
||||
if (module === undefined) continue;
|
||||
for (const segment of module.segments) moduleNames.add(segment);
|
||||
}
|
||||
|
||||
return { crateRoots, standaloneRootFiles, moduleNames };
|
||||
}
|
||||
|
||||
/**
|
||||
* Could this qualifier name a module that exists in the workspace?
|
||||
*
|
||||
* A negative answer is authoritative and cheap: if the head segment matches no
|
||||
* module name anywhere, no candidate channel can resolve it. Anchors keep their
|
||||
* meaning (`crate::`/`self::`/`super::` are relative to the caller, so the head
|
||||
* to test is the first non-anchor segment); an all-anchor qualifier (`self::f()`)
|
||||
* names the caller's own module and is always worth trying.
|
||||
*/
|
||||
export function couldNameAModule(qualifier: readonly string[], index: RustModuleIndex): boolean {
|
||||
for (const segment of qualifier) {
|
||||
if (segment === 'crate' || segment === '$crate' || segment === 'self' || segment === 'super') {
|
||||
continue;
|
||||
}
|
||||
return index.moduleNames.has(segment);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module identity of the file itself: its crate root plus the `::`-segments
|
||||
* below it.
|
||||
*
|
||||
* src/main.rs → { src, [] } (crate root module)
|
||||
* src/tools.rs → { src, ['tools'] }
|
||||
* src/a/mod.rs → { src, ['a'] }
|
||||
* src/a/b.rs → { src, ['a', 'b'] }
|
||||
* crates/noob/src/tools/mod.rs → { crates/noob/src, ['tools'] }
|
||||
*
|
||||
* Returns `undefined` for a file under no known crate root — the caller then has
|
||||
* no module identity to reason about and must refuse rather than guess.
|
||||
*/
|
||||
export function moduleOfFile(filePath: string, index: RustModuleIndex): RustModule | undefined {
|
||||
// A `src/bin/<name>.rs` entry file is its own crate root, not a module of the
|
||||
// surrounding library.
|
||||
const standalone = index.standaloneRootFiles.get(filePath);
|
||||
if (standalone !== undefined) return { crateRoot: standalone, segments: [] };
|
||||
|
||||
for (const crateRoot of index.crateRoots) {
|
||||
const prefix = crateRoot === '' ? '' : `${crateRoot}/`;
|
||||
if (crateRoot !== '' && !filePath.startsWith(prefix)) continue;
|
||||
const rel = filePath.slice(prefix.length);
|
||||
if (!rel.includes('/') && CRATE_ROOT_FILES.has(rel)) return { crateRoot, segments: [] };
|
||||
const segments = rel.split('/');
|
||||
const last = segments.pop();
|
||||
if (last === undefined) return undefined;
|
||||
if (last !== MODULE_DIR_FILE) {
|
||||
if (!last.endsWith('.rs')) return undefined;
|
||||
segments.push(last.slice(0, -'.rs'.length));
|
||||
}
|
||||
return { crateRoot, segments };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Full module identity of a definition: its file's module plus any `mod` blocks around it. */
|
||||
export function moduleOfDef(
|
||||
filePath: string,
|
||||
namespacePrefix: string | undefined,
|
||||
index: RustModuleIndex,
|
||||
): RustModule | undefined {
|
||||
const fileModule = moduleOfFile(filePath, index);
|
||||
if (fileModule === undefined) return undefined;
|
||||
if (namespacePrefix === undefined || namespacePrefix === '') return fileModule;
|
||||
return {
|
||||
crateRoot: fileModule.crateRoot,
|
||||
segments: [...fileModule.segments, ...namespacePrefix.split('.')],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a written path's leading segments to a module, from the calling
|
||||
* module. Mirrors rustc's anchor handling:
|
||||
*
|
||||
* crate::a::b → the caller's OWN crate, segments ['a','b']
|
||||
* self::a → callerModule ++ ['a']
|
||||
* super::a → callerModule[:-1] ++ ['a']
|
||||
* a::b → relative; the caller resolves this against the candidate
|
||||
* channels (child module, `use` binding), so it is returned
|
||||
* as-is for the caller to try in context.
|
||||
*
|
||||
* An anchored path always stays inside the caller's crate — `crate::` names the
|
||||
* current crate, and `super::`/`self::` are relative to it — so the resolved
|
||||
* module inherits `callerModule.crateRoot`.
|
||||
*
|
||||
* `super` chains (`super::super::x`) are consumed left to right. Returns
|
||||
* `undefined` when the chain walks above the crate root — an invalid path that
|
||||
* must not silently resolve to something else.
|
||||
*/
|
||||
export function resolveAnchoredModulePath(
|
||||
qualifier: readonly string[],
|
||||
callerModule: RustModule,
|
||||
): { readonly module: RustModule; readonly anchored: boolean } | undefined {
|
||||
if (qualifier.length === 0) return undefined;
|
||||
const crateRoot = callerModule.crateRoot;
|
||||
|
||||
const head = qualifier[0];
|
||||
if (head === 'crate' || head === '$crate') {
|
||||
return { module: { crateRoot, segments: qualifier.slice(1) }, anchored: true };
|
||||
}
|
||||
if (head === 'self') {
|
||||
return {
|
||||
module: { crateRoot, segments: [...callerModule.segments, ...qualifier.slice(1)] },
|
||||
anchored: true,
|
||||
};
|
||||
}
|
||||
if (head === 'super') {
|
||||
let i = 0;
|
||||
const base = [...callerModule.segments];
|
||||
while (i < qualifier.length && qualifier[i] === 'super') {
|
||||
if (base.length === 0) return undefined; // above the crate root
|
||||
base.pop();
|
||||
i++;
|
||||
}
|
||||
return { module: { crateRoot, segments: [...base, ...qualifier.slice(i)] }, anchored: true };
|
||||
}
|
||||
return { module: { crateRoot, segments: [...qualifier] }, anchored: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity comparison for two modules. The crate root participates: two modules
|
||||
* with the same internal path in DIFFERENT crates are different modules, and
|
||||
* conflating them is what let a workspace mis-bind or refuse (#2730 review H1).
|
||||
*/
|
||||
export function sameModule(a: RustModule, b: RustModule): boolean {
|
||||
return (
|
||||
a.crateRoot === b.crateRoot &&
|
||||
a.segments.length === b.segments.length &&
|
||||
a.segments.every((seg, i) => seg === b.segments[i])
|
||||
);
|
||||
}
|
||||
445
gitnexus/src/core/ingestion/languages/rust/qualified-call.ts
Normal file
445
gitnexus/src/core/ingestion/languages/rust/qualified-call.ts
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
/**
|
||||
* Rust module-qualified call resolution (#2730) — path resolution over the
|
||||
* module tree, the way rustc does it.
|
||||
*
|
||||
* `tools::dispatch(ctx, name)` is captured as a FREE call whose `name` is the
|
||||
* tail identifier and whose written path rides along in `site.rawQualifiedName`
|
||||
* (the same channel #1982 built for qualified inheritance bases). Without
|
||||
* consulting that path, the shared scope-chain walk resolves the bare tail and
|
||||
* binds it to whatever `dispatch` is lexically nearest — which, for the wrapper
|
||||
* idiom `fn dispatch(..) { tools::dispatch(..) }`, is the wrapper itself. The
|
||||
* real cross-module edge then does not exist and `impact` reports the callee as
|
||||
* unreached.
|
||||
*
|
||||
* The fix follows rustc's actual rule rather than a filename heuristic:
|
||||
*
|
||||
* 1. A path's leading segments name MODULES, resolved in the type namespace.
|
||||
* A same-named `fn` lives in the value namespace and therefore can never
|
||||
* shadow them — which is exactly the shadowing this bug was about.
|
||||
* 2. `crate::` / `self::` / `super::` are prefix transforms on the caller's
|
||||
* own module path, not reasons to give up.
|
||||
* 3. The final segment is a MEMBER of the resolved module — looked up in that
|
||||
* module's binding table, so `pub use` re-exports resolve like any other
|
||||
* binding.
|
||||
*
|
||||
* Module identity comes from `module-path.ts`: file path below the crate root,
|
||||
* plus any enclosing `mod` blocks (carried on `namespacePrefix`, stamped by the
|
||||
* shared `tagNamespacePrefixes` pass now that `mod_item` emits a Namespace def).
|
||||
*
|
||||
* Refuses — returns `undefined`, leaving the shared chain untouched — whenever
|
||||
* the path names no known module, the module has no such member, or two
|
||||
* candidates tie. A wrong CALLS edge is worse than a missing one: it is what
|
||||
* made this issue dangerous in the first place.
|
||||
*/
|
||||
|
||||
import type { ParsedFile, Scope, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
||||
import { isOverloadableCallable } from '../../utils/callable-labels.js';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import type { WorkspaceResolutionIndex } from '../../scope-resolution/workspace-index.js';
|
||||
import {
|
||||
buildRustModuleIndex,
|
||||
couldNameAModule,
|
||||
moduleOfDef,
|
||||
moduleOfFile,
|
||||
resolveAnchoredModulePath,
|
||||
sameModule,
|
||||
type RustModule,
|
||||
type RustModuleIndex,
|
||||
} from './module-path.js';
|
||||
|
||||
/**
|
||||
* Per-run memo of the crate-root index, keyed by the file set it was built from.
|
||||
* The hook is invoked per call site; rebuilding the index each time would make
|
||||
* qualified-call resolution O(sites x files).
|
||||
*/
|
||||
const MODULE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, RustModuleIndex>();
|
||||
|
||||
function moduleIndexFor(allFilePaths: ReadonlySet<string>): RustModuleIndex {
|
||||
let index = MODULE_INDEX_CACHE.get(allFilePaths);
|
||||
if (index === undefined) {
|
||||
index = buildRustModuleIndex(allFilePaths);
|
||||
MODULE_INDEX_CACHE.set(allFilePaths, index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
export function resolveRustQualifiedFreeCall(
|
||||
site: { readonly name: string; readonly rawQualifiedName?: string; readonly inScope: ScopeId },
|
||||
callerParsed: ParsedFile,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
allFilePaths: ReadonlySet<string>,
|
||||
): SymbolDefinition | undefined {
|
||||
const raw = site.rawQualifiedName;
|
||||
if (raw === undefined) return undefined;
|
||||
|
||||
// A leading `::` anchors at the EXTERN PRELUDE — `::tools::dispatch()` names
|
||||
// the crate `tools`, not a module of this one. Extern crates are outside the
|
||||
// workspace module tree, so the honest answer is to refuse. Filtering the empty
|
||||
// first segment out instead silently reinterpreted the path as relative and
|
||||
// resolved it against a local module of the same name.
|
||||
if (raw.trimStart().startsWith('::')) return undefined;
|
||||
|
||||
const segments = raw
|
||||
.split('::')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
// Everything before the callee name names modules.
|
||||
const qualifier = segments.slice(0, -1);
|
||||
if (qualifier.length === 0) return undefined;
|
||||
|
||||
// Cheap rejection BEFORE any index work. The capture that carries
|
||||
// `rawQualifiedName` matches every `scoped_identifier` callee, so this hook is
|
||||
// reached by `Vec::new()`, `String::from()`, `Self::method()` and every other
|
||||
// type-qualified call — the overwhelming majority of `::` calls in real Rust,
|
||||
// none of which name a module. Letting those run the full candidate search
|
||||
// meant each one paid a same-name-bucket scan plus a walk of every module
|
||||
// scope in the workspace before returning undefined (#2741 review).
|
||||
const index = moduleIndexFor(allFilePaths);
|
||||
if (!couldNameAModule(qualifier, index)) return undefined;
|
||||
|
||||
const callerModule = callerModuleOf(callerParsed, site.inScope, scopes, index);
|
||||
if (callerModule === undefined) return undefined;
|
||||
|
||||
const anchored = resolveAnchoredModulePath(qualifier, callerModule);
|
||||
if (anchored === undefined) return undefined;
|
||||
|
||||
for (const targetModule of candidateModules(
|
||||
anchored,
|
||||
qualifier,
|
||||
callerModule,
|
||||
callerParsed,
|
||||
scopes,
|
||||
workspaceIndex,
|
||||
index,
|
||||
)) {
|
||||
const hit =
|
||||
findMemberInModule(targetModule, site.name, scopes, workspaceIndex, index) ??
|
||||
findReexportedMember(targetModule, site.name, scopes, workspaceIndex, index);
|
||||
if (hit !== undefined) return hit;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The module the call site sits in: its file's module path plus any `mod` blocks
|
||||
* around it. Walking the scope chain (rather than reading the file alone) is what
|
||||
* makes `super::` correct from inside an inline module.
|
||||
*/
|
||||
function callerModuleOf(
|
||||
callerParsed: ParsedFile,
|
||||
inScope: ScopeId,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
index: RustModuleIndex,
|
||||
): RustModule | undefined {
|
||||
const fileModule = moduleOfFile(callerParsed.filePath, index);
|
||||
if (fileModule === undefined) return undefined;
|
||||
|
||||
const inline: string[] = [];
|
||||
let scopeId: ScopeId | null = inScope;
|
||||
while (scopeId !== null) {
|
||||
const scope = scopes.scopeTree.getScope(scopeId);
|
||||
if (scope === undefined) break;
|
||||
if (scope.kind === 'Namespace') {
|
||||
const nsDef = scope.ownedDefs.find((d) => d.type === 'Namespace');
|
||||
const name = nsDef?.qualifiedName;
|
||||
if (name !== undefined && name.length > 0) {
|
||||
inline.unshift(name.slice(name.lastIndexOf('.') + 1));
|
||||
}
|
||||
}
|
||||
scopeId = scope.parent;
|
||||
}
|
||||
return { crateRoot: fileModule.crateRoot, segments: [...fileModule.segments, ...inline] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Module paths the qualifier could name, in rustc's first-segment lookup order.
|
||||
* An anchored path (`crate::`, `self::`, `super::`) names exactly one module and
|
||||
* admits no alternatives.
|
||||
*/
|
||||
function* candidateModules(
|
||||
anchored: { readonly module: RustModule; readonly anchored: boolean },
|
||||
qualifier: readonly string[],
|
||||
callerModule: RustModule,
|
||||
callerParsed: ParsedFile,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
index: RustModuleIndex,
|
||||
): Generator<RustModule> {
|
||||
if (anchored.anchored) {
|
||||
yield anchored.module;
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. A submodule the caller actually DECLARES (`mod inner { … }` or
|
||||
// `mod tools;`) — the in-scope type-namespace binding.
|
||||
//
|
||||
// This must be checked, not assumed. Yielding `callerModule ++ qualifier`
|
||||
// unconditionally let file layout outrank a real `use` binding: with
|
||||
// `use crate::b;` in `src/a/mod.rs` and an undeclared (or `cfg`-gated)
|
||||
// `src/a/b.rs` sitting on disk, `b::f()` bound to the sibling file, where
|
||||
// rustc resolves it to `crate::b` (#2741 review).
|
||||
//
|
||||
// A `mod` declaration — inline or file-backed — emits a `Namespace` def
|
||||
// bound in the declaring scope, so the check is a binding lookup.
|
||||
const head = qualifier[0];
|
||||
if (head !== undefined && declaresSubmodule(callerParsed, head, workspaceIndex)) {
|
||||
yield { crateRoot: callerModule.crateRoot, segments: [...callerModule.segments, ...qualifier] };
|
||||
}
|
||||
|
||||
// 2. A `use` binding for the first segment. Finalize already resolved the
|
||||
// import to a file, so the module path comes back through the same
|
||||
// file → module mapping as everything else. Covers `use crate::tools;`,
|
||||
// `use crate::tools::{self}` and `use crate::a::b as tools`.
|
||||
//
|
||||
// The binding must name a MODULE, not a symbol inside one. Import
|
||||
// resolution deliberately strips a trailing symbol segment when probing for
|
||||
// a file ("the last segment might be a symbol, not a module" —
|
||||
// import-resolvers/rust.ts), so `use crate::client::ClientBuilder;` also
|
||||
// lands on `client/mod.rs`. Taking that at face value made the imported
|
||||
// TYPE look like the module `client`, and `ClientBuilder::new()` then
|
||||
// resolved against `client`'s module members — binding an associated
|
||||
// function to an unrelated module-level `new` (#2741 review H2).
|
||||
for (const edge of scopes.imports.get(callerParsed.moduleScope) ?? []) {
|
||||
if (edge.localName !== head || edge.targetFile === null) continue;
|
||||
const importedModule = moduleOfFile(edge.targetFile, index);
|
||||
if (importedModule === undefined) continue;
|
||||
if (!importNamesModule(edge.targetExportedName, importedModule)) continue;
|
||||
yield {
|
||||
crateRoot: importedModule.crateRoot,
|
||||
segments: [...importedModule.segments, ...qualifier.slice(1)],
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Crate-root-relative (`a::b::f()` written from a nested module — 2015
|
||||
// edition style, and still what a single-file crate looks like).
|
||||
if (callerModule.segments.length > 0) {
|
||||
yield { crateRoot: callerModule.crateRoot, segments: [...qualifier] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the calling file declare `name` as a submodule (`mod name;` or
|
||||
* `mod name { … }`)? Both forms emit a `Namespace` def bound locally in the
|
||||
* declaring scope, so this is a binding lookup rather than a filesystem probe.
|
||||
*/
|
||||
function declaresSubmodule(
|
||||
callerParsed: ParsedFile,
|
||||
name: string,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
): boolean {
|
||||
const moduleScope = workspaceIndex.moduleScopeByFile.get(callerParsed.filePath);
|
||||
if (moduleScope === undefined) return false;
|
||||
for (const ref of moduleScope.bindings.get(name) ?? []) {
|
||||
if (ref.origin === 'local' && ref.def.type === 'Namespace') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this `use` binding name the module it resolved to, rather than a symbol
|
||||
* declared inside it?
|
||||
*
|
||||
* The edge's `targetExportedName` is the tail of the written path, so comparing
|
||||
* it to the resolved module's own tail separates the two cases exactly:
|
||||
*
|
||||
* use crate::tools; tail `tools` module ['tools'] ✓
|
||||
* use crate::a::b as tools; tail `b` module ['a','b'] ✓ (alias)
|
||||
* use crate::tools::{self, Ctx}; tail `tools` module ['tools'] ✓
|
||||
* use crate::client::ClientBuilder; tail `ClientBuilder` module ['client'] ✗ a type
|
||||
*
|
||||
* An import of the crate-root module itself has no tail segment to match; those
|
||||
* are left to the anchored (`crate::`) channel rather than guessed at here.
|
||||
*/
|
||||
function importNamesModule(targetExportedName: string, module: RustModule): boolean {
|
||||
const tail = module.segments[module.segments.length - 1];
|
||||
return tail !== undefined && tail === targetExportedName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is `def` a MEMBER of its module, rather than merely a callable sitting in the
|
||||
* same file?
|
||||
*
|
||||
* Module membership cannot be inferred from the file path: a `fn` nested inside
|
||||
* another `fn` has the same `filePath`, the same bare `qualifiedName` and no
|
||||
* owner, so a path-only test counts it as a second member of the module. That
|
||||
* ties `findMemberInModule`, which then refuses and hands the site back to the
|
||||
* lexical walk — reinstating the very self-loop #2730 fixes, from a module that
|
||||
* merely happens to contain a local helper (#2741 review H3).
|
||||
*
|
||||
* The scope model already draws the line exactly: a module-level item is bound
|
||||
* with `origin: 'local'` in its module's own scope, a function-local item binds
|
||||
* in the enclosing Block, and an `impl`/trait method binds in the Class scope.
|
||||
* So membership is a binding lookup, not a path comparison.
|
||||
*
|
||||
* Inline-`mod` members bind in their `Namespace` scope rather than the file's
|
||||
* Module scope, and reaching that scope would mean walking every child scope
|
||||
* (faulting them in from disk on the out-of-core path). They are instead
|
||||
* identified by the `namespacePrefix` the shared tagging pass stamps on them,
|
||||
* which a file-module member never carries. Residual: a `fn` nested inside a
|
||||
* `fn` that is itself inside an inline `mod` inherits that prefix and is still
|
||||
* counted — a strictly smaller hole than before, and one that only costs a
|
||||
* refusal, never a wrong edge.
|
||||
*/
|
||||
function isModuleLevelMember(
|
||||
def: SymbolDefinition,
|
||||
name: string,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
): boolean {
|
||||
if (def.namespacePrefix !== undefined && def.namespacePrefix !== '') return true;
|
||||
const moduleScope = workspaceIndex.moduleScopeByFile.get(def.filePath);
|
||||
if (moduleScope === undefined) return false;
|
||||
for (const ref of moduleScope.bindings.get(name) ?? []) {
|
||||
if (ref.origin === 'local' && ref.def.nodeId === def.nodeId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A callable named `name` declared directly in `targetModule`. Refuses on a tie. */
|
||||
function findMemberInModule(
|
||||
targetModule: RustModule,
|
||||
name: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
index: RustModuleIndex,
|
||||
): SymbolDefinition | undefined {
|
||||
let unique: SymbolDefinition | undefined;
|
||||
let count = 0;
|
||||
for (const defId of scopes.qualifiedNames.get(name)) {
|
||||
const def = scopes.defs.get(defId);
|
||||
if (def === undefined || !isOverloadableCallable(def.type)) continue;
|
||||
const defModule = moduleOfDef(def.filePath, def.namespacePrefix, index);
|
||||
if (defModule === undefined || !sameModule(defModule, targetModule)) continue;
|
||||
if (!isModuleLevelMember(def, name, workspaceIndex)) continue;
|
||||
unique = def;
|
||||
count++;
|
||||
}
|
||||
return count === 1 ? unique : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A member the target module re-exports rather than declares (`pub use
|
||||
* crate::tools::dispatch;`). rustc treats a re-export as an ordinary binding in
|
||||
* the module's resolution table, so a call through the facade must land on the
|
||||
* original definition.
|
||||
*
|
||||
* Finalize does NOT create a local binding for a re-export on the re-exporting
|
||||
* module's own scope — a `pub use` is modelled as visibility granted to
|
||||
* IMPORTERS, so the re-exporting file's `bindings` map is empty. The re-export
|
||||
* survives as an `ImportEdge` on that module scope, which is what this reads.
|
||||
*
|
||||
* Known limitation: only FILE modules are reachable here. `moduleScopeByFile`
|
||||
* holds one Module scope per file, so a re-export declared inside an inline
|
||||
* `mod facade { pub use … }` has no entry and is not resolved. Reaching it would
|
||||
* mean walking every child scope, which faults the whole scope tree back in from
|
||||
* disk on the out-of-core path — the cost this index exists to avoid. A miss
|
||||
* here falls through to the unchanged chain rather than guessing (#2741 review).
|
||||
*/
|
||||
function findReexportedMember(
|
||||
targetModule: RustModule,
|
||||
name: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
index: RustModuleIndex,
|
||||
): SymbolDefinition | undefined {
|
||||
let unique: SymbolDefinition | undefined;
|
||||
let count = 0;
|
||||
|
||||
for (const moduleScope of moduleScopesFor(targetModule, workspaceIndex, index)) {
|
||||
for (const edge of scopes.imports.get(moduleScope.id) ?? []) {
|
||||
if (edge.localName !== name) continue;
|
||||
// Only a `pub use` re-exports. A private `use` (`named`) makes the name
|
||||
// visible INSIDE the module and does not put it on the module's public
|
||||
// surface, so treating one as a re-export resolved paths that do not
|
||||
// compile. `alias` carries `pub use x::y as name`, which does re-export.
|
||||
if (edge.kind !== 'reexport' && edge.kind !== 'alias') continue;
|
||||
|
||||
const resolved = resolveReexportTarget(edge, name, scopes, workspaceIndex);
|
||||
if (resolved === undefined) continue;
|
||||
// Refuse on a tie rather than taking whichever file the pool happened to
|
||||
// parse first: two `cfg`-exclusive facades re-exporting the same name are
|
||||
// indistinguishable here, and picking one is a coin flip baked into the graph.
|
||||
if (unique !== undefined && resolved.nodeId !== unique.nodeId) return undefined;
|
||||
unique = resolved;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count >= 1 ? unique : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module → the file-module scopes that realise it, built once per resolution
|
||||
* pass instead of per candidate.
|
||||
*
|
||||
* The previous shape walked all of `workspaceIndex.moduleScopeByFile` for every
|
||||
* candidate module of every qualified call, so total cost grew as
|
||||
* `sites x files`. On the out-of-core scope index that walk is worse than CPU:
|
||||
* `moduleScopeByFile` fetches through `scopeTree.getScope`, so a full sweep can
|
||||
* fault every module scope back in from disk — the exact pattern
|
||||
* `workspace-index.ts` added `exportedCallableByName` to avoid (#2741 review).
|
||||
*
|
||||
* Keyed on the `WorkspaceResolutionIndex` identity, which is rebuilt per pass.
|
||||
*/
|
||||
const MODULE_SCOPE_CACHE = new WeakMap<
|
||||
WorkspaceResolutionIndex,
|
||||
ReadonlyMap<string, readonly Scope[]>
|
||||
>();
|
||||
|
||||
function moduleKey(module: RustModule): string {
|
||||
return `${module.crateRoot}\u0000${module.segments.join('::')}`;
|
||||
}
|
||||
|
||||
function moduleScopesFor(
|
||||
targetModule: RustModule,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
index: RustModuleIndex,
|
||||
): readonly Scope[] {
|
||||
let byModule = MODULE_SCOPE_CACHE.get(workspaceIndex);
|
||||
if (byModule === undefined) {
|
||||
const built = new Map<string, Scope[]>();
|
||||
for (const [filePath, moduleScope] of workspaceIndex.moduleScopeByFile) {
|
||||
const fileModule = moduleOfFile(filePath, index);
|
||||
if (fileModule === undefined) continue;
|
||||
const key = moduleKey(fileModule);
|
||||
const bucket = built.get(key);
|
||||
if (bucket === undefined) built.set(key, [moduleScope]);
|
||||
else bucket.push(moduleScope);
|
||||
}
|
||||
byModule = built;
|
||||
MODULE_SCOPE_CACHE.set(workspaceIndex, byModule);
|
||||
}
|
||||
return byModule.get(moduleKey(targetModule)) ?? EMPTY_SCOPES;
|
||||
}
|
||||
|
||||
const EMPTY_SCOPES: readonly Scope[] = Object.freeze([]);
|
||||
|
||||
/** Follow one re-export edge to the definition it exposes. */
|
||||
function resolveReexportTarget(
|
||||
edge: { readonly targetDefId?: string; readonly targetFile: string | null },
|
||||
name: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
): SymbolDefinition | undefined {
|
||||
const viaDefId = edge.targetDefId;
|
||||
if (viaDefId !== undefined) {
|
||||
const def = scopes.defs.get(viaDefId);
|
||||
if (def !== undefined && isOverloadableCallable(def.type)) return def;
|
||||
}
|
||||
// No pre-resolved def id: fall back to the exporting file's own module.
|
||||
if (edge.targetFile === null) return undefined;
|
||||
return findExportedCallable(edge.targetFile, name, workspaceIndex);
|
||||
}
|
||||
|
||||
/** Module-scope callable declared locally by `targetFile`. */
|
||||
function findExportedCallable(
|
||||
targetFile: string,
|
||||
name: string,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
): SymbolDefinition | undefined {
|
||||
const moduleScope = workspaceIndex.moduleScopeByFile.get(targetFile);
|
||||
if (moduleScope === undefined) return undefined;
|
||||
for (const ref of moduleScope.bindings.get(name) ?? []) {
|
||||
if (ref.origin === 'local' && isOverloadableCallable(ref.def.type)) return ref.def;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -44,6 +44,18 @@ const RUST_SCOPE_QUERY = `
|
|||
(union_item
|
||||
name: (type_identifier) @declaration.name) @declaration.struct
|
||||
|
||||
;; Declarations — module (mod foo { ... } / mod foo;)
|
||||
;; A Rust mod is an ITEM, not just a lexical region: rustc resolves the first
|
||||
;; segment of a path (inner::dispatch) against the module tree in the TYPE
|
||||
;; namespace, which is why a same-named fn can never shadow it. Capturing the
|
||||
;; module as a named DEF (not only the @scope.namespace region above) is what
|
||||
;; makes that tree addressable — it feeds the shared tagNamespacePrefixes pass
|
||||
;; so members carry inner / a.b as their namespacePrefix, which qualified
|
||||
;; call resolution then matches against the written path (#2730). Mirrors the
|
||||
;; C++ namespace_definition capture.
|
||||
(mod_item
|
||||
name: (identifier) @declaration.name) @declaration.namespace
|
||||
|
||||
;; Declarations — macro (macro_rules! foo { ... })
|
||||
;; Captured as @declaration.macro → Macro label. A macro invocation
|
||||
;; (@reference.macro, below) resolves to this definition via MacroRegistry,
|
||||
|
|
@ -150,10 +162,14 @@ const RUST_SCOPE_QUERY = `
|
|||
value: (_) @reference.receiver
|
||||
field: (field_identifier) @reference.name)) @reference.call.member
|
||||
|
||||
;; References — scoped calls (Foo::bar())
|
||||
;; References — scoped calls (Foo::bar(), tools::dispatch())
|
||||
;; The call stays a FREE call (resolution is the lexical scope chain), but the
|
||||
;; written path is carried along as @reference.qualified-name so a module-
|
||||
;; qualified call can be resolved against the module the qualifier names before
|
||||
;; the scope-chain walk binds it to a same-named local shadow (#2730).
|
||||
(call_expression
|
||||
function: (scoped_identifier
|
||||
name: (identifier) @reference.name)) @reference.call.free
|
||||
name: (identifier) @reference.name) @reference.qualified-name) @reference.call.free
|
||||
|
||||
;; References — constructor calls (struct literal)
|
||||
;; tree-sitter-rust gives struct_expression.name one of three node types
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolv
|
|||
import { rustProvider } from '../rust.js';
|
||||
import { rustArityCompatibility, rustMergeBindings, resolveRustImportTarget } from './index.js';
|
||||
import { populateRustOwners } from './method-owners.js';
|
||||
import { resolveRustQualifiedFreeCall } from './qualified-call.js';
|
||||
import { populateRustRangeBindings } from './range-binding.js';
|
||||
import {
|
||||
isClassLike,
|
||||
|
|
@ -152,6 +153,9 @@ export const rustScopeResolver: ScopeResolver = {
|
|||
|
||||
arityCompatibility: (callsite, def) => rustArityCompatibility(def, callsite),
|
||||
|
||||
resolveQualifiedFreeCall: (site, callerParsed, scopes, workspaceIndex, allFilePaths) =>
|
||||
resolveRustQualifiedFreeCall(site, callerParsed, scopes, workspaceIndex, allFilePaths),
|
||||
|
||||
buildMro: (graph, parsedFiles, nodeLookup) => buildRustMro(graph, parsedFiles, nodeLookup),
|
||||
|
||||
emitHeritageEdges: (graph, parsedFiles, nodeLookup, scopes) =>
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ import { LanguageProvider } from '../../language-provider.js';
|
|||
import { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import type { SemanticModel } from '../../model/semantic-model.js';
|
||||
import type { ConversionRankFn } from '../passes/overload-narrowing.js';
|
||||
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
||||
|
||||
/** A LinearizeStrategy receives the full ancestor map so C3-style
|
||||
* algorithms (which need to merge each parent's MRO) can implement
|
||||
|
|
@ -906,6 +907,46 @@ export interface ScopeResolver {
|
|||
parsedFiles: readonly ParsedFile[],
|
||||
) => readonly SymbolDefinition[] | undefined;
|
||||
|
||||
/**
|
||||
* Optional resolver for a module-qualified FREE call — a call written with
|
||||
* an explicit path but no value receiver (Rust `tools::dispatch(...)`, where
|
||||
* `tools` names a module, not a variable or a type).
|
||||
*
|
||||
* These sites are captured as free calls (`callForm === 'free'`) with the
|
||||
* written path preserved in `site.rawQualifiedName`. Without this hook the
|
||||
* qualifier is inert and the scope-chain walk resolves the bare tail name —
|
||||
* which silently binds to a same-named definition in the CALLER's own file
|
||||
* when one exists, producing a self-loop and dropping the real cross-module
|
||||
* edge (#2730: `fn dispatch` in `sched.rs` calling `tools::dispatch`
|
||||
* resolved to itself, so `impact` reported the central dispatcher as
|
||||
* risk LOW with 0 affected processes).
|
||||
*
|
||||
* `emitFreeCallFallback` invokes this BEFORE the implicit-`this` and
|
||||
* scope-chain lookups, so an explicit path outranks a lexical shadow.
|
||||
* Returning `undefined` (unqualified call, unknown module, or no such
|
||||
* member in the named module) falls through to the unchanged chain — the
|
||||
* hook is strictly additive and never removes an edge the prior tiers
|
||||
* would have produced.
|
||||
*
|
||||
* Languages whose qualified calls carry a value/type receiver (`x.foo()`,
|
||||
* `Type::foo()`) are served by the receiver-bound-calls pass and leave
|
||||
* this undefined.
|
||||
*/
|
||||
readonly resolveQualifiedFreeCall?: (
|
||||
site: {
|
||||
readonly name: string;
|
||||
readonly rawQualifiedName?: string;
|
||||
/** Needed to locate the calling MODULE, not just the calling file — a
|
||||
* relative anchor (`super::`) is resolved against the module the call
|
||||
* sits in, which may be an inline `mod` block inside that file. */
|
||||
readonly inScope: ScopeId;
|
||||
},
|
||||
callerParsed: ParsedFile,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
allFilePaths: ReadonlySet<string>,
|
||||
) => SymbolDefinition | undefined;
|
||||
|
||||
/**
|
||||
* Optional resolver for qualified-receiver member calls where the
|
||||
* receiver is a namespace (not a class) and ordinary scope-chain /
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ export function emitFreeCallFallback(
|
|||
scopes: ScopeResolutionIndexes,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
) => readonly SymbolDefinition[] | undefined;
|
||||
/** Module-qualified free-call resolver (#2730) — see
|
||||
* `ScopeResolver.resolveQualifiedFreeCall`. */
|
||||
readonly resolveQualifiedFreeCall?: ScopeResolver['resolveQualifiedFreeCall'];
|
||||
readonly conversionRankFn?: ConversionRankFn;
|
||||
readonly conversionOnlyArgTypePrefixes?: readonly string[];
|
||||
/** Optional per-language constraint hook threaded into
|
||||
|
|
@ -125,6 +128,12 @@ export function emitFreeCallFallback(
|
|||
// defs.byId.values() at every constructor call. Same simple-name keying
|
||||
// and class-like kind filter the previous per-site scan applied.
|
||||
const globalClassesBySimpleName = buildGlobalClassIndex(scopes);
|
||||
// Workspace file-path set for `resolveQualifiedFreeCall` (a module path maps
|
||||
// to a file by language convention). Built lazily and once: languages without
|
||||
// the hook never pay for it.
|
||||
let allFilePathsMemo: ReadonlySet<string> | undefined;
|
||||
const allFilePaths = (): ReadonlySet<string> =>
|
||||
(allFilePathsMemo ??= new Set(parsedFiles.map((p) => p.filePath)));
|
||||
// Per-pass memo of pickUniqueGlobalCallable's post-filter candidate list,
|
||||
// keyed (simpleName, callerFilePath). Only created when no per-caller
|
||||
// visibility filter applies (the list is then a pure function of name+file —
|
||||
|
|
@ -217,6 +226,26 @@ export function emitFreeCallFallback(
|
|||
}
|
||||
}
|
||||
}
|
||||
// Module-qualified free call (`mod::fn()`): the source named the module
|
||||
// explicitly, so that path outranks every lexical tier below — otherwise
|
||||
// the scope-chain walk binds the bare tail to a same-named definition in
|
||||
// the caller's own file and emits a self-loop instead of the real
|
||||
// cross-module edge (#2730). Only fires when the language populated
|
||||
// `rawQualifiedName` AND provides the hook; `undefined` falls through to
|
||||
// the unchanged chain, so this tier is strictly additive.
|
||||
if (
|
||||
fnDef === undefined &&
|
||||
options.resolveQualifiedFreeCall !== undefined &&
|
||||
site.rawQualifiedName !== undefined
|
||||
) {
|
||||
fnDef = options.resolveQualifiedFreeCall(
|
||||
site,
|
||||
parsed,
|
||||
scopes,
|
||||
workspaceIndex,
|
||||
allFilePaths(),
|
||||
);
|
||||
}
|
||||
// Implicit-this overload narrowing: an unqualified call inside
|
||||
// a method body might be calling a sibling overload on the
|
||||
// enclosing class. When the workspace has multiple methods of
|
||||
|
|
|
|||
|
|
@ -840,6 +840,7 @@ export function runScopeResolution(
|
|||
freeCallsRequireInstanceOwnership: provider.freeCallsRequireInstanceOwnership === true,
|
||||
isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller,
|
||||
resolveAdlCandidates: provider.resolveAdlCandidates,
|
||||
resolveQualifiedFreeCall: provider.resolveQualifiedFreeCall,
|
||||
conversionRankFn: provider.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: provider.constraintCompatibility,
|
||||
|
|
|
|||
|
|
@ -1019,7 +1019,20 @@ export function populateClassOwnedMembers(parsed: ParsedFile): void {
|
|||
* for nested namespaces regardless of whether the inner namespace's name is
|
||||
* stored simple or already dotted. Skips defs already carrying the prefix.
|
||||
*/
|
||||
export function tagNamespacePrefixes(parsed: ParsedFile): void {
|
||||
export function tagNamespacePrefixes(
|
||||
parsed: ParsedFile,
|
||||
options: { readonly qualifiedNamesCarryNamespace?: boolean } = {},
|
||||
): void {
|
||||
// Whether a def's `qualifiedName` may ALREADY encode its enclosing namespace.
|
||||
// Where it can (C++, C#), a name equal to — or prefixed by — the namespace path
|
||||
// must not be tagged again. Where it cannot, that guard misreads a coincidence:
|
||||
// a Rust `mod a { pub fn a() }` has `qualifiedName === 'a'` purely because the
|
||||
// item and its module share a name, and skipping it leaves the member looking
|
||||
// like it belongs to the PARENT module.
|
||||
const alreadyQualified =
|
||||
options.qualifiedNamesCarryNamespace === undefined
|
||||
? true
|
||||
: options.qualifiedNamesCarryNamespace;
|
||||
const scopesById = new Map<ScopeId, ParsedFile['scopes'][number]>();
|
||||
for (const scope of parsed.scopes) scopesById.set(scope.id, scope);
|
||||
|
||||
|
|
@ -1051,7 +1064,7 @@ export function tagNamespacePrefixes(parsed: ParsedFile): void {
|
|||
for (const def of scope.ownedDefs) {
|
||||
const q = def.qualifiedName;
|
||||
if (q === undefined || q.length === 0) continue;
|
||||
if (q === prefix || q.startsWith(`${prefix}.`)) continue; // already namespaced
|
||||
if (alreadyQualified && (q === prefix || q.startsWith(`${prefix}.`))) continue;
|
||||
def.namespacePrefix = prefix;
|
||||
}
|
||||
}
|
||||
|
|
@ -1075,7 +1088,7 @@ export function tagNamespacePrefixes(parsed: ParsedFile): void {
|
|||
if (def.type === 'Namespace') continue;
|
||||
const q = def.qualifiedName;
|
||||
if (q === undefined || q.length === 0) continue;
|
||||
if (q === fullPrefix || q.startsWith(`${fullPrefix}.`)) continue; // already namespaced
|
||||
if (alreadyQualified && (q === fullPrefix || q.startsWith(`${fullPrefix}.`))) continue;
|
||||
if (def.namespacePrefix !== undefined) continue;
|
||||
def.namespacePrefix = fullPrefix;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j
|
|||
// the main thread (the #1983 OOM). Because the two stores share this version,
|
||||
// any future change to the `ParsedFile` serialization shape MUST bump
|
||||
// SCHEMA_BUMP so both invalidate in lockstep.
|
||||
// v31: Rust `mod_item` gained `@declaration.namespace` and scoped call sites
|
||||
// gained `@reference.qualified-name` (#2730). Both are PARSE-TIME captures, so a
|
||||
// warm cache replays the old capture set verbatim: `rawQualifiedName` comes back
|
||||
// undefined and no Namespace def exists to hang a module prefix on, which turns
|
||||
// the whole module-qualified resolution tier into a no-op on unchanged files.
|
||||
// Re-checked against origin/main at commit time per the v29 note below.
|
||||
// v29: closure-binding declaration rules for PHP/Rust/Kotlin/Ruby/Dart, a Rust
|
||||
// graph node for `let f = || …`, a Dart closure scope, and function-local VALUES
|
||||
// (Variable/Const/Property/Static) qualified by their enclosing callable plus
|
||||
|
|
@ -119,7 +125,7 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j
|
|||
// JLS 13.1 immediate-host chains (#2555).
|
||||
// v18: Worker$N anonymous bodies. v17: callable-value-flow operand identity.
|
||||
// v16: direct callee identity.
|
||||
const SCHEMA_BUMP = 30;
|
||||
const SCHEMA_BUMP = 31;
|
||||
const GITNEXUS_PKG_VERSION = (() => {
|
||||
try {
|
||||
// package.json sits at gitnexus/package.json — two levels up from
|
||||
|
|
|
|||
|
|
@ -542,8 +542,17 @@ export interface RepoMeta {
|
|||
* These change what is emitted for source whose CONTENT has not changed, so a v21
|
||||
* index would keep serving the pre-fix graph for every unchanged CommonJS file —
|
||||
* the exact "Target not found" symptom #2723 reported. Force a full re-analyze.
|
||||
* v23: Rust module-qualified calls resolve against the module tree (#2730).
|
||||
* RUST_SCOPE_QUERY gained `@declaration.namespace` on `mod_item` and
|
||||
* `@reference.qualified-name` on scoped call sites, and a new resolution tier
|
||||
* binds `tools::dispatch(..)` to the module the path names instead of the
|
||||
* lexically nearest same-named fn. Same v11/v12 contract: the incremental
|
||||
* write set only covers CHANGED files, so a top-up against a pre-v23 index
|
||||
* would keep the wrong self-loop — and keep reporting the callee as unreached
|
||||
* — for every unchanged Rust file, which is exactly the symptom #2730
|
||||
* reported. Force a full re-analyze.
|
||||
*/
|
||||
export const INCREMENTAL_SCHEMA_VERSION = 22;
|
||||
export const INCREMENTAL_SCHEMA_VERSION = 23;
|
||||
|
||||
export interface IndexedRepo {
|
||||
repoPath: string;
|
||||
|
|
|
|||
2
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/Cargo.toml
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[workspace]
|
||||
members = ["crates/noob"]
|
||||
3
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/Cargo.toml
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[package]
|
||||
name = "noob"
|
||||
version = "0.1.0"
|
||||
1
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/src/agent/mod.rs
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/src/agent/mod.rs
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod sched;
|
||||
9
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/src/agent/sched.rs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/src/agent/sched.rs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use crate::tools::{self, ToolCtx};
|
||||
|
||||
pub fn execute(ctx: &ToolCtx, name: &str) -> usize {
|
||||
dispatch(ctx, name)
|
||||
}
|
||||
|
||||
fn dispatch(ctx: &ToolCtx, name: &str) -> usize {
|
||||
tools::dispatch(ctx, name)
|
||||
}
|
||||
2
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/src/lib.rs
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod agent;
|
||||
pub mod tools;
|
||||
7
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/src/tools/mod.rs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/rust-2730-crate-layout/crates/noob/src/tools/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub struct ToolCtx {
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
pub fn dispatch(ctx: &ToolCtx, name: &str) -> usize {
|
||||
ctx.depth + name.len()
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/a/b.rs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/a/b.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub fn dispatch() -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
pub fn go() -> usize {
|
||||
super::dispatch()
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/a/mod.rs
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/a/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
pub mod b;
|
||||
|
||||
// Case 4: super:: path from a child module, with a local shadow present.
|
||||
pub fn dispatch() -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
// Second definition: makes `helper` globally ambiguous on purpose.
|
||||
pub fn helper() -> usize {
|
||||
8
|
||||
}
|
||||
1
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/facade.rs
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/facade.rs
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub use crate::tools::dispatch;
|
||||
44
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/main.rs
vendored
Normal file
44
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
mod a;
|
||||
mod facade;
|
||||
mod private_facade;
|
||||
mod tools;
|
||||
|
||||
// Case 1: inline module — rustc resolves `inner::dispatch` via the module tree.
|
||||
// There is no `inner.rs` on disk.
|
||||
mod inner {
|
||||
pub fn dispatch() -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch() -> usize {
|
||||
inner::dispatch()
|
||||
}
|
||||
|
||||
// Case 2: nested path a::b::dispatch()
|
||||
fn nested() -> usize {
|
||||
a::b::dispatch()
|
||||
}
|
||||
|
||||
// Case 3: through a `pub use` re-export facade
|
||||
fn via_reexport() -> usize {
|
||||
facade::dispatch()
|
||||
}
|
||||
|
||||
fn via_private() -> usize {
|
||||
private_facade::helper()
|
||||
}
|
||||
|
||||
// A leading `::` names an EXTERN CRATE, not this crate's `tools` module.
|
||||
// Must not resolve to the local module of the same name (#2741 review).
|
||||
fn via_extern() -> usize {
|
||||
::tools::dispatch()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dispatch();
|
||||
nested();
|
||||
via_reexport();
|
||||
via_private();
|
||||
via_extern();
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/private_facade.rs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/private_facade.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// A PRIVATE use. It does not put `helper` on this module's public surface, so
|
||||
// `private_facade::helper()` does not compile and must not resolve (#2741 review).
|
||||
use crate::tools::helper;
|
||||
|
||||
pub fn touch() -> usize {
|
||||
helper()
|
||||
}
|
||||
17
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/tools.rs
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/rust-2730-gaps/src/tools.rs
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
pub fn dispatch() -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
// A local helper that happens to share the name. It is NOT a module member:
|
||||
// before #2741 review H3 it was counted as one, tying the lookup and sending
|
||||
// every `tools::dispatch()` call back to the self-loop tier.
|
||||
pub fn wrapper() -> usize {
|
||||
fn dispatch() -> usize {
|
||||
99
|
||||
}
|
||||
dispatch()
|
||||
}
|
||||
|
||||
pub fn helper() -> usize {
|
||||
7
|
||||
}
|
||||
16
gitnexus/test/fixtures/lang-resolution/rust-2730-samename-wrapper/src/main.rs
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/rust-2730-samename-wrapper/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
mod sched;
|
||||
mod tools;
|
||||
|
||||
use crate::tools::ToolCtx;
|
||||
|
||||
// `mod tools;` with no `use` binding for the module itself — the qualified
|
||||
// call still has to reach tools.rs even though the local shadow exists here.
|
||||
fn dispatch(ctx: &ToolCtx, name: &str) -> usize {
|
||||
tools::dispatch(ctx, name)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let ctx = ToolCtx { depth: 0 };
|
||||
sched::run(&ctx, "x");
|
||||
dispatch(&ctx, "y");
|
||||
}
|
||||
21
gitnexus/test/fixtures/lang-resolution/rust-2730-samename-wrapper/src/sched.rs
vendored
Normal file
21
gitnexus/test/fixtures/lang-resolution/rust-2730-samename-wrapper/src/sched.rs
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use crate::tools::{self, ToolCtx};
|
||||
|
||||
pub fn run(ctx: &ToolCtx, name: &str) -> usize {
|
||||
dispatch(ctx, name)
|
||||
}
|
||||
|
||||
// Same-name wrapper: the qualified call below must bind to tools::dispatch,
|
||||
// not to this function (#2730).
|
||||
fn dispatch(ctx: &ToolCtx, name: &str) -> usize {
|
||||
tools::dispatch(ctx, name)
|
||||
}
|
||||
|
||||
// Control: no local shadow, resolves today via the global-unique fallback.
|
||||
fn wrapper(ctx: &ToolCtx, name: &str) -> usize {
|
||||
tools::helper(ctx, name)
|
||||
}
|
||||
|
||||
// Control: fully-path-qualified form.
|
||||
fn crate_qualified(ctx: &ToolCtx, name: &str) -> usize {
|
||||
crate::tools::dispatch(ctx, name)
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/rust-2730-samename-wrapper/src/tools.rs
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/rust-2730-samename-wrapper/src/tools.rs
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
pub struct ToolCtx {
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
pub fn dispatch(ctx: &ToolCtx, name: &str) -> usize {
|
||||
ctx.depth + name.len()
|
||||
}
|
||||
|
||||
pub fn helper(ctx: &ToolCtx, name: &str) -> usize {
|
||||
ctx.depth + name.len() + 1
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/rust-2730-type-qualified/src/client/mod.rs
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/rust-2730-type-qualified/src/client/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
pub struct ClientBuilder;
|
||||
|
||||
impl ClientBuilder {
|
||||
pub fn new() -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
// A module-level `new` living alongside the type. Before #2741 review H2 the
|
||||
// imported TYPE was treated as the module `client`, so `ClientBuilder::new()`
|
||||
// bound HERE instead of to the associated function.
|
||||
pub fn new() -> usize {
|
||||
2
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/rust-2730-type-qualified/src/driver.rs
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/rust-2730-type-qualified/src/driver.rs
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
use crate::client::ClientBuilder;
|
||||
use crate::client;
|
||||
|
||||
pub fn build() -> usize {
|
||||
ClientBuilder::new()
|
||||
}
|
||||
|
||||
// Control: a genuine module qualifier must still resolve.
|
||||
pub fn via_module() -> usize {
|
||||
client::new()
|
||||
}
|
||||
2
gitnexus/test/fixtures/lang-resolution/rust-2730-type-qualified/src/lib.rs
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/rust-2730-type-qualified/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod client;
|
||||
pub mod driver;
|
||||
2
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/Cargo.toml
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[workspace]
|
||||
members = ["crates/alpha", "crates/beta"]
|
||||
3
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/alpha/Cargo.toml
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/alpha/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[package]
|
||||
name = "alpha"
|
||||
version = "0.1.0"
|
||||
2
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/alpha/src/lib.rs
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/alpha/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod sched;
|
||||
pub mod tools;
|
||||
6
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/alpha/src/sched.rs
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/alpha/src/sched.rs
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use crate::tools;
|
||||
|
||||
// Same-name wrapper (#2730). Must bind to alpha's tools::dispatch, never beta's.
|
||||
pub fn dispatch(name: &str) -> usize {
|
||||
tools::dispatch(name)
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/alpha/src/tools.rs
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/alpha/src/tools.rs
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub fn dispatch(name: &str) -> usize {
|
||||
name.len()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/beta/Cargo.toml
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/beta/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[package]
|
||||
name = "beta"
|
||||
version = "0.1.0"
|
||||
2
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/beta/src/lib.rs
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/beta/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod sched;
|
||||
pub mod tools;
|
||||
5
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/beta/src/sched.rs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/beta/src/sched.rs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
use crate::tools;
|
||||
|
||||
pub fn dispatch(name: &str) -> usize {
|
||||
tools::dispatch(name)
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/beta/src/tools.rs
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/rust-2730-workspace-crates/crates/beta/src/tools.rs
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub fn dispatch(name: &str) -> usize {
|
||||
name.len() + 1
|
||||
}
|
||||
|
|
@ -1,47 +1,135 @@
|
|||
{
|
||||
"rust-2730-crate-layout/crates/noob/src/agent/mod.rs": {
|
||||
"captureGroups": 3,
|
||||
"digest": "6f0b853796f272d7b082b5e77bdc5d550c0817d3d021638d666222f81d05d7ea"
|
||||
},
|
||||
"rust-2730-crate-layout/crates/noob/src/agent/sched.rs": {
|
||||
"captureGroups": 25,
|
||||
"digest": "86c9c1fdc226b159083adb89441ff4af1b299067ca172f4868fb0c1900e385a8"
|
||||
},
|
||||
"rust-2730-crate-layout/crates/noob/src/lib.rs": {
|
||||
"captureGroups": 5,
|
||||
"digest": "05bd68de498361a0b8b747ed6cc22be8cf5059368b66fbefe2ea9562ade58c46"
|
||||
},
|
||||
"rust-2730-crate-layout/crates/noob/src/tools/mod.rs": {
|
||||
"captureGroups": 15,
|
||||
"digest": "0e40a25c99e388fc72cbdfcc68cc0b0fc1fda2299e3b757d6efe0694b6f6ed86"
|
||||
},
|
||||
"rust-2730-gaps/src/a/b.rs": {
|
||||
"captureGroups": 10,
|
||||
"digest": "a20d02f246b5e35775050650c82a08d207a95d4da5ecdb81db46f786b2f717fc"
|
||||
},
|
||||
"rust-2730-gaps/src/a/mod.rs": {
|
||||
"captureGroups": 11,
|
||||
"digest": "7be125d388e9759769b1533fd063f061b24d43c058226bf1b8555f7e5f533b96"
|
||||
},
|
||||
"rust-2730-gaps/src/facade.rs": {
|
||||
"captureGroups": 2,
|
||||
"digest": "a367994c959ed693e53cad3ca2960b7a5cfb6c30c32034cd8151edfe8ac1b2c6"
|
||||
},
|
||||
"rust-2730-gaps/src/main.rs": {
|
||||
"captureGroups": 48,
|
||||
"digest": "f9d211046e86d1a5d129d73fd89799acb6944bc7c5ef8372cafc0ad6873e160b"
|
||||
},
|
||||
"rust-2730-gaps/src/private_facade.rs": {
|
||||
"captureGroups": 7,
|
||||
"digest": "8bbaa5956035779257ae970d2bbaa5175183b82ae8d27e1fe831519eed3eac8d"
|
||||
},
|
||||
"rust-2730-gaps/src/tools.rs": {
|
||||
"captureGroups": 18,
|
||||
"digest": "988a4d373dfd7e7589486f628997d2a3e3ad2ae7a3e56ea981626da2e1e9f680"
|
||||
},
|
||||
"rust-2730-samename-wrapper/src/main.rs": {
|
||||
"captureGroups": 27,
|
||||
"digest": "f0513d4697c066574ddeeb4e4811b5775c0a68d6a2bdfe606fe8c1a4d37b1ed7"
|
||||
},
|
||||
"rust-2730-samename-wrapper/src/sched.rs": {
|
||||
"captureGroups": 47,
|
||||
"digest": "0a21e306764ee28d9521cd7aca488d29207a21b0d6a95fa2e5197b2a0a351d30"
|
||||
},
|
||||
"rust-2730-samename-wrapper/src/tools.rs": {
|
||||
"captureGroups": 26,
|
||||
"digest": "d55cde148f74dbe323020ec696f949d18394684b8cc2c7c9babc1f80b2eba914"
|
||||
},
|
||||
"rust-2730-type-qualified/src/client/mod.rs": {
|
||||
"captureGroups": 13,
|
||||
"digest": "d74618e1f3444b492ae7728f127981824e5f5abb4f846d36728fddee5a9d3d0d"
|
||||
},
|
||||
"rust-2730-type-qualified/src/driver.rs": {
|
||||
"captureGroups": 13,
|
||||
"digest": "71689600fdf5cb039f486bf88b328caa822ada15ed7abd1c93cec9156f75c125"
|
||||
},
|
||||
"rust-2730-type-qualified/src/lib.rs": {
|
||||
"captureGroups": 5,
|
||||
"digest": "8d94bd9dc806aef8ce3f610090548c7b344c1c21f7b59ce836a27689515a30bb"
|
||||
},
|
||||
"rust-2730-workspace-crates/crates/alpha/src/lib.rs": {
|
||||
"captureGroups": 5,
|
||||
"digest": "9a0eafa33b6ded6c218fe85317e830731342a77e6e669b8561a3a0486d385539"
|
||||
},
|
||||
"rust-2730-workspace-crates/crates/alpha/src/sched.rs": {
|
||||
"captureGroups": 10,
|
||||
"digest": "4c5c9bd4854f1667d2236cb701217c7c1c0eb73429f4cfb40a3e82b7f749ac2c"
|
||||
},
|
||||
"rust-2730-workspace-crates/crates/alpha/src/tools.rs": {
|
||||
"captureGroups": 9,
|
||||
"digest": "0c618f7418939f9d66f5e5177c55091ff1e296fa4be0331879502bb0970a1aa7"
|
||||
},
|
||||
"rust-2730-workspace-crates/crates/beta/src/lib.rs": {
|
||||
"captureGroups": 5,
|
||||
"digest": "9a0eafa33b6ded6c218fe85317e830731342a77e6e669b8561a3a0486d385539"
|
||||
},
|
||||
"rust-2730-workspace-crates/crates/beta/src/sched.rs": {
|
||||
"captureGroups": 10,
|
||||
"digest": "ef9247afbd74aa506934347c2125e56e88727ea000f2d8bf31b47285d3de4fab"
|
||||
},
|
||||
"rust-2730-workspace-crates/crates/beta/src/tools.rs": {
|
||||
"captureGroups": 9,
|
||||
"digest": "fdbdd68d6920a9d7861562c562acc4fbd4580836467cad1ce8428c57a11d0d19"
|
||||
},
|
||||
"rust-abstract-dispatch/src/lib.rs": {
|
||||
"captureGroups": 34,
|
||||
"digest": "973679363065ecd54c4e5128a9fab214ea27eca24f0f079c63a3c6f285e678b0"
|
||||
},
|
||||
"rust-abstract-dispatch/src/main.rs": {
|
||||
"captureGroups": 21,
|
||||
"digest": "9e37c9806579acc6ad6c43e98b7c1320d411020fb0c0e88f5b2c8bbed2053d7d"
|
||||
"captureGroups": 22,
|
||||
"digest": "a9dd5493777b5d4ed6c1e658bb1c8e3800f5c76c88c36c7595173947d6233f4c"
|
||||
},
|
||||
"rust-alias-imports/src/main.rs": {
|
||||
"captureGroups": 19,
|
||||
"digest": "ffa9d81433d8d4931f07ba0151b2349f0bbf876e7b327a5ffeeaf3ed18a43abf"
|
||||
"captureGroups": 20,
|
||||
"digest": "aec319b49b359ca1c9219bd61855aba837abea09b935b297654e1e0e426d2502"
|
||||
},
|
||||
"rust-alias-imports/src/models.rs": {
|
||||
"captureGroups": 21,
|
||||
"digest": "fa89b58c454471da61f7e1585854f42e2f331517094420cf7f76fdf5b898795e"
|
||||
},
|
||||
"rust-ambiguous/src/main.rs": {
|
||||
"captureGroups": 12,
|
||||
"digest": "7d29d164d6f64e2bcac4bba8d4b86d725f696a836341b5265a5dce9543b1691e"
|
||||
"captureGroups": 15,
|
||||
"digest": "6bdf56e7183e7a1585adaeeb05e166c7145f3148dcd2600b61b237a190384b69"
|
||||
},
|
||||
"rust-ambiguous/src/models/handler.rs": {
|
||||
"captureGroups": 9,
|
||||
"digest": "5a1729f00b573f6abb214866ae618cd32fe4c166591327f4de782e262af39266"
|
||||
},
|
||||
"rust-ambiguous/src/models/mod.rs": {
|
||||
"captureGroups": 3,
|
||||
"digest": "f83985431e11f24a46d28f6c5256dd2e7b6d17a1079dd51bf8c4fe4f9087be11"
|
||||
"captureGroups": 4,
|
||||
"digest": "4ccc48b1de50bc7453d9c7b78a8443a1259abb79b6d99a5aa26519e2495967a5"
|
||||
},
|
||||
"rust-ambiguous/src/other/handler.rs": {
|
||||
"captureGroups": 9,
|
||||
"digest": "cbcf55909786187d156c6ce9e94e37602897a3b1515aa80bb15110014f31d048"
|
||||
},
|
||||
"rust-ambiguous/src/other/mod.rs": {
|
||||
"captureGroups": 3,
|
||||
"digest": "f83985431e11f24a46d28f6c5256dd2e7b6d17a1079dd51bf8c4fe4f9087be11"
|
||||
"captureGroups": 4,
|
||||
"digest": "4ccc48b1de50bc7453d9c7b78a8443a1259abb79b6d99a5aa26519e2495967a5"
|
||||
},
|
||||
"rust-ambiguous/src/services/mod.rs": {
|
||||
"captureGroups": 8,
|
||||
"digest": "225b1a2de0f98873a99c669275ecce6a7f6f0d04f2b50f6a15f7546c06f2e295"
|
||||
"digest": "db2a88326a1f8438fdddd3f5aaa47b09ba88ed31cf4faf8f330d8661358136a7"
|
||||
},
|
||||
"rust-assignment-chain/src/main.rs": {
|
||||
"captureGroups": 37,
|
||||
"digest": "d9dc8c233465fb4d160c8547380fa05a0e2d1cdc4dae1205a33f6e96506f0df6"
|
||||
"captureGroups": 39,
|
||||
"digest": "5ebb5d5806ae96f00a9cb3b27e3f862627567002b16cc8c8678ea5a0b4686901"
|
||||
},
|
||||
"rust-assignment-chain/src/repo.rs": {
|
||||
"captureGroups": 10,
|
||||
|
|
@ -52,8 +140,8 @@
|
|||
"digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7"
|
||||
},
|
||||
"rust-async-binding/src/main.rs": {
|
||||
"captureGroups": 33,
|
||||
"digest": "4c8c0c21c2dffe6efaf91d26e92c1bafee95f940909fa4d5a73ad0c07b5aee2f"
|
||||
"captureGroups": 35,
|
||||
"digest": "004a83d1de5240719da2bc23774168bf1ef2a93b0273f3f59ab1031d5e3bf747"
|
||||
},
|
||||
"rust-async-binding/src/repo.rs": {
|
||||
"captureGroups": 11,
|
||||
|
|
@ -64,16 +152,16 @@
|
|||
"digest": "60fc4ac44f58ae67d462e243655b0571392a18de85b9e200e9391b50c941a6c9"
|
||||
},
|
||||
"rust-call-result-binding/src/main.rs": {
|
||||
"captureGroups": 11,
|
||||
"digest": "db75d1b522e5d3574037b692e51423f5f71b6bba011aa120e33d0c6fbc6c1033"
|
||||
"captureGroups": 12,
|
||||
"digest": "3e1fdfce3c731e9d6dd83cb8502453988d5fcc1912e1ec0c71513b4510d68392"
|
||||
},
|
||||
"rust-call-result-binding/src/models.rs": {
|
||||
"captureGroups": 20,
|
||||
"digest": "768d02914dc9e5b495b091ae3f6a27f90ac7b043eeb54e1401c07db4126a5e36"
|
||||
},
|
||||
"rust-calls/src/main.rs": {
|
||||
"captureGroups": 9,
|
||||
"digest": "2ec004ce1d77e3aee80ae1332bc0ab64fb366deffbac84f84ff1861286edc4e5"
|
||||
"captureGroups": 11,
|
||||
"digest": "f14b9c0b916d4f352fe6393d6883870f3de1a8ee8d69f088a94b9b099110a8f5"
|
||||
},
|
||||
"rust-calls/src/onearg/mod.rs": {
|
||||
"captureGroups": 7,
|
||||
|
|
@ -84,12 +172,12 @@
|
|||
"digest": "aae68fe5023fa93180b05ce8034654bfa1d2d95bc7eea67625e2e3a3cff6ebee"
|
||||
},
|
||||
"rust-chain-call/src/main.rs": {
|
||||
"captureGroups": 24,
|
||||
"digest": "eae71a411927bfc015e03800fda779c065c2513e6a6aa130211216d9bee61a35"
|
||||
"captureGroups": 25,
|
||||
"digest": "4b47bbd3de939f8692c1615442e233ad00440075fafc5b6a7f7db35851492a64"
|
||||
},
|
||||
"rust-chain-call/src/models/mod.rs": {
|
||||
"captureGroups": 3,
|
||||
"digest": "c96e9c3df9991cd8e40612b0f1a5fc83a42013cf0870458209fe80c41dd5abd1"
|
||||
"captureGroups": 5,
|
||||
"digest": "ba2fb04d7ff79e394c88aaa6c81737fd084a5948b9615a49c89fd3336e7a73b7"
|
||||
},
|
||||
"rust-chain-call/src/models/repo.rs": {
|
||||
"captureGroups": 11,
|
||||
|
|
@ -104,16 +192,16 @@
|
|||
"digest": "4af705aa98a718b54c1e8a2037989c747c3e4e6bb8ba833cdf6672a1f8983cec"
|
||||
},
|
||||
"rust-child-extends-parent/src/main.rs": {
|
||||
"captureGroups": 18,
|
||||
"digest": "0c87c741f9b05ff5e01eaf6a27c285ba6ee76b753d6609fb43f8d78e5f66fc0a"
|
||||
"captureGroups": 20,
|
||||
"digest": "a08c648a571aa7d96ef573053f47ceea76411102c87075d60f19f84709b5d924"
|
||||
},
|
||||
"rust-child-extends-parent/src/parent.rs": {
|
||||
"captureGroups": 7,
|
||||
"digest": "89bbfe22788bf85e8fda4ad0d1c8dc14f4ec983624e59b58a68852e093290b25"
|
||||
},
|
||||
"rust-constructor-type-inference/src/main.rs": {
|
||||
"captureGroups": 21,
|
||||
"digest": "5cb66a46d7a52800325aab132297e002ccbda7da322ee84d9b95bd7367842c7c"
|
||||
"captureGroups": 23,
|
||||
"digest": "47a20c924b3a69fac902a4d1906b4b17eaa3af699aa1f66200b146d8804f754e"
|
||||
},
|
||||
"rust-constructor-type-inference/src/repo.rs": {
|
||||
"captureGroups": 15,
|
||||
|
|
@ -144,8 +232,8 @@
|
|||
"digest": "45fbd21cf6ed58ca9501d26be034a4969388a730dfc6d36e3982fbc3aa56dcd2"
|
||||
},
|
||||
"rust-cross-module-collision/src/main.rs": {
|
||||
"captureGroups": 7,
|
||||
"digest": "e0120e3f215282e68d83b4f8f5d8918945e0b3e7ce4e0128c6afd2aa43caa1c0"
|
||||
"captureGroups": 10,
|
||||
"digest": "bb44d63ebf56d2d7a6cddff45d6c9ebb5230cd01b67e610f6d7905867c02bcec"
|
||||
},
|
||||
"rust-cross-module-collision/src/traits.rs": {
|
||||
"captureGroups": 5,
|
||||
|
|
@ -160,8 +248,8 @@
|
|||
"digest": "49d6a6bff4bfde6d29d8e63c2899f49688bee98273c9f743d9189dbfdf19ce2c"
|
||||
},
|
||||
"rust-default-constructor/src/main.rs": {
|
||||
"captureGroups": 34,
|
||||
"digest": "6516c6b21d2cca74b18ee443ca0049228b4832efc551ca23e5cf3098c95d34f4"
|
||||
"captureGroups": 36,
|
||||
"digest": "9ec5470e4a76abc78e2507d0a58494354cbd64d6d55992236ba42cb53c04296c"
|
||||
},
|
||||
"rust-default-constructor/src/repo.rs": {
|
||||
"captureGroups": 22,
|
||||
|
|
@ -180,8 +268,8 @@
|
|||
"digest": "cd02dd0f2b74d8e9495634f3a33477c2b20d5f2877612733b66402eae6fe8426"
|
||||
},
|
||||
"rust-dup-fields-2/src/main.rs": {
|
||||
"captureGroups": 16,
|
||||
"digest": "e96013ac801f874a1ad902c7bd2be277202b38fbf12030c4ebbb46edd8c0fe79"
|
||||
"captureGroups": 18,
|
||||
"digest": "89459329c0f81b1b51a0e83fdf84ab61ca61075ce7a8ca4c3af5eeb18763fa4f"
|
||||
},
|
||||
"rust-dup-fields-3/src/c_a.rs": {
|
||||
"captureGroups": 11,
|
||||
|
|
@ -196,12 +284,12 @@
|
|||
"digest": "a5263afa5bc9cb6b499d3c5394cc8b0a942d9a1fe388a2366a3baca963ead634"
|
||||
},
|
||||
"rust-dup-fields-3/src/main.rs": {
|
||||
"captureGroups": 17,
|
||||
"digest": "fad21b046b2f34b8a6fd98ffc3719446f176f7207864b3fdd9ba2d9bb07d607f"
|
||||
"captureGroups": 20,
|
||||
"digest": "2a7b48f4ead88a794f87d30fb1a6ce3d3a8ea818132e370572a06b77f493118f"
|
||||
},
|
||||
"rust-dup-return-2/src/main.rs": {
|
||||
"captureGroups": 14,
|
||||
"digest": "a4d637dc57a09e56dce75102c70ca6a45998f498990289855d4953bf4ed5461f"
|
||||
"captureGroups": 16,
|
||||
"digest": "18dda4ed5e9f24f3e75c037336072bf2489deb5e12d12daf1a8e93cfb965f3ce"
|
||||
},
|
||||
"rust-dup-return-2/src/t_a.rs": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -220,16 +308,16 @@
|
|||
"digest": "cb5f21ac23b71efdf24b121268f783d5b224e7d498ffa7a54555c97ab0f904bf"
|
||||
},
|
||||
"rust-dup-return-3-reordered/src/main.rs": {
|
||||
"captureGroups": 15,
|
||||
"digest": "0f68357dbffb22af2c36ca5a935c3eea4025c119afa1a7c1d258ab9a74fe96e8"
|
||||
"captureGroups": 18,
|
||||
"digest": "d4c83f0221ba70ff2a3a39e603a02f74cc2dc855775b604f9f02b80592f85bfc"
|
||||
},
|
||||
"rust-dup-return-3-reordered/src/z_user.rs": {
|
||||
"captureGroups": 14,
|
||||
"digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9"
|
||||
},
|
||||
"rust-dup-return-3/src/main.rs": {
|
||||
"captureGroups": 15,
|
||||
"digest": "a93ca0874eeaedd21dba987143fa389281d8b738612ca49c314ab91dd73e4065"
|
||||
"captureGroups": 18,
|
||||
"digest": "33e63c3ca60640583d0212d224d31d384870c2d3d485db130dfe5fc17ba7b30b"
|
||||
},
|
||||
"rust-dup-return-3/src/t_a.rs": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -252,8 +340,8 @@
|
|||
"digest": "798c8e01c6e54792ba69e845248efc8abf0cba38fa3d16fb8e0d1f6dd2ad2b7e"
|
||||
},
|
||||
"rust-err-unwrap/src/main.rs": {
|
||||
"captureGroups": 26,
|
||||
"digest": "d7da37b0af98c116dba470de505610c4fbb06f152078b684c0094b1e81aff7e4"
|
||||
"captureGroups": 28,
|
||||
"digest": "ea004c338da0a5e93e1e51d676fd32458606ba8a47efaf9b2bede6b72feedb6f"
|
||||
},
|
||||
"rust-err-unwrap/src/repo.rs": {
|
||||
"captureGroups": 9,
|
||||
|
|
@ -272,8 +360,8 @@
|
|||
"digest": "5bba21d0f792eb98025ce0531c575dac215492027cd877df09b19877e80da4ad"
|
||||
},
|
||||
"rust-for-call-expr/src/main.rs": {
|
||||
"captureGroups": 24,
|
||||
"digest": "292eba3d86490a2ee600ebe4d9e19434204b43706af459c25ae82c2b646da5f9"
|
||||
"captureGroups": 26,
|
||||
"digest": "9d462a7fcfe6ddd56e5d82d856adb54a7e16a1e1ddc0e0f25c4a7875f769b91c"
|
||||
},
|
||||
"rust-for-call-expr/src/repo.rs": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -284,8 +372,8 @@
|
|||
"digest": "7331591b986aa2a9b0ee57020c1f0285ad0da10d5044c326b4beaaeb26019aa8"
|
||||
},
|
||||
"rust-for-loop/src/main.rs": {
|
||||
"captureGroups": 26,
|
||||
"digest": "1f80bd8d194a6e0e843e0a2a76e910d25bab0712bd9208f69c045b6be25cbaed"
|
||||
"captureGroups": 28,
|
||||
"digest": "4a089fa88dfef98ecf474560c2fea94fe4b5e44617d8e89f3bca01660093fc8f"
|
||||
},
|
||||
"rust-for-loop/src/repo.rs": {
|
||||
"captureGroups": 9,
|
||||
|
|
@ -296,16 +384,16 @@
|
|||
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
|
||||
},
|
||||
"rust-generic-impl-same-method-name/lib.rs": {
|
||||
"captureGroups": 19,
|
||||
"digest": "494ee20e8d5ebff07a8358938b706e15a48c9a9fb21e0ce361c2a8c391080352"
|
||||
"captureGroups": 21,
|
||||
"digest": "b947e11ce51005eeba5cb7720f33be8829959aedbd344d7a2531f7b00d51ad37"
|
||||
},
|
||||
"rust-grouped-imports/src/helpers/mod.rs": {
|
||||
"captureGroups": 16,
|
||||
"digest": "4363614087aacd43e98bef6c90e33a49edf7a4c4bee2a20a9e1dd8301c84310a"
|
||||
},
|
||||
"rust-grouped-imports/src/main.rs": {
|
||||
"captureGroups": 14,
|
||||
"digest": "d0997acf33c23b27864423aebaa1b1f4341d57bf5374c6f40c7efa6a166b39e7"
|
||||
"captureGroups": 15,
|
||||
"digest": "9a29f609177db7bc35b8d153616493fb3850f9e59e4c4beec47d0174dc5beae9"
|
||||
},
|
||||
"rust-if-let-unwrap/models/mod.rs": {
|
||||
"captureGroups": 1,
|
||||
|
|
@ -320,8 +408,8 @@
|
|||
"digest": "abf2026b72dacf36eca93895d2601c900424f30ff7f59f904f0dcf1fa377829c"
|
||||
},
|
||||
"rust-if-let-unwrap/src/main.rs": {
|
||||
"captureGroups": 16,
|
||||
"digest": "fa486e6573e6f8372fdff4ffbce6c726e354f1961f27e4f91be5aa0ab49b0496"
|
||||
"captureGroups": 18,
|
||||
"digest": "8d2584ced7b2a0705ce62e0ac13287b8f590fcf7f7c2a862614a5af06f19aac9"
|
||||
},
|
||||
"rust-if-let-unwrap/src/repo.rs": {
|
||||
"captureGroups": 9,
|
||||
|
|
@ -332,16 +420,16 @@
|
|||
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
|
||||
},
|
||||
"rust-if-let/main.rs": {
|
||||
"captureGroups": 32,
|
||||
"digest": "dd9d7112782afac2c55cc9808ea3893551efba8ff4af0c83a55610d5533d24b7"
|
||||
"captureGroups": 33,
|
||||
"digest": "e306c807e89ddc8bb6f9d61a63731c49a9f28ab797e3aa19ed73d3bd5b0d3659"
|
||||
},
|
||||
"rust-if-let/models.rs": {
|
||||
"captureGroups": 20,
|
||||
"digest": "d8c1eb57431b915dd5c9055d8451054a454e340c4842628e38e4d46f69471abd"
|
||||
},
|
||||
"rust-import-alias-return/src/main.rs": {
|
||||
"captureGroups": 16,
|
||||
"digest": "c14fd2abf932f09fe19821951e73afdf03839bdb761a7d0ad347c9926a0d5542"
|
||||
"captureGroups": 19,
|
||||
"digest": "74a6ab2a5b5994c1cd63307a8ae93b8df91955374edc916a19d93574669bf7b1"
|
||||
},
|
||||
"rust-import-alias-return/src/t_a.rs": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -356,8 +444,8 @@
|
|||
"digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5"
|
||||
},
|
||||
"rust-import-dup-fields/src/main.rs": {
|
||||
"captureGroups": 18,
|
||||
"digest": "82b48d00fe2e4b5a2f52185d5d2501fdef408faf148b509fdc5f428171b9a784"
|
||||
"captureGroups": 21,
|
||||
"digest": "87c05f2fd9a846d75e469f406d1fc9d8b074f2aad3d1cd29465d59da3a77b7b6"
|
||||
},
|
||||
"rust-import-dup-fields/src/t_a.rs": {
|
||||
"captureGroups": 11,
|
||||
|
|
@ -372,8 +460,8 @@
|
|||
"digest": "a5263afa5bc9cb6b499d3c5394cc8b0a942d9a1fe388a2366a3baca963ead634"
|
||||
},
|
||||
"rust-import-dup-return/src/main.rs": {
|
||||
"captureGroups": 16,
|
||||
"digest": "e70780bcc2ccbfca9fe2099887e30187f70c48b68a11948592778997d1e2bd13"
|
||||
"captureGroups": 19,
|
||||
"digest": "a86f7bc84a4280d4aa7e600517849fb80582435343c496ecb7d20ae5626b6ca5"
|
||||
},
|
||||
"rust-import-dup-return/src/t_a.rs": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -388,8 +476,8 @@
|
|||
"digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5"
|
||||
},
|
||||
"rust-import-glob-ambiguous/src/main.rs": {
|
||||
"captureGroups": 17,
|
||||
"digest": "31f683937b9d9208e7f2c6bd2d1092c59bcf6ab16a6a6923b48bba6fcab2a1a6"
|
||||
"captureGroups": 20,
|
||||
"digest": "50b0655c9f717a3f740708cdc1134d9f7c67d4992eae5c4486807285b04501f8"
|
||||
},
|
||||
"rust-import-glob-ambiguous/src/t_a.rs": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -404,8 +492,8 @@
|
|||
"digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5"
|
||||
},
|
||||
"rust-import-glob-local-shadows/src/main.rs": {
|
||||
"captureGroups": 28,
|
||||
"digest": "5242d2ee3c9bdb8c679ddc55726e423fc48997db2bcfc4f79dca3645289d1149"
|
||||
"captureGroups": 31,
|
||||
"digest": "dc4df4a03dd1eab2c93c940486277780759814187f1bfa94e6d320fdf790cc29"
|
||||
},
|
||||
"rust-import-glob-local-shadows/src/t_a.rs": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -420,8 +508,8 @@
|
|||
"digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5"
|
||||
},
|
||||
"rust-import-glob-return/src/main.rs": {
|
||||
"captureGroups": 16,
|
||||
"digest": "a157af0c6d7b8f9861712c1822bb5bab8e7fadbcdd58c08d4d034343117053cb"
|
||||
"captureGroups": 19,
|
||||
"digest": "aa2bf2d9b094fba31372e001a0580335aef7e0d41695c41835e3abecfd0d483a"
|
||||
},
|
||||
"rust-import-glob-return/src/t_a.rs": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -436,8 +524,8 @@
|
|||
"digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5"
|
||||
},
|
||||
"rust-iter-for-loop/src/main.rs": {
|
||||
"captureGroups": 30,
|
||||
"digest": "529fda7f9f188814ce6044e2c99d6b005ec4a9b98835fe9609530897998b9f32"
|
||||
"captureGroups": 32,
|
||||
"digest": "17e5404ce443af29626d634c382d589998f07461e19d7fc90cf009e814c1111a"
|
||||
},
|
||||
"rust-iter-for-loop/src/repo.rs": {
|
||||
"captureGroups": 9,
|
||||
|
|
@ -448,8 +536,8 @@
|
|||
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
|
||||
},
|
||||
"rust-local-shadow/src/main.rs": {
|
||||
"captureGroups": 17,
|
||||
"digest": "fa630d884c95e92a2d9c9b372f88e752a24aa4feb8089c3e43726793f3d12188"
|
||||
"captureGroups": 18,
|
||||
"digest": "b5a4009f1b5640c303beae8717381c9ae5ec4b0e9bc1baf50ae44a810c17484d"
|
||||
},
|
||||
"rust-local-shadow/src/utils.rs": {
|
||||
"captureGroups": 7,
|
||||
|
|
@ -460,8 +548,8 @@
|
|||
"digest": "19b650be99256aa211356edc5a3dde83aff21e5d763e2c079322a24045bf69c9"
|
||||
},
|
||||
"rust-match-unwrap/src/main.rs": {
|
||||
"captureGroups": 26,
|
||||
"digest": "ffec7f4401caefc75df450004d4b0828b053799dcbf7247ff2b431ea6ac5a46b"
|
||||
"captureGroups": 28,
|
||||
"digest": "938f98b69f632575c614f683154aa86bc1fe2827511d4891c7c944e59ffd0e11"
|
||||
},
|
||||
"rust-match-unwrap/src/repo.rs": {
|
||||
"captureGroups": 9,
|
||||
|
|
@ -472,16 +560,16 @@
|
|||
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
|
||||
},
|
||||
"rust-member-calls/src/main.rs": {
|
||||
"captureGroups": 15,
|
||||
"digest": "2ef8b5f8ad187b1476af8ceff54051dcf9aa71236aa2b7485a28b3ff97e58e1e"
|
||||
"captureGroups": 16,
|
||||
"digest": "b647063e4383f8885e492b74e3225221c0da72a2669b407fdee9aa24cc13649a"
|
||||
},
|
||||
"rust-member-calls/src/user.rs": {
|
||||
"captureGroups": 10,
|
||||
"digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7"
|
||||
},
|
||||
"rust-method-chain-binding/src/main.rs": {
|
||||
"captureGroups": 18,
|
||||
"digest": "b4767bb9c5e6856309d04e0a3dff90940d391a64b7a6f7efa9109d419af6891a"
|
||||
"captureGroups": 19,
|
||||
"digest": "88b8b2352e0901eea5922f550e964d3e1896d3f2bceb243f44034dada05e25af"
|
||||
},
|
||||
"rust-method-chain-binding/src/models.rs": {
|
||||
"captureGroups": 34,
|
||||
|
|
@ -492,20 +580,20 @@
|
|||
"digest": "7465fc5519b3f643f367ff8c0ae3769a0cb2dbd10e2ea7582bbcff8127a139eb"
|
||||
},
|
||||
"rust-method-enrichment/src/main.rs": {
|
||||
"captureGroups": 18,
|
||||
"digest": "3326eb4f82b1559b6afec497dc52cab734e6f3209501a4bd982bf5eab9ec6dba"
|
||||
"captureGroups": 19,
|
||||
"digest": "f1250e83772cc93ca4f010c68774bd15b18fd507863bc2e9ca804608efaeb992"
|
||||
},
|
||||
"rust-nested-tail-collision-generic/lib.rs": {
|
||||
"captureGroups": 29,
|
||||
"digest": "1bfdaaf207a83924fc25d2eec0a47e5807754bbd0de499b81adea48342c6e687"
|
||||
"captureGroups": 33,
|
||||
"digest": "ab9d8bdfc674adac69965aa9b214fe6c5028184a98a504db7219af7bc9835265"
|
||||
},
|
||||
"rust-nested-tail-collision/lib.rs": {
|
||||
"captureGroups": 17,
|
||||
"digest": "2fc1fe1eb4e8727a89ab283ae34a0ae8df0c421551a7bd5e6e7ffb9d4aa54189"
|
||||
"captureGroups": 19,
|
||||
"digest": "c9670e084d501e3af186bf7b890480c1e364ee18447d39cfcc3d43599eedaadd"
|
||||
},
|
||||
"rust-nullable-receiver/src/main.rs": {
|
||||
"captureGroups": 39,
|
||||
"digest": "2fdecb2bce41738daf30b86b8b145b4ca3438e72387ab25e5b3ba7a5af2c1090"
|
||||
"captureGroups": 41,
|
||||
"digest": "811410fc6afe4722331ff1edfc79ce38a94c241ae52b86c10511d65d8603ef19"
|
||||
},
|
||||
"rust-nullable-receiver/src/repo.rs": {
|
||||
"captureGroups": 10,
|
||||
|
|
@ -516,8 +604,8 @@
|
|||
"digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7"
|
||||
},
|
||||
"rust-option-receiver/src/main.rs": {
|
||||
"captureGroups": 27,
|
||||
"digest": "497145ee37b17cf2eacb3fab98b71b3a04162d011d1e887c588fb9f77d4283be"
|
||||
"captureGroups": 29,
|
||||
"digest": "ab7a69f652524d51534b66defb8f754ceb69c4929668e4e8cfde11d277c6026a"
|
||||
},
|
||||
"rust-option-receiver/src/repo.rs": {
|
||||
"captureGroups": 8,
|
||||
|
|
@ -528,8 +616,8 @@
|
|||
"digest": "7e1cca87f7cec8a11d3f96a561639a65813d5dc202cf91f515ed65afc5b8df8b"
|
||||
},
|
||||
"rust-parent-resolution/src/lib.rs": {
|
||||
"captureGroups": 3,
|
||||
"digest": "141388068614e16d96f27cfdf18ac9001b9e202ce832fe10f38dab990637b3ab"
|
||||
"captureGroups": 5,
|
||||
"digest": "d2b4f82c6096f14ce68304deb98920a0f9b22010f97ac8950d53fba3d26f2c7b"
|
||||
},
|
||||
"rust-parent-resolution/src/serializable.rs": {
|
||||
"captureGroups": 5,
|
||||
|
|
@ -537,11 +625,11 @@
|
|||
},
|
||||
"rust-parent-resolution/src/user.rs": {
|
||||
"captureGroups": 13,
|
||||
"digest": "00d29171a1c471087eb3f06cca66c275611e626e6e8f82b8f8fa9b2f6dfb6125"
|
||||
"digest": "84d0e3980d4f725f8e0831b753a2570ddace613ebf6847ee1a5e86b9fcd4148b"
|
||||
},
|
||||
"rust-qualified-trait/src/main.rs": {
|
||||
"captureGroups": 6,
|
||||
"digest": "bc8946d31db81b85d780633608fdaa7565258cd788285fa00cd6dcb0de3dd16c"
|
||||
"captureGroups": 8,
|
||||
"digest": "8c71cad628449f8c770fffe66f0aa1c33fc84c82de86fdb770887e00a937adcd"
|
||||
},
|
||||
"rust-qualified-trait/src/traits.rs": {
|
||||
"captureGroups": 9,
|
||||
|
|
@ -552,8 +640,8 @@
|
|||
"digest": "ee34385539f7e9398123c056738c6a662a80dac41fc038db96fab0da5c84c8ac"
|
||||
},
|
||||
"rust-receiver-resolution/src/main.rs": {
|
||||
"captureGroups": 23,
|
||||
"digest": "89d03ee72c223fde12b407ff50ae512dedb1a63aa4239e6f5dd9a54949566786"
|
||||
"captureGroups": 25,
|
||||
"digest": "61192da77580c3d846d9648df22e91643fb76d1b0f0edac589211cba8e4432ea"
|
||||
},
|
||||
"rust-receiver-resolution/src/repo.rs": {
|
||||
"captureGroups": 10,
|
||||
|
|
@ -564,44 +652,44 @@
|
|||
"digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7"
|
||||
},
|
||||
"rust-reexport-chain/src/main.rs": {
|
||||
"captureGroups": 12,
|
||||
"digest": "d4ddd026d2159710adb889629e17104bb36d2855f15a37b39dadd1dc4cd6b993"
|
||||
"captureGroups": 13,
|
||||
"digest": "b21d08ceb71c980b618e39e7f01f055c6f5d424c11dda3d9c709070f68a82614"
|
||||
},
|
||||
"rust-reexport-chain/src/models/handler.rs": {
|
||||
"captureGroups": 11,
|
||||
"digest": "04091a9f859230d384307beffab59331e960dad13953ef3919ab6b3cfbb585e7"
|
||||
},
|
||||
"rust-reexport-chain/src/models/mod.rs": {
|
||||
"captureGroups": 3,
|
||||
"digest": "f83985431e11f24a46d28f6c5256dd2e7b6d17a1079dd51bf8c4fe4f9087be11"
|
||||
"captureGroups": 4,
|
||||
"digest": "4ccc48b1de50bc7453d9c7b78a8443a1259abb79b6d99a5aa26519e2495967a5"
|
||||
},
|
||||
"rust-return-type-inference/src/main.rs": {
|
||||
"captureGroups": 32,
|
||||
"digest": "0f47930a3070f27b80e89065c29dfee3ce4c1923d5e66053553d7536bb521a23"
|
||||
"captureGroups": 33,
|
||||
"digest": "7b09dc582e5af3640c7b81b743bc44d415effbf51121a70962a23c6ff3df1671"
|
||||
},
|
||||
"rust-return-type-inference/src/models.rs": {
|
||||
"captureGroups": 21,
|
||||
"digest": "465517463a6f648955d92b6d179eb9f581a5431077c1eb22c4048d6283387458"
|
||||
},
|
||||
"rust-return-type/src/main.rs": {
|
||||
"captureGroups": 11,
|
||||
"digest": "2ab4961fb869cf40db597b422b24c7ba7a7b63d97487cea7013e065a32e497f3"
|
||||
"captureGroups": 12,
|
||||
"digest": "eda00e4ff23254e5f08f7248571630b9a522955a0943dc793fdcef53e80e245f"
|
||||
},
|
||||
"rust-return-type/src/models.rs": {
|
||||
"captureGroups": 18,
|
||||
"digest": "c589e22d34215100a8e69f0c0a3a801814203edfb28d46a0342d1b1f813af06b"
|
||||
},
|
||||
"rust-scoped-impl/lib.rs": {
|
||||
"captureGroups": 17,
|
||||
"digest": "7bae61e3bde8ce20eade29ce06b13f0a57b7da631d81604218bd07b881a7b754"
|
||||
"captureGroups": 19,
|
||||
"digest": "a58a78608812023fb6298c1d74e3216a7582da513f8f7bb7ccb7d51e46ddef4f"
|
||||
},
|
||||
"rust-scoped-multi-file/src/main.rs": {
|
||||
"captureGroups": 17,
|
||||
"digest": "cbb0ad90a6a6ddcb71afb98a98a172bede7f5c06b5c1d31484a11315a264b311"
|
||||
"captureGroups": 18,
|
||||
"digest": "9964f6acd248a3bb83d4861a5a4b5f899f257e7372373fc554f3c10260e963d5"
|
||||
},
|
||||
"rust-scoped-multi-file/src/models/mod.rs": {
|
||||
"captureGroups": 5,
|
||||
"digest": "7346b2cf62e4946b261ed0eb46f2623fb1882f46d832abdc2068012917c7b16a"
|
||||
"captureGroups": 7,
|
||||
"digest": "ebf5a058cc30c3409521a3432421ffc44c595b1d6ef8e3235a8d896ea2360be0"
|
||||
},
|
||||
"rust-scoped-multi-file/src/models/repo.rs": {
|
||||
"captureGroups": 20,
|
||||
|
|
@ -612,12 +700,12 @@
|
|||
"digest": "bbc80d3aab883aa959627a8915ed18f1e90ea1061d65bc41eaf967a85ba91f05"
|
||||
},
|
||||
"rust-self-struct-literal/main.rs": {
|
||||
"captureGroups": 11,
|
||||
"digest": "3c0beb60f1487a63c60329e0843c1913b6a3854bb1ed3df84e4a07bc16dbd82d"
|
||||
"captureGroups": 12,
|
||||
"digest": "fcf43627f34d4b17b807f368b40da41da559ea51c1f74835eee5e6ddadb0ae97"
|
||||
},
|
||||
"rust-self-struct-literal/models.rs": {
|
||||
"captureGroups": 32,
|
||||
"digest": "d4d831b113acf0a809b8aeec9a8ff02a2ce763fd649c16ceb5792c0d1a270047"
|
||||
"digest": "8928de22daa88f6f311a407e0051d1dbd3e52c81503f4b295eecb3a3c86b23cf"
|
||||
},
|
||||
"rust-self-this-resolution/src/repo.rs": {
|
||||
"captureGroups": 10,
|
||||
|
|
@ -628,8 +716,8 @@
|
|||
"digest": "9354dad2a222a66b46f00ceede6777ba8c0634294c41313f393bc9ebcbbb6aa7"
|
||||
},
|
||||
"rust-struct-destructuring/main.rs": {
|
||||
"captureGroups": 17,
|
||||
"digest": "79ce4f3fa933214532c96161865231c51c5505773794e0c87d5c995a2684cd4b"
|
||||
"captureGroups": 19,
|
||||
"digest": "fb1912a7068bd6666472ee5e4cdaecb5afe4b247a44cb70a93ceb654ff2e74b8"
|
||||
},
|
||||
"rust-struct-destructuring/point.rs": {
|
||||
"captureGroups": 6,
|
||||
|
|
@ -640,16 +728,16 @@
|
|||
"digest": "ff25512c9a7c4c58d9e06e8ec55727ba13ca20361e7758b560bf0f53b6aa3a93"
|
||||
},
|
||||
"rust-struct-literal-inference/main.rs": {
|
||||
"captureGroups": 19,
|
||||
"digest": "e71269ff626cd0b0655591ee08e48d90e9a1421f3be8d9e8ac23262388a3f7ad"
|
||||
"captureGroups": 20,
|
||||
"digest": "89bb1fd5ce51a13418dc9b35def89b8711df27db996636491b477fce0c94ba91"
|
||||
},
|
||||
"rust-struct-literal-inference/models.rs": {
|
||||
"captureGroups": 27,
|
||||
"digest": "dedd93d6214ff00ee0ee267140918403b7f59e0ab010cb9058af40cb3b08bb81"
|
||||
},
|
||||
"rust-struct-literals/app.rs": {
|
||||
"captureGroups": 13,
|
||||
"digest": "2a1a44788e40cf855c235849c932b4e82f6551a7b5e747e1f6f91b250ead6ec4"
|
||||
"captureGroups": 14,
|
||||
"digest": "7b88f81b3b25be95ea5a4a128c07408f936c526dcc8f218d93e7c0cb8d398d7c"
|
||||
},
|
||||
"rust-struct-literals/user.rs": {
|
||||
"captureGroups": 11,
|
||||
|
|
@ -660,8 +748,8 @@
|
|||
"digest": "30b0e4e92385e7f7eb83ad97e78ed65b0eced81bbcb20bf52a846924c75ab5bb"
|
||||
},
|
||||
"rust-traits/src/main.rs": {
|
||||
"captureGroups": 11,
|
||||
"digest": "f1b9f72d74467be55a8b7679215b49bcabb4d0fced6080f752672070b32ed93d"
|
||||
"captureGroups": 13,
|
||||
"digest": "0f78c887c5a94ea2b03e38d2060046253b1e09f096356b5ae21cf9c6992ed83a"
|
||||
},
|
||||
"rust-traits/src/traits/clickable.rs": {
|
||||
"captureGroups": 7,
|
||||
|
|
@ -676,8 +764,8 @@
|
|||
"digest": "e2a6fb9eab259b8c7104f1530b96b8c1f42ab32fe1d71d6bdca04d68263507f2"
|
||||
},
|
||||
"rust-unique-return/src/main.rs": {
|
||||
"captureGroups": 13,
|
||||
"digest": "72cc5728b40f51fae75359c2443986ce73f5d5450ebf8a367a2d860c87c84ed0"
|
||||
"captureGroups": 14,
|
||||
"digest": "adc8d4faef631517f4be281c973c61c46325b359fbf482d895fbdfb16feb0931"
|
||||
},
|
||||
"rust-unique-return/src/t_a.rs": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -689,7 +777,7 @@
|
|||
},
|
||||
"rust-write-access/service.rs": {
|
||||
"captureGroups": 17,
|
||||
"digest": "4c5055277bdbb762af4f58623c3aba323c9618e900ca731ead19171d4a3a9495"
|
||||
"digest": "a2c105b1a0efa7f1b303c349bcedc2cebde5592456c53c882fc61711f188901d"
|
||||
},
|
||||
"synthetic:dao-20": {
|
||||
"captureGroups": 421,
|
||||
|
|
|
|||
|
|
@ -2559,3 +2559,232 @@ describe('Rust import-disambiguated duplicate resolution (#2514 follow-up)', ()
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2730 — a module-qualified call must not bind to a same-named local fn
|
||||
//
|
||||
// `tools::dispatch(...)` is captured as a FREE call named `dispatch`. Before
|
||||
// the fix the qualifier was discarded, so the scope-chain walk bound the bare
|
||||
// tail to the ENCLOSING same-named wrapper and emitted a self-loop — the real
|
||||
// cross-module edge never existed, and `impact` on the callee reported the
|
||||
// production caller as absent (risk LOW, 0 affected processes) while still
|
||||
// labelling itself `epistemic: "exact"`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust module-qualified free calls (#2730)', () => {
|
||||
describe('flat src/ layout', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'rust-2730-samename-wrapper'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('binds tools::dispatch to tools.rs, not to the same-named wrapper (use ::{self})', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.sourceFilePath === 'src/sched.rs' && c.source === 'dispatch',
|
||||
);
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({
|
||||
source: 'dispatch',
|
||||
target: 'dispatch',
|
||||
targetFilePath: 'src/tools.rs',
|
||||
});
|
||||
expect(edges[0].rel.reason).toBe('import-resolved');
|
||||
});
|
||||
|
||||
it('binds tools::dispatch through a bare `mod tools;` with no use binding', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.sourceFilePath === 'src/main.rs' && c.source === 'dispatch',
|
||||
);
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({ target: 'dispatch', targetFilePath: 'src/tools.rs' });
|
||||
});
|
||||
|
||||
it('binds a fully path-qualified crate::tools::dispatch to tools.rs', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'crate_qualified');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({ target: 'dispatch', targetFilePath: 'src/tools.rs' });
|
||||
});
|
||||
|
||||
it('leaves genuinely unqualified calls on the lexical scope chain', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'run');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({ target: 'dispatch', targetFilePath: 'src/sched.rs' });
|
||||
expect(edges[0].rel.reason).toBe('local-call');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cargo workspace crates/<name>/src layout', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-2730-crate-layout'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('resolves crate::tools through the use edge when no sibling file matches the path', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.sourceFilePath === 'crates/noob/src/agent/sched.rs' && c.source === 'dispatch',
|
||||
);
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({
|
||||
target: 'dispatch',
|
||||
targetFilePath: 'crates/noob/src/tools/mod.rs',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the unqualified sibling call local', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'execute');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].targetFilePath).toBe('crates/noob/src/agent/sched.rs');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2730 — path resolution over the module tree (rustc semantics)
|
||||
//
|
||||
// The leading segments of a path name MODULES, resolved in the type namespace,
|
||||
// so a same-named `fn` (value namespace) can never shadow them. `crate::`,
|
||||
// `self::` and `super::` are prefix transforms on the calling module, and the
|
||||
// final segment is a member of the resolved module — including members it only
|
||||
// re-exports.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust qualified paths resolve against the module tree (#2730)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-2730-gaps'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('resolves a multi-segment path a::b::dispatch() past a same-named local fn', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'nested');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({ target: 'dispatch', targetFilePath: 'src/a/b.rs' });
|
||||
});
|
||||
|
||||
it('resolves super::dispatch() to the parent module, not the caller file', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'go');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({ target: 'dispatch', targetFilePath: 'src/a/mod.rs' });
|
||||
});
|
||||
|
||||
it('ignores a function-local fn of the same name in the target module (H3)', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'via_reexport');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({ target: 'dispatch', targetFilePath: 'src/tools.rs' });
|
||||
});
|
||||
|
||||
it('follows a `pub use` re-export through to the original definition', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'via_reexport');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({ target: 'dispatch', targetFilePath: 'src/tools.rs' });
|
||||
});
|
||||
|
||||
it('does not bind a leading :: path into the local module of the same name', () => {
|
||||
// `::tools::dispatch()` names an EXTERN crate. GitNexus does not model extern
|
||||
// crates, so the qualified tier must refuse; whatever the unchanged lexical
|
||||
// tier then does is out of scope here. What must NOT happen is this binding
|
||||
// to the local `tools` module as though the `::` were absent.
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'via_extern');
|
||||
expect(edges.filter((c) => c.targetFilePath === 'src/tools.rs')).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not treat a private `use` as a re-export', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'via_private');
|
||||
expect(edges).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps every qualified target distinct from the crate-root fn of the same name', () => {
|
||||
const targets = getRelationships(result, 'CALLS')
|
||||
.filter((c) => ['nested', 'go', 'via_reexport'].includes(c.source))
|
||||
.map((c) => c.targetFilePath)
|
||||
.sort();
|
||||
expect(targets).toEqual(['src/a/b.rs', 'src/a/mod.rs', 'src/tools.rs']);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2730 review H1 — module identity carries the crate.
|
||||
//
|
||||
// A cargo workspace routinely gives several members the same internal module
|
||||
// name. With identity by path segments alone, `crates/alpha/src/tools.rs` and
|
||||
// `crates/beta/src/tools.rs` were the SAME module: where only one defined the
|
||||
// member the call bound across crates, and where both did the lookup tied and
|
||||
// refused — handing the site back to the lexical walk that reinstates the very
|
||||
// self-loop #2730 is about. Rust has no implicit cross-crate paths, so two
|
||||
// modules in different crates are never the same module.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust qualified calls stay inside their own crate (#2730 review H1)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-2730-workspace-crates'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('binds alpha::sched::dispatch to alpha tools, not beta', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.sourceFilePath === 'crates/alpha/src/sched.rs' && c.source === 'dispatch',
|
||||
);
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({
|
||||
target: 'dispatch',
|
||||
targetFilePath: 'crates/alpha/src/tools.rs',
|
||||
});
|
||||
});
|
||||
|
||||
it('binds beta::sched::dispatch to beta tools, not alpha', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.sourceFilePath === 'crates/beta/src/sched.rs' && c.source === 'dispatch',
|
||||
);
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({
|
||||
target: 'dispatch',
|
||||
targetFilePath: 'crates/beta/src/tools.rs',
|
||||
});
|
||||
});
|
||||
|
||||
it('emits no self-loop in either crate', () => {
|
||||
const selfLoops = getRelationships(result, 'CALLS').filter(
|
||||
(c) =>
|
||||
c.source === 'dispatch' && c.target === 'dispatch' && c.sourceFilePath === c.targetFilePath,
|
||||
);
|
||||
expect(selfLoops).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2730 review H2 — a `use` binding must name a MODULE, not a type.
|
||||
//
|
||||
// Import resolution deliberately strips a trailing symbol segment when probing
|
||||
// for a file, so `use crate::client::ClientBuilder;` also resolves to
|
||||
// `client/mod.rs`. Taking that at face value made the imported TYPE look like
|
||||
// the module `client`, and `ClientBuilder::new()` bound to an unrelated
|
||||
// module-level `new` instead of the associated function.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust type-qualified calls are not treated as module paths (#2730 review H2)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-2730-type-qualified'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('does not bind ClientBuilder::new() to the module-level new', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'build' && c.target === 'new',
|
||||
);
|
||||
const moduleLevel = edges.filter((c) => c.targetLabel === 'Function');
|
||||
expect(moduleLevel).toEqual([]);
|
||||
});
|
||||
|
||||
it('still resolves a genuine module qualifier', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter((c) => c.source === 'via_module');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0]).toMatchObject({ target: 'new', targetFilePath: 'src/client/mod.rs' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -73,12 +73,12 @@ describe('CALL_SUMMARY relation-type exclusion (U-C1)', () => {
|
|||
});
|
||||
|
||||
describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => {
|
||||
it('INCREMENTAL_SCHEMA_VERSION is bumped to 22 (CommonJS export indexing, #2723)', () => {
|
||||
it('INCREMENTAL_SCHEMA_VERSION is bumped to 23 (Rust module-qualified calls, #2730)', () => {
|
||||
// Moves with every bump BY DESIGN — that is the point of pinning it. A
|
||||
// change that alters emitted ids or edges without bumping would otherwise
|
||||
// ship silently, and an existing index would keep serving the old graph
|
||||
// through the reuse gate below.
|
||||
expect(INCREMENTAL_SCHEMA_VERSION).toBe(22);
|
||||
expect(INCREMENTAL_SCHEMA_VERSION).toBe(23);
|
||||
});
|
||||
|
||||
it('a pre-current stamp fails the `=== INCREMENTAL_SCHEMA_VERSION` reuse gate → forces full re-analyze', () => {
|
||||
|
|
@ -168,11 +168,14 @@ describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => {
|
|||
// collapsed onto one node and asserted a CALLS edge present nowhere in the
|
||||
// source.
|
||||
expect(passesReuseGate(20)).toBe(false);
|
||||
// A current-version stamp passes the gate (incremental top-up eligible).
|
||||
// A pre-v22 (v21) index predates CommonJS export indexing (#2723): every
|
||||
// unchanged CJS file would keep its pre-fix graph → must NOT reuse.
|
||||
expect(passesReuseGate(21)).toBe(false);
|
||||
// The current stamp passes.
|
||||
expect(passesReuseGate(22)).toBe(true);
|
||||
// A pre-v23 (v22) index predates Rust module-qualified call resolution
|
||||
// (#2730): every unchanged Rust file would keep the same-name self-loop and
|
||||
// keep reporting the real callee as unreached → must NOT reuse.
|
||||
expect(passesReuseGate(22)).toBe(false);
|
||||
// The current stamp passes (incremental top-up eligible).
|
||||
expect(passesReuseGate(23)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
/**
|
||||
* Unit tests for the Rust module-path model (#2730 review).
|
||||
*
|
||||
* These pin the identity rules the qualified-call resolver depends on. The
|
||||
* integration tests exercise resolution end to end; these cover the arithmetic
|
||||
* directly, including the branches a fixture cannot reach (a file under no crate
|
||||
* root, `super::` walking above the crate root).
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
buildRustModuleIndex,
|
||||
moduleOfDef,
|
||||
moduleOfFile,
|
||||
resolveAnchoredModulePath,
|
||||
sameModule,
|
||||
} from '../../../../src/core/ingestion/languages/rust/module-path.js';
|
||||
|
||||
const WORKSPACE = buildRustModuleIndex(
|
||||
new Set([
|
||||
'crates/alpha/src/lib.rs',
|
||||
'crates/alpha/src/tools.rs',
|
||||
'crates/alpha/src/a/mod.rs',
|
||||
'crates/alpha/src/a/b.rs',
|
||||
'crates/beta/src/lib.rs',
|
||||
'crates/beta/src/tools.rs',
|
||||
]),
|
||||
);
|
||||
|
||||
const SINGLE = buildRustModuleIndex(new Set(['src/main.rs', 'src/tools.rs', 'src/a/mod.rs']));
|
||||
|
||||
describe('buildRustModuleIndex', () => {
|
||||
it('finds one crate root per member, longest first', () => {
|
||||
expect(WORKSPACE.crateRoots).toEqual(['crates/alpha/src', 'crates/beta/src']);
|
||||
});
|
||||
|
||||
it('finds a single crate root for a plain package', () => {
|
||||
expect(SINGLE.crateRoots).toEqual(['src']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moduleOfFile', () => {
|
||||
it('maps a crate root file to the crate-root module', () => {
|
||||
expect(moduleOfFile('src/main.rs', SINGLE)).toMatchObject({ crateRoot: 'src', segments: [] });
|
||||
});
|
||||
|
||||
it('maps a plain module file to its own segment', () => {
|
||||
expect(moduleOfFile('src/tools.rs', SINGLE)).toMatchObject({
|
||||
crateRoot: 'src',
|
||||
segments: ['tools'],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not let mod.rs contribute a segment', () => {
|
||||
expect(moduleOfFile('src/a/mod.rs', SINGLE)).toMatchObject({
|
||||
crateRoot: 'src',
|
||||
segments: ['a'],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps a nested module file to the full path', () => {
|
||||
expect(moduleOfFile('crates/alpha/src/a/b.rs', WORKSPACE)).toMatchObject({
|
||||
crateRoot: 'crates/alpha/src',
|
||||
segments: ['a', 'b'],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined for a file under no crate root', () => {
|
||||
expect(moduleOfFile('scripts/helper.rs', SINGLE)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('module identity carries the crate (#2730 review H1)', () => {
|
||||
it('gives two crates the same internal segments', () => {
|
||||
const alpha = moduleOfFile('crates/alpha/src/tools.rs', WORKSPACE);
|
||||
const beta = moduleOfFile('crates/beta/src/tools.rs', WORKSPACE);
|
||||
expect(alpha).toMatchObject({ segments: ['tools'] });
|
||||
expect(beta).toMatchObject({ segments: ['tools'] });
|
||||
});
|
||||
|
||||
it('but does NOT treat them as the same module', () => {
|
||||
const alpha = moduleOfFile('crates/alpha/src/tools.rs', WORKSPACE)!;
|
||||
const beta = moduleOfFile('crates/beta/src/tools.rs', WORKSPACE)!;
|
||||
expect(sameModule(alpha, beta)).toBe(false);
|
||||
});
|
||||
|
||||
it('treats a module as equal to itself', () => {
|
||||
const one = moduleOfFile('crates/alpha/src/tools.rs', WORKSPACE)!;
|
||||
const two = moduleOfFile('crates/alpha/src/tools.rs', WORKSPACE)!;
|
||||
expect(sameModule(one, two)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moduleOfDef', () => {
|
||||
it('appends an inline mod prefix to the file module', () => {
|
||||
expect(moduleOfDef('src/tools.rs', 'inner', SINGLE)).toMatchObject({
|
||||
crateRoot: 'src',
|
||||
segments: ['tools', 'inner'],
|
||||
});
|
||||
});
|
||||
|
||||
it('splits a nested inline prefix', () => {
|
||||
expect(moduleOfDef('src/main.rs', 'outer.inner', SINGLE)).toMatchObject({
|
||||
segments: ['outer', 'inner'],
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves the file module alone when there is no prefix', () => {
|
||||
expect(moduleOfDef('src/tools.rs', undefined, SINGLE)).toMatchObject({ segments: ['tools'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAnchoredModulePath', () => {
|
||||
const caller = { crateRoot: 'src', segments: ['a', 'b'] };
|
||||
|
||||
it('anchors crate:: at the caller crate root', () => {
|
||||
expect(resolveAnchoredModulePath(['crate', 'tools'], caller)).toMatchObject({
|
||||
anchored: true,
|
||||
module: { crateRoot: 'src', segments: ['tools'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('anchors self:: at the calling module', () => {
|
||||
expect(resolveAnchoredModulePath(['self', 'inner'], caller)).toMatchObject({
|
||||
anchored: true,
|
||||
module: { segments: ['a', 'b', 'inner'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('pops one segment per super', () => {
|
||||
expect(resolveAnchoredModulePath(['super', 'sibling'], caller)).toMatchObject({
|
||||
anchored: true,
|
||||
module: { segments: ['a', 'sibling'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('consumes a super chain left to right', () => {
|
||||
expect(resolveAnchoredModulePath(['super', 'super', 'top'], caller)).toMatchObject({
|
||||
anchored: true,
|
||||
module: { segments: ['top'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses a super chain that walks above the crate root', () => {
|
||||
expect(resolveAnchoredModulePath(['super', 'super', 'super', 'x'], caller)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps a bare path relative for the caller to try in context', () => {
|
||||
expect(resolveAnchoredModulePath(['tools'], caller)).toMatchObject({
|
||||
anchored: false,
|
||||
module: { crateRoot: 'src', segments: ['tools'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps an anchored path inside the caller crate', () => {
|
||||
const inBeta = { crateRoot: 'crates/beta/src', segments: [] };
|
||||
expect(resolveAnchoredModulePath(['crate', 'tools'], inBeta)).toMatchObject({
|
||||
module: { crateRoot: 'crates/beta/src' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2741 review — `src/bin/<name>.rs` is its own crate, not a library module.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('auto-discovered binary targets', () => {
|
||||
const WITH_BIN = buildRustModuleIndex(
|
||||
new Set([
|
||||
'src/lib.rs',
|
||||
'src/helper.rs',
|
||||
'src/bin/tool.rs',
|
||||
'src/bin/tool/helper.rs',
|
||||
'src/bin/other/main.rs',
|
||||
]),
|
||||
);
|
||||
|
||||
it('treats a src/bin entry file as its own crate root, not module bin::tool', () => {
|
||||
expect(moduleOfFile('src/bin/tool.rs', WITH_BIN)).toMatchObject({
|
||||
crateRoot: 'src/bin/tool',
|
||||
segments: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('places a binary submodule under the binary, not the library', () => {
|
||||
expect(moduleOfFile('src/bin/tool/helper.rs', WITH_BIN)).toMatchObject({
|
||||
crateRoot: 'src/bin/tool',
|
||||
segments: ['helper'],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the library module separate from the same-named binary module', () => {
|
||||
const lib = moduleOfFile('src/helper.rs', WITH_BIN)!;
|
||||
const bin = moduleOfFile('src/bin/tool/helper.rs', WITH_BIN)!;
|
||||
expect(sameModule(lib, bin)).toBe(false);
|
||||
});
|
||||
|
||||
it('handles the src/bin/<name>/main.rs directory form', () => {
|
||||
expect(moduleOfFile('src/bin/other/main.rs', WITH_BIN)).toMatchObject({
|
||||
crateRoot: 'src/bin/other',
|
||||
segments: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Rust opts out of the shared "already namespaced" guard (#2741 review).
|
||||
*
|
||||
* `tagNamespacePrefixes` skips a def whose `qualifiedName` already equals, or is
|
||||
* prefixed by, its enclosing namespace path — correct for C++/C#, where the
|
||||
* qualified name really does carry the namespace. Rust qualified names never do,
|
||||
* so that guard fired on a coincidence: in `mod a { pub fn a() }` the member's
|
||||
* name equals its module's name, the prefix was skipped, and the member was left
|
||||
* looking like it belonged to the PARENT module.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { emitRustScopeCaptures } from '../../../../src/core/ingestion/languages/rust/index.js';
|
||||
import { extract } from '../../../../src/core/ingestion/scope-extractor.js';
|
||||
import { rustProvider } from '../../../../src/core/ingestion/languages/rust.js';
|
||||
import { populateRustOwners } from '../../../../src/core/ingestion/languages/rust/method-owners.js';
|
||||
|
||||
function defsFor(source: string) {
|
||||
const parsed = extract(emitRustScopeCaptures(source, 'src/lib.rs'), 'src/lib.rs', rustProvider);
|
||||
populateRustOwners(parsed);
|
||||
return parsed.localDefs;
|
||||
}
|
||||
|
||||
describe('Rust namespace-prefix tagging', () => {
|
||||
it('tags a member whose name matches its own module', () => {
|
||||
const fn = defsFor('pub mod a { pub fn a() -> usize { 1 } }\n').find(
|
||||
(d) => d.type === 'Function',
|
||||
);
|
||||
expect(fn).toMatchObject({ qualifiedName: 'a', namespacePrefix: 'a' });
|
||||
});
|
||||
|
||||
it('tags a member whose name differs from its module', () => {
|
||||
const fn = defsFor('pub mod tools { pub fn dispatch() -> usize { 1 } }\n').find(
|
||||
(d) => d.type === 'Function',
|
||||
);
|
||||
expect(fn).toMatchObject({ qualifiedName: 'dispatch', namespacePrefix: 'tools' });
|
||||
});
|
||||
|
||||
it('composes nested module prefixes', () => {
|
||||
const fn = defsFor('pub mod outer { pub mod inner { pub fn f() -> usize { 1 } } }\n').find(
|
||||
(d) => d.type === 'Function',
|
||||
);
|
||||
expect(fn).toMatchObject({ namespacePrefix: 'outer.inner' });
|
||||
});
|
||||
|
||||
it('leaves a crate-root item untagged', () => {
|
||||
const fn = defsFor('pub fn f() -> usize { 1 }\n').find((d) => d.type === 'Function');
|
||||
expect(fn?.namespacePrefix).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not tag the module def itself', () => {
|
||||
const ns = defsFor('pub mod a { pub fn a() -> usize { 1 } }\n').find(
|
||||
(d) => d.type === 'Namespace',
|
||||
);
|
||||
expect(ns?.namespacePrefix).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue