ucn 5.5.2 → 5.6.0
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 +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -6
package/core/bridge.js
CHANGED
|
@@ -522,6 +522,19 @@ const GO_ROUTER_MODULES = [
|
|
|
522
522
|
[/^github\.com\/julienschmidt\/httprouter$/, 'httprouter'],
|
|
523
523
|
];
|
|
524
524
|
|
|
525
|
+
// The framework labels a route or a request can carry. A label is known to
|
|
526
|
+
// the `framework` filter when it is listed here or appears in the answer it
|
|
527
|
+
// filters, so a label the answer prints is always one the filter takes.
|
|
528
|
+
const ENDPOINT_FRAMEWORKS = Object.freeze([...new Set([
|
|
529
|
+
...Object.values(SERVER_RECEIVER_PATTERNS).flat().map(pattern => pattern.framework),
|
|
530
|
+
...Object.values(CLIENT_PATTERNS).flatMap(patterns => patterns.receivers).map(pattern => pattern.framework),
|
|
531
|
+
...JS_ROUTER_MODULES.map(([, framework]) => framework),
|
|
532
|
+
...GO_ROUTER_MODULES.map(([, framework]) => framework),
|
|
533
|
+
'actix', 'aiohttp', 'aspnet', 'django', 'fastapi', 'fetch', 'flask', 'go-inprocess',
|
|
534
|
+
'inject', 'jax-rs', 'nestjs', 'nextjs', 'request-helper', 'route-config', 'spring',
|
|
535
|
+
'starlette', 'supertest', 'unknown-python',
|
|
536
|
+
])].sort());
|
|
537
|
+
|
|
525
538
|
function frameworkOfModule(table, spec) {
|
|
526
539
|
for (const [re, framework] of table) if (re.test(spec)) return framework;
|
|
527
540
|
return null;
|
|
@@ -2279,53 +2292,6 @@ function extractNextjsRoutes(index) {
|
|
|
2279
2292
|
* Detect HTTP client requests across the project.
|
|
2280
2293
|
* Cached on `index._endpointsCache.clientRequests`.
|
|
2281
2294
|
*/
|
|
2282
|
-
// Python HTTP client types whose instances are receivers of request calls.
|
|
2283
|
-
// A receiver typed to one of these (with-binding, constructor assignment) or
|
|
2284
|
-
// bound to a pytest fixture that constructs one is a client by evidence, not
|
|
2285
|
-
// by name (fix #349: 145 of 625 real `tc.get("/api/...")` sites on one repo
|
|
2286
|
-
// were invisible because the receiver was not literally named `client`).
|
|
2287
|
-
const PY_CLIENT_TYPES = new Set([
|
|
2288
|
-
'TestClient', 'Client', 'AsyncClient', 'Session', 'FlaskClient',
|
|
2289
|
-
'ClientSession', 'AsyncSession', 'HTTPConnection', 'HTTPSConnection',
|
|
2290
|
-
]);
|
|
2291
|
-
const PY_CLIENT_FACTORIES = new Set(['test_client', 'Session', 'Client', 'AsyncClient']);
|
|
2292
|
-
const PY_REQUEST_METHODS = /^(get|post|put|delete|patch|options|head|request)$/;
|
|
2293
|
-
|
|
2294
|
-
function _pythonClientFixtures(index) {
|
|
2295
|
-
if (index._endpointsCache?.pyClientFixtures) return index._endpointsCache.pyClientFixtures;
|
|
2296
|
-
const fixtures = new Set();
|
|
2297
|
-
for (const [filePath, fileEntry] of index.files) {
|
|
2298
|
-
if (fileEntry.language !== 'python') continue;
|
|
2299
|
-
const fixtureDefs = (fileEntry.symbols || []).filter(s =>
|
|
2300
|
-
(s.decorators || []).some(d => /(^|\.)fixture$/.test(String(d))));
|
|
2301
|
-
if (fixtureDefs.length === 0) continue;
|
|
2302
|
-
const calls = getCachedCalls(index, filePath) || [];
|
|
2303
|
-
for (const def of fixtureDefs) {
|
|
2304
|
-
const constructsClient = calls.some(c =>
|
|
2305
|
-
c.enclosingFunction?.name === def.name &&
|
|
2306
|
-
c.enclosingFunction?.startLine === def.startLine &&
|
|
2307
|
-
(PY_CLIENT_TYPES.has(c.name) || (c.isMethod && PY_CLIENT_FACTORIES.has(c.name))));
|
|
2308
|
-
if (constructsClient) fixtures.add(def.name);
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
if (!index._endpointsCache) index._endpointsCache = {};
|
|
2312
|
-
index._endpointsCache.pyClientFixtures = fixtures;
|
|
2313
|
-
return fixtures;
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
function _pythonClientReceiver(index, fileEntry, call, fixtures) {
|
|
2317
|
-
if (!call.isMethod || !call.receiver || !PY_REQUEST_METHODS.test(call.name)) return null;
|
|
2318
|
-
if (call.receiverType && PY_CLIENT_TYPES.has(String(call.receiverType).split('.').pop())) {
|
|
2319
|
-
return 'python-client';
|
|
2320
|
-
}
|
|
2321
|
-
if (fixtures.size === 0 || !fixtures.has(call.receiver)) return null;
|
|
2322
|
-
const fn = call.enclosingFunction;
|
|
2323
|
-
if (!fn) return null;
|
|
2324
|
-
const sym = (fileEntry.symbols || []).find(s => s.name === fn.name && s.startLine === fn.startLine);
|
|
2325
|
-
const params = String(sym?.params || '').split(',').map(p => p.trim().split(/[:=]/)[0].trim());
|
|
2326
|
-
return params.includes(call.receiver) ? 'pytest-client-fixture' : null;
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
2295
|
// `fastify().route({...})` - a route declared on a freshly built server.
|
|
2330
2296
|
const JS_SERVER_FACTORY_CALL = /^(fastify|Fastify|express|Hono|Koa)$/;
|
|
2331
2297
|
|
|
@@ -2347,7 +2313,7 @@ function extractClientRequests(index) {
|
|
|
2347
2313
|
}
|
|
2348
2314
|
const requests = [];
|
|
2349
2315
|
const uncertain = [];
|
|
2350
|
-
const
|
|
2316
|
+
const clientProof = new (require('./client-receivers').ClientReceiverProof)(index, endpointsSession(index));
|
|
2351
2317
|
let helperProof = null;
|
|
2352
2318
|
|
|
2353
2319
|
for (const [filePath, fileEntry] of index.files) {
|
|
@@ -2431,7 +2397,9 @@ function extractClientRequests(index) {
|
|
|
2431
2397
|
continue;
|
|
2432
2398
|
}
|
|
2433
2399
|
if (!helperProof) helperProof = new RequestHelperProof(index, endpointsSession(index));
|
|
2434
|
-
const proof =
|
|
2400
|
+
const proof = call.isMethod
|
|
2401
|
+
? { proven: !!clientProof.classify(filePath, call), reason: 'receiver-unproven' }
|
|
2402
|
+
: helperProof.classify(filePath, call);
|
|
2435
2403
|
const verb = call.isMethod && /^(get|post|put|delete|patch|options|head)$/i.test(call.name)
|
|
2436
2404
|
? call.name.toUpperCase() : null;
|
|
2437
2405
|
const method = cfg.method || verb || 'ALL';
|
|
@@ -2462,7 +2430,7 @@ function extractClientRequests(index) {
|
|
|
2462
2430
|
// fix #383: Go request constructors read their method and URL
|
|
2463
2431
|
// from their own argument positions.
|
|
2464
2432
|
if (call.requestTarget && lang === 'go' && call.receiver && !call.isPathCall &&
|
|
2465
|
-
matchClientRequest({ ...call, firstStringArg: call.requestTarget.url }, lang,
|
|
2433
|
+
matchClientRequest({ ...call, firstStringArg: call.requestTarget.url }, lang, clientProof.classify(filePath, call))) {
|
|
2466
2434
|
const target = call.requestTarget;
|
|
2467
2435
|
if (isPathShaped(target.url)) {
|
|
2468
2436
|
// A request with a path but no scheme and host cannot be
|
|
@@ -2496,26 +2464,22 @@ function extractClientRequests(index) {
|
|
|
2496
2464
|
continue;
|
|
2497
2465
|
}
|
|
2498
2466
|
if (!call.firstStringArg) continue;
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
r = { method: call.name.toUpperCase() === 'REQUEST' ? 'ALL' : call.name.toUpperCase(),
|
|
2504
|
-
framework, methodInferred: call.name === 'request' };
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2467
|
+
const conf = CLIENT_PATTERNS[lang];
|
|
2468
|
+
const candidate = conf && (conf.bareCalls.has(call.name) ||
|
|
2469
|
+
conf.receivers.some(p => p.methodPattern.test(call.name)));
|
|
2470
|
+
const r = matchClientRequest(call, lang, candidate ? clientProof.classify(filePath, call) : null);
|
|
2507
2471
|
if (!r) {
|
|
2508
2472
|
// Visible uncertainty: request-shaped call on an unrecognized
|
|
2509
2473
|
// receiver with a path-shaped literal. Listed, never counted.
|
|
2510
|
-
const conf = CLIENT_PATTERNS[lang];
|
|
2511
2474
|
const pathShaped = call.firstStringArg.startsWith('/') || call.firstStringArg.includes('://');
|
|
2512
|
-
if (conf && call.
|
|
2475
|
+
if (conf && call.receiver && pathShaped &&
|
|
2513
2476
|
conf.receivers.some(p => p.methodPattern.test(call.name))) {
|
|
2514
2477
|
uncertain.push({
|
|
2515
2478
|
receiver: call.receiver, method: call.name, path: call.firstStringArg,
|
|
2516
2479
|
file: fileEntry.relativePath || filePath, absoluteFile: filePath, line: call.line,
|
|
2517
2480
|
callerName: call.enclosingFunction?.name || '<top-level>',
|
|
2518
|
-
reason:
|
|
2481
|
+
reason: conf.receivers.some(p => p.receiverPattern.test(call.receiver))
|
|
2482
|
+
? 'receiver-name-only' : 'receiver-unrecognized',
|
|
2519
2483
|
});
|
|
2520
2484
|
}
|
|
2521
2485
|
continue;
|
|
@@ -2640,9 +2604,9 @@ function supertestProducerOf(call, calls, producers, locals) {
|
|
|
2640
2604
|
* Match a call against client request patterns.
|
|
2641
2605
|
* Returns { method, framework, methodInferred } or null.
|
|
2642
2606
|
*/
|
|
2643
|
-
function matchClientRequest(call, lang,
|
|
2607
|
+
function matchClientRequest(call, lang, origin) {
|
|
2644
2608
|
const conf = CLIENT_PATTERNS[lang];
|
|
2645
|
-
if (!conf) return null;
|
|
2609
|
+
if (!conf || !origin) return null;
|
|
2646
2610
|
|
|
2647
2611
|
// 1) Bare-call patterns: fetch('/path') or fetch('/path', { method: 'POST' })
|
|
2648
2612
|
if (!call.isMethod && conf.bareCalls.has(call.name)) {
|
|
@@ -2662,7 +2626,6 @@ function matchClientRequest(call, lang, allCallsInFile) {
|
|
|
2662
2626
|
const isMethodLike = call.isMethod || (lang === 'go' && !!call.receiver && !call.isPathCall);
|
|
2663
2627
|
if (isMethodLike && call.receiver) {
|
|
2664
2628
|
for (const p of conf.receivers) {
|
|
2665
|
-
if (!p.receiverPattern.test(call.receiver)) continue;
|
|
2666
2629
|
if (!p.methodPattern.test(call.name)) continue;
|
|
2667
2630
|
|
|
2668
2631
|
// Determine method
|
|
@@ -2697,7 +2660,7 @@ function matchClientRequest(call, lang, allCallsInFile) {
|
|
|
2697
2660
|
} else {
|
|
2698
2661
|
method = methodName.toUpperCase();
|
|
2699
2662
|
}
|
|
2700
|
-
return { method, framework:
|
|
2663
|
+
return { method, framework: origin, methodInferred: inferred };
|
|
2701
2664
|
}
|
|
2702
2665
|
}
|
|
2703
2666
|
|
|
@@ -3188,6 +3151,7 @@ function endpoints(index, options = {}) {
|
|
|
3188
3151
|
}
|
|
3189
3152
|
|
|
3190
3153
|
module.exports = {
|
|
3154
|
+
ENDPOINT_FRAMEWORKS,
|
|
3191
3155
|
endpoints,
|
|
3192
3156
|
extractServerRoutes,
|
|
3193
3157
|
extractClientRequests,
|
package/core/cache.js
CHANGED
|
@@ -10,8 +10,8 @@ const path = require('path');
|
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const crypto = require('crypto');
|
|
12
12
|
const {
|
|
13
|
-
expandGlob, detectProjectPattern, parseGitignore, gitTrackedPaths, gitListing,
|
|
14
|
-
classifyUnsupportedSourceFile,
|
|
13
|
+
expandGlob, detectProjectPattern, parseGitignore, gitTrackedPaths, gitListing,
|
|
14
|
+
classifyUnsupportedSourceFile, projectDiscoveryOptions,
|
|
15
15
|
} = require('./discovery');
|
|
16
16
|
const { codeUnitCompare } = require('./shared');
|
|
17
17
|
const { syntaxWeightKind } = require('./source-memo');
|
|
@@ -998,7 +998,24 @@ function clearAllCaches() {
|
|
|
998
998
|
// v326: C++ files retain raw base-clause positions and their syntactic scopes.
|
|
999
999
|
// v327: Python explicit-argument super calls retain a producer, not a lexical parent receiver.
|
|
1000
1000
|
// v328: ambiguous C++ direct initializers retain empty template-call records.
|
|
1001
|
-
|
|
1001
|
+
// v331: source paths excluded by .ucn.json are recorded one issue per source
|
|
1002
|
+
// file (searched as text per name), with `outside-project` and
|
|
1003
|
+
// `text-scan-budget` reasons; parse-failed files keep their size and mtime.
|
|
1004
|
+
// v332: members a subtype or a structural satisfier must supply carry
|
|
1005
|
+
// `requiredMember` (interface and object-type signatures that are not
|
|
1006
|
+
// optional, abstract members, bodiless trait methods, Go interface method
|
|
1007
|
+
// elements, Python abstract and Protocol members, C++ pure virtuals); a
|
|
1008
|
+
// Python protocol class carries `protocolClass`.
|
|
1009
|
+
// v333: a JS/TS file's `moduleAssignedNames` leaves out writes to a name a
|
|
1010
|
+
// function or block declares itself (they never reach the module's binding);
|
|
1011
|
+
// a Python call whose only argument is a bare generator counts one argument.
|
|
1012
|
+
// v334: file entries carry `reflectionUses` and `evalUses`, call sites of the
|
|
1013
|
+
// language's reflection and code-execution vocabulary counted in the tree at
|
|
1014
|
+
// index time (`repo` and the completeness footer counted them by reading
|
|
1015
|
+
// every file's text on each query).
|
|
1016
|
+
// v335: reflection/eval facts follow commented and parenthesized callees;
|
|
1017
|
+
// package reflection qualifiers retain Unicode names and lexical ownership.
|
|
1018
|
+
const CACHE_FORMAT_VERSION = 335;
|
|
1002
1019
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
1003
1020
|
|
|
1004
1021
|
function usageResultKey(index, filePath, name, mode) {
|
|
@@ -1141,6 +1158,12 @@ function saveCache(index, cachePath) {
|
|
|
1141
1158
|
|
|
1142
1159
|
const cacheFile = cachePath || path.join(cacheDir, 'index.json');
|
|
1143
1160
|
|
|
1161
|
+
// HOT rankings stored for the generation in memory describe the state
|
|
1162
|
+
// this save writes: read them before the generation is replaced, and
|
|
1163
|
+
// store them again under the new one below.
|
|
1164
|
+
const hotSummary = require('./hot-summary');
|
|
1165
|
+
hotSummary.ensure(index);
|
|
1166
|
+
|
|
1144
1167
|
// A partially loaded (per-file) calls cache must be complete before it
|
|
1145
1168
|
// is rewritten: the shard directory is replaced wholesale below.
|
|
1146
1169
|
if (index._callsCachePrepared && !index._callsCacheLoaded) {
|
|
@@ -1270,6 +1293,9 @@ function saveCache(index, cachePath) {
|
|
|
1270
1293
|
failedFiles: index.failedFiles
|
|
1271
1294
|
? Array.from(index.failedFiles).map(f => path.relative(root, f))
|
|
1272
1295
|
: [],
|
|
1296
|
+
// A parse failure is retried when its file changes: keep what the
|
|
1297
|
+
// file looked like when it failed.
|
|
1298
|
+
failedFileStats: failedFileStats(index, root),
|
|
1273
1299
|
unsupportedFiles: Array.isArray(index.unsupportedFiles)
|
|
1274
1300
|
? index.unsupportedFiles
|
|
1275
1301
|
: [],
|
|
@@ -1314,6 +1340,7 @@ function saveCache(index, cachePath) {
|
|
|
1314
1340
|
const tmpFile = cacheFile + '.tmp';
|
|
1315
1341
|
fs.writeFileSync(tmpFile, JSON.stringify(cacheData));
|
|
1316
1342
|
fs.renameSync(tmpFile, cacheFile);
|
|
1343
|
+
index._cacheDir = path.dirname(cacheFile);
|
|
1317
1344
|
|
|
1318
1345
|
// MED-1 (Round 5): clear the reachabilityDirty flag now that the set is
|
|
1319
1346
|
// safely persisted. The cli/index.js cache-save guard checks this flag
|
|
@@ -1384,6 +1411,11 @@ function saveCache(index, cachePath) {
|
|
|
1384
1411
|
}
|
|
1385
1412
|
}
|
|
1386
1413
|
|
|
1414
|
+
// A failed save must not authorize a summary for a state that was never
|
|
1415
|
+
// completely written. In particular, direct indexFile mutations cannot
|
|
1416
|
+
// publish against the old calls generation while only in memory.
|
|
1417
|
+
hotSummary.adoptSavedGeneration(index);
|
|
1418
|
+
try { hotSummary.save(index, cacheFile); } catch (_) { /* the ranking is computed again */ }
|
|
1387
1419
|
if (!cachePath) pruneUserCache();
|
|
1388
1420
|
return cacheFile;
|
|
1389
1421
|
}
|
|
@@ -1453,6 +1485,7 @@ function loadCache(index, cachePath) {
|
|
|
1453
1485
|
index._clearParsedTreeCache?.();
|
|
1454
1486
|
index._groundSetCache = new Map();
|
|
1455
1487
|
index._groundSetCacheLines = 0;
|
|
1488
|
+
index._sourceUniverse = null;
|
|
1456
1489
|
index._nameBindingReachCache = new Map();
|
|
1457
1490
|
index._returnTypeFlowCache = new Map();
|
|
1458
1491
|
index._classIdentity = null;
|
|
@@ -1533,6 +1566,9 @@ function loadCache(index, cachePath) {
|
|
|
1533
1566
|
? cacheData.callsGeneration : null;
|
|
1534
1567
|
index._callNameCache = null;
|
|
1535
1568
|
index._callNameCacheReadGeneration = null;
|
|
1569
|
+
// HOT rankings stored beside this file are read when one is asked.
|
|
1570
|
+
index._cacheDir = path.dirname(cacheFile);
|
|
1571
|
+
require('./hot-summary').forget(index);
|
|
1536
1572
|
|
|
1537
1573
|
// Prepare lazy calls cache loading — load manifest but defer shard parsing.
|
|
1538
1574
|
// Shards are loaded on first getCachedCalls access via ensureCallsCacheLoaded().
|
|
@@ -1551,6 +1587,8 @@ function loadCache(index, cachePath) {
|
|
|
1551
1587
|
cacheData.failedFiles.map(f => path.isAbsolute(f) ? f : toAbs(f))
|
|
1552
1588
|
);
|
|
1553
1589
|
}
|
|
1590
|
+
index._failedFileStats = new Map(Object.entries(cacheData.failedFileStats || {})
|
|
1591
|
+
.map(([relative, stat]) => [toAbs(relative), { size: stat[0], mtimeMs: stat[1] }]));
|
|
1554
1592
|
index.unsupportedFiles = Array.isArray(cacheData.unsupportedFiles)
|
|
1555
1593
|
? cacheData.unsupportedFiles
|
|
1556
1594
|
: [];
|
|
@@ -1651,6 +1689,17 @@ function loadCache(index, cachePath) {
|
|
|
1651
1689
|
* @param {object} index - ProjectIndex instance
|
|
1652
1690
|
* @returns {boolean} - True if cache needs rebuilding
|
|
1653
1691
|
*/
|
|
1692
|
+
function failedFileStats(index, root) {
|
|
1693
|
+
const stats = {};
|
|
1694
|
+
// Only what the build recorded at the failure: a stat taken now could
|
|
1695
|
+
// describe a file that was repaired since.
|
|
1696
|
+
for (const failedPath of index.failedFiles || []) {
|
|
1697
|
+
const known = index._failedFileStats && index._failedFileStats.get(failedPath);
|
|
1698
|
+
if (known) stats[path.relative(root, failedPath)] = [known.size, known.mtimeMs];
|
|
1699
|
+
}
|
|
1700
|
+
return stats;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1654
1703
|
function isCacheStale(index) {
|
|
1655
1704
|
const currentConfigHash = crypto.createHash('md5')
|
|
1656
1705
|
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
@@ -1706,6 +1755,38 @@ function isCacheStale(index) {
|
|
|
1706
1755
|
}
|
|
1707
1756
|
}
|
|
1708
1757
|
|
|
1758
|
+
// A file that failed to parse is a source file: when it changes, a
|
|
1759
|
+
// build must try it again (it is in no other sweep, so a repaired file
|
|
1760
|
+
// used to stay "failed" until something else forced a rebuild).
|
|
1761
|
+
if (index.failedFiles && index.failedFiles.size > 0) {
|
|
1762
|
+
for (const failedPath of index.failedFiles) {
|
|
1763
|
+
if (index.files.has(failedPath)) continue;
|
|
1764
|
+
let stat;
|
|
1765
|
+
try { stat = fs.statSync(failedPath); } catch (_) { return true; }
|
|
1766
|
+
// The build records what the file looked like when it failed
|
|
1767
|
+
// (ProjectIndex._noteFailedFile). Without that record nothing
|
|
1768
|
+
// shows the file is unchanged, so it is tried again: reading the
|
|
1769
|
+
// stat here instead would call a repaired file "unchanged".
|
|
1770
|
+
const known = index._failedFileStats && index._failedFileStats.get(failedPath);
|
|
1771
|
+
if (!known || known.size !== stat.size || known.mtimeMs !== stat.mtimeMs) return true;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// Text-only source (core/source-universe.js) is searched per name, never
|
|
1776
|
+
// indexed. An edit that leaves a file text-only changes no symbol: the
|
|
1777
|
+
// classification is refreshed and name scans are dropped, with no
|
|
1778
|
+
// rebuild. An edit that can change WHAT is indexed (a file gone, an
|
|
1779
|
+
// oversized file back under the limit) is a rebuild.
|
|
1780
|
+
const discovery = projectDiscoveryOptions(index.root, index.config || {},
|
|
1781
|
+
{ includeBundled: index.includeBundled === true });
|
|
1782
|
+
const refreshed = require('./source-universe').refreshTextOnly(index,
|
|
1783
|
+
{ maxFileSize: discovery.maxFileSize ?? (8 * 1024 * 1024) });
|
|
1784
|
+
if (refreshed === 'reindex') return true;
|
|
1785
|
+
if (refreshed === 'refreshed') {
|
|
1786
|
+
index._groundSetCache = new Map();
|
|
1787
|
+
index._groundSetCacheLines = 0;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1709
1790
|
// Cargo manifests steer Rust import resolution (fix #372).
|
|
1710
1791
|
if (index.cargoManifests &&
|
|
1711
1792
|
!require('./imports').workspaceManifestsCurrent(index.root, index.cargoManifests)) {
|
|
@@ -1723,13 +1804,14 @@ function isCacheStale(index) {
|
|
|
1723
1804
|
// Only reached when all cached files are unchanged.
|
|
1724
1805
|
const pattern = detectProjectPattern(index.root);
|
|
1725
1806
|
const currentUnsupported = [];
|
|
1726
|
-
const
|
|
1807
|
+
const currentIssues = [];
|
|
1727
1808
|
const currentManifests = [];
|
|
1809
|
+
// The same options the build used (shared with project.js): a walk that
|
|
1810
|
+
// reads the configuration differently sees a different file set.
|
|
1728
1811
|
const globOpts = {
|
|
1729
|
-
|
|
1730
|
-
includeBundled: index.includeBundled === true,
|
|
1812
|
+
...discovery,
|
|
1731
1813
|
onDiscoveryIssue: issue => {
|
|
1732
|
-
|
|
1814
|
+
currentIssues.push(`${issue.reason}\0${path.relative(index.root, issue.path || index.root) || '.'}`);
|
|
1733
1815
|
},
|
|
1734
1816
|
onSkippedFile: (filePath) => {
|
|
1735
1817
|
if (path.basename(filePath) === 'Cargo.toml') {
|
|
@@ -1746,16 +1828,19 @@ function isCacheStale(index) {
|
|
|
1746
1828
|
if (!gitignoreRules) gitignoreRules = parseGitignore(index.root, gitListingOnce());
|
|
1747
1829
|
globOpts.gitignoreRules = gitignoreRules;
|
|
1748
1830
|
globOpts.trackedPaths = gitTrackedPaths(index.root, gitListingOnce());
|
|
1749
|
-
const configExclude = index.config.exclude || [];
|
|
1750
|
-
if (configExclude.length > 0) {
|
|
1751
|
-
globOpts.ignores = [...DEFAULT_IGNORES, ...configExclude];
|
|
1752
|
-
}
|
|
1753
1831
|
const currentFiles = expandGlob(pattern, globOpts);
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1832
|
+
// Every path the walk reports as not indexed, with its reason: a file
|
|
1833
|
+
// that became a bundle, crossed the size limit, entered or left an
|
|
1834
|
+
// excluded path changes what a claim ranges over. Issues the build
|
|
1835
|
+
// records about itself (an unusable config, a failed git listing) are
|
|
1836
|
+
// not the walk's to compare.
|
|
1837
|
+
const BUILD_ISSUES = new Set(['invalid-config', 'git-listing-failed']);
|
|
1838
|
+
const cachedIssues = [...new Set((index.discoveryIssues || [])
|
|
1839
|
+
.filter(issue => !BUILD_ISSUES.has(issue.reason))
|
|
1840
|
+
.map(issue => `${issue.reason}\0${issue.relativePath || '.'}`))].sort();
|
|
1841
|
+
const walkedIssues = [...new Set(currentIssues)].sort();
|
|
1842
|
+
if (cachedIssues.length !== walkedIssues.length ||
|
|
1843
|
+
cachedIssues.some((value, i) => value !== walkedIssues[i])) return true;
|
|
1759
1844
|
const cachedPaths = new Set(index.files.keys());
|
|
1760
1845
|
const currentPaths = new Set(currentFiles);
|
|
1761
1846
|
|