feat(rust): Migrate Rust to scope-based resolution (RFC #909 Ring 3) (#1639)

* 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>
This commit is contained in:
Copilot 2026-05-25 13:20:08 +01:00 • committed by GitHub
parent 9f99259e5c
commit bdb3b8aaa3
19 changed files with 3265 additions and 0 deletions

View file

@ -32,6 +32,15 @@ import { rustVariableConfig } from '../variable-extractors/configs/rust.js';
import { createCallExtractor } from '../call-extractors/generic.js';
import { rustCallConfig } from '../call-extractors/configs/rust.js';
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
import {
emitRustScopeCaptures,
rustArityCompatibility,
rustBindingScopeFor,
rustImportOwningScope,
rustReceiverBinding,
interpretRustImport,
interpretRustTypeBinding,
} from './rust/index.js';
/** Rust impl_item: find the function_item child and extract its name as a Method. */
const rustExtractFunctionName = (
@ -173,4 +182,12 @@ export const rustProvider = defineLanguage({
classExtractor: createClassExtractor(rustClassConfig),
heritageExtractor: createHeritageExtractor(SupportedLanguages.Rust),
builtInNames: BUILT_INS,
// ── RFC #909 Ring 3: scope-based resolution hooks ──────────
emitScopeCaptures: emitRustScopeCaptures,
interpretImport: interpretRustImport,
interpretTypeBinding: interpretRustTypeBinding,
bindingScopeFor: rustBindingScopeFor,
importOwningScope: rustImportOwningScope,
receiverBinding: rustReceiverBinding,
arityCompatibility: rustArityCompatibility,
});

View file

@ -0,0 +1,15 @@
import type { Callsite, SymbolDefinition } from 'gitnexus-shared';
export function rustArityCompatibility(
def: SymbolDefinition,
callsite: Callsite,
): 'compatible' | 'unknown' | 'incompatible' {
const max = def.parameterCount;
const min = def.requiredParameterCount;
if (max === undefined && min === undefined) return 'unknown';
if (!Number.isFinite(callsite.arity) || callsite.arity < 0) return 'unknown';
if (min !== undefined && callsite.arity < min) return 'incompatible';
if (max !== undefined && callsite.arity > max) return 'incompatible';
return 'compatible';
}

View file

@ -0,0 +1,18 @@
let hits = 0;
let misses = 0;
export function recordRustCacheHit(): void {
hits++;
}
export function recordRustCacheMiss(): void {
misses++;
}
export function getRustCaptureCacheStats(): { readonly hits: number; readonly misses: number } {
return { hits, misses };
}
export function resetRustCaptureCacheStats(): void {
hits = 0;
misses = 0;
}

View file

@ -0,0 +1,223 @@
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import {
findNodeAtRange,
nodeToCapture,
syntheticCapture,
type SyntaxNode,
} from '../../utils/ast-helpers.js';
import { getRustParser, getRustScopeQuery } from './query.js';
import { recordRustCacheHit, recordRustCacheMiss } from './cache-stats.js';
import { splitRustUseDeclaration } from './import-decomposer.js';
import { synthesizeRustReceiverBinding } from './receiver-binding.js';
import { getTreeSitterBufferSize } from '../../constants.js';
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
export function emitRustScopeCaptures(
sourceText: string,
_filePath: string,
cachedTree?: unknown,
): readonly CaptureMatch[] {
let tree = cachedTree as ReturnType<ReturnType<typeof getRustParser>['parse']> | undefined;
if (tree === undefined) {
tree = parseSourceSafe(getRustParser(), sourceText, undefined, {
bufferSize: getTreeSitterBufferSize(sourceText),
});
recordRustCacheMiss();
} else {
recordRustCacheHit();
}
const rawMatches = getRustScopeQuery().matches(tree.rootNode);
const out: CaptureMatch[] = [];
for (const m of rawMatches) {
const grouped: Record<string, Capture> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
if (tag.startsWith('@_')) continue;
grouped[tag] = nodeToCapture(tag, c.node);
}
if (Object.keys(grouped).length === 0) continue;
// Decompose use declarations into individual import captures
if (grouped['@import.statement'] !== undefined) {
const anchor = grouped['@import.statement']!;
const useNode = findNodeAtRange(tree.rootNode, anchor.range, 'use_declaration');
if (useNode !== null) {
out.push(...splitRustUseDeclaration(useNode));
continue;
}
}
// Synthesize self receiver bindings for methods inside impl blocks
let cachedImplLookup: { fnNode: SyntaxNode; implNode: SyntaxNode | null } | undefined;
if (grouped['@scope.function'] !== undefined) {
const scopeCap = grouped['@scope.function']!;
const fnNode = findNodeAtRange(tree.rootNode, scopeCap.range, 'function_item');
if (fnNode !== null) {
const implNode = findEnclosingImpl(fnNode);
cachedImplLookup = { fnNode, implNode };
const receiver = synthesizeRustReceiverBinding(fnNode, implNode);
if (receiver !== null) out.push(receiver);
}
}
// Attach declaration arity for functions/methods
const declAnchor = grouped['@declaration.function'];
if (declAnchor !== undefined) {
const fnNode = findNodeAtRange(tree.rootNode, declAnchor.range, 'function_item');
if (fnNode !== null) {
const implNode =
cachedImplLookup?.fnNode === fnNode
? cachedImplLookup.implNode
: findEnclosingImpl(fnNode);
const traitNode = implNode === null ? findEnclosingTrait(fnNode) : null;
// Reclassify as method if inside an impl block or trait definition
if (implNode !== null || traitNode !== null) {
const nameCap = grouped['@declaration.name'];
delete (grouped as Record<string, Capture | undefined>)['@declaration.function'];
grouped['@declaration.method'] = syntheticCapture(
'@declaration.method',
fnNode,
fnNode.text,
);
if (nameCap !== undefined) {
grouped['@declaration.name'] = nameCap;
}
}
const arity = computeRustDeclarationArity(fnNode);
if (arity.parameterCount !== undefined) {
grouped['@declaration.parameter-count'] = syntheticCapture(
'@declaration.parameter-count',
fnNode,
String(arity.parameterCount),
);
}
if (arity.requiredParameterCount !== undefined) {
grouped['@declaration.required-parameter-count'] = syntheticCapture(
'@declaration.required-parameter-count',
fnNode,
String(arity.requiredParameterCount),
);
}
}
}
// Hoist return-type bindings from impl block functions to module level.
// The auto-hoist in the scope-extractor places a type binding whose
// anchor matches its innermost scope on the parent scope. By using the
// impl_item node as the anchor (which matches the impl's Class scope),
// the binding lands on the Module scope — making it visible to the
// compound receiver's hoistTypeBindingsToModule walk.
if (
grouped['@type-binding.return'] !== undefined &&
grouped['@type-binding.name'] !== undefined
) {
const tbReturnAnchor = grouped['@type-binding.return']!;
const fnNode = findNodeAtRange(tree.rootNode, tbReturnAnchor.range, 'function_item');
if (fnNode !== null) {
const implNode = findEnclosingImpl(fnNode);
if (implNode !== null) {
out.push({
'@type-binding.name': syntheticCapture(
'@type-binding.name',
implNode,
grouped['@type-binding.name']!.text,
),
'@type-binding.type': syntheticCapture(
'@type-binding.type',
implNode,
grouped['@type-binding.type']!.text,
),
'@type-binding.return': syntheticCapture(
'@type-binding.return',
implNode,
tbReturnAnchor.text,
),
});
}
}
}
// Attach call arity for call expressions
const callAnchor =
grouped['@reference.call.free'] ??
grouped['@reference.call.member'] ??
grouped['@reference.call.constructor'];
if (callAnchor !== undefined) {
const callNode =
findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
findNodeAtRange(tree.rootNode, callAnchor.range, 'struct_expression');
if (callNode !== null) {
const arity = computeRustCallArity(callNode);
grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(arity));
}
}
out.push(grouped);
}
return out;
}
function findEnclosingImpl(node: SyntaxNode): SyntaxNode | null {
let current: SyntaxNode | null = node.parent;
while (current !== null) {
if (current.type === 'impl_item') return current;
if (current.type === 'source_file' || current.type === 'mod_item') return null;
current = current.parent;
}
return null;
}
function findEnclosingTrait(node: SyntaxNode): SyntaxNode | null {
let current: SyntaxNode | null = node.parent;
while (current !== null) {
if (current.type === 'trait_item') return current;
if (current.type === 'source_file' || current.type === 'mod_item') return null;
current = current.parent;
}
return null;
}
function computeRustDeclarationArity(fnNode: SyntaxNode): {
parameterCount?: number;
requiredParameterCount?: number;
} {
const params = fnNode.childForFieldName('parameters');
if (params === null) return {};
let count = 0;
for (let i = 0; i < params.namedChildCount; i++) {
const child = params.namedChild(i);
if (child === null) continue;
if (child.type === 'self_parameter') continue;
if (child.type === 'parameter') count++;
}
// Rust has no default parameters or overloading
return { parameterCount: count, requiredParameterCount: count };
}
function computeRustCallArity(callNode: SyntaxNode): number {
if (callNode.type === 'struct_expression') {
const body = callNode.childForFieldName('body');
if (body === null) return 0;
let count = 0;
for (let i = 0; i < body.namedChildCount; i++) {
const t = body.namedChild(i)?.type;
if (t === 'field_initializer' || t === 'shorthand_field_initializer') count++;
}
return count;
}
const args = callNode.childForFieldName('arguments');
if (args === null) return 0;
let count = 0;
for (let i = 0; i < args.namedChildCount; i++) {
const child = args.namedChild(i);
if (child !== null) count++;
}
return count;
}

