Skip to content

[BUG][auto] rust/rust-unimported-bare-name-resolved-tree-wide: a Send bound is counted as a dependency on the crate's own struct Send in an unrelated private module #215

Open

Description

@DaisukeYoda

Bug

A bare Rust type name that its file neither declares nor imports is resolved by looking it up across the whole tree. rustName in polyscan/internal/analysis/coupling.go returns the name unchanged when file.uses has no entry for it, and resolve then falls through byFileBare to byBare, which returns the single declaration of that name anywhere in the tree. A name that is neither local nor imported can only come from the prelude, a glob import or a macro, so the lookup is wrong whenever a crate declares a type with a prelude name: every Send bound in the crate is then counted as a dependency on that type. PR #214 made imported and path-qualified names resolve by their path; an unqualified name that no use binds still reaches the tree-wide lookup.

Repro

// src/lib.rs
mod inner;

pub trait Checker: Send + Sync {
    fn check(&self) -> bool;
}

pub struct Worker {
    name: String,
}

impl Worker {
    pub fn spawn(&self, task: impl FnOnce() + Send + 'static) -> usize {
        task();
        self.name.len()
    }
}

// src/inner.rs
pub struct Send {
    pub bytes: usize,
}

polyscan analyze --format json --select cbo .

Expected

Checker and Worker have no dependency on a type of the tree. inner is a private module and neither item imports inner::Send, so Send can only be std::marker::Send.

Actual

{"name":"Checker","coupling_count":1,"dependent_classes":["Send"]}
{"name":"Worker","coupling_count":1,"dependent_classes":["Send"]}

On cfal/shoes, which declares pub struct Send in src/xhttp/h3.rs:152 behind a private mod h3;, 41 of the 513 listed Rust types carry Send in dependent_classes, 36 of them in files outside src/xhttp/ that cannot name h3::Send at all. Two risk levels change once the entry is removed.

Type Use of Send coupling_count
SaltChecker (src/shadowsocks/salt_checker.rs:3) pub trait SaltChecker: Send + Sync + Debug 1, its only dependency
ProxyConnector (src/tcp/proxy_connector.rs:41) trait bound 8 (high, 7 without it)
CarrierDialer (src/tcp/carrier_dialer.rs:18) trait bound 4 (medium, 3 without it)
State (src/xhttp/server.rs:41) impl FnOnce() + Send + 'static at line 638 23

A fix needs rustdeps.ParseUses to report the glob imports of a file, so that rustName can return false for an unimported bare name when the file has none, and the tree-wide lookup can be limited to the modules a glob names. ParseUses merges the uses of every scope, so a use super::* inside a #[cfg(test)] module would still count as a glob for the file.

polyscan version 0.6.0-10-g4b6a880

Priority P2: the trigger, a crate type named like a prelude item, is uncommon, but one such declaration mis-measures every Send bound in the crate.

Found via the FP-audit skill in repo cfal/shoes@1fac4c231c93f340f71c92bf04ea8e8ab1465877.

Activity

  1. added
    bugSomething isn't working
    P2Medium priority
    auto-filedFiled automatically by the polyscan-fp-audit skill (review before acting)
    on Oct 31, 2026
  2. DaisukeYoda commented on Oct 31, 2026

    @DaisukeYoda
    MemberAuthor

    The same fallback also fires on two other kinds of bare name, found by the triage pass on cfal/shoes with an instrumented build:

    • The name of an associated type item is captured as a reference: type Send = UdpSender; and type Recv = UdpReceiver; at src/wireguard/io.rs:133-134 resolve to src/xhttp/h3.rs:152 and :157, type Value = ConfigSelection<T> at src/config/types/selection.rs:98 resolves to rule_set/geodata.rs:30, and type Udp = ... at src/dns/proxy_runtime.rs:82 resolves to wireguard/io.rs:79.
    • A file-local type alias is never registered as a local name, so a use of it falls through to the tree: type Connection (src/dns/exchange.rs:18, used at :27) resolves to xhttp/h3.rs:12, and type Reservation (src/xhttp/stream.rs:630, used at :626) resolves to xhttp/carrier.rs:26.

    Gating the tree-wide lookup on a glob import covers all three cases.

  3. DaisukeYoda commented on Nov 7, 2026

    @DaisukeYoda
    MemberAuthor

    Associated-type labels are still counted when the tree declares a type of the same name in the same file. A glob gate on the tree-wide lookup does not reach that case, because the name resolves locally before the lookup runs.

    // src/lib.rs
    pub struct Item {
        pub id: u32,
    }
    
    pub struct Numbers {
        values: Vec<u8>,
    }
    
    impl Numbers {
        pub fn iter(&self) -> impl Iterator<Item = &u8> {
            self.values.iter()
        }
    }
    
    pub struct Counter {
        n: u8,
    }
    
    impl Iterator for Counter {
        type Item = u8;
        fn next(&mut self) -> Option<Self::Item> {
            self.n += 1;
            Some(self.n)
        }
    }

    polyscan analyze --format json --select cbo . prints:

    {"name":"Numbers","coupling_count":1,"dependent_classes":["Item"]}
    {"name":"Counter","coupling_count":1,"dependent_classes":["Item"]}

    Neither type refers to struct Item. Item is the label of Iterator<Item = ..> and of type Item = u8;. A label such as Item, Target, Output or Error is not a type reference wherever the same-named type is declared.

    The tree-wide case is widespread on EmbarkStudios/quilkin@58f4f0b: 54 of its 515 listed Rust types carry a dependency that comes only from such a label. Item (a private struct in src/net/io/nic/xdp/process.rs:307) is counted for 28 types through impl Iterator<Item = ..>. Target (a private enum in crates/test/src/bin/loadtest/main.rs:136) is counted for 10 types whose only use is impl Deref { type Target = ..; }. Configuration (pub trait Configuration in crates/xds/src/config.rs:236) is counted for 15 filters and the StaticFilter trait, through type Configuration = Config;. Six types drop from high to medium once these entries are removed: Pass, Capture, Debug, Drop, Match and HashedTokenRouter. Pass (src/filters/pass.rs:24) is a unit struct with no-op read/write methods, and it is reported at CBO 8, high.

    polyscan version 0.6.1. Found via the polyscan-enhancement-audit skill.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium priorityauto-filedFiled automatically by the polyscan-fp-audit skill (review before acting)bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions