mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +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>
203 lines
6.6 KiB
TypeScript
203 lines
6.6 KiB
TypeScript
/**
|
|
* 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: [],
|
|
});
|
|
});
|
|
});
|