Files
codegraph/__tests__/rust-prelude-scope.test.ts
T
Colby Mchenry ddc03f7c79 fix(resolution): a bare Rust name reaches only what is in scope (#2124)
Found by the README-wide sweep: ripgrep bound 469 `Some(x)` calls to its
own `EncodingMode::Some` variant and 536 `Ok(x)` to `ParseResult::Ok`;
serde bound its `Ok(…)`/`Result<…>` to the `struct Ok` / `struct Result`
its macro-hygiene test declares (845 on one); tokio 924 `Ok` onto
`TransitionToIdle::Ok`, 290 `drop(x)` onto a `drop` method.

For a name that is bare IN THE SOURCE (a `::` path is kept by its last
segment and is not a prelude lookup — checked at the reference's column):
- an enum variant is in scope only through a `use` of it (and its enum) or
  of its enum's `*`, and never names a type;
- a prelude name (Ok/Err/Some/None/Result/Option/Box/Vec/String/drop/…)
  reaches a project item in another file only through a project `use` of
  that name or a glob of that item's own module (`use crate::glob::*`,
  `use super::*` for the parent) — `use std::…` trees don't count, and the
  `use` scan runs on comment-stripped text (a doc comment's "…use the
  Option…" once read as an import);
- fuzzy matching is case-exact for Rust (`Bytes` ≠ method `bytes`,
  `Ok` ≠ fn `ok`).
Applied as a candidate filter (so the in-scope item can win) and at
resolveOne's seam — a Rust framework resolver's `Ok(x)` → `struct Ok`
construction bypassed name matching.

A/B vs main (edge diffs, every change classified): ripgrep −1,462/+0,
serde −2,072/+40, tokio −1,820/+2, clap −752/+7, axum −397/+4, bat −45/+0 —
losses are Ok/Some/Err/Result/Option/Vec/drop and same-named variants
pinned on project items; gains are pre-existing same-name ambiguity
(serde's two `Content` enums).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 05:34:51 +00:00

80 lines
2.8 KiB
TypeScript

/**
* A bare Rust name reaches only what is in scope. `Some(x)` / `Ok(x)` are the
* prelude's: ripgrep bound every one to its own `EncodingMode::Some` and
* `ParseResult::Ok` variants, serde its `Ok`/`Result` to the structs its
* macro-hygiene test declares. A variant needs a `use` of it or its enum's `*`.
*/
import { describe, it, expect, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
const roots: string[] = [];
afterAll(() => {
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
});
const FILES: Record<string, string> = {
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n',
'src/lowargs.rs': `pub enum EncodingMode {
Auto,
Some(u8),
Disabled,
}
pub enum ParseResult<T> {
Ok(T),
Err(String),
}
`,
'src/hygiene.rs': `struct Ok;
struct Result;
fn local() -> Ok { Ok }
`,
'src/decompress.rs': `pub fn find(cmd: u8) -> Option<u8> {
if cmd > 0 { return Some(cmd); }
let r: std::result::Result<u8, ()> = Ok(cmd);
None
}
`,
'src/error.rs': `pub struct Error;
pub type Result<T> = std::result::Result<T, Error>;
`,
'src/api.rs': `pub fn load() -> crate::error::Result<u8> { unimplemented!() }
`,
'src/modes.rs': `use crate::lowargs::EncodingMode::*;
pub fn pick() -> crate::lowargs::EncodingMode { Some(1) }
`,
};
describe('Rust: a bare name reaches what is in scope', () => {
it('the prelude, a used variant, a same-file item', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-prelude-'));
roots.push(root);
for (const [rel, content] of Object.entries(FILES)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
const cg = await CodeGraph.init(root, { index: true });
try {
const targets = (fn: string): string[] => {
const from = cg.getNodesByName(fn).filter((n) => n.kind === 'function');
return cg
.getOutgoingEdgesFrom(from.map((n) => n.id), ['calls', 'references', 'instantiates'])
.map((e) => cg.getNode(e.target)!)
.map((n) => `${n.kind}:${n.qualifiedName}`);
};
// Prelude `Some` / `Ok`, not the project's variants or structs.
expect(targets('find').filter((t) => /Some|Ok|Result/.test(t))).toEqual([]);
// `use EncodingMode::*` brings the variant in.
expect(targets('pick')).toContain('enum_member:EncodingMode::Some');
// A path (`crate::error::Result`) is not a prelude lookup, so it still links.
expect(targets('load').some((t) => t.endsWith(':Result'))).toBe(true);
// A same-file struct still shadows the prelude.
expect(targets('local')).toContain('struct:Ok');
} finally {
cg.close();
}
});
});