ucn 5.5.0 → 5.5.2
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 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +41 -27
- package/cli/index.js +6 -1
- package/core/accessors.js +341 -54
- package/core/account.js +106 -31
- package/core/analysis.js +266 -127
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +144 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +5111 -1024
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +35 -15
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +1 -0
- package/core/output/reporting.js +6 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +228 -114
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +1202 -157
- package/core/rust-modules.js +21 -3
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +157 -46
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +548 -89
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
|
@@ -96,6 +96,8 @@ function createState(index, inventory = null) {
|
|
|
96
96
|
textualMemo: new Map(),
|
|
97
97
|
productive: new Map(),
|
|
98
98
|
hygieneSeq: 0,
|
|
99
|
+
contexts: new Map(), // file -> calls-pass context (fix #405)
|
|
100
|
+
resolved: new Map(), // invocation -> resolveMacro answer
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -317,6 +319,13 @@ function lineOffset(state, file, line) {
|
|
|
317
319
|
return starts[Math.min(Math.max(line - 1, 0), starts.length - 1)];
|
|
318
320
|
}
|
|
319
321
|
|
|
322
|
+
/** Offset just past `line` (its newline included), or the content's end. */
|
|
323
|
+
function lineEndOffset(state, file, line) {
|
|
324
|
+
lineOffset(state, file, 1);
|
|
325
|
+
const starts = state.lineStarts.get(file);
|
|
326
|
+
return line < starts.length ? starts[line] : (readContent(state, file) || '').length;
|
|
327
|
+
}
|
|
328
|
+
|
|
320
329
|
function pickLatest(candidates) {
|
|
321
330
|
return candidates.reduce((best, candidate) => (!best || candidate.pos > best.pos ? candidate : best), null);
|
|
322
331
|
}
|
|
@@ -327,6 +336,16 @@ function pickLatest(candidates) {
|
|
|
327
336
|
* in scope).
|
|
328
337
|
*/
|
|
329
338
|
function resolveMacro(state, file, offset, line, name, qualifier, dollarCratePackage) {
|
|
339
|
+
// An invocation's answer is asked again by the pass that expands it
|
|
340
|
+
// (fix #405: fileMayExpand resolved every invocation first).
|
|
341
|
+
const key = `${file}\0${offset}\0${line}\0${name}\0${qualifier || ''}\0${dollarCratePackage || ''}`;
|
|
342
|
+
if (state.resolved.has(key)) return state.resolved.get(key);
|
|
343
|
+
const result = resolveMacroUncached(state, file, offset, line, name, qualifier, dollarCratePackage);
|
|
344
|
+
state.resolved.set(key, result);
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function resolveMacroUncached(state, file, offset, line, name, qualifier, dollarCratePackage) {
|
|
330
349
|
const all = state.inventory.get(name);
|
|
331
350
|
if (!all || all.length === 0) return null;
|
|
332
351
|
const inPackage = pkgRoot => all.filter(def => packageOf(state, def.file)?.root === pkgRoot);
|
|
@@ -801,6 +820,9 @@ function flatten(tokens, out = []) {
|
|
|
801
820
|
|
|
802
821
|
const MULTI_PUNCT = ['::', '->', '=>', '==', '!=', '<=', '>=', '&&', '||', '+=', '-=', '*=', '/=',
|
|
803
822
|
'%=', '^=', '&=', '|=', '<<', '>>', '..', '//', '/*', '*/'];
|
|
823
|
+
// The two-character joints that start a multi-character punctuation token
|
|
824
|
+
// (a one-character joint, from an empty leaf, starts any that begins with it).
|
|
825
|
+
const MULTI_PUNCT_JOINTS = new Set(MULTI_PUNCT.flatMap(p => [p.slice(0, 2), p[0]]));
|
|
804
826
|
const SPACE_AFTER = new Set([',', ';', ':', '=>', '->', '=', '+', '|', '&&', '||', '==', '!=']);
|
|
805
827
|
const SPACE_BEFORE = new Set(['=>', '->', '=', '+', '{', '==', '!=', '&&', '||']);
|
|
806
828
|
// ASCII letters, digits, '_', quotes (and '#' at a word's end): a leaf that
|
|
@@ -832,10 +854,13 @@ function needsSpace(prev, leaf) {
|
|
|
832
854
|
if (wordEnd && wordStart) return true;
|
|
833
855
|
const a = prev.v;
|
|
834
856
|
const b = leaf.v;
|
|
835
|
-
if (b === '.'
|
|
857
|
+
if (b === '.') {
|
|
858
|
+
const first = String(a).charCodeAt(0);
|
|
859
|
+
if (first >= 48 && first <= 57) return true;
|
|
860
|
+
}
|
|
836
861
|
if (!wordEnd && !wordStart) {
|
|
837
862
|
const joint = a.slice(-1) + b[0];
|
|
838
|
-
if (
|
|
863
|
+
if (MULTI_PUNCT_JOINTS.has(joint) && !(a === '::' || b === '::')) return true;
|
|
839
864
|
}
|
|
840
865
|
if (a === ':' && b === ':') return true;
|
|
841
866
|
if (SPACE_AFTER.has(a) && a !== '::') return !(a === '|' && prev.closer);
|
|
@@ -949,7 +974,8 @@ function buildSynthetic(content, regions, keep) {
|
|
|
949
974
|
region.synthStart = text.length;
|
|
950
975
|
text += region.layout.text;
|
|
951
976
|
region.synthEnd = text.length;
|
|
952
|
-
|
|
977
|
+
// A stand-in for an expansion another group reads keeps its length.
|
|
978
|
+
if (!region.blank) addSpan(region.synthStart, region.synthEnd);
|
|
953
979
|
cursor = region.end;
|
|
954
980
|
}
|
|
955
981
|
copy(cursor, content.length);
|
|
@@ -1205,11 +1231,29 @@ const DECLARATION_TYPES = [
|
|
|
1205
1231
|
'field_declaration', 'enum_variant', 'associated_type',
|
|
1206
1232
|
];
|
|
1207
1233
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1234
|
+
const DECLARATION_TYPE_SET = new Set(DECLARATION_TYPES);
|
|
1235
|
+
let declarationWalkTypes = null;
|
|
1236
|
+
function declarationWalkTypesOf() {
|
|
1237
|
+
return declarationWalkTypes || (declarationWalkTypes =
|
|
1238
|
+
[...new Set([...DECLARATION_TYPES, ...require('../languages/rust').DECLARATION_NODE_TYPES])]);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* (startLine\0name) keys of declarations whose name was produced inside a
|
|
1243
|
+
* region. `within`: the roots spanning the regions and their walked
|
|
1244
|
+
* descendants, when every such declaration lies inside one (else the whole
|
|
1245
|
+
* tree is searched).
|
|
1246
|
+
*/
|
|
1247
|
+
function generatedDeclarations(root, regions, within = null) {
|
|
1210
1248
|
const out = new Map();
|
|
1211
1249
|
if (regions.length === 0) return out;
|
|
1212
|
-
|
|
1250
|
+
const nodes = within
|
|
1251
|
+
? within.roots.flatMap((spanning, i) => [
|
|
1252
|
+
...(DECLARATION_TYPE_SET.has(spanning.type) ? [spanning] : []),
|
|
1253
|
+
...within.walked[i].filter(node => DECLARATION_TYPE_SET.has(node.type)),
|
|
1254
|
+
])
|
|
1255
|
+
: root.descendantsOfType(DECLARATION_TYPES);
|
|
1256
|
+
for (const node of nodes) {
|
|
1213
1257
|
const nameNode = node.type === 'impl_item' ? node.childForFieldName('type')
|
|
1214
1258
|
: node.childForFieldName('name');
|
|
1215
1259
|
if (!nameNode) continue;
|
|
@@ -1244,24 +1288,13 @@ function macroFact(region, origin) {
|
|
|
1244
1288
|
}
|
|
1245
1289
|
|
|
1246
1290
|
/**
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1291
|
+
* The invocations of a file the expansion pass reads, in document order:
|
|
1292
|
+
* project macros in scope that are worth expanding. Invocations without a
|
|
1293
|
+
* context or token tree are recorded blind in `rec`.
|
|
1249
1294
|
*/
|
|
1250
|
-
function
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
state.hygieneSeq = 0;
|
|
1254
|
-
const index = state.index;
|
|
1255
|
-
const fileEntry = index.files.get(filePath);
|
|
1256
|
-
const content = readContent(state, filePath);
|
|
1257
|
-
if (!fileEntry || content === null) return null;
|
|
1258
|
-
if (!fileMayExpand(state, filePath)) return { sites: 0, blind: [], used: new Map(), expanded: false };
|
|
1259
|
-
const tree = safeParse(state.parser, content, undefined, PARSE_OPTIONS);
|
|
1260
|
-
const rec = { sites: 0, blind: [], used: new Map(), expanded: false };
|
|
1261
|
-
const regions = [];
|
|
1262
|
-
let lineStarts = null;
|
|
1263
|
-
const invocations = tree.rootNode.descendantsOfType('macro_invocation');
|
|
1264
|
-
for (const node of invocations) {
|
|
1295
|
+
function plannedInvocations(state, filePath, content, tree, rec, invocations = null) {
|
|
1296
|
+
const plan = [];
|
|
1297
|
+
for (const node of invocations || tree.rootNode.descendantsOfType('macro_invocation')) {
|
|
1265
1298
|
const id = invocationName(node);
|
|
1266
1299
|
if (!id || !state.inventory.has(id.name)) continue;
|
|
1267
1300
|
let inDefinition = false;
|
|
@@ -1279,53 +1312,96 @@ function expandFile(state, filePath, mode = 'symbols') {
|
|
|
1279
1312
|
context === 'expr')) continue;
|
|
1280
1313
|
const tokenTree = node.namedChildren.find(child => child.type === 'token_tree');
|
|
1281
1314
|
if (info.compiled?.passThroughItems && context === 'items' && itemRecoveryPosition(node)) continue;
|
|
1282
|
-
|
|
1283
|
-
if (!
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1315
|
+
if (!context) { rec.blind.push({ line, macro: id.name, reason: 'context' }); continue; }
|
|
1316
|
+
if (!tokenTree) { rec.blind.push({ line, macro: id.name, reason: 'token-error' }); continue; }
|
|
1317
|
+
plan.push({ node, id, resolved, def, context, tokenTree, line });
|
|
1318
|
+
}
|
|
1319
|
+
return plan;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/** Expand one planned invocation: its region, or null (recorded blind). */
|
|
1323
|
+
function expandPlanned(state, filePath, content, item, rec) {
|
|
1324
|
+
const { node, id, resolved, def, context, tokenTree, line } = item;
|
|
1325
|
+
const site = {
|
|
1326
|
+
file: filePath, offset: node.startIndex, line, budget: MAX_OUTPUT_TOKENS,
|
|
1327
|
+
used: rec.used, nestedBlind: [], ambiguous: resolved.ambiguous,
|
|
1328
|
+
};
|
|
1329
|
+
let tokens;
|
|
1330
|
+
try {
|
|
1331
|
+
lineOffset(state, filePath, 1);
|
|
1332
|
+
const toks = argTokensOf(tokenTree, content, state.lineStarts.get(filePath));
|
|
1333
|
+
tokens = resolved.alternatives
|
|
1334
|
+
? expandAlternatives(state, site, resolved, toks, 0, context)
|
|
1335
|
+
: expandTokens(state, site, def, toks, 0);
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
if (!(error instanceof MacroBlind)) throw error;
|
|
1338
|
+
rec.blind.push({ line, macro: id.name, reason: error.reason });
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
for (const nested of site.nestedBlind) rec.blind.push({ line, macro: nested.macro, reason: `nested:${nested.reason}` });
|
|
1342
|
+
renameHygienicLocals(tokens);
|
|
1343
|
+
let body = tokens;
|
|
1344
|
+
if (context === 'expr') body = wrapExpression(tokens);
|
|
1345
|
+
const startLine = line;
|
|
1346
|
+
const endLine = node.endPosition.row + 1;
|
|
1347
|
+
const ancestors = new Set();
|
|
1348
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
1349
|
+
const nameNode = parent.type === 'impl_item' ? parent.childForFieldName('type')
|
|
1350
|
+
: DECLARATION_TYPES.includes(parent.type) ? parent.childForFieldName('name') : null;
|
|
1351
|
+
if (nameNode) {
|
|
1352
|
+
ancestors.add(`${parent.startPosition.row + 1}\0${parent.type === 'impl_item' ? typeNameOf(nameNode) : nameNode.text}`);
|
|
1317
1353
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
start: node.startIndex, end: node.endIndex, startLine, endLine,
|
|
1357
|
+
macro: id.name, definition: defKey(def), ambiguous: site.ambiguous, ancestors,
|
|
1358
|
+
node, layout: layout(body, startLine, node.startPosition.column, endLine),
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Expand the project macro invocations of one file. Returns the expansion
|
|
1364
|
+
* record, or null when the file invokes no project macro in scope.
|
|
1365
|
+
*/
|
|
1366
|
+
function expandFile(state, filePath, mode = 'symbols') {
|
|
1367
|
+
// Hygiene contexts are numbered per file, so an expansion's renamed
|
|
1368
|
+
// locals do not depend on what else this state expanded before.
|
|
1369
|
+
state.hygieneSeq = 0;
|
|
1370
|
+
const index = state.index;
|
|
1371
|
+
const fileEntry = index.files.get(filePath);
|
|
1372
|
+
const content = readContent(state, filePath);
|
|
1373
|
+
if (!fileEntry || content === null) return null;
|
|
1374
|
+
if (!fileMayExpand(state, filePath)) return { sites: 0, blind: [], used: new Map(), expanded: false };
|
|
1375
|
+
const tree = safeParse(state.parser, content, undefined, PARSE_OPTIONS);
|
|
1376
|
+
const rec = { sites: 0, blind: [], used: new Map(), expanded: false };
|
|
1377
|
+
const regions = [];
|
|
1378
|
+
// Each expansion's hygiene start and expanded length (fix #405): the
|
|
1379
|
+
// calls pass expands one group of units with the same numbering and
|
|
1380
|
+
// the same offsets as a pass over the whole file.
|
|
1381
|
+
const expansions = mode === 'symbols' ? [] : null;
|
|
1382
|
+
// The symbols pass also needs the file's literals and comments: one
|
|
1383
|
+
// walk lists both (fix #405b).
|
|
1384
|
+
const clean = !tree.rootNode.hasError;
|
|
1385
|
+
const walked = mode === 'symbols' && clean
|
|
1386
|
+
? tree.rootNode.descendantsOfType(['macro_invocation', ...NON_CODE_TOKENS]) : null;
|
|
1387
|
+
const invocations = walked ? walked.filter(node => node.type === 'macro_invocation') : null;
|
|
1388
|
+
const nonCode = walked ? walked.filter(node => node.type !== 'macro_invocation') : [];
|
|
1389
|
+
for (const item of plannedInvocations(state, filePath, content, tree, rec, invocations)) {
|
|
1390
|
+
const hygiene = state.hygieneSeq;
|
|
1391
|
+
const region = expandPlanned(state, filePath, content, item, rec);
|
|
1392
|
+
if (expansions) expansions.push(item.node.startIndex, hygiene, region ? region.layout.text.length : -1);
|
|
1393
|
+
if (region) regions.push(region);
|
|
1323
1394
|
}
|
|
1324
1395
|
rec.sites = regions.length;
|
|
1325
1396
|
if (regions.length === 0) return rec;
|
|
1326
1397
|
regions.sort((a, b) => a.start - b.start);
|
|
1398
|
+
// The identifiers each expansion outputs.
|
|
1399
|
+
const outputs = mode === 'symbols' ? regions.map(outputWordsOf) : null;
|
|
1327
1400
|
// Every word the expanded text of the calls pass can hold (fix #375).
|
|
1328
|
-
const words = mode === 'symbols' ? expansionWords(content, regions) : null;
|
|
1401
|
+
const words = mode === 'symbols' ? expansionWords(content, regions, nonCode, outputs) : null;
|
|
1402
|
+
// What each group of units can bear, read before parsing it (fix #405b).
|
|
1403
|
+
const groups = mode === 'symbols' && !tree.rootNode.hasError
|
|
1404
|
+
? groupRecords(content, regions, nonCode, outputs) : null;
|
|
1329
1405
|
|
|
1330
1406
|
// Parse the expanded file; an expansion that does not parse in its
|
|
1331
1407
|
// context stays as written (blind), and the rest are parsed again.
|
|
@@ -1356,41 +1432,14 @@ function expandFile(state, filePath, mode = 'symbols') {
|
|
|
1356
1432
|
if (regions.length === 0) return rec;
|
|
1357
1433
|
|
|
1358
1434
|
const rust = require('../languages/rust');
|
|
1359
|
-
// The expanded file holds no unexpanded item bodies worth recovering
|
|
1360
|
-
// inside its units; the extractors share the included-ranges tree.
|
|
1361
|
-
rust.primeDeclarationTrees(synthetic, state.parser, synthTree);
|
|
1362
1435
|
const mapper = offsetMapper(regions);
|
|
1363
|
-
|
|
1364
|
-
// Units are re-derived; everything outside them keeps its parse.
|
|
1365
|
-
// (Sorted and disjoint: binary searches.)
|
|
1366
|
-
const unitAtOffset = offset => {
|
|
1367
|
-
let lo = 0;
|
|
1368
|
-
let hi = units.length - 1;
|
|
1369
|
-
while (lo <= hi) {
|
|
1370
|
-
const mid = (lo + hi) >> 1;
|
|
1371
|
-
if (offset < units[mid].start) hi = mid - 1;
|
|
1372
|
-
else if (offset >= units[mid].end) lo = mid + 1;
|
|
1373
|
-
else return units[mid];
|
|
1374
|
-
}
|
|
1375
|
-
return null;
|
|
1376
|
-
};
|
|
1377
|
-
const byLine = (list, line) => {
|
|
1378
|
-
let lo = 0;
|
|
1379
|
-
let hi = list.length - 1;
|
|
1380
|
-
while (lo <= hi) {
|
|
1381
|
-
const mid = (lo + hi) >> 1;
|
|
1382
|
-
if (line < list[mid].startLine) hi = mid - 1;
|
|
1383
|
-
else if (line > list[mid].endLine) lo = mid + 1;
|
|
1384
|
-
else return list[mid];
|
|
1385
|
-
}
|
|
1386
|
-
return null;
|
|
1387
|
-
};
|
|
1388
|
-
const unitAtLine = line => byLine(units, line);
|
|
1389
|
-
const inUnit = record => (Number.isFinite(record.callStart) ? !!unitAtOffset(record.callStart)
|
|
1390
|
-
: !!unitAtLine(record.line));
|
|
1436
|
+
const lookups = unitLookups(units);
|
|
1391
1437
|
rec.expanded = true;
|
|
1392
1438
|
|
|
1393
1439
|
if (mode === 'symbols') {
|
|
1440
|
+
// The expanded file holds no unexpanded item bodies worth recovering
|
|
1441
|
+
// inside its units; the extractors share the included-ranges tree.
|
|
1442
|
+
rust.primeDeclarationTrees(synthetic, state.parser, synthTree);
|
|
1394
1443
|
// Declarations of the regions only: the smallest nodes spanning each.
|
|
1395
1444
|
const spanning = regions.map(region => (region.synthEnd > region.synthStart
|
|
1396
1445
|
? synthTree.rootNode.descendantForIndex(region.synthStart, region.synthEnd - 1) : null))
|
|
@@ -1401,44 +1450,195 @@ function expandFile(state, filePath, mode = 'symbols') {
|
|
|
1401
1450
|
if (last && node.startIndex >= last.startIndex && node.endIndex <= last.endIndex) continue;
|
|
1402
1451
|
roots.push(node);
|
|
1403
1452
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1453
|
+
// One walk of each root serves both extractions (fix #405b). In a
|
|
1454
|
+
// file that parses cleanly an expansion stands as whole items,
|
|
1455
|
+
// statements or a wrapped expression, so a declaration named inside
|
|
1456
|
+
// it lies inside the root spanning it; otherwise the whole tree is
|
|
1457
|
+
// searched.
|
|
1458
|
+
const walked = roots.map(root => root.descendantsOfType(declarationWalkTypesOf()));
|
|
1459
|
+
rec.symbols = generatedSymbols(filePath, rust.parseDeclarationsIn(synthetic, roots, {
|
|
1460
|
+
descendants: (root, i) => walked[i],
|
|
1461
|
+
}), {
|
|
1462
|
+
regions, mapper, generated: generatedDeclarations(synthTree.rootNode, regions,
|
|
1463
|
+
tree.rootNode.hasError ? null : { roots, walked }),
|
|
1406
1464
|
});
|
|
1407
1465
|
rec.calleeNames = callableNames(regions);
|
|
1466
|
+
rec.expansions = expansions;
|
|
1467
|
+
// Groups as the calls pass reads them apart: only when no expansion
|
|
1468
|
+
// was left blind by a parse error (the pass then reads the file in
|
|
1469
|
+
// one pass, whose groups can differ).
|
|
1408
1470
|
rec.words = words;
|
|
1471
|
+
if (groups && !rec.blind.some(site => site.reason === 'parse-error')) rec.groups = groups;
|
|
1409
1472
|
rec.callsPending = true;
|
|
1410
1473
|
return rec;
|
|
1411
1474
|
}
|
|
1412
1475
|
|
|
1413
1476
|
// Calls: records of the units from the expanded file, the rest as parsed,
|
|
1414
|
-
// plus the invocations themselves.
|
|
1477
|
+
// plus the invocations themselves (a derivation context, fix #405).
|
|
1478
|
+
rec.context = callsContext(state, filePath, {
|
|
1479
|
+
content, tree, regions, units, synthetic, synthTree, mapper, lookups,
|
|
1480
|
+
});
|
|
1481
|
+
return rec;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/** Binary-search lookups over sorted, disjoint units. */
|
|
1485
|
+
function unitLookups(units) {
|
|
1486
|
+
const unitAtOffset = offset => {
|
|
1487
|
+
let lo = 0;
|
|
1488
|
+
let hi = units.length - 1;
|
|
1489
|
+
while (lo <= hi) {
|
|
1490
|
+
const mid = (lo + hi) >> 1;
|
|
1491
|
+
if (offset < units[mid].start) hi = mid - 1;
|
|
1492
|
+
else if (offset >= units[mid].end) lo = mid + 1;
|
|
1493
|
+
else return units[mid];
|
|
1494
|
+
}
|
|
1495
|
+
return null;
|
|
1496
|
+
};
|
|
1497
|
+
const unitAtLine = line => byLine(units, line);
|
|
1498
|
+
const inUnit = record => (Number.isFinite(record.callStart) ? !!unitAtOffset(record.callStart)
|
|
1499
|
+
: !!unitAtLine(record.line));
|
|
1500
|
+
return { unitAtOffset, unitAtLine, inUnit };
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function byLine(list, line) {
|
|
1504
|
+
let lo = 0;
|
|
1505
|
+
let hi = list.length - 1;
|
|
1506
|
+
while (lo <= hi) {
|
|
1507
|
+
const mid = (lo + hi) >> 1;
|
|
1508
|
+
if (line < list[mid].startLine) hi = mid - 1;
|
|
1509
|
+
else if (line > list[mid].endLine) lo = mid + 1;
|
|
1510
|
+
else return list[mid];
|
|
1511
|
+
}
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// ── Calls pass (fix #405) ─────────────────────────────────────────────────
|
|
1516
|
+
//
|
|
1517
|
+
// The calls of an expanded file are the parsed records outside its units,
|
|
1518
|
+
// the records each unit yields in the expanded file, and the invocations
|
|
1519
|
+
// themselves. Units sharing a line form one group (records without a span
|
|
1520
|
+
// are placed by line). The extractor keeps no scope across functions, so a
|
|
1521
|
+
// group's records do not depend on the other groups: a query that needs
|
|
1522
|
+
// the records bearing one name, or those of a line range, reads only the
|
|
1523
|
+
// groups that can hold them, and the file's calls are composed, as one pass
|
|
1524
|
+
// derives them, when a consumer needs them all. Where the build recorded
|
|
1525
|
+
// every expansion's hygiene start and expanded length, a group expands and
|
|
1526
|
+
// parses alone with the numbering and the offsets of the whole file;
|
|
1527
|
+
// otherwise the file's expansions parse in one pass and each group's
|
|
1528
|
+
// records are read from that tree with the other units skipped.
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Everything the calls pass of a file shares: its regions, units and their
|
|
1532
|
+
* groups, the expanded file and its tree. Per-group records are added as
|
|
1533
|
+
* groups are read.
|
|
1534
|
+
*/
|
|
1535
|
+
function callsContext(state, filePath, parts) {
|
|
1536
|
+
const { content, tree, regions, units, synthetic, synthTree, mapper, lookups } = parts;
|
|
1537
|
+
const index = state.index;
|
|
1538
|
+
// Units sharing a line form one group.
|
|
1539
|
+
const groups = groupsOf(units);
|
|
1540
|
+
// Unit spans in the expanded file: a unit starts and ends outside every
|
|
1541
|
+
// region, shifted by the regions before it.
|
|
1542
|
+
const toSynth = offset => {
|
|
1543
|
+
let lo = 0;
|
|
1544
|
+
let hi = regions.length - 1;
|
|
1545
|
+
let before = null;
|
|
1546
|
+
while (lo <= hi) {
|
|
1547
|
+
const mid = (lo + hi) >> 1;
|
|
1548
|
+
if (regions[mid].end <= offset) { before = regions[mid]; lo = mid + 1; } else hi = mid - 1;
|
|
1549
|
+
}
|
|
1550
|
+
return offset + (before ? before.deltaAfter : 0);
|
|
1551
|
+
};
|
|
1552
|
+
for (const unit of units) {
|
|
1553
|
+
unit.synthStart = toSynth(unit.start);
|
|
1554
|
+
unit.synthEnd = toSynth(unit.end);
|
|
1555
|
+
}
|
|
1415
1556
|
const cached = index.callsCache.get(filePath);
|
|
1416
1557
|
const original = cached?.calls || [];
|
|
1417
|
-
const invocationStarts = new Set(regions.map(region => region.start));
|
|
1418
|
-
const calls = original.filter(record => !inUnit(record));
|
|
1419
|
-
const seen = new Set();
|
|
1420
1558
|
// Records without a source span (struct-literal constructors) are placed
|
|
1421
1559
|
// by line: one the unexpanded parse also has on that line is ordinary
|
|
1422
1560
|
// code, any other one on an invocation's lines was generated.
|
|
1423
|
-
const spanless = record => `${record.name}\0${record.line}\0${record.isConstructor ? 1 : 0}`;
|
|
1424
1561
|
const originalSpanless = new Map();
|
|
1425
1562
|
for (const record of original) {
|
|
1426
|
-
if (Number.isFinite(record.callStart) || !unitAtLine(record.line)) continue;
|
|
1427
|
-
const key =
|
|
1563
|
+
if (Number.isFinite(record.callStart) || !lookups.unitAtLine(record.line)) continue;
|
|
1564
|
+
const key = spanlessKey(record);
|
|
1428
1565
|
originalSpanless.set(key, (originalSpanless.get(key) || 0) + 1);
|
|
1429
1566
|
}
|
|
1567
|
+
const rust = require('../languages/rust');
|
|
1568
|
+
return {
|
|
1569
|
+
filePath, content, tree, regions, units, groups, synthetic, synthTree, mapper, lookups,
|
|
1570
|
+
original, originalSpanless, seen: new Set(),
|
|
1571
|
+
itemRecovery: rust.rustExpandedItemRecovery(synthetic, state.parser, synthTree),
|
|
1572
|
+
// An expanded file whose parse holds an error is read in one pass.
|
|
1573
|
+
groupwise: !synthTree.rootNode.hasError,
|
|
1574
|
+
invocations: new Map(),
|
|
1575
|
+
parser: state.parser,
|
|
1576
|
+
live: true,
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function spanlessKey(record) {
|
|
1581
|
+
return `${record.name}\0${record.line}\0${record.isConstructor ? 1 : 0}`;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/** Read the records of the groups `wanted` (indices) not read yet. */
|
|
1585
|
+
function readGroups(ctx, wanted) {
|
|
1586
|
+
let targets = wanted.filter(i => ctx.groups[i].records === null);
|
|
1587
|
+
if (targets.length === 0) return;
|
|
1588
|
+
if (ctx.isolated) {
|
|
1589
|
+
readIsolatedGroups(ctx, targets);
|
|
1590
|
+
ctx.readLookups = null;
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (!ctx.groupwise) targets = ctx.groups.map((group, i) => i).filter(i => ctx.groups[i].records === null);
|
|
1594
|
+
const target = new Set(targets);
|
|
1595
|
+
const all = target.size === ctx.groups.filter(group => group.records === null).length &&
|
|
1596
|
+
ctx.groups.every(group => group.records === null);
|
|
1597
|
+
const units = ctx.units;
|
|
1598
|
+
// Nodes inside a unit of another group are not read.
|
|
1599
|
+
const skip = all ? null : node => {
|
|
1600
|
+
const start = node.startIndex;
|
|
1601
|
+
let lo = 0;
|
|
1602
|
+
let hi = units.length - 1;
|
|
1603
|
+
while (lo <= hi) {
|
|
1604
|
+
const mid = (lo + hi) >> 1;
|
|
1605
|
+
const unit = units[mid];
|
|
1606
|
+
if (start < unit.synthStart) hi = mid - 1;
|
|
1607
|
+
else if (start >= unit.synthEnd) lo = mid + 1;
|
|
1608
|
+
else return node.endIndex <= unit.synthEnd && !target.has(unit.group);
|
|
1609
|
+
}
|
|
1610
|
+
return false;
|
|
1611
|
+
};
|
|
1612
|
+
for (const i of targets) ctx.groups[i].records = [];
|
|
1613
|
+
const rust = require('../languages/rust');
|
|
1614
|
+
const synthCalls = rust.findCallsInCode(ctx.synthetic, ctx.parser, {
|
|
1615
|
+
tree: ctx.synthTree, itemRecovery: ctx.itemRecovery, skip,
|
|
1616
|
+
});
|
|
1617
|
+
mergeRecords(ctx, synthCalls, {
|
|
1618
|
+
mapper: ctx.mapper, lookups: ctx.lookups, regions: ctx.regions, synthTree: ctx.synthTree,
|
|
1619
|
+
originalSpanless: ctx.originalSpanless,
|
|
1620
|
+
accept: unit => target.has(unit.group), push: (unit, call) => ctx.groups[unit.group].records.push(call),
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* The records an expanded file's parse yields for the accepted units, with
|
|
1626
|
+
* source offsets, expansion facts, and repeats dropped (`ctx.seen`).
|
|
1627
|
+
*/
|
|
1628
|
+
function mergeRecords(ctx, synthCalls, { mapper, lookups, regions, synthTree, originalSpanless, accept, push }) {
|
|
1430
1629
|
const regionOnLine = line => byLine(regions, line);
|
|
1431
|
-
const synthCalls = rust.findCallsInCode(synthetic, state.parser);
|
|
1432
1630
|
for (const record of synthCalls) {
|
|
1433
1631
|
const synthStart = record.callStart;
|
|
1632
|
+
let unit;
|
|
1434
1633
|
if (Number.isFinite(synthStart)) {
|
|
1435
1634
|
const mapped = mapper.map(synthStart, false);
|
|
1436
|
-
|
|
1437
|
-
} else
|
|
1635
|
+
unit = lookups.unitAtOffset(Math.floor(mapped));
|
|
1636
|
+
} else unit = lookups.unitAtLine(record.line);
|
|
1637
|
+
if (!unit || !accept(unit)) continue;
|
|
1438
1638
|
const call = { ...record };
|
|
1439
1639
|
let region = Number.isFinite(synthStart) ? mapper.regionAt(synthStart) : null;
|
|
1440
1640
|
if (!Number.isFinite(synthStart) && regionOnLine(record.line)) {
|
|
1441
|
-
const key =
|
|
1641
|
+
const key = spanlessKey(record);
|
|
1442
1642
|
const left = originalSpanless.get(key) || 0;
|
|
1443
1643
|
if (left > 0) originalSpanless.set(key, left - 1);
|
|
1444
1644
|
else region = regionOnLine(record.line);
|
|
@@ -1463,24 +1663,84 @@ function expandFile(state, filePath, mode = 'symbols') {
|
|
|
1463
1663
|
: `t\0${call.name}\0${call.line}\0${call.isMethod ? 1 : 0}\0${call.receiver || ''}\0` +
|
|
1464
1664
|
`${call.receiverType || ''}\0${call.receiverRootType || ''}\0${call.receiverField || ''}\0` +
|
|
1465
1665
|
`${call.enclosingFunction?.name || ''}\0${call.argCount ?? ''}`;
|
|
1466
|
-
if (seen.has(key)) continue;
|
|
1467
|
-
seen.add(key);
|
|
1666
|
+
if (ctx.seen.has(key)) continue;
|
|
1667
|
+
ctx.seen.add(key);
|
|
1468
1668
|
}
|
|
1469
|
-
|
|
1669
|
+
push(unit, call);
|
|
1470
1670
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* The invocations stay calls of their macros; the value one produces is the
|
|
1675
|
+
* expansion's, so its own value-flow facts are dropped. Built once per
|
|
1676
|
+
* context (the same records in every view and in the composed calls).
|
|
1677
|
+
*/
|
|
1678
|
+
function invocationRecords(ctx) {
|
|
1679
|
+
// Expanded invocations: every region (one pass), the regions of the
|
|
1680
|
+
// groups read so far (groups read apart).
|
|
1681
|
+
const starts = new Set();
|
|
1682
|
+
if (ctx.isolated) {
|
|
1683
|
+
for (const group of ctx.groups) for (const start of group.starts || []) starts.add(start);
|
|
1684
|
+
} else {
|
|
1685
|
+
for (const region of ctx.regions) starts.add(region.start);
|
|
1483
1686
|
}
|
|
1687
|
+
const out = [];
|
|
1688
|
+
for (const record of ctx.original) {
|
|
1689
|
+
if (!record.isMacro || !starts.has(record.callStart)) continue;
|
|
1690
|
+
let invocation = ctx.invocations.get(record);
|
|
1691
|
+
if (!invocation) {
|
|
1692
|
+
invocation = {
|
|
1693
|
+
name: record.name, line: record.line, callStart: record.callStart, callEnd: record.callEnd,
|
|
1694
|
+
isMethod: false, isMacro: true,
|
|
1695
|
+
...(record.receiver && { receiver: record.receiver, isPathMacro: true }),
|
|
1696
|
+
...(record.macroExpr && { macroExpr: true }),
|
|
1697
|
+
enclosingFunction: record.enclosingFunction,
|
|
1698
|
+
macroExpanded: true,
|
|
1699
|
+
};
|
|
1700
|
+
ctx.invocations.set(record, invocation);
|
|
1701
|
+
}
|
|
1702
|
+
out.push(invocation);
|
|
1703
|
+
}
|
|
1704
|
+
return out;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/** Whether a parsed record lies in a unit of a group read so far (all units, one pass). */
|
|
1708
|
+
function inReadUnit(ctx, record) {
|
|
1709
|
+
if (!ctx.isolated) return ctx.lookups.inUnit(record);
|
|
1710
|
+
if (!ctx.readLookups) {
|
|
1711
|
+
const units = [];
|
|
1712
|
+
for (const group of ctx.groups) if (group.units) units.push(...group.units);
|
|
1713
|
+
units.sort((a, b) => a.start - b.start);
|
|
1714
|
+
ctx.readLookups = unitLookups(units);
|
|
1715
|
+
}
|
|
1716
|
+
return ctx.readLookups.inUnit(record);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* The calls of the file: parsed records outside the units, each group's
|
|
1721
|
+
* records, the invocations; stable by line. Installed in the calls cache
|
|
1722
|
+
* with the facts the file entry derives from them.
|
|
1723
|
+
*/
|
|
1724
|
+
function composeCalls(index, ctx) {
|
|
1725
|
+
const calls = composedCalls(ctx);
|
|
1726
|
+
installComposedCalls(index, ctx.filePath, calls, ctx.original);
|
|
1727
|
+
return calls;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/** The calls a derivation context composes, stable by line (not installed). */
|
|
1731
|
+
function composedCalls(ctx) {
|
|
1732
|
+
readGroups(ctx, ctx.groups.map((group, i) => i));
|
|
1733
|
+
const calls = ctx.original.filter(record => !inReadUnit(ctx, record));
|
|
1734
|
+
for (const group of ctx.groups) for (const record of group.records) calls.push(record);
|
|
1735
|
+
for (const record of invocationRecords(ctx)) calls.push(record);
|
|
1736
|
+
// Stable by line: records keep document order within a line.
|
|
1737
|
+
calls.sort((a, b) => a.line - b.line);
|
|
1738
|
+
return calls;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/** Install a file's composed calls in the calls cache, with the facts the file entry derives from them. */
|
|
1742
|
+
function installComposedCalls(index, filePath, calls, original) {
|
|
1743
|
+
const fileEntry = index.files.get(filePath);
|
|
1484
1744
|
// Shapes of generated calls whose value may be lost (audit-async).
|
|
1485
1745
|
const { openCallShape } = require('../languages/rust-value-flow');
|
|
1486
1746
|
const shapes = calls.filter(call => call.macroExpansion && call.name && !call.inMacro && !call.valueConsumed)
|
|
@@ -1488,8 +1748,7 @@ function expandFile(state, filePath, mode = 'symbols') {
|
|
|
1488
1748
|
if (shapes.length > 0) {
|
|
1489
1749
|
fileEntry.openCalls = [...new Set([...(fileEntry.openCalls || []), ...shapes])].sort(codeUnitCompare);
|
|
1490
1750
|
}
|
|
1491
|
-
|
|
1492
|
-
calls.sort((a, b) => a.line - b.line);
|
|
1751
|
+
const cached = index.callsCache.get(filePath);
|
|
1493
1752
|
if (cached) {
|
|
1494
1753
|
if (index.calleeIndex) index._removeFromCalleeIndex(filePath, original);
|
|
1495
1754
|
cached.calls = calls;
|
|
@@ -1498,8 +1757,6 @@ function expandFile(state, filePath, mode = 'symbols') {
|
|
|
1498
1757
|
index.callsCache.set(filePath, { mtime: fileEntry.mtime, hash: fileEntry.hash, calls });
|
|
1499
1758
|
}
|
|
1500
1759
|
index.callsCacheDirty = true;
|
|
1501
|
-
rec.expanded = true;
|
|
1502
|
-
return rec;
|
|
1503
1760
|
}
|
|
1504
1761
|
|
|
1505
1762
|
const CALL_NODES = new Set(['call_expression', 'macro_invocation', 'struct_expression', 'generic_function']);
|
|
@@ -1621,11 +1878,12 @@ function callableNames(regions) {
|
|
|
1621
1878
|
for (const region of regions) {
|
|
1622
1879
|
const marks = region.layout.marks;
|
|
1623
1880
|
for (let i = 0; i < marks.length - 1; i++) {
|
|
1881
|
+
const next = marks[i + 1].leaf.v;
|
|
1882
|
+
if (!(next === '(' || next === '!' || next === '{' ||
|
|
1883
|
+
(next === '::' && marks[i + 2]?.leaf.v === '<'))) continue;
|
|
1624
1884
|
const leaf = marks[i].leaf;
|
|
1625
1885
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(leaf.v) || RUST_KEYWORDS.has(leaf.v)) continue;
|
|
1626
|
-
|
|
1627
|
-
if (next === '(' || next === '!' || next === '{' ||
|
|
1628
|
-
(next === '::' && marks[i + 2]?.leaf.v === '<')) names.add(leaf.v);
|
|
1886
|
+
names.add(leaf.v);
|
|
1629
1887
|
}
|
|
1630
1888
|
}
|
|
1631
1889
|
return [...names].sort(codeUnitCompare);
|
|
@@ -1634,27 +1892,117 @@ function callableNames(regions) {
|
|
|
1634
1892
|
const WORD = /[\p{L}\p{N}_]+/gu;
|
|
1635
1893
|
const WHOLE_WORD = /^[\p{L}\p{N}_]+$/u;
|
|
1636
1894
|
|
|
1895
|
+
// Tokens whose text is never an identifier: literals and comments.
|
|
1896
|
+
const NON_CODE_TOKENS = ['string_literal', 'raw_string_literal', 'char_literal', 'line_comment', 'block_comment'];
|
|
1897
|
+
|
|
1637
1898
|
/**
|
|
1638
1899
|
* The words (identifier-like runs) of the text the calls pass analyzes for
|
|
1639
1900
|
* these regions: the kept source around them (units and their headers) and
|
|
1640
1901
|
* every expansion's tokens. A call record derived from that text can bear no
|
|
1641
1902
|
* other name, so a file whose calls are not derived yet needs deriving only
|
|
1642
|
-
* for a name among them (fix #375).
|
|
1903
|
+
* for a name among them (fix #375). A record's name and written receiver are
|
|
1904
|
+
* identifier tokens, so the text of literals and comments is left out (fix
|
|
1905
|
+
* #405: `"--count"` and `// the count` made a `count` query derive four
|
|
1906
|
+
* test files); kept ranges are whole items and headers, which the calls
|
|
1907
|
+
* pass lexes as the file's own parse did. A file with parse errors is read
|
|
1908
|
+
* whole. Space-separated, sorted.
|
|
1643
1909
|
*/
|
|
1644
|
-
function expansionWords(content, regions) {
|
|
1910
|
+
function expansionWords(content, regions, nonCode = [], outputs = regions.map(outputWordsOf)) {
|
|
1645
1911
|
const words = new Set();
|
|
1646
|
-
|
|
1912
|
+
scanCodeWords(content, analysisUnits(regions, 'calls').keep, nonCode, words);
|
|
1913
|
+
for (const output of outputs) for (const word of output) words.add(word);
|
|
1914
|
+
return [...words].sort(codeUnitCompare).join(' ');
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/** Words of `ranges` (sorted, disjoint) of the source, literals and comments (`nonCode`, sorted) left out. */
|
|
1918
|
+
function scanCodeWords(content, ranges, nonCode, words) {
|
|
1919
|
+
const scan = (start, end) => {
|
|
1920
|
+
if (end <= start) return;
|
|
1647
1921
|
for (const word of content.slice(start, end).match(WORD) || []) words.add(word);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1922
|
+
};
|
|
1923
|
+
let next = 0;
|
|
1924
|
+
for (const [start, end] of ranges) {
|
|
1925
|
+
let at = start;
|
|
1926
|
+
while (next < nonCode.length && nonCode[next].endIndex <= at) next++;
|
|
1927
|
+
for (let i = next; i < nonCode.length; i++) {
|
|
1928
|
+
const token = nonCode[i];
|
|
1929
|
+
if (token.startIndex >= end) break;
|
|
1930
|
+
scan(at, Math.min(token.startIndex, end));
|
|
1931
|
+
at = Math.max(at, token.endIndex);
|
|
1655
1932
|
}
|
|
1933
|
+
scan(at, end);
|
|
1656
1934
|
}
|
|
1657
|
-
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
/** The identifier tokens an expansion outputs (a non-ASCII identifier is a 'punct' leaf), once each. */
|
|
1938
|
+
function outputWordsOf(region) {
|
|
1939
|
+
const words = new Set();
|
|
1940
|
+
for (const mark of region.layout.marks) {
|
|
1941
|
+
const leaf = mark.leaf;
|
|
1942
|
+
if (leaf.k === 'ident' || (leaf.k === 'punct' && WHOLE_WORD.test(leaf.v))) words.add(leaf.v);
|
|
1943
|
+
}
|
|
1944
|
+
return [...words];
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/**
|
|
1948
|
+
* The groups of units the calls pass reads apart (fix #405b), each as
|
|
1949
|
+
* [startLine, endLine, expanded length, words]: the identifiers its
|
|
1950
|
+
* expansions output (with those of any expansion in its kept headers) and
|
|
1951
|
+
* the code of its kept headers outside its units (`Self { .. }` names the
|
|
1952
|
+
* impl's type). A record a group derives in the units' own code is written
|
|
1953
|
+
* there, as the file's parsed record on that line is: the calls pass
|
|
1954
|
+
* consults those for the rest (rustPendingRecords). Expanded length: the
|
|
1955
|
+
* text the group's own expansions add, for the cost of reading it.
|
|
1956
|
+
*/
|
|
1957
|
+
function groupRecords(content, regions, nonCode, outputs) {
|
|
1958
|
+
const { units } = analysisUnits(regions, 'calls');
|
|
1959
|
+
const groups = groupsOf(units);
|
|
1960
|
+
const lookups = unitLookups(units);
|
|
1961
|
+
const members = groups.map(() => []);
|
|
1962
|
+
const groupOf = regions.map((region, i) => {
|
|
1963
|
+
const group = lookups.unitAtOffset(region.start).group;
|
|
1964
|
+
members[group].push(i);
|
|
1965
|
+
return group;
|
|
1966
|
+
});
|
|
1967
|
+
// Expansions are disjoint and sorted: the first one ending after `at`.
|
|
1968
|
+
const firstEndingAfter = at => {
|
|
1969
|
+
let lo = 0;
|
|
1970
|
+
let hi = regions.length;
|
|
1971
|
+
while (lo < hi) {
|
|
1972
|
+
const mid = (lo + hi) >> 1;
|
|
1973
|
+
if (regions[mid].end <= at) lo = mid + 1; else hi = mid;
|
|
1974
|
+
}
|
|
1975
|
+
return lo;
|
|
1976
|
+
};
|
|
1977
|
+
return groups.map((group, gi) => {
|
|
1978
|
+
const keep = analysisUnits(members[gi].map(i => regions[i]), 'calls').keep;
|
|
1979
|
+
const words = new Set();
|
|
1980
|
+
let length = 0;
|
|
1981
|
+
for (const i of members[gi]) {
|
|
1982
|
+
length += regions[i].layout.text.length;
|
|
1983
|
+
for (const word of outputs[i]) words.add(word);
|
|
1984
|
+
}
|
|
1985
|
+
// Expansions inside its kept headers (`impl T for m!() {..}`).
|
|
1986
|
+
for (const [start, end] of keep) {
|
|
1987
|
+
for (let i = firstEndingAfter(start); i < regions.length && regions[i].start < end; i++) {
|
|
1988
|
+
if (groupOf[i] !== gi) for (const word of outputs[i]) words.add(word);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
// Header code: the kept ranges outside the group's units.
|
|
1992
|
+
const headers = [];
|
|
1993
|
+
for (const [start, end] of keep) {
|
|
1994
|
+
let at = start;
|
|
1995
|
+
for (const unit of group.units) {
|
|
1996
|
+
if (unit.end <= at || unit.start >= end) continue;
|
|
1997
|
+
if (unit.start > at) headers.push([at, unit.start]);
|
|
1998
|
+
at = Math.max(at, unit.end);
|
|
1999
|
+
}
|
|
2000
|
+
if (at < end) headers.push([at, end]);
|
|
2001
|
+
}
|
|
2002
|
+
scanCodeWords(content, headers, nonCode, words);
|
|
2003
|
+
// Insertion order is the expansions' and the text's: deterministic.
|
|
2004
|
+
return [group.startLine, group.endLine, length, [...words].join(' ')];
|
|
2005
|
+
});
|
|
1658
2006
|
}
|
|
1659
2007
|
|
|
1660
2008
|
/** Whether the derived calls of a file pending derivation can bear `name`. */
|
|
@@ -1665,8 +2013,56 @@ function rustPendingMayBear(fileEntry, name) {
|
|
|
1665
2013
|
if (typeof rec.words !== 'string') return true;
|
|
1666
2014
|
let words = pendingWordSets.get(rec);
|
|
1667
2015
|
if (!words) pendingWordSets.set(rec, words = new Set(rec.words.split(' ')));
|
|
1668
|
-
|
|
1669
|
-
|
|
2016
|
+
return hasWord(words, name);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Whether a recorded group's word list (space-separated) holds `name`, as
|
|
2021
|
+
* hasWord does for a set (fix #405b: one name is asked per group, so the
|
|
2022
|
+
* list is searched rather than split).
|
|
2023
|
+
*/
|
|
2024
|
+
function groupHasWord(list, name) {
|
|
2025
|
+
const text = String(name);
|
|
2026
|
+
const bare = text.replace(/^r#/, '');
|
|
2027
|
+
return listHasWord(list, text) || (bare !== text && listHasWord(list, bare)) || listHasWord(list, `r#${text}`);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
function listHasWord(list, word) {
|
|
2031
|
+
if (!word) return false;
|
|
2032
|
+
for (let at = list.indexOf(word); at >= 0; at = list.indexOf(word, at + 1)) {
|
|
2033
|
+
const end = at + word.length;
|
|
2034
|
+
if ((at === 0 || list.charCodeAt(at - 1) === 32) && (end === list.length || list.charCodeAt(end) === 32)) return true;
|
|
2035
|
+
}
|
|
2036
|
+
return false;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
const pendingFileLists = new WeakMap(); // operation content cache -> pending file paths
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* Files whose macro expansions' calls are not derived yet and whose
|
|
2043
|
+
* expanded text can hold one of `names` (fix #405). The callee index lists
|
|
2044
|
+
* such a file under its parsed records' names and its expansions'
|
|
2045
|
+
* callee-position names, but a derived record can bear any word of the
|
|
2046
|
+
* expanded text (`self.view` passed as a value, `Self { .. }` named by its
|
|
2047
|
+
* impl): a query visits these files too, so the files it reads do not
|
|
2048
|
+
* depend on which files earlier queries derived.
|
|
2049
|
+
*/
|
|
2050
|
+
function rustPendingFilesBearing(index, names) {
|
|
2051
|
+
const scope = index._opContentCache;
|
|
2052
|
+
let files = scope ? pendingFileLists.get(scope) : undefined;
|
|
2053
|
+
if (!files) {
|
|
2054
|
+
files = [];
|
|
2055
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
2056
|
+
if (fileEntry.rustMacroExpansion?.callsPending) files.push(filePath);
|
|
2057
|
+
}
|
|
2058
|
+
if (scope) pendingFileLists.set(scope, files);
|
|
2059
|
+
}
|
|
2060
|
+
const bearing = [];
|
|
2061
|
+
for (const filePath of files) {
|
|
2062
|
+
const fileEntry = index.files.get(filePath);
|
|
2063
|
+
if (names.some(name => rustPendingMayBear(fileEntry, name))) bearing.push(filePath);
|
|
2064
|
+
}
|
|
2065
|
+
return bearing;
|
|
1670
2066
|
}
|
|
1671
2067
|
|
|
1672
2068
|
function tokenText(tokens) {
|
|
@@ -1771,11 +2167,62 @@ function startRustMacroExpansion(index, pending, options = {}) {
|
|
|
1771
2167
|
const files = invokingFiles(index, pending, inventory, names);
|
|
1772
2168
|
let bytes = 0;
|
|
1773
2169
|
for (const file of files) bytes += index.files.get(file)?.size || 0;
|
|
1774
|
-
|
|
1775
|
-
|
|
2170
|
+
// A sequential build has no parse workers to keep (fix #422): when the
|
|
2171
|
+
// planned expansion is large - a crate whose API macros generate most of
|
|
2172
|
+
// its code has little source to parse - fresh workers expand it.
|
|
2173
|
+
let pool = options.pool?.available.length > 0 ? options.pool : null;
|
|
2174
|
+
let ownPool = null;
|
|
2175
|
+
if (!pool && freshExpansionWorthwhile(index, files, inventory)) {
|
|
2176
|
+
ownPool = require('./parallel-build').spawnExpansionPool(index,
|
|
2177
|
+
freshWorkerCount(index, Math.ceil(files.length / FRESH_FILES_PER_WORKER), files.length));
|
|
2178
|
+
pool = ownPool;
|
|
2179
|
+
}
|
|
2180
|
+
const run = files.length >= PARALLEL_MIN_FILES && (bytes >= PARALLEL_MIN_BYTES || ownPool)
|
|
2181
|
+
? startWorkers(index, files, { ...options, pool, inventory }) : null;
|
|
2182
|
+
if (ownPool) {
|
|
2183
|
+
if (run) {
|
|
2184
|
+
const dispose = run.dispose;
|
|
2185
|
+
run.dispose = () => { dispose(); ownPool.dispose(); };
|
|
2186
|
+
} else {
|
|
2187
|
+
ownPool.dispose();
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
1776
2190
|
return { inventory, files, names, results: run };
|
|
1777
2191
|
}
|
|
1778
2192
|
|
|
2193
|
+
// Fresh expansion workers start cold (module load, parser warm-up): they
|
|
2194
|
+
// pay for themselves from this much planned expansion.
|
|
2195
|
+
const FRESH_MIN_SITES = 48;
|
|
2196
|
+
const FRESH_MIN_FILES = 12;
|
|
2197
|
+
const FRESH_FILES_PER_WORKER = 6;
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* How many fresh threads to start for `wanted` workers' worth of work over
|
|
2201
|
+
* `files` files: 2 to EXPANSION_WORKERS, never more than an explicit build
|
|
2202
|
+
* worker count (a request for that execution shape) and none when the build
|
|
2203
|
+
* disabled workers; 0 when fewer than two would run.
|
|
2204
|
+
*/
|
|
2205
|
+
function freshWorkerCount(index, wanted, files) {
|
|
2206
|
+
const limit = index.lastBuildWorkerLimit ?? EXPANSION_WORKERS;
|
|
2207
|
+
const count = Math.min(EXPANSION_WORKERS, limit, files, Math.max(2, wanted));
|
|
2208
|
+
return count >= 2 ? count : 0;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/** Is the planned expansion of a build without kept parse workers worth fresh workers? */
|
|
2212
|
+
function freshExpansionWorthwhile(index, files, inventory) {
|
|
2213
|
+
const env = process.env.UCN_FRESH_EXPANSION;
|
|
2214
|
+
if (env === '0' || freshWorkerCount(index, 2, files.length) === 0) return false;
|
|
2215
|
+
if (env === '1') return true;
|
|
2216
|
+
if (files.length < FRESH_MIN_FILES) return false;
|
|
2217
|
+
let sites = 0;
|
|
2218
|
+
for (const file of files) {
|
|
2219
|
+
for (const call of index.callsCache.get(file)?.calls || []) {
|
|
2220
|
+
if (call.isMacro && inventory.has(call.name)) sites++;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
return sites >= FRESH_MIN_SITES;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
1779
2226
|
/**
|
|
1780
2227
|
* Build-time pass, phase 2b (after the import graph resolved `mod`
|
|
1781
2228
|
* declarations): apply the expansions of the planned files, expanding in
|
|
@@ -1824,7 +2271,10 @@ function applyRustMacroExpansion(index, pending, options = {}, started = undefin
|
|
|
1824
2271
|
...(rec.blind.length > 0 && { blind: rec.blind.sort((a, b) => a.line - b.line ||
|
|
1825
2272
|
codeUnitCompare(a.macro, b.macro) || codeUnitCompare(a.reason, b.reason)) }),
|
|
1826
2273
|
...(rec.expanded && { expanded: true }),
|
|
1827
|
-
...(rec.callsPending && {
|
|
2274
|
+
...(rec.callsPending && {
|
|
2275
|
+
callsPending: true, calleeNames: rec.calleeNames || [], words: rec.words, expansions: rec.expansions,
|
|
2276
|
+
...(rec.groups && { groups: rec.groups }),
|
|
2277
|
+
}),
|
|
1828
2278
|
};
|
|
1829
2279
|
}
|
|
1830
2280
|
return touched || false;
|
|
@@ -1847,7 +2297,7 @@ const EXPANSION_WORKERS = 3;
|
|
|
1847
2297
|
* to expand their import facts, impl/trait spans and macro invocation
|
|
1848
2298
|
* records. Also every name a project callable bears.
|
|
1849
2299
|
*/
|
|
1850
|
-
function workerSnapshot(index, workload, inventory) {
|
|
2300
|
+
function workerSnapshot(index, workload, inventory, fullCalls = false) {
|
|
1851
2301
|
const expanding = new Set(workload);
|
|
1852
2302
|
// Invocations resolve at the invoking file, and nested ones also at the
|
|
1853
2303
|
// defining file.
|
|
@@ -1878,7 +2328,10 @@ function workerSnapshot(index, workload, inventory) {
|
|
|
1878
2328
|
record.importBindings = (entry.importBindings || [])
|
|
1879
2329
|
.filter(binding => inventory.has(binding.alias || binding.name));
|
|
1880
2330
|
}
|
|
1881
|
-
if (full) {
|
|
2331
|
+
if (full && fullCalls) {
|
|
2332
|
+
// A calls derivation reads every parsed record of the file.
|
|
2333
|
+
record.calls = index.callsCache.get(file)?.calls || [];
|
|
2334
|
+
} else if (full) {
|
|
1882
2335
|
record.calls = (index.callsCache.get(file)?.calls || [])
|
|
1883
2336
|
.filter(call => call.isMacro && !call.macroExpansion && inventory.has(call.name))
|
|
1884
2337
|
.map(call => ({
|
|
@@ -1949,7 +2402,7 @@ function snapshotIndex(snapshot) {
|
|
|
1949
2402
|
* `signal[queueIndex]`), posting each record to `port`; `signal[workerIndex]`
|
|
1950
2403
|
* is set when this worker is done.
|
|
1951
2404
|
*/
|
|
1952
|
-
function runExpansionQueue({ snapshot, files, signal, workerIndex, queueIndex }, port) {
|
|
2405
|
+
async function runExpansionQueue({ snapshot, files, signal, workerIndex, queueIndex, mode }, port) {
|
|
1953
2406
|
const signalArray = new Int32Array(signal);
|
|
1954
2407
|
try {
|
|
1955
2408
|
const state = createState(snapshotIndex(JSON.parse(Buffer.from(snapshot).toString('utf8'))));
|
|
@@ -1959,11 +2412,15 @@ function runExpansionQueue({ snapshot, files, signal, workerIndex, queueIndex },
|
|
|
1959
2412
|
const file = files[next];
|
|
1960
2413
|
let result;
|
|
1961
2414
|
try {
|
|
1962
|
-
result = { file, record: expandForWorker(state, file) };
|
|
2415
|
+
result = { file, record: mode === 'calls' ? deriveForWorker(state, file) : expandForWorker(state, file) };
|
|
1963
2416
|
} catch (error) {
|
|
1964
2417
|
result = { file, error: error.message };
|
|
1965
2418
|
}
|
|
1966
2419
|
port.postMessage([result]);
|
|
2420
|
+
// Yield between files, as the parse loop does (fix #365): the
|
|
2421
|
+
// native trees of an expansion are freed only when this thread
|
|
2422
|
+
// returns to its event loop (fix #422).
|
|
2423
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
1967
2424
|
}
|
|
1968
2425
|
} finally {
|
|
1969
2426
|
Atomics.store(signalArray, workerIndex, 1);
|
|
@@ -1971,6 +2428,12 @@ function runExpansionQueue({ snapshot, files, signal, workerIndex, queueIndex },
|
|
|
1971
2428
|
}
|
|
1972
2429
|
}
|
|
1973
2430
|
|
|
2431
|
+
/** Worker-side: the whole-file calls derivation of one file (fix #422). */
|
|
2432
|
+
function deriveForWorker(state, filePath) {
|
|
2433
|
+
const ctx = expandFile(state, filePath, 'calls')?.context || null;
|
|
2434
|
+
return ctx ? { calls: composedCalls(ctx) } : { none: true };
|
|
2435
|
+
}
|
|
2436
|
+
|
|
1974
2437
|
/** Worker-side: symbols-mode expansion of one file, as plain records. */
|
|
1975
2438
|
function expandForWorker(state, filePath) {
|
|
1976
2439
|
const rec = expandFile(state, filePath) || { sites: 0, blind: [], used: new Map(), expanded: false };
|
|
@@ -1978,7 +2441,10 @@ function expandForWorker(state, filePath) {
|
|
|
1978
2441
|
sites: rec.sites, blind: rec.blind, expanded: rec.expanded,
|
|
1979
2442
|
usedNames: [...rec.used.values()].map(def => def.name),
|
|
1980
2443
|
...(rec.symbols && { symbols: rec.symbols }),
|
|
1981
|
-
...(rec.callsPending && {
|
|
2444
|
+
...(rec.callsPending && {
|
|
2445
|
+
callsPending: true, calleeNames: rec.calleeNames, words: rec.words, expansions: rec.expansions,
|
|
2446
|
+
...(rec.groups && { groups: rec.groups }),
|
|
2447
|
+
}),
|
|
1982
2448
|
};
|
|
1983
2449
|
}
|
|
1984
2450
|
|
|
@@ -2000,7 +2466,8 @@ function startWorkers(index, files, options = {}) {
|
|
|
2000
2466
|
codeUnitCompare(a, b));
|
|
2001
2467
|
// One serialization shared by every worker (a structured clone per
|
|
2002
2468
|
// worker cost more than the expansion on large workspaces).
|
|
2003
|
-
const encoded = Buffer.from(JSON.stringify(workerSnapshot(index, files, options.inventory
|
|
2469
|
+
const encoded = Buffer.from(JSON.stringify(workerSnapshot(index, files, options.inventory,
|
|
2470
|
+
options.mode === 'calls')), 'utf8');
|
|
2004
2471
|
const snapshot = new SharedArrayBuffer(encoded.length);
|
|
2005
2472
|
encoded.copy(Buffer.from(snapshot));
|
|
2006
2473
|
const sab = new SharedArrayBuffer(4 * (count + 1));
|
|
@@ -2009,7 +2476,8 @@ function startWorkers(index, files, options = {}) {
|
|
|
2009
2476
|
for (let i = 0; i < count; i++) {
|
|
2010
2477
|
const worker = pool.available[i];
|
|
2011
2478
|
ports.push(pool.ports[worker]);
|
|
2012
|
-
pool.post(worker, { snapshot, files: ordered, signal: sab, workerIndex: i, queueIndex: count
|
|
2479
|
+
pool.post(worker, { snapshot, files: ordered, signal: sab, workerIndex: i, queueIndex: count,
|
|
2480
|
+
...(options.mode === 'calls' && { mode: 'calls' }) });
|
|
2013
2481
|
}
|
|
2014
2482
|
// Kept workers this job does not need end now.
|
|
2015
2483
|
for (const worker of pool.available.slice(count)) pool.retire(worker);
|
|
@@ -2051,29 +2519,602 @@ function rustExpandedCalls(index, filePath, rawCalls, { materialize = true } = {
|
|
|
2051
2519
|
if (!rec?.callsPending || !Array.isArray(rawCalls)) return rawCalls;
|
|
2052
2520
|
const hints = (rec.calleeNames || []).map(name => ({ name, line: 0, calleeHint: true }));
|
|
2053
2521
|
if (!materialize) return hints.length > 0 ? rawCalls.concat(hints) : rawCalls;
|
|
2054
|
-
|
|
2522
|
+
const ctx = pendingContext(index, filePath, { whole: true });
|
|
2055
2523
|
if (index.calleeIndex && hints.length > 0) index._removeFromCalleeIndex(filePath, hints);
|
|
2056
2524
|
rec.callsPending = false;
|
|
2057
2525
|
try {
|
|
2058
|
-
|
|
2526
|
+
if (ctx) composeCalls(index, ctx);
|
|
2059
2527
|
} catch (error) {
|
|
2060
2528
|
rec.callsPending = true;
|
|
2061
2529
|
throw error;
|
|
2062
2530
|
}
|
|
2531
|
+
finishDerivation(index, filePath, rec);
|
|
2532
|
+
return index.callsCache.get(filePath)?.calls || rawCalls;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
function finishDerivation(index, filePath, rec) {
|
|
2536
|
+
index._rustMacroState?.contexts.delete(filePath);
|
|
2063
2537
|
delete rec.words;
|
|
2538
|
+
delete rec.groups;
|
|
2539
|
+
delete rec.expansions;
|
|
2064
2540
|
index.macroExpansionDirty = true;
|
|
2065
|
-
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// Derivation contexts whose trees are kept (fix #405); a context dropped
|
|
2544
|
+
// earlier keeps the records of the groups it read.
|
|
2545
|
+
const LIVE_CONTEXTS = 3;
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* The calls-pass context of a file whose expansion calls are pending, or
|
|
2549
|
+
* null when nothing of it expands (its parsed records stand).
|
|
2550
|
+
*/
|
|
2551
|
+
function pendingContext(index, filePath, { whole = false } = {}) {
|
|
2552
|
+
if (!index._rustMacroState) index._rustMacroState = createState(index);
|
|
2553
|
+
const state = index._rustMacroState;
|
|
2554
|
+
const contexts = state.contexts;
|
|
2555
|
+
let ctx = contexts.get(filePath);
|
|
2556
|
+
if (ctx === null) return null;
|
|
2557
|
+
if (ctx && ctx.live) {
|
|
2558
|
+
contexts.delete(filePath);
|
|
2559
|
+
contexts.set(filePath, ctx);
|
|
2560
|
+
return ctx;
|
|
2561
|
+
}
|
|
2562
|
+
// A file derived whole before any part of it was read is read in one
|
|
2563
|
+
// pass (fix #405b): the groups apart cost the same expansions and parse
|
|
2564
|
+
// plus a second plan of the file's invocations.
|
|
2565
|
+
const fresh = whole && ctx === undefined
|
|
2566
|
+
? expandFile(state, filePath, 'calls')?.context || null
|
|
2567
|
+
: buildContext(state, filePath);
|
|
2568
|
+
if (fresh && ctx) {
|
|
2569
|
+
// Rebuilt: the groups the dropped context read keep their records.
|
|
2570
|
+
if (fresh.groups.length !== ctx.groups.length || !!fresh.isolated !== !!ctx.isolated) {
|
|
2571
|
+
throw new Error(`Rust macro expansion context changed: ${filePath}`);
|
|
2572
|
+
}
|
|
2573
|
+
for (let i = 0; i < fresh.groups.length; i++) {
|
|
2574
|
+
const kept = ctx.groups[i];
|
|
2575
|
+
if (!kept.records) continue;
|
|
2576
|
+
Object.assign(fresh.groups[i], { records: kept.records, units: kept.units, starts: kept.starts });
|
|
2577
|
+
}
|
|
2578
|
+
fresh.invocations = ctx.invocations;
|
|
2579
|
+
fresh.seen = ctx.seen;
|
|
2580
|
+
}
|
|
2581
|
+
contexts.delete(filePath);
|
|
2582
|
+
contexts.set(filePath, fresh);
|
|
2583
|
+
if (fresh) {
|
|
2584
|
+
let live = 0;
|
|
2585
|
+
for (const [file, other] of [...contexts].reverse()) {
|
|
2586
|
+
if (!other?.live) continue;
|
|
2587
|
+
if (++live <= LIVE_CONTEXTS || file === filePath) continue;
|
|
2588
|
+
contexts.set(file, releaseContext(other));
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return fresh;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
/**
|
|
2595
|
+
* A file's calls-pass context: its groups read apart when the build's
|
|
2596
|
+
* record of its expansions still matches its invocations, else one pass
|
|
2597
|
+
* over the whole file. null when nothing of it expands.
|
|
2598
|
+
*/
|
|
2599
|
+
function buildContext(state, filePath) {
|
|
2600
|
+
const rec = state.index.files.get(filePath)?.rustMacroExpansion;
|
|
2601
|
+
// Expansions the build could not parse in context are read in one pass
|
|
2602
|
+
// (whose retries can also drop a neighbor).
|
|
2603
|
+
if (Array.isArray(rec?.expansions) && !(rec.blind || []).some(site => site.reason === 'parse-error')) {
|
|
2604
|
+
const isolated = isolatedContext(state, filePath, rec.expansions);
|
|
2605
|
+
if (isolated !== undefined) return isolated;
|
|
2606
|
+
}
|
|
2607
|
+
return expandFile(state, filePath, 'calls')?.context || null;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
/** What a dropped context keeps: the records it read. */
|
|
2611
|
+
function releaseContext(ctx) {
|
|
2612
|
+
return {
|
|
2613
|
+
isolated: ctx.isolated,
|
|
2614
|
+
groups: ctx.groups.map(group => ({ records: group.records, units: group.units, starts: group.starts })),
|
|
2615
|
+
invocations: ctx.invocations,
|
|
2616
|
+
seen: ctx.seen,
|
|
2617
|
+
live: false,
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
/**
|
|
2622
|
+
* A context that reads each group apart (fix #405). The build recorded, for
|
|
2623
|
+
* every invocation it expanded, its hygiene start and its expanded length:
|
|
2624
|
+
* a group expands alone with the numbering and the offsets of a pass over
|
|
2625
|
+
* the whole file, every other expansion standing in as a blank of its
|
|
2626
|
+
* length. undefined when that record no longer matches the file's
|
|
2627
|
+
* invocations or the file has parse errors (one pass reads it), null when
|
|
2628
|
+
* nothing of it expands.
|
|
2629
|
+
*/
|
|
2630
|
+
function isolatedContext(state, filePath, expansions) {
|
|
2631
|
+
const index = state.index;
|
|
2632
|
+
const content = readContent(state, filePath);
|
|
2633
|
+
if (!index.files.get(filePath) || content === null) return undefined;
|
|
2634
|
+
if (!fileMayExpand(state, filePath)) return null;
|
|
2635
|
+
const tree = safeParse(state.parser, content, undefined, PARSE_OPTIONS);
|
|
2636
|
+
if (tree.rootNode.hasError) return undefined;
|
|
2637
|
+
const plan = plannedInvocations(state, filePath, content, tree, { blind: [] });
|
|
2638
|
+
if (plan.length * 3 !== expansions.length) return undefined;
|
|
2639
|
+
const slots = [];
|
|
2640
|
+
for (let i = 0; i < plan.length; i++) {
|
|
2641
|
+
const item = plan[i];
|
|
2642
|
+
if (item.node.startIndex !== expansions[3 * i]) return undefined;
|
|
2643
|
+
item.hygiene = expansions[3 * i + 1];
|
|
2644
|
+
item.length = expansions[3 * i + 2];
|
|
2645
|
+
if (item.length < 0) continue;
|
|
2646
|
+
slots.push({
|
|
2647
|
+
item, node: item.node, start: item.node.startIndex, end: item.node.endIndex,
|
|
2648
|
+
startLine: item.line, endLine: item.node.endPosition.row + 1,
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
if (slots.length === 0) return null;
|
|
2652
|
+
slots.sort((a, b) => a.start - b.start);
|
|
2653
|
+
const { units } = analysisUnits(slots, 'calls');
|
|
2654
|
+
const lookups = unitLookups(units);
|
|
2655
|
+
const groups = groupsOf(units);
|
|
2656
|
+
for (const group of groups) group.slots = [];
|
|
2657
|
+
for (const slot of slots) groups[lookups.unitAtOffset(slot.start).group].slots.push(slot);
|
|
2658
|
+
return {
|
|
2659
|
+
isolated: true, filePath, content, tree, slots, groups,
|
|
2660
|
+
original: index.callsCache.get(filePath)?.calls || [],
|
|
2661
|
+
seen: new Set(), invocations: new Map(), parser: state.parser, state, live: true,
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
/** Units sharing a line form one group; units get their group's index. */
|
|
2666
|
+
function groupsOf(units) {
|
|
2667
|
+
const groups = [];
|
|
2668
|
+
for (const unit of units) {
|
|
2669
|
+
const last = groups[groups.length - 1];
|
|
2670
|
+
if (last && unit.startLine <= last.endLine) {
|
|
2671
|
+
last.units.push(unit);
|
|
2672
|
+
if (unit.endLine > last.endLine) last.endLine = unit.endLine;
|
|
2673
|
+
} else {
|
|
2674
|
+
groups.push({ units: [unit], startLine: unit.startLine, endLine: unit.endLine, records: null });
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
for (let i = 0; i < groups.length; i++) for (const unit of groups[i].units) unit.group = i;
|
|
2678
|
+
return groups;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
/**
|
|
2682
|
+
* Read groups of an isolated context. Groups whose expanded text parses
|
|
2683
|
+
* without an error read together as alone (each unit is an item of its
|
|
2684
|
+
* container); otherwise each group is read alone.
|
|
2685
|
+
*/
|
|
2686
|
+
function readIsolatedGroups(ctx, targets) {
|
|
2687
|
+
if (targets.length === 1) {
|
|
2688
|
+
readIsolatedGroup(ctx, targets[0]);
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
const rec = { blind: [], used: new Map() };
|
|
2692
|
+
const wanted = new Set(targets);
|
|
2693
|
+
const mine = new Set(targets.flatMap(i => ctx.groups[i].slots));
|
|
2694
|
+
const expand = slot => expandSlot(ctx, slot, rec);
|
|
2695
|
+
const live = ctx.slots.filter(slot => mine.has(slot)).map(expand);
|
|
2696
|
+
const keep = analysisUnits(live, 'calls').keep;
|
|
2697
|
+
const extras = ctx.slots.filter(slot => !mine.has(slot) &&
|
|
2698
|
+
keep.some(([start, end]) => slot.start < end && slot.end > start)).map(expand);
|
|
2699
|
+
const expanded = live.concat(extras);
|
|
2700
|
+
const bySlot = new Map(expanded.map(region => [region.slot, region]));
|
|
2701
|
+
const all = ctx.slots.map(slot => bySlot.get(slot) || placeholderOf(ctx, slot));
|
|
2702
|
+
const built = buildSynthetic(ctx.content, all, analysisUnits(expanded, 'calls').keep);
|
|
2703
|
+
const synthTree = parseSynthetic(ctx.parser, built.text, built.spans);
|
|
2704
|
+
if (collectErrorRanges(synthTree.rootNode, []).length > 0) {
|
|
2705
|
+
for (const i of targets) readIsolatedGroup(ctx, i);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
const units = [];
|
|
2709
|
+
for (const i of targets) {
|
|
2710
|
+
const group = ctx.groups[i];
|
|
2711
|
+
const regions = live.filter(region => region.slot && ctx.groups[i].slots.includes(region.slot));
|
|
2712
|
+
group.units = analysisUnits(regions, 'calls').units;
|
|
2713
|
+
for (const unit of group.units) unit.group = i;
|
|
2714
|
+
group.starts = regions.map(region => region.start);
|
|
2715
|
+
group.records = [];
|
|
2716
|
+
units.push(...group.units);
|
|
2717
|
+
}
|
|
2718
|
+
units.sort((a, b) => a.start - b.start);
|
|
2719
|
+
const lookups = unitLookups(units);
|
|
2720
|
+
const originalSpanless = new Map();
|
|
2721
|
+
for (const record of ctx.original) {
|
|
2722
|
+
if (Number.isFinite(record.callStart) || !lookups.unitAtLine(record.line)) continue;
|
|
2723
|
+
const key = spanlessKey(record);
|
|
2724
|
+
originalSpanless.set(key, (originalSpanless.get(key) || 0) + 1);
|
|
2725
|
+
}
|
|
2726
|
+
const rust = require('../languages/rust');
|
|
2727
|
+
const synthCalls = rust.findCallsInCode(built.text, ctx.parser, {
|
|
2728
|
+
tree: synthTree, itemRecovery: rust.rustExpandedItemRecovery(built.text, ctx.parser, synthTree),
|
|
2729
|
+
});
|
|
2730
|
+
mergeRecords(ctx, synthCalls, {
|
|
2731
|
+
mapper: offsetMapper(all), lookups, regions: live, synthTree, originalSpanless,
|
|
2732
|
+
accept: unit => wanted.has(unit.group), push: (unit, call) => ctx.groups[unit.group].records.push(call),
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
/** Expand one invocation of an isolated context as the build did. */
|
|
2737
|
+
function expandSlot(ctx, slot, rec) {
|
|
2738
|
+
ctx.state.hygieneSeq = slot.item.hygiene;
|
|
2739
|
+
const region = expandPlanned(ctx.state, ctx.filePath, ctx.content, slot.item, rec);
|
|
2740
|
+
if (!region || region.layout.text.length !== slot.item.length) {
|
|
2741
|
+
throw new Error(`Rust macro expansion at ${ctx.filePath}:${slot.startLine} differs from the build's`);
|
|
2742
|
+
}
|
|
2743
|
+
region.slot = slot;
|
|
2744
|
+
return region;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
/** A blank of an expansion's length standing in for it (its newlines kept). */
|
|
2748
|
+
function placeholderOf(ctx, slot) {
|
|
2749
|
+
if (!ctx.placeholders) ctx.placeholders = new Map();
|
|
2750
|
+
let region = ctx.placeholders.get(slot);
|
|
2751
|
+
if (!region) {
|
|
2752
|
+
const breaks = slot.endLine - slot.startLine;
|
|
2753
|
+
region = {
|
|
2754
|
+
start: slot.start, end: slot.end, startLine: slot.startLine, endLine: slot.endLine, blank: true,
|
|
2755
|
+
layout: { text: ' '.repeat(slot.item.length - breaks) + '\n'.repeat(breaks), marks: [] },
|
|
2756
|
+
};
|
|
2757
|
+
ctx.placeholders.set(slot, region);
|
|
2758
|
+
}
|
|
2759
|
+
// Its offsets are set by the synthetic it stands in.
|
|
2760
|
+
return { ...region };
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/** Read one group of an isolated context: its expansions, parsed alone. */
|
|
2764
|
+
function readIsolatedGroup(ctx, gi) {
|
|
2765
|
+
const group = ctx.groups[gi];
|
|
2766
|
+
const rec = { blind: [], used: new Map() };
|
|
2767
|
+
const expand = slot => expandSlot(ctx, slot, rec);
|
|
2768
|
+
const mine = new Set(group.slots);
|
|
2769
|
+
let live = group.slots.map(expand);
|
|
2770
|
+
// Expansions inside the kept headers of this group's units
|
|
2771
|
+
// (`impl T for m!() { .. }`) are part of its text.
|
|
2772
|
+
const headerKeep = analysisUnits(live, 'calls').keep;
|
|
2773
|
+
let extras = ctx.slots.filter(slot => !mine.has(slot) &&
|
|
2774
|
+
headerKeep.some(([start, end]) => slot.start < end && slot.end > start)).map(expand);
|
|
2775
|
+
let synthetic = null;
|
|
2776
|
+
let synthTree = null;
|
|
2777
|
+
let all = null;
|
|
2778
|
+
for (let attempt = 0; live.length > 0 && attempt <= ctx.slots.length; attempt++) {
|
|
2779
|
+
const expanded = live.concat(extras);
|
|
2780
|
+
const bySlot = new Map(expanded.map(region => [region.slot, region]));
|
|
2781
|
+
const dropped = new Set(group.slots.filter(slot => !bySlot.has(slot)));
|
|
2782
|
+
// A dropped expansion stays as written, as in one pass.
|
|
2783
|
+
all = ctx.slots.filter(slot => !dropped.has(slot)).map(slot => bySlot.get(slot) || placeholderOf(ctx, slot));
|
|
2784
|
+
const built = buildSynthetic(ctx.content, all, analysisUnits(expanded, 'calls').keep);
|
|
2785
|
+
synthetic = built.text;
|
|
2786
|
+
synthTree = parseSynthetic(ctx.parser, synthetic, built.spans);
|
|
2787
|
+
const errors = collectErrorRanges(synthTree.rootNode, []);
|
|
2788
|
+
const bad = new Set();
|
|
2789
|
+
for (const [start, end] of errors) {
|
|
2790
|
+
for (const region of expanded) {
|
|
2791
|
+
if (start < region.synthEnd && end > region.synthStart) bad.add(region);
|
|
2792
|
+
else if (start === end && start >= region.synthStart && start <= region.synthEnd) bad.add(region);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
if (bad.size === 0) break;
|
|
2796
|
+
live = live.filter(region => !bad.has(region));
|
|
2797
|
+
extras = extras.filter(region => !bad.has(region));
|
|
2798
|
+
}
|
|
2799
|
+
group.units = live.length > 0 ? analysisUnits(live, 'calls').units : [];
|
|
2800
|
+
group.starts = live.map(region => region.start);
|
|
2801
|
+
group.records = [];
|
|
2802
|
+
if (live.length === 0) return;
|
|
2803
|
+
const lookups = unitLookups(group.units);
|
|
2804
|
+
const mapper = offsetMapper(all);
|
|
2805
|
+
const originalSpanless = new Map();
|
|
2806
|
+
for (const record of ctx.original) {
|
|
2807
|
+
if (Number.isFinite(record.callStart) || !lookups.unitAtLine(record.line)) continue;
|
|
2808
|
+
const key = spanlessKey(record);
|
|
2809
|
+
originalSpanless.set(key, (originalSpanless.get(key) || 0) + 1);
|
|
2810
|
+
}
|
|
2811
|
+
const rust = require('../languages/rust');
|
|
2812
|
+
const synthCalls = rust.findCallsInCode(synthetic, ctx.parser, {
|
|
2813
|
+
tree: synthTree, itemRecovery: rust.rustExpandedItemRecovery(synthetic, ctx.parser, synthTree),
|
|
2814
|
+
});
|
|
2815
|
+
mergeRecords(ctx, synthCalls, {
|
|
2816
|
+
mapper, lookups, regions: live, synthTree, originalSpanless,
|
|
2817
|
+
accept: () => true, push: (unit, call) => group.records.push(call),
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
/**
|
|
2822
|
+
* Words the records of an isolated group can bear: its lines and its units'
|
|
2823
|
+
* kept headers in the file (literals and comments left out), the text of
|
|
2824
|
+
* every project macro its expansions can transcribe, and the crate names
|
|
2825
|
+
* `$crate` becomes.
|
|
2826
|
+
*/
|
|
2827
|
+
function isolatedGroupWords(ctx, group) {
|
|
2828
|
+
if (group.words) return group.words;
|
|
2829
|
+
const state = ctx.state;
|
|
2830
|
+
const words = new Set();
|
|
2831
|
+
const ranges = analysisUnits(group.slots, 'calls').keep.concat([[
|
|
2832
|
+
lineOffset(state, ctx.filePath, group.startLine), lineEndOffset(state, ctx.filePath, group.endLine),
|
|
2833
|
+
]]).sort((a, b) => a[0] - b[0]);
|
|
2834
|
+
if (!ctx.nonCode) ctx.nonCode = ctx.tree.rootNode.descendantsOfType(NON_CODE_TOKENS);
|
|
2835
|
+
const nonCode = ctx.nonCode;
|
|
2836
|
+
const scan = (start, end) => {
|
|
2837
|
+
if (end <= start) return;
|
|
2838
|
+
for (const word of ctx.content.slice(start, end).match(WORD) || []) words.add(word);
|
|
2839
|
+
};
|
|
2840
|
+
let next = 0;
|
|
2841
|
+
let done = 0;
|
|
2842
|
+
for (const [rangeStart, end] of ranges) {
|
|
2843
|
+
let at = Math.max(rangeStart, done);
|
|
2844
|
+
if (end <= at) continue;
|
|
2845
|
+
while (next < nonCode.length && nonCode[next].endIndex <= at) next++;
|
|
2846
|
+
for (let i = next; i < nonCode.length; i++) {
|
|
2847
|
+
const token = nonCode[i];
|
|
2848
|
+
if (token.startIndex >= end) break;
|
|
2849
|
+
scan(at, Math.min(token.startIndex, end));
|
|
2850
|
+
at = Math.max(at, token.endIndex);
|
|
2851
|
+
}
|
|
2852
|
+
scan(at, end);
|
|
2853
|
+
done = end;
|
|
2854
|
+
}
|
|
2855
|
+
for (const word of [...words]) if (state.inventory.has(word)) addTemplateWords(state, word, words);
|
|
2856
|
+
words.add('crate');
|
|
2857
|
+
group.words = words;
|
|
2858
|
+
return words;
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
/** Words of every definition of macro `name` and the macros they name, transitively. */
|
|
2862
|
+
function addTemplateWords(state, name, words) {
|
|
2863
|
+
if (!state.templateWords) state.templateWords = new Map();
|
|
2864
|
+
let own = state.templateWords.get(name);
|
|
2865
|
+
if (!own) {
|
|
2866
|
+
own = new Set();
|
|
2867
|
+
state.templateWords.set(name, own);
|
|
2868
|
+
const pending = [name];
|
|
2869
|
+
const visited = new Set();
|
|
2870
|
+
while (pending.length > 0) {
|
|
2871
|
+
const next = pending.pop();
|
|
2872
|
+
if (visited.has(next)) continue;
|
|
2873
|
+
visited.add(next);
|
|
2874
|
+
for (const def of state.inventory.get(next) || []) {
|
|
2875
|
+
const content = readContent(state, def.file);
|
|
2876
|
+
if (content === null) continue;
|
|
2877
|
+
const text = content.slice(lineOffset(state, def.file, def.startLine), lineOffset(state, def.file, def.endLine + 1));
|
|
2878
|
+
for (const word of text.match(WORD) || []) {
|
|
2879
|
+
own.add(word);
|
|
2880
|
+
if (state.inventory.has(word) && !visited.has(word)) pending.push(word);
|
|
2881
|
+
}
|
|
2882
|
+
const crate = packageOf(state, def.file)?.packageName;
|
|
2883
|
+
if (crate) own.add(crate);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
for (const word of own) words.add(word);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
/**
|
|
2891
|
+
* The records of a file whose expansion calls are pending that a consumer
|
|
2892
|
+
* reading only part of them needs (fix #405), in the order the composed
|
|
2893
|
+
* calls hold them and as the same record objects. `name`: the records
|
|
2894
|
+
* `keep` accepts, each of which must bear the name as one of its own
|
|
2895
|
+
* identifier tokens (its name or written receiver); `lines`: [start, end],
|
|
2896
|
+
* the records on those lines; `lineSet`: the records on any of those lines. Only the groups that can hold such records
|
|
2897
|
+
* are read. undefined when the file's calls are not pending, `name` is not
|
|
2898
|
+
* an identifier, or every group is read (the file's calls are then derived
|
|
2899
|
+
* whole by the caller).
|
|
2900
|
+
*/
|
|
2901
|
+
const IDENTIFIER = /^(?:r#)?[\p{L}_][\p{L}\p{N}_]*$/u;
|
|
2902
|
+
// Tenths of a file's unread expanded text from which a view derives the file
|
|
2903
|
+
// whole (fix #405b).
|
|
2904
|
+
const WHOLE_FILE_SHARE = 9;
|
|
2905
|
+
|
|
2906
|
+
function rustPendingRecords(index, filePath, { name = null, keep = null, lines = null, lineSet = null } = {}) {
|
|
2907
|
+
const entry = index.files.get(filePath);
|
|
2908
|
+
if (!entry?.rustMacroExpansion?.callsPending) return undefined;
|
|
2909
|
+
// Hygiene renames locals a transcriber introduces (`x__ucn_h3`): no
|
|
2910
|
+
// source text holds such a name.
|
|
2911
|
+
if (name !== null && (!IDENTIFIER.test(name) || /__ucn_h\d+$/.test(name))) return undefined;
|
|
2912
|
+
const raw = index.callsCache.get(filePath)?.calls;
|
|
2913
|
+
if (!Array.isArray(raw)) return undefined;
|
|
2914
|
+
const matches = record => (!lines || (record.line >= lines[0] && record.line <= lines[1])) &&
|
|
2915
|
+
(!lineSet || lineSet.has(record.line)) && (!keep || keep(record));
|
|
2916
|
+
// No derived record bears a name outside the file's words (#375): the
|
|
2917
|
+
// parsed records then answer, unless one of them bears it.
|
|
2918
|
+
if (name !== null && !rustPendingMayBear(entry, name) && !raw.some(matches)) return [];
|
|
2919
|
+
// The groups recorded at build (fix #405b) are chosen before anything is
|
|
2920
|
+
// parsed: a group can hold a record `matches` accepts when a word its
|
|
2921
|
+
// expansions output or its headers hold is the name, or when a parsed
|
|
2922
|
+
// record on its lines is accepted (the units' own code is written there).
|
|
2923
|
+
const rec = entry.rustMacroExpansion;
|
|
2924
|
+
const recorded = Array.isArray(rec.groups) ? rec.groups : null;
|
|
2925
|
+
let planned = null;
|
|
2926
|
+
if (recorded) {
|
|
2927
|
+
const read = index._rustMacroState?.contexts?.get(filePath)?.groups || null;
|
|
2928
|
+
const parsedOn = name !== null ? parsedRecordLines(raw, matches) : null;
|
|
2929
|
+
planned = [];
|
|
2930
|
+
let wantedLength = 0;
|
|
2931
|
+
let unreadLength = 0;
|
|
2932
|
+
for (let i = 0; i < recorded.length; i++) {
|
|
2933
|
+
const [startLine, endLine, length] = recorded[i];
|
|
2934
|
+
const unread = !read?.[i]?.records;
|
|
2935
|
+
if (unread) unreadLength += length;
|
|
2936
|
+
if (lines && (endLine < lines[0] || startLine > lines[1])) continue;
|
|
2937
|
+
if (lineSet && ![...lineSet].some(line => line >= startLine && line <= endLine)) continue;
|
|
2938
|
+
if (name !== null && !groupHasWord(recorded[i][3], name) &&
|
|
2939
|
+
!parsedOn.some(line => line >= startLine && line <= endLine)) continue;
|
|
2940
|
+
planned.push(i);
|
|
2941
|
+
if (unread) wantedLength += length;
|
|
2942
|
+
}
|
|
2943
|
+
if (planned.length === 0) {
|
|
2944
|
+
// Every group read: the caller derives the file's calls whole.
|
|
2945
|
+
if (read && read.length > 0 && read.every(group => group.records)) return undefined;
|
|
2946
|
+
// No group can hold one: the accepted records are parsed ones
|
|
2947
|
+
// outside every unit, as the composed calls hold them.
|
|
2948
|
+
return raw.filter(matches).sort((a, b) => a.line - b.line);
|
|
2949
|
+
}
|
|
2950
|
+
// Reading most of the expanded text that is still unread costs more
|
|
2951
|
+
// than deriving the file whole (groups read together cost ~5-10%
|
|
2952
|
+
// over one pass), which then answers every later query of the file:
|
|
2953
|
+
// a cost decision, not a semantic one - either way the records are
|
|
2954
|
+
// the composed calls'.
|
|
2955
|
+
if (wantedLength > 0 && wantedLength * 10 >= unreadLength * WHOLE_FILE_SHARE) return undefined;
|
|
2956
|
+
}
|
|
2957
|
+
const ctx = pendingContext(index, filePath);
|
|
2958
|
+
if (!ctx) return raw.filter(matches);
|
|
2959
|
+
// Every group read: the caller derives the file's calls whole.
|
|
2960
|
+
if (ctx.groups.every(group => group.records)) return undefined;
|
|
2961
|
+
let wanted;
|
|
2962
|
+
if (planned && ctx.groups.length === recorded.length && ctx.groups.every((group, i) =>
|
|
2963
|
+
group.startLine === recorded[i][0] && group.endLine === recorded[i][1])) {
|
|
2964
|
+
wanted = planned;
|
|
2965
|
+
} else {
|
|
2966
|
+
wanted = [];
|
|
2967
|
+
for (let i = 0; i < ctx.groups.length; i++) {
|
|
2968
|
+
const group = ctx.groups[i];
|
|
2969
|
+
if (lines && (group.endLine < lines[0] || group.startLine > lines[1])) continue;
|
|
2970
|
+
if (lineSet && ![...lineSet].some(line => line >= group.startLine && line <= group.endLine)) continue;
|
|
2971
|
+
if (name !== null && !groupMayBear(ctx, group, name)) continue;
|
|
2972
|
+
wanted.push(i);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
readGroups(ctx, wanted);
|
|
2976
|
+
if (ctx.groups.every(group => group.records)) return undefined;
|
|
2977
|
+
const out = ctx.original.filter(record => matches(record) && !inReadUnit(ctx, record));
|
|
2978
|
+
for (const i of wanted) for (const record of ctx.groups[i].records) if (matches(record)) out.push(record);
|
|
2979
|
+
for (const record of invocationRecords(ctx)) if (matches(record)) out.push(record);
|
|
2980
|
+
return out.sort((a, b) => a.line - b.line);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
/**
|
|
2984
|
+
* Whether a group's records can bear `name`: a word of the group's lines in
|
|
2985
|
+
* the expanded file or of the kept text outside every unit (headers: a
|
|
2986
|
+
* `Self` record bears its impl's type name).
|
|
2987
|
+
*/
|
|
2988
|
+
function groupMayBear(ctx, group, name) {
|
|
2989
|
+
if (ctx.isolated) return hasWord(isolatedGroupWords(ctx, group), name);
|
|
2990
|
+
if (!ctx.outsideWords) {
|
|
2991
|
+
const words = new Set();
|
|
2992
|
+
let at = 0;
|
|
2993
|
+
for (const unit of ctx.units) {
|
|
2994
|
+
for (const word of ctx.synthetic.slice(at, unit.synthStart).match(WORD) || []) words.add(word);
|
|
2995
|
+
at = Math.max(at, unit.synthEnd);
|
|
2996
|
+
}
|
|
2997
|
+
for (const word of ctx.synthetic.slice(at).match(WORD) || []) words.add(word);
|
|
2998
|
+
ctx.outsideWords = words;
|
|
2999
|
+
}
|
|
3000
|
+
if (hasWord(ctx.outsideWords, name)) return true;
|
|
3001
|
+
if (!group.words) {
|
|
3002
|
+
if (!ctx.synthLineStarts) {
|
|
3003
|
+
const starts = [0];
|
|
3004
|
+
const text = ctx.synthetic;
|
|
3005
|
+
for (let i = text.indexOf('\n'); i >= 0; i = text.indexOf('\n', i + 1)) starts.push(i + 1);
|
|
3006
|
+
ctx.synthLineStarts = starts;
|
|
3007
|
+
}
|
|
3008
|
+
const starts = ctx.synthLineStarts;
|
|
3009
|
+
const from = starts[Math.max(group.startLine - 1, 0)] ?? ctx.synthetic.length;
|
|
3010
|
+
const to = starts[group.endLine] ?? ctx.synthetic.length;
|
|
3011
|
+
group.words = new Set(ctx.synthetic.slice(from, to).match(WORD) || []);
|
|
3012
|
+
}
|
|
3013
|
+
return hasWord(group.words, name);
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
/** Lines of the parsed records `matches` accepts. */
|
|
3017
|
+
function parsedRecordLines(raw, matches) {
|
|
3018
|
+
const out = [];
|
|
3019
|
+
for (const record of raw) if (matches(record)) out.push(record.line);
|
|
3020
|
+
return out;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
function hasWord(words, name) {
|
|
3024
|
+
// A raw identifier's token is `r#name`; text holds both parts.
|
|
3025
|
+
return words.has(name) || words.has(String(name).replace(/^r#/, '')) || words.has(`r#${name}`);
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
/**
|
|
3029
|
+
* Relative paths of the files whose expansion calls are pending with no
|
|
3030
|
+
* part derived yet, sorted.
|
|
3031
|
+
*/
|
|
3032
|
+
function rustUntouchedPendingFiles(index) {
|
|
3033
|
+
const contexts = index._rustMacroState?.contexts;
|
|
3034
|
+
const out = [];
|
|
3035
|
+
for (const [filePath, entry] of index.files) {
|
|
3036
|
+
if (!entry.rustMacroExpansion?.callsPending) continue;
|
|
3037
|
+
if (contexts?.get(filePath)?.groups?.some(group => group.records)) continue;
|
|
3038
|
+
out.push(entry.relativePath);
|
|
3039
|
+
}
|
|
3040
|
+
return out.sort(codeUnitCompare);
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
/** Derive whole every file whose expansion calls were derived in part. */
|
|
3044
|
+
function completeRustMacroCalls(index) {
|
|
3045
|
+
const contexts = index._rustMacroState?.contexts;
|
|
3046
|
+
if (!contexts || contexts.size === 0) return 0;
|
|
3047
|
+
let done = 0;
|
|
3048
|
+
for (const filePath of [...contexts.keys()].sort(codeUnitCompare)) {
|
|
3049
|
+
if (!index.files.get(filePath)?.rustMacroExpansion?.callsPending) continue;
|
|
3050
|
+
const cached = index.callsCache.get(filePath);
|
|
3051
|
+
if (!cached) continue;
|
|
3052
|
+
rustExpandedCalls(index, filePath, cached.calls);
|
|
3053
|
+
done++;
|
|
3054
|
+
}
|
|
3055
|
+
return done;
|
|
2066
3056
|
}
|
|
2067
3057
|
|
|
2068
3058
|
/** Derive every pending file's expansion calls (whole-program consumers). */
|
|
2069
3059
|
function materializeRustMacroCalls(index) {
|
|
3060
|
+
const pending = [];
|
|
3061
|
+
for (const [filePath, entry] of index.files) {
|
|
3062
|
+
if (!entry.rustMacroExpansion?.callsPending || !index.callsCache.get(filePath)) continue;
|
|
3063
|
+
// A file a query derived in part keeps its context's records.
|
|
3064
|
+
if (index._rustMacroState?.contexts.has(filePath)) continue;
|
|
3065
|
+
pending.push(filePath);
|
|
3066
|
+
}
|
|
3067
|
+
// Many files to derive at once (an orientation ranking, a deadcode
|
|
3068
|
+
// audit): fresh workers derive them (fix #422), each whole in one pass
|
|
3069
|
+
// exactly as below.
|
|
3070
|
+
const derived = deriveInWorkers(index, pending);
|
|
2070
3071
|
for (const [filePath, entry] of index.files) {
|
|
2071
3072
|
if (!entry.rustMacroExpansion?.callsPending) continue;
|
|
2072
3073
|
const cached = index.callsCache.get(filePath);
|
|
2073
|
-
if (cached)
|
|
3074
|
+
if (!cached) continue;
|
|
3075
|
+
const result = derived?.get(filePath);
|
|
3076
|
+
if (result) installDerived(index, filePath, entry.rustMacroExpansion, cached, result);
|
|
3077
|
+
else rustExpandedCalls(index, filePath, cached.calls);
|
|
2074
3078
|
}
|
|
2075
3079
|
}
|
|
2076
3080
|
|
|
3081
|
+
// Expanded text a materialization derives in fresh workers from, and per
|
|
3082
|
+
// worker (the build recorded each expansion's length): below it, starting
|
|
3083
|
+
// threads costs more than deriving in place.
|
|
3084
|
+
const DERIVE_IN_WORKERS_MIN_BYTES = 192 * 1024;
|
|
3085
|
+
const DERIVE_BYTES_PER_WORKER = 192 * 1024;
|
|
3086
|
+
|
|
3087
|
+
/** Whole-file calls derivations of `files` in fresh workers: Map<file, { calls } | { none }>, or null. */
|
|
3088
|
+
function deriveInWorkers(index, files) {
|
|
3089
|
+
const env = process.env.UCN_FRESH_EXPANSION;
|
|
3090
|
+
if (env === '0' || freshWorkerCount(index, 2, files.length) === 0) return null;
|
|
3091
|
+
let bytes = 0;
|
|
3092
|
+
for (const file of files) {
|
|
3093
|
+
const recorded = index.files.get(file)?.rustMacroExpansion?.expansions || [];
|
|
3094
|
+
for (let i = 2; i < recorded.length; i += 3) if (recorded[i] > 0) bytes += recorded[i];
|
|
3095
|
+
}
|
|
3096
|
+
if (env !== '1' && bytes < DERIVE_IN_WORKERS_MIN_BYTES) return null;
|
|
3097
|
+
const inventory = index._rustMacroState?.inventory || macroInventory(index);
|
|
3098
|
+
if (inventory.size === 0) return null;
|
|
3099
|
+
const count = freshWorkerCount(index, Math.ceil(bytes / DERIVE_BYTES_PER_WORKER), files.length);
|
|
3100
|
+
const pool = require('./parallel-build').spawnExpansionPool(index, count);
|
|
3101
|
+
if (!pool) return null;
|
|
3102
|
+
try {
|
|
3103
|
+
const run = startWorkers(index, files, { pool, inventory, mode: 'calls' });
|
|
3104
|
+
return run ? run.collect() : null;
|
|
3105
|
+
} finally {
|
|
3106
|
+
pool.dispose();
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
function installDerived(index, filePath, rec, cached, result) {
|
|
3111
|
+
const hints = (rec.calleeNames || []).map(name => ({ name, line: 0, calleeHint: true }));
|
|
3112
|
+
if (index.calleeIndex && hints.length > 0) index._removeFromCalleeIndex(filePath, hints);
|
|
3113
|
+
rec.callsPending = false;
|
|
3114
|
+
if (result.calls) installComposedCalls(index, filePath, result.calls, cached.calls);
|
|
3115
|
+
finishDerivation(index, filePath, rec);
|
|
3116
|
+
}
|
|
3117
|
+
|
|
2077
3118
|
/** Expanded and blind invocation sites, for health and deadcode disclosure. */
|
|
2078
3119
|
function rustMacroExpansionSummary(index) {
|
|
2079
3120
|
const summary = { sites: 0, files: 0, blind: { count: 0, fileCount: 0, files: [], sample: [], reasons: {} } };
|
|
@@ -2110,4 +3151,8 @@ module.exports = {
|
|
|
2110
3151
|
materializeRustMacroCalls,
|
|
2111
3152
|
rustMacroExpansionSummary,
|
|
2112
3153
|
rustPendingMayBear,
|
|
3154
|
+
rustPendingFilesBearing,
|
|
3155
|
+
rustPendingRecords,
|
|
3156
|
+
completeRustMacroCalls,
|
|
3157
|
+
rustUntouchedPendingFiles,
|
|
2113
3158
|
};
|