Skip to content

Derive Rust crate roots per Cargo target instead of treating every src/** file as the package crate #559

Description

@morluto

Summary

rust_crate_roots derives Rust crate:: and unqualified import candidates from only one predicate: whether the source path begins with src.

That conflates a Cargo package with a Rust crate. A package can contain many distinct crates—library, default binary, src/bin binaries, examples, benches, and integration tests—whose crate:: roots are different source files and directories.

Consequently, imports inside non-default targets can be unresolved or, worse, attached to a same-named module in another crate in the package.

Affected revision: 85dd1b1b918e863f5535333cf435a5ac6163c30e.

Rust/Cargo contract

Cargo documents that:

  • every file under tests/ is compiled as a separate crate;
  • examples and explicitly declared targets are independent targets with their own path;
  • files under src/bin/ are separate binary crates;
  • a crate is one compilation unit with its own module tree and crate root.

References:

Therefore crate::x is relative to the current target's crate root, not automatically to the package's src/ directory.

Relevant implementation

src/indexer/import_resolution.rs routes crate:: and ordinary Rust paths through:

pub(super) fn rust_crate_roots(source: &Path) -> Vec<PathBuf> {
    if source.starts_with("src") {
        vec![PathBuf::from("src"), PathBuf::new()]
    } else {
        vec![PathBuf::new(), PathBuf::from("src")]
    }
}

The function has no target manifest, target path, or owning crate-root input.

Counterexample 1: wrong edge from a src/bin crate

Repository:

Cargo.toml
src/lib.rs
src/util.rs                 # library module
src/bin/admin.rs            # separate binary crate root
src/bin/util.rs             # binary-local module

src/bin/admin.rs:

mod util;
use crate::util::Config;

Rust resolves crate::util in the admin binary crate to:

src/bin/util.rs

LeanToken computes:

rust_crate_roots("src/bin/admin.rs") = ["src", ""]

and therefore generates, in preferred order, candidates including:

src/util.rs
src/util/mod.rs
util.rs
util/mod.rs

It never generates src/bin/util.rs.

Because src/util.rs exists, resolve_import_candidates returns that first existing candidate. LeanToken therefore records a dependency from the binary to the library's unrelated module rather than to the module Rust compiles.

Formally:

RustResolve(admin, crate::util)      = src/bin/util.rs
LeanTokenResolve(admin, crate::util) = src/util.rs

with both paths present. This is a deterministic false edge, not only missing recall.

Counterexample 2: integration-test crate

Repository:

src/common.rs
 tests/api.rs
 tests/common.rs

tests/api.rs:

mod common;
use crate::common::fixture;

Cargo compiles tests/api.rs as a separate crate, and its sibling module is tests/common.rs (or an owned tests/api/common.rs layout depending on the declaration structure).

LeanToken uses roots "" and "src", so it checks common.rs and src/common.rs, never tests/common.rs. If src/common.rs exists, the integration-test import is falsely attached to product code that the test crate's crate::common does not denote.

Counterexample 3: explicit target path

Cargo permits:

[[bin]]
name = "worker"
path = "tools/worker/main.rs"

For tools/worker/main.rs, crate::protocol can be declared by tools/worker/protocol.rs. LeanToken sees a non-src path and tries only repository root and src, so the configured target root is invisible.

The same issue applies to [[example]], [[test]], and [[bench]] paths.

Why #471 does not cover this defect

#471 concerns nested workspace package roots such as crates/core/src/lib.rs, where the nearest package-local src directory should own crate::.

This issue concerns multiple crate targets inside one Cargo package. Even after recognizing crates/core/src as a package root, crates/core/src/bin/tool.rs, crates/core/tests/api.rs, and explicit target paths remain separate crates with distinct roots.

Impact

Incorrect import projections affect:

  • reverse-importer discovery during targeted reconciliation;
  • graph-based context expansion and ranking;
  • owner/test recommendations;
  • changed-file impact analysis;
  • symbol/reference neighborhood evidence;
  • retrieval evaluations that assume import edges approximate compiled module ownership.

The wrong-edge form is especially harmful: a plausible existing file is returned, so the result looks complete while identifying code from a different crate.

Required invariant

For every indexed Rust source file s, LeanToken should first determine its owning Cargo target T and crate root r(T). Candidate construction for crate::p must satisfy:

RustCompilerModulePath(T, p) = x
    => x ∈ LeanTokenCandidates(s, crate::p, T)

and a path belonging only to a different target must not outrank x.

Design direction

Parse Cargo target ownership once into a narrow domain model, for example:

CargoPackage
  └── CargoTarget { kind, name, crate_root, module_root }

Then assign each Rust file to an owning target/module tree and pass the typed target root into import resolution. Sources not provably owned by a target should remain explicit/ambiguous rather than silently falling back to another crate.

Target discovery must include:

Acceptance criteria

  • crate::util in src/bin/admin.rs resolves to src/bin/util.rs, not src/util.rs.
  • A crate::common import in an integration-test target resolves within that test crate and cannot attach to the package library's same-named module.
  • Explicit target paths from Cargo.toml are honored.
  • Default library and default binary targets remain distinct when both exist.
  • Nested workspace packages compose correctly with per-package target ownership.
  • Ambiguous/shared source ownership fails conservatively and is diagnosable.
  • Initial indexing and targeted reconciliation both preserve the corrected reverse-import graph.
  • Tests include missing-edge and wrong-edge cases for bin, integration-test, example, and explicit-path targets.

Non-goals

  • Do not run build scripts or procedural macros to discover modules.
  • Do not claim full rustc name resolution or macro expansion.
  • Do not merge all package sources into one synthetic crate graph; that is the root cause of this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions