Skip to content

Rust crate::, self:: and super:: imports are classified as I/O #2892

Description

@RaghavChamadiya

Summary

A Rust import that points inside the same crate is classified as an I/O
import whenever one of its path segments happens to match a name in the I/O
table. use crate::request::Foo; becomes a network import because
request is the Node HTTP client, use super::http::fetch; becomes network
because of Dart's http, and use self::fs::helper; becomes filesystem.
The leading crate, self or super segment says the path is local code, but
nothing reads it.

packages/core/src/repowise/core/analysis/health/perf/io_boundaries.py:107-108

    for tok in re.findall(r"[A-Za-z0-9_.:@/]+", text):
        candidates |= _candidate_variants(tok)

This builds the candidate names the classifier tries, and it includes every
interior :: segment of the path.

Mechanism

  • _candidate_variants (io_boundaries.py:56-86) is deliberately generous so
    one function can serve every language. For a :: path it adds each interior
    segment on its own (out.update(colon_parts), io_boundaries.py:79), so
    crate::request::Foo yields crate, request, Foo and the progressive
    prefixes.

  • classify_io_kind (ingestion/external_systems/io_kind.py:180-189) is an
    exact, case-insensitive lookup, not a prefix match. So the candidate
    request hits "request" in _NETWORK_NAMES (io_kind.py:88, the Node
    package), http hits io_kind.py:100 (the Dart package), and fs hits
    io_kind.py:121. Nothing in the table is Rust-specific for these; the Rust
    HTTP crates are only reqwest, hyper and similar (io_kind.py:97).

  • _classify_import (io_boundaries.py:110-122) takes the first candidate
    that resolves and then binds every identifier in the statement to that kind:

    bound = [
        t for t in re.split(r"[^A-Za-z0-9_]+", text) if t.isidentifier() and t not in _IMPORT_KW
    ]

    crate, self and super are not in _IMPORT_KW (io_boundaries.py:37-39),
    so they get bound as well.

  • Rust reaches this through node_type in ("using_directive", "use_declaration")
    at io_boundaries.py:192.

Repro

from tree_sitter_language_pack import get_parser
from repowise.core.analysis.health.perf.io_boundaries import collect_io_names

p = get_parser("rust")
for src in ["use crate::request::Foo;", "use super::http::fetch;",
            "use self::fs::helper;", "use reqwest::Client;"]:
    print(src, collect_io_names(p.parse(src.encode()).root_node, "rust"))

On main (68d6c1d):

use crate::request::Foo;  {'crate': 'network', 'request': 'network', 'Foo': 'network'}
use super::http::fetch;   {'super': 'network', 'http': 'network', 'fetch': 'network'}
use self::fs::helper;     {'self': 'filesystem', 'fs': 'filesystem', 'helper': 'filesystem'}
use reqwest::Client;      {'reqwest': 'network', 'Client': 'network'}

The last line is the control: a real external HTTP crate stays network.
With the _classify_import body from PR #2871 patched into a scratch copy, the
first three lines are unchanged: its longest-variant-first order still reaches
request, http and fs.

This is not only cosmetic. The Rust dialect treats any db name in the file
as a database import (complexity/perf_walk.py:238,
has_db_import = any(k == "db" for k in io_names.values())) and then flags
every .execute(...) call as a db sink (perf/dialects/rust.py:179). A local
module named after a database crate is enough:

use crate::postgres::Row;
fn f(items: Vec<i32>, c: &Conn) { for i in items { c.execute(i); } }
io_boundary_names: {'crate': 'db', 'postgres': 'db', 'Row': 'db'}
perf_hits: [('io_in_loop', 2)]

The same function without the use line produces no hit.

Impact

Rust projects routinely have modules called http, fs, request,
postgres or drift. A use crate::... or use super::... of such a module
silently turns the file into one that "imports a database" or "imports a
network client", which produces io_in_loop findings on unrelated
.execute(...) calls. The wrong kind also lands in io_boundary_names for the
file.

Done looks like

  • _classify_import (io_boundaries.py:89) returns (None, []) for a Rust
    path whose first segment is crate, self or super, so nothing in the
    statement is bound. A leading :: (::std::fs), std and core stay
    external. Keep this scoped to the Rust use shape: do not special-case the
    words self or super for Python, Java or other languages that share this
    function.
  • This builds on fix(health): classify IO imports the same way on every run #2871, which rewrites the candidate loop in
    _classify_import. Start after that PR merges and apply the check to the
    rewritten function. The grouped-use issue (Rust grouped use {a::x, b::y} gives every imported name the first module's I/O kind #2894) will later apply
    the same check per leaf; for this issue checking the statement's first path
    is enough.
  • What must NOT change: use reqwest::Client;, use std::fs::File; and
    use sqlx::Pool; still classify as before. The shared table in
    io_kind.py is not edited, since request, http and fs are right for the
    ecosystems that own them.
  • Because stored perf findings change, bump HEALTH_ANALYZER_VERSION
    (analysis/health/engine.py:382) with a comment in the version history above
    it, as earlier classification changes (v29, v30) did.
  • Why this is a good first issue: one function, one new early return, the table
    and the sink logic are untouched, and the repro is five lines.
  • Worth checking in the same pass: pub(crate) use and use crate::{fs::a, db::b}
    (a brace group with a local prefix), which should both come out empty.

Tests

  • tests/unit/health/test_perf_io_in_loop.py (it already drives walk_file
    for Rust): use crate::request::Foo;, use super::http::fetch; and
    use self::fs::helper; give an empty io_boundary_names.
  • Same file: use crate::postgres::Row; followed by a for loop calling
    c.execute(i) produces no io_in_loop; the same loop with
    use sqlx::PgPool; still produces one.
  • Control: use reqwest::Client; still gives {'reqwest': 'network', 'Client': 'network'}.

Activity

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

    bugSomething isn't workinggood first issueGood for newcomers

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions