Merge branch 'main' into codex/xaml-support-3202

This commit is contained in:
azizur100389 2026-09-16 12:19:43 +01:00 • committed by GitHub
commit 8f11faaa81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 2688 additions and 32 deletions

View file

@ -608,6 +608,16 @@ jobs:
run: node --import tsx bench/python-workspace-import-scan/measure.mjs --check
working-directory: gitnexus
- name: Rust Cargo target membership guards (#3253)
if: ${{ !cancelled() }}
# Build-free: same baseline approach as parse-dispatch-rounds —
# exact membership floors plus a fingerprint, then ratio timing
# only (loadRustCargoTargets 4n/n). Pins typical-Rust completeness
# (derive / println!), include!-abort, and src/target vs Cargo
# artifact layouts. See bench/rust-cargo-targets/measure.mjs.
run: node --import tsx bench/rust-cargo-targets/measure.mjs --check
working-directory: gitnexus
- name: MCP tools/list countRepos vs listRepos guards (#3259, #3184)
if: ${{ !cancelled() }}
# Build-free: exact registry cardinality + tool-roster + schema-flag

View file

@ -664,7 +664,7 @@ XAML files are indexed as documents. Literal `x:Name`, `x:Key`, and `x:Class` de
| Kotlin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| C# | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Go | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Rust | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| Rust | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| PHP | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ |
| Ruby | ✓ | — | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
| Swift | — | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |

View file

@ -431,7 +431,7 @@ TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift,
| Kotlin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| C# | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Go | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Rust | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| Rust | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| PHP | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ |
| Ruby | ✓ | — | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
| Swift | — | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |

View file

@ -0,0 +1,31 @@
{
"_what": "Baselines for bench/rust-cargo-targets/measure.mjs --check. Guards Cargo target membership after #3253: typical-Rust completeness, include!-abort, src/target and explicit target/entry.rs survival. Same approach as bench/parse-dispatch-rounds/baselines.json — exact floors and a fingerprint first; the only timing arm is a ratio.",
"_triage": "READ THIS BEFORE RE-RUNNING. packages, rust_files, typical_complete, include_unknown, explicit_disjoint, disjoint_false, shared_nested, shared_target_module and layout_fingerprint are DETERMINISTIC: a re-run never changes them, and none may be re-baselined to make CI green. load_scaling_ratio is the only timing arm; runner contention dominates it, so re-run on an idle machine before investigating and read the reported `reps` first. If exactly one arm fails and it is that one, suspect the machine.",
"packages": 8,
"rust_files": 32,
"_shape_note": "THE FLOOR. Without these two, every arm below is a ceiling over nothing. typical_complete only asserts something while the corpus still walks many crates. Shrink it to one happy-path package and typical_complete still reads 1 and still passes, asserting a property the corpus no longer has.",
"typical_complete": 1,
"include_unknown": 1,
"explicit_disjoint": 1,
"disjoint_false": 8,
"shared_nested": 8,
"shared_target_module": 8,
"_membership_note": "Exact membership counts. typical_complete=1 pins that #[derive]/println!/assert_eq! do not abort the snapshot. include_unknown=1 pins item-position include! still aborts. explicit_disjoint=1 pins [lib] path = target/entry.rs against tests/helper.rs as a complete negative proof. disjoint_false=8 is each crate's lib vs tests/helper. shared_nested=8 and shared_target_module=8 pin src/nested.rs and src/target/mod.rs as library modules — the latter is the glob that would drop every path segment named target.",
"layout_fingerprint": "1f85812c93302dc4c5089b284c6c395379efc146bfc11b886de499417ec1ae01",
"_layout_fingerprint_note": "sha256 over sorted caller|candidate|share rows on the typical corpus. A change here is a BEHAVIOUR change — the loader returned a different membership set. Explain it, never re-baseline it alone.",
"load_scaling_budget": 1.6,
"_load_scaling_note": "(t_4n / t_n) / 4 for loadRustCargoTargets over the typical corpus; ~1.0 is linear. A RATIO rather than a millisecond ceiling, deliberately: wall-clock is runner-speed-dependent, and this repo has already been bitten by a fixed ms budget. Budget is 1.6, matching parse-dispatch-rounds' pack_scaling_budget. min-of-15 estimator.",
"_measured": {
"load_scaling_ratio": 0.924,
"load_scaling_ratio_samples": [0.681, 0.859, 0.798, 0.834, 0.924],
"small_ms": 24.43,
"large_ms_4x": 75.08,
"reps": 15
},
"_measured_note": "Maxima (and sample lists) over 5 consecutive local runs using the min-of-15 estimator from parse-dispatch-rounds. Milliseconds are diagnostic context only — nothing gates on them."
}

View file

@ -0,0 +1,309 @@
/**
* Build-free bench for Rust Cargo target membership (#3253).
*
* WHY THIS EXISTS. `loadRustCargoTargets` is a negative-proof loader: a
* complete snapshot lets name-fallback refuse a cross-target unique name, and
* an incomplete one fail-opens. Graph output does not show "how many files we
* walked" or "we aborted because of `#[derive]`". A revert to treating
* derive/println! as unknown, a glob that drops every path segment named
* target, or a superlinear membership walk can still emit the same one CALLS
* edge on a tiny fixture.
* This file is the same shape as `bench/parse-dispatch-rounds`: exact floors
* first, one ratio timing arm, never a millisecond ceiling.
*
* ARMS:
*
* - `typical_complete` / `include_unknown` / `explicit_disjoint` — EXACT.
* Typical crates (derive + expression-position std macros) must still
* certify a snapshot. `include!` must still abort it. An explicit
* `[lib] path = "target/entry.rs"` must stay a complete negative proof
* against `tests/helper.rs`, not vanish into the artifact glob.
*
* - `packages` / `rust_files` / `disjoint_false` / `shared_nested` /
* `shared_target_module` — EXACT, and they are the FLOOR. `typical_complete`
* only asserts something while the corpus still has many crates to walk.
* Shrink it to one happy-path package and the complete arm still passes,
* gating a property the corpus no longer has.
*
* - `layout_fingerprint` — EXACT. sha256 over sorted `caller|candidate|share`
* rows on the typical corpus. Catches a membership-set change that leaves
* the counts intact.
*
* - `load_scaling_ratio` — the only timing arm, a RATIO not a millisecond
* ceiling. `(t_4n / t_n) / 4` divides the machine out; ~1.0 is linear.
* Superlinear AST work over crate count lands here.
*
* Usage:
* node --import tsx bench/rust-cargo-targets/measure.mjs
* node --import tsx bench/rust-cargo-targets/measure.mjs --check
*/
import { createHash } from 'node:crypto';
import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import {
loadRustCargoTargets,
rustFilesShareCargoTarget,
} from '../../src/core/ingestion/languages/rust/cargo-targets.ts';
const baselines = JSON.parse(readFileSync(new URL('./baselines.json', import.meta.url), 'utf8'));
const REPS = 15;
const PACKAGES = 8;
const TYPICAL_LIB = [
'#[derive(Debug)]',
'struct S;',
'mod nested;',
'mod target;',
'pub fn helper() {}',
'fn t() { println!("hi"); assert_eq!(1, 1); let _ = vec![1]; let _ = format!("{}", 1); }',
'',
].join('\n');
function writeCrate(root, name, { libPath = 'src/lib.rs', libBody = TYPICAL_LIB } = {}) {
const crateDir = path.join(root, 'crates', name);
mkdirSync(path.join(crateDir, 'src', 'target'), { recursive: true });
mkdirSync(path.join(crateDir, 'src', 'nested'), { recursive: true });
mkdirSync(path.join(crateDir, 'tests'), { recursive: true });
mkdirSync(path.join(crateDir, path.dirname(libPath)), { recursive: true });
const manifest = ['[package]', `name="${name}"`, 'version="0.1.0"', 'edition="2021"', ''];
if (libPath !== 'src/lib.rs') {
manifest.push('[lib]', `path="${libPath}"`, '');
}
writeFileSync(path.join(crateDir, 'Cargo.toml'), `${manifest.join('\n')}`);
writeFileSync(path.join(crateDir, libPath), libBody);
const moduleDir = path.posix.dirname(libPath);
writeFileSync(path.join(crateDir, moduleDir, 'nested.rs'), 'pub fn nested_helper() {}\n');
writeFileSync(path.join(crateDir, 'src', 'target', 'mod.rs'), 'pub fn target_helper() {}\n');
writeFileSync(path.join(crateDir, 'tests', 'helper.rs'), 'pub fn helper() {}\n');
}
function writeTypical(root, packages) {
const names = Array.from({ length: packages }, (_, i) => `c${i}`);
writeFileSync(
path.join(root, 'Cargo.toml'),
`[workspace]\nmembers=[${names.map((n) => `"crates/${n}"`).join(', ')}]\n`,
);
for (const name of names) writeCrate(root, name);
return names;
}
function cratePaths(name, libPath = 'src/lib.rs') {
const lib = `crates/${name}/${libPath}`;
const moduleDir = path.posix.dirname(libPath);
return {
lib,
nested: `crates/${name}/${moduleDir}/nested.rs`,
target: `crates/${name}/src/target/mod.rs`,
tests: `crates/${name}/tests/helper.rs`,
};
}
function countRs(dir) {
let n = 0;
for (const ent of readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) n += countRs(p);
else if (ent.name.endsWith('.rs')) n++;
}
return n;
}
function shareLabel(value) {
if (value === true) return 'true';
if (value === false) return 'false';
return 'undefined';
}
function probesFor(config, names, libPath = 'src/lib.rs') {
const rows = [];
let disjointFalse = 0;
let sharedNested = 0;
let sharedTarget = 0;
for (const name of names) {
const paths = cratePaths(name, libPath);
const nested = rustFilesShareCargoTarget(config, paths.lib, paths.nested);
const target = rustFilesShareCargoTarget(config, paths.lib, paths.target);
const tests = rustFilesShareCargoTarget(config, paths.lib, paths.tests);
if (tests === false) disjointFalse++;
if (nested === true) sharedNested++;
if (target === true) sharedTarget++;
rows.push(
`${paths.lib}|${paths.nested}|${shareLabel(nested)}`,
`${paths.lib}|${paths.target}|${shareLabel(target)}`,
`${paths.lib}|${paths.tests}|${shareLabel(tests)}`,
);
}
return {
disjointFalse,
sharedNested,
sharedTarget,
fingerprint: createHash('sha256').update(rows.sort().join('\n')).digest('hex'),
};
}
async function fastest(fn, reps) {
await fn();
let best = Infinity;
for (let r = 0; r < reps; r++) {
const t0 = performance.now();
await fn();
best = Math.min(best, performance.now() - t0);
}
return best;
}
const roots = [];
function workspace(build) {
const root = mkdtempSync(path.join(tmpdir(), 'gn-rust-cargo-bench-'));
roots.push(root);
build(root);
return root;
}
try {
const typicalRoot = workspace((root) => writeTypical(root, PACKAGES));
const typical4xRoot = workspace((root) => writeTypical(root, PACKAGES * 4));
const explicitRoot = workspace((root) => {
writeFileSync(path.join(root, 'Cargo.toml'), '[workspace]\nmembers=["crates/explicit"]\n');
writeCrate(root, 'explicit', {
libPath: 'target/entry.rs',
libBody: '#[derive(Debug)] struct S;\nmod nested;\npub fn helper() {}\n',
});
});
const includeRoot = workspace((root) => {
writeFileSync(path.join(root, 'Cargo.toml'), '[workspace]\nmembers=["crates/unknown"]\n');
writeCrate(root, 'unknown', { libBody: 'include!("generated.rs");\n' });
});
const typicalNames = Array.from({ length: PACKAGES }, (_, i) => `c${i}`);
const [typicalConfig, explicitConfig, includeConfig] = await Promise.all([
loadRustCargoTargets(typicalRoot),
loadRustCargoTargets(explicitRoot),
loadRustCargoTargets(includeRoot),
]);
const typical = probesFor(typicalConfig, typicalNames);
const explicitPaths = cratePaths('explicit', 'target/entry.rs');
const explicitShare = rustFilesShareCargoTarget(
explicitConfig,
explicitPaths.lib,
explicitPaths.tests,
);
const includeShare = rustFilesShareCargoTarget(
includeConfig,
'crates/unknown/src/lib.rs',
'crates/unknown/tests/helper.rs',
);
const rustFiles = countRs(typicalRoot);
const typicalComplete = typicalConfig !== undefined ? 1 : 0;
const includeUnknown = includeConfig === undefined && includeShare === undefined ? 1 : 0;
const explicitDisjoint = explicitShare === false ? 1 : 0;
const smallMs = await fastest(() => loadRustCargoTargets(typicalRoot), REPS);
const largeMs = await fastest(() => loadRustCargoTargets(typical4xRoot), REPS);
const loadScaling = largeMs / smallMs / 4;
console.log(`packages : ${PACKAGES} (expect ${baselines.packages})`);
console.log(`rust_files : ${rustFiles} (expect ${baselines.rust_files})`);
console.log(
`typical_complete : ${typicalComplete} (expect ${baselines.typical_complete})`,
);
console.log(`include_unknown : ${includeUnknown} (expect ${baselines.include_unknown})`);
console.log(
`explicit_disjoint : ${explicitDisjoint} (expect ${baselines.explicit_disjoint})`,
);
console.log(
`disjoint_false : ${typical.disjointFalse} (expect ${baselines.disjoint_false})`,
);
console.log(
`shared_nested : ${typical.sharedNested} (expect ${baselines.shared_nested})`,
);
console.log(
`shared_target_module : ${typical.sharedTarget} (expect ${baselines.shared_target_module})`,
);
console.log(`layout_fingerprint : ${typical.fingerprint}`);
console.log(
`load_scaling_ratio : ${loadScaling.toFixed(3)} (budget <= ${baselines.load_scaling_budget}; ~1.0 is linear)`,
);
console.log(
`reps : ${REPS} small ${smallMs.toFixed(2)}ms / 4x ${largeMs.toFixed(2)}ms`,
);
if (process.argv.includes('--check')) {
let failed = false;
if (typical.fingerprint !== baselines.layout_fingerprint) {
failed = true;
console.error(
`\nFAIL layout_fingerprint: ${typical.fingerprint}\n` +
` expected ${baselines.layout_fingerprint}\n` +
` Typical-corpus membership moved. Explain it; do not re-baseline alone.`,
);
}
if (typicalComplete !== baselines.typical_complete) {
failed = true;
console.error(
`\nFAIL typical_complete: ${typicalComplete}, expected ${baselines.typical_complete}.\n` +
` Ordinary #[derive] / println! / assert_eq! aborted the snapshot, so the\n` +
` #3253 veto never loads on typical crates.`,
);
}
if (includeUnknown !== baselines.include_unknown) {
failed = true;
console.error(
`\nFAIL include_unknown: ${includeUnknown}, expected ${baselines.include_unknown}.\n` +
` Item-position include! must still abort the membership proof.`,
);
}
if (explicitDisjoint !== baselines.explicit_disjoint) {
failed = true;
console.error(
`\nFAIL explicit_disjoint: ${explicitDisjoint}, expected ${baselines.explicit_disjoint}.\n` +
` [lib] path = "target/entry.rs" was dropped or left unknown — usually\n` +
` **/target/** glob ignore rather than Cargo artifact layouts.`,
);
}
if (
typical.disjointFalse !== baselines.disjoint_false ||
typical.sharedNested !== baselines.shared_nested ||
typical.sharedTarget !== baselines.shared_target_module
) {
failed = true;
console.error(
`\nFAIL membership counts: disjoint_false ${typical.disjointFalse} (expected ${baselines.disjoint_false}), ` +
`shared_nested ${typical.sharedNested} (expected ${baselines.shared_nested}), ` +
`shared_target_module ${typical.sharedTarget} (expected ${baselines.shared_target_module}).\n` +
` Cross-target tests/helper.rs must stay proven-false; src/target/mod.rs must stay a library module.`,
);
}
if (PACKAGES !== baselines.packages || rustFiles !== baselines.rust_files) {
failed = true;
console.error(
`\nFAIL shape: packages ${PACKAGES} (expected ${baselines.packages}), ` +
`rust_files ${rustFiles} (expected ${baselines.rust_files}).\n` +
` The corpus must stay large enough that the complete/disjoint arms still measure a walk.`,
);
}
if (loadScaling > baselines.load_scaling_budget) {
failed = true;
console.error(
`\nFAIL load_scaling_ratio: ${loadScaling.toFixed(3)} exceeds ` +
`${baselines.load_scaling_budget} (~1.0 is linear).\n` +
` Re-run on an idle machine before investigating, and check \`reps\` first.`,
);
}
if (failed) process.exit(1);
console.log('\nOK — within budget.');
}
} finally {
for (const root of roots) {
rmSync(root, { recursive: true, force: true });
}
}

View file

@ -37,6 +37,7 @@
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"proxy-addr": "^2.0.7",
"smol-toml": "^1.8.0",
"tree-sitter": "0.21.1",
"tree-sitter-c-sharp": "0.23.1",
"tree-sitter-cpp": "0.23.2",
@ -4184,6 +4185,18 @@
"dev": true,
"license": "ISC"
},
"node_modules/smol-toml": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz",
"integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 18"
},
"funding": {
"url": "https://github.com/sponsors/cyyynthia"
}
},
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",

