ucn 5.4.2 → 5.5.1
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/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +529 -31
- package/core/callers.js +12228 -921
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +3026 -0
- package/core/rust-modules.js +203 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1185 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/core/imports.js
CHANGED
|
@@ -78,6 +78,14 @@ function resolveImport(importPath, fromFile, config = {}) {
|
|
|
78
78
|
// Strip query strings (e.g., ?raw, ?url)
|
|
79
79
|
importPath = importPath.split('?')[0];
|
|
80
80
|
|
|
81
|
+
// C/C++ angle-bracket includes search the -I directories (never the
|
|
82
|
+
// including file's directory); a project header found there is part of
|
|
83
|
+
// the translation unit exactly like a quoted include.
|
|
84
|
+
if ((config.language === 'c' || config.language === 'cpp') &&
|
|
85
|
+
!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
86
|
+
return resolveCIncludeFromDirectories(importPath, fromFile, config);
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
// External packages (not relative or alias)
|
|
82
90
|
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
83
91
|
// Check aliases
|
|
@@ -124,6 +132,9 @@ function resolveImport(importPath, fromFile, config = {}) {
|
|
|
124
132
|
// Package self-reference (import own package by name)
|
|
125
133
|
const selfResolved = resolveSelfReference(importPath, fromDir, config);
|
|
126
134
|
if (selfResolved) return selfResolved;
|
|
135
|
+
// Another package of the same repository (fix #397).
|
|
136
|
+
const workspaceResolved = resolveWorkspacePackage(importPath, config);
|
|
137
|
+
if (workspaceResolved) return workspaceResolved;
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
// Check Go module imports
|
|
@@ -154,6 +165,15 @@ function resolveImport(importPath, fromFile, config = {}) {
|
|
|
154
165
|
const srcPath = path.join(config.root, 'src', modulePath);
|
|
155
166
|
const srcResolved = resolveFilePath(srcPath, getExtensions('python'));
|
|
156
167
|
if (srcResolved) return srcResolved;
|
|
168
|
+
// Nested source root (fix #366): an application under a
|
|
169
|
+
// subdirectory (`backend/app/...`) runs with that subdirectory on
|
|
170
|
+
// sys.path, so `from app.core import x` means backend/app/core.
|
|
171
|
+
// The nearest ancestor of the importing file that is NOT itself a
|
|
172
|
+
// package and directly contains the module's first segment is
|
|
173
|
+
// the source root the interpreter would use. Package directories
|
|
174
|
+
// are skipped so a sibling module never answers an absolute import.
|
|
175
|
+
const nested = _resolvePythonNestedRoot(importPath, fromDir, config);
|
|
176
|
+
if (nested) return nested;
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
return null; // External package
|
|
@@ -186,34 +206,53 @@ function resolveImport(importPath, fromFile, config = {}) {
|
|
|
186
206
|
if (direct) return direct;
|
|
187
207
|
|
|
188
208
|
// C/C++ quoted includes may be rooted at compiler -I/-iquote paths rather
|
|
189
|
-
// than the importing file.
|
|
190
|
-
// build metadata when present; unresolved system includes remain external.
|
|
209
|
+
// than the importing file.
|
|
191
210
|
if (config.language === 'c' || config.language === 'cpp') {
|
|
192
|
-
|
|
193
|
-
const includeName = normalizedPath.replace(/^\.\//, '');
|
|
194
|
-
const includeDirs = includeDirectoriesForFile(fromFile, config.root);
|
|
195
|
-
for (const configured of config.includePaths || []) {
|
|
196
|
-
if (typeof configured !== 'string' || !configured.trim()) continue;
|
|
197
|
-
includeDirs.push(path.isAbsolute(configured)
|
|
198
|
-
? configured
|
|
199
|
-
: path.resolve(config.root || fromDir, configured));
|
|
200
|
-
}
|
|
201
|
-
// Header-only/source-distribution projects commonly omit a generated
|
|
202
|
-
// compile_commands.json but still use the conventional public
|
|
203
|
-
// `include/` root (`#include "fmt/format.h"`). These are project-owned
|
|
204
|
-
// files, not external packages. Try explicit compiler metadata first,
|
|
205
|
-
// then deterministic project roots; never search arbitrary parents.
|
|
206
|
-
if (config.root) {
|
|
207
|
-
includeDirs.push(config.root, path.join(config.root, 'include'));
|
|
208
|
-
}
|
|
209
|
-
for (const includeDir of [...new Set(includeDirs)]) {
|
|
210
|
-
const candidate = resolveFilePath(path.resolve(includeDir, includeName), extensions);
|
|
211
|
-
if (candidate) return candidate;
|
|
212
|
-
}
|
|
211
|
+
return resolveCIncludeFromDirectories(normalizedPath.replace(/^\.\//, ''), fromFile, config);
|
|
213
212
|
}
|
|
214
213
|
return null;
|
|
215
214
|
}
|
|
216
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Search the compiler include directories for a C/C++ include name.
|
|
218
|
+
* compile_commands.json is the authoritative build metadata when present;
|
|
219
|
+
* configured include paths follow, then the deterministic project roots.
|
|
220
|
+
* Unresolved system includes remain external.
|
|
221
|
+
*/
|
|
222
|
+
function resolveCIncludeFromDirectories(includeName, fromFile, config) {
|
|
223
|
+
const { includeDirectoriesForFile } = require('./compilation-database');
|
|
224
|
+
const extensions = config.extensions || getExtensions(config.language);
|
|
225
|
+
const includeDirs = includeDirectoriesForFile(fromFile, config.root);
|
|
226
|
+
for (const configured of config.includePaths || []) {
|
|
227
|
+
if (typeof configured !== 'string' || !configured.trim()) continue;
|
|
228
|
+
includeDirs.push(path.isAbsolute(configured)
|
|
229
|
+
? configured
|
|
230
|
+
: path.resolve(config.root || path.dirname(fromFile), configured));
|
|
231
|
+
}
|
|
232
|
+
// Header-only/source-distribution projects commonly omit a generated
|
|
233
|
+
// compile_commands.json but still use the conventional public
|
|
234
|
+
// `include/` root (`#include "fmt/format.h"`). These are project-owned
|
|
235
|
+
// files, not external packages. Try explicit compiler metadata first,
|
|
236
|
+
// then deterministic project roots; never search arbitrary parents.
|
|
237
|
+
if (config.root) {
|
|
238
|
+
includeDirs.push(config.root, path.join(config.root, 'include'));
|
|
239
|
+
}
|
|
240
|
+
// One import-graph build probes the same few directories for the same
|
|
241
|
+
// system headers from every file (`<stdio.h>`); the filesystem does not
|
|
242
|
+
// change within it, so a caller-owned memo answers repeats (fix #365).
|
|
243
|
+
const dirs = [...new Set(includeDirs)];
|
|
244
|
+
const memo = config.probeCache;
|
|
245
|
+
const memoKey = memo ? `${includeName}\0${extensions.join(',')}\0${dirs.join('\0')}` : null;
|
|
246
|
+
if (memo && memo.has(memoKey)) return memo.get(memoKey);
|
|
247
|
+
let found = null;
|
|
248
|
+
for (const includeDir of dirs) {
|
|
249
|
+
const candidate = resolveFilePath(path.resolve(includeDir, includeName), extensions);
|
|
250
|
+
if (candidate) { found = candidate; break; }
|
|
251
|
+
}
|
|
252
|
+
if (memo) memo.set(memoKey, found);
|
|
253
|
+
return found;
|
|
254
|
+
}
|
|
255
|
+
|
|
217
256
|
// Cache for Go module paths
|
|
218
257
|
const goModuleCache = new Map();
|
|
219
258
|
|
|
@@ -331,19 +370,25 @@ const cargoCache = new Map();
|
|
|
331
370
|
// Workspace crate registry (fix #258): Cargo [package] name → crate source
|
|
332
371
|
// root for EVERY crate in the project tree, so cross-crate workspace imports
|
|
333
372
|
// (`use clap::Command` from clap_bench/) resolve like own-package imports.
|
|
334
|
-
// One bounded scan per project root
|
|
373
|
+
// One bounded scan per project root. The scan runs at build time and its
|
|
374
|
+
// manifests are persisted in the index cache (fix #372): a cache-loaded index
|
|
375
|
+
// seeds the registry instead of walking the tree on the first query.
|
|
335
376
|
const workspaceCrateCache = new Map();
|
|
336
377
|
const _WORKSPACE_SCAN_PRUNE = new Set([
|
|
337
378
|
'node_modules', '.git', 'target', 'vendor', 'dist', 'build', '.ucn-cache',
|
|
338
379
|
]);
|
|
380
|
+
const _WORKSPACE_SCAN_MAX_DEPTH = 6;
|
|
339
381
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Directories holding a Cargo.toml, in the registry scan's order (depth-first,
|
|
384
|
+
* directory-entry order). Bounded by depth and the prune set.
|
|
385
|
+
* @param {string} projectRoot
|
|
386
|
+
* @returns {string[]} absolute directories
|
|
387
|
+
*/
|
|
388
|
+
function scanWorkspaceManifestDirs(projectRoot) {
|
|
389
|
+
const dirs = [];
|
|
345
390
|
const walk = (dir, depth) => {
|
|
346
|
-
if (depth >
|
|
391
|
+
if (depth > _WORKSPACE_SCAN_MAX_DEPTH) return;
|
|
347
392
|
let entries;
|
|
348
393
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
349
394
|
catch { return; }
|
|
@@ -352,18 +397,125 @@ function workspaceCrateRegistry(projectRoot) {
|
|
|
352
397
|
if (_WORKSPACE_SCAN_PRUNE.has(e.name) || e.name.startsWith('.')) continue;
|
|
353
398
|
walk(path.join(dir, e.name), depth + 1);
|
|
354
399
|
} else if (e.name === 'Cargo.toml') {
|
|
355
|
-
|
|
356
|
-
if (info && info.packageName && !registry.has(info.packageName)) {
|
|
357
|
-
registry.set(info.packageName, info);
|
|
358
|
-
}
|
|
400
|
+
dirs.push(dir);
|
|
359
401
|
}
|
|
360
402
|
}
|
|
361
403
|
};
|
|
362
404
|
walk(projectRoot, 0);
|
|
405
|
+
return dirs;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Order-free fingerprint of the Cargo.toml paths file discovery reports, so
|
|
410
|
+
* a staleness check (which walks the tree for new files anyway) sees a
|
|
411
|
+
* manifest appear or disappear without a second walk.
|
|
412
|
+
* @param {string[]} relativeManifestPaths
|
|
413
|
+
*/
|
|
414
|
+
function manifestSetFingerprint(relativeManifestPaths) {
|
|
415
|
+
const sorted = [...relativeManifestPaths].sort((x, y) => (x < y ? -1 : x > y ? 1 : 0));
|
|
416
|
+
return require('crypto').createHash('md5').update(sorted.join('\0')).digest('hex');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function _registryFromManifestDirs(dirs) {
|
|
420
|
+
const registry = new Map();
|
|
421
|
+
for (const dir of dirs) {
|
|
422
|
+
const info = findCargoRoot(dir);
|
|
423
|
+
if (info && info.packageName && !registry.has(info.packageName)) {
|
|
424
|
+
registry.set(info.packageName, info);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return registry;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function workspaceCrateRegistry(projectRoot) {
|
|
431
|
+
if (workspaceCrateCache.has(projectRoot)) {
|
|
432
|
+
return workspaceCrateCache.get(projectRoot);
|
|
433
|
+
}
|
|
434
|
+
const registry = _registryFromManifestDirs(scanWorkspaceManifestDirs(projectRoot));
|
|
363
435
|
workspaceCrateCache.set(projectRoot, registry);
|
|
364
436
|
return registry;
|
|
365
437
|
}
|
|
366
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Snapshot of the project's Cargo manifests for the index cache: every
|
|
441
|
+
* manifest the registry scan reaches, with its stat identity and the parsed
|
|
442
|
+
* crate facts findCargoRoot derives from it. Also (re)seeds the in-process
|
|
443
|
+
* caches from a fresh read.
|
|
444
|
+
* @param {string} projectRoot
|
|
445
|
+
* @returns {Array<{dir, mtime, size, packageName, srcDir, targetDirs}>} project-relative
|
|
446
|
+
*/
|
|
447
|
+
function snapshotWorkspaceManifests(projectRoot) {
|
|
448
|
+
resetCargoCaches(projectRoot);
|
|
449
|
+
const dirs = scanWorkspaceManifestDirs(projectRoot);
|
|
450
|
+
const rel = p => path.relative(projectRoot, p);
|
|
451
|
+
const out = [];
|
|
452
|
+
for (const dir of dirs) {
|
|
453
|
+
let stat;
|
|
454
|
+
try { stat = fs.statSync(path.join(dir, 'Cargo.toml')); } catch { continue; }
|
|
455
|
+
const info = findCargoRoot(dir);
|
|
456
|
+
if (!info || info.root !== dir) continue;
|
|
457
|
+
out.push({
|
|
458
|
+
dir: rel(dir), mtime: stat.mtimeMs, size: stat.size,
|
|
459
|
+
packageName: info.packageName,
|
|
460
|
+
srcDir: rel(info.srcDir),
|
|
461
|
+
targetDirs: info.targetDirs.map(rel),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
workspaceCrateCache.set(projectRoot, _registryFromManifestDirs(dirs));
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Seed the in-process manifest caches from a persisted snapshot, so a
|
|
470
|
+
* cache-loaded index resolves workspace crates without reading the tree.
|
|
471
|
+
*/
|
|
472
|
+
function seedWorkspaceManifests(projectRoot, manifests) {
|
|
473
|
+
if (!Array.isArray(manifests)) return;
|
|
474
|
+
const dirs = [];
|
|
475
|
+
for (const m of manifests) {
|
|
476
|
+
const dir = path.join(projectRoot, m.dir);
|
|
477
|
+
dirs.push(dir);
|
|
478
|
+
cargoCache.set(dir, {
|
|
479
|
+
root: dir,
|
|
480
|
+
srcDir: path.join(projectRoot, m.srcDir),
|
|
481
|
+
packageName: m.packageName,
|
|
482
|
+
targetDirs: m.targetDirs.map(t => path.join(projectRoot, t)),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
workspaceCrateCache.set(projectRoot, _registryFromManifestDirs(dirs));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Whether a persisted manifest snapshot still describes the tree: every
|
|
490
|
+
* manifest keeps its size and mtime, and (when discovery results are given)
|
|
491
|
+
* discovery reports the same set of Cargo.toml paths as when it was taken.
|
|
492
|
+
* @param {string} projectRoot
|
|
493
|
+
* @param {Array} manifests - persisted snapshot
|
|
494
|
+
* @param {string[]|null} [discovered] - project-relative Cargo.toml paths discovery saw now
|
|
495
|
+
* @param {string|null} [seenFingerprint] - manifestSetFingerprint at snapshot time
|
|
496
|
+
*/
|
|
497
|
+
function workspaceManifestsCurrent(projectRoot, manifests, discovered = null, seenFingerprint = null) {
|
|
498
|
+
if (!Array.isArray(manifests)) return false;
|
|
499
|
+
for (const m of manifests) {
|
|
500
|
+
let stat;
|
|
501
|
+
try { stat = fs.statSync(path.join(projectRoot, m.dir, 'Cargo.toml')); } catch { return false; }
|
|
502
|
+
if (stat.size !== m.size || stat.mtimeMs !== m.mtime) return false;
|
|
503
|
+
}
|
|
504
|
+
if (discovered && seenFingerprint &&
|
|
505
|
+
manifestSetFingerprint(discovered) !== seenFingerprint) return false;
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Drop in-process manifest facts below a project root (a manifest changed). */
|
|
510
|
+
function resetCargoCaches(projectRoot) {
|
|
511
|
+
workspaceCrateCache.delete(projectRoot);
|
|
512
|
+
rustImportMemo.clear();
|
|
513
|
+
const prefix = projectRoot.endsWith(path.sep) ? projectRoot : projectRoot + path.sep;
|
|
514
|
+
for (const key of [...cargoCache.keys()]) {
|
|
515
|
+
if (key === projectRoot || key.startsWith(prefix)) cargoCache.delete(key);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
367
519
|
/**
|
|
368
520
|
* Find the nearest Cargo.toml and return the crate's source root
|
|
369
521
|
* @param {string} startDir - Directory to start searching from
|
|
@@ -451,13 +603,30 @@ function rustPathHasExactCase(base, file) {
|
|
|
451
603
|
// Check directory entries, rather than realpath, so legitimate symlinked
|
|
452
604
|
// modules keep their declared spelling and remain resolvable.
|
|
453
605
|
let current = base;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
606
|
+
for (const part of path.relative(base, file).split(path.sep)) {
|
|
607
|
+
const entries = rustDirEntries(current);
|
|
608
|
+
if (!entries || !entries.has(part)) return false;
|
|
609
|
+
current = path.join(current, part);
|
|
610
|
+
}
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Directory listings the case check reads, memoized with the resolution memo
|
|
615
|
+
// below (fix #375: every module probe re-read its directories; ~40% of a
|
|
616
|
+
// large workspace's import graph build).
|
|
617
|
+
const rustDirEntriesMemo = new Map();
|
|
618
|
+
|
|
619
|
+
function rustDirEntries(dir) {
|
|
620
|
+
let entries = rustDirEntriesMemo.get(dir);
|
|
621
|
+
if (entries === undefined) {
|
|
622
|
+
try {
|
|
623
|
+
entries = new Set(fs.readdirSync(dir));
|
|
624
|
+
} catch {
|
|
625
|
+
entries = null;
|
|
458
626
|
}
|
|
459
|
-
|
|
460
|
-
}
|
|
627
|
+
rustDirEntriesMemo.set(dir, entries);
|
|
628
|
+
}
|
|
629
|
+
return entries;
|
|
461
630
|
}
|
|
462
631
|
|
|
463
632
|
function resolveRustModulePath(dir, segments) {
|
|
@@ -486,7 +655,27 @@ function resolveRustModulePath(dir, segments) {
|
|
|
486
655
|
* @param {string} projectRoot - Project root directory
|
|
487
656
|
* @returns {string|null}
|
|
488
657
|
*/
|
|
658
|
+
// Resolution memo (fix #372): a query resolves the same use paths from the
|
|
659
|
+
// same files many times (receiver typing, module producers, glob walks), each
|
|
660
|
+
// probing the filesystem. Answers depend only on the tree, so they are
|
|
661
|
+
// memoized until the next build (resetRustResolveMemo).
|
|
662
|
+
const rustImportMemo = new Map();
|
|
663
|
+
|
|
489
664
|
function resolveRustImport(importPath, fromFile, projectRoot) {
|
|
665
|
+
const key = `${projectRoot}\0${fromFile}\0${importPath}`;
|
|
666
|
+
if (rustImportMemo.has(key)) return rustImportMemo.get(key);
|
|
667
|
+
const resolved = resolveRustImportUncached(importPath, fromFile, projectRoot);
|
|
668
|
+
rustImportMemo.set(key, resolved);
|
|
669
|
+
return resolved;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Forget memoized Rust import resolutions (the tree may have changed). */
|
|
673
|
+
function resetRustResolveMemo() {
|
|
674
|
+
rustImportMemo.clear();
|
|
675
|
+
rustDirEntriesMemo.clear();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function resolveRustImportUncached(importPath, fromFile, projectRoot) {
|
|
490
679
|
const fromDir = path.dirname(fromFile);
|
|
491
680
|
|
|
492
681
|
// crate:: paths - resolve from the crate's src/ directory, or from a
|
|
@@ -626,6 +815,55 @@ function resolveRustImport(importPath, fromFile, projectRoot) {
|
|
|
626
815
|
return null;
|
|
627
816
|
}
|
|
628
817
|
|
|
818
|
+
/**
|
|
819
|
+
* The file that holds a Rust module's own items (fix #368): `self` is the
|
|
820
|
+
* current file, `super` (repeatable) the parent module's file, `crate` the
|
|
821
|
+
* crate root, and any other path the module file resolveRustImport finds.
|
|
822
|
+
* Used to anchor glob imports (`use super::*`) and module-qualified calls
|
|
823
|
+
* (`crate::f()`), whose items live IN the module file, not in a child file.
|
|
824
|
+
* @returns {string|null}
|
|
825
|
+
*/
|
|
826
|
+
function resolveRustModuleFile(modulePath, fromFile, projectRoot, inlineDepth = 0) {
|
|
827
|
+
let spec = String(modulePath || '');
|
|
828
|
+
if (!spec) return null;
|
|
829
|
+
if (spec === 'self') return fromFile;
|
|
830
|
+
// Inside `mod tests { use super::*; }` the first `super` hops out of the
|
|
831
|
+
// inline module, which still lives in this file.
|
|
832
|
+
let depth = inlineDepth;
|
|
833
|
+
while (depth > 0 && (spec === 'super' || spec.startsWith('super::'))) {
|
|
834
|
+
spec = spec === 'super' ? 'self' : spec.slice('super::'.length);
|
|
835
|
+
depth--;
|
|
836
|
+
}
|
|
837
|
+
if (spec === 'self') return fromFile;
|
|
838
|
+
if (depth > 0) return null; // crate/self paths from inside an inline module
|
|
839
|
+
if (spec.startsWith('self::')) return resolveRustImport(spec, fromFile, projectRoot);
|
|
840
|
+
const fromDir = path.dirname(fromFile);
|
|
841
|
+
if (spec === 'crate') {
|
|
842
|
+
const cargo = findCargoRoot(fromDir);
|
|
843
|
+
if (!cargo) return null;
|
|
844
|
+
const candidates = [cargo.srcDir, ...(cargo.targetDirs || [])]
|
|
845
|
+
.filter(d => fromDir === d || fromDir.startsWith(d + path.sep))
|
|
846
|
+
.sort((a, b) => b.length - a.length);
|
|
847
|
+
if (candidates.length === 0) candidates.push(cargo.srcDir);
|
|
848
|
+
for (const cand of candidates) {
|
|
849
|
+
for (const root of ['lib.rs', 'main.rs']) {
|
|
850
|
+
const file = path.join(cand, root);
|
|
851
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile()) return file;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
if (/^super(::super)*$/.test(spec)) {
|
|
857
|
+
const superCount = spec.split('::').length;
|
|
858
|
+
const basename = path.basename(fromFile);
|
|
859
|
+
const isMod = basename === 'mod.rs' || basename === 'lib.rs' || basename === 'main.rs';
|
|
860
|
+
let dir = fromDir;
|
|
861
|
+
for (let i = 0; i < (isMod ? superCount : superCount - 1); i++) dir = path.dirname(dir);
|
|
862
|
+
return rustModuleOwnFile(dir, fromFile);
|
|
863
|
+
}
|
|
864
|
+
return resolveRustImport(spec, fromFile, projectRoot);
|
|
865
|
+
}
|
|
866
|
+
|
|
629
867
|
/**
|
|
630
868
|
* Try to resolve a path with various extensions
|
|
631
869
|
*/
|
|
@@ -652,6 +890,8 @@ function _findPackageJson(fromDir, stopDir) {
|
|
|
652
890
|
exports: pkg.exports,
|
|
653
891
|
main: pkg.main,
|
|
654
892
|
source: pkg.source,
|
|
893
|
+
module: pkg.module,
|
|
894
|
+
types: pkg.types,
|
|
655
895
|
};
|
|
656
896
|
}
|
|
657
897
|
} catch { /* unreadable or invalid JSON */ }
|
|
@@ -694,6 +934,63 @@ function resolveSelfReference(importPath, fromDir, config) {
|
|
|
694
934
|
const pkg = _findPackageJson(fromDir, config.root ? path.dirname(config.root) : null);
|
|
695
935
|
if (!pkg || !pkg.name) return null;
|
|
696
936
|
if (importPath !== pkg.name && !importPath.startsWith(pkg.name + '/')) return null;
|
|
937
|
+
return _resolvePackageSpecifier(pkg, importPath, config);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* In-repository workspace packages (fix #397, TanStack-query-measured: a
|
|
942
|
+
* subclass in packages/solid-query extending `QueryClient` imported from
|
|
943
|
+
* '@tanstack/query-core' left every inherited call unresolved): a bare
|
|
944
|
+
* specifier naming a package whose package.json sits inside the project
|
|
945
|
+
* resolves to that package's source, the way the workspace links it.
|
|
946
|
+
* `packages` maps each package name declared exactly once in the project to
|
|
947
|
+
* its manifest facts (a name declared by two manifests stays unresolved).
|
|
948
|
+
*/
|
|
949
|
+
function resolveWorkspacePackage(importPath, config) {
|
|
950
|
+
const packages = config.workspacePackages;
|
|
951
|
+
if (!packages || packages.size === 0) return null;
|
|
952
|
+
let name = importPath;
|
|
953
|
+
for (;;) {
|
|
954
|
+
let pkg = packages.get(name);
|
|
955
|
+
if (pkg?.manifestPending) {
|
|
956
|
+
// Restored from a cache: the manifest is read on first use.
|
|
957
|
+
const manifest = _findPackageJson(pkg.dir, pkg.dir);
|
|
958
|
+
pkg = manifest ? { ...manifest, workspace: true } : { ambiguous: true };
|
|
959
|
+
packages.set(name, pkg);
|
|
960
|
+
}
|
|
961
|
+
if (pkg) return pkg.ambiguous ? null : _resolvePackageSpecifier(pkg, importPath, config);
|
|
962
|
+
const slash = name.lastIndexOf('/');
|
|
963
|
+
if (slash <= 0) return null;
|
|
964
|
+
name = name.slice(0, slash);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* The package manifests of a project's JS/TS directories: the nearest
|
|
970
|
+
* package.json of every directory holding a JS/TS source file, inside the
|
|
971
|
+
* project root. Returns Map name -> manifest facts ({ ambiguous: true } for a
|
|
972
|
+
* name two manifests declare).
|
|
973
|
+
* @param {string} root - project root
|
|
974
|
+
* @param {Iterable<string>} dirs - directories of the project's JS/TS files
|
|
975
|
+
*/
|
|
976
|
+
function jsWorkspacePackages(root, dirs) {
|
|
977
|
+
const packages = new Map();
|
|
978
|
+
const seenDirs = new Set();
|
|
979
|
+
const seenManifests = new Set();
|
|
980
|
+
for (const dir of dirs) {
|
|
981
|
+
if (seenDirs.has(dir)) continue;
|
|
982
|
+
seenDirs.add(dir);
|
|
983
|
+
const pkg = _findPackageJson(dir, root);
|
|
984
|
+
if (!pkg || !pkg.name || seenManifests.has(pkg.dir)) continue;
|
|
985
|
+
seenManifests.add(pkg.dir);
|
|
986
|
+
if (pkg.dir !== root && !pkg.dir.startsWith(root + path.sep)) continue;
|
|
987
|
+
const existing = packages.get(pkg.name);
|
|
988
|
+
packages.set(pkg.name, existing ? { ambiguous: true } : { ...pkg, workspace: true });
|
|
989
|
+
}
|
|
990
|
+
return packages;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function _resolvePackageSpecifier(pkg, importPath, config) {
|
|
697
994
|
const subpath = importPath === pkg.name ? '.' : './' + importPath.slice(pkg.name.length + 1);
|
|
698
995
|
const extensions = config.extensions || getExtensions(config.language);
|
|
699
996
|
const tryTargets = (entry, wildcard) => {
|
|
@@ -733,16 +1030,29 @@ function resolveSelfReference(importPath, fromDir, config) {
|
|
|
733
1030
|
const source = resolveFilePath(path.resolve(pkg.dir, pkg.source), extensions);
|
|
734
1031
|
if (source) return source;
|
|
735
1032
|
}
|
|
1033
|
+
if (subpath === '.' && pkg.workspace) return _workspaceSourceEntry(pkg, extensions);
|
|
736
1034
|
return null;
|
|
737
1035
|
}
|
|
738
1036
|
// No exports map: bare name -> main/index; subpath -> direct file
|
|
739
1037
|
if (subpath === '.') {
|
|
740
1038
|
return (pkg.main && resolveFilePath(path.resolve(pkg.dir, pkg.main), extensions)) ||
|
|
1039
|
+
(pkg.workspace && _workspaceSourceEntry(pkg, extensions)) ||
|
|
741
1040
|
resolveFilePath(path.resolve(pkg.dir, 'index'), extensions);
|
|
742
1041
|
}
|
|
743
1042
|
return resolveFilePath(path.resolve(pkg.dir, subpath), extensions);
|
|
744
1043
|
}
|
|
745
1044
|
|
|
1045
|
+
// A workspace package whose `main` names a build artifact absent from the
|
|
1046
|
+
// source checkout: its declared source/module/types entries, else src/index.
|
|
1047
|
+
function _workspaceSourceEntry(pkg, extensions) {
|
|
1048
|
+
for (const entry of [pkg.source, pkg.module, pkg.types]) {
|
|
1049
|
+
if (typeof entry !== 'string') continue;
|
|
1050
|
+
const hit = resolveFilePath(path.resolve(pkg.dir, entry), extensions);
|
|
1051
|
+
if (hit && !hit.endsWith('.d.ts')) return hit;
|
|
1052
|
+
}
|
|
1053
|
+
return resolveFilePath(path.resolve(pkg.dir, 'src', 'index'), extensions);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
746
1056
|
function resolveFilePath(basePath, extensions) {
|
|
747
1057
|
// Check exact path
|
|
748
1058
|
if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) {
|
|
@@ -779,13 +1089,61 @@ function resolveFilePath(basePath, extensions) {
|
|
|
779
1089
|
return null;
|
|
780
1090
|
}
|
|
781
1091
|
|
|
1092
|
+
function _probeCached(cache, key, probe) {
|
|
1093
|
+
if (!cache) return probe();
|
|
1094
|
+
if (cache.has(key)) return cache.get(key);
|
|
1095
|
+
const value = probe();
|
|
1096
|
+
cache.set(key, value);
|
|
1097
|
+
return value;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function _isFile(p) {
|
|
1101
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function _isDir(p) {
|
|
1105
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Resolve an absolute Python module against the nearest non-package ancestor
|
|
1110
|
+
* of the importing file (fix #366). Walks from the importing directory up to
|
|
1111
|
+
* (excluding) the project root, which the caller already tried.
|
|
1112
|
+
*/
|
|
1113
|
+
function _resolvePythonNestedRoot(importPath, fromDir, config) {
|
|
1114
|
+
const root = path.resolve(config.root);
|
|
1115
|
+
const first = importPath.split('.')[0];
|
|
1116
|
+
if (!first) return null;
|
|
1117
|
+
const cache = config.probeCache;
|
|
1118
|
+
const modulePath = importPath.replace(/\./g, '/');
|
|
1119
|
+
let dir = path.resolve(fromDir);
|
|
1120
|
+
while (dir.length > root.length && dir.startsWith(root + path.sep)) {
|
|
1121
|
+
const isPackage = _probeCached(cache, `pyinit\0${dir}`,
|
|
1122
|
+
() => _isFile(path.join(dir, '__init__.py')));
|
|
1123
|
+
if (!isPackage) {
|
|
1124
|
+
const hasFirst = _probeCached(cache, `pyfirst\0${dir}\0${first}`,
|
|
1125
|
+
() => _isDir(path.join(dir, first)) || _isFile(path.join(dir, first + '.py')) ||
|
|
1126
|
+
_isFile(path.join(dir, first + '.pyi')));
|
|
1127
|
+
if (hasFirst) {
|
|
1128
|
+
const resolved = resolveFilePath(path.join(dir, modulePath), getExtensions('python'));
|
|
1129
|
+
if (resolved) return resolved;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
dir = path.dirname(dir);
|
|
1133
|
+
}
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
782
1137
|
/**
|
|
783
1138
|
* Get file extensions for a language
|
|
784
1139
|
*/
|
|
785
1140
|
function getExtensions(language) {
|
|
786
1141
|
switch (language) {
|
|
787
1142
|
case 'javascript':
|
|
788
|
-
|
|
1143
|
+
// A JS file's extensionless specifier names a TS source when that
|
|
1144
|
+
// is the only file there (JS tests of a TS package run through a
|
|
1145
|
+
// TS-aware loader, fix #397); JS files win when both exist.
|
|
1146
|
+
return ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'];
|
|
789
1147
|
case 'typescript':
|
|
790
1148
|
case 'tsx':
|
|
791
1149
|
return ['.ts', '.tsx', '.js', '.jsx'];
|
|
@@ -978,11 +1336,115 @@ function stripJsonComments(content) {
|
|
|
978
1336
|
return result.replace(/,(\s*[}\]])/g, '$1');
|
|
979
1337
|
}
|
|
980
1338
|
|
|
1339
|
+
/**
|
|
1340
|
+
* The inline `mod NAME { ... }` blocks of a Rust file (module symbols that
|
|
1341
|
+
* are not `mod NAME;` declarations), as { name, startLine, endLine }.
|
|
1342
|
+
*/
|
|
1343
|
+
function rustInlineModules(fileEntry) {
|
|
1344
|
+
const declared = new Set();
|
|
1345
|
+
for (const detail of fileEntry.importDetails || []) {
|
|
1346
|
+
if (detail.type === 'mod' && detail.module) declared.add(`${detail.module}\0${detail.line}`);
|
|
1347
|
+
}
|
|
1348
|
+
const inline = [];
|
|
1349
|
+
for (const symbol of fileEntry.symbols || []) {
|
|
1350
|
+
if (symbol.type !== 'module' || declared.has(`${symbol.name}\0${symbol.startLine}`)) continue;
|
|
1351
|
+
inline.push({ name: symbol.name, startLine: symbol.startLine, endLine: symbol.endLine });
|
|
1352
|
+
}
|
|
1353
|
+
inline.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
|
|
1354
|
+
return inline;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/** Names of the inline modules enclosing `line`, outermost first. */
|
|
1358
|
+
function rustInlineChainAt(inlineModules, line) {
|
|
1359
|
+
const chain = [];
|
|
1360
|
+
for (const mod of inlineModules) {
|
|
1361
|
+
if (mod.startLine <= line && mod.endLine >= line) chain.push(mod.name);
|
|
1362
|
+
}
|
|
1363
|
+
return chain;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Files a Rust `mod NAME;` declaration loads (fix #377), per the Rust
|
|
1368
|
+
* reference: in a mod-rs file (lib.rs, main.rs, mod.rs) the module directory
|
|
1369
|
+
* is the file's directory, in any other file `foo.rs` it is `foo/`; enclosing
|
|
1370
|
+
* inline modules add their names; `#[path = "p"]` is relative to the file's
|
|
1371
|
+
* directory outside inline modules and to the module directory inside them.
|
|
1372
|
+
* A non-mod-rs file can itself be a crate root (src/bin/x.rs, tests/x.rs,
|
|
1373
|
+
* build.rs), whose module directory is the file's directory: that layout is
|
|
1374
|
+
* the fallback when the first one names no indexed file. `exists(path)`
|
|
1375
|
+
* decides which candidate is loaded. Returns Map name -> [files] (several
|
|
1376
|
+
* files for cfg-selected alternatives of one name).
|
|
1377
|
+
*/
|
|
1378
|
+
function rustModDeclarationFiles(filePath, fileEntry, exists, inlineModules = null) {
|
|
1379
|
+
const out = new Map();
|
|
1380
|
+
const details = (fileEntry.importDetails || []).filter(detail => detail.type === 'mod' && detail.module);
|
|
1381
|
+
if (details.length === 0) return out;
|
|
1382
|
+
const inline = inlineModules || rustInlineModules(fileEntry);
|
|
1383
|
+
const moduleSymbols = new Map();
|
|
1384
|
+
for (const symbol of fileEntry.symbols || []) {
|
|
1385
|
+
if (symbol.type === 'module') moduleSymbols.set(`${symbol.name}\0${symbol.startLine}`, symbol);
|
|
1386
|
+
}
|
|
1387
|
+
const dir = path.dirname(filePath);
|
|
1388
|
+
const base = path.basename(filePath);
|
|
1389
|
+
const modRs = base === 'mod.rs' || base === 'lib.rs' || base === 'main.rs';
|
|
1390
|
+
const layouts = modRs ? [dir] : [path.join(dir, path.basename(filePath, '.rs')), dir];
|
|
1391
|
+
for (const detail of details) {
|
|
1392
|
+
const decl = moduleSymbols.get(`${detail.module}\0${detail.line}`);
|
|
1393
|
+
let pathAttr = null;
|
|
1394
|
+
for (const modifier of decl?.modifiers || []) {
|
|
1395
|
+
const match = /^path\s*=\s*"([^"]*)"$/.exec(String(modifier).trim());
|
|
1396
|
+
if (match) pathAttr = match[1];
|
|
1397
|
+
}
|
|
1398
|
+
const chain = rustInlineChainAt(inline, detail.line);
|
|
1399
|
+
let hit = null;
|
|
1400
|
+
for (const layout of layouts) {
|
|
1401
|
+
const moduleDir = path.join(layout, ...chain);
|
|
1402
|
+
const candidates = pathAttr != null
|
|
1403
|
+
? [chain.length === 0 ? path.join(dir, pathAttr) : path.join(moduleDir, pathAttr)]
|
|
1404
|
+
: [path.join(moduleDir, detail.module + '.rs'), path.join(moduleDir, detail.module, 'mod.rs')];
|
|
1405
|
+
hit = candidates.find(candidate => candidate !== filePath && exists(candidate));
|
|
1406
|
+
if (hit) break;
|
|
1407
|
+
}
|
|
1408
|
+
if (!hit) continue;
|
|
1409
|
+
if (!out.has(detail.module)) out.set(detail.module, []);
|
|
1410
|
+
if (!out.get(detail.module).includes(hit)) out.get(detail.module).push(hit);
|
|
1411
|
+
}
|
|
1412
|
+
return out;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* `moduleResolved` entries of a Rust file's `mod` declarations, resolved as
|
|
1417
|
+
* the import graph build resolves them (rustModDeclarationFiles against the
|
|
1418
|
+
* indexed files). `index` needs root and files (fix #375: macro expansion
|
|
1419
|
+
* workers derive module ancestry while the main thread builds the import
|
|
1420
|
+
* graph).
|
|
1421
|
+
*/
|
|
1422
|
+
function rustModuleDeclarationsResolved(index, filePath, fileEntry) {
|
|
1423
|
+
const out = {};
|
|
1424
|
+
const declared = rustModDeclarationFiles(filePath, fileEntry, candidate => index.files.has(candidate));
|
|
1425
|
+
for (const [name, files] of declared) out[name] = path.relative(index.root, files[0]);
|
|
1426
|
+
return out;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
981
1429
|
module.exports = {
|
|
982
1430
|
extractImports,
|
|
1431
|
+
jsWorkspacePackages,
|
|
1432
|
+
rustModuleDeclarationsResolved,
|
|
1433
|
+
rustModDeclarationFiles,
|
|
1434
|
+
rustInlineModules,
|
|
1435
|
+
rustInlineChainAt,
|
|
983
1436
|
extractExports,
|
|
984
1437
|
resolveImport,
|
|
985
1438
|
resolveFilePath,
|
|
986
1439
|
resolveRustImport,
|
|
987
|
-
|
|
1440
|
+
resolveRustModuleFile,
|
|
1441
|
+
findGoModule,
|
|
1442
|
+
findCargoRoot,
|
|
1443
|
+
workspaceCrateRegistry,
|
|
1444
|
+
snapshotWorkspaceManifests,
|
|
1445
|
+
seedWorkspaceManifests,
|
|
1446
|
+
workspaceManifestsCurrent,
|
|
1447
|
+
manifestSetFingerprint,
|
|
1448
|
+
resetCargoCaches,
|
|
1449
|
+
resetRustResolveMemo,
|
|
988
1450
|
};
|