gitnexus 1.6.13-rc.7 → 1.6.13-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -431,7 +431,7 @@ TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift,
431
431
  | Kotlin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
432
432
  | C# | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
433
433
  | Go | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
434
- | Rust | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
434
+ | Rust | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
435
435
  | PHP | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ |
436
436
  | Ruby | ✓ | — | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
437
437
  | Swift | — | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
@@ -0,0 +1,9 @@
1
+ import type Parser from 'tree-sitter';
2
+ /** Public use evidence from the same AST used for membership. Keeps restricted
3
+ * and private globs distinct without changing the shared import/cache shape. */
4
+ export declare function rustPublicUses(root: Parser.SyntaxNode): ReadonlySet<string>;
5
+ /** External modules reachable from one source file; undefined is incomplete. */
6
+ export declare function rustModuleFiles(root: Parser.SyntaxNode, file: string, ownsDirectory: boolean, files: ReadonlySet<string>, missingFiles?: Set<string>, isCrateRoot?: boolean): readonly {
7
+ file: string;
8
+ ownsDirectory: boolean;
9
+ }[] | undefined;
@@ -0,0 +1,319 @@
1
+ import path from 'node:path';
2
+ import { splitRustUseDeclaration } from './import-decomposer.js';
3
+ // Built-in attributes cannot expand to new module declarations. cfg is a union:
4
+ // visiting both alternatives is conservative; cfg_attr may change a path.
5
+ const NON_EXPANDING_ATTRIBUTES = new Set([
6
+ 'cfg',
7
+ 'path',
8
+ 'allow',
9
+ 'warn',
10
+ 'deny',
11
+ 'forbid',
12
+ 'expect',
13
+ 'doc',
14
+ 'test',
15
+ 'should_panic',
16
+ 'ignore',
17
+ 'automatically_derived',
18
+ 'proc_macro',
19
+ 'proc_macro_derive',
20
+ 'proc_macro_attribute',
21
+ 'inline',
22
+ 'cold',
23
+ 'no_mangle',
24
+ 'export_name',
25
+ 'repr',
26
+ 'non_exhaustive',
27
+ 'must_use',
28
+ 'deprecated',
29
+ 'no_std',
30
+ 'no_main',
31
+ 'feature',
32
+ 'crate_type',
33
+ 'crate_name',
34
+ 'recursion_limit',
35
+ 'type_length_limit',
36
+ ]);
37
+ const BUILTIN_DERIVES = new Set([
38
+ 'Clone',
39
+ 'Copy',
40
+ 'Debug',
41
+ 'Default',
42
+ 'Eq',
43
+ 'Hash',
44
+ 'Ord',
45
+ 'PartialEq',
46
+ 'PartialOrd',
47
+ ]);
48
+ const EXPRESSION_MACROS = new Set([
49
+ 'print',
50
+ 'println',
51
+ 'eprint',
52
+ 'eprintln',
53
+ 'assert',
54
+ 'assert_eq',
55
+ 'assert_ne',
56
+ 'debug_assert',
57
+ 'debug_assert_eq',
58
+ 'debug_assert_ne',
59
+ 'vec',
60
+ 'format',
61
+ 'format_args',
62
+ 'write',
63
+ 'writeln',
64
+ 'panic',
65
+ 'todo',
66
+ 'unimplemented',
67
+ 'unreachable',
68
+ 'dbg',
69
+ 'matches',
70
+ ]);
71
+ /** Token arguments can contain blocks/modules or further macro expansion.
72
+ * Inspect token nodes, never strings/comments that merely mention those words. */
73
+ function hasExpandingArguments(tokens) {
74
+ for (let i = 0; i < tokens.childCount; i++) {
75
+ const child = tokens.child(i);
76
+ if (child.type === 'mod' || child.type === '#')
77
+ return true;
78
+ if (child.type === '!' && tokens.child(i - 1)?.type === 'identifier')
79
+ return true;
80
+ if (child.type === 'token_tree' && hasExpandingArguments(child))
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+ /** Public use evidence from the same AST used for membership. Keeps restricted
86
+ * and private globs distinct without changing the shared import/cache shape. */
87
+ export function rustPublicUses(root) {
88
+ const uses = new Set();
89
+ const pending = [{ node: root, module: '' }];
90
+ while (pending.length > 0) {
91
+ const { node, module } = pending.pop();
92
+ for (const child of node.namedChildren) {
93
+ if (child.type === 'mod_item') {
94
+ const body = child.childForFieldName('body');
95
+ const name = child.childForFieldName('name')?.text;
96
+ if (body && name)
97
+ pending.push({ node: body, module: [module, name].filter(Boolean).join('::') });
98
+ }
99
+ else if (child.type === 'use_declaration' &&
100
+ child.namedChildren.some((part) => part.type === 'visibility_modifier' && part.text === 'pub')) {
101
+ for (const capture of splitRustUseDeclaration(child)) {
102
+ uses.add(JSON.stringify([
103
+ module,
104
+ capture['@import.source']?.text,
105
+ capture['@import.kind']?.text,
106
+ capture['@import.name']?.text,
107
+ ]));
108
+ }
109
+ }
110
+ }
111
+ }
112
+ return uses;
113
+ }
114
+ /** Decode a literal path without mistaking strings/comments for Rust syntax. */
115
+ function literalPath(text) {
116
+ const raw = /^r(#+)?"([\s\S]*)"\1$/.exec(text);
117
+ if (raw)
118
+ return raw[2];
119
+ // Escape forms beyond JSON's subset remain unknown, never a guessed path.
120
+ try {
121
+ const value = JSON.parse(text);
122
+ return typeof value === 'string' ? value : undefined;
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ }
128
+ /** External modules reachable from one source file; undefined is incomplete. */
129
+ export function rustModuleFiles(root, file, ownsDirectory, files, missingFiles, isCrateRoot = false) {
130
+ if (root.hasError)
131
+ return undefined;
132
+ // Built-in spellings are not proof when an import/local macro can shadow
133
+ // them. Cross-file macro_use/macro_export remain unknown attributes below.
134
+ const shadowed = new Set();
135
+ const globs = [];
136
+ const scan = [root];
137
+ while (scan.length) {
138
+ const node = scan.pop();
139
+ if (node.type === 'macro_definition' || node.type === 'mod_item') {
140
+ const name = node.childForFieldName('name')?.text;
141
+ if (name)
142
+ shadowed.add(name);
143
+ // macro_rules textual scope can extend into child module files. Without
144
+ // expansion/scope receipts, do not assume their same-named calls are std.
145
+ if (node.type === 'macro_definition' && name && EXPRESSION_MACROS.has(name))
146
+ return undefined;
147
+ }
148
+ if (node.type === 'use_declaration') {
149
+ for (const capture of splitRustUseDeclaration(node)) {
150
+ if (capture['@import.kind']?.text === 'wildcard')
151
+ globs.push({ node, path: capture['@import.source']?.text ?? '' });
152
+ const name = capture['@import.name']?.text;
153
+ if (name)
154
+ shadowed.add(name);
155
+ }
156
+ }
157
+ if (node.type !== 'macro_definition' && node.type !== 'token_tree')
158
+ scan.push(...node.namedChildren);
159
+ }
160
+ const wildcard = globs.some(({ node, path: imported }) => {
161
+ const parts = imported.split('::').filter(Boolean);
162
+ // Cargo aliases for these names are rejected by the loader. A local
163
+ // module/import can still shadow a standard-library path in this file.
164
+ if (['std', 'core', 'alloc'].includes(parts[0] ?? '') && !shadowed.has(parts[0]))
165
+ return false;
166
+ let depth = 0;
167
+ let localModuleScope = true;
168
+ for (let parent = node.parent; parent && parent !== root; parent = parent.parent) {
169
+ if (parent.type === 'mod_item')
170
+ depth++;
171
+ if (parent.type === 'function_item' ||
172
+ parent.type === 'block' ||
173
+ parent.type === 'closure_expression')
174
+ localModuleScope = false;
175
+ }
176
+ // The common inline unit-test `use super::*` stays within this AST.
177
+ // A file-level super glob has an external parent and remains unknown.
178
+ if (localModuleScope &&
179
+ parts.length > 0 &&
180
+ parts.every((part) => part === 'super') &&
181
+ parts.length <= depth)
182
+ return false;
183
+ // At a Cargo root, bare/self/crate paths to inline modules have the same
184
+ // meaning across editions. Do not extend this assumption to file modules.
185
+ if (isCrateRoot && node.parent === root) {
186
+ if (parts[0] === 'self' || parts[0] === 'crate')
187
+ parts.shift();
188
+ let body = root;
189
+ for (const part of parts) {
190
+ body =
191
+ body?.namedChildren
192
+ .find((child) => child.type === 'mod_item' && child.childForFieldName('name')?.text === part)
193
+ ?.childForFieldName('body') ?? undefined;
194
+ }
195
+ if (parts.length > 0 && body !== undefined)
196
+ return false;
197
+ }
198
+ return true;
199
+ });
200
+ const builtin = (name) => !wildcard && !shadowed.has(name);
201
+ const fileDir = path.posix.dirname(file);
202
+ const moduleDir = ownsDirectory || path.posix.basename(file) === 'mod.rs'
203
+ ? fileDir
204
+ : file.slice(0, -'.rs'.length);
205
+ const pending = [{ node: root, moduleDir, attributeDir: fileDir }];
206
+ const result = [];
207
+ let unresolved = false;
208
+ while (pending.length) {
209
+ const context = pending.pop();
210
+ let attributes = [];
211
+ for (const node of context.node.namedChildren) {
212
+ if (node.type === 'line_comment' || node.type === 'block_comment')
213
+ continue;
214
+ if (node.type === 'attribute_item' || node.type === 'inner_attribute_item') {
215
+ const attribute = node.namedChildren[0];
216
+ const name = attribute?.namedChildren[0]?.text;
217
+ if (name === 'derive') {
218
+ const argumentsNode = attribute?.childForFieldName('arguments');
219
+ if (!argumentsNode ||
220
+ !builtin('derive') ||
221
+ argumentsNode.namedChildren.length === 0 ||
222
+ argumentsNode.namedChildren.some((item) => item.type !== 'identifier' ||
223
+ !BUILTIN_DERIVES.has(item.text) ||
224
+ !builtin(item.text)) ||
225
+ argumentsNode.children.some((item) => !['(', ')', ',', 'identifier', 'line_comment', 'block_comment'].includes(item.type)))
226
+ return undefined;
227
+ }
228
+ else if (!name || !NON_EXPANDING_ATTRIBUTES.has(name))
229
+ return undefined;
230
+ if (node.type === 'attribute_item')
231
+ attributes.push(node);
232
+ continue;
233
+ }
234
+ const attrs = attributes;
235
+ attributes = [];
236
+ // The current scope captures do not carry extern-crate aliases. Do not
237
+ // certify a negative import-root proof from an incomplete namespace view.
238
+ if (node.type === 'extern_crate_declaration' && node.childForFieldName('alias') !== null) {
239
+ return undefined;
240
+ }
241
+ const macro = node.type === 'macro_invocation'
242
+ ? node
243
+ : node.type === 'expression_statement' &&
244
+ node.namedChildren[0]?.type === 'macro_invocation'
245
+ ? node.namedChildren[0]
246
+ : undefined;
247
+ if (macro) {
248
+ const name = macro.childForFieldName('macro')?.text;
249
+ const tokens = macro.namedChildren.find((child) => child.type === 'token_tree');
250
+ const parts = name?.split('::').filter(Boolean) ?? [];
251
+ const standard = parts.length === 1 ||
252
+ (parts.length === 2 && ['std', 'core', 'alloc'].includes(parts[0]));
253
+ if (!standard ||
254
+ !EXPRESSION_MACROS.has(parts.at(-1) ?? '') ||
255
+ !parts.every(builtin) ||
256
+ !tokens ||
257
+ hasExpandingArguments(tokens) ||
258
+ context.node.type === 'source_file' ||
259
+ context.node.type === 'declaration_list')
260
+ return undefined;
261
+ continue;
262
+ }
263
+ if (node.type !== 'mod_item') {
264
+ // Items (including external #[path] modules) can also occur in blocks.
265
+ // Inspect them too; a macro expansion there can add shared membership.
266
+ // Macro definitions/token trees and literal contents are not expansions.
267
+ if (node.type !== 'macro_definition' &&
268
+ node.type !== 'token_tree' &&
269
+ node.namedChildCount > 0) {
270
+ pending.push({ ...context, node });
271
+ }
272
+ continue;
273
+ }
274
+ const name = node.childForFieldName('name')?.text;
275
+ if (!name)
276
+ return undefined;
277
+ let override;
278
+ for (const attr of attrs) {
279
+ const attribute = attr.namedChildren[0];
280
+ if (attribute.namedChildren[0]?.text !== 'path')
281
+ continue;
282
+ const value = attribute.childForFieldName('value');
283
+ if (!value || override !== undefined)
284
+ return undefined;
285
+ override = literalPath(value.text);
286
+ if (override === undefined || path.posix.isAbsolute(override) || override.includes('\\'))
287
+ return undefined;
288
+ }
289
+ const body = node.childForFieldName('body');
290
+ if (body) {
291
+ const dir = override === undefined
292
+ ? path.posix.join(context.moduleDir, name)
293
+ : path.posix.join(context.attributeDir, override);
294
+ pending.push({ node: body, moduleDir: dir, attributeDir: dir });
295
+ continue;
296
+ }
297
+ const candidates = override !== undefined
298
+ ? [path.posix.normalize(path.posix.join(context.attributeDir, override))]
299
+ : [
300
+ path.posix.join(context.moduleDir, `${name}.rs`),
301
+ path.posix.join(context.moduleDir, name, 'mod.rs'),
302
+ ];
303
+ const existing = candidates.filter((candidate) => files.has(candidate));
304
+ if (existing.length === 0) {
305
+ if (!missingFiles)
306
+ return undefined;
307
+ for (const candidate of candidates)
308
+ missingFiles.add(candidate);
309
+ unresolved = true;
310
+ continue;
311
+ }
312
+ // Union conditional alternatives; shared membership must never be erased.
313
+ // #[path] makes the loaded file own its containing directory, just like
314
+ // a crate root; its children do NOT acquire the file stem as a prefix.
315
+ result.push(...existing.map((file) => ({ file, ownsDirectory: override !== undefined })));
316
+ }
317
+ }
318
+ return unresolved ? undefined : result;
319
+ }
@@ -0,0 +1,21 @@
1
+ /** A target is identified by its entry file, not its package directory. */
2
+ export declare function cargoTargetRoots(manifest: string, content: string, files: ReadonlySet<string>, workspaceEditions?: ReadonlyMap<string, string>): readonly string[] | undefined;
3
+ /** Exact public-use evidence, independent of the capture's coarse reexport kind. */
4
+ export declare function rustCargoPubliclyReexports(config: unknown, file: string, module: string, target: string, kind: string, name: string): boolean;
5
+ /** Positive evidence that this import names this library's ROOT, not a module
6
+ * elsewhere (or a binary/test entry point that cannot be imported as a lib). */
7
+ export declare function rustImportNamesCargoRoot(config: unknown, caller: string, candidate: string, importedModule: string): boolean;
8
+ /** Every known membership must identify this file as the entry point. A file
9
+ * shared as a module in another target does not have a single root role. */
10
+ export declare function rustIsExclusiveCargoRoot(config: unknown, file: string): boolean;
11
+ /** Establish crate identity before the existing module-path plausibility test. */
12
+ export declare function rustImportReachesCargoTarget(config: unknown, caller: string, candidate: string, importedModule: string): boolean;
13
+ /** Undefined means no complete membership proof; never interpret it as disjoint. */
14
+ export declare function rustFilesShareCargoTarget(config: unknown, caller: string, candidate: string): boolean | undefined;
15
+ /**
16
+ * Static, bounded, one-shot provider loader. No cargo/rustc, build scripts,
17
+ * repository wrappers, or network. Parse only files reachable from Cargo roots.
18
+ * Unknown expansion anywhere can add shared membership, so it invalidates the
19
+ * negative proof for this snapshot rather than producing a partial veto.
20
+ */
21
+ export declare function loadRustCargoTargets(repoPath: string): Promise<unknown>;
@@ -0,0 +1,473 @@
1
+ /** Cargo target evidence for the name-guess veto, never a directory heuristic. */
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { glob, escape } from 'glob';
5
+ import { parse } from 'smol-toml';
6
+ import Parser from 'tree-sitter';
7
+ import { SupportedLanguages } from '../../../../_shared/index.js';
8
+ import { getLanguageGrammar } from '../../../tree-sitter/parser-loader.js';
9
+ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
10
+ import { readRepoControlFile } from '../../../../config/repo-control-file.js';
11
+ import { rustModuleFiles, rustPublicUses } from './cargo-module-files.js';
12
+ const MAX_FILES = 100_000;
13
+ const table = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
14
+ /** A target is identified by its entry file, not its package directory. */
15
+ export function cargoTargetRoots(manifest, content, files, workspaceEditions) {
16
+ let data;
17
+ try {
18
+ data = parse(content);
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ if (!table(data.package))
24
+ return table(data.workspace) ? [] : undefined;
25
+ const pkg = data.package;
26
+ if (typeof pkg.name !== 'string')
27
+ return undefined;
28
+ if (pkg.build !== undefined && typeof pkg.build !== 'string' && typeof pkg.build !== 'boolean')
29
+ return undefined;
30
+ const dir = path.posix.dirname(manifest);
31
+ let edition = pkg.edition ?? '2015';
32
+ if (table(edition) && edition.workspace === true) {
33
+ let workspace = typeof pkg.workspace === 'string' ? path.posix.join(dir, pkg.workspace, '.') : dir;
34
+ while (!workspaceEditions?.has(workspace) &&
35
+ typeof pkg.workspace !== 'string' &&
36
+ workspace !== '.') {
37
+ workspace = path.posix.dirname(workspace);
38
+ }
39
+ edition = workspaceEditions?.get(workspace);
40
+ }
41
+ if (typeof edition !== 'string' || !['2015', '2018', '2021', '2024'].includes(edition))
42
+ return undefined;
43
+ const relative = (file) => path.posix.normalize(path.posix.join(dir, file));
44
+ const roots = new Set();
45
+ for (const [kind, folder] of [
46
+ ['lib', 'src'],
47
+ ['bin', 'src/bin'],
48
+ ['test', 'tests'],
49
+ ['bench', 'benches'],
50
+ ['example', 'examples'],
51
+ ]) {
52
+ const autoKey = {
53
+ lib: 'autolib',
54
+ bin: 'autobins',
55
+ test: 'autotests',
56
+ bench: 'autobenches',
57
+ example: 'autoexamples',
58
+ }[kind];
59
+ if (pkg[autoKey] !== undefined && typeof pkg[autoKey] !== 'boolean')
60
+ return undefined;
61
+ const discovered = new Map();
62
+ if (kind === 'lib') {
63
+ if (files.has(relative('src/lib.rs')))
64
+ discovered.set(pkg.name, relative('src/lib.rs'));
65
+ }
66
+ else {
67
+ if (kind === 'bin' && files.has(relative('src/main.rs'))) {
68
+ discovered.set(pkg.name, relative('src/main.rs'));
69
+ }
70
+ const prefix = `${relative(folder)}/`;
71
+ for (const file of files) {
72
+ if (!file.startsWith(prefix))
73
+ continue;
74
+ const tail = file.slice(prefix.length);
75
+ const match = /^([^/]+)\.rs$/.exec(tail) ?? /^([^/]+)\/main\.rs$/.exec(tail);
76
+ if (match)
77
+ discovered.set(match[1], file);
78
+ }
79
+ }
80
+ const explicit = data[kind] === undefined ? [] : kind === 'lib' ? [data[kind]] : data[kind];
81
+ if (!Array.isArray(explicit))
82
+ return undefined;
83
+ // Cargo 2015's opt-in discovery rule is PER target kind: an explicit
84
+ // binary does not disable integration tests, examples, benches or the lib.
85
+ const legacy = edition === '2015' && explicit.length > 0;
86
+ const overridden = new Set();
87
+ for (const entry of explicit) {
88
+ if (!table(entry))
89
+ return undefined;
90
+ const name = kind === 'lib' ? pkg.name : entry.name;
91
+ if (typeof name !== 'string')
92
+ return undefined;
93
+ if (entry.path !== undefined && typeof entry.path !== 'string')
94
+ return undefined;
95
+ if (typeof entry.path === 'string' && path.posix.isAbsolute(entry.path))
96
+ return undefined;
97
+ const file = typeof entry.path === 'string' ? relative(entry.path) : discovered.get(name);
98
+ if (!file || !files.has(file))
99
+ return undefined;
100
+ roots.add(file);
101
+ overridden.add(name);
102
+ }
103
+ if (pkg[autoKey] === true || (pkg[autoKey] !== false && !legacy)) {
104
+ for (const [name, file] of discovered)
105
+ if (!overridden.has(name))
106
+ roots.add(file);
107
+ }
108
+ }
109
+ // Build scripts are crates too, even when located outside src/.
110
+ if (typeof pkg.build === 'string') {
111
+ const file = relative(pkg.build);
112
+ if (!files.has(file))
113
+ return undefined;
114
+ if (roots.has(file))
115
+ return undefined; // Multiple target roles need separate identities.
116
+ roots.add(file);
117
+ }
118
+ else if (pkg.build !== false && files.has(relative('build.rs'))) {
119
+ if (roots.has(relative('build.rs')))
120
+ return undefined;
121
+ roots.add(relative('build.rs'));
122
+ }
123
+ return [...roots];
124
+ }
125
+ class RustCargoTargets {
126
+ targetsByFile;
127
+ rootImports;
128
+ publicUsesByFile;
129
+ constructor(targetsByFile, rootImports, publicUsesByFile) {
130
+ this.targetsByFile = targetsByFile;
131
+ this.rootImports = rootImports;
132
+ this.publicUsesByFile = publicUsesByFile;
133
+ }
134
+ }
135
+ /** Exact public-use evidence, independent of the capture's coarse reexport kind. */
136
+ export function rustCargoPubliclyReexports(config, file, module, target, kind, name) {
137
+ return (config instanceof RustCargoTargets &&
138
+ config.publicUsesByFile.get(file)?.has(JSON.stringify([module, target, kind, name])) === true);
139
+ }
140
+ /** Positive evidence that this import names this library's ROOT, not a module
141
+ * elsewhere (or a binary/test entry point that cannot be imported as a lib). */
142
+ export function rustImportNamesCargoRoot(config, caller, candidate, importedModule) {
143
+ if (!(config instanceof RustCargoTargets))
144
+ return false;
145
+ const segments = importedModule.split('::').filter(Boolean);
146
+ if (segments.length !== 1)
147
+ return false;
148
+ for (const target of config.targetsByFile.get(caller) ?? []) {
149
+ if (config.rootImports.get(target)?.get(segments[0])?.has(candidate))
150
+ return true;
151
+ }
152
+ return false;
153
+ }
154
+ /** Every known membership must identify this file as the entry point. A file
155
+ * shared as a module in another target does not have a single root role. */
156
+ export function rustIsExclusiveCargoRoot(config, file) {
157
+ if (!(config instanceof RustCargoTargets))
158
+ return false;
159
+ const targets = config.targetsByFile.get(file);
160
+ return targets?.size === 1 && targets.has(file);
161
+ }
162
+ /** Establish crate identity before the existing module-path plausibility test. */
163
+ export function rustImportReachesCargoTarget(config, caller, candidate, importedModule) {
164
+ if (!(config instanceof RustCargoTargets))
165
+ return false;
166
+ const name = importedModule.split('::').filter(Boolean)[0];
167
+ if (!name)
168
+ return false;
169
+ const candidates = config.targetsByFile.get(candidate);
170
+ for (const target of config.targetsByFile.get(caller) ?? []) {
171
+ for (const imported of config.rootImports.get(target)?.get(name) ?? []) {
172
+ if (candidates?.has(imported))
173
+ return true;
174
+ }
175
+ }
176
+ return false;
177
+ }
178
+ /** Import names are target/package-relative. A dependency alias in another
179
+ * package must not authorize a guess here merely because its spelling matches. */
180
+ function cargoRootImports(manifests, targets) {
181
+ const libraries = new Map();
182
+ for (const [manifest, data] of manifests) {
183
+ if (!table(data.package))
184
+ continue;
185
+ const lib = table(data.lib) ? data.lib : undefined;
186
+ if (!lib && data.package.autolib === false)
187
+ continue;
188
+ const root = path.posix.join(path.posix.dirname(manifest), typeof lib?.path === 'string' ? lib.path : 'src/lib.rs');
189
+ const name = lib?.name ?? data.package.name;
190
+ if (typeof name === 'string' && targets.get(manifest)?.includes(root)) {
191
+ libraries.set(manifest, {
192
+ root,
193
+ name: name.replaceAll('-', '_'),
194
+ });
195
+ }
196
+ }
197
+ const result = new Map();
198
+ for (const [manifest, data] of manifests) {
199
+ if (!table(data.package))
200
+ continue;
201
+ const dir = path.posix.dirname(manifest);
202
+ let workspace = typeof data.package.workspace === 'string'
203
+ ? path.posix.join(dir, data.package.workspace, '.')
204
+ : dir;
205
+ while (!table(manifests.get(path.posix.join(workspace, 'Cargo.toml'))?.workspace) &&
206
+ typeof data.package.workspace !== 'string' &&
207
+ workspace !== '.')
208
+ workspace = path.posix.dirname(workspace);
209
+ const workspaceData = manifests.get(path.posix.join(workspace, 'Cargo.toml'))?.workspace;
210
+ const workspaceDeps = table(workspaceData) && table(workspaceData.dependencies) ? workspaceData.dependencies : {};
211
+ const sections = [
212
+ data,
213
+ ...(table(data.target) ? Object.values(data.target).filter(table) : []),
214
+ ];
215
+ for (const target of targets.get(manifest) ?? []) {
216
+ let imports = result.get(target);
217
+ if (!imports)
218
+ result.set(target, (imports = new Map()));
219
+ const add = (name, root) => {
220
+ let roots = imports.get(name);
221
+ if (!roots)
222
+ imports.set(name, (roots = new Set()));
223
+ roots.add(root);
224
+ };
225
+ const own = libraries.get(manifest);
226
+ const buildRoot = path.posix.join(dir, typeof data.package.build === 'string' ? data.package.build : 'build.rs');
227
+ const isBuild = data.package.build !== false && target === buildRoot;
228
+ if (own && !isBuild)
229
+ add(own.name, own.root);
230
+ for (const section of sections) {
231
+ // Libraries/binaries can also compile as unit-test targets, so retain
232
+ // dev dependencies across cfg modes. Build scripts have their own
233
+ // dependency namespace and cannot import the package's own library.
234
+ const kinds = isBuild ? ['build-dependencies'] : ['dependencies', 'dev-dependencies'];
235
+ for (const kind of kinds) {
236
+ const deps = section[kind];
237
+ if (!table(deps))
238
+ continue;
239
+ for (const [key, declared] of Object.entries(deps)) {
240
+ const inherited = table(declared) && declared.workspace === true;
241
+ const dep = inherited ? workspaceDeps[key] : declared;
242
+ if (!table(dep) || typeof dep.path !== 'string')
243
+ continue;
244
+ const dependency = libraries.get(path.posix.join(inherited ? workspace : dir, dep.path, 'Cargo.toml'));
245
+ if (!dependency)
246
+ continue;
247
+ // Cargo uses the dependency key whenever `package` is explicit,
248
+ // even if it equals the package name and [lib].name differs.
249
+ const renamed = typeof dep.package === 'string';
250
+ add(renamed ? key.replaceAll('-', '_') : dependency.name, dependency.root);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ return result;
257
+ }
258
+ /** Undefined means no complete membership proof; never interpret it as disjoint. */
259
+ export function rustFilesShareCargoTarget(config, caller, candidate) {
260
+ if (!(config instanceof RustCargoTargets))
261
+ return undefined;
262
+ const callers = config.targetsByFile.get(caller);
263
+ const candidates = config.targetsByFile.get(candidate);
264
+ if (!callers || !candidates)
265
+ return undefined;
266
+ return [...callers].some((root) => candidates.has(root));
267
+ }
268
+ /**
269
+ * Static, bounded, one-shot provider loader. No cargo/rustc, build scripts,
270
+ * repository wrappers, or network. Parse only files reachable from Cargo roots.
271
+ * Unknown expansion anywhere can add shared membership, so it invalidates the
272
+ * negative proof for this snapshot rather than producing a partial veto.
273
+ */
274
+ export async function loadRustCargoTargets(repoPath) {
275
+ try {
276
+ const root = await fs.realpath(repoPath);
277
+ const files = new Set();
278
+ const manifests = [];
279
+ for await (const entry of glob.iterate('**/Cargo.toml', {
280
+ // Cargo metadata must include targets the graph scanner omits (notably
281
+ // src/bin). An omitted target can share a source file with another crate.
282
+ cwd: root,
283
+ nodir: true,
284
+ follow: false,
285
+ posix: true,
286
+ dot: true,
287
+ ignore: [
288
+ '**/.git/**',
289
+ '**/node_modules/**',
290
+ '**/.gitnexus/**',
291
+ '**/target/debug/**',
292
+ '**/target/release/**',
293
+ '**/target/incremental/**',
294
+ '**/target/doc/**',
295
+ '**/target/tmp/**',
296
+ '**/target/.fingerprint/**',
297
+ '**/target/CACHEDIR.TAG',
298
+ ],
299
+ })) {
300
+ if (files.size >= MAX_FILES)
301
+ return undefined;
302
+ files.add(entry);
303
+ manifests.push(entry);
304
+ }
305
+ if (manifests.length === 0)
306
+ return undefined;
307
+ // Artifact-layout pruning (`target/debug`, `target/release`, …) must not
308
+ // erase a source path whose segment is named `target` (`src/target/mod.rs`,
309
+ // `[lib] path = "target/entry.rs"`, `src/bin/target/main.rs`). Re-scan
310
+ // Cargo's auto-target slots without that filter.
311
+ for (const manifest of manifests) {
312
+ for await (const entry of glob.iterate([
313
+ 'src/lib.rs',
314
+ 'src/main.rs',
315
+ 'src/bin/*.rs',
316
+ 'src/bin/*/main.rs',
317
+ 'tests/*.rs',
318
+ 'tests/*/main.rs',
319
+ 'benches/*.rs',
320
+ 'benches/*/main.rs',
321
+ 'examples/*.rs',
322
+ 'examples/*/main.rs',
323
+ 'build.rs',
324
+ ], {
325
+ cwd: path.join(root, path.posix.dirname(manifest)),
326
+ nodir: true,
327
+ follow: false,
328
+ posix: true,
329
+ })) {
330
+ if (files.size >= MAX_FILES)
331
+ return undefined;
332
+ files.add(path.posix.join(path.posix.dirname(manifest), entry));
333
+ }
334
+ }
335
+ const read = async (file) => {
336
+ const requested = path.resolve(root, file);
337
+ const absolute = await fs.realpath(requested);
338
+ const rel = path.relative(root, absolute);
339
+ if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
340
+ throw new Error('Cargo module outside repository');
341
+ }
342
+ if (absolute !== requested)
343
+ throw new Error('Cargo module alias has unknown membership');
344
+ // The shared reader validates its opened descriptor and bounds streamed
345
+ // bytes; the outer realpath check is a separate containment/alias guard.
346
+ const content = await readRepoControlFile(root, file);
347
+ if (content === null)
348
+ throw new Error('Cargo source disappeared');
349
+ return content;
350
+ };
351
+ const contents = new Map();
352
+ const manifestData = new Map();
353
+ const workspaceEditions = new Map();
354
+ for (const manifest of manifests) {
355
+ const content = await read(manifest);
356
+ contents.set(manifest, content);
357
+ const data = parse(content);
358
+ // A dependency alias can replace a std/core/alloc extern-prelude entry.
359
+ // In that case the module walker cannot identify standard macros safely.
360
+ const sections = [
361
+ data,
362
+ ...(table(data.target) ? Object.values(data.target).filter(table) : []),
363
+ ];
364
+ if (sections.some((section) => ['dependencies', 'dev-dependencies', 'build-dependencies'].some((kind) => table(section[kind]) &&
365
+ ['std', 'core', 'alloc'].some((name) => Object.hasOwn(section[kind], name)))))
366
+ return undefined;
367
+ manifestData.set(manifest, data);
368
+ if (table(data.workspace) &&
369
+ table(data.workspace.package) &&
370
+ typeof data.workspace.package.edition === 'string') {
371
+ workspaceEditions.set(path.posix.dirname(manifest), data.workspace.package.edition);
372
+ }
373
+ }
374
+ // Manifest discovery prunes artifacts, but explicit source paths and mod
375
+ // declarations are authoritative candidates even beneath a `target` folder.
376
+ // Probe only those literal paths; never crawl the artifact tree recursively.
377
+ const discover = async (candidates) => {
378
+ const patterns = [...candidates].map((file) => {
379
+ if (path.posix.isAbsolute(file) ||
380
+ file === '..' ||
381
+ file.startsWith('../') ||
382
+ file.includes('\\'))
383
+ throw new Error('Cargo source outside repository');
384
+ if (!file.endsWith('.rs') ||
385
+ file
386
+ .split('/')
387
+ .some((part) => ['.git', '.gitnexus', 'node_modules'].includes(part.toLowerCase())))
388
+ throw new Error('Cargo source excluded from inventory');
389
+ return escape(file);
390
+ });
391
+ for await (const entry of glob.iterate(patterns, {
392
+ cwd: root,
393
+ nodir: true,
394
+ follow: false,
395
+ posix: true,
396
+ dot: true,
397
+ })) {
398
+ if (files.size >= MAX_FILES)
399
+ throw new Error('Cargo file limit');
400
+ files.add(entry);
401
+ }
402
+ };
403
+ for (const [manifest, data] of manifestData) {
404
+ const explicit = [
405
+ data.lib,
406
+ ...['bin', 'test', 'bench', 'example'].flatMap((kind) => Array.isArray(data[kind]) ? data[kind] : []),
407
+ ];
408
+ const paths = explicit
409
+ .filter(table)
410
+ .map((entry) => entry.path)
411
+ .filter((value) => typeof value === 'string');
412
+ if (table(data.package) && typeof data.package.build === 'string')
413
+ paths.push(data.package.build);
414
+ await discover(paths.map((file) => path.posix.join(path.posix.dirname(manifest), file)));
415
+ }
416
+ const roots = new Set();
417
+ const targetsByManifest = new Map();
418
+ for (const [manifest, content] of contents) {
419
+ const targets = cargoTargetRoots(manifest, content, files, workspaceEditions);
420
+ if (targets === undefined)
421
+ return undefined;
422
+ targetsByManifest.set(manifest, targets);
423
+ for (const target of targets)
424
+ roots.add(target);
425
+ }
426
+ const parser = new Parser();
427
+ parser.setLanguage(getLanguageGrammar(SupportedLanguages.Rust));
428
+ const targetsByFile = new Map();
429
+ const publicUsesByFile = new Map();
430
+ // A file may be reached conventionally AND through #[path]. Those have
431
+ // different submodule bases, so cache and visit both contexts separately.
432
+ const childrenByFile = new Map();
433
+ let visits = 0;
434
+ for (const target of roots) {
435
+ const pending = [{ file: target, ownsDirectory: true }];
436
+ const visited = new Set();
437
+ while (pending.length > 0) {
438
+ if (++visits > MAX_FILES)
439
+ return undefined;
440
+ const { file, ownsDirectory } = pending.pop();
441
+ const key = `${file === target ? 'root' : ownsDirectory ? 'owned' : 'module'}:${file}`;
442
+ if (visited.has(key))
443
+ continue;
444
+ visited.add(key);
445
+ let owners = targetsByFile.get(file);
446
+ if (!owners)
447
+ targetsByFile.set(file, (owners = new Set()));
448
+ owners.add(target);
449
+ let children = childrenByFile.get(key);
450
+ if (!children) {
451
+ const tree = parseSourceSafe(parser, await read(file));
452
+ const missing = new Set();
453
+ let result = rustModuleFiles(tree.rootNode, file, ownsDirectory, files, missing, file === target);
454
+ if (result === undefined && missing.size > 0) {
455
+ await discover(missing);
456
+ result = rustModuleFiles(tree.rootNode, file, ownsDirectory, files, undefined, file === target);
457
+ }
458
+ if (result === undefined)
459
+ return undefined;
460
+ publicUsesByFile.set(file, rustPublicUses(tree.rootNode));
461
+ children = result;
462
+ childrenByFile.set(key, children);
463
+ }
464
+ pending.push(...children);
465
+ }
466
+ }
467
+ return new RustCargoTargets(targetsByFile, cargoRootImports(manifestData, targetsByManifest), publicUsesByFile);
468
+ }
469
+ catch {
470
+ // I/O, parse or containment failure cannot establish target separation.
471
+ return undefined;
472
+ }
473
+ }
@@ -7,15 +7,18 @@ import { syntheticCapture } from '../../utils/ast-helpers.js';
7
7
  export function splitRustUseDeclaration(node) {
8
8
  if (node.type !== 'use_declaration')
9
9
  return [];
10
- const isReexport = hasVisibilityModifier(node);
10
+ const isReexport = hasUnrestrictedPub(node);
11
11
  const argument = getUseArgument(node);
12
12
  if (argument === null)
13
13
  return [];
14
14
  return decomposeUseArgument(argument, '', isReexport, node);
15
15
  }
16
- function hasVisibilityModifier(node) {
16
+ function hasUnrestrictedPub(node) {
17
17
  for (let i = 0; i < node.childCount; i++) {
18
- if (node.child(i)?.type === 'visibility_modifier')
18
+ const child = node.child(i);
19
+ // `pub(crate)` / `pub(super)` / `pub(in …)` / `crate` are not the crate's
20
+ // public surface, so they must not walk as `pub use` re-export evidence.
21
+ if (child?.type === 'visibility_modifier' && child.text === 'pub')
19
22
  return true;
20
23
  }
21
24
  return false;
@@ -106,7 +109,9 @@ function buildScopedPath(node) {
106
109
  if (node.type === 'scoped_identifier') {
107
110
  const parts = [];
108
111
  collectScopedParts(node, parts);
109
- return parts.join('::');
112
+ // An absolute extern-prelude path bypasses a same-named local module.
113
+ // Keep that evidence when flattening the AST into an import specifier.
114
+ return `${node.text.startsWith('::') ? '::' : ''}${parts.join('::')}`;
110
115
  }
111
116
  return node.text;
112
117
  }
@@ -136,7 +141,7 @@ function buildWildcardPath(node) {
136
141
  continue;
137
142
  if (child.type === 'scoped_identifier')
138
143
  return buildScopedPath(child);
139
- if (child.type === 'identifier')
144
+ if (['identifier', 'crate', 'self', 'super'].includes(child.type))
140
145
  return child.text;
141
146
  }
142
147
  return '';
@@ -22,6 +22,8 @@ import type { ParsedFile, ScopeId, SymbolDefinition } from '../../../../_shared/
22
22
  export declare function rustIsGlobalNameFallbackPlausible(ctx: {
23
23
  readonly callerParsed: ParsedFile;
24
24
  readonly candidate: SymbolDefinition;
25
+ readonly resolutionConfig?: unknown;
26
+ readonly parsedFileOf?: (filePath: string) => ParsedFile | undefined;
25
27
  readonly site: {
26
28
  readonly name: string;
27
29
  readonly rawQualifiedName?: string;
@@ -18,6 +18,7 @@
18
18
  * `a::b`). `crate::` names the root; `self::` and `super::` resolve relative
19
19
  * to the caller's module before comparison.
20
20
  */
21
+ import { rustFilesShareCargoTarget, rustImportNamesCargoRoot, rustIsExclusiveCargoRoot, rustImportReachesCargoTarget, rustCargoPubliclyReexports, } from './cargo-targets.js';
21
22
  import { modulePathReaches, stripExtension, } from '../../scope-resolution/utils/name-fallback-visibility.js';
22
23
  /** File-stems that name their PARENT directory as the module, not themselves. */
23
24
  const RUST_DIRECTORY_MODULE_STEMS = new Set(['mod', 'lib', 'main']);
@@ -26,6 +27,16 @@ const RUST_DIRECTORY_MODULE_STEMS = new Set(['mod', 'lib', 'main']);
26
27
  const RUST_CRATE_ROOT_DIRS = new Set(['src', 'tests', 'benches', 'examples']);
27
28
  /** Path prefixes of a `use` that name a root rather than a module segment. */
28
29
  const RUST_USE_ROOT_PREFIXES = new Set(['crate', '$crate']);
30
+ const RUST_TYPE_NAMESPACE_KINDS = new Set([
31
+ 'Namespace',
32
+ 'Class',
33
+ 'Struct',
34
+ 'Enum',
35
+ 'Trait',
36
+ 'Interface',
37
+ 'TypeAlias',
38
+ 'Union',
39
+ ]);
29
40
  // One scope lookup per immutable parsed-file snapshot, not per fallback site.
30
41
  // Weak keys release both the snapshot and its index at the end of ingestion.
31
42
  const scopeLookupByFile = new WeakMap();
@@ -92,12 +103,64 @@ export function rustIsGlobalNameFallbackPlausible(ctx) {
92
103
  if (ctx.site.rawQualifiedName !== undefined)
93
104
  return true;
94
105
  const candidateModule = rustModulePathOf(ctx.candidate.filePath);
95
- // A candidate whose file maps to no module path (a crate root reduced to '')
96
- // is not something this rule can speak about; allow the labeled edge rather
97
- // than refuse on an unanswered question.
98
- if (candidateModule === '')
99
- return true;
106
+ const sharesTarget = rustFilesShareCargoTarget(ctx.resolutionConfig, ctx.callerParsed.filePath, ctx.candidate.filePath);
107
+ const separateRoot = sharesTarget === false &&
108
+ rustIsExclusiveCargoRoot(ctx.resolutionConfig, ctx.candidate.filePath);
109
+ // Cargo membership does not establish cross-file lexical visibility.
110
+ // Root candidates must also pass the import checks below; same-file and
111
+ // explicitly qualified calls have already been handled above.
100
112
  const candidateName = rustSimpleNameOf(ctx.candidate);
113
+ const exportModules = new Set([(ctx.candidate.namespacePrefix ?? '').replaceAll('.', '::')]);
114
+ const candidateParsed = sharesTarget === false ? ctx.parsedFileOf?.(ctx.candidate.filePath) : undefined;
115
+ if (candidateParsed !== undefined) {
116
+ const moduleByScope = new Map();
117
+ const moduleScopes = new Set();
118
+ for (const scope of candidateParsed.scopes) {
119
+ const parent = scope.parent === null ? '' : (moduleByScope.get(scope.parent) ?? '');
120
+ const own = scope.kind === 'Namespace'
121
+ ? scope.ownedDefs.find((def) => def.type === 'Namespace')?.qualifiedName
122
+ : undefined;
123
+ moduleByScope.set(scope.id, [parent, own].filter(Boolean).join('::'));
124
+ if (scope.kind === 'Namespace' || scope.kind === 'Module')
125
+ moduleScopes.add(scope.id);
126
+ }
127
+ // Follow same-file re-exports without confusing the defining module with
128
+ // the module an importer sees. Each iteration adds a known module scope,
129
+ // so cycles terminate. Cargo's AST snapshot supplies public visibility,
130
+ // which the coarse parsed reexport/wildcard kind does not preserve.
131
+ let changed = true;
132
+ while (changed) {
133
+ changed = false;
134
+ for (const imp of candidateParsed.parsedImports) {
135
+ if (imp.declaredAtScope === undefined || !moduleScopes.has(imp.declaredAtScope))
136
+ continue;
137
+ if (imp.kind !== 'wildcard' &&
138
+ (imp.kind !== 'reexport' ||
139
+ imp.localName !== candidateName ||
140
+ imp.importedName !== candidateName))
141
+ continue;
142
+ if (imp.targetRaw.startsWith('::'))
143
+ continue;
144
+ const owner = moduleByScope.get(imp.declaredAtScope);
145
+ if (!rustCargoPubliclyReexports(ctx.resolutionConfig, ctx.candidate.filePath, owner, imp.targetRaw, imp.kind, imp.kind === 'wildcard' ? '*' : imp.localName))
146
+ continue;
147
+ const parts = imp.targetRaw.split('::').filter(Boolean);
148
+ if (imp.kind !== 'wildcard')
149
+ parts.pop();
150
+ const base = parts[0] === 'self' || parts[0] === 'super' ? owner.split('::').filter(Boolean) : [];
151
+ if (parts[0] === 'crate' || parts[0] === 'self')
152
+ parts.shift();
153
+ while (parts[0] === 'super') {
154
+ base.pop();
155
+ parts.shift();
156
+ }
157
+ if (exportModules.has([...base, ...parts].join('::')) && !exportModules.has(owner)) {
158
+ exportModules.add(owner);
159
+ changed = true;
160
+ }
161
+ }
162
+ }
163
+ }
101
164
  // Imports are lexical evidence, not a file-wide allowlist. Legacy/synthetic
102
165
  // imports without a scope receipt retain the previous conservative behavior.
103
166
  let visibleScopes;
@@ -117,24 +180,95 @@ export function rustIsGlobalNameFallbackPlausible(ctx) {
117
180
  current = scope.parent;
118
181
  }
119
182
  }
120
- for (const imp of ctx.callerParsed.parsedImports) {
121
- if (imp.declaredAtScope !== undefined &&
122
- visibleScopes !== undefined &&
123
- !visibleScopes.has(imp.declaredAtScope))
124
- continue;
183
+ const visibleImports = ctx.callerParsed.parsedImports.filter((imp) => imp.declaredAtScope === undefined ||
184
+ visibleScopes === undefined ||
185
+ visibleScopes.has(imp.declaredAtScope));
186
+ const scopeRanks = new Map([...(visibleScopes ?? [])].map((scope, rank) => [scope, rank]));
187
+ const namesCandidateRoot = (module, entryOnly) => {
188
+ const pending = [module];
189
+ const seen = new Set();
190
+ while (pending.length > 0) {
191
+ const name = pending.pop();
192
+ const parts = name.split('::').filter(Boolean);
193
+ const head = parts[0];
194
+ const bindingName = name.startsWith('::') ? name : head;
195
+ if (!bindingName || seen.has(bindingName))
196
+ continue;
197
+ seen.add(bindingName);
198
+ const aliases = visibleImports.filter((imported) => 'localName' in imported && imported.localName === bindingName);
199
+ const rank = (imported) => imported.declaredAtScope === undefined
200
+ ? Infinity
201
+ : (scopeRanks.get(imported.declaredAtScope) ?? Infinity);
202
+ const nearest = aliases.length > 0 ? Math.min(...aliases.map(rank)) : Infinity;
203
+ let localTypeRank = Infinity;
204
+ for (const [scopeId, depth] of scopeRanks) {
205
+ const bindings = scopeLookupByFile
206
+ .get(ctx.callerParsed)
207
+ ?.get(scopeId)
208
+ ?.bindings.get(bindingName);
209
+ if (bindings?.some((binding) => binding.origin === 'local' && RUST_TYPE_NAMESPACE_KINDS.has(binding.def.type))) {
210
+ localTypeRank = depth;
211
+ break;
212
+ }
213
+ }
214
+ // A local module/type shadows the extern prelude, but a nearer import
215
+ // can shadow that declaration. Value-namespace functions do not block it.
216
+ if (localTypeRank !== Infinity && localTypeRank <= nearest)
217
+ continue;
218
+ if (aliases.length > 0) {
219
+ const destinations = new Set(aliases
220
+ .filter((imported) => rank(imported) === nearest)
221
+ .map((imported) => imported.targetRaw));
222
+ if (destinations.size !== 1)
223
+ continue;
224
+ const destination = [...destinations][0];
225
+ if (destination !== bindingName) {
226
+ pending.push([destination, ...parts.slice(1)].join('::'));
227
+ continue;
228
+ }
229
+ }
230
+ // A root FILE can also contain inline modules. Its Cargo identity names
231
+ // the crate, while the scope model supplies the member's module suffix.
232
+ if (rustImportNamesCargoRoot(ctx.resolutionConfig, ctx.callerParsed.filePath, ctx.candidate.filePath, head)) {
233
+ if (exportModules.has(parts.slice(1).join('::')))
234
+ return 'root';
235
+ continue;
236
+ }
237
+ if (!entryOnly &&
238
+ rustImportReachesCargoTarget(ctx.resolutionConfig, ctx.callerParsed.filePath, ctx.candidate.filePath, name))
239
+ return 'module';
240
+ }
241
+ return false;
242
+ };
243
+ for (const imp of visibleImports) {
244
+ let importedRoot = false;
125
245
  // `crate::` is the caller's crate. A same trailing module in another
126
246
  // workspace crate is a different item and cannot authorize the guess.
127
247
  if (imp.targetRaw === 'crate' || imp.targetRaw.startsWith('crate::')) {
248
+ if (sharesTarget === false)
249
+ continue;
128
250
  const callerRoot = rustCrateRootOf(ctx.callerParsed.filePath);
129
251
  const candidateRoot = rustCrateRootOf(ctx.candidate.filePath);
130
- if (callerRoot !== '' && candidateRoot !== '' && callerRoot !== candidateRoot)
252
+ if (sharesTarget === undefined &&
253
+ callerRoot !== '' &&
254
+ candidateRoot !== '' &&
255
+ callerRoot !== candidateRoot)
256
+ continue;
257
+ }
258
+ if (sharesTarget === false) {
259
+ const module = imp.kind === 'wildcard'
260
+ ? imp.targetRaw
261
+ : imp.targetRaw.slice(0, Math.max(0, imp.targetRaw.lastIndexOf('::')));
262
+ const reached = namesCandidateRoot(module, separateRoot);
263
+ if (!reached)
131
264
  continue;
265
+ importedRoot = reached === 'root';
132
266
  }
133
267
  const usePath = rustUsePathOf(imp.targetRaw, ctx.callerParsed.filePath);
134
268
  // Only a glob introduces every bare item of a module. A named import must
135
269
  // match both the candidate's original name and the call's local spelling.
136
270
  if (imp.kind === 'wildcard') {
137
- if (modulePathReaches(usePath, candidateModule))
271
+ if (importedRoot || candidateModule === '' || modulePathReaches(usePath, candidateModule))
138
272
  return true;
139
273
  continue;
140
274
  }
@@ -147,9 +281,14 @@ export function rustIsGlobalNameFallbackPlausible(ctx) {
147
281
  // An alias authorizes only the local spelling checked above.
148
282
  if (importedNameOf(imp) !== candidateName)
149
283
  continue;
284
+ // A different target may import the library crate's root exports. Even
285
+ // when that root has no path segment to compare, a named import must name
286
+ // THIS callable: `use std::fmt` cannot revive a rejected `crate::helper`.
287
+ const parent = usePath.slice(0, Math.max(0, usePath.lastIndexOf('::')));
288
+ if (importedRoot || candidateModule === '')
289
+ return true;
150
290
  if (modulePathReaches(usePath, candidateModule))
151
291
  return true;
152
- const parent = usePath.slice(0, Math.max(0, usePath.lastIndexOf('::')));
153
292
  if (parent !== '' && modulePathReaches(parent, candidateModule))
154
293
  return true;
155
294
  }
@@ -10,6 +10,7 @@ import { isClassLike, findClassBindingInScope, resolveAmbiguousInheritanceBaseVi
10
10
  import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
11
11
  import { generateId } from '../../../../lib/utils.js';
12
12
  import { rustIsGlobalNameFallbackPlausible } from './name-fallback-visibility.js';
13
+ import { loadRustCargoTargets } from './cargo-targets.js';
13
14
  /**
14
15
  * Emit Rust `S IMPLEMENTS T` edges from `impl T for S` trait implementations.
15
16
  *
@@ -131,6 +132,7 @@ export const rustScopeResolver = {
131
132
  language: SupportedLanguages.Rust,
132
133
  languageProvider: rustProvider,
133
134
  importEdgeReason: 'rust-scope: use',
135
+ loadResolutionConfig: loadRustCargoTargets,
134
136
  resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => resolveRustImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig),
135
137
  mergeBindings: (existing, incoming, scopeId) => rustMergeBindings(existing, incoming, scopeId),
136
138
  arityCompatibility: (callsite, def) => rustArityCompatibility(def, callsite),
@@ -819,6 +819,8 @@ export interface ScopeResolver {
819
819
  readonly isGlobalNameFallbackPlausible?: (ctx: {
820
820
  readonly callerParsed: ParsedFile;
821
821
  readonly candidate: SymbolDefinition;
822
+ /** Opaque workspace metadata from this provider's loadResolutionConfig. */
823
+ readonly resolutionConfig?: unknown;
822
824
  readonly parsedFileOf: (filePath: string) => ParsedFile | undefined;
823
825
  /**
824
826
  * Raw source of any parsed file, for languages whose visibility rule needs
@@ -37,6 +37,7 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
37
37
  /** Per-language veto on a name guess — see
38
38
  * `ScopeResolver.isGlobalNameFallbackPlausible`. */
39
39
  readonly isGlobalNameFallbackPlausible?: ScopeResolver['isGlobalNameFallbackPlausible'];
40
+ readonly resolutionConfig?: unknown;
40
41
  /** Raw source lookup handed to `isGlobalNameFallbackPlausible` (optional). */
41
42
  readonly sourceTextOf?: (filePath: string) => string | undefined;
42
43
  /** When true, `Type(...)` constructor calls link to the Class def
@@ -469,6 +469,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
469
469
  options.isGlobalNameFallbackPlausible?.({
470
470
  callerParsed: parsed,
471
471
  candidate: vetoCandidate,
472
+ resolutionConfig: options.resolutionConfig,
472
473
  parsedFileOf: parsedFileByPath(),
473
474
  sourceTextOf: options.sourceTextOf,
474
475
  site: {
@@ -695,6 +695,7 @@ export function runScopeResolution(input, provider) {
695
695
  allowGlobalFallback: provider.allowGlobalFreeCallFallback === true,
696
696
  language: provider.language,
697
697
  isGlobalNameFallbackPlausible: provider.isGlobalNameFallbackPlausible,
698
+ resolutionConfig,
698
699
  sourceTextOf: provider.isGlobalNameFallbackPlausible !== undefined
699
700
  ? (filePath) => getFileContents().get(filePath)
700
701
  : undefined,
@@ -758,7 +758,13 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
758
758
  // #3190. Old durable ParsedFiles lack the facts needed for scoped binding;
759
759
  // invalidate both stores so warm indexing actually applies the correction.
760
760
  // origin/main took 98 for #3219; 99 is the next free value.
761
- const SCHEMA_BUMP = 99;
761
+ // v100 (#3253): Rust import captures preserve the leading `::` that selects
762
+ // the extern prelude. Old warm captures erase it and cannot distinguish an
763
+ // absolute library import from a same-named local module. Reparse both stores.
764
+ // v101 (#3294 review): Rust bare-keyword glob imports retain crate/self/super
765
+ // instead of an empty target path; restricted pub(...) imports are no longer
766
+ // captured as unrestricted reexports. Re-extract both facts on warm indexes.
767
+ const SCHEMA_BUMP = 101;
762
768
  const GITNEXUS_PKG_VERSION = (() => {
763
769
  try {
764
770
  // package.json sits at gitnexus/package.json — two levels up from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.13-rc.7",
3
+ "version": "1.6.13-rc.8",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -92,6 +92,7 @@
92
92
  "pino": "^10.3.1",
93
93
  "pino-pretty": "^13.1.3",
94
94
  "proxy-addr": "^2.0.7",
95
+ "smol-toml": "^1.8.0",
95
96
  "tree-sitter": "0.21.1",
96
97
  "tree-sitter-c-sharp": "0.23.1",
97
98
  "tree-sitter-cpp": "0.23.2",
@@ -305,6 +305,9 @@ const NATIVE_ADDON_SMOKE = [
305
305
  // Filesystem behavior tests — exercise operations that vary across
306
306
  // platforms (CRLF, symlinks, permissions, temp dirs)
307
307
  const FILESYSTEM = [
308
+ // Cargo membership uses path normalization, descriptor validation, symlinks,
309
+ // and Rust native parsing (including long Windows source strings).
310
+ 'test/unit/scope-resolution/rust-cargo-targets.test.ts',
308
311
  // The durable ParsedFile store's prune tolerates a chunk directory it cannot
309
312
  // delete (#3204). The failures that motivate it — held handles, read-only
310
313
  // mounts — are Windows- and macOS-flavored, and the permission-based case