View file

@ -0,0 +1,207 @@
import type { CaptureMatch } from 'gitnexus-shared';
import { syntheticCapture } from '../../utils/ast-helpers.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Decompose a Rust `use_declaration` into individual import captures.
* Handles simple paths, grouped imports ({A, B}), wildcards (*),
* renames (as), and `pub use` re-exports.
*/
export function splitRustUseDeclaration(node: SyntaxNode): CaptureMatch[] {
if (node.type !== 'use_declaration') return [];
const isReexport = hasVisibilityModifier(node);
const argument = getUseArgument(node);
if (argument === null) return [];
return decomposeUseArgument(argument, '', isReexport, node);
}
function hasVisibilityModifier(node: SyntaxNode): boolean {
for (let i = 0; i < node.childCount; i++) {
if (node.child(i)?.type === 'visibility_modifier') return true;
}
return false;
}
function getUseArgument(node: SyntaxNode): SyntaxNode | null {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child === null) continue;
if (
child.type === 'scoped_identifier' ||
child.type === 'scoped_use_list' ||
child.type === 'use_wildcard' ||
child.type === 'use_as_clause' ||
child.type === 'identifier' ||
child.type === 'use_list'
) {
return child;
}
}
return null;
}
function decomposeUseArgument(
node: SyntaxNode,
prefixPath: string,
isReexport: boolean,
anchor: SyntaxNode,
): CaptureMatch[] {
switch (node.type) {
case 'scoped_identifier': {
const path = buildScopedPath(node);
const segments = path.split('::');
const name = segments[segments.length - 1];
return [
makeImportCapture(
anchor,
isReexport ? 'reexport' : 'named',
joinPaths(prefixPath, path),
name,
undefined,
),
];
}
case 'scoped_use_list': {
const pathNode = node.childForFieldName('path');
const listNode = node.childForFieldName('list');
const pathStr = pathNode ? buildNodePath(pathNode) : '';
const fullPrefix = joinPaths(prefixPath, pathStr);
if (listNode === null) return [];
return decomposeUseList(listNode, fullPrefix, isReexport, anchor);
}
case 'use_list': {
return decomposeUseList(node, prefixPath, isReexport, anchor);
}
case 'use_wildcard': {
const wcPath = buildWildcardPath(node);
return [makeImportCapture(anchor, 'wildcard', joinPaths(prefixPath, wcPath), '*', undefined)];
}
case 'use_as_clause': {
const pathChild = node.childForFieldName('path');
const aliasChild = node.childForFieldName('alias');
if (pathChild === null || aliasChild === null) return [];
const originalName =
pathChild.type === 'scoped_identifier' ? buildScopedPath(pathChild) : pathChild.text;
const aliasName = aliasChild.text;
const segments = originalName.split('::');
const importedName = segments[segments.length - 1];
return [
makeImportCapture(
anchor,
isReexport ? 'reexport' : 'named',
joinPaths(prefixPath, originalName),
importedName,
aliasName,
),
];
}
case 'identifier': {
return [
makeImportCapture(
anchor,
isReexport ? 'reexport' : 'named',
joinPaths(prefixPath, node.text),
node.text,
undefined,
),
];
}
default:
return [];
}
}
function decomposeUseList(
listNode: SyntaxNode,
prefix: string,
isReexport: boolean,
anchor: SyntaxNode,
): CaptureMatch[] {
const out: CaptureMatch[] = [];
for (let i = 0; i < listNode.namedChildCount; i++) {
const child = listNode.namedChild(i);
if (child === null) continue;
if (child.type === 'self') {
// `use crate::models::{self}` — imports the module itself
const segments = prefix.split('::').filter(Boolean);
const name = segments[segments.length - 1] ?? 'self';
out.push(makeImportCapture(anchor, 'namespace', prefix, name, undefined));
} else {
out.push(...decomposeUseArgument(child, prefix, isReexport, anchor));
}
}
return out;
}
function buildScopedPath(node: SyntaxNode): string {
if (node.type === 'scoped_identifier') {
const parts: string[] = [];
collectScopedParts(node, parts);
return parts.join('::');
}
return node.text;
}
function collectScopedParts(node: SyntaxNode, parts: string[]): void {
if (node.type === 'scoped_identifier') {
const pathNode = node.childForFieldName('path');
const nameNode = node.childForFieldName('name');
if (pathNode) collectScopedParts(pathNode, parts);
if (nameNode) parts.push(nameNode.text);
} else {
parts.push(node.text);
}
}
function buildNodePath(node: SyntaxNode): string {
if (node.type === 'scoped_identifier') {
return buildScopedPath(node);
}
return node.text;
}
function buildWildcardPath(node: SyntaxNode): string {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child === null) continue;
if (child.type === 'scoped_identifier') return buildScopedPath(child);
if (child.type === 'identifier') return child.text;
}
return '';
}
function joinPaths(prefix: string, suffix: string): string {
if (!prefix) return suffix;
if (!suffix) return prefix;
return `${prefix}::${suffix}`;
}
function makeImportCapture(
anchor: SyntaxNode,
kind: string,
source: string,
name: string,
alias: string | undefined,
): CaptureMatch {
return {
'@import.statement': syntheticCapture('@import.statement', anchor, anchor.text),
'@import.kind': syntheticCapture('@import.kind', anchor, kind),
'@import.source': syntheticCapture('@import.source', anchor, source),
'@import.name': syntheticCapture('@import.name', anchor, alias ?? name),
...(alias !== undefined
? {
'@import.alias': syntheticCapture('@import.alias', anchor, alias),
'@import.original-name': syntheticCapture('@import.original-name', anchor, name),
}
: {}),
};
}

View file

@ -0,0 +1,129 @@
/**
* Resolve a Rust `use` import path to a repo-relative file path.
*
* Rust module resolution rules:
* - `crate::foo::bar` → `src/foo/bar.rs` or `src/foo/bar/mod.rs`
* - `super::foo` → parent directory's `foo.rs` or `foo/mod.rs`
* - `self::foo` → same directory's `foo.rs` or `foo/mod.rs`
* - External crate imports (no `crate::`/`super::`/`self::`) → null
*/
export function resolveRustImportTarget(
targetRaw: string,
fromFile: string,
allFilePaths: ReadonlySet<string>,
_resolutionConfig?: unknown,
): string | readonly string[] | null {
if (!targetRaw) return null;
const segments = targetRaw.split('::').filter(Boolean);
if (segments.length === 0) return null;
const fromNormalized = fromFile.replace(/\\/g, '/');
const fromDir = fromNormalized.includes('/')
? fromNormalized.slice(0, fromNormalized.lastIndexOf('/'))
: '';
if (segments[0] === 'crate') {
const cratePath = segments.slice(1);
return resolveModulePath(cratePath, findSrcRoot(fromNormalized), allFilePaths);
}
if (segments[0] === 'super') {
const parentDir = fromDir.includes('/') ? fromDir.slice(0, fromDir.lastIndexOf('/')) : '';
const restPath = segments.slice(1);
return resolveModulePath(restPath, parentDir, allFilePaths);
}
if (segments[0] === 'self') {
const restPath = segments.slice(1);
return resolveModulePath(restPath, fromDir, allFilePaths);
}
// External crate — try workspace-level resolution
const workspaceResult = resolveWorkspaceCrate(segments, allFilePaths);
if (workspaceResult !== null) return workspaceResult;
// Fallback: treat as implicit crate-relative (Rust 2015 edition or
// when the first segment matches a sibling module name).
return resolveModulePath(segments, findSrcRoot(fromNormalized), allFilePaths);
}
function findSrcRoot(filePath: string): string {
const normalized = filePath.replace(/\\/g, '/');
const srcIdx = normalized.lastIndexOf('/src/');
if (srcIdx !== -1) return normalized.slice(0, srcIdx + 4); // includes trailing /src
if (normalized.startsWith('src/')) return 'src';
return '';
}
function resolveModulePath(
pathSegments: string[],
baseDir: string,
allFilePaths: ReadonlySet<string>,
): string | readonly string[] | null {
if (pathSegments.length === 0) {
const modPath = baseDir ? `${baseDir}/mod.rs` : 'mod.rs';
if (allFilePaths.has(modPath)) return modPath;
return null;
}
const modulePath = pathSegments.join('/');
// Try direct file
const directFile = baseDir ? `${baseDir}/${modulePath}.rs` : `${modulePath}.rs`;
if (allFilePaths.has(directFile)) return directFile;
// Try mod.rs inside directory
const modFile = baseDir ? `${baseDir}/${modulePath}/mod.rs` : `${modulePath}/mod.rs`;
if (allFilePaths.has(modFile)) return modFile;
// Try partial path resolution: for `use crate::models::User` where
// User is a type inside models.rs, resolve to `src/models.rs`
if (pathSegments.length >= 2) {
const parentPath = pathSegments.slice(0, -1).join('/');
const parentFile = baseDir ? `${baseDir}/${parentPath}.rs` : `${parentPath}.rs`;
if (allFilePaths.has(parentFile)) return parentFile;
const parentModFile = baseDir ? `${baseDir}/${parentPath}/mod.rs` : `${parentPath}/mod.rs`;
if (allFilePaths.has(parentModFile)) return parentModFile;
}
// Fallback: try increasingly shorter path prefixes
for (let i = pathSegments.length - 2; i >= 1; i--) {
const prefix = pathSegments.slice(0, i).join('/');
const prefixFile = baseDir ? `${baseDir}/${prefix}.rs` : `${prefix}.rs`;
if (allFilePaths.has(prefixFile)) return prefixFile;
const prefixModFile = baseDir ? `${baseDir}/${prefix}/mod.rs` : `${prefix}/mod.rs`;
if (allFilePaths.has(prefixModFile)) return prefixModFile;
}
return null;
}
function resolveWorkspaceCrate(
segments: string[],
allFilePaths: ReadonlySet<string>,
): string | null {
const crateName = segments[0];
const restSegments = segments.slice(1);
const candidates = [
restSegments.length > 0
? `${crateName}/src/${restSegments.join('/')}.rs`
: `${crateName}/src/lib.rs`,
restSegments.length > 0
? `${crateName}/src/${restSegments.join('/')}/mod.rs`
: `${crateName}/src/lib.rs`,
];
for (const candidate of candidates) {
if (allFilePaths.has(candidate)) return candidate;
}
return null;
}
export interface RustResolveContext {
readonly fromFile: string;
readonly allFilePaths: ReadonlySet<string>;
}