View file

@ -92,6 +92,7 @@
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"proxy-addr": "^2.0.7",
"smol-toml": "^1.8.0",
"tree-sitter": "0.21.1",
"tree-sitter-c-sharp": "0.23.1",
"tree-sitter-cpp": "0.23.2",

View file

@ -306,6 +306,9 @@ const NATIVE_ADDON_SMOKE = [
// Filesystem behavior tests — exercise operations that vary across
// platforms (CRLF, symlinks, permissions, temp dirs)
const FILESYSTEM = [
// Cargo membership uses path normalization, descriptor validation, symlinks,
// and Rust native parsing (including long Windows source strings).
'test/unit/scope-resolution/rust-cargo-targets.test.ts',
// The durable ParsedFile store's prune tolerates a chunk directory it cannot
// delete (#3204). The failures that motivate it — held handles, read-only
// mounts — are Windows- and macOS-flavored, and the permission-based case

View file

@ -0,0 +1,340 @@
import path from 'node:path';
import type Parser from 'tree-sitter';
import { splitRustUseDeclaration } from './import-decomposer.js';
// Built-in attributes cannot expand to new module declarations. cfg is a union:
// visiting both alternatives is conservative; cfg_attr may change a path.
const NON_EXPANDING_ATTRIBUTES = new Set([
'cfg',
'path',
'allow',
'warn',
'deny',
'forbid',
'expect',
'doc',
'test',
'should_panic',
'ignore',
'automatically_derived',
'proc_macro',
'proc_macro_derive',
'proc_macro_attribute',
'inline',
'cold',
'no_mangle',
'export_name',
'repr',
'non_exhaustive',
'must_use',
'deprecated',
'no_std',
'no_main',
'feature',
'crate_type',
'crate_name',
'recursion_limit',
'type_length_limit',
]);
const BUILTIN_DERIVES = new Set([
'Clone',
'Copy',
'Debug',
'Default',
'Eq',
'Hash',
'Ord',
'PartialEq',
'PartialOrd',
]);
const EXPRESSION_MACROS = new Set([
'print',
'println',
'eprint',
'eprintln',
'assert',
'assert_eq',
'assert_ne',
'debug_assert',
'debug_assert_eq',
'debug_assert_ne',
'vec',
'format',
'format_args',
'write',
'writeln',
'panic',
'todo',
'unimplemented',
'unreachable',
'dbg',
'matches',
]);
/** Token arguments can contain blocks/modules or further macro expansion.
* Inspect token nodes, never strings/comments that merely mention those words. */
function hasExpandingArguments(tokens: Parser.SyntaxNode): boolean {
for (let i = 0; i < tokens.childCount; i++) {
const child = tokens.child(i)!;
if (child.type === 'mod' || child.type === '#') return true;
if (child.type === '!' && tokens.child(i - 1)?.type === 'identifier') return true;
if (child.type === 'token_tree' && hasExpandingArguments(child)) return true;
}
return false;
}
/** Public use evidence from the same AST used for membership. Keeps restricted
* and private globs distinct without changing the shared import/cache shape. */
export function rustPublicUses(root: Parser.SyntaxNode): ReadonlySet<string> {
const uses = new Set<string>();
const pending = [{ node: root, module: '' }];
while (pending.length > 0) {
const { node, module } = pending.pop()!;
for (const child of node.namedChildren) {
if (child.type === 'mod_item') {
const body = child.childForFieldName('body');
const name = child.childForFieldName('name')?.text;
if (body && name)
pending.push({ node: body, module: [module, name].filter(Boolean).join('::') });
} else if (
child.type === 'use_declaration' &&
child.namedChildren.some(
(part) => part.type === 'visibility_modifier' && part.text === 'pub',
)
) {
for (const capture of splitRustUseDeclaration(child)) {
uses.add(
JSON.stringify([
module,
capture['@import.source']?.text,
capture['@import.kind']?.text,
capture['@import.name']?.text,
]),
);
}
}
}
}
return uses;
}
/** Decode a literal path without mistaking strings/comments for Rust syntax. */
function literalPath(text: string): string | undefined {
const raw = /^r(#+)?"([\s\S]*)"\1$/.exec(text);
if (raw) return raw[2];
// Escape forms beyond JSON's subset remain unknown, never a guessed path.
try {
const value: unknown = JSON.parse(text);
return typeof value === 'string' ? value : undefined;
} catch {
return undefined;
}
}
/** External modules reachable from one source file; undefined is incomplete. */
export function rustModuleFiles(
root: Parser.SyntaxNode,
file: string,
ownsDirectory: boolean,
files: ReadonlySet<string>,
missingFiles?: Set<string>,
isCrateRoot = false,
): readonly { file: string; ownsDirectory: boolean }[] | undefined {
if (root.hasError) return undefined;
// Built-in spellings are not proof when an import/local macro can shadow
// them. Cross-file macro_use/macro_export remain unknown attributes below.
const shadowed = new Set<string>();
const globs: { node: Parser.SyntaxNode; path: string }[] = [];
const scan = [root];
while (scan.length) {
const node = scan.pop()!;
if (node.type === 'macro_definition' || node.type === 'mod_item') {
const name = node.childForFieldName('name')?.text;
if (name) shadowed.add(name);
// macro_rules textual scope can extend into child module files. Without
// expansion/scope receipts, do not assume their same-named calls are std.
if (node.type === 'macro_definition' && name && EXPRESSION_MACROS.has(name)) return undefined;
}
if (node.type === 'use_declaration') {
for (const capture of splitRustUseDeclaration(node)) {
if (capture['@import.kind']?.text === 'wildcard')
globs.push({ node, path: capture['@import.source']?.text ?? '' });
const name = capture['@import.name']?.text;
if (name) shadowed.add(name);
}
}
if (node.type !== 'macro_definition' && node.type !== 'token_tree')
scan.push(...node.namedChildren);
}
const wildcard = globs.some(({ node, path: imported }) => {
const parts = imported.split('::').filter(Boolean);
// Cargo aliases for these names are rejected by the loader. A local
// module/import can still shadow a standard-library path in this file.
if (['std', 'core', 'alloc'].includes(parts[0] ?? '') && !shadowed.has(parts[0]!)) return false;
let depth = 0;
let localModuleScope = true;
for (let parent = node.parent; parent && parent !== root; parent = parent.parent) {
if (parent.type === 'mod_item') depth++;
if (
parent.type === 'function_item' ||
parent.type === 'block' ||
parent.type === 'closure_expression'
)
localModuleScope = false;
}
// The common inline unit-test `use super::*` stays within this AST.
// A file-level super glob has an external parent and remains unknown.
if (
localModuleScope &&
parts.length > 0 &&
parts.every((part) => part === 'super') &&
parts.length <= depth
)
return false;
// At a Cargo root, bare/self/crate paths to inline modules have the same
// meaning across editions. Do not extend this assumption to file modules.
if (isCrateRoot && node.parent === root) {
if (parts[0] === 'self' || parts[0] === 'crate') parts.shift();
let body: Parser.SyntaxNode | undefined = root;
for (const part of parts) {
body =
body?.namedChildren
.find(
(child) =>
child.type === 'mod_item' && child.childForFieldName('name')?.text === part,
)
?.childForFieldName('body') ?? undefined;
}
if (parts.length > 0 && body !== undefined) return false;
}
return true;
});
const builtin = (name: string): boolean => !wildcard && !shadowed.has(name);
const fileDir = path.posix.dirname(file);
const moduleDir =
ownsDirectory || path.posix.basename(file) === 'mod.rs'
? fileDir
: file.slice(0, -'.rs'.length);
const pending = [{ node: root, moduleDir, attributeDir: fileDir }];
const result: { file: string; ownsDirectory: boolean }[] = [];
let unresolved = false;
while (pending.length) {
const context = pending.pop()!;
let attributes: Parser.SyntaxNode[] = [];
for (const node of context.node.namedChildren) {
if (node.type === 'line_comment' || node.type === 'block_comment') continue;
if (node.type === 'attribute_item' || node.type === 'inner_attribute_item') {
const attribute = node.namedChildren[0];
const name = attribute?.namedChildren[0]?.text;
if (name === 'derive') {
const argumentsNode = attribute?.childForFieldName('arguments');
if (
!argumentsNode ||
!builtin('derive') ||
argumentsNode.namedChildren.length === 0 ||
argumentsNode.namedChildren.some(
(item) =>
item.type !== 'identifier' ||
!BUILTIN_DERIVES.has(item.text) ||
!builtin(item.text),
) ||
argumentsNode.children.some(
(item) =>
!['(', ')', ',', 'identifier', 'line_comment', 'block_comment'].includes(item.type),
)
)
return undefined;
} else if (!name || !NON_EXPANDING_ATTRIBUTES.has(name)) return undefined;
if (node.type === 'attribute_item') attributes.push(node);
continue;
}
const attrs = attributes;
attributes = [];
// The current scope captures do not carry extern-crate aliases. Do not
// certify a negative import-root proof from an incomplete namespace view.
if (node.type === 'extern_crate_declaration' && node.childForFieldName('alias') !== null) {
return undefined;
}
const macro =
node.type === 'macro_invocation'
? node
: node.type === 'expression_statement' &&
node.namedChildren[0]?.type === 'macro_invocation'
? node.namedChildren[0]
: undefined;
if (macro) {
const name = macro.childForFieldName('macro')?.text;
const tokens = macro.namedChildren.find((child) => child.type === 'token_tree');
const parts = name?.split('::').filter(Boolean) ?? [];
const standard =
parts.length === 1 ||
(parts.length === 2 && ['std', 'core', 'alloc'].includes(parts[0]!));
if (
!standard ||
!EXPRESSION_MACROS.has(parts.at(-1) ?? '') ||
!parts.every(builtin) ||
!tokens ||
hasExpandingArguments(tokens) ||
context.node.type === 'source_file' ||
context.node.type === 'declaration_list'
)
return undefined;
continue;
}
if (node.type !== 'mod_item') {
// Items (including external #[path] modules) can also occur in blocks.
// Inspect them too; a macro expansion there can add shared membership.
// Macro definitions/token trees and literal contents are not expansions.
if (
node.type !== 'macro_definition' &&
node.type !== 'token_tree' &&
node.namedChildCount > 0
) {
pending.push({ ...context, node });
}
continue;
}
const name = node.childForFieldName('name')?.text;
if (!name) return undefined;
let override: string | undefined;
for (const attr of attrs) {
const attribute = attr.namedChildren[0]!;
if (attribute.namedChildren[0]?.text !== 'path') continue;
const value = attribute.childForFieldName('value');
if (!value || override !== undefined) return undefined;
override = literalPath(value.text);
if (override === undefined || path.posix.isAbsolute(override) || override.includes('\\'))
return undefined;
}
const body = node.childForFieldName('body');
if (body) {
const dir =
override === undefined
? path.posix.join(context.moduleDir, name)
: path.posix.join(context.attributeDir, override);
pending.push({ node: body, moduleDir: dir, attributeDir: dir });
continue;
}
const candidates =
override !== undefined
? [path.posix.normalize(path.posix.join(context.attributeDir, override))]
: [
path.posix.join(context.moduleDir, `${name}.rs`),
path.posix.join(context.moduleDir, name, 'mod.rs'),
];
const existing = candidates.filter((candidate) => files.has(candidate));
if (existing.length === 0) {
if (!missingFiles) return undefined;
for (const candidate of candidates) missingFiles.add(candidate);
unresolved = true;
continue;
}
// Union conditional alternatives; shared membership must never be erased.
// #[path] makes the loaded file own its containing directory, just like
// a crate root; its children do NOT acquire the file stem as a prefix.
result.push(...existing.map((file) => ({ file, ownsDirectory: override !== undefined })));
}
}
return unresolved ? undefined : result;
}

View file

@ -0,0 +1,513 @@
/** Cargo target evidence for the name-guess veto, never a directory heuristic. */
import fs from 'node:fs/promises';
import path from 'node:path';
import { glob, escape } from 'glob';
import { parse } from 'smol-toml';
import Parser from 'tree-sitter';
import { SupportedLanguages } from 'gitnexus-shared';
import { getLanguageGrammar } from '../../../tree-sitter/parser-loader.js';
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
import { readRepoControlFile } from '../../../../config/repo-control-file.js';
import { rustModuleFiles, rustPublicUses } from './cargo-module-files.js';
const MAX_FILES = 100_000;
type Table = Record<string, unknown>;
const table = (value: unknown): value is Table =>
value !== null && typeof value === 'object' && !Array.isArray(value);
/** A target is identified by its entry file, not its package directory. */
export function cargoTargetRoots(
manifest: string,
content: string,
files: ReadonlySet<string>,
workspaceEditions?: ReadonlyMap<string, string>,
): readonly string[] | undefined {
let data: Table;
try {
data = parse(content);
} catch {
return undefined;
}
if (!table(data.package)) return table(data.workspace) ? [] : undefined;
const pkg = data.package;
if (typeof pkg.name !== 'string') return undefined;
if (pkg.build !== undefined && typeof pkg.build !== 'string' && typeof pkg.build !== 'boolean')
return undefined;
const dir = path.posix.dirname(manifest);
let edition: unknown = pkg.edition ?? '2015';
if (table(edition) && edition.workspace === true) {
let workspace =
typeof pkg.workspace === 'string' ? path.posix.join(dir, pkg.workspace, '.') : dir;
while (
!workspaceEditions?.has(workspace) &&
typeof pkg.workspace !== 'string' &&
workspace !== '.'
) {
workspace = path.posix.dirname(workspace);
}
edition = workspaceEditions?.get(workspace);
}
if (typeof edition !== 'string' || !['2015', '2018', '2021', '2024'].includes(edition))
return undefined;
const relative = (file: string): string => path.posix.normalize(path.posix.join(dir, file));
const roots = new Set<string>();
for (const [kind, folder] of [
['lib', 'src'],
['bin', 'src/bin'],
['test', 'tests'],
['bench', 'benches'],
['example', 'examples'],
] as const) {
const autoKey = {
lib: 'autolib',
bin: 'autobins',
test: 'autotests',
bench: 'autobenches',
example: 'autoexamples',
}[kind];
if (pkg[autoKey] !== undefined && typeof pkg[autoKey] !== 'boolean') return undefined;
const discovered = new Map<string, string>();
if (kind === 'lib') {
if (files.has(relative('src/lib.rs'))) discovered.set(pkg.name, relative('src/lib.rs'));
} else {
if (kind === 'bin' && files.has(relative('src/main.rs'))) {
discovered.set(pkg.name, relative('src/main.rs'));
}
const prefix = `${relative(folder)}/`;
for (const file of files) {
if (!file.startsWith(prefix)) continue;
const tail = file.slice(prefix.length);
const match = /^([^/]+)\.rs$/.exec(tail) ?? /^([^/]+)\/main\.rs$/.exec(tail);
if (match) discovered.set(match[1]!, file);
}
}
const explicit = data[kind] === undefined ? [] : kind === 'lib' ? [data[kind]] : data[kind];
if (!Array.isArray(explicit)) return undefined;
// Cargo 2015's opt-in discovery rule is PER target kind: an explicit
// binary does not disable integration tests, examples, benches or the lib.
const legacy = edition === '2015' && explicit.length > 0;
const overridden = new Set<string>();
for (const entry of explicit) {
if (!table(entry)) return undefined;
const name = kind === 'lib' ? pkg.name : entry.name;
if (typeof name !== 'string') return undefined;
if (entry.path !== undefined && typeof entry.path !== 'string') return undefined;
if (typeof entry.path === 'string' && path.posix.isAbsolute(entry.path)) return undefined;
const file = typeof entry.path === 'string' ? relative(entry.path) : discovered.get(name);
if (!file || !files.has(file)) return undefined;
roots.add(file);
overridden.add(name);
}
if (pkg[autoKey] === true || (pkg[autoKey] !== false && !legacy)) {
for (const [name, file] of discovered) if (!overridden.has(name)) roots.add(file);
}
}
// Build scripts are crates too, even when located outside src/.
if (typeof pkg.build === 'string') {
const file = relative(pkg.build);
if (!files.has(file)) return undefined;
if (roots.has(file)) return undefined; // Multiple target roles need separate identities.
roots.add(file);
} else if (pkg.build !== false && files.has(relative('build.rs'))) {
if (roots.has(relative('build.rs'))) return undefined;
roots.add(relative('build.rs'));
}
return [...roots];
}
class RustCargoTargets {
constructor(
readonly targetsByFile: ReadonlyMap<string, ReadonlySet<string>>,
readonly rootImports: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>,
readonly publicUsesByFile: ReadonlyMap<string, ReadonlySet<string>>,
) {}
}
/** Exact public-use evidence, independent of the capture's coarse reexport kind. */
export function rustCargoPubliclyReexports(
config: unknown,
file: string,
module: string,
target: string,
kind: string,
name: string,
): boolean {
return (
config instanceof RustCargoTargets &&
config.publicUsesByFile.get(file)?.has(JSON.stringify([module, target, kind, name])) === true
);
}
/** Positive evidence that this import names this library's ROOT, not a module
* elsewhere (or a binary/test entry point that cannot be imported as a lib). */
export function rustImportNamesCargoRoot(
config: unknown,
caller: string,
candidate: string,
importedModule: string,
): boolean {
if (!(config instanceof RustCargoTargets)) return false;
const segments = importedModule.split('::').filter(Boolean);
if (segments.length !== 1) return false;
for (const target of config.targetsByFile.get(caller) ?? []) {
if (config.rootImports.get(target)?.get(segments[0]!)?.has(candidate)) return true;
}
return false;
}
/** Every known membership must identify this file as the entry point. A file
* shared as a module in another target does not have a single root role. */
export function rustIsExclusiveCargoRoot(config: unknown, file: string): boolean {
if (!(config instanceof RustCargoTargets)) return false;
const targets = config.targetsByFile.get(file);
return targets?.size === 1 && targets.has(file);
}
/** Establish crate identity before the existing module-path plausibility test. */
export function rustImportReachesCargoTarget(
config: unknown,
caller: string,
candidate: string,
importedModule: string,
): boolean {
if (!(config instanceof RustCargoTargets)) return false;
const name = importedModule.split('::').filter(Boolean)[0];
if (!name) return false;
const candidates = config.targetsByFile.get(candidate);
for (const target of config.targetsByFile.get(caller) ?? []) {
for (const imported of config.rootImports.get(target)?.get(name) ?? []) {
if (candidates?.has(imported)) return true;
}
}
return false;
}
/** Import names are target/package-relative. A dependency alias in another
* package must not authorize a guess here merely because its spelling matches. */
function cargoRootImports(
manifests: ReadonlyMap<string, Table>,
targets: ReadonlyMap<string, readonly string[]>,
): Map<string, Map<string, Set<string>>> {
const libraries = new Map<string, { root: string; name: string }>();
for (const [manifest, data] of manifests) {
if (!table(data.package)) continue;
const lib = table(data.lib) ? data.lib : undefined;
if (!lib && data.package.autolib === false) continue;
const root = path.posix.join(
path.posix.dirname(manifest),
typeof lib?.path === 'string' ? lib.path : 'src/lib.rs',
);
const name = lib?.name ?? data.package.name;
if (typeof name === 'string' && targets.get(manifest)?.includes(root)) {
libraries.set(manifest, {
root,
name: name.replaceAll('-', '_'),
});
}
}
const result = new Map<string, Map<string, Set<string>>>();
for (const [manifest, data] of manifests) {
if (!table(data.package)) continue;
const dir = path.posix.dirname(manifest);
let workspace =
typeof data.package.workspace === 'string'
? path.posix.join(dir, data.package.workspace, '.')
: dir;
while (
!table(manifests.get(path.posix.join(workspace, 'Cargo.toml'))?.workspace) &&
typeof data.package.workspace !== 'string' &&
workspace !== '.'
)
workspace = path.posix.dirname(workspace);
const workspaceData = manifests.get(path.posix.join(workspace, 'Cargo.toml'))?.workspace;
const workspaceDeps =
table(workspaceData) && table(workspaceData.dependencies) ? workspaceData.dependencies : {};
const sections = [
data,
...(table(data.target) ? Object.values(data.target).filter(table) : []),
];
for (const target of targets.get(manifest) ?? []) {
let imports = result.get(target);
if (!imports) result.set(target, (imports = new Map()));
const add = (name: string, root: string) => {
let roots = imports.get(name);
if (!roots) imports.set(name, (roots = new Set()));
roots.add(root);
};
const own = libraries.get(manifest);
const buildRoot = path.posix.join(
dir,
typeof data.package.build === 'string' ? data.package.build : 'build.rs',
);
const isBuild = data.package.build !== false && target === buildRoot;
if (own && !isBuild) add(own.name, own.root);
for (const section of sections) {
// Libraries/binaries can also compile as unit-test targets, so retain
// dev dependencies across cfg modes. Build scripts have their own
// dependency namespace and cannot import the package's own library.
const kinds = isBuild ? ['build-dependencies'] : ['dependencies', 'dev-dependencies'];
for (const kind of kinds) {
const deps = section[kind];
if (!table(deps)) continue;
for (const [key, declared] of Object.entries(deps)) {
const inherited = table(declared) && declared.workspace === true;
const dep = inherited ? workspaceDeps[key] : declared;
if (!table(dep) || typeof dep.path !== 'string') continue;
const dependency = libraries.get(
path.posix.join(inherited ? workspace : dir, dep.path, 'Cargo.toml'),
);
if (!dependency) continue;
// Cargo uses the dependency key whenever `package` is explicit,
// even if it equals the package name and [lib].name differs.
const renamed = typeof dep.package === 'string';
add(renamed ? key.replaceAll('-', '_') : dependency.name, dependency.root);
}
}
}
}
}
return result;
}
/** Undefined means no complete membership proof; never interpret it as disjoint. */
export function rustFilesShareCargoTarget(
config: unknown,
caller: string,
candidate: string,
): boolean | undefined {
if (!(config instanceof RustCargoTargets)) return undefined;
const callers = config.targetsByFile.get(caller);
const candidates = config.targetsByFile.get(candidate);
if (!callers || !candidates) return undefined;
return [...callers].some((root) => candidates.has(root));
}
/**
* Static, bounded, one-shot provider loader. No cargo/rustc, build scripts,
* repository wrappers, or network. Parse only files reachable from Cargo roots.
* Unknown expansion anywhere can add shared membership, so it invalidates the
* negative proof for this snapshot rather than producing a partial veto.
*/
export async function loadRustCargoTargets(repoPath: string): Promise<unknown> {
try {
const root = await fs.realpath(repoPath);
const files = new Set<string>();
const manifests: string[] = [];
for await (const entry of glob.iterate('**/Cargo.toml', {
// Cargo metadata must include targets the graph scanner omits (notably
// src/bin). An omitted target can share a source file with another crate.
cwd: root,
nodir: true,
follow: false,
posix: true,
dot: true,
ignore: [
'**/.git/**',
'**/node_modules/**',
'**/.gitnexus/**',
'**/target/debug/**',
'**/target/release/**',
'**/target/incremental/**',
'**/target/doc/**',
'**/target/tmp/**',
'**/target/.fingerprint/**',
'**/target/CACHEDIR.TAG',
],
})) {
if (files.size >= MAX_FILES) return undefined;
files.add(entry);
manifests.push(entry);
}
if (manifests.length === 0) return undefined;
// Artifact-layout pruning (`target/debug`, `target/release`, …) must not
// erase a source path whose segment is named `target` (`src/target/mod.rs`,
// `[lib] path = "target/entry.rs"`, `src/bin/target/main.rs`). Re-scan
// Cargo's auto-target slots without that filter.
for (const manifest of manifests) {
for await (const entry of glob.iterate(
[
'src/lib.rs',
'src/main.rs',
'src/bin/*.rs',
'src/bin/*/main.rs',
'tests/*.rs',
'tests/*/main.rs',
'benches/*.rs',
'benches/*/main.rs',
'examples/*.rs',
'examples/*/main.rs',
'build.rs',
],
{
cwd: path.join(root, path.posix.dirname(manifest)),
nodir: true,
follow: false,
posix: true,
},
)) {
if (files.size >= MAX_FILES) return undefined;
files.add(path.posix.join(path.posix.dirname(manifest), entry));
}
}
const read = async (file: string): Promise<string> => {
const requested = path.resolve(root, file);
const absolute = await fs.realpath(requested);
const rel = path.relative(root, absolute);
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
throw new Error('Cargo module outside repository');
}
if (absolute !== requested) throw new Error('Cargo module alias has unknown membership');
// The shared reader validates its opened descriptor and bounds streamed
// bytes; the outer realpath check is a separate containment/alias guard.
const content = await readRepoControlFile(root, file);
if (content === null) throw new Error('Cargo source disappeared');
return content;
};
const contents = new Map<string, string>();
const manifestData = new Map<string, Table>();
const workspaceEditions = new Map<string, string>();
for (const manifest of manifests) {
const content = await read(manifest);
contents.set(manifest, content);
const data = parse(content);
// A dependency alias can replace a std/core/alloc extern-prelude entry.
// In that case the module walker cannot identify standard macros safely.
const sections = [
data,
...(table(data.target) ? Object.values(data.target).filter(table) : []),
];
if (
sections.some((section) =>
['dependencies', 'dev-dependencies', 'build-dependencies'].some(
(kind) =>
table(section[kind]) &&
['std', 'core', 'alloc'].some((name) => Object.hasOwn(section[kind], name)),
),
)
)
return undefined;
manifestData.set(manifest, data);
if (
table(data.workspace) &&
table(data.workspace.package) &&
typeof data.workspace.package.edition === 'string'
) {
workspaceEditions.set(path.posix.dirname(manifest), data.workspace.package.edition);
}
}
// Manifest discovery prunes artifacts, but explicit source paths and mod
// declarations are authoritative candidates even beneath a `target` folder.
// Probe only those literal paths; never crawl the artifact tree recursively.
const discover = async (candidates: Iterable<string>): Promise<void> => {
const patterns = [...candidates].map((file) => {
if (
path.posix.isAbsolute(file) ||
file === '..' ||
file.startsWith('../') ||
file.includes('\\')
)
throw new Error('Cargo source outside repository');
if (
!file.endsWith('.rs') ||
file
.split('/')
.some((part) => ['.git', '.gitnexus', 'node_modules'].includes(part.toLowerCase()))
)
throw new Error('Cargo source excluded from inventory');
return escape(file);
});
for await (const entry of glob.iterate(patterns, {
cwd: root,
nodir: true,
follow: false,
posix: true,
dot: true,
})) {
if (files.size >= MAX_FILES) throw new Error('Cargo file limit');
files.add(entry);
}
};
for (const [manifest, data] of manifestData) {
const explicit = [
data.lib,
...['bin', 'test', 'bench', 'example'].flatMap((kind) =>
Array.isArray(data[kind]) ? data[kind] : [],
),
];
const paths = explicit
.filter(table)
.map((entry) => entry.path)
.filter((value): value is string => typeof value === 'string');
if (table(data.package) && typeof data.package.build === 'string')
paths.push(data.package.build);
await discover(paths.map((file) => path.posix.join(path.posix.dirname(manifest), file)));
}
const roots = new Set<string>();
const targetsByManifest = new Map<string, readonly string[]>();
for (const [manifest, content] of contents) {
const targets = cargoTargetRoots(manifest, content, files, workspaceEditions);
if (targets === undefined) return undefined;
targetsByManifest.set(manifest, targets);
for (const target of targets) roots.add(target);
}
const parser = new Parser();
parser.setLanguage(getLanguageGrammar(SupportedLanguages.Rust));
const targetsByFile = new Map<string, Set<string>>();
const publicUsesByFile = new Map<string, ReadonlySet<string>>();
// A file may be reached conventionally AND through #[path]. Those have
// different submodule bases, so cache and visit both contexts separately.
const childrenByFile = new Map<string, NonNullable<ReturnType<typeof rustModuleFiles>>>();
let visits = 0;
for (const target of roots) {
const pending = [{ file: target, ownsDirectory: true }];
const visited = new Set<string>();
while (pending.length > 0) {
if (++visits > MAX_FILES) return undefined;
const { file, ownsDirectory } = pending.pop()!;
const key = `${file === target ? 'root' : ownsDirectory ? 'owned' : 'module'}:${file}`;
if (visited.has(key)) continue;
visited.add(key);
let owners = targetsByFile.get(file);
if (!owners) targetsByFile.set(file, (owners = new Set()));
owners.add(target);
let children = childrenByFile.get(key);
if (!children) {
const tree = parseSourceSafe(parser, await read(file));
const missing = new Set<string>();
let result = rustModuleFiles(
tree.rootNode,
file,
ownsDirectory,
files,
missing,
file === target,
);
if (result === undefined && missing.size > 0) {
await discover(missing);
result = rustModuleFiles(
tree.rootNode,
file,
ownsDirectory,
files,
undefined,
file === target,
);
}
if (result === undefined) return undefined;
publicUsesByFile.set(file, rustPublicUses(tree.rootNode));
children = result;
childrenByFile.set(key, children);
}
pending.push(...children);
}
}
return new RustCargoTargets(
targetsByFile,
cargoRootImports(manifestData, targetsByManifest),
publicUsesByFile,
);
} catch {
// I/O, parse or containment failure cannot establish target separation.
return undefined;
}
}

View file

@ -10,16 +10,19 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js';
export function splitRustUseDeclaration(node: SyntaxNode): CaptureMatch[] {
if (node.type !== 'use_declaration') return [];
const isReexport = hasVisibilityModifier(node);
const isReexport = hasUnrestrictedPub(node);
const argument = getUseArgument(node);
if (argument === null) return [];
return decomposeUseArgument(argument, '', isReexport, node);
}
function hasVisibilityModifier(node: SyntaxNode): boolean {
function hasUnrestrictedPub(node: SyntaxNode): boolean {
for (let i = 0; i < node.childCount; i++) {
if (node.child(i)?.type === 'visibility_modifier') return true;
const child = node.child(i);
// `pub(crate)` / `pub(super)` / `pub(in …)` / `crate` are not the crate's
// public surface, so they must not walk as `pub use` re-export evidence.
if (child?.type === 'visibility_modifier' && child.text === 'pub') return true;
}
return false;
}
@ -146,7 +149,9 @@ function buildScopedPath(node: SyntaxNode): string {
if (node.type === 'scoped_identifier') {
const parts: string[] = [];
collectScopedParts(node, parts);
return parts.join('::');
// An absolute extern-prelude path bypasses a same-named local module.
// Keep that evidence when flattening the AST into an import specifier.
return `${node.text.startsWith('::') ? '::' : ''}${parts.join('::')}`;
}
return node.text;
}
@ -174,7 +179,7 @@ function buildWildcardPath(node: SyntaxNode): string {
const child = node.child(i);
if (child === null) continue;
if (child.type === 'scoped_identifier') return buildScopedPath(child);
if (child.type === 'identifier') return child.text;
if (['identifier', 'crate', 'self', 'super'].includes(child.type)) return child.text;
}
return '';
}

View file

@ -20,6 +20,13 @@
*/
import type { ParsedFile, Scope, ScopeId, SymbolDefinition } from 'gitnexus-shared';
import {
rustFilesShareCargoTarget,
rustImportNamesCargoRoot,
rustIsExclusiveCargoRoot,
rustImportReachesCargoTarget,
rustCargoPubliclyReexports,
} from './cargo-targets.js';
import {
modulePathReaches,
stripExtension,
@ -34,6 +41,16 @@ const RUST_CRATE_ROOT_DIRS: ReadonlySet<string> = new Set(['src', 'tests', 'benc
/** Path prefixes of a `use` that name a root rather than a module segment. */
const RUST_USE_ROOT_PREFIXES: ReadonlySet<string> = new Set(['crate', '$crate']);
const RUST_TYPE_NAMESPACE_KINDS: ReadonlySet<string> = new Set([
'Namespace',
'Class',
'Struct',
'Enum',
'Trait',
'Interface',
'TypeAlias',
'Union',
]);
// One scope lookup per immutable parsed-file snapshot, not per fallback site.
// Weak keys release both the snapshot and its index at the end of ingestion.
@ -92,6 +109,8 @@ function rustUsePathOf(targetRaw: string, callerFilePath: string): string {
export function rustIsGlobalNameFallbackPlausible(ctx: {
readonly callerParsed: ParsedFile;
readonly candidate: SymbolDefinition;
readonly resolutionConfig?: unknown;
readonly parsedFileOf?: (filePath: string) => ParsedFile | undefined;
readonly site: {
readonly name: string;
readonly rawQualifiedName?: string;
@ -106,12 +125,79 @@ export function rustIsGlobalNameFallbackPlausible(ctx: {
if (ctx.site.rawQualifiedName !== undefined) return true;
const candidateModule = rustModulePathOf(ctx.candidate.filePath);
// A candidate whose file maps to no module path (a crate root reduced to '')
// is not something this rule can speak about; allow the labeled edge rather
// than refuse on an unanswered question.
if (candidateModule === '') return true;
const sharesTarget = rustFilesShareCargoTarget(
ctx.resolutionConfig,
ctx.callerParsed.filePath,
ctx.candidate.filePath,
);
const separateRoot =
sharesTarget === false &&
rustIsExclusiveCargoRoot(ctx.resolutionConfig, ctx.candidate.filePath);
// Cargo membership does not establish cross-file lexical visibility.
// Root candidates must also pass the import checks below; same-file and
// explicitly qualified calls have already been handled above.
const candidateName = rustSimpleNameOf(ctx.candidate);
const exportModules = new Set([(ctx.candidate.namespacePrefix ?? '').replaceAll('.', '::')]);
const candidateParsed =
sharesTarget === false ? ctx.parsedFileOf?.(ctx.candidate.filePath) : undefined;
if (candidateParsed !== undefined) {
const moduleByScope = new Map<ScopeId, string>();
const moduleScopes = new Set<ScopeId>();
for (const scope of candidateParsed.scopes) {
const parent = scope.parent === null ? '' : (moduleByScope.get(scope.parent) ?? '');
const own =
scope.kind === 'Namespace'
? scope.ownedDefs.find((def) => def.type === 'Namespace')?.qualifiedName
: undefined;
moduleByScope.set(scope.id, [parent, own].filter(Boolean).join('::'));
if (scope.kind === 'Namespace' || scope.kind === 'Module') moduleScopes.add(scope.id);
}
// Follow same-file re-exports without confusing the defining module with
// the module an importer sees. Each iteration adds a known module scope,
// so cycles terminate. Cargo's AST snapshot supplies public visibility,
// which the coarse parsed reexport/wildcard kind does not preserve.
let changed = true;
while (changed) {
changed = false;
for (const imp of candidateParsed.parsedImports) {
if (imp.declaredAtScope === undefined || !moduleScopes.has(imp.declaredAtScope)) continue;
if (
imp.kind !== 'wildcard' &&
(imp.kind !== 'reexport' ||
imp.localName !== candidateName ||
imp.importedName !== candidateName)
)
continue;
if (imp.targetRaw.startsWith('::')) continue;
const owner = moduleByScope.get(imp.declaredAtScope)!;
if (
!rustCargoPubliclyReexports(
ctx.resolutionConfig,
ctx.candidate.filePath,
owner,
imp.targetRaw,
imp.kind,
imp.kind === 'wildcard' ? '*' : imp.localName,
)
)
continue;
const parts = imp.targetRaw.split('::').filter(Boolean);
if (imp.kind !== 'wildcard') parts.pop();
const base =
parts[0] === 'self' || parts[0] === 'super' ? owner.split('::').filter(Boolean) : [];
if (parts[0] === 'crate' || parts[0] === 'self') parts.shift();
while (parts[0] === 'super') {
base.pop();
parts.shift();
}
if (exportModules.has([...base, ...parts].join('::')) && !exportModules.has(owner)) {
exportModules.add(owner);
changed = true;
}
}
}
}
// Imports are lexical evidence, not a file-wide allowlist. Legacy/synthetic
// imports without a scope receipt retain the previous conservative behavior.
let visibleScopes: Set<ScopeId> | undefined;
@ -130,25 +216,120 @@ export function rustIsGlobalNameFallbackPlausible(ctx: {
current = scope.parent;
}
}
for (const imp of ctx.callerParsed.parsedImports) {
if (
imp.declaredAtScope !== undefined &&
visibleScopes !== undefined &&
!visibleScopes.has(imp.declaredAtScope)
)
continue;
const visibleImports = ctx.callerParsed.parsedImports.filter(
(imp) =>
imp.declaredAtScope === undefined ||
visibleScopes === undefined ||
visibleScopes.has(imp.declaredAtScope),
);
const scopeRanks = new Map([...(visibleScopes ?? [])].map((scope, rank) => [scope, rank]));
const namesCandidateRoot = (module: string, entryOnly: boolean): 'root' | 'module' | false => {
const pending = [module];
const seen = new Set<string>();
while (pending.length > 0) {
const name = pending.pop()!;
const parts = name.split('::').filter(Boolean);
const head = parts[0];
const bindingName = name.startsWith('::') ? name : head;
if (!bindingName || seen.has(bindingName)) continue;
seen.add(bindingName);
const aliases = visibleImports.filter(
(imported) => 'localName' in imported && imported.localName === bindingName,
);
const rank = (imported: (typeof visibleImports)[number]) =>
imported.declaredAtScope === undefined
? Infinity
: (scopeRanks.get(imported.declaredAtScope) ?? Infinity);
const nearest = aliases.length > 0 ? Math.min(...aliases.map(rank)) : Infinity;
let localTypeRank = Infinity;
for (const [scopeId, depth] of scopeRanks) {
const bindings = scopeLookupByFile
.get(ctx.callerParsed)
?.get(scopeId)
?.bindings.get(bindingName);
if (
bindings?.some(
(binding) =>
binding.origin === 'local' && RUST_TYPE_NAMESPACE_KINDS.has(binding.def.type),
)
) {
localTypeRank = depth;
break;
}
}
// A local module/type shadows the extern prelude, but a nearer import
// can shadow that declaration. Value-namespace functions do not block it.
if (localTypeRank !== Infinity && localTypeRank <= nearest) continue;
if (aliases.length > 0) {
const destinations = new Set(
aliases
.filter((imported) => rank(imported) === nearest)
.map((imported) => imported.targetRaw),
);
if (destinations.size !== 1) continue;
const destination = [...destinations][0]!;
if (destination !== bindingName) {
pending.push([destination, ...parts.slice(1)].join('::'));
continue;
}
}
// A root FILE can also contain inline modules. Its Cargo identity names
// the crate, while the scope model supplies the member's module suffix.
if (
rustImportNamesCargoRoot(
ctx.resolutionConfig,
ctx.callerParsed.filePath,
ctx.candidate.filePath,
head!,
)
) {
if (exportModules.has(parts.slice(1).join('::'))) return 'root';
continue;
}
if (
!entryOnly &&
rustImportReachesCargoTarget(
ctx.resolutionConfig,
ctx.callerParsed.filePath,
ctx.candidate.filePath,
name,
)
)
return 'module';
}
return false;
};
for (const imp of visibleImports) {
let importedRoot = false;
// `crate::` is the caller's crate. A same trailing module in another
// workspace crate is a different item and cannot authorize the guess.
if (imp.targetRaw === 'crate' || imp.targetRaw.startsWith('crate::')) {
if (sharesTarget === false) continue;
const callerRoot = rustCrateRootOf(ctx.callerParsed.filePath);
const candidateRoot = rustCrateRootOf(ctx.candidate.filePath);
if (callerRoot !== '' && candidateRoot !== '' && callerRoot !== candidateRoot) continue;
if (
sharesTarget === undefined &&
callerRoot !== '' &&
candidateRoot !== '' &&
callerRoot !== candidateRoot
)
continue;
}
if (sharesTarget === false) {
const module =
imp.kind === 'wildcard'
? imp.targetRaw
: imp.targetRaw.slice(0, Math.max(0, imp.targetRaw.lastIndexOf('::')));
const reached = namesCandidateRoot(module, separateRoot);
if (!reached) continue;
importedRoot = reached === 'root';
}
const usePath = rustUsePathOf(imp.targetRaw, ctx.callerParsed.filePath);
// Only a glob introduces every bare item of a module. A named import must
// match both the candidate's original name and the call's local spelling.
if (imp.kind === 'wildcard') {
if (modulePathReaches(usePath, candidateModule)) return true;
if (importedRoot || candidateModule === '' || modulePathReaches(usePath, candidateModule))
return true;
continue;
}
if (!('localName' in imp) || imp.localName !== ctx.site.name) continue;
@ -158,8 +339,12 @@ export function rustIsGlobalNameFallbackPlausible(ctx: {
// parent-path match used to accept every item of `a` on its strength.
// An alias authorizes only the local spelling checked above.
if (importedNameOf(imp) !== candidateName) continue;
if (modulePathReaches(usePath, candidateModule)) return true;
// A different target may import the library crate's root exports. Even
// when that root has no path segment to compare, a named import must name
// THIS callable: `use std::fmt` cannot revive a rejected `crate::helper`.
const parent = usePath.slice(0, Math.max(0, usePath.lastIndexOf('::')));
if (importedRoot || candidateModule === '') return true;
if (modulePathReaches(usePath, candidateModule)) return true;
if (parent !== '' && modulePathReaches(parent, candidateModule)) return true;
}
return false;

View file

@ -20,6 +20,7 @@ import type { HeritageTypeArgumentSink } from '../../scope-resolution/utils/gene
import type { KnowledgeGraph } from '../../../graph/types.js';
import { generateId } from '../../../../lib/utils.js';
import { rustIsGlobalNameFallbackPlausible } from './name-fallback-visibility.js';
import { loadRustCargoTargets } from './cargo-targets.js';
/**
* Emit Rust `S IMPLEMENTS T` edges from `impl T for S` trait implementations.
@ -157,6 +158,7 @@ export const rustScopeResolver: ScopeResolver = {
language: SupportedLanguages.Rust,
languageProvider: rustProvider,
importEdgeReason: 'rust-scope: use',
loadResolutionConfig: loadRustCargoTargets,
resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) =>
resolveRustImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig),

View file

@ -920,6 +920,8 @@ export interface ScopeResolver {
readonly isGlobalNameFallbackPlausible?: (ctx: {
readonly callerParsed: ParsedFile;
readonly candidate: SymbolDefinition;
/** Opaque workspace metadata from this provider's loadResolutionConfig. */
readonly resolutionConfig?: unknown;
readonly parsedFileOf: (filePath: string) => ParsedFile | undefined;
/**
* Raw source of any parsed file, for languages whose visibility rule needs

View file

@ -74,6 +74,7 @@ export function emitFreeCallFallback(
/** Per-language veto on a name guess — see
* `ScopeResolver.isGlobalNameFallbackPlausible`. */
readonly isGlobalNameFallbackPlausible?: ScopeResolver['isGlobalNameFallbackPlausible'];
readonly resolutionConfig?: unknown;
/** Raw source lookup handed to `isGlobalNameFallbackPlausible` (optional). */
readonly sourceTextOf?: (filePath: string) => string | undefined;
/** When true, `Type(...)` constructor calls link to the Class def
@ -650,6 +651,7 @@ export function emitFreeCallFallback(
options.isGlobalNameFallbackPlausible?.({
callerParsed: parsed,
candidate: vetoCandidate,
resolutionConfig: options.resolutionConfig,
parsedFileOf: parsedFileByPath(),
sourceTextOf: options.sourceTextOf,
site: {

View file

@ -1102,6 +1102,7 @@ export function runScopeResolution(
allowGlobalFallback: provider.allowGlobalFreeCallFallback === true,
language: provider.language,
isGlobalNameFallbackPlausible: provider.isGlobalNameFallbackPlausible,
resolutionConfig,
sourceTextOf:
provider.isGlobalNameFallbackPlausible !== undefined
? (filePath: string) => getFileContents().get(filePath)

View file

@ -763,7 +763,13 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
// #3190. Old durable ParsedFiles lack the facts needed for scoped binding;
// invalidate both stores so warm indexing actually applies the correction.
// origin/main took 98 for #3219; 99 is the next free value.
const SCHEMA_BUMP = 99;
// v100 (#3253): Rust import captures preserve the leading `::` that selects
// the extern prelude. Old warm captures erase it and cannot distinguish an
// absolute library import from a same-named local module. Reparse both stores.
// v101 (#3294 review): Rust bare-keyword glob imports retain crate/self/super
// instead of an empty target path; restricted pub(...) imports are no longer
// captured as unrestricted reexports. Re-extract both facts on warm indexes.
const SCHEMA_BUMP = 101;
const GITNEXUS_PKG_VERSION = (() => {
try {
// package.json sits at gitnexus/package.json — two levels up from

View file

@ -0,0 +1,182 @@
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { getRelationships, runPipelineFromRepo, writeFixtureRepo } from './helpers.js';
import {
loadRustCargoTargets,
rustFilesShareCargoTarget,
} from '../../../src/core/ingestion/languages/rust/cargo-targets.js';
const PACKAGE = '[package]\nname="demo"\nedition="2021"\n';
async function check(files: Record<string, string>, expectedCalls: number, complete = true) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-cargo-review-'));
try {
writeFixtureRepo(dir, { 'Cargo.toml': PACKAGE, ...files });
const config = await loadRustCargoTargets(dir);
if (complete) expect(config).toBeDefined();
else expect(config).toBeUndefined();
const result = await runPipelineFromRepo(dir, () => {});
const candidates = [...result.graph.iterNodes()].filter(
(node) => node.properties.name === 'helper',
);
expect(candidates.length).toBeGreaterThan(0);
const calls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
);
expect(calls).toHaveLength(expectedCalls);
return { config, calls };
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
}
describe('Cargo review regressions (#3294)', () => {
it.each(['', 'use crate::helper;', 'use super::helper;', 'use crate::*;', 'use super::*;'])(
'requires lexical import evidence within the same Cargo target: %s',
async (imported) => {
await check(
{
'src/lib.rs': 'fn helper() {} mod child;',
'src/child.rs': `${imported} pub fn caller() { helper(); }`,
},
imported === '' ? 0 : 1,
);
},
);
it.each([
'#[derive(Debug, Clone)] pub struct T;',
'fn noisy() { println!("x"); assert_eq!(1, 1); let _v = vec![1, 2]; }',
'fn noisy() { std::println!("x"); core::assert_eq!(1, 1); }',
'#[derive(Debug)] struct T; #[cfg(test)] mod tests { use super::*; #[test] fn f() { assert_eq!(1,1); } }',
'use std::fmt::*; fn noisy() { println!("x"); }',
'#[test] #[should_panic] #[ignore] fn expected_panic() { panic!("expected"); }',
])('retains target separation with ordinary Rust: %s', async (source) => {
await check(
{
'src/lib.rs': `${source} use crate::helper; pub fn caller() { helper(); }`,
'tests/helper.rs': 'pub fn helper() {}',
},
0,
);
});
it('ordinary macros in a sibling target do not erase the proof', async () => {
await check(
{
'src/lib.rs': 'use crate::helper; pub fn caller() { helper(); }',
'tests/helper.rs': 'pub fn helper() {}',
'tests/other.rs': '#[test] fn ordinary() { assert_eq!(1, 1); }',
},
0,
);
});
it('restores a real module directory named target', async () => {
const { config } = await check(
{
'src/lib.rs': 'mod target; use crate::helper; pub fn caller() { helper(); }',
'src/target/mod.rs': 'pub fn other() {}',
'tests/helper.rs': 'pub fn helper() {}',
},
0,
);
expect(rustFilesShareCargoTarget(config, 'src/lib.rs', 'src/target/mod.rs')).toBe(true);
});
it('does not import a build-script root into the library', async () => {
await check(
{
'src/lib.rs': 'use std::build::*; pub fn caller() { helper(); }',
'build.rs': 'pub fn helper() {} fn main() {}',
},
0,
);
});
it('restores explicit roots under artifact-pruned directories', async () => {
await check(
{
'Cargo.toml': `${PACKAGE}[[bin]]\nname="custom"\npath="target/entry.rs"\n`,
'target/entry.rs': 'fn main() {}',
'src/lib.rs': 'use crate::helper; pub fn caller() { helper(); }',
'tests/helper.rs': 'pub fn helper() {}',
},
0,
);
});
it('keeps a custom library root import when that file is shared with a binary', async () => {
await check(
{
'Cargo.toml': `${PACKAGE}[lib]\npath="custom/entry.rs"\n`,
'custom/entry.rs': 'pub fn helper() {}',
'src/main.rs': '#[path="../custom/entry.rs"] mod shared; fn main() {}',
'tests/caller.rs': 'use demo::*; pub fn caller() { helper(); }',
},
1,
);
});
it.each(['', 'pub(crate) ', 'pub(super) ', 'pub(in crate) ', 'pub '])(
'honors re-export visibility for %suse',
async (visibility) => {
for (const imported of ['helper', '*']) {
await check(
{
'src/lib.rs': `#[derive(Debug)] struct T; mod nested { pub fn helper() {} } ${visibility}use nested::${imported};`,
'tests/caller.rs': 'use demo::*; pub fn caller() { helper(); }',
},
visibility === 'pub ' ? 1 : 0,
);
}
},
);
it.each([
['pub fn helper() {}', 'demo', 1],
['pub fn helper() {}', 'demo::nested', 0],
['pub mod nested { pub fn helper() {} }', 'demo', 0],
['pub mod nested { pub fn helper() {} }', 'demo::nested', 1],
] as const)(
'selects the imported root role even for a shared root: %s / %s',
async (source, imported, count) => {
await check(
{
'src/lib.rs': source,
'src/main.rs': '#[path="lib.rs"] mod shared; fn main() {}',
'tests/caller.rs': `use ${imported}::*; pub fn caller() { helper(); }`,
},
count,
);
},
);
it('keeps uncertainty for an unknown macro that can share both files', async () => {
const { calls } = await check(
{
'src/lib.rs': 'use crate::helper; pub fn caller() { helper(); }',
'tests/helper.rs': 'pub fn helper() {}',
'src/main.rs':
'macro_rules! share { () => { include!("../tests/helper.rs"); #[path="lib.rs"] mod library; } } share!(); fn main() {}',
},
1,
false,
);
expect(calls[0]?.rel).toMatchObject({ reason: 'global-name-fallback', confidence: 0.5 });
});
it('preserves a labeled guess for an unmodeled extern-crate alias, not an alias-resolution claim', async () => {
const { calls } = await check(
{
'src/lib.rs': 'pub fn helper() {}',
'tests/caller.rs': 'extern crate demo as api; use api::*; pub fn caller() { helper(); }',
},
1,
false,
);
expect(calls[0]?.targetFilePath).toBe('src/lib.rs');
expect(calls[0]?.rel).toMatchObject({ reason: 'global-name-fallback', confidence: 0.5 });
});
});

View file

@ -0,0 +1,389 @@
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { getRelationships, runPipelineFromRepo, writeFixtureRepo } from './helpers.js';
import { loadRustCargoTargets } from '../../../src/core/ingestion/languages/rust/cargo-targets.js';
describe('Rust Cargo target boundaries in name fallback (#3253)', () => {
it.each([
['use demo::*;', false],
['use demo::helper;', false],
['use demo::nested::*;', true],
['use demo::nested::helper;', true],
['use demo as api; use api::nested::*;', true],
['use ::demo as demo; use demo::nested::*;', true],
['use b as a; use a as b; use a::*;', false],
] as const)('matches inline modules within a Cargo root: %s', async (source, allowed) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-inline-root-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="demo"\nedition="2021"\n',
'src/lib.rs': 'pub mod nested { pub fn helper() {} }',
'tests/caller.rs': `${source} pub fn caller() { helper(); }`,
});
const result = await runPipelineFromRepo(dir, () => {});
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
),
).toHaveLength(allowed ? 1 : 0);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it.each([
['pub mod nested { pub fn helper() {} } pub use nested::helper;', 'demo'],
['pub mod nested { pub fn helper() {} } pub use nested::*;', 'demo'],
['pub fn helper() {} pub mod nested { pub use super::helper; }', 'demo::nested'],
[
'pub mod a { pub fn helper() {} } pub mod b { pub use crate::a::helper; } pub use b::helper;',
'demo',
],
])('preserves same-file re-export evidence: %s', async (library, imported) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-reexport-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="demo"\nedition="2021"\n',
'src/lib.rs': library,
'tests/caller.rs': `use ${imported}::*; pub fn caller() { helper(); }`,
});
const result = await runPipelineFromRepo(dir, () => {});
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
),
).toHaveLength(1);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it.each(['tests/fmt.rs', 'benches/fmt.rs', 'examples/fmt.rs'])(
'uses Cargo root identity for the nonempty file stem %s',
async (target) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-root-stem-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="demo"\nversion="0.1.0"\nedition="2021"\n',
'src/lib.rs': 'use std::fmt::*; pub fn caller() { helper(); }',
[target]: 'pub fn helper() {}',
});
const result = await runPipelineFromRepo(dir, () => {});
expect(result.graph.getNode(`Function:${target}:helper`)).toBeDefined();
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
),
).toEqual([]);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
},
);
it('recognizes a custom library entry file as the imported crate root', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-custom-root-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml':
'[package]\nname="demo"\nversion="0.1.0"\nedition="2021"\n[lib]\npath="library/entry.rs"\n',
'library/entry.rs': 'pub fn helper() {}',
'tests/caller.rs': 'use demo::*; pub fn caller() { helper(); }',
});
const result = await runPipelineFromRepo(dir, () => {});
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
),
).toHaveLength(1);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it.each([
['use target_boundary::shared::*;', true],
['use target_boundary as api; use api::shared::*;', true],
['use std::shared::*;', false],
] as const)(
'keeps crate identity for a file shared as both module and target: %s',
async (source, allowed) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-shared-root-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="target-boundary"\nversion="0.1.0"\nedition="2021"\n',
'src/lib.rs': '#[path="../tests/shared.rs"] pub mod shared;',
'tests/shared.rs': 'pub fn helper() {}',
'examples/caller.rs': `${source} pub fn caller() { helper(); }`,
});
const result = await runPipelineFromRepo(dir, () => {});
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
),
).toHaveLength(allowed ? 1 : 0);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
},
);
it.each(['use std::fmt::*;', 'use target_boundary::nested::*;', 'use std::helper;'])(
'an unrelated import cannot reach a binary-root helper: %s',
async (source) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-unrelated-glob-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="target-boundary"\nversion="0.1.0"\nedition="2021"\n',
'src/lib.rs': `${source} pub fn caller() { helper(); }`,
'src/main.rs': 'pub fn helper() {}',
});
const result = await runPipelineFromRepo(dir, () => {});
expect(result.graph.getNode('Function:src/main.rs:helper')).toBeDefined();
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
),
).toEqual([]);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
},
);
it('an unrelated import cannot revive a rejected crate-root candidate', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-unrelated-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="target-boundary"\nversion="0.1.0"\nedition="2021"\n',
'src/lib.rs': 'use crate::helper; use std::fmt; pub fn caller() { helper(); }',
'src/main.rs': 'pub fn helper() {}',
});
const result = await runPipelineFromRepo(dir, () => {});
expect(result.graph.getNode('Function:src/main.rs:helper')).toBeDefined();
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
),
).toEqual([]);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it.each([
'use target_boundary::helper;',
'use target_boundary::*;',
'use target_boundary as api; use api::*;',
])('preserves an explicit library import from an integration target: %s', async (source) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-library-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="target-boundary"\nversion="0.1.0"\nedition="2021"\n',
'src/lib.rs': 'pub fn helper() {}',
'tests/caller.rs': `${source} pub fn caller() { helper(); }`,
});
expect(await loadRustCargoTargets(dir)).toBeDefined();
const result = await runPipelineFromRepo(dir, () => {});
const calls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
);
expect(calls).toHaveLength(1);
expect(calls[0]!.targetFilePath).toBe('src/lib.rs');
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it.each([
['mod target_boundary {} use target_boundary::*;', '', false],
['mod target_boundary {} use ::target_boundary::*;', '', true],
['fn target_boundary() {} use target_boundary::*;', '', true],
['mod api {} fn allowed() { use target_boundary as api; use api::*; helper(); }', '', true],
['use std::*;', '', false],
['use target_boundary::nested::*;', '', false],
['use public_api::*;', '[lib]\nname="public_api"\n', true],
['use target_boundary::*;', '[lib]\nname="public_api"\n', false],
[
'use target_boundary as api; fn denied() { use std::fmt as api; use api::*; helper(); }',
'',
false,
],
] as const)('requires the actual library root for %s', async (source, lib, allowed) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-root-name-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': `[package]\nname="target-boundary"\nversion="0.1.0"\nedition="2021"\n${lib}`,
'src/lib.rs': 'pub fn helper() {}',
'tests/caller.rs': `${source} pub fn caller() { helper(); }`,
});
const result = await runPipelineFromRepo(dir, () => {});
const calls = getRelationships(result, 'CALLS').filter((edge) => edge.target === 'helper');
expect(calls).toHaveLength(allowed ? 1 : 0);
expect(calls.map((edge) => edge.source)).toEqual(
allowed ? [source.includes('fn allowed()') ? 'allowed' : 'caller'] : [],
);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it.each([false, true])(
'recognizes a renamed path dependency (workspace inherited: %s)',
async (inherited) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-dep-alias-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml':
'[package]\nname="root-lib"\nversion="0.1.0"\nedition="2021"\n[workspace]\nmembers=["consumer"]\n' +
(inherited ? '[workspace.dependencies]\nrenamed={package="root-lib",path="."}\n' : ''),
'src/lib.rs': 'pub fn helper() {}',
'consumer/Cargo.toml':
'[package]\nname="consumer"\nversion="0.1.0"\nedition="2021"\n[dependencies]\n' +
(inherited ? 'renamed={workspace=true}\n' : 'renamed={package="root-lib",path=".."}\n'),
'consumer/src/lib.rs': 'use renamed::*; pub fn caller() { helper(); }',
});
const result = await runPipelineFromRepo(dir, () => {});
const calls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
);
expect(calls).toHaveLength(1);
expect(calls[0]!.targetFilePath).toBe('src/lib.rs');
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
},
);
it.each([
'tests/helper.rs',
'benches/helper.rs',
'examples/helper.rs',
'src/bin/helper.rs',
'src/main.rs',
])('does not use the separate target %s to satisfy a library crate import', async (target) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-target-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname = "target-boundary"\nversion = "0.1.0"\nedition = "2021"\n',
'.gitnexusignore': '!src/bin/\n',
'src/lib.rs': 'use crate::helper;\npub fn caller() { helper(); }\n',
[target]: 'pub fn helper() {}\n',
});
const result = await runPipelineFromRepo(dir, () => {});
expect(result.graph.getNode(`Function:${target}:helper`)).toBeDefined();
const calls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
);
expect(calls).toEqual([]);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it.each([
['library module', 'src/helper.rs', 'mod helper; use crate::helper::helper;'],
[
'unit-test module',
'src/tests/helper.rs',
'#[cfg(test)] mod tests { pub mod helper; } use crate::tests::helper::helper;',
],
[
'shared integration-test source',
'tests/helper.rs',
'#[path="../tests/helper.rs"] mod shared; use crate::shared::helper;',
],
])('preserves a valid %s call', async (_name, target, source) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-positive-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="positive"\nversion="0.1.0"\nedition="2021"\n',
'src/lib.rs': `${source}\npub fn caller() { helper(); }\n`,
[target]: 'pub fn helper() {}\n',
});
const result = await runPipelineFromRepo(dir, () => {});
const calls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
);
expect(calls).toHaveLength(1);
expect(calls[0]!.targetFilePath).toBe(target);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it.each([
['missing metadata', undefined, ''],
['malformed metadata', '[package', ''],
[
'unmodeled module expansion',
'[package]\nname="unknown"\nversion="0.1.0"\nedition="2021"\n',
'include!("generated.rs");',
],
[
'unmodeled extern-crate alias',
'[package]\nname="unknown"\nversion="0.1.0"\nedition="2021"\n',
'extern crate self as api;',
],
])('preserves a labeled guess with %s', async (_name, manifest, prefix) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-unknown-'));
try {
writeFixtureRepo(dir, {
...(manifest === undefined ? {} : { 'Cargo.toml': manifest }),
'src/lib.rs': `${prefix}\nuse crate::helper; pub fn caller() { helper(); }`,
'tests/helper.rs': 'pub fn helper() {}',
});
expect(await loadRustCargoTargets(dir)).toBeUndefined();
const result = await runPipelineFromRepo(dir, () => {});
const calls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
);
expect(calls).toHaveLength(1);
expect(calls[0]!.rel.reason).toBe('global-name-fallback');
expect(calls[0]!.rel.confidence).toBe(0.5);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it('refuses a crate-root helper from another target on typical derive/assert_eq source', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-derive-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="demo"\nversion="0.1.0"\nedition="2021"\n',
'src/lib.rs':
'#[derive(Debug)] struct S;\npub fn helper() {}\nfn t() { assert_eq!(1, 1); }\n',
'tests/caller.rs': 'pub fn caller() { helper(); }\n',
});
const result = await runPipelineFromRepo(dir, () => {});
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
),
).toEqual([]);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
it('does not treat pub(crate) use as a public re-export for integration targets', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-cargo-pub-crate-'));
try {
writeFixtureRepo(dir, {
'Cargo.toml': '[package]\nname="demo"\nedition="2021"\n',
'src/lib.rs': 'pub mod nested { pub fn helper() {} } pub(crate) use nested::helper;',
'tests/caller.rs': 'use demo::*; pub fn caller() { helper(); }',
});
const result = await runPipelineFromRepo(dir, () => {});
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'caller' && edge.target === 'helper',
),
).toEqual([]);
} finally {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
});

View file

@ -88,3 +88,16 @@ describe('F72 — macro invocations (capture layer)', () => {
expect(macroDecls[0]['@declaration.name'].text).toBe('greet');
});
});
describe('restricted visibility use is not a re-export', () => {
it.each([
['pub use foo::helper;', 'reexport'],
['pub(crate) use foo::helper;', 'named'],
['pub(super) use foo::helper;', 'named'],
['use foo::helper;', 'named'],
])('%s', (src, kind) => {
const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[];
const imps = matches.filter((m) => m['@import.kind']);
expect(imps.map((m) => m['@import.kind']?.text)).toEqual([kind]);
});
});

View file

@ -275,9 +275,12 @@ describe('PARSE_CACHE_VERSION', () => {
// registered accessor goes back to `epistemic: "exact"`: the #3399 defect,
// silently un-fixed on exactly the incremental path most users are on.
// Moved 98 -> 99 for #3190: lexical import provenance and corrected export
// evidence. origin/main took 98 for #3219; 99 is the next free value.
it('pins SCHEMA_BUMP to 99 so concurrent bumps cannot silently collide (#2766, #3015, #3088, #2885, #3128, #2865, #3130, #1432, #3161, #3179, #3219, #3190)', () => {
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(99);
// evidence. origin/main took 98 for #3219 and 99 for #3190.
// Moved 99 -> 100 for #3253: retain absolute Rust import qualifiers.
// Moved 100 -> 101 for #3294 review: retain keyword glob paths and distinguish
// restricted pub(...) imports from unrestricted reexports.
it('pins SCHEMA_BUMP to 101 so concurrent bumps cannot silently collide (#2766, #3015, #3088, #2885, #3128, #2865, #3130, #1432, #3161, #3179, #3219, #3190, #3253)', () => {
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(101);
expect(PARSE_CACHE_BUCKET_COUNT).toBe(128);
// The PREVIOUS version must fail the reuse gate, not merely differ from the
// current one — a hardcoded number outside the conflict hunk rebases cleanly
@ -285,7 +288,7 @@ describe('PARSE_CACHE_VERSION', () => {
// Every nearby historical or in-flight value is rejected.
for (const taken of [
59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100,
]) {
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(taken);
}

View file

@ -3,11 +3,11 @@
* shared path arithmetic they are built on.
*
* These hooks decide whether a UNIQUE-NAME GUESS is allowed to become a labeled
* CALLS edge or must be dropped as impossible. The asymmetry matters for how
* these tests are written: a wrong `false` deletes a real edge, so every case
* that the language cannot decide is asserted to return `true`. "Refuses when
* impossible" and "does not refuse when merely unproven" are therefore BOTH
* requirements, and both are tested per language.
* CALLS edge or must be dropped by a language visibility rule. A wrong `false`
* deletes a real edge, so uncertainty alone must not invent a refusal. It also
* must not bypass an independent rule: Rust cross-file bare calls still need
* a visible use path, whether Cargo membership is known or unknown. Tests pin
* both required refusals and permitted guesses under incomplete evidence.
*
* Pure functions over synthetic stubs — no pipeline, no fixtures.
*/
@ -438,13 +438,23 @@ describe('Rust: isGlobalNameFallbackPlausible', () => {
).toBe(true);
});
it('does not refuse when the candidate file maps to no module path', () => {
it('REFUSES a crate-root candidate without cargo identity or a covering use', () => {
expect(
rustIsGlobalNameFallbackPlausible({
site: BARE_SITE,
callerParsed: mkCaller('src/b.rs'),
candidate: mkCandidate('lib.rs', 'unique_helper_xyz'),
}),
).toBe(false);
});
it('allows a crate-root candidate when a covering use names it', () => {
expect(
rustIsGlobalNameFallbackPlausible({
site: BARE_SITE,
callerParsed: mkCaller('src/b.rs', [{ kind: 'wildcard', targetRaw: 'crate' }]),
candidate: mkCandidate('lib.rs', 'unique_helper_xyz'),
}),
).toBe(true);
});

View file

@ -0,0 +1,636 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
cargoTargetRoots,
loadRustCargoTargets,
rustFilesShareCargoTarget,
rustImportNamesCargoRoot,
} from '../../../src/core/ingestion/languages/rust/cargo-targets.js';
import { emitRustScopeCaptures } from '../../../src/core/ingestion/languages/rust/captures.js';
import { interpretRustImport } from '../../../src/core/ingestion/languages/rust/interpret.js';
const PACKAGE = '[package]\nname = "demo"\nversion = "0.1.0"\nedition = "2021"\n';
const temporary: string[] = [];
afterEach(() => {
vi.restoreAllMocks();
for (const dir of temporary.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
});
function fixture(files: Record<string, string>): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-cargo-targets-'));
temporary.push(dir);
for (const [file, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(dir, file)), { recursive: true });
fs.writeFileSync(path.join(dir, file), content);
}
return dir;
}
describe('Cargo manifest target metadata', () => {
it.each(['crate', 'self', 'super'])('preserves the %s keyword in a glob import', (keyword) => {
const imports = emitRustScopeCaptures(`use ${keyword}::*;`, 'fixture.rs')
.map(interpretRustImport)
.filter((entry) => entry !== null);
expect(imports).toEqual([{ kind: 'wildcard', targetRaw: keyword }]);
});
const files = new Set([
'src/lib.rs',
'src/main.rs',
'src/bin/tool.rs',
'src/bin/other/main.rs',
'tests/helper.rs',
'benches/speed.rs',
'examples/demo/main.rs',
'custom/entry.rs',
'build.rs',
]);
it('discovers lib, main, binary, test, bench, example and build-script roots', () => {
expect(new Set(cargoTargetRoots('Cargo.toml', PACKAGE, files))).toEqual(
new Set([...files].filter((file) => file !== 'custom/entry.rs')),
);
});
it('accepts Cargo build=true and declines overlapping build and library roles', () => {
expect(cargoTargetRoots('Cargo.toml', `${PACKAGE}build=true\n`, files)).toContain('build.rs');
expect(
cargoTargetRoots('Cargo.toml', `${PACKAGE}[lib]\npath="build.rs"\n`, files),
).toBeUndefined();
});
it.each([
['autolib', 'src/lib.rs'],
['autobins', 'src/bin/tool.rs'],
['autotests', 'tests/helper.rs'],
['autobenches', 'benches/speed.rs'],
['autoexamples', 'examples/demo/main.rs'],
])('honors %s = false', (key, absent) => {
const roots = cargoTargetRoots('Cargo.toml', `${PACKAGE}${key} = false\n`, files);
expect(roots).toBeDefined();
expect(roots).not.toContain(absent);
expect(roots).toContain(key === 'autolib' ? 'src/main.rs' : 'src/lib.rs');
});
it('uses an explicit build-script path instead of the default', () => {
const roots = cargoTargetRoots('Cargo.toml', `${PACKAGE}build="custom/entry.rs"\n`, files);
expect(roots).toContain('custom/entry.rs');
expect(roots).toContain('src/lib.rs');
expect(roots).not.toContain('build.rs');
});
it('explicit paths override auto-discovered targets of the same name', () => {
const roots = cargoTargetRoots(
'Cargo.toml',
`${PACKAGE}\n[[test]]\nname = 'helper'\npath = 'custom/entry.rs'\n`,
files,
);
expect(roots).toContain('custom/entry.rs');
expect(roots).not.toContain('tests/helper.rs');
});
it('explicit lib paths work with autolib disabled', () => {
const roots = cargoTargetRoots(
'Cargo.toml',
`${PACKAGE}autolib = false\n[lib]\npath = 'custom/entry.rs'\n`,
files,
);
expect(roots).toContain('custom/entry.rs');
expect(roots).not.toContain('src/lib.rs');
});
it.each([
['bin', 'tool', ['src/main.rs', 'src/bin/other/main.rs']],
['test', 'helper', ['tests/extra.rs']],
['bench', 'speed', ['benches/extra.rs']],
['example', 'demo', ['examples/extra.rs']],
] as const)(
'Cargo 2015 explicit %s targets only disable discovery of that kind',
(kind, name, excluded) => {
const discovered = new Set([
...files,
'tests/extra.rs',
'benches/extra.rs',
'examples/extra.rs',
]);
expect(
new Set(
cargoTargetRoots(
'Cargo.toml',
`[package]\nname="demo"\nbuild=false\n[[${kind}]]\nname="${name}"\n`,
discovered,
),
),
).toEqual(
new Set(
[...discovered].filter(
(file) =>
file !== 'build.rs' &&
file !== 'custom/entry.rs' &&
!(excluded as readonly string[]).includes(file),
),
),
);
},
);
it('retains workspace/package prefixes', () => {
expect(
cargoTargetRoots('crates/a/Cargo.toml', PACKAGE, new Set(['crates/a/src/lib.rs'])),
).toEqual(['crates/a/src/lib.rs']);
expect(cargoTargetRoots('Cargo.toml', '[workspace]\nmembers=["crates/a"]\n', files)).toEqual(
[],
);
});
it.each([
'[package',
`${PACKAGE}\n[[test]]\npath="missing.rs"\n`,
`${PACKAGE}autotests="false"\n`,
`${PACKAGE}build=1\n`,
`${PACKAGE}[[bin]]\npath="custom/entry.rs"\n`,
`${PACKAGE}[[test]]\npath="custom/entry.rs"\n`,
])('does not manufacture evidence from malformed metadata', (manifest) => {
expect(cargoTargetRoots('Cargo.toml', manifest, files)).toBeUndefined();
});
});
describe('Rust module membership', () => {
it.each([
'.env',
'.git/hidden.rs',
'.GiT/hidden.rs',
'.gitnexus/hidden.rs',
'node_modules/pkg/hidden.rs',
])('does not restore excluded non-source/control paths: %s', async (hidden) => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': `#[path="../${hidden}"] mod hidden;`,
[hidden]: 'pub fn helper() {}',
});
const realpath = vi.spyOn(fs.promises, 'realpath');
expect(await loadRustCargoTargets(dir)).toBeUndefined();
expect(realpath.mock.calls.some(([file]) => String(file) === path.join(dir, hidden))).toBe(
false,
);
});
it('does not interpret a Cargo std alias as the standard library', async () => {
const dir = fixture({
'Cargo.toml': `${PACKAGE}[dependencies]\nstd={package="custom",version="1"}\n`,
'src/lib.rs': 'use std::*; fn f(){ println!(); }',
});
expect(await loadRustCargoTargets(dir)).toBeUndefined();
});
it('retains path dependencies from conditional target sections', async () => {
const dir = fixture({
'Cargo.toml': `${PACKAGE}[target.'cfg(unix)'.dependencies]\nother={path="other"}\n`,
'src/lib.rs': '',
'other/Cargo.toml': '[package]\nname="other"\nedition="2021"\n',
'other/src/lib.rs': '',
});
const config = await loadRustCargoTargets(dir);
expect(config).toBeDefined();
expect(rustImportNamesCargoRoot(config, 'src/lib.rs', 'other/src/lib.rs', 'other')).toBe(true);
});
it.each(['../..', '../../'])(
'resolves a directory-form workspace pointer: %s',
async (workspace) => {
const dir = fixture({
'Cargo.toml':
'[workspace]\nmembers=["crates/a", "crates/b"]\n[workspace.package]\nedition="2021"\n[workspace.dependencies]\nb={path="crates/b"}\n',
'crates/a/Cargo.toml': `[package]\nname="a"\nworkspace="${workspace}"\nedition.workspace=true\n[dependencies]\nb.workspace=true\n`,
'crates/a/src/lib.rs': '',
'crates/a/tests/helper.rs': '',
'crates/b/Cargo.toml': '[package]\nname="b"\nedition="2021"\n',
'crates/b/src/lib.rs': '',
});
const config = await loadRustCargoTargets(dir);
expect(config).toBeDefined();
expect(
rustFilesShareCargoTarget(config, 'crates/a/src/lib.rs', 'crates/a/tests/helper.rs'),
).toBe(false);
expect(
rustImportNamesCargoRoot(config, 'crates/a/src/lib.rs', 'crates/b/src/lib.rs', 'b'),
).toBe(true);
},
);
it('rejects a manifest filename as workspace pointer, as Cargo does', async () => {
const dir = fixture({
'Cargo.toml': '[workspace]\nmembers=["crates/a"]\n[workspace.package]\nedition="2021"\n',
'crates/a/Cargo.toml':
'[package]\nname="a"\nworkspace="../../Cargo.toml"\nedition.workspace=true\n',
'crates/a/src/lib.rs': '',
});
expect(await loadRustCargoTargets(dir)).toBeUndefined();
});
it.each([
'#[derive(Custom)] struct T;',
'use custom::Debug; #[derive(Debug)] struct T;',
'macro_rules! println { () => { #[path="../tests/helper.rs"] mod shared; } } fn f() { println!(); }',
'use custom::println; fn f() { println!(); }',
'use custom::*; fn f() { println!(); }',
'fn f() { println!("{}", { #[path="../tests/helper.rs"] mod shared; 1 }); }',
'fn f() { println!("{}", include!("generated.rs")); }',
'#[tokio::test] async fn f() {}',
'#[some_macro::cfg] fn f() {}',
'#[some_macro::allow] fn f() {}',
'fn f() { custom::println!(); }',
'println!("item position");',
])('does not mistake unknown or shadowed expansion for a builtin: %s', async (source) => {
const dir = fixture({ 'Cargo.toml': PACKAGE, 'src/lib.rs': source, 'tests/helper.rs': '' });
expect(await loadRustCargoTargets(dir)).toBeUndefined();
});
it('does not assume a child-module macro is std when its parent shadows that name', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': 'macro_rules! println { () => { mod generated; } } mod child;',
'src/child.rs': 'fn f() { println!(); }',
'tests/helper.rs': '',
});
expect(await loadRustCargoTargets(dir)).toBeUndefined();
});
it('keeps build dependencies separate while retaining unit-test dependencies', async () => {
const dir = fixture({
'Cargo.toml': `${PACKAGE}[dependencies]\nnormal={path="normal"}\n[dev-dependencies]\ndev={path="dev"}\n[build-dependencies]\nbuilder={path="builder"}\n`,
'src/lib.rs': '',
'build.rs': 'fn main() {}',
...Object.fromEntries(
['normal', 'dev', 'builder'].flatMap((name) => [
[`${name}/Cargo.toml`, `[package]\nname="${name}"\nedition="2021"\n`],
[`${name}/src/lib.rs`, ''],
]),
),
});
const config = await loadRustCargoTargets(dir);
expect(config).toBeDefined();
for (const name of ['normal', 'dev', 'builder']) {
expect(rustImportNamesCargoRoot(config, 'src/lib.rs', `${name}/src/lib.rs`, name)).toBe(
name !== 'builder',
);
expect(rustImportNamesCargoRoot(config, 'build.rs', `${name}/src/lib.rs`, name)).toBe(
name === 'builder',
);
}
expect(rustImportNamesCargoRoot(config, 'build.rs', 'src/lib.rs', 'demo')).toBe(false);
});
it.each([false, true])(
'uses the correct import name for a custom library (package explicit: %s)',
async (explicit) => {
const dir = fixture({
'Cargo.toml': `${PACKAGE}[lib]\nname="public_api"\n`,
'src/lib.rs': '',
'consumer/Cargo.toml': `[package]\nname="consumer"\nedition="2021"\n[dependencies]\ndemo={${explicit ? 'package="demo",' : ''}path=".."}\n`,
'consumer/src/lib.rs': '',
});
const config = await loadRustCargoTargets(dir);
expect(
rustImportNamesCargoRoot(
config,
'consumer/src/lib.rs',
'src/lib.rs',
explicit ? 'demo' : 'public_api',
),
).toBe(true);
expect(
rustImportNamesCargoRoot(
config,
'consumer/src/lib.rs',
'src/lib.rs',
explicit ? 'public_api' : 'demo',
),
).toBe(false);
},
);
it('a dependency alias in another package is not import evidence for this caller', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': '',
'a/Cargo.toml':
'[package]\nname="a"\nedition="2021"\n[dependencies]\napi={package="demo",path=".."}\n',
'a/src/lib.rs': '',
'b/Cargo.toml': '[package]\nname="b"\nedition="2021"\n',
'b/src/lib.rs': '',
});
const config = await loadRustCargoTargets(dir);
expect(rustImportNamesCargoRoot(config, 'a/src/lib.rs', 'src/lib.rs', 'api')).toBe(true);
expect(rustImportNamesCargoRoot(config, 'b/src/lib.rs', 'src/lib.rs', 'api')).toBe(false);
});
it('uses Cargo library metadata rather than the entry file name', async () => {
const dir = fixture({
'Cargo.toml': `${PACKAGE}[lib]\npath="src/main.rs"\nname="api"\n`,
'src/main.rs': '',
'tests/caller.rs': '',
});
expect(
rustImportNamesCargoRoot(
await loadRustCargoTargets(dir),
'tests/caller.rs',
'src/main.rs',
'api',
),
).toBe(true);
fs.writeFileSync(
path.join(dir, 'Cargo.toml'),
`${PACKAGE}autolib=false\n[[bin]]\nname="api"\npath="src/main.rs"\n`,
);
expect(
rustImportNamesCargoRoot(
await loadRustCargoTargets(dir),
'tests/caller.rs',
'src/main.rs',
'api',
),
).toBe(false);
});
it('distinguishes all package targets even though directory prefixes overlap', async () => {
const paths = [
'src/lib.rs',
'src/main.rs',
'src/bin/tool.rs',
'tests/helper.rs',
'benches/helper.rs',
'examples/helper.rs',
];
const dir = fixture({
'Cargo.toml': PACKAGE,
...Object.fromEntries(paths.map((file) => [file, 'pub fn helper() {}'])),
});
const config = await loadRustCargoTargets(dir);
for (const target of paths.slice(1))
expect(rustFilesShareCargoTarget(config, paths[0]!, target)).toBe(false);
});
it('follows normal, nested, inline and unit-test modules', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': 'mod foo; #[cfg(test)] mod tests { mod helper; }',
'src/foo.rs': 'mod nested;',
'src/foo/nested.rs': '',
'src/tests/helper.rs': '',
});
const config = await loadRustCargoTargets(dir);
for (const file of ['src/foo.rs', 'src/foo/nested.rs', 'src/tests/helper.rs']) {
expect(rustFilesShareCargoTarget(config, 'src/lib.rs', file)).toBe(true);
}
});
it('does not mistake an auto-target named target for a build artifact directory', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': '',
'tests/helper.rs': '',
'src/bin/target/main.rs':
'#[path="../../lib.rs"] mod lib; #[path="../../../tests/helper.rs"] mod helper;',
});
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBe(true);
});
it('permits a tests/ file shared with the library using #[path]', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': '#[path = "../tests/helper.rs"] mod helper;',
'tests/helper.rs': '',
});
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBe(true);
});
it('a #[path] file owns its directory when loading its own submodules', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': '#[path="../tests/helper.rs"] mod helper;',
'tests/helper.rs': 'pub mod inner;',
'tests/inner.rs': 'pub fn found() {}',
'tests/helper/inner.rs': 'pub fn different() {}',
});
const config = await loadRustCargoTargets(dir);
expect(rustFilesShareCargoTarget(config, 'src/lib.rs', 'tests/inner.rs')).toBe(true);
expect(
rustFilesShareCargoTarget(config, 'src/lib.rs', 'tests/helper/inner.rs'),
).toBeUndefined();
});
it('supports raw-string paths and inline path bases in non-mod.rs files', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': 'mod foo;',
'src/foo.rs': 'mod inner { #[path = r#"helper.rs"#] mod helper; }',
'src/foo/inner/helper.rs': '',
});
expect(
rustFilesShareCargoTarget(
await loadRustCargoTargets(dir),
'src/lib.rs',
'src/foo/inner/helper.rs',
),
).toBe(true);
});
it('an inline module path override is relative to the source directory', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': 'mod foo;',
'src/foo.rs': '#[path="thread_files"] mod thread { #[path="tls.rs"] mod local_data; }',
'src/thread_files/tls.rs': '',
});
expect(
rustFilesShareCargoTarget(
await loadRustCargoTargets(dir),
'src/lib.rs',
'src/thread_files/tls.rs',
),
).toBe(true);
});
it('does not treat derive or expression-position std macros as unknown expansion', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs':
'#[derive(Debug)] struct S;\n#[test] #[should_panic] fn t() { println!("hi"); assert_eq!(1, 1); let _ = vec![1]; let _ = format!("{}", 1); }',
'tests/helper.rs': '',
});
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBe(false);
});
it('keeps a library module whose path segment is named target', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': 'mod target;',
'src/target/mod.rs': '',
'tests/helper.rs': '',
});
const config = await loadRustCargoTargets(dir);
expect(rustFilesShareCargoTarget(config, 'src/lib.rs', 'src/target/mod.rs')).toBe(true);
expect(rustFilesShareCargoTarget(config, 'src/lib.rs', 'tests/helper.rs')).toBe(false);
});
it('keeps an explicit [lib] path under target/', async () => {
const dir = fixture({
'Cargo.toml': `${PACKAGE}[lib]\npath = "target/entry.rs"\n`,
'target/entry.rs': '',
'tests/helper.rs': '',
});
expect(
rustFilesShareCargoTarget(
await loadRustCargoTargets(dir),
'target/entry.rs',
'tests/helper.rs',
),
).toBe(false);
});
it.each([
'include!("generated.rs");',
'extern crate self as api;',
'#[cfg_attr(feature="x", path="elsewhere.rs")] mod helper;',
'#[custom_macro] mod helper;',
'mod missing;',
'mod broken {',
])('returns unknown for incomplete module evidence: %s', async (source) => {
const dir = fixture({ 'Cargo.toml': PACKAGE, 'src/lib.rs': source, 'tests/helper.rs': '' });
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBeUndefined();
});
it('ignores module-like text in strings and comments', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': '// mod missing;\nconst S: &str = "mod absent;";',
'tests/helper.rs': '',
});
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBe(false);
});
it('does not retain membership across changed source snapshots', async () => {
const dir = fixture({ 'Cargo.toml': PACKAGE, 'src/lib.rs': '', 'tests/helper.rs': '' });
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBe(false);
fs.writeFileSync(path.join(dir, 'src/lib.rs'), '#[path="../tests/helper.rs"] mod helper;');
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBe(true);
});
it('honors inherited workspace editions and package-local custom targets', async () => {
const dir = fixture({
'Cargo.toml':
'[workspace]\nmembers=["crates/a", "crates/b"]\n[workspace.package]\nedition="2021"\n',
'crates/a/Cargo.toml':
'[package]\nname="a"\nedition.workspace=true\n[lib]\npath="library/entry.rs"\n',
'crates/a/library/entry.rs': '',
'crates/a/tests/helper.rs': '',
'crates/b/Cargo.toml': PACKAGE,
'crates/b/src/lib.rs': '',
});
const config = await loadRustCargoTargets(dir);
expect(
rustFilesShareCargoTarget(config, 'crates/a/library/entry.rs', 'crates/a/tests/helper.rs'),
).toBe(false);
expect(
rustFilesShareCargoTarget(config, 'crates/a/library/entry.rs', 'crates/b/src/lib.rs'),
).toBe(false);
});
it('a disabled integration target can still be a library module', async () => {
const dir = fixture({
'Cargo.toml': `${PACKAGE}autotests=false\n`,
'src/lib.rs': '#[path="../tests/helper.rs"] mod helper;',
'tests/helper.rs': '',
});
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBe(true);
fs.writeFileSync(path.join(dir, 'src/lib.rs'), '');
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBeUndefined();
});
it('inspects external modules and expansion uncertainty in function bodies', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': 'fn local() { #[path="../tests/helper.rs"] mod helper; }',
'tests/helper.rs': '',
});
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBe(true);
fs.writeFileSync(path.join(dir, 'src/lib.rs'), 'fn local() { include!("generated.rs"); }');
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBeUndefined();
});
it('uses the safe parser for sources exceeding the native Windows string limit', async () => {
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': `// ${'x'.repeat(40_000)}\nmod helper;`,
'src/helper.rs': '',
'tests/helper.rs': '',
});
const config = await loadRustCargoTargets(dir);
expect(rustFilesShareCargoTarget(config, 'src/lib.rs', 'src/helper.rs')).toBe(true);
expect(rustFilesShareCargoTarget(config, 'src/lib.rs', 'tests/helper.rs')).toBe(false);
});
it('does not read through a module symlink outside the repository', async () => {
const outside = fixture({ 'helper.rs': 'pub fn helper() {}' });
const dir = fixture({
'Cargo.toml': PACKAGE,
'src/lib.rs': 'mod helper;',
'tests/helper.rs': '',
});
fs.symlinkSync(path.join(outside, 'helper.rs'), path.join(dir, 'src/helper.rs'));
expect(
rustFilesShareCargoTarget(await loadRustCargoTargets(dir), 'src/lib.rs', 'tests/helper.rs'),
).toBeUndefined();
});
it('discards membership when a checked source is replaced before the read', async () => {
const dir = fixture({ 'Cargo.toml': PACKAGE, 'src/lib.rs': '', 'tests/helper.rs': '' });
const source = path.join(dir, 'src/lib.rs');
const originalStat = fs.statSync(source);
let replaced = false;
const replace = (stat: fs.Stats) => {
if (!replaced && stat.dev === originalStat.dev && stat.ino === originalStat.ino) {
replaced = true;
fs.renameSync(source, `${source}.old`);
fs.writeFileSync(source, 'pub fn replacement() {}');
}
};
// Exercise the same replacement against the old path-stat/read sequence
// and the descriptor-based reader. Neither may accept the unchecked file.
const pathStat = fs.promises.stat.bind(fs.promises);
vi.spyOn(fs.promises, 'stat').mockImplementation(async (...args) => {
const stat = await pathStat(...args);
replace(stat as fs.Stats);
return stat;
});
const descriptorStat = fs.fstatSync.bind(fs);
vi.spyOn(fs, 'fstatSync').mockImplementation((...args) => {
const stat = descriptorStat(...args);
replace(stat as fs.Stats);
return stat;
});
const config = await loadRustCargoTargets(dir);
expect(replaced).toBe(true);
expect(rustFilesShareCargoTarget(config, 'src/lib.rs', 'tests/helper.rs')).toBeUndefined();
});
});