mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
* Initial plan * feat: add Rust scope-resolution hooks (RFC #909 Ring 3) Implement the scope-based resolution pipeline for Rust, following the established pattern from Go and other migrated languages. New files in gitnexus/src/core/ingestion/languages/rust/: - query.ts: tree-sitter scope query covering scopes, declarations, imports, type bindings, and references - cache-stats.ts: parse cache hit/miss counters - import-decomposer.ts: decomposes use declarations into individual import captures (handles grouped, wildcard, renamed, re-exported) - receiver-binding.ts: synthesizes self type bindings for impl methods - interpret.ts: interprets captures into ParsedImport/ParsedTypeBinding - arity.ts: arity compatibility checker (no overloading in Rust) - merge-bindings.ts: local-shadows-import binding merge strategy - simple-hooks.ts: binding scope, import owning scope, receiver binding - import-target.ts: resolves Rust module paths (crate/super/self) - method-owners.ts: bridges impl block methods to struct defs - captures.ts: main emit function with import decomposition and self-binding synthesis - scope-resolver.ts: ScopeResolver implementation - index.ts: barrel re-exports Wiring changes: - rust.ts: add scope hook imports and properties to defineLanguage - registry.ts: register rustScopeResolver in SCOPE_RESOLVERS - registry-primary-flag.ts: add Rust to MIGRATED_LANGUAGES Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * feat(LANG-rust): add scope-resolution hooks and register Rust ScopeResolver Implements RFC #909 Ring 3 deliverables: - query.ts: tree-sitter scope query for Rust - captures.ts: emitRustScopeCaptures with method reclassification - import-decomposer.ts: use statement decomposition (groups, renames, globs) - interpret.ts: interpretRustImport + interpretRustTypeBinding - import-target.ts: crate/module/super/self path resolution - receiver-binding.ts: self/&self/&mut self receiver synthesis - method-owners.ts: impl block → struct ownership bridging - arity.ts: no-overloading arity check - merge-bindings.ts: local > import > wildcard binding precedence - simple-hooks.ts: binding/import scope, receiver binding - scope-resolver.ts: ScopeResolver contract implementation - Wired into rustProvider (rust.ts) with scope hooks - Registered in SCOPE_RESOLVERS (pipeline/registry.ts) - NOT yet added to MIGRATED_LANGUAGES (29 advanced pattern tests pending) Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/8f14b730-79d4-4356-9505-325750d71f84 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * Add Rust scope-resolution integration tests (RFC #909 Ring 3) Tests cover the core deliverables for the Rust scope-resolution pipeline: - impl blocks and trait implementations - Module resolution (crate::, super::, self::) - Struct fields and type bindings - Self/&self/&mut self receiver binding - Generic functions (V1 ignores generic args) - Grouped imports (use foo::{A, B}) - Renamed imports (use foo::Bar as Baz) - Arity checking (no overloading) - Struct literal constructor inference - Return type inference - Scoped/qualified calls (Foo::new()) - Enum declarations - Multiple impl blocks - Free function calls - Typed let bindings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * test(LANG-rust): add 28 scope-resolution integration tests Covers 15 test suites validating: impl blocks, trait impls, grouped imports, renamed imports, module resolution, receiver binding, arity filtering, struct literal inference, return type inference, qualified calls, struct fields, enums, multiple impl blocks, free calls, and typed let bindings. Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/8f14b730-79d4-4356-9505-325750d71f84 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * feat(LANG-rust): add implicit crate path fallback, 35 scope tests - Import resolver now falls back to crate-relative for unqualified module paths (Rust 2015 edition compat) - Added 7 more test cases: re-exports, shadowing, closures, default trait methods (35 total, exceeding ≥30 requirement) Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/8f14b730-79d4-4356-9505-325750d71f84 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * feat(rust): add Rust to MIGRATED_LANGUAGES with 100% scope-resolution parity 35/35 integration tests pass under both REGISTRY_PRIMARY_RUST=0 (legacy) and =1 (scope-resolution). Rust scope-resolution is now the default production call-resolution path. * test(rust): add pipeline benchmark matching PHP benchmark pattern Generates synthetic Rust codebases at 100/250/500 files with structs, impl blocks, traits, cross-module use declarations, and method calls. Measures wall-clock time, peak heap, and scaling ratios. Results: sub-linear scaling (0.79x ratio), 500 files in 3.8s with workers, 110MB peak heap. Gated by GITNEXUS_BENCH=1. * chore(autofix): apply prettier + eslint fixes via /autofix command * perf(rust): optimize captures, type normalization, and method-owner linking - Cache findEnclosingImpl result to avoid duplicate tree walk per function - Avoid double namedChild accessor call in struct field arity counting - Extract regex constants (REF_PREFIX_RE, PTR_PREFIX_RE) from hot normalization loops - Replace O(s) suffix-match scan with O(1) Map lookup in method-owner linking Benchmark: 500 files 3806ms → 2970ms (-22%), 250 files 2432ms → 1752ms (-28%) * fix(rust): resolve CodeQL alerts — file-system race and dead code - Remove existsSync+appendFileSync/writeFileSync TOCTOU in benchmark fixture generator; appendFileSync creates if missing - Remove always-true guard on computeRustCallArity return; narrow return type from number|undefined to number - Remove no-op .filter() in fixture generator * feat(rust): hoist impl return-type bindings to struct scope for chain resolution Synthesize a module-level duplicate of @type-binding.return captures for methods inside impl blocks. The scope-extractor's auto-hoist places these on the struct's Class scope, making them visible to the compound receiver chain resolver via classScopeByDefId. Without this, method return types are only on the impl block scope (which has no class-like def and is not indexed by classScopeByDefId), so chains like svc.get_user().save() cannot follow the intermediate return type. This is the structural prerequisite for chain resolution, pattern binding, and for-loop element-type parity (28 remaining tests). The cross-file return-type propagation step still needs wiring for full parity. * fix(rust): use implNode anchor for return-type hoisting — fixes chain resolution Use the enclosing impl_item node (not tree.rootNode) as the synthetic capture anchor. The scope-extractor's auto-hoist places bindings whose anchor matches the innermost scope on the parent scope. With implNode, the binding lands on the Module scope (parent of impl's Class scope), giving declaredAtScope the correct context for findClassBindingInScope to resolve the return type across the scope chain. Unlocks: chain calls (svc.get_user().save()), return-type inference, assignment chains, cross-file binding propagation, call-result binding, deep field chains — 28→27 failing tests. * feat(rust): add populateRangeBindings hook + .await query capture Implement populateRustRangeBindings (Phase 2 hook, same pattern as Go's populateGoRangeBindings) to populate type bindings that need runtime type lookup — for-loop element types, if-let/while-let captured patterns, match arm patterns, and struct destructuring field types. Also add tree-sitter query capture for let x = fn().await — unwraps await_expression to find the inner call_expression. 28→19 failing tests: fixes for-loop Tier 1c, .iter()/.into_iter(), async .await, if-let captured_pattern. * fix(rust): fix tuple_struct_pattern variable extraction + Result<T,E> raw type lookup - Skip wrapper type identifier when finding bound variable in tuple_struct_pattern (Some(user) was binding 'Some' not 'user') - Add lookupRawParameterType to read unstripped generic type from AST for Ok/Err pattern resolution (normalizeRustTypeName strips generics) 28→15 failing tests: fixes if-let Some, if-let Ok/Err, match arm patterns. * fix(rust): fix match_arm parent traversal + raw return type for for-loop calls - Walk up from match_arm through match_block to find match_expression for source variable extraction - Add lookupRawFunctionReturnType to find unstripped return type from AST for same-file for-loop call expression iterables 28→14 failing tests. * feat(rust): inject field type bindings on struct scopes for chain resolution Walk struct_item AST nodes and inject field types (e.g., address -> Address) as typeBindings on the struct's Class scope. The compound receiver chain resolver uses these to follow field chains like user.address.save(). Also fixes: match_arm parent traversal to match_expression, lookupFieldType to check typeBindings first. 28→11 failing tests: fixes field type chains, deep chains, struct destructuring. * feat(rust): cross-file return type lookup for for-loop call iterables Build allReturnTypes map across all parsedFiles in Phase 2 first pass, then use it to resolve for-loop iterables like `for x in get_fn()` when get_fn is defined in another file. 28→9 failing tests. * feat(rust): cross-file field type map for struct destructuring Build allFieldTypes map across parsedFiles in Phase 2 first pass. Used by processStructDestructuring to resolve `let Point { x, y } = p` when Point is defined in another file. 28→7 failing tests. * fix(rust): compound assignment write capture + pending assignment fixpoint - Add compound_assignment_expr query for +=, -=, etc. field writes - Add processPendingAssignments with 3-pass fixpoint for field access and method call result variable bindings (let addr = user.address, let city = addr.get_city()) 28→5 failing tests. * fix(rust): identity method return-type bindings for unwrap/expect chains Inject unwrap/expect/clone/as_ref/as_mut as return-type bindings on struct scopes that return the struct's own type. Since normalizeRustTypeName already unwraps Option<T> → T, calling .unwrap() on a value typed as T is semantically an identity — the return type equals the receiver type. 28→3 failing tests: fixes user.unwrap().save() and repo.unwrap().save() chains. * fix(rust): skip enum variant call-return bindings + cross-file pending assignments + identity alias - Skip Some/None/Ok/Err in @type-binding.call-return — these are enum variant constructors, not type names; let the annotation capture win - Add identifier alias handler in processPendingAssignments for `let alias = opt` chains - Cross-file field type and method return type lookup in pending assignment fixpoint via findFieldTypeAcrossFiles/findMethodReturnTypeAcrossFiles - Identity method bindings (unwrap/expect/clone) on struct scopes 155/156 tests pass (99.4%). Remaining: trait default method dispatch via MRO (repo.count() where count has default impl on Repository trait). * feat(rust): 100% scope-resolution parity — MRO with same-file IMPLEMENTS + trait default method reclassification - Add buildRustMro that includes same-file IMPLEMENTS edges in the MRO chain, so trait default methods (e.g., repo.count()) resolve through the struct → trait ancestry walk - Only add IMPLEMENTS to MRO when struct and trait are in the same file; cross-file trait calls require the trait to be imported (Rust semantics) - Reclassify function_item inside trait_item as @declaration.method so default trait methods register in the model's methods lookup 156/156 legacy parity tests pass. 35/35 scope tests pass. 0 regressions. * fix(rust): address code review findings — null guard, name collision, scope order - Fix Array.find() null guard: check === undefined not === null in processCapturedPattern (find() never returns null) - Fix allReturnTypes/allFieldTypes name collision: delete entry on second occurrence so colliding names (new, default, Config) produce no result rather than a wrong result - Fix lookupTypeInScopes: search function scope then module scope only, skip unrelated Class scopes that could shadow names from other functions --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Test <test@example.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1023 lines
26 KiB
TypeScript
1023 lines
26 KiB
TypeScript
/**
|
|
* Rust scope-resolution integration tests (RFC #909 Ring 3).
|
|
*
|
|
* These tests run with REGISTRY_PRIMARY_RUST=true to exercise the
|
|
* scope-based resolution path. They validate the core deliverables:
|
|
* impl blocks, use statements, receiver binding, module resolution.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import path from 'path';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import {
|
|
getRelationships,
|
|
getNodesByLabel,
|
|
runPipelineFromRepo,
|
|
type PipelineResult,
|
|
} from './helpers.js';
|
|
|
|
function writeFixtureRepo(root: string, files: Record<string, string>): void {
|
|
for (const [rel, content] of Object.entries(files)) {
|
|
const abs = path.join(root, rel);
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, content, 'utf8');
|
|
}
|
|
}
|
|
|
|
let savedEnv: string | undefined;
|
|
|
|
beforeAll(() => {
|
|
savedEnv = process.env['REGISTRY_PRIMARY_RUST'];
|
|
process.env['REGISTRY_PRIMARY_RUST'] = 'true';
|
|
});
|
|
|
|
afterAll(() => {
|
|
if (savedEnv === undefined) delete process.env['REGISTRY_PRIMARY_RUST'];
|
|
else process.env['REGISTRY_PRIMARY_RUST'] = savedEnv;
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. Impl blocks: methods classified as Method, owned by struct
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: impl block method classification', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-impl-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod user;
|
|
use crate::user::User;
|
|
|
|
fn main() {
|
|
let u = User { name: String::new() };
|
|
u.save();
|
|
}
|
|
`,
|
|
'src/user.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
pub fn create(name: String) -> User {
|
|
User { name }
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects User struct', () => {
|
|
expect(getNodesByLabel(result, 'Struct')).toContain('User');
|
|
});
|
|
|
|
it('classifies save as a function owned by User (HAS_METHOD edge)', () => {
|
|
const edges = getRelationships(result, 'HAS_METHOD');
|
|
const userSave = edges.find((e) => e.source === 'User' && e.target === 'save');
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('emits HAS_METHOD edge from User to save', () => {
|
|
const edges = getRelationships(result, 'HAS_METHOD');
|
|
const userSave = edges.find((e) => e.source === 'User' && e.target === 'save');
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('resolves main → u.save() as CALLS edge to save in user.rs', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const mainSave = calls.find(
|
|
(c) => c.target === 'save' && c.source === 'main' && c.targetFilePath?.includes('user.rs'),
|
|
);
|
|
expect(mainSave).toBeDefined();
|
|
});
|
|
|
|
it('emits IMPORTS edge from main.rs to user.rs', () => {
|
|
const imports = getRelationships(result, 'IMPORTS');
|
|
const imp = imports.find((e) => e.targetFilePath?.includes('user.rs'));
|
|
expect(imp).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Trait implementations: IMPLEMENTS edges + method resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: trait implementation', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-trait-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod shapes;
|
|
use crate::shapes::{Circle, Drawable};
|
|
|
|
fn main() {
|
|
let c = Circle { radius: 5.0 };
|
|
c.draw();
|
|
}
|
|
`,
|
|
'src/shapes.rs': `
|
|
pub trait Drawable {
|
|
fn draw(&self);
|
|
}
|
|
|
|
pub struct Circle {
|
|
pub radius: f64,
|
|
}
|
|
|
|
impl Drawable for Circle {
|
|
fn draw(&self) {
|
|
println!("Drawing circle");
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects Drawable trait and Circle struct', () => {
|
|
expect(getNodesByLabel(result, 'Trait')).toContain('Drawable');
|
|
expect(getNodesByLabel(result, 'Struct')).toContain('Circle');
|
|
});
|
|
|
|
it('emits IMPLEMENTS edge from Circle to Drawable', () => {
|
|
const impls = getRelationships(result, 'IMPLEMENTS');
|
|
const circleDrawable = impls.find((e) => e.source === 'Circle' && e.target === 'Drawable');
|
|
expect(circleDrawable).toBeDefined();
|
|
});
|
|
|
|
it('resolves main → c.draw() to shapes.rs', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const drawCall = calls.find((c) => c.target === 'draw' && c.source === 'main');
|
|
expect(drawCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. Grouped imports: use crate::models::{User, Config}
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: grouped imports', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-grouped-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod models;
|
|
use crate::models::{User, Config};
|
|
|
|
fn process() {
|
|
let u = User { name: String::new() };
|
|
u.save();
|
|
let c = Config { debug: true };
|
|
c.validate();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/models.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
|
|
pub struct Config {
|
|
pub debug: bool,
|
|
}
|
|
|
|
impl Config {
|
|
pub fn validate(&self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves grouped import to models.rs', () => {
|
|
const imports = getRelationships(result, 'IMPORTS');
|
|
const modelsImport = imports.find((e) => e.targetFilePath?.includes('models.rs'));
|
|
expect(modelsImport).toBeDefined();
|
|
});
|
|
|
|
it('resolves u.save() to User#save via grouped import binding', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'process');
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
|
|
it('resolves c.validate() to Config#validate via grouped import binding', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const validateCall = calls.find((c) => c.target === 'validate' && c.source === 'process');
|
|
expect(validateCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 4. Renamed imports: use Foo as Bar
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: renamed imports (use as)', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-alias-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod models;
|
|
use crate::models::User as U;
|
|
|
|
fn process() {
|
|
let u = U { name: String::new() };
|
|
u.save();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/models.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('emits IMPORTS edge to models.rs', () => {
|
|
const imports = getRelationships(result, 'IMPORTS');
|
|
const imp = imports.find((e) => e.targetFilePath?.includes('models.rs'));
|
|
expect(imp).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5. Module resolution: super:: and self::
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: module resolution (crate/super/self)', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-modules-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod models;
|
|
mod services;
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/models.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
`,
|
|
'src/services.rs': `
|
|
use crate::models::User;
|
|
|
|
pub fn process() {
|
|
let u = User { name: String::new() };
|
|
u.save();
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves crate::models::User import from services.rs to models.rs', () => {
|
|
const imports = getRelationships(result, 'IMPORTS');
|
|
const imp = imports.find(
|
|
(e) => e.targetFilePath?.includes('models.rs') && e.sourceFilePath?.includes('services.rs'),
|
|
);
|
|
expect(imp).toBeDefined();
|
|
});
|
|
|
|
it('resolves process → u.save() to models.rs', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(
|
|
(c) =>
|
|
c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('models.rs'),
|
|
);
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 6. Receiver binding: &self, &mut self
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: receiver binding disambiguation', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-receiver-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod user;
|
|
mod repo;
|
|
use crate::user::User;
|
|
use crate::repo::Repo;
|
|
|
|
fn process() {
|
|
let u = User { name: String::new() };
|
|
u.save();
|
|
let r = Repo { path: String::new() };
|
|
r.save();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/user.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
`,
|
|
'src/repo.rs': `
|
|
pub struct Repo {
|
|
pub path: String,
|
|
}
|
|
|
|
impl Repo {
|
|
pub fn save(&mut self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves u.save() 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('resolves r.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).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 7. Arity filtering: no Rust overloading
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: arity filtering', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-arity-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod onearg;
|
|
mod twoarg;
|
|
|
|
fn main() {
|
|
onearg::write_audit(String::from("test"));
|
|
}
|
|
`,
|
|
'src/onearg.rs': `
|
|
pub fn write_audit(msg: String) {}
|
|
`,
|
|
'src/twoarg.rs': `
|
|
pub fn write_audit(msg: String, level: i32) {}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves main → write_audit to onearg.rs (1-arg match)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const call = calls.find(
|
|
(c) => c.target === 'write_audit' && c.targetFilePath?.includes('onearg.rs'),
|
|
);
|
|
expect(call).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 8. Struct constructor inference: let x = Foo { ... }
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: struct literal constructor inference', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-ctor-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod user;
|
|
use crate::user::User;
|
|
|
|
fn process() {
|
|
let u = User { name: String::new() };
|
|
u.save();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/user.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves u.save() to User#save via struct literal type inference', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(
|
|
(c) => c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('user.rs'),
|
|
);
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 9. Return type inference: fn get_user() -> User
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: return type inference', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-rettype-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod models;
|
|
use crate::models::User;
|
|
|
|
fn get_user() -> User {
|
|
User { name: String::new() }
|
|
}
|
|
|
|
fn process() {
|
|
let u = get_user();
|
|
u.save();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/models.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves process → u.save() via return type of get_user()', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(
|
|
(c) =>
|
|
c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('models.rs'),
|
|
);
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
|
|
it('resolves process → get_user() free call', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const getUserCall = calls.find((c) => c.target === 'get_user' && c.source === 'process');
|
|
expect(getUserCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 10. Scoped call: Type::method()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: scoped/qualified calls (Foo::new())', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-qualified-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod user;
|
|
use crate::user::User;
|
|
|
|
fn process() {
|
|
let u = User::new(String::from("test"));
|
|
u.save();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/user.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn new(name: String) -> User {
|
|
User { name }
|
|
}
|
|
|
|
pub fn save(&self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves process → User::new() free call', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const newCall = calls.find((c) => c.target === 'new' && c.source === 'process');
|
|
expect(newCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 11. Struct field declarations captured as Property
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: struct field declarations', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-fields-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
struct Point {
|
|
x: i32,
|
|
y: i32,
|
|
}
|
|
|
|
fn main() {
|
|
let p = Point { x: 1, y: 2 };
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects Point struct', () => {
|
|
expect(getNodesByLabel(result, 'Struct')).toContain('Point');
|
|
});
|
|
|
|
it('captures x and y as Property nodes', () => {
|
|
const props = getNodesByLabel(result, 'Property');
|
|
expect(props).toContain('x');
|
|
expect(props).toContain('y');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 12. Enum declarations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: enum declarations', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-enum-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
enum Color {
|
|
Red,
|
|
Green,
|
|
Blue,
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects Color enum', () => {
|
|
expect(getNodesByLabel(result, 'Enum')).toContain('Color');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 13. Multiple impl blocks for same struct
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: multiple impl blocks', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-multi-impl-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod user;
|
|
use crate::user::User;
|
|
|
|
fn process() {
|
|
let u = User { name: String::new() };
|
|
u.save();
|
|
u.display();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/user.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
|
|
impl User {
|
|
pub fn display(&self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves u.save() from first impl block', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'process');
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
|
|
it('resolves u.display() from second impl block', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const displayCall = calls.find((c) => c.target === 'display' && c.source === 'process');
|
|
expect(displayCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 14. Free function calls (non-member)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: free function calls', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-freecall-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod utils;
|
|
use crate::utils::helper;
|
|
|
|
fn main() {
|
|
helper();
|
|
}
|
|
`,
|
|
'src/utils.rs': `
|
|
pub fn helper() {
|
|
println!("helping");
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves main → helper() free call', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const helperCall = calls.find((c) => c.target === 'helper' && c.source === 'main');
|
|
expect(helperCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 15. Variable type binding via let: type annotation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: typed let binding', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-lettype-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod user;
|
|
use crate::user::User;
|
|
|
|
fn process() {
|
|
let u: User = User { name: String::new() };
|
|
u.save();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/user.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves u.save() via typed let binding', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'process');
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 16. Re-export chain: pub use re-exports
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: pub use re-exports', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-reexport-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod models;
|
|
use crate::models::User;
|
|
|
|
fn process() {
|
|
let u = User { name: String::new() };
|
|
u.save();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/models.rs': `
|
|
mod user;
|
|
pub use self::user::User;
|
|
`,
|
|
'src/models/user.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('emits IMPORTS edges through re-export chain', () => {
|
|
const imports = getRelationships(result, 'IMPORTS');
|
|
expect(imports.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 17. Local shadow: inner variable shadows outer
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: local variable shadowing', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-shadow-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
mod user;
|
|
use crate::user::User;
|
|
|
|
fn process() {
|
|
let x = 42;
|
|
let x = User { name: String::new() };
|
|
x.save();
|
|
}
|
|
|
|
fn main() {}
|
|
`,
|
|
'src/user.rs': `
|
|
pub struct User {
|
|
pub name: String,
|
|
}
|
|
|
|
impl User {
|
|
pub fn save(&self) {}
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves x.save() to User#save after shadow rebind', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'process');
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 18. Closure / nested function scope
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: closure scope isolation', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-closure-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
fn adder(x: i32) -> i32 {
|
|
let f = |y: i32| -> i32 { x + y };
|
|
f(10)
|
|
}
|
|
|
|
fn main() {
|
|
adder(5);
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects adder function', () => {
|
|
const fns = getNodesByLabel(result, 'Function');
|
|
expect(fns).toContain('adder');
|
|
});
|
|
|
|
it('resolves main → adder() call', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const call = calls.find((c) => c.target === 'adder' && c.source === 'main');
|
|
expect(call).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 19. Trait default method
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Rust scope: trait default methods', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-scope-default-method-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'src/main.rs': `
|
|
trait Greeter {
|
|
fn name(&self) -> String;
|
|
fn greet(&self) -> String {
|
|
format!("Hello, {}!", self.name())
|
|
}
|
|
}
|
|
|
|
struct User {
|
|
username: String,
|
|
}
|
|
|
|
impl Greeter for User {
|
|
fn name(&self) -> String {
|
|
self.username.clone()
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let u = User { username: String::from("alice") };
|
|
u.greet();
|
|
}
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects Greeter trait', () => {
|
|
expect(getNodesByLabel(result, 'Trait')).toContain('Greeter');
|
|
});
|
|
|
|
it('detects User struct', () => {
|
|
expect(getNodesByLabel(result, 'Struct')).toContain('User');
|
|
});
|
|
|
|
it('emits IMPLEMENTS edge from User to Greeter', () => {
|
|
const impls = getRelationships(result, 'IMPLEMENTS');
|
|
const edge = impls.find((e) => e.source === 'User' && e.target === 'Greeter');
|
|
expect(edge).toBeDefined();
|
|
});
|
|
});
|