View file

@ -0,0 +1,16 @@
/**
* Rust scope-resolution hooks (RFC #909 Ring 3).
*/
export { emitRustScopeCaptures } from './captures.js';
export { getRustCaptureCacheStats, resetRustCaptureCacheStats } from './cache-stats.js';
export {
interpretRustImport,
interpretRustTypeBinding,
normalizeRustTypeName,
} from './interpret.js';
export { splitRustUseDeclaration } from './import-decomposer.js';
export { synthesizeRustReceiverBinding } from './receiver-binding.js';
export { rustArityCompatibility } from './arity.js';
export { rustMergeBindings } from './merge-bindings.js';
export { rustBindingScopeFor, rustImportOwningScope, rustReceiverBinding } from './simple-hooks.js';
export { resolveRustImportTarget, type RustResolveContext } from './import-target.js';

View file

@ -0,0 +1,166 @@
import type { CaptureMatch, ParsedImport, ParsedTypeBinding, TypeRef } from 'gitnexus-shared';
const REF_PREFIX_RE = /^&\s*(mut\s+)?/;
const PTR_PREFIX_RE = /^\*\s*(const|mut)?\s*/;
const ENUM_VARIANT_NAMES = new Set(['Some', 'None', 'Ok', 'Err']);
// ─── interpretImport ──────────────────────────────────────────────────────
export function interpretRustImport(captures: CaptureMatch): ParsedImport | null {
const kind = captures['@import.kind']?.text;
const source = captures['@import.source']?.text;
const name = captures['@import.name']?.text;
const alias = captures['@import.alias']?.text;
if (kind === undefined || source === undefined) return null;
if (kind === 'wildcard') return { kind: 'wildcard', targetRaw: source };
if (kind === 'namespace') {
if (name === undefined) return null;
return { kind: 'namespace', localName: name, importedName: name, targetRaw: source };
}
if (kind === 'reexport') {
if (name === undefined) return null;
const originalName = captures['@import.original-name']?.text;
return {
kind: 'reexport',
localName: alias ?? name,
importedName: originalName ?? name,
targetRaw: source,
};
}
// kind === 'named'
if (name === undefined) return null;
const originalName = captures['@import.original-name']?.text;
return {
kind: 'named',
localName: alias ?? name,
importedName: originalName ?? name,
targetRaw: source,
};
}
// ─── interpretTypeBinding ─────────────────────────────────────────────────
export function interpretRustTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null {
const name = captures['@type-binding.name']?.text;
const type = captures['@type-binding.type']?.text;
if (name === undefined || type === undefined) return null;
let source: TypeRef['source'] = 'annotation';
let normalizedType: string;
if (captures['@type-binding.self'] !== undefined) {
source = 'self';
normalizedType = normalizeRustTypeName(type);
} else if (captures['@type-binding.constructor'] !== undefined) {
source = 'constructor-inferred';
normalizedType = normalizeRustTypeName(type);
} else if (captures['@type-binding.call-return'] !== undefined) {
if (ENUM_VARIANT_NAMES.has(type)) return null;
source = 'constructor-inferred';
normalizedType = normalizeRustCallReturnType(type);
} else if (captures['@type-binding.return'] !== undefined) {
source = 'return-annotation';
normalizedType = normalizeRustReturnType(type);
} else if (captures['@type-binding.assignment'] !== undefined) {
source = 'assignment-inferred';
normalizedType = normalizeRustTypeName(type);
} else if (captures['@type-binding.alias'] !== undefined) {
source = 'assignment-inferred';
normalizedType = normalizeRustTypeName(type);
} else if (captures['@type-binding.parameter'] !== undefined) {
source = 'parameter-annotation';
normalizedType = normalizeRustTypeName(type);
} else {
normalizedType = normalizeRustTypeName(type);
}
return { boundName: name, rawTypeName: normalizedType, source };
}
export function normalizeRustTypeName(text: string): string {
let t = text.trim();
// Strip reference prefixes (&, &mut, *const, *mut)
while (t.startsWith('&')) t = t.replace(REF_PREFIX_RE, '');
while (t.startsWith('*')) t = t.replace(PTR_PREFIX_RE, '');
// Unwrap common smart-pointer/container wrappers to their inner type
const wrappers = ['Box', 'Option', 'Arc', 'Rc', 'Mutex', 'RwLock', 'RefCell', 'Cell'];
for (const w of wrappers) {
if (t.startsWith(`${w}<`)) {
const inner = extractFirstGenericArg(t);
if (inner !== null) {
t = inner;
break;
}
}
}
if (t.startsWith('Vec<')) {
const inner = extractFirstGenericArg(t);
if (inner !== null) t = inner;
}
const bracket = t.indexOf('<');
if (bracket !== -1) t = t.slice(0, bracket);
// Take last segment of qualified paths (crate::foo::Bar → Bar)
const lastColon = t.lastIndexOf('::');
if (lastColon !== -1) t = t.slice(lastColon + 2);
return t.trim();
}
function extractFirstGenericArg(text: string): string | null {
const open = text.indexOf('<');
if (open === -1) return null;
let depth = 0;
for (let i = open; i < text.length; i++) {
if (text[i] === '<') depth++;
else if (text[i] === '>') {
depth--;
if (depth === 0) {
const inner = text.slice(open + 1, i).trim();
const comma = findTopLevelComma(inner);
return comma === -1 ? inner : inner.slice(0, comma).trim();
}
}
}
return null;
}
function findTopLevelComma(text: string): number {
let depth = 0;
for (let i = 0; i < text.length; i++) {
if (text[i] === '<') depth++;
else if (text[i] === '>') depth--;
else if (text[i] === ',' && depth === 0) return i;
}
return -1;
}
function normalizeRustCallReturnType(text: string): string {
let t = text.trim();
// For scoped calls like `Foo::new()`, extract the type part before `::`
const scopeIdx = t.indexOf('::');
if (scopeIdx !== -1) {
t = t.slice(0, scopeIdx);
}
return normalizeRustTypeName(t);
}
function normalizeRustReturnType(text: string): string {
let t = text.trim();
while (t.startsWith('&')) t = t.replace(REF_PREFIX_RE, '');
// Unwrap Result<T, E>, Option<T> for return types
const wrappers = ['Result', 'Option'];
for (const w of wrappers) {
if (t.startsWith(`${w}<`)) {
const inner = extractFirstGenericArg(t);
if (inner !== null) {
t = inner;
break;
}
}
}
const bracket = t.indexOf('<');
if (bracket !== -1) t = t.slice(0, bracket);
const lastColon = t.lastIndexOf('::');
if (lastColon !== -1) t = t.slice(lastColon + 2);
return t.trim();
}

View file

@ -0,0 +1,27 @@
import type { BindingRef } from 'gitnexus-shared';
const TIER: Record<BindingRef['origin'], number> = {
local: 0,
namespace: 1,
import: 2,
reexport: 3,
wildcard: 4,
};
export function rustMergeBindings(
existing: readonly BindingRef[],
incoming: readonly BindingRef[],
_scopeId: string,
): BindingRef[] {
const seen = new Set<string>();
return [...existing, ...incoming]
.sort(
(a, b) =>
(TIER[a.origin] ?? 99) - (TIER[b.origin] ?? 99) || a.def.nodeId.localeCompare(b.def.nodeId),
)
.filter((binding) => {
if (seen.has(binding.def.nodeId)) return false;
seen.add(binding.def.nodeId);
return true;
});
}

View file

