Skip to content

Rust use crate::… import paths never resolve to real files — module-function edges rely on accidental global-name fallback #2007

Description

@carlos-alm

Summary

While fixing #1876 (receiver-typed calls + trait dispatch through local vars), I traced why cross-file return-type propagation (Phase 8.2 — returnTypeMap/callAssignments, the same generic mechanism the JS/TS extractor already uses) does not resolve let service = build_service(); in main.rs even though build_service is declared in service.rs and imported via use crate::service::build_service;.

Root cause: resolveImportPathJS (src/domain/graph/resolve.ts) has no handling for Rust's crate::/self::/super::-prefixed module paths. For any import source that doesn't start with . (which covers every Rust use path — Rust has no relative-path import syntax), it falls through to the workspace/exports resolution branches (npm-package semantics) and, finding no match, returns the literal unresolved import-source string (e.g. "crate::service::build_service") as if it were a resolved file path.

This means buildImportedNamesMap's importedNames map for a Rust file maps each imported local name to this bogus non-file string, not to the actual .rs file that declares it.

Why this was hidden until now

Rust's actual "module-function" call edges (e.g. UserService.add_user -> create_user via use crate::models::create_user;) still resolve today — but not through real import-aware resolution. They fall through resolveCallTargets's import-aware branch (which fails silently, since lookup.byNameAndFile(name, "crate::models::create_user") never matches a real file), all the way to resolveExactGlobalMatch's bare global-name fallback, which happens to succeed because the callee names in the fixture are globally unique. This is coincidental, not by design — any Rust project with two same-named functions in different modules would silently produce zero or wrong edges for this pattern, and it also means Rust import edges/confidence scoring never get the importedFrom === targetFile proximity bonus real import resolution provides.

It also blocks legitimate cross-file features that depend on knowing the real target file, such as the return-type propagation added in #1876main -> UserService.add_user/get_user/remove_user (typing service from build_service()'s cross-file return type) remains unresolved specifically because of this gap, not because of anything specific to receiver typing.

Fix direction

Add a Rust-aware branch to resolveImportPathJS (and the native Rust mirror in resolve_import.rs/equivalent) that maps crate::a::b::name / self::a::name / super::a::name paths to real files by walking the project's module tree (mod declarations → file or dir/mod.rs/dir.rs conventions), analogous to how Python's dotted-module imports or Go's package-path imports are resolved elsewhere in this file. This is a non-trivial addition (Rust's module system has its own visibility/path rules) — scoping it as its own issue rather than folding it into #1876's receiver-typing fix.

Evidence

Fixture: tests/benchmarks/resolution/fixtures/rust/. After #1876's fix, resolution-benchmark.test.ts's rust suite sits at 100% precision / 83.3% recall (20/24), with the remaining 4 false negatives all being main -> UserService.*/User.display_name edges that depend on this import-path gap (see expected-edges.json, mode: "receiver-typed" entries citing "local var typed by return value").

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions