GitNexus/gitnexus/test/integration/resolvers/rust.test.ts
Copilot bb68cc1eb0
Extract resolveMemberCall from resolveCallTarget (SM-11) (#744)
* Initial plan

* feat(SM-11): extract resolveMemberCall from resolveCallTarget

- Create resolveMemberCall(ownerType, methodName, currentFile, ctx, heritageMap?)
  that uses owner-scoped + MRO resolution only (no fuzzy lookup)
- resolveCallTarget delegates member calls (D0 path) to resolveMemberCall
- walkMixedChain uses resolveMemberCall for owner-scoped member-call resolution
- Add 7 unit tests for resolveMemberCall covering direct, inherited, MRO,
  null cases, and confidence tier assertions
- Export resolveMemberCall for external use

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3b7889a9-5f2f-4572-8904-45084210f10d

* fix(SM-11): address PR #744 review

Blocking fixes:

- B1: Revert unrelated package-lock.json gitnexus-shared addition

- B2: Document confidence-tier semantic change on resolveMemberCall

Performance / coupling fixes:

- S1: walkMixedChain now calls resolveMethodByOwner directly (hot path) to avoid throwaway ResolveResult allocation per chain step

- S2: Thread tier from resolveMethodByOwner via { def, tier } tuple; eliminates double ctx.resolve

Alignment with semantic-model plan (Phase 3 target):

- resolveMethodByOwner now iterates ALL class-like candidates from ctx.resolve, deduplicating matches by nodeId. Absorbs D4's ownerId-filtering into the owner-scoped path.

- Handles homonym classes (two Users in different files) without falling through to D1-D4 fuzzy widening

- Shared-ancestor MRO walks automatically dedup (both homonyms walk to same base method)

- Unified direct-vs-MRO lookup under a single canWalkMRO check

Tests added:

- T1: Three D0 skip-condition tests via new _resolveCallTargetForTesting internal export (overloadHints, preComputedArgTypes, hasActiveModuleAlias)

- T2: Rust qualified-syntax null test (trait-inherited method) + direct impl control

- T3: C++ leftmost-base diamond inheritance test

- B2 lock-in: cross-file class tier assertion

- Homonym disambiguation: only-one-owns-method, both-own-method ambiguity, shared-ancestor MRO convergence

Verification:

- tsc --noEmit: clean

- vitest run test/unit/: 3014 passed

- vitest run test/integration/resolvers/: 1746 passed

* test(SM-11): address second PR #744 review round + per-language integration tests

Review fixes (https://github.com/abhigyanpatwari/GitNexus/pull/744#issuecomment-4211877593):

P1 (Performance): Replace Map allocation in resolveMethodByOwner with a firstDef+ambiguous flag pattern. Zero allocation for the common single-candidate case on the hot path — the previous Map approach allocated on every member call regardless of whether deduplication was needed.

P2 (Test gap): Strengthen the module-alias D0 skip test with a homonym fixture (two Users in different files). Previously the test passed whether or not D0 was actually bypassed; the new version proves D0 must be skipped by showing that resolveMemberCall directly returns null (ambiguous) but D1-D4 with alias narrowing picks the right one. Also fixes the underlying D2-vs-alias widening interaction: when filteredCandidates was narrowed by module-alias disambiguation, D2 no longer widens back to the full fuzzy pool (introduces aliasNarrowed boolean flag).

L1 (Language coverage): Add C# and Kotlin implements-split tests at the resolveMemberCall layer.

L2 (Maintainability): Export OverloadHints as @internal so the test can use a direct cast instead of fragile Parameters<...> type inference.

Per-language integration tests:

- rust-child-extends-parent: Direct impl method resolution via D0 (with honest documentation of the trait-method-as-Function gap that is Phase 5 / SM-16 scope)

- java-interface-default-method: User implements Validator with default method resolved via implements-split MRO

- csharp-interface-default-method: Same pattern for C# 8.0+ default interface methods

- kotlin-interface-default-method: Same pattern for Kotlin interfaces with default implementations

- python-multi-level-mro: 3-level C3 linearization (Grandparent ← Parent ← Child)

- cpp-diamond-inheritance: Classic diamond (Base ← A, B ← Derived) via leftmost-base MRO

Verification:

- tsc --noEmit: clean

- vitest run test/unit/: 3015 passed

- vitest run test/integration/resolvers/: 1763 passed (+17 new per-language tests)

* fix(SM-11): Codex adversarial review corrections + deeper D0 fixes

Addresses the three high-severity findings from the Codex adversarial review of PR #744 (https://github.com/abhigyanpatwari/GitNexus/pull/744#issuecomment-4212075120), plus four deeper fixes discovered during regression triage. All discovered issues are now addressed end-to-end rather than papered over with tail-return fallbacks.

Codex review findings:

R1 (C++ diamond): The cpp-diamond-inheritance fixture used non-virtual inheritance, which is genuinely ambiguous in real C++ (two Base subobjects). Changed A and B to use 'virtual public Base' so there's a single shared Base subobject and d.method() is an unambiguous call that the leftmost-base MRO walk correctly resolves.

R2 (C# default-interface): The csharp-interface-default-method fixture called user.Validate() via a User-typed variable, but C# does not inherit default interface methods as callable class members — the call is only valid through an interface-typed variable. Changed App.cs to 'IValidator user = new User(...)' which is the idiomatic dispatch pattern.

R3 (resolveCallTarget tail-return): When D1-D4 receiver filtering produced zero file-matched and zero owner-matched candidates for a member call, the function fell through to the permissive single-candidate tail return — silently emitting CALLS edges for methods that don't belong to the receiver. Added an explicit null-route inside the D1-D4 block that fires only when both filters yielded 0.

R4 (Rust negative assertion): Added the c.trait_only() negative integration test in rust.test.ts demonstrating that direct member calls on Rust structs do not walk trait ancestry. The test now passes because of R3 (previously fell through to the tail return).

Regression triage discoveries:

1. D0 was dead code on the sequential pipeline. The sequential path sets overloadHints for every call regardless of whether the method is overloaded, and the original D0 skip condition '!overloadHints && !preComputedArgTypes' was therefore always false. The Java/C#/C++ SM-9/SM-10 inheritance tests were passing ONLY via the tail-return fallback. Fix: narrow the skip to 'overloadHints && filteredCandidates.length > 1' — skip D0 only when there are actually multiple candidates that need overload disambiguation.

2. lookupMethodByOwner couldn't disambiguate arity-differing overloads (e.g. C++ greet() vs greet(string)). With D0 now firing on the sequential path, same-name/different-arity overloads would collapse to an arbitrary first pick. Fix: added an optional argCount parameter to lookupMethodByOwner + lookupMethodByOwnerWithMRO that filters the overload set by parameterCount/requiredParameterCount before the returnType dedup.

3. Python and Rust class methods are captured as Function nodes (not Method) with ownerId set to the class. The methodByOwner index only accepted 'Method' and 'Constructor' types, so Python class methods and Rust trait methods were invisible to D0. Fix: extended the methodByOwner indexing condition to include 'Function' when ownerId is set. This also unlocks the Rust trait-method negative assertion by ensuring the qualified-syntax MRO strategy has something to return null for.

4. D0 was being skipped when a local variable shadowed an imported module name (Python 'from models.c import C; c = C()' creates both a module alias 'c → models/c.py' AND a typed local 'c'). Fix: the D0 skip now gates on 'aliasNarrowed' (a new boolean tracking whether the alias block actually narrowed filteredCandidates) instead of 'hasActiveModuleAlias'. If the method isn't in the aliased module, the receiver is a typed local variable and D0 should run.

5. PHP trait walk missed the HasTimestamps trait because lookupClassByName did not include 'Trait' type. buildHeritageMap uses lookupClassByName to resolve parent names, so 'BaseModel use HasTimestamps' was failing to register an ancestor edge for BaseModel → HasTimestamps. Fix: added 'Trait' to CLASS_TYPES. The trait is now a valid class-like type for heritage resolution (PHP use, Rust impl Trait for Struct, Scala traits).

Test updates:

- Updated the 'no heritageMap' unit test in call-processor.test.ts to assert the correct null-route behavior instead of the old tail-return fallback.

- Added a new unit test asserting Trait inclusion in the class set.

- Updated the 'does NOT include other type-like labels' test to remove Trait from its rejection set.

Verification:

- tsc --noEmit: clean

- vitest run test/unit/: 3016 passed (+1 new Trait inclusion test)

- vitest run test/integration/resolvers/: 1764 passed (+1 new Rust negative assertion)

- Zero regressions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-09 09:52:12 +01:00

1922 lines
75 KiB
TypeScript

/**
* Rust: trait implementations + ambiguous module import disambiguation
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import {
FIXTURES,
CROSS_FILE_FIXTURES,
getRelationships,
getNodesByLabel,
getNodesByLabelFull,
edgeSet,
runPipelineFromRepo,
type PipelineResult,
} from './helpers.js';
// ---------------------------------------------------------------------------
// Heritage: trait implementations
// ---------------------------------------------------------------------------
describe('Rust trait implementation resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-traits'), () => {});
}, 60000);
it('detects exactly 1 struct and 2 traits', () => {
expect(getNodesByLabel(result, 'Struct')).toEqual(['Button']);
expect(getNodesByLabel(result, 'Trait')).toEqual(['Clickable', 'Drawable']);
});
it('emits exactly 2 IMPLEMENTS edges with reason trait-impl', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(implements_.length).toBe(2);
expect(edgeSet(implements_)).toEqual(['Button → Clickable', 'Button → Drawable']);
for (const edge of implements_) {
expect(edge.rel.reason).toBe('trait-impl');
}
});
it('does not emit any EXTENDS edges for trait impls', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(extends_.length).toBe(0);
});
it('resolves exactly 1 IMPORTS edge: main.rs → button.rs', () => {
const imports = getRelationships(result, 'IMPORTS');
expect(imports.length).toBe(1);
expect(imports[0].source).toBe('main.rs');
expect(imports[0].target).toBe('button.rs');
});
it('detects 2 modules and functions (trait signatures + impls)', () => {
expect(getNodesByLabel(result, 'Module')).toEqual(['impls', 'traits']);
const fns = getNodesByLabel(result, 'Function');
// With function_signature_item captured, trait abstract methods AND their
// concrete impls both appear (distinct qualified IDs, same name)
expect(fns).toContain('main');
expect(fns).toContain('draw');
expect(fns).toContain('is_enabled');
expect(fns).toContain('on_click');
expect(fns).toContain('resize');
// draw/is_enabled/on_click/resize appear twice (trait + impl)
expect(fns.filter((n) => n === 'draw')).toHaveLength(2);
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
expect(target!.label).not.toBe('Property');
}
});
});
// ---------------------------------------------------------------------------
// Ambiguous: Handler struct in two modules, crate:: import disambiguates
// ---------------------------------------------------------------------------
describe('Rust ambiguous symbol resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-ambiguous'), () => {});
}, 60000);
it('detects 2 Handler structs in separate modules', () => {
const structs: string[] = [];
result.graph.forEachNode((n) => {
if (n.label === 'Struct') structs.push(`${n.properties.name}@${n.properties.filePath}`);
});
const handlers = structs.filter((s) => s.startsWith('Handler@'));
expect(handlers.length).toBe(2);
expect(handlers.some((h) => h.includes('src/models/'))).toBe(true);
expect(handlers.some((h) => h.includes('src/other/'))).toBe(true);
});
it('import resolves to src/models/mod.rs (not src/other/mod.rs)', () => {
const imports = getRelationships(result, 'IMPORTS');
const modelsImport = imports.find((e) => e.targetFilePath.includes('models'));
expect(modelsImport).toBeDefined();
expect(modelsImport!.targetFilePath).toBe('src/models/mod.rs');
});
it('no import edge to src/other/', () => {
const imports = getRelationships(result, 'IMPORTS');
for (const imp of imports) {
expect(imp.targetFilePath).not.toMatch(/src\/other\//);
}
});
});
describe('Rust call resolution with arity filtering', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-calls'), () => {});
}, 60000);
it('resolves main → write_audit to src/onearg/mod.rs via arity narrowing', () => {
const calls = getRelationships(result, 'CALLS');
expect(calls.length).toBe(1);
expect(calls[0].source).toBe('main');
expect(calls[0].target).toBe('write_audit');
expect(calls[0].targetFilePath).toBe('src/onearg/mod.rs');
expect(calls[0].rel.reason).toBe('import-resolved');
});
});
// ---------------------------------------------------------------------------
// Member-call resolution: obj.method() resolves through pipeline
// ---------------------------------------------------------------------------
describe('Rust member-call resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-member-calls'), () => {});
}, 60000);
it('resolves process_user → save as a member call on User', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save');
expect(saveCall).toBeDefined();
expect(saveCall!.source).toBe('process_user');
expect(saveCall!.targetFilePath).toBe('src/user.rs');
});
it('detects User struct and save function (Rust impl fns are Function nodes)', () => {
const structs: string[] = [];
result.graph.forEachNode((n) => {
if (n.label === 'Struct') structs.push(n.properties.name);
});
expect(structs).toContain('User');
// Rust tree-sitter captures all function_item as Function, including impl methods
expect(getNodesByLabel(result, 'Function')).toContain('save');
});
});
// ---------------------------------------------------------------------------
// Struct literal resolution: User { ... } resolves to Struct node
// ---------------------------------------------------------------------------
describe('Rust struct literal resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-struct-literals'), () => {});
}, 60000);
it('resolves User { ... } as a CALLS edge to the User struct', () => {
const calls = getRelationships(result, 'CALLS');
const ctorCall = calls.find((c) => c.target === 'User');
expect(ctorCall).toBeDefined();
expect(ctorCall!.source).toBe('process_user');
expect(ctorCall!.targetLabel).toBe('Struct');
expect(ctorCall!.targetFilePath).toBe('user.rs');
expect(ctorCall!.rel.reason).toBe('import-resolved');
});
it('also resolves user.save() as a member call', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save');
expect(saveCall).toBeDefined();
expect(saveCall!.source).toBe('process_user');
});
it('detects User struct and process_user function', () => {
const structs: string[] = [];
result.graph.forEachNode((n) => {
if (n.label === 'Struct') structs.push(n.properties.name);
});
expect(structs).toContain('User');
expect(getNodesByLabel(result, 'Function')).toContain('process_user');
});
});
// ---------------------------------------------------------------------------
// Receiver-constrained resolution: typed variables disambiguate same-named methods
// ---------------------------------------------------------------------------
describe('Rust receiver-constrained resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-receiver-resolution'), () => {});
}, 60000);
it('detects User and Repo structs, both with save functions', () => {
const structs: string[] = [];
result.graph.forEachNode((n) => {
if (n.label === 'Struct') structs.push(n.properties.name);
});
expect(structs).toContain('User');
expect(structs).toContain('Repo');
// Rust tree-sitter captures impl fns as Function nodes
const saveFns = getNodesByLabel(result, 'Function').filter((m) => m === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves user.save() to User.save and repo.save() to Repo.save via receiver typing', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((c) => c.target === 'save');
expect(saveCalls.length).toBe(2);
const userSave = saveCalls.find((c) => c.targetFilePath === 'src/user.rs');
const repoSave = saveCalls.find((c) => c.targetFilePath === 'src/repo.rs');
expect(userSave).toBeDefined();
expect(repoSave).toBeDefined();
expect(userSave!.source).toBe('process_entities');
expect(repoSave!.source).toBe('process_entities');
});
});
// ---------------------------------------------------------------------------
// Alias import resolution: use crate::models::User as U resolves U → User
// ---------------------------------------------------------------------------
describe('Rust alias import resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-alias-imports'), () => {});
}, 60000);
it('detects User and Repo structs with their methods', () => {
const structs: string[] = [];
result.graph.forEachNode((n) => {
if (n.label === 'Struct') structs.push(n.properties.name);
});
expect(structs).toContain('User');
expect(structs).toContain('Repo');
expect(getNodesByLabel(result, 'Function')).toContain('save');
expect(getNodesByLabel(result, 'Function')).toContain('persist');
});
it('resolves u.save() to src/models.rs and r.persist() to src/models.rs via alias', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save');
const persistCall = calls.find((c) => c.target === 'persist');
expect(saveCall).toBeDefined();
expect(saveCall!.source).toBe('main');
expect(saveCall!.targetFilePath).toBe('src/models.rs');
expect(persistCall).toBeDefined();
expect(persistCall!.source).toBe('main');
expect(persistCall!.targetFilePath).toBe('src/models.rs');
});
it('emits exactly 1 IMPORTS edge: src/main.rs → src/models.rs', () => {
const imports = getRelationships(result, 'IMPORTS');
expect(imports.length).toBe(1);
expect(imports[0].sourceFilePath).toBe('src/main.rs');
expect(imports[0].targetFilePath).toBe('src/models.rs');
});
});
// ---------------------------------------------------------------------------
// Local shadow: same-file definition takes priority over imported name
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Re-export chain: pub use in mod.rs followed through to definition file
// ---------------------------------------------------------------------------
describe('Rust re-export chain resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-reexport-chain'), () => {});
}, 60000);
it('detects Handler struct in handler.rs', () => {
const structs: string[] = [];
result.graph.forEachNode((n) => {
if (n.label === 'Struct') structs.push(`${n.properties.name}@${n.properties.filePath}`);
});
expect(structs).toContain('Handler@src/models/handler.rs');
});
it('resolves Handler { ... } to src/models/handler.rs via re-export chain, not mod.rs', () => {
const calls = getRelationships(result, 'CALLS');
const ctorCall = calls.find((c) => c.target === 'Handler');
expect(ctorCall).toBeDefined();
expect(ctorCall!.source).toBe('main');
expect(ctorCall!.targetLabel).toBe('Struct');
expect(ctorCall!.targetFilePath).toBe('src/models/handler.rs');
});
it('resolves h.process() to src/models/handler.rs', () => {
const calls = getRelationships(result, 'CALLS');
const processCall = calls.find((c) => c.target === 'process');
expect(processCall).toBeDefined();
expect(processCall!.source).toBe('main');
expect(processCall!.targetFilePath).toBe('src/models/handler.rs');
});
});
// ---------------------------------------------------------------------------
// Local shadow: same-file definition takes priority over imported name
// ---------------------------------------------------------------------------
describe('Rust local definition shadows import', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-local-shadow'), () => {});
}, 60000);
it('resolves run → save to same-file definition, not the imported one', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'run');
expect(saveCall).toBeDefined();
expect(saveCall!.targetFilePath).toBe('src/main.rs');
});
it('does NOT resolve save to utils.rs', () => {
const calls = getRelationships(result, 'CALLS');
const saveToUtils = calls.find(
(c) => c.target === 'save' && c.targetFilePath === 'src/utils.rs',
);
expect(saveToUtils).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Grouped imports: use crate::helpers::{func_a, func_b}
// Verifies no spurious binding for the path prefix (e.g. "helpers")
// ---------------------------------------------------------------------------
describe('Rust grouped import resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-grouped-imports'), () => {});
}, 60000);
it('resolves main → format_name to src/helpers/mod.rs', () => {
const calls = getRelationships(result, 'CALLS');
const call = calls.find((c) => c.target === 'format_name');
expect(call).toBeDefined();
expect(call!.source).toBe('main');
expect(call!.targetFilePath).toBe('src/helpers/mod.rs');
expect(call!.rel.reason).toBe('import-resolved');
});
it('resolves main → validate_email to src/helpers/mod.rs', () => {
const calls = getRelationships(result, 'CALLS');
const call = calls.find((c) => c.target === 'validate_email');
expect(call).toBeDefined();
expect(call!.source).toBe('main');
expect(call!.targetFilePath).toBe('src/helpers/mod.rs');
expect(call!.rel.reason).toBe('import-resolved');
});
it('does not create a spurious CALLS edge for the path prefix "helpers"', () => {
const calls = getRelationships(result, 'CALLS');
const spurious = calls.find((c) => c.target === 'helpers' || c.source === 'helpers');
expect(spurious).toBeUndefined();
});
it('emits exactly 1 IMPORTS edge: main.rs → helpers/mod.rs', () => {
const imports = getRelationships(result, 'IMPORTS');
expect(imports.length).toBe(1);
expect(imports[0].source).toBe('main.rs');
expect(imports[0].target).toBe('mod.rs');
expect(imports[0].targetFilePath).toBe('src/helpers/mod.rs');
});
});
// ---------------------------------------------------------------------------
// Scoped grouped imports with multi-file resolution:
// use crate::models::{User, Repo} where User and Repo are in separate files.
// Verifies IMPORTS edges are created for each file AND namedImportMap entries
// match bindings to files by basename.
// ---------------------------------------------------------------------------
describe('Rust scoped grouped imports (multi-file)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-scoped-multi-file'), () => {});
}, 60000);
it('detects User and Repo structs', () => {
const classes = getNodesByLabel(result, 'Struct');
expect(classes).toContain('User');
expect(classes).toContain('Repo');
});
it('emits IMPORTS edge from main.rs to models/mod.rs', () => {
const imports = getRelationships(result, 'IMPORTS');
const edge = imports.find(
(e) => e.sourceFilePath.includes('main') && e.targetFilePath.includes('models'),
);
expect(edge).toBeDefined();
});
it('resolves user.save() call to User#save in models/user.rs', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) => c.target === 'save' && c.source === 'main' && c.targetFilePath.includes('user'),
);
expect(saveCall).toBeDefined();
});
it('resolves repo.clone_repo() call to Repo#clone_repo in models/repo.rs', () => {
const calls = getRelationships(result, 'CALLS');
const cloneCall = calls.find(
(c) => c.target === 'clone_repo' && c.source === 'main' && c.targetFilePath.includes('repo'),
);
expect(cloneCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Constructor-inferred type resolution: let user = User::new(); user.save()
// Rust scoped_identifier constructor pattern (no explicit type annotations)
// ---------------------------------------------------------------------------
describe('Rust constructor-inferred type resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'rust-constructor-type-inference'),
() => {},
);
}, 60000);
it('detects User and Repo structs, both with save methods', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((m) => m === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves user.save() to src/user.rs via constructor-inferred type', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find((c) => c.target === 'save' && c.targetFilePath === 'src/user.rs');
expect(userSave).toBeDefined();
expect(userSave!.source).toBe('process_entities');
});
it('resolves repo.save() to src/repo.rs via constructor-inferred type', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find((c) => c.target === 'save' && c.targetFilePath === 'src/repo.rs');
expect(repoSave).toBeDefined();
expect(repoSave!.source).toBe('process_entities');
});
it('emits exactly 2 save() CALLS edges (one per receiver type)', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((c) => c.target === 'save');
expect(saveCalls.length).toBe(2);
});
});
// ---------------------------------------------------------------------------
// self.save() resolves to enclosing impl's own save method
// ---------------------------------------------------------------------------
describe('Rust self resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-self-this-resolution'), () => {});
}, 60000);
it('detects User and Repo structs, each with a save function', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((m) => m === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves self.save() inside User::process to User::save, not Repo::save', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'process');
expect(saveCall).toBeDefined();
expect(saveCall!.targetFilePath).toBe('src/user.rs');
});
});
// ---------------------------------------------------------------------------
// Trait impl emits IMPLEMENTS edge
// ---------------------------------------------------------------------------
describe('Rust parent resolution (trait impl)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-parent-resolution'), () => {});
}, 60000);
it('detects User struct and Serializable trait', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Trait')).toContain('Serializable');
});
it('emits IMPLEMENTS edge: User → Serializable (trait impl)', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(implements_.length).toBe(1);
expect(implements_[0].source).toBe('User');
expect(implements_[0].target).toBe('Serializable');
expect(implements_[0].rel.reason).toBe('trait-impl');
});
it('no EXTENDS edges (Rust has no class inheritance)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(extends_.length).toBe(0);
});
});
// ---------------------------------------------------------------------------
// Struct literal inference: let user = User { ... }; user.save()
// ---------------------------------------------------------------------------
describe('Rust struct literal type inference', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'rust-struct-literal-inference'),
() => {},
);
}, 60000);
it('resolves user.save() via struct literal inference (User { ... })', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save' && c.targetFilePath === 'models.rs');
expect(saveCall).toBeDefined();
expect(saveCall!.source).toBe('main');
});
it('resolves config.validate() via struct literal inference (Config { ... })', () => {
const calls = getRelationships(result, 'CALLS');
const validateCall = calls.find(
(c) => c.target === 'validate' && c.targetFilePath === 'models.rs',
);
expect(validateCall).toBeDefined();
expect(validateCall!.source).toBe('main');
});
});
// ---------------------------------------------------------------------------
// Rust Self {} struct literal: Self resolves to enclosing impl type
// ---------------------------------------------------------------------------
describe('Rust Self {} struct literal resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-self-struct-literal'), () => {});
}, 60000);
it('resolves fresh.validate() inside impl User via Self {} inference', () => {
const calls = getRelationships(result, 'CALLS');
const validateCall = calls.find((c) => c.target === 'validate' && c.source === 'blank');
expect(validateCall).toBeDefined();
expect(validateCall!.targetFilePath).toBe('models.rs');
});
});
// ---------------------------------------------------------------------------
// if let / while let: captured_pattern type extraction
// Extracts type from `user @ User { .. }` patterns in if-let/while-let
// ---------------------------------------------------------------------------
describe('Rust if-let captured_pattern type resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-if-let'), () => {});
}, 60000);
it('detects User and Config structs with their methods', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Config');
expect(getNodesByLabel(result, 'Function')).toContain('save');
expect(getNodesByLabel(result, 'Function')).toContain('validate');
});
it('resolves user.save() inside if-let via captured_pattern binding', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'process_if_let');
expect(saveCall).toBeDefined();
expect(saveCall!.targetFilePath).toBe('models.rs');
});
it('resolves cfg.validate() inside while-let via captured_pattern binding', () => {
const calls = getRelationships(result, 'CALLS');
const validateCall = calls.find(
(c) => c.target === 'validate' && c.source === 'process_while_let',
);
expect(validateCall).toBeDefined();
expect(validateCall!.targetFilePath).toBe('models.rs');
});
});
// ---------------------------------------------------------------------------
// Return type inference: let user = get_user("alice"); user.save()
// Plain function call (no ::new) with no type annotation
// ---------------------------------------------------------------------------
describe('Rust return type inference', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-return-type'), () => {});
}, 60000);
it('detects User struct and get_user + save functions', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Function')).toContain('get_user');
expect(getNodesByLabel(result, 'Function')).toContain('save');
});
it('resolves main → get_user as a CALLS edge to src/models.rs', () => {
const calls = getRelationships(result, 'CALLS');
const getUserCall = calls.find((c) => c.target === 'get_user' && c.source === 'main');
expect(getUserCall).toBeDefined();
expect(getUserCall!.targetFilePath).toBe('src/models.rs');
});
it('resolves user.save() to src/models.rs via return-type-inferred binding', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'main');
expect(saveCall).toBeDefined();
expect(saveCall!.targetFilePath).toBe('src/models.rs');
});
});
// ---------------------------------------------------------------------------
// Return-type inference with competing methods:
// Two structs both have save(), factory functions disambiguate via return type
// ---------------------------------------------------------------------------
describe('Rust return-type inference via function return type', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-return-type-inference'), () => {});
}, 60000);
it('resolves user.save() to models.rs User#save via return type of get_user()', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_user' && c.targetFilePath.includes('models'),
);
expect(saveCall).toBeDefined();
});
it('user.save() does NOT resolve to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find((c) => c.target === 'save' && c.source === 'process_user');
// Should resolve to exactly one target — if it resolves at all, check it's the right one
if (wrongSave) {
expect(wrongSave.targetFilePath).toContain('models');
}
});
it('resolves repo.save() to models.rs Repo#save via return type of get_repo()', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_repo' && c.targetFilePath.includes('models'),
);
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Rust ::default() constructor resolution — scanner exclusion
// ---------------------------------------------------------------------------
describe('Rust ::default() constructor resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-default-constructor'), () => {});
}, 60000);
it('detects User and Repo structs', () => {
const structs = getNodesByLabel(result, 'Struct');
expect(structs).toContain('User');
expect(structs).toContain('Repo');
});
it('detects save methods on both structs', () => {
const methods = [...getNodesByLabel(result, 'Function'), ...getNodesByLabel(result, 'Method')];
expect(methods.filter((m: string) => m === 'save').length).toBe(2);
});
it('resolves user.save() in process_with_new() via User::new() constructor', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_with_new' &&
c.targetFilePath.includes('user.rs'),
);
expect(saveCall).toBeDefined();
});
it('resolves user.save() in process_with_default() via User::default() constructor', () => {
// User::default() should be resolved by extractInitializer (Tier 1),
// NOT by the scanner — the scanner excludes ::default() to avoid
// wasted cross-file lookups on the broadly-implemented Default trait
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_with_default' &&
c.targetFilePath.includes('user.rs'),
);
expect(saveCall).toBeDefined();
});
it('disambiguates repo.save() in process_with_default() to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_with_default' &&
c.targetFilePath.includes('repo.rs'),
);
expect(repoSave).toBeDefined();
});
it('does NOT cross-contaminate (user.save() does not resolve to Repo#save)', () => {
const calls = getRelationships(result, 'CALLS');
// In process_with_new: user.save() should go to user.rs, not repo.rs
const wrongCall = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_with_new' &&
c.targetFilePath.includes('repo.rs'),
);
// Either undefined (correctly disambiguated) or present (both resolved) — no single wrong one
if (wrongCall) {
// If both are present, there should also be a correct one
const correctCall = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_with_new' &&
c.targetFilePath.includes('user.rs'),
);
expect(correctCall).toBeDefined();
}
});
});
// ---------------------------------------------------------------------------
// Rust async .await constructor binding resolution
// Verifies that `let user = create_user().await` correctly unwraps the
// await_expression to find the call_expression underneath, producing a
// constructor binding that enables receiver-based disambiguation of user.save().
// ---------------------------------------------------------------------------
describe('Rust async .await constructor binding resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-async-binding'), () => {});
}, 60000);
it('detects User and Repo structs', () => {
const structs = getNodesByLabel(result, 'Struct');
expect(structs).toContain('User');
expect(structs).toContain('Repo');
});
it('detects save methods in separate files', () => {
const methods = [...getNodesByLabel(result, 'Function'), ...getNodesByLabel(result, 'Method')];
expect(methods.filter((m: string) => m === 'save').length).toBe(2);
});
it('resolves user.save() after .await to user.rs via return type of get_user()', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_user' && c.targetFilePath.includes('user'),
);
expect(saveCall).toBeDefined();
});
it('user.save() does NOT resolve to Repo#save in repo.rs', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_user' && c.targetFilePath.includes('repo'),
);
expect(wrongSave).toBeUndefined();
});
it('resolves repo.save() after .await to repo.rs via return type of get_repo()', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_repo' && c.targetFilePath.includes('repo'),
);
expect(saveCall).toBeDefined();
});
it('repo.save() does NOT resolve to User#save in user.rs', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_repo' && c.targetFilePath.includes('user'),
);
expect(wrongSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Nullable receiver: let user: Option<User> = find_user(); user.unwrap().save()
// Rust Option<User> — stripNullable unwraps Option wrapper to inner type.
// ---------------------------------------------------------------------------
describe('Rust nullable receiver resolution (Option<T>)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-nullable-receiver'), () => {});
}, 60000);
it('detects User and Repo structs, both with save functions', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((m) => m === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves user.unwrap().save() to User#save via Option<User> unwrapping', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_entities' &&
c.targetFilePath?.includes('user'),
);
expect(userSave).toBeDefined();
});
it('resolves repo.unwrap().save() to Repo#save via Option<Repo> unwrapping', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_entities' &&
c.targetFilePath?.includes('repo'),
);
expect(repoSave).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Assignment chain propagation (Phase 4.3)
// ---------------------------------------------------------------------------
describe('Rust assignment chain propagation', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-assignment-chain'), () => {});
}, 60000);
it('detects User and Repo structs each with a save function', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((m) => m === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves alias.save() to User#save via assignment chain', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_entities' &&
c.targetFilePath?.includes('user.rs'),
);
expect(userSave).toBeDefined();
});
it('resolves r_alias.save() to Repo#save via assignment chain', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_entities' &&
c.targetFilePath?.includes('repo.rs'),
);
expect(repoSave).toBeDefined();
});
it('alias.save() does NOT resolve to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((c) => c.target === 'save' && c.source === 'process_entities');
expect(saveCalls.filter((c) => c.targetFilePath?.includes('user.rs')).length).toBe(1);
expect(saveCalls.filter((c) => c.targetFilePath?.includes('repo.rs')).length).toBe(1);
});
});
// ---------------------------------------------------------------------------
// Rust Option<User> receiver resolution — extractSimpleTypeName unwraps
// Option<User> to "User" via NULLABLE_WRAPPER_TYPES. The variable declared
// as Option<User> now stores "User" in TypeEnv, enabling direct receiver
// disambiguation without chained .unwrap() inference.
// ---------------------------------------------------------------------------
describe('Rust Option<User> receiver resolution via wrapper unwrapping', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-option-receiver'), () => {});
}, 60000);
it('detects User and Repo structs each with a save function', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((m) => m === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves alias.save() to User#save via Option<User> → assignment chain', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_entities' &&
c.targetFilePath?.includes('user.rs'),
);
expect(userSave).toBeDefined();
});
it('resolves repo.save() to Repo#save alongside Option usage', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_entities' &&
c.targetFilePath?.includes('repo.rs'),
);
expect(repoSave).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// if let Some(user) = opt — Phase 5.2 pattern binding: unwrap Option<T>
// `opt: Option<User>` → Option<User> is stored as "User" in TypeEnv via
// NULLABLE_WRAPPER_TYPES. extractPatternBinding maps `user` → "User".
// Disambiguation: User.save vs Repo.save — only User.save should be called.
// ---------------------------------------------------------------------------
describe('Rust if-let Some(x) = opt pattern binding (Phase 5.2)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-if-let-unwrap'), () => {});
}, 60000);
it('detects User and Repo structs each with a save function', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((f) => f === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves user.save() inside if-let Some(user) = opt to User#save', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) => c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('user.rs'),
);
expect(userSave).toBeDefined();
});
it('does NOT resolve user.save() to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) => c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('repo.rs'),
);
expect(repoSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Rust if-let Err(e) = res pattern binding (Phase 5 review fix)
// Result<User, AppError> → Err(e) should type e as AppError (typeArgs[1]).
// Also tests Ok(user) in the same fixture to verify both arms work.
// ---------------------------------------------------------------------------
describe('Rust if-let Err(e) pattern binding (Phase 5 review fix)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-err-unwrap'), () => {});
}, 60000);
it('detects User and AppError structs', () => {
const structs = getNodesByLabel(result, 'Struct');
expect(structs).toContain('User');
expect(structs).toContain('AppError');
});
it('resolves e.report() inside if-let Err(e) to AppError#report', () => {
const calls = getRelationships(result, 'CALLS');
const reportCall = calls.find(
(c) =>
c.target === 'report' &&
c.source === 'handle_err' &&
c.targetFilePath?.includes('error.rs'),
);
expect(reportCall).toBeDefined();
});
it('resolves user.save() inside if-let Ok(user) to User#save', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'handle_ok' && c.targetFilePath?.includes('user.rs'),
);
expect(saveCall).toBeDefined();
});
it('does NOT resolve e.report() to User#save (no cross-contamination)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongCall = calls.find((c) => c.target === 'save' && c.source === 'handle_err');
expect(wrongCall).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Chained method calls: svc.get_user().save()
// Tests that Rust chain call resolution correctly infers the intermediate
// receiver type from get_user()'s return type and resolves save() to User.
// ---------------------------------------------------------------------------
describe('Rust chained method call resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-chain-call'), () => {});
}, 60000);
it('detects User and Repo structs, and UserService', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
expect(getNodesByLabel(result, 'Struct')).toContain('UserService');
});
it('detects get_user and save functions', () => {
const fns = getNodesByLabel(result, 'Function');
expect(fns).toContain('get_user');
expect(fns).toContain('save');
});
it('resolves svc.get_user().save() to User#save via chain resolution', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_user' && c.targetFilePath?.includes('user.rs'),
);
expect(userSave).toBeDefined();
});
it('does NOT resolve svc.get_user().save() to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_user' && c.targetFilePath?.includes('repo.rs'),
);
expect(repoSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Rust for-loop Tier 1c: for user in &users with Vec<User> parameter
// ---------------------------------------------------------------------------
describe('Rust for-loop type resolution (Tier 1c)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-for-loop'), () => {});
}, 60000);
it('detects User and Repo structs with save functions', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((f) => f === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves user.save() in for-loop to User#save via Tier 1c', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_users' &&
c.targetFilePath?.includes('user.rs'),
);
expect(userSave).toBeDefined();
});
it('does NOT resolve user.save() to Repo#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_users' &&
c.targetFilePath?.includes('repo.rs'),
);
expect(wrongSave).toBeUndefined();
});
it('resolves repo.save() in for-loop to Repo#save via Tier 1c', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_repos' &&
c.targetFilePath?.includes('repo.rs'),
);
expect(repoSave).toBeDefined();
});
it('does NOT resolve repo.save() to User#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_repos' &&
c.targetFilePath?.includes('user.rs'),
);
expect(wrongSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Rust match arm: match opt { Some(user) => user.save() }
// ---------------------------------------------------------------------------
describe('Rust match arm type resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-match-unwrap'), () => {});
}, 60000);
it('detects User and Repo structs with save functions', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((f) => f === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves user.save() inside match Some(user) to User#save', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) => c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('user.rs'),
);
expect(userSave).toBeDefined();
});
it('does NOT resolve user.save() in match to Repo#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) => c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('repo.rs'),
);
expect(wrongSave).toBeUndefined();
});
it('resolves repo.save() inside if-let Ok(repo) to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) => c.target === 'save' && c.source === 'check' && c.targetFilePath?.includes('repo.rs'),
);
expect(repoSave).toBeDefined();
});
it('does NOT resolve repo.save() in if-let to User#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) => c.target === 'save' && c.source === 'check' && c.targetFilePath?.includes('user.rs'),
);
expect(wrongSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// for user in users.iter() — call_expression iterable resolution
// ---------------------------------------------------------------------------
describe('Rust .iter() for-loop call_expression resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-iter-for-loop'), () => {});
}, 60000);
it('detects User and Repo structs with save functions', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((f) => f === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves user.save() via users.iter() to User#save', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_users' &&
c.targetFilePath?.includes('user.rs'),
);
expect(userSave).toBeDefined();
});
it('resolves repo.save() via repos.into_iter() to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_repos' &&
c.targetFilePath?.includes('repo.rs'),
);
expect(repoSave).toBeDefined();
});
it('does NOT cross-resolve user.save() to Repo#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_users' &&
c.targetFilePath?.includes('repo.rs'),
);
expect(wrongSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// for user in get_users() — direct call_expression iterable resolution
// Phase 7.3: unlike rust-iter-for-loop (typed variable .iter()), this tests
// iterating over a function call's return value directly.
// ---------------------------------------------------------------------------
describe('Rust for-loop direct call_expression iterable resolution (Phase 7.3)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-for-call-expr'), () => {});
}, 60000);
it('detects User and Repo structs with competing save functions', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
const saveFns = getNodesByLabel(result, 'Function').filter((f) => f === 'save');
expect(saveFns.length).toBe(2);
});
it('resolves user.save() in for-loop over get_users() to User#save', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_users' &&
c.targetFilePath?.includes('user.rs'),
);
expect(userSave).toBeDefined();
});
it('resolves repo.save() in for-loop over get_repos() to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_repos' &&
c.targetFilePath?.includes('repo.rs'),
);
expect(repoSave).toBeDefined();
});
it('does NOT resolve user.save() to Repo#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_users' &&
c.targetFilePath?.includes('repo.rs'),
);
expect(wrongSave).toBeUndefined();
});
it('does NOT resolve repo.save() to User#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'process_repos' &&
c.targetFilePath?.includes('user.rs'),
);
expect(wrongSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Phase 8: Field/property type resolution — struct field capture
// ---------------------------------------------------------------------------
describe('Field type resolution (Rust)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-field-types'), () => {});
}, 60000);
it('detects structs: Address, User', () => {
expect(getNodesByLabel(result, 'Struct')).toEqual(['Address', 'User']);
});
it('detects Property nodes for Rust struct fields', () => {
const properties = getNodesByLabel(result, 'Property');
expect(properties).toContain('address');
expect(properties).toContain('name');
expect(properties).toContain('city');
});
it('emits HAS_PROPERTY edges linking fields to structs', () => {
const propEdges = getRelationships(result, 'HAS_PROPERTY');
expect(propEdges.length).toBe(3);
expect(edgeSet(propEdges)).toContain('User → name');
expect(edgeSet(propEdges)).toContain('User → address');
expect(edgeSet(propEdges)).toContain('Address → city');
});
it('resolves user.address.save() → Address#save via field type', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((e) => e.target === 'save' && e.source === 'process_user');
expect(saveCalls.length).toBe(1);
expect(saveCalls[0].targetFilePath).toContain('models');
});
it('populates field metadata (visibility, isReadonly, declaredType) on Property nodes', () => {
const properties = getNodesByLabelFull(result, 'Property');
const city = properties.find((p) => p.name === 'city');
expect(city).toBeDefined();
expect(city!.properties.visibility).toBe('public');
expect(city!.properties.isStatic).toBe(false);
expect(city!.properties.isReadonly).toBe(true);
expect(city!.properties.declaredType).toBe('String');
const addr = properties.find((p) => p.name === 'address');
expect(addr).toBeDefined();
expect(addr!.properties.visibility).toBe('public');
expect(addr!.properties.isReadonly).toBe(true);
expect(addr!.properties.declaredType).toBe('Address');
});
});
// ---------------------------------------------------------------------------
// Phase 8B: Deep field chain resolution (3-level)
// ---------------------------------------------------------------------------
describe('Deep field chain resolution (Rust)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-deep-field-chain'), () => {});
}, 60000);
it('detects structs: Address, City, User', () => {
expect(getNodesByLabel(result, 'Struct')).toEqual(['Address', 'City', 'User']);
});
it('detects Property nodes for Rust struct fields', () => {
const properties = getNodesByLabel(result, 'Property');
expect(properties).toContain('address');
expect(properties).toContain('city');
expect(properties).toContain('zip_code');
});
it('emits HAS_PROPERTY edges for nested type chain', () => {
const propEdges = getRelationships(result, 'HAS_PROPERTY');
expect(propEdges.length).toBe(5);
expect(edgeSet(propEdges)).toContain('User → name');
expect(edgeSet(propEdges)).toContain('User → address');
expect(edgeSet(propEdges)).toContain('Address → city');
expect(edgeSet(propEdges)).toContain('Address → street');
expect(edgeSet(propEdges)).toContain('City → zip_code');
});
it('resolves 2-level chain: user.address.save() → Address#save', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((e) => e.target === 'save' && e.source === 'process_user');
const addressSave = saveCalls.find((e) => e.targetFilePath.includes('models'));
expect(addressSave).toBeDefined();
});
it('resolves 3-level chain: user.address.city.get_name() → City#get_name', () => {
const calls = getRelationships(result, 'CALLS');
const getNameCalls = calls.filter(
(e) => e.target === 'get_name' && e.source === 'process_user',
);
const cityGetName = getNameCalls.find((e) => e.targetFilePath.includes('models'));
expect(cityGetName).toBeDefined();
});
});
// ACCESSES write edges from assignment expressions
// ---------------------------------------------------------------------------
describe('Write access tracking (Rust)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-write-access'), () => {});
}, 60000);
it('emits ACCESSES write edges for field assignments', () => {
const accesses = getRelationships(result, 'ACCESSES');
const writes = accesses.filter((e) => e.rel.reason === 'write');
expect(writes.length).toBe(3);
const fieldNames = writes.map((e) => e.target);
expect(fieldNames).toContain('name');
expect(fieldNames).toContain('address');
expect(fieldNames).toContain('score');
const sources = writes.map((e) => e.source);
expect(sources).toContain('update_user');
});
it('write ACCESSES edges have confidence 1.0', () => {
const accesses = getRelationships(result, 'ACCESSES');
const writes = accesses.filter((e) => e.rel.reason === 'write');
for (const edge of writes) {
expect(edge.rel.confidence).toBe(1.0);
}
});
it('emits ACCESSES write edge for compound assignment', () => {
const accesses = getRelationships(result, 'ACCESSES');
const writes = accesses.filter((e) => e.rel.reason === 'write');
const scoreWrite = writes.find((e) => e.target === 'score');
expect(scoreWrite).toBeDefined();
expect(scoreWrite!.source).toBe('update_user');
});
});
// ---------------------------------------------------------------------------
// Call-result variable binding (Phase 9): let user = get_user(); user.save()
// ---------------------------------------------------------------------------
describe('Rust call-result variable binding (Tier 2b)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-call-result-binding'), () => {});
}, 60000);
it('resolves user.save() to User#save via call-result binding', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_user' && c.targetFilePath.includes('models'),
);
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Method chain binding (Phase 9C): get_user() → .address → .get_city() → .save()
// ---------------------------------------------------------------------------
describe('Rust method chain binding via unified fixpoint (Phase 9C)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-method-chain-binding'), () => {});
}, 60000);
it('resolves city.save() to City#save via method chain', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'process_chain' && c.targetFilePath.includes('models'),
);
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Phase A: Rust struct_pattern destructuring — let Point { x, y } = p
// Each field emits a fieldAccess PendingAssignment; fixpoint resolves x/y → Vec2
// ---------------------------------------------------------------------------
describe('Rust struct_pattern destructuring resolution (Phase A)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-struct-destructuring'), () => {});
}, 60000);
it('detects Point and Vec2 structs', () => {
const classes = getNodesByLabel(result, 'Struct');
expect(classes).toContain('Point');
expect(classes).toContain('Vec2');
});
it('resolves x.save() to Vec2#save via struct destructuring', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) => c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('vec2'),
);
expect(saveCall).toBeDefined();
});
it('resolves both x.save() and y.save() — emits at least 1 CALLS to Vec2#save', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((c) => c.target === 'save' && c.targetFilePath.includes('vec2'));
// Both x and y are Vec2 — the same function, so calls may deduplicate to 1
expect(saveCalls.length).toBeGreaterThanOrEqual(1);
});
});
// ---------------------------------------------------------------------------
// Phase 14: Cross-file binding propagation
// src/models.rs exports User struct with save() and get_name() methods
// src/factory.rs exports get_user() -> User (uses crate::models::User)
// src/main.rs uses crate::factory::get_user, calls u.save() / u.get_name()
// → u is typed User via cross-file return type propagation
// ---------------------------------------------------------------------------
describe('Rust cross-file binding propagation', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(CROSS_FILE_FIXTURES, 'rs-cross-file'), () => {});
}, 60000);
it('detects User struct with save and get_name methods', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Function')).toContain('save');
expect(getNodesByLabel(result, 'Function')).toContain('get_name');
});
it('detects get_user and process functions', () => {
expect(getNodesByLabel(result, 'Function')).toContain('get_user');
expect(getNodesByLabel(result, 'Function')).toContain('process');
});
it('emits IMPORTS edge from main.rs to factory.rs', () => {
const imports = getRelationships(result, 'IMPORTS');
const edge = imports.find(
(e) => e.sourceFilePath.includes('main') && e.targetFilePath.includes('factory'),
);
expect(edge).toBeDefined();
});
it('resolves u.save() in process() to User#save via cross-file return type propagation', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) => c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('models'),
);
expect(saveCall).toBeDefined();
});
it('resolves u.get_name() in process() to User#get_name via cross-file return type propagation', () => {
const calls = getRelationships(result, 'CALLS');
const getNameCall = calls.find(
(c) =>
c.target === 'get_name' && c.source === 'process' && c.targetFilePath.includes('models'),
);
expect(getNameCall).toBeDefined();
});
it('emits HAS_METHOD edges linking save and get_name to User', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const saveEdge = hasMethod.find((e) => e.source === 'User' && e.target === 'save');
const getNameEdge = hasMethod.find((e) => e.source === 'User' && e.target === 'get_name');
expect(saveEdge).toBeDefined();
expect(getNameEdge).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Method enrichment: trait vs inherent impl, isAbstract, isStatic, annotations
// ---------------------------------------------------------------------------
describe('Rust method enrichment (trait + inherent impl)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-method-enrichment'), () => {});
}, 60000);
it('detects Dog struct and Animal trait', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('Dog');
expect(getNodesByLabel(result, 'Trait')).toContain('Animal');
});
it('emits IMPLEMENTS edge from Dog to Animal', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
const edge = implements_.find((e) => e.source === 'Dog' && e.target === 'Animal');
expect(edge).toBeDefined();
});
it('emits HAS_METHOD edges for all Dog methods (trait + inherent)', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const dogMethods = hasMethod
.filter((e) => e.source === 'Dog')
.map((e) => e.target)
.sort();
expect(dogMethods).toContain('speak');
expect(dogMethods).toContain('fetch');
expect(dogMethods).toContain('new');
expect(dogMethods).toContain('wag');
});
it('emits HAS_METHOD edges for Animal trait methods (abstract + default)', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const traitMethods = hasMethod
.filter((e) => e.source === 'Animal')
.map((e) => e.target)
.sort();
expect(traitMethods).toContain('breathe');
// With function_signature_item query, abstract speak is also captured
expect(traitMethods).toContain('speak');
});
// With the function_signature_item query, abstract trait speak IS captured.
// Due to ID collision (both trait and impl speak share Function:src/lib.rs:speak),
// only the first-processed node survives — the abstract one from the trait.
// TODO: Phase 2 (qualified IDs) will disambiguate both nodes.
it('captures abstract trait speak via function_signature_item query', () => {
const methods = getNodesByLabelFull(result, 'Function');
const traitSpeak = methods.find(
(m) => m.name === 'speak' && m.properties.filePath?.includes('lib.rs'),
);
expect(traitSpeak).toBeDefined();
expect(traitSpeak!.properties.isAbstract).toBe(true);
});
it('marks trait default method breathe as isAbstract=false', () => {
const methods = getNodesByLabelFull(result, 'Function');
const breathe = methods.find(
(m) => m.name === 'breathe' && m.properties.filePath?.includes('lib.rs'),
);
expect(breathe).toBeDefined();
expect(breathe!.properties.isAbstract).toBe(false);
});
it('marks Dog::new() as isStatic=true (no self parameter)', () => {
const methods = getNodesByLabelFull(result, 'Function');
const newFn = methods.find(
(m) => m.name === 'new' && m.properties.filePath?.includes('lib.rs'),
);
expect(newFn).toBeDefined();
expect(newFn!.properties.isStatic).toBe(true);
});
it('records parameterTypes for fetch(&self, item: &str)', () => {
const methods = getNodesByLabelFull(result, 'Function');
const fetchFn = methods.find(
(m) => m.name === 'fetch' && m.properties.filePath?.includes('lib.rs'),
);
expect(fetchFn).toBeDefined();
expect(fetchFn!.properties.parameterTypes).toContain('str');
});
it('records #[inline] annotation on wag()', () => {
const methods = getNodesByLabelFull(result, 'Function');
const wagFn = methods.find(
(m) => m.name === 'wag' && m.properties.filePath?.includes('lib.rs'),
);
expect(wagFn).toBeDefined();
expect(wagFn!.properties.annotations).toContain('#[inline]');
});
it('uses Impl source label for HAS_METHOD edges from inherent impl', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
// Dog inherent impl (plain `impl Dog {}`) → Impl label
const dogImplEdges = hasMethod.filter(
(e) =>
e.source === 'Dog' && (e.target === 'new' || e.target === 'wag' || e.target === 'fetch'),
);
for (const edge of dogImplEdges) {
expect(edge.sourceLabel).toBe('Impl');
}
});
it('resolves main.rs calls: Dog::new(), dog.speak(), dog.fetch()', () => {
const calls = getRelationships(result, 'CALLS');
const mainCalls = calls.filter((c) => c.source === 'main');
const newCall = mainCalls.find((c) => c.target === 'new');
const speakCall = mainCalls.find((c) => c.target === 'speak');
const fetchCall = mainCalls.find((c) => c.target === 'fetch');
expect(newCall).toBeDefined();
expect(speakCall).toBeDefined();
expect(fetchCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Abstract dispatch: trait required vs default methods, IMPLEMENTS + HAS_METHOD
// ---------------------------------------------------------------------------
describe('Rust abstract dispatch (Repository trait)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-abstract-dispatch'), () => {});
}, 60000);
it('detects SqlRepo struct and Repository trait', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('SqlRepo');
expect(getNodesByLabel(result, 'Trait')).toContain('Repository');
});
it('emits IMPLEMENTS edge from SqlRepo to Repository', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
const edge = implements_.find((e) => e.source === 'SqlRepo' && e.target === 'Repository');
expect(edge).toBeDefined();
});
it('emits HAS_METHOD edge for Repository default method count', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const traitMethods = hasMethod
.filter((e) => e.source === 'Repository')
.map((e) => e.target)
.sort();
// Only default (non-abstract) methods get HAS_METHOD on the trait itself
expect(traitMethods).toContain('count');
});
it('emits HAS_METHOD edges linking find and save to SqlRepo (not Repository)', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const sqlRepoMethods = hasMethod
.filter((e) => e.source === 'SqlRepo')
.map((e) => e.target)
.sort();
// impl Repository for SqlRepo methods should be owned by SqlRepo (concrete type)
expect(sqlRepoMethods).toContain('find');
expect(sqlRepoMethods).toContain('save');
});
it('uses Struct source label for HAS_METHOD edges from trait impl (impl Trait for Struct)', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
// impl Repository for SqlRepo → Struct label (no Impl node for trait impls)
const sqlRepoEdges = hasMethod.filter(
(e) => e.source === 'SqlRepo' && (e.target === 'find' || e.target === 'save'),
);
for (const edge of sqlRepoEdges) {
expect(edge.sourceLabel).toBe('Struct');
}
});
it('uses Trait source label for HAS_METHOD edge on Repository default method', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const traitCount = hasMethod.find((e) => e.source === 'Repository' && e.target === 'count');
expect(traitCount).toBeDefined();
expect(traitCount!.sourceLabel).toBe('Trait');
});
it('marks trait find/save as isAbstract=true and impl find/save as isAbstract=false', () => {
const methods = getNodesByLabelFull(result, 'Function');
// With qualified IDs, both abstract (trait) and concrete (impl) find/save exist
const abstractFind = methods.find((m) => m.name === 'find' && m.properties.isAbstract === true);
const concreteFind = methods.find(
(m) => m.name === 'find' && m.properties.isAbstract === false,
);
const abstractSave = methods.find((m) => m.name === 'save' && m.properties.isAbstract === true);
const concreteSave = methods.find(
(m) => m.name === 'save' && m.properties.isAbstract === false,
);
expect(abstractFind).toBeDefined();
expect(concreteFind).toBeDefined();
expect(abstractSave).toBeDefined();
expect(concreteSave).toBeDefined();
});
it('marks default trait method count as isAbstract=false', () => {
const methods = getNodesByLabelFull(result, 'Function');
const countFn = methods.find(
(m) => m.name === 'count' && m.properties.filePath?.includes('lib.rs'),
);
expect(countFn).toBeDefined();
expect(countFn!.properties.isAbstract).toBe(false);
});
it('records parameterTypes for find(&self, id: i32)', () => {
const methods = getNodesByLabelFull(result, 'Function');
const findFn = methods.find(
(m) => m.name === 'find' && m.properties.filePath?.includes('lib.rs'),
);
expect(findFn).toBeDefined();
expect(findFn!.properties.parameterTypes).toContain('i32');
});
it('records parameterTypes for save(&self, entity: &str)', () => {
const methods = getNodesByLabelFull(result, 'Function');
const saveFn = methods.find(
(m) => m.name === 'save' && m.properties.filePath?.includes('lib.rs'),
);
expect(saveFn).toBeDefined();
expect(saveFn!.properties.parameterTypes).toContain('str');
});
it('resolves process() calls: repo.find(), repo.save(), repo.count()', () => {
const calls = getRelationships(result, 'CALLS');
const processCalls = calls.filter((c) => c.source === 'process');
const findCall = processCalls.find((c) => c.target === 'find');
const saveCall = processCalls.find((c) => c.target === 'save');
const countCall = processCalls.find((c) => c.target === 'count');
expect(findCall).toBeDefined();
expect(saveCall).toBeDefined();
expect(countCall).toBeDefined();
});
it('emits METHOD_IMPLEMENTS edges from SqlRepo impl methods → Repository trait methods', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
// find and save are required trait methods; count has a default impl so no METHOD_IMPLEMENTS
const libEdges = mi.filter((e) => e.sourceFilePath.includes('lib.rs'));
expect(libEdges.length).toBe(2);
const names = libEdges.map((e) => e.source).sort();
expect(names).toEqual(['find', 'save']);
});
});
// ---------------------------------------------------------------------------
// SM-11: Rust Child extends Parent — qualified-syntax MRO
//
// Companion integration test for the unit-level Rust qualified-syntax tests
// in symbol-table.test.ts. Validates end-to-end that:
//
// 1. Direct `impl` methods on a struct resolve through the D0 owner-scoped
// path (`resolveMemberCall`) — the positive control.
//
// 2. Trait-inherited default methods are NOT reachable via direct
// `obj.trait_method()` syntax. Rust requires the trait to be in scope
// and uses qualified syntax for trait dispatch; the resolver correctly
// treats direct member calls as opaque to trait ancestry.
//
// Previously this case emitted a false-positive CALLS edge via the
// permissive tail-return in resolveCallTarget — Codex review finding
// R3 (PR #744). The tail-return is now null-routed when D1-D4 receiver
// filtering produces zero matches on both file and owner dimensions.
// ---------------------------------------------------------------------------
describe('Rust Child extends Parent — qualified-syntax MRO (SM-11)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-child-extends-parent'), () => {});
}, 60000);
it('detects Child struct and Parent trait', () => {
const structs = getNodesByLabel(result, 'Struct');
expect(structs).toContain('Child');
const traits = getNodesByLabel(result, 'Trait');
expect(traits).toContain('Parent');
});
it('resolves c.own_method() to Child::own_method via D0 owner-scoped path', () => {
// Direct impl method — D0 short-circuits to lookupMethodByOwner which
// returns Child::own_method without falling through to D1-D4 fuzzy.
const calls = getRelationships(result, 'CALLS');
const ownCall = calls.find(
(c) =>
c.target === 'own_method' && c.source === 'run' && c.targetFilePath.includes('child.rs'),
);
expect(ownCall).toBeDefined();
});
it('does NOT resolve c.trait_only() to Parent::trait_only via direct member call', () => {
// Qualified-syntax MRO: direct member calls on structs do not walk trait
// ancestry. `c.trait_only()` must null-route because `trait_only` is
// defined on the trait, not on the Child struct.
//
// The resolveCallTarget tail-return tightening (R3) is what makes this
// assertion testable: before the fix, resolveCallTarget would fall
// through D1-D4 (zero file matches, zero owner matches) and silently
// pick the single fuzzy candidate as a false-positive edge.
const calls = getRelationships(result, 'CALLS');
const traitCall = calls.find(
(c) =>
c.target === 'trait_only' && c.source === 'run' && c.targetFilePath.includes('parent.rs'),
);
expect(traitCall).toBeUndefined();
});
});