@ -0,0 +1,68 @@
import type { ParsedFile } from 'gitnexus-shared';
import { isClassLike, populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
/**
* Populate `ownerId` on Rust method defs.
*
* Rust methods are declared inside `impl TypeName { ... }` blocks, not
* directly inside struct bodies. The tree-sitter query creates Class scopes
* for impl blocks, and the generic `populateClassOwnedMembers` handles methods
* that are structurally nested inside those Class scopes. But we also need to
* bridge the impl block's methods to the actual struct def, since the impl
* block is semantically "owned by" the struct.
*
* Strategy:
* 1. Run the generic `populateClassOwnedMembers` (handles property fields in
* structs and methods in impl blocks).
* 2. For each method in an impl block's Class scope whose ownerId points to
* the impl block, re-point ownerId to the struct def (if found in the
* same module).
*/
export function populateRustOwners(parsed: ParsedFile): void {
populateClassOwnedMembers(parsed);
populateRustImplOwners(parsed);
}
function populateRustImplOwners(parsed: ParsedFile): void {
// Build a map of struct name → def nodeId from all scopes.
const structByName = new Map<string, string>();
for (const scope of parsed.scopes) {
for (const def of scope.ownedDefs) {
if (isClassLike(def.type) && def.qualifiedName) {
structByName.set(def.qualifiedName, def.nodeId);
}
}
}
if (structByName.size === 0) return;
const structBySuffix = new Map<string, string>();
for (const [qname, nodeId] of structByName) {
const dot = qname.lastIndexOf('.');
const suffix = dot !== -1 ? qname.slice(dot + 1) : qname;
structBySuffix.set(suffix, nodeId);
}
for (const scope of parsed.scopes) {
if (scope.kind !== 'Function') continue;
const methodDefs = scope.ownedDefs.filter(
(d) => d.type === 'Method' && d.ownerId === undefined,
);
if (methodDefs.length === 0) continue;
let receiverType: string | undefined;
for (const [, tb] of scope.typeBindings) {
if (tb.source === 'self') {
receiverType = tb.rawName;
break;
}
}
if (receiverType === undefined) continue;
const ownerId = structByName.get(receiverType) ?? structBySuffix.get(receiverType);
if (ownerId !== undefined) {
for (const def of methodDefs) {
(def as { ownerId?: string }).ownerId = ownerId;
}
}
}
}

View file

@ -0,0 +1,149 @@
import Parser from 'tree-sitter';
import Rust from 'tree-sitter-rust';
const RUST_SCOPE_QUERY = `
;; Scopes
(source_file) @scope.module
(struct_item) @scope.class
(trait_item) @scope.class
(impl_item) @scope.class
(enum_item) @scope.class
(function_item) @scope.function
(closure_expression) @scope.function
(block) @scope.block
(if_expression) @scope.block
(match_expression) @scope.block
(for_expression) @scope.block
(while_expression) @scope.block
(loop_expression) @scope.block
(mod_item) @scope.namespace
;; Declarations — struct
(struct_item
name: (type_identifier) @declaration.name) @declaration.struct
;; Declarations — trait
(trait_item
name: (type_identifier) @declaration.name) @declaration.trait
;; Declarations — enum
(enum_item
name: (type_identifier) @declaration.name) @declaration.enum
;; Declarations — function (top-level or inside mod)
(function_item
name: (identifier) @declaration.name) @declaration.function
;; Declarations — struct fields
(field_declaration
name: (field_identifier) @declaration.name
type: (_) @declaration.field-type) @declaration.field
;; Declarations — variables (let bindings)
(let_declaration
pattern: (identifier) @declaration.name) @declaration.variable
;; Declarations — const
(const_item
name: (identifier) @declaration.name) @declaration.const
;; Declarations — static
(static_item
name: (identifier) @declaration.name) @declaration.const
;; Imports
(use_declaration) @import.statement
;; Type bindings — parameter annotations
(parameter
pattern: (identifier) @type-binding.name
type: (_) @type-binding.type) @type-binding.parameter
;; Type bindings — let with type annotation
(let_declaration
pattern: (identifier) @type-binding.name
type: (_) @type-binding.type) @type-binding.assignment
;; Type bindings — struct literal constructor inference
(let_declaration
pattern: (identifier) @type-binding.name
value: (struct_expression
name: (_) @type-binding.type)) @type-binding.constructor
;; Type bindings — call-return inference (let x = Foo::new())
(let_declaration
pattern: (identifier) @type-binding.name
value: (call_expression
function: (_) @type-binding.type)) @type-binding.call-return
;; Type bindings — call-return inference through .await (let x = foo().await)
(let_declaration
pattern: (identifier) @type-binding.name
value: (await_expression
(call_expression
function: (_) @type-binding.type))) @type-binding.call-return
;; Type bindings — variable alias (let x = y)
(let_declaration
pattern: (identifier) @type-binding.name
value: (identifier) @type-binding.type) @type-binding.alias
;; Type bindings — return type annotation
(function_item
name: (identifier) @type-binding.name
return_type: (_) @type-binding.type) @type-binding.return
;; References — free calls
(call_expression
function: (identifier) @reference.name) @reference.call.free
;; References — member calls (obj.method())
(call_expression
function: (field_expression
value: (_) @reference.receiver
field: (field_identifier) @reference.name)) @reference.call.member
;; References — scoped calls (Foo::bar())
(call_expression
function: (scoped_identifier
name: (identifier) @reference.name)) @reference.call.free
;; References — constructor calls (struct literal)
(struct_expression
name: (_) @reference.name) @reference.call.constructor
;; References — field reads
(field_expression
value: (_) @reference.receiver
field: (field_identifier) @reference.name) @reference.read
;; References — field writes (assignment)
(assignment_expression
left: (field_expression
value: (_) @reference.receiver
field: (field_identifier) @reference.name)) @reference.write
;; References — field writes (compound assignment: +=, -=, etc.)
(compound_assignment_expr
left: (field_expression
value: (_) @reference.receiver
field: (field_identifier) @reference.name)) @reference.write
`;
let _parser: Parser | null = null;
let _query: Parser.Query | null = null;
export function getRustParser(): Parser {
if (_parser === null) {
_parser = new Parser();
_parser.setLanguage(Rust as Parameters<Parser['setLanguage']>[0]);
}
return _parser;
}
export function getRustScopeQuery(): Parser.Query {
if (_query === null) {
_query = new Parser.Query(Rust as Parameters<Parser['setLanguage']>[0], RUST_SCOPE_QUERY);
}
return _query;
}

View file

@ -0,0 +1,708 @@
import type { ParsedFile, Scope, ScopeId, TypeRef } from 'gitnexus-shared';
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
import { getRustParser } from './query.js';
import { getTreeSitterBufferSize } from '../../constants.js';
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Populate type bindings for patterns and iterators that the tree-sitter
* query can't handle (they need runtime type lookup, not just syntax).
*
* Covers: for-loop element types, if-let/while-let pattern bindings,
* match arm patterns, and struct destructuring.
*
* Runs in Phase 2 (after propagateImportedReturnTypes) so all cross-file
* type bindings are available for lookup.
*/
export function populateRustRangeBindings(
parsedFiles: readonly ParsedFile[],
_indexes: ScopeResolutionIndexes,
ctx: {
readonly fileContents: ReadonlyMap<string, string>;
readonly treeCache?: { get(filePath: string): unknown };
},
): void {
const parser = getRustParser();
const allReturnTypes = new Map<string, string>();
const allFieldTypes = new Map<string, Map<string, string>>();
for (const parsed of parsedFiles) {
const sourceText = ctx.fileContents.get(parsed.filePath);
if (sourceText === undefined) continue;
const cachedTree = ctx.treeCache?.get(parsed.filePath);
const tree =
(cachedTree as ReturnType<typeof parser.parse> | undefined) ??
parseSourceSafe(parser, sourceText, undefined, {
bufferSize: getTreeSitterBufferSize(sourceText),
});
for (const fn of tree.rootNode.descendantsOfType('function_item')) {
const nameNode = fn.childForFieldName('name');
const retType = fn.childForFieldName('return_type');
if (nameNode !== null && retType !== null) {
const name = nameNode.text;
if (allReturnTypes.has(name)) {
allReturnTypes.delete(name);
} else {
allReturnTypes.set(name, retType.text);
}
}
}
for (const structNode of tree.rootNode.descendantsOfType('struct_item')) {
const nameNode = structNode.childForFieldName('name');
const body = structNode.childForFieldName('body');
if (nameNode === null || body === null) continue;
const fields = new Map<string, string>();
for (const field of body.descendantsOfType('field_declaration')) {
const fieldName = field.childForFieldName('name');
const fieldType = field.childForFieldName('type');
if (fieldName !== null && fieldType !== null) {
fields.set(fieldName.text, normalizeFieldType(fieldType.text));
}
}
if (fields.size > 0) {
const name = nameNode.text;
if (allFieldTypes.has(name)) {
allFieldTypes.delete(name);
} else {
allFieldTypes.set(name, fields);
}
}
}
}
for (const parsed of parsedFiles) {
const sourceText = ctx.fileContents.get(parsed.filePath);
if (sourceText === undefined) continue;
const cachedTree = ctx.treeCache?.get(parsed.filePath);
const tree =
(cachedTree as ReturnType<typeof parser.parse> | undefined) ??
parseSourceSafe(parser, sourceText, undefined, {
bufferSize: getTreeSitterBufferSize(sourceText),
});
const scopeMap = new Map(parsed.scopes.map((s) => [s.id, s]));
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
if (moduleScope === undefined) continue;
processFieldTypeBindings(tree.rootNode, parsed, scopeMap);
processIdentityMethodBindings(parsed);
processForLoops(tree.rootNode, parsed, scopeMap, moduleScope, allReturnTypes);
processPatternBindings(tree.rootNode, parsed, scopeMap, moduleScope);
processStructDestructuring(tree.rootNode, parsed, scopeMap, moduleScope, allFieldTypes);
processPendingAssignments(
tree.rootNode,
parsed,
parsedFiles,
scopeMap,
moduleScope,
allReturnTypes,
);
}
}
function processFieldTypeBindings(
root: SyntaxNode,
parsed: ParsedFile,
scopeMap: ReadonlyMap<string, Scope>,
): void {
for (const structNode of root.descendantsOfType('struct_item')) {
const nameNode = structNode.childForFieldName('name');
if (nameNode === null) continue;
const structScope = findScopeForNode(structNode, parsed, scopeMap);
if (structScope === null) continue;
const body = structNode.childForFieldName('body');
if (body === null) continue;
for (const field of body.descendantsOfType('field_declaration')) {
const fieldName = field.childForFieldName('name');
const fieldType = field.childForFieldName('type');
if (fieldName === null || fieldType === null) continue;
const normalizedType = normalizeFieldType(fieldType.text);
injectTypeBinding(structScope, fieldName.text, normalizedType);
}
}
}
function findScopeForNode(
node: SyntaxNode,
parsed: ParsedFile,
scopeMap: ReadonlyMap<string, Scope>,
): Scope | null {
for (const scope of parsed.scopes) {
if (
scope.kind === 'Class' &&
scope.range.startLine === node.startPosition.row + 1 &&
scope.range.startCol === node.startPosition.column
) {
return scope;
}
}
return null;
}
function normalizeFieldType(text: string): string {
let t = text.trim();
if (t.startsWith('&')) t = t.replace(/^&\s*(mut\s+)?/, '');
const bracket = t.indexOf('<');
if (bracket !== -1) t = t.slice(0, bracket);
const lastColon = t.lastIndexOf('::');
if (lastColon !== -1) t = t.slice(lastColon + 2);
return t.trim();
}
function processForLoops(
root: SyntaxNode,
parsed: ParsedFile,
scopeMap: ReadonlyMap<string, Scope>,
moduleScope: Scope,
allReturnTypes: ReadonlyMap<string, string>,
): void {
for (const forNode of root.descendantsOfType('for_expression')) {
const patternNode = forNode.childForFieldName('pattern');
const valueNode = forNode.childForFieldName('value');
if (patternNode === null || valueNode === null) continue;
const varName = extractVarName(patternNode);
if (varName === null) continue;
const elementType = resolveIterableElementType(
valueNode,
parsed,
scopeMap,
moduleScope,
allReturnTypes,
);
if (elementType === null) continue;
const targetScope = findEnclosingFunctionScope(forNode, scopeMap) ?? moduleScope;
injectTypeBinding(targetScope, varName, elementType);
}
}
function processPatternBindings(
root: SyntaxNode,
parsed: ParsedFile,
scopeMap: ReadonlyMap<string, Scope>,
moduleScope: Scope,
): void {
for (const nodeType of ['let_condition', 'match_arm'] as const) {
for (const node of root.descendantsOfType(nodeType)) {
const patternNode =
nodeType === 'let_condition'
? node.childForFieldName('pattern')
: (node.childForFieldName('pattern')?.firstNamedChild ?? null);
if (patternNode === null) continue;
if (patternNode.type === 'captured_pattern') {
processCapturedPattern(patternNode, node, parsed, scopeMap, moduleScope);
continue;
}
if (patternNode.type === 'tuple_struct_pattern') {
processTupleStructPattern(patternNode, node, parsed, scopeMap, moduleScope);
}
}
}
}
function processCapturedPattern(
patternNode: SyntaxNode,
contextNode: SyntaxNode,
_parsed: ParsedFile,
scopeMap: ReadonlyMap<string, Scope>,
moduleScope: Scope,
): void {
const varNode = patternNode.namedChildren.find((c) => c.type === 'identifier');
const structPatternNode = patternNode.namedChildren.find((c) => c.type === 'struct_pattern');
if (varNode === undefined || structPatternNode === undefined) return;
const typeName = structPatternNode.childForFieldName('type')?.text;
if (typeName === undefined) return;
const targetScope = findEnclosingFunctionScope(contextNode, scopeMap) ?? moduleScope;
injectTypeBinding(targetScope, varNode.text, typeName);
}
function processTupleStructPattern(
patternNode: SyntaxNode,
contextNode: SyntaxNode,
parsed: ParsedFile,
scopeMap: ReadonlyMap<string, Scope>,
moduleScope: Scope,
): void {
const wrapperNode = patternNode.childForFieldName('type');
if (wrapperNode === null) return;
const wrapper = wrapperNode.text;
const wrapperIdx = patternNode.namedChildren.indexOf(wrapperNode);
const innerIdent = patternNode.namedChildren.find(
(c, i) => i > wrapperIdx && c.type === 'identifier',
);
if (innerIdent === null || innerIdent === undefined) return;
const varName = innerIdent.text;
let sourceVarNode: SyntaxNode | null = null;
if (contextNode.type === 'let_condition') {
sourceVarNode = contextNode.childForFieldName('value');
} else {
let matchExpr: SyntaxNode | null = contextNode.parent;
while (matchExpr !== null && matchExpr.type !== 'match_expression') {
matchExpr = matchExpr.parent;
}
sourceVarNode = matchExpr?.childForFieldName('value') ?? null;
}
if (sourceVarNode === null || sourceVarNode === undefined) return;
const sourceVarName = sourceVarNode.type === 'identifier' ? sourceVarNode.text : null;
if (sourceVarName === null) return;
const sourceType = lookupTypeInScopes(sourceVarName, contextNode, parsed, scopeMap, moduleScope);
if (sourceType === null) return;
let resolvedType: string | null = null;
if (wrapper === 'Some') {
resolvedType = unwrapGeneric(sourceType);
} else if (wrapper === 'Ok' || wrapper === 'Err') {
const rawType = lookupRawParameterType(sourceVarName, contextNode);
if (rawType !== null) {
const argIdx = wrapper === 'Ok' ? 0 : 1;
resolvedType = extractNthGenericArg(rawType, argIdx);
}
if (resolvedType === null) {
resolvedType = wrapper === 'Ok' ? unwrapGeneric(sourceType) : null;
}
}
if (resolvedType === null) return;
const targetScope = findEnclosingFunctionScope(contextNode, scopeMap) ?? moduleScope;
injectTypeBinding(targetScope, varName, resolvedType);
}
function processStructDestructuring(
root: SyntaxNode,
parsed: ParsedFile,
scopeMap: ReadonlyMap<string, Scope>,
moduleScope: Scope,
allFieldTypes?: ReadonlyMap<string, Map<string, string>>,
): void {
for (const letNode of root.descendantsOfType('let_declaration')) {
const patternNode = letNode.childForFieldName('pattern');
if (patternNode === null || patternNode.type !== 'struct_pattern') continue;
const typeName = patternNode.childForFieldName('type')?.text;
if (typeName === undefined) continue;
const valueNode = letNode.childForFieldName('value');
if (valueNode === null) continue;
const targetScope = findEnclosingFunctionScope(letNode, scopeMap) ?? moduleScope;
for (const fieldNode of patternNode.namedChildren) {
let fieldName: string | undefined;
if (fieldNode.type === 'field_pattern') {
fieldName = fieldNode.childForFieldName('name')?.text;
} else if (fieldNode.type === 'shorthand_field_pattern') {
fieldName = fieldNode.firstNamedChild?.text;
}
if (fieldName === undefined) continue;
let fieldType = lookupFieldType(typeName, fieldName, parsed, scopeMap, moduleScope);
if (fieldType === null) {
fieldType = allFieldTypes?.get(typeName)?.get(fieldName) ?? null;
}
if (fieldType !== null) {
injectTypeBinding(targetScope, fieldName, fieldType);
}
}
}
}
const IDENTITY_METHODS = ['unwrap', 'expect', 'clone', 'as_ref', 'as_mut'];
function processIdentityMethodBindings(parsed: ParsedFile): void {
for (const scope of parsed.scopes) {
if (scope.kind !== 'Class') continue;
const classDef = scope.ownedDefs.find((d) => {
const t = d.type;
return t === 'Struct' || t === 'Enum' || t === 'Class';
});
if (classDef === undefined) continue;
const name = classDef.qualifiedName?.split('.').pop();
if (name === undefined) continue;
for (const method of IDENTITY_METHODS) {
if (!scope.typeBindings.has(method)) {
(scope.typeBindings as Map<string, TypeRef>).set(method, {
rawName: name,
declaredAtScope: scope.id,
source: 'return-annotation',
});
}
}
}
}
function processPendingAssignments(
root: SyntaxNode,
parsed: ParsedFile,
allParsedFiles: readonly ParsedFile[],
scopeMap: ReadonlyMap<string, Scope>,
moduleScope: Scope,
allReturnTypes: ReadonlyMap<string, string>,
): void {
for (let pass = 0; pass < 3; pass++) {
for (const letNode of root.descendantsOfType('let_declaration')) {
const patternNode = letNode.childForFieldName('pattern');
if (patternNode === null) continue;
const varName = extractVarName(patternNode);
if (varName === null) continue;
const targetScope = findEnclosingFunctionScope(letNode, scopeMap) ?? moduleScope;
if (targetScope.typeBindings.has(varName)) continue;
const valueNode = letNode.childForFieldName('value');
if (valueNode === null) continue;
if (valueNode.type === 'identifier') {
const rhsType = lookupTypeInScopes(valueNode.text, letNode, parsed, scopeMap, moduleScope);
if (rhsType !== null) {
injectTypeBinding(targetScope, varName, rhsType);
continue;
}
}
if (valueNode.type === 'field_expression') {
const receiver = valueNode.childForFieldName('value');
const field = valueNode.childForFieldName('field');
if (receiver !== null && field !== null && receiver.type === 'identifier') {
const receiverType = lookupTypeInScopes(
receiver.text,
letNode,
parsed,
scopeMap,
moduleScope,
);
if (receiverType !== null) {
const fieldType = findFieldTypeAcrossFiles(receiverType, field.text, allParsedFiles);
if (fieldType !== null) {
injectTypeBinding(targetScope, varName, fieldType);
}
}
}
}
if (valueNode.type === 'call_expression') {
const func = valueNode.childForFieldName('function');
if (func !== null && func.type === 'field_expression') {
const receiver = func.childForFieldName('value');
const method = func.childForFieldName('field');
if (receiver !== null && method !== null && receiver.type === 'identifier') {
const receiverType = lookupTypeInScopes(
receiver.text,
letNode,
parsed,
scopeMap,
moduleScope,
);
if (receiverType !== null) {
const retType = findMethodReturnTypeAcrossFiles(
receiverType,
method.text,
allParsedFiles,
);
if (retType !== null) {
injectTypeBinding(targetScope, varName, retType);
}
}
}
}
if (func !== null && func.type === 'identifier') {
const rawReturn = allReturnTypes.get(func.text);
if (rawReturn !== undefined) {
injectTypeBinding(targetScope, varName, normalizeFieldType(rawReturn));
}
}
}
}
}
}
function resolveIterableElementType(
valueNode: SyntaxNode,
parsed: ParsedFile,
scopeMap: ReadonlyMap<string, Scope>,
moduleScope: Scope,
allReturnTypes?: ReadonlyMap<string, string>,
): string | null {
let iterableNode = valueNode;
if (iterableNode.type === 'reference_expression') {
iterableNode = iterableNode.firstNamedChild ?? iterableNode;
}
if (iterableNode.type === 'identifier') {
const rawType = lookupTypeInScopes(iterableNode.text, valueNode, parsed, scopeMap, moduleScope);
if (rawType !== null) return unwrapGeneric(rawType);
}
if (iterableNode.type === 'call_expression') {
const func = iterableNode.childForFieldName('function');
if (func === null) return null;
if (func.type === 'field_expression') {
const receiver = func.childForFieldName('value');
if (receiver !== null && receiver.type === 'identifier') {
const rawType = lookupTypeInScopes(receiver.text, valueNode, parsed, scopeMap, moduleScope);
if (rawType !== null) return unwrapGeneric(rawType);
}
}
if (func.type === 'identifier') {
const crossFileReturn = allReturnTypes?.get(func.text);
if (crossFileReturn !== undefined) return unwrapGeneric(crossFileReturn);
const rawReturn = lookupRawFunctionReturnType(func.text, valueNode);
if (rawReturn !== null) return unwrapGeneric(rawReturn);
const returnType = lookupReturnTypeInScopes(func.text, parsed, scopeMap, moduleScope);
if (returnType !== null) return unwrapGeneric(returnType);
}
}
return null;
}
function findFieldTypeAcrossFiles(
structName: string,
fieldName: string,
allParsedFiles: readonly ParsedFile[],
): string | null {
for (const pf of allParsedFiles) {
for (const scope of pf.scopes) {
if (scope.kind !== 'Class') continue;
const hasDef = scope.ownedDefs.some(
(d) => d.qualifiedName === structName || d.qualifiedName?.endsWith('.' + structName),
);
if (!hasDef) continue;
const tb = scope.typeBindings.get(fieldName);
if (tb !== undefined) return tb.rawName;
}
}
return null;
}
function findMethodReturnTypeAcrossFiles(
structName: string,
methodName: string,
allParsedFiles: readonly ParsedFile[],
): string | null {
for (const pf of allParsedFiles) {
for (const scope of pf.scopes) {
if (scope.kind !== 'Class') continue;
const hasDef = scope.ownedDefs.some(
(d) => d.qualifiedName === structName || d.qualifiedName?.endsWith('.' + structName),
);
if (!hasDef) continue;
const tb = scope.typeBindings.get(methodName);
if (tb !== undefined && tb.source === 'return-annotation') return tb.rawName;
}
}
return null;
}
function lookupRawFunctionReturnType(funcName: string, contextNode: SyntaxNode): string | null {
let root: SyntaxNode = contextNode;
while (root.parent !== null) root = root.parent;
for (const fn of root.descendantsOfType('function_item')) {
const nameNode = fn.childForFieldName('name');
if (nameNode !== null && nameNode.text === funcName) {
const retType = fn.childForFieldName('return_type');
if (retType !== null) return retType.text;
}
}
return null;
}
function lookupRawParameterType(paramName: string, contextNode: SyntaxNode): string | null {
let current: SyntaxNode | null = contextNode;
while (current !== null) {
if (current.type === 'function_item') {
const params = current.childForFieldName('parameters');
if (params !== null) {
for (let i = 0; i < params.namedChildCount; i++) {
const param = params.namedChild(i);
if (param === null || param.type !== 'parameter') continue;
const pattern = param.childForFieldName('pattern');
const typeNode = param.childForFieldName('type');
if (pattern !== null && typeNode !== null && pattern.text === paramName) {
return typeNode.text;
}
}
}
break;
}
current = current.parent;
}
return null;
}
function lookupTypeInScopes(
name: string,
contextNode: SyntaxNode,
parsed: ParsedFile,
scopeMap: ReadonlyMap<string, Scope>,
moduleScope: Scope,
): string | null {
const fnScope = findEnclosingFunctionScope(contextNode, scopeMap);
if (fnScope !== null) {
const tb = fnScope.typeBindings.get(name);
if (tb !== undefined) return tb.rawName;
}
const mtb = moduleScope.typeBindings.get(name);
if (mtb !== undefined) return mtb.rawName;
return null;
}
function lookupReturnTypeInScopes(
funcName: string,
parsed: ParsedFile,
_scopeMap: ReadonlyMap<string, Scope>,
moduleScope: Scope,
): string | null {
const tb = moduleScope.typeBindings.get(funcName);
if (tb !== undefined && tb.source === 'return-annotation') return tb.rawName;
for (const scope of parsed.scopes) {
const stb = scope.typeBindings.get(funcName);
if (stb !== undefined && stb.source === 'return-annotation') return stb.rawName;
}
return null;
}
function lookupFieldType(
structName: string,
fieldName: string,
parsed: ParsedFile,
_scopeMap: ReadonlyMap<string, Scope>,
_moduleScope: Scope,
): string | null {
for (const scope of parsed.scopes) {
if (scope.kind !== 'Class') continue;
const hasDef = scope.ownedDefs.some(
(d) => d.qualifiedName === structName || d.qualifiedName?.endsWith('.' + structName),
);
if (!hasDef) continue;
const tb = scope.typeBindings.get(fieldName);
if (tb !== undefined) return tb.rawName;
for (const def of scope.ownedDefs) {
const defName = def.qualifiedName?.split('.').pop();
if (def.type === 'Property' && defName === fieldName) {
const tb = scope.typeBindings.get(fieldName);
if (tb !== undefined) return tb.rawName;
}
}
}
return null;
}
function unwrapGeneric(rawType: string): string {
const match = rawType.match(/^(?:Vec|Option|Arc|Rc|Box|Mutex|RwLock|RefCell|Cell)<(.+)>$/);
if (match) {
const inner = match[1].trim();
const comma = findTopLevelComma(inner);
return comma === -1 ? inner : inner.slice(0, comma).trim();
}
if (rawType.startsWith('&[') && rawType.endsWith(']')) {
return rawType.slice(2, -1).trim();
}
return rawType;
}
function extractNthGenericArg(rawType: string, n: number): string | null {
const open = rawType.indexOf('<');
if (open === -1) return null;
const close = rawType.lastIndexOf('>');
if (close === -1) return null;
const inner = rawType.slice(open + 1, close).trim();
const args = splitTopLevelComma(inner);
return n < args.length ? args[n].trim() : null;
}
function splitTopLevelComma(text: string): string[] {
const parts: string[] = [];
let depth = 0;
let start = 0;
for (let i = 0; i < text.length; i++) {
if (text[i] === '<') depth++;
else if (text[i] === '>') depth--;
else if (text[i] === ',' && depth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
}
parts.push(text.slice(start));
return parts;
}
function findTopLevelComma(text: string): number {
let depth = 0;
for (let i = 0; i < text.length; i++) {
if (text[i] === '<') depth++;
else if (text[i] === '>') depth--;
else if (text[i] === ',' && depth === 0) return i;
}
return -1;
}
function extractVarName(node: SyntaxNode): string | null {
if (node.type === 'identifier') return node.text;
if (node.type === 'mut_pattern' || node.type === 'reference_pattern') {
return node.firstNamedChild?.text ?? null;
}
return null;
}
function injectTypeBinding(scope: Scope, name: string, typeName: string): void {
if (scope.typeBindings.has(name)) return;
(scope.typeBindings as Map<string, TypeRef>).set(name, {
rawName: typeName,
declaredAtScope: scope.id,
source: 'annotation',
});
}
function findEnclosingFunctionScope(
node: SyntaxNode,
scopeMap: ReadonlyMap<ScopeId, Scope>,
): Scope | null {
let current: SyntaxNode | null = node as SyntaxNode;
while (current !== null) {
if (current.type === 'function_item') {
for (const scope of scopeMap.values()) {
if (
scope.kind === 'Function' &&
scope.range.startLine === current.startPosition.row + 1 &&
scope.range.startCol === current.startPosition.column
) {
return scope;
}
}
break;
}
current = current.parent;
}
return null;
}

View file

@ -0,0 +1,139 @@
import type { CaptureMatch } from 'gitnexus-shared';
import { syntheticCapture } from '../../utils/ast-helpers.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Given a function_item node that is inside an impl_item, synthesize a
* self-type-binding capture if the function has a `self_parameter`.
*
* The impl_item structure:
* impl [TraitName for] TypeName { fn method(&self) { ... } }
*/
export function synthesizeRustReceiverBinding(
fnNode: SyntaxNode,
implNode: SyntaxNode | null,
): CaptureMatch | null {
if (fnNode.type !== 'function_item') return null;
if (implNode === null) return null;
const params = fnNode.childForFieldName('parameters');
if (params === null) return null;
let hasSelf = false;
for (let i = 0; i < params.namedChildCount; i++) {
if (params.namedChild(i)?.type === 'self_parameter') {
hasSelf = true;
break;
}
}
if (!hasSelf) return null;
const implType = getImplTargetType(implNode);
if (implType === null) return null;
return {
'@type-binding.self': syntheticCapture('@type-binding.self', fnNode, 'self'),
'@type-binding.name': syntheticCapture('@type-binding.name', fnNode, 'self'),
'@type-binding.type': syntheticCapture('@type-binding.type', fnNode, implType),
};
}
/**
* Extract the target type from an impl_item.
* `impl TypeName { ... }` → "TypeName"
* `impl TraitName for TypeName { ... }` → "TypeName"
*/
export function getImplTargetType(implNode: SyntaxNode): string | null {
if (implNode.type !== 'impl_item') return null;
// Look for `for` keyword — if present, impl is `impl Trait for Type`
let hasFor = false;
let typeAfterFor: SyntaxNode | null = null;
for (let i = 0; i < implNode.childCount; i++) {
const child = implNode.child(i);
if (child === null) continue;
if (child.type === 'for') {
hasFor = true;
continue;
}
if (hasFor && child.type === 'type_identifier') {
typeAfterFor = child;
break;
}
if (hasFor && child.type === 'scoped_type_identifier') {
typeAfterFor = child;
break;
}
if (hasFor && child.type === 'generic_type') {
typeAfterFor = child;
break;
}
}
if (hasFor && typeAfterFor !== null) {
return normalizeRustTypeName(typeAfterFor.text);
}
// No `for` keyword: impl TypeName { ... }
const typeField = implNode.childForFieldName('type');
if (typeField !== null) {
return normalizeRustTypeName(typeField.text);
}
// Fallback: find first type_identifier after `impl`
let afterImpl = false;
for (let i = 0; i < implNode.childCount; i++) {
const child = implNode.child(i);
if (child === null) continue;
if (child.type === 'impl') {
afterImpl = true;
continue;
}
if (afterImpl && (child.type === 'type_identifier' || child.type === 'generic_type')) {
return normalizeRustTypeName(child.text);
}
}
return null;
}
/**
* Extract the trait name from an impl_item when it's `impl Trait for Type`.
*/
export function getImplTraitName(implNode: SyntaxNode): string | null {
if (implNode.type !== 'impl_item') return null;
let afterImpl = false;
for (let i = 0; i < implNode.childCount; i++) {
const child = implNode.child(i);
if (child === null) continue;
if (child.type === 'impl') {
afterImpl = true;
continue;
}
if (child.type === 'for') {
break;
}
if (
afterImpl &&
(child.type === 'type_identifier' || child.type === 'scoped_type_identifier')
) {
for (let j = i + 1; j < implNode.childCount; j++) {
const next = implNode.child(j);
if (next === null) continue;
if (next.type === 'for') {
return normalizeRustTypeName(child.text);
}
break;
}
}
}
return null;
}
function normalizeRustTypeName(text: string): string {
let t = text.trim();
while (t.startsWith('&')) t = t.replace(/^&\s*(mut\s+)?/, '');
while (t.startsWith('*')) t = t.slice(1).trim();
const bracket = t.indexOf('<');
if (bracket !== -1) t = t.slice(0, bracket);
return t.trim();
}

View file

@ -0,0 +1,79 @@
import type { ParsedFile } from 'gitnexus-shared';
import { SupportedLanguages } from 'gitnexus-shared';
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
import { rustProvider } from '../rust.js';
import { rustArityCompatibility, rustMergeBindings, resolveRustImportTarget } from './index.js';
import { populateRustOwners } from './method-owners.js';
import { populateRustRangeBindings } from './range-binding.js';
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
function buildRustMro(
graph: Parameters<ScopeResolver['buildMro']>[0],
parsedFiles: readonly ParsedFile[],
nodeLookup: Parameters<ScopeResolver['buildMro']>[2],
): Map<string, string[]> {
const baseMro = buildMro(graph, parsedFiles, nodeLookup, defaultLinearize);
const defIdByGraphId = new Map<string, string>();
for (const parsed of parsedFiles) {
for (const def of parsed.localDefs) {
if (!isClassLike(def.type)) continue;
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
if (graphId !== undefined) defIdByGraphId.set(graphId, def.nodeId);
}
}
const fileByDefId = new Map<string, string>();
for (const parsed of parsedFiles) {
for (const def of parsed.localDefs) {
fileByDefId.set(def.nodeId, parsed.filePath);
}
}
for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
const childDefId = defIdByGraphId.get(rel.sourceId);
const parentDefId = defIdByGraphId.get(rel.targetId);
if (childDefId === undefined || parentDefId === undefined) continue;
const childFile = fileByDefId.get(childDefId);
const parentFile = fileByDefId.get(parentDefId);
if (childFile !== parentFile) continue;
const existing = baseMro.get(childDefId);
if (existing !== undefined) {
if (!existing.includes(parentDefId)) existing.push(parentDefId);
} else {
baseMro.set(childDefId, [parentDefId]);
}
}
return baseMro;
}
export const rustScopeResolver: ScopeResolver = {
language: SupportedLanguages.Rust,
languageProvider: rustProvider,
importEdgeReason: 'rust-scope: use',
resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) =>
resolveRustImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig),
mergeBindings: (existing, incoming, scopeId) => rustMergeBindings(existing, incoming, scopeId),
arityCompatibility: (callsite, def) => rustArityCompatibility(def, callsite),
buildMro: (graph, parsedFiles, nodeLookup) => buildRustMro(graph, parsedFiles, nodeLookup),
populateOwners: (parsed: ParsedFile) => populateRustOwners(parsed),
isSuperReceiver: () => false,
populateRangeBindings: populateRustRangeBindings,
fieldFallbackOnMethodLookup: false,
hoistTypeBindingsToModule: true,
propagatesReturnTypesAcrossImports: true,
allowGlobalFreeCallFallback: true,
};

View file

@ -0,0 +1,44 @@
import type {
CaptureMatch,
ParsedImport,
Scope,
ScopeId,
ScopeTree,
TypeRef,
} from 'gitnexus-shared';
export function rustBindingScopeFor(
decl: CaptureMatch,
innermost: Scope,
_tree: ScopeTree,
): ScopeId | null {
// Keep self typeBindings in the method's Function scope so
// populateRustOwners can match Method defs to their receiver types.
if (decl['@type-binding.self'] !== undefined) {
return innermost.id;
}
return null;
}
/**
* Rust `use` statements inside a function body should attach at function scope,
* not module scope. If the innermost scope is a Function, attach there.
*/
export function rustImportOwningScope(
_imp: ParsedImport,
innermost: Scope,
_tree: ScopeTree,
): ScopeId | null {
if (innermost.kind === 'Function') {
return innermost.id;
}
return null;
}
export function rustReceiverBinding(functionScope: Scope): TypeRef | null {
if (functionScope.kind !== 'Function') return null;
for (const binding of functionScope.typeBindings.values()) {
if (binding.source === 'self') return binding;
}
return null;
}

View file

@ -78,6 +78,7 @@ export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> = new Set<Suppo
SupportedLanguages.JavaScript,
SupportedLanguages.Kotlin,
SupportedLanguages.Java,
SupportedLanguages.Rust,
]);
/**

View file

@ -19,6 +19,7 @@ import { javaScopeResolver } from '../../languages/java/scope-resolver.js';
import { cScopeResolver } from '../../languages/c/scope-resolver.js';
import { cppScopeResolver } from '../../languages/cpp/scope-resolver.js';
import { phpScopeResolver } from '../../languages/php/scope-resolver.js';
import { rustScopeResolver } from '../../languages/rust/scope-resolver.js';
import { javascriptScopeResolver } from '../../languages/javascript/scope-resolver.js';
import { kotlinScopeResolver } from '../../languages/kotlin/scope-resolver.js';
@ -38,6 +39,7 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
[SupportedLanguages.C, cScopeResolver],
[SupportedLanguages.CPlusPlus, cppScopeResolver],
[SupportedLanguages.PHP, phpScopeResolver],
[SupportedLanguages.Rust, rustScopeResolver],
[SupportedLanguages.JavaScript, javascriptScopeResolver],
[SupportedLanguages.Kotlin, kotlinScopeResolver],
]);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,234 @@
/**
* Rust ingestion pipeline benchmark.
*
* Generates synthetic Rust codebases at increasing scales and measures
* wall-clock time and peak heap through the full pipeline — parsing,
* scope extraction, impl/trait resolution, use decomposition, and
* call resolution.
*
* Run: GITNEXUS_BENCH=1 npx vitest run test/integration/rust-pipeline-benchmark.test.ts
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
const BENCH_ENABLED = process.env.GITNEXUS_BENCH === '1';
interface BenchResult {
fileCount: number;
structCount: number;
moduleCount: number;
elapsedMs: number;
peakHeapMB: number;
nodeCount: number;
edgeCount: number;
}
function generateRustFixture(
fileCount: number,
modulesPerLevel: number,
): { dir: string; structCount: number; moduleCount: number } {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `rust-bench-${fileCount}-`));
const srcDir = path.join(dir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
const modules: string[] = [];
for (let i = 0; i < modulesPerLevel; i++) {
for (let j = 0; j < modulesPerLevel; j++) {
modules.push(`mod${i}_sub${j}`);
}
}
const moduleCount = modules.length;
const structCount = fileCount;
const modDeclarations: string[] = [];
const createdModules = new Set<string>();
for (let f = 0; f < fileCount; f++) {
const modName = modules[f % modules.length];
const structName = `Item${f}`;
const traitName = `Process${f}`;
if (!createdModules.has(modName)) {
const modDir = path.join(srcDir, modName);
fs.mkdirSync(modDir, { recursive: true });
createdModules.add(modName);
modDeclarations.push(`pub mod ${modName};`);
}
const siblingIdx = (f + 1) % fileCount;
const siblingStruct = `Item${siblingIdx}`;
const siblingMod = modules[siblingIdx % modules.length];
const crossIdx = (f + Math.floor(fileCount / 3)) % fileCount;
const crossStruct = `Item${crossIdx}`;
const crossMod = modules[crossIdx % modules.length];
const needsCrossImport = crossMod !== modName;
const content = [
needsCrossImport ? `use crate::${crossMod}::${crossStruct};` : '',
modName !== siblingMod ? `use crate::${siblingMod}::${siblingStruct};` : '',
'',
`pub trait ${traitName} {`,
` fn process(&self) -> String;`,
` fn default_method(&self) -> bool { true }`,
`}`,
'',
`pub struct ${structName} {`,
` pub id: u64,`,
` pub name: String,`,
` pub value: f64,`,
`}`,
'',
`impl ${structName} {`,
` pub fn new(id: u64, name: String) -> Self {`,
` Self { id, name, value: 0.0 }`,
` }`,
'',
` pub fn get_id(&self) -> u64 {`,
` self.id`,
` }`,
'',
` pub fn set_value(&mut self, v: f64) {`,
` self.value = v;`,
` }`,
'',
` pub fn compute(&self) -> f64 {`,
` self.value * self.id as f64`,
` }`,
`}`,
'',
`impl ${traitName} for ${structName} {`,
` fn process(&self) -> String {`,
` format!("{}: {}", self.name, self.compute())`,
` }`,
`}`,
'',
`pub fn create_${structName.toLowerCase()}(id: u64) -> ${structName} {`,
` let mut item = ${structName}::new(id, String::from("test"));`,
` item.set_value(42.0);`,
` let _result = item.compute();`,
` let _processed = item.process();`,
` item`,
`}`,
'',
].join('\n');
const modDir = path.join(srcDir, modName);
const existingMod = path.join(modDir, 'mod.rs');
const fileBaseName = structName.toLowerCase();
fs.writeFileSync(path.join(modDir, `${fileBaseName}.rs`), content);
const modEntry = `pub mod ${fileBaseName};\npub use ${fileBaseName}::*;\n`;
fs.appendFileSync(existingMod, modEntry);
}
const libContent = modDeclarations.join('\n') + '\n';
fs.writeFileSync(path.join(srcDir, 'lib.rs'), libContent);
const cargoToml = [
'[package]',
'name = "bench-rust-pipeline"',
'version = "0.1.0"',
'edition = "2021"',
'',
].join('\n');
fs.writeFileSync(path.join(dir, 'Cargo.toml'), cargoToml);
return { dir, structCount, moduleCount };
}
async function runBenchmark(
fileCount: number,
modLevels: number,
budgetMs: number,
): Promise<BenchResult> {
const { dir, structCount, moduleCount } = generateRustFixture(fileCount, modLevels);
let peakHeapMB = 0;
const heapSampler = setInterval(() => {
const heap = process.memoryUsage().heapUsed / 1024 / 1024;
if (heap > peakHeapMB) peakHeapMB = heap;
}, 50);
try {
const start = Date.now();
const result = await Promise.race([
runPipelineFromRepo(dir, () => {}, { skipGraphPhases: true }),
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error(`Pipeline exceeded ${budgetMs}ms at ${fileCount} files`)),
budgetMs,
),
),
]);
const elapsedMs = Date.now() - start;
return {
fileCount,
structCount,
moduleCount,
elapsedMs,
peakHeapMB: Math.round(peakHeapMB),
nodeCount: result.graph.nodeCount,
edgeCount: result.graph.relationshipCount,
};
} finally {
clearInterval(heapSampler);
fs.rmSync(dir, { recursive: true, force: true });
}
}
function printResults(label: string, results: BenchResult[]) {
console.log(`\n${label}`);
console.log('┌──────────┬─────────┬──────────┬───────────┬──────────┬───────┬───────┐');
console.log('│ Files │ Structs │ Modules │ Time (ms) │ Heap MB │ Nodes │ Edges │');
console.log('├──────────┼─────────┼──────────┼───────────┼──────────┼───────┼───────┤');
for (const r of results) {
console.log(
`│ ${String(r.fileCount).padStart(8)} │ ${String(r.structCount).padStart(7)} │ ${String(r.moduleCount).padStart(8)} │ ${String(r.elapsedMs).padStart(9)} │ ${String(r.peakHeapMB).padStart(8)} │ ${String(r.nodeCount).padStart(5)} │ ${String(r.edgeCount).padStart(5)} │`,
);
}
console.log('└──────────┴─────────┴──────────┴───────────┴──────────┴───────┴───────┘');
if (results.length >= 2) {
console.log('\nScaling ratios (time_ratio / file_ratio):');
for (let i = 1; i < results.length; i++) {
const fileRatio = results[i].fileCount / results[i - 1].fileCount;
const timeRatio = results[i].elapsedMs / results[i - 1].elapsedMs;
const scaling = timeRatio / fileRatio;
console.log(
` ${results[i - 1].fileCount} → ${results[i].fileCount}: ${scaling.toFixed(2)}x (${scaling < 1.5 ? 'linear' : scaling < 3 ? 'superlinear' : 'WARNING: quadratic'})`,
);
}
}
}
describe.skipIf(!BENCH_ENABLED)('Rust pipeline benchmark', () => {
it('scales with file count (workers enabled)', async () => {
const scales = [100, 250, 500];
const results: BenchResult[] = [];
for (const fileCount of scales) {
const modLevels = Math.max(2, Math.ceil(Math.sqrt(fileCount / 4)));
const result = await runBenchmark(fileCount, modLevels, 180_000);
results.push(result);
console.log(
` ${fileCount} files: ${result.elapsedMs}ms, ${result.peakHeapMB}MB heap, ${result.nodeCount} nodes, ${result.edgeCount} edges`,
);
}
printResults('Rust Pipeline — Workers Enabled', results);
for (let i = 1; i < results.length; i++) {
const fileRatio = results[i].fileCount / results[i - 1].fileCount;
const timeRatio = results[i].elapsedMs / results[i - 1].elapsedMs;
expect(timeRatio / fileRatio).toBeLessThan(3);
}
}, 300_000);
});