ucn 5.5.0 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/cli/index.js +5 -0
- package/core/account.js +32 -11
- package/core/analysis.js +9 -2
- package/core/cache.js +8 -1
- package/core/callers.js +378 -77
- package/core/project.js +15 -6
- package/core/rust-macro-expansion.js +1061 -148
- package/core/rust-modules.js +20 -2
- package/core/type-denotation.js +33 -9
- package/languages/rust.js +38 -7
- package/package.json +1 -1
|
@@ -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,70 @@ 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);
|
|
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);
|
|
1483
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
|
+
readGroups(ctx, ctx.groups.map((group, i) => i));
|
|
1726
|
+
const fileEntry = index.files.get(ctx.filePath);
|
|
1727
|
+
const calls = ctx.original.filter(record => !inReadUnit(ctx, record));
|
|
1728
|
+
for (const group of ctx.groups) for (const record of group.records) calls.push(record);
|
|
1729
|
+
for (const record of invocationRecords(ctx)) calls.push(record);
|
|
1484
1730
|
// Shapes of generated calls whose value may be lost (audit-async).
|
|
1485
1731
|
const { openCallShape } = require('../languages/rust-value-flow');
|
|
1486
1732
|
const shapes = calls.filter(call => call.macroExpansion && call.name && !call.inMacro && !call.valueConsumed)
|
|
@@ -1490,16 +1736,16 @@ function expandFile(state, filePath, mode = 'symbols') {
|
|
|
1490
1736
|
}
|
|
1491
1737
|
// Stable by line: records keep document order within a line.
|
|
1492
1738
|
calls.sort((a, b) => a.line - b.line);
|
|
1739
|
+
const cached = index.callsCache.get(ctx.filePath);
|
|
1493
1740
|
if (cached) {
|
|
1494
|
-
if (index.calleeIndex) index._removeFromCalleeIndex(filePath, original);
|
|
1741
|
+
if (index.calleeIndex) index._removeFromCalleeIndex(ctx.filePath, ctx.original);
|
|
1495
1742
|
cached.calls = calls;
|
|
1496
|
-
if (index.calleeIndex) index._addToCalleeIndex(filePath, calls);
|
|
1743
|
+
if (index.calleeIndex) index._addToCalleeIndex(ctx.filePath, calls);
|
|
1497
1744
|
} else {
|
|
1498
|
-
index.callsCache.set(filePath, { mtime: fileEntry.mtime, hash: fileEntry.hash, calls });
|
|
1745
|
+
index.callsCache.set(ctx.filePath, { mtime: fileEntry.mtime, hash: fileEntry.hash, calls });
|
|
1499
1746
|
}
|
|
1500
1747
|
index.callsCacheDirty = true;
|
|
1501
|
-
|
|
1502
|
-
return rec;
|
|
1748
|
+
return calls;
|
|
1503
1749
|
}
|
|
1504
1750
|
|
|
1505
1751
|
const CALL_NODES = new Set(['call_expression', 'macro_invocation', 'struct_expression', 'generic_function']);
|
|
@@ -1621,11 +1867,12 @@ function callableNames(regions) {
|
|
|
1621
1867
|
for (const region of regions) {
|
|
1622
1868
|
const marks = region.layout.marks;
|
|
1623
1869
|
for (let i = 0; i < marks.length - 1; i++) {
|
|
1870
|
+
const next = marks[i + 1].leaf.v;
|
|
1871
|
+
if (!(next === '(' || next === '!' || next === '{' ||
|
|
1872
|
+
(next === '::' && marks[i + 2]?.leaf.v === '<'))) continue;
|
|
1624
1873
|
const leaf = marks[i].leaf;
|
|
1625
1874
|
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);
|
|
1875
|
+
names.add(leaf.v);
|
|
1629
1876
|
}
|
|
1630
1877
|
}
|
|
1631
1878
|
return [...names].sort(codeUnitCompare);
|
|
@@ -1634,27 +1881,117 @@ function callableNames(regions) {
|
|
|
1634
1881
|
const WORD = /[\p{L}\p{N}_]+/gu;
|
|
1635
1882
|
const WHOLE_WORD = /^[\p{L}\p{N}_]+$/u;
|
|
1636
1883
|
|
|
1884
|
+
// Tokens whose text is never an identifier: literals and comments.
|
|
1885
|
+
const NON_CODE_TOKENS = ['string_literal', 'raw_string_literal', 'char_literal', 'line_comment', 'block_comment'];
|
|
1886
|
+
|
|
1637
1887
|
/**
|
|
1638
1888
|
* The words (identifier-like runs) of the text the calls pass analyzes for
|
|
1639
1889
|
* these regions: the kept source around them (units and their headers) and
|
|
1640
1890
|
* every expansion's tokens. A call record derived from that text can bear no
|
|
1641
1891
|
* other name, so a file whose calls are not derived yet needs deriving only
|
|
1642
|
-
* for a name among them (fix #375).
|
|
1892
|
+
* for a name among them (fix #375). A record's name and written receiver are
|
|
1893
|
+
* identifier tokens, so the text of literals and comments is left out (fix
|
|
1894
|
+
* #405: `"--count"` and `// the count` made a `count` query derive four
|
|
1895
|
+
* test files); kept ranges are whole items and headers, which the calls
|
|
1896
|
+
* pass lexes as the file's own parse did. A file with parse errors is read
|
|
1897
|
+
* whole. Space-separated, sorted.
|
|
1643
1898
|
*/
|
|
1644
|
-
function expansionWords(content, regions) {
|
|
1899
|
+
function expansionWords(content, regions, nonCode = [], outputs = regions.map(outputWordsOf)) {
|
|
1645
1900
|
const words = new Set();
|
|
1646
|
-
|
|
1901
|
+
scanCodeWords(content, analysisUnits(regions, 'calls').keep, nonCode, words);
|
|
1902
|
+
for (const output of outputs) for (const word of output) words.add(word);
|
|
1903
|
+
return [...words].sort(codeUnitCompare).join(' ');
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
/** Words of `ranges` (sorted, disjoint) of the source, literals and comments (`nonCode`, sorted) left out. */
|
|
1907
|
+
function scanCodeWords(content, ranges, nonCode, words) {
|
|
1908
|
+
const scan = (start, end) => {
|
|
1909
|
+
if (end <= start) return;
|
|
1647
1910
|
for (const word of content.slice(start, end).match(WORD) || []) words.add(word);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1911
|
+
};
|
|
1912
|
+
let next = 0;
|
|
1913
|
+
for (const [start, end] of ranges) {
|
|
1914
|
+
let at = start;
|
|
1915
|
+
while (next < nonCode.length && nonCode[next].endIndex <= at) next++;
|
|
1916
|
+
for (let i = next; i < nonCode.length; i++) {
|
|
1917
|
+
const token = nonCode[i];
|
|
1918
|
+
if (token.startIndex >= end) break;
|
|
1919
|
+
scan(at, Math.min(token.startIndex, end));
|
|
1920
|
+
at = Math.max(at, token.endIndex);
|
|
1655
1921
|
}
|
|
1922
|
+
scan(at, end);
|
|
1656
1923
|
}
|
|
1657
|
-
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/** The identifier tokens an expansion outputs (a non-ASCII identifier is a 'punct' leaf), once each. */
|
|
1927
|
+
function outputWordsOf(region) {
|
|
1928
|
+
const words = new Set();
|
|
1929
|
+
for (const mark of region.layout.marks) {
|
|
1930
|
+
const leaf = mark.leaf;
|
|
1931
|
+
if (leaf.k === 'ident' || (leaf.k === 'punct' && WHOLE_WORD.test(leaf.v))) words.add(leaf.v);
|
|
1932
|
+
}
|
|
1933
|
+
return [...words];
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/**
|
|
1937
|
+
* The groups of units the calls pass reads apart (fix #405b), each as
|
|
1938
|
+
* [startLine, endLine, expanded length, words]: the identifiers its
|
|
1939
|
+
* expansions output (with those of any expansion in its kept headers) and
|
|
1940
|
+
* the code of its kept headers outside its units (`Self { .. }` names the
|
|
1941
|
+
* impl's type). A record a group derives in the units' own code is written
|
|
1942
|
+
* there, as the file's parsed record on that line is: the calls pass
|
|
1943
|
+
* consults those for the rest (rustPendingRecords). Expanded length: the
|
|
1944
|
+
* text the group's own expansions add, for the cost of reading it.
|
|
1945
|
+
*/
|
|
1946
|
+
function groupRecords(content, regions, nonCode, outputs) {
|
|
1947
|
+
const { units } = analysisUnits(regions, 'calls');
|
|
1948
|
+
const groups = groupsOf(units);
|
|
1949
|
+
const lookups = unitLookups(units);
|
|
1950
|
+
const members = groups.map(() => []);
|
|
1951
|
+
const groupOf = regions.map((region, i) => {
|
|
1952
|
+
const group = lookups.unitAtOffset(region.start).group;
|
|
1953
|
+
members[group].push(i);
|
|
1954
|
+
return group;
|
|
1955
|
+
});
|
|
1956
|
+
// Expansions are disjoint and sorted: the first one ending after `at`.
|
|
1957
|
+
const firstEndingAfter = at => {
|
|
1958
|
+
let lo = 0;
|
|
1959
|
+
let hi = regions.length;
|
|
1960
|
+
while (lo < hi) {
|
|
1961
|
+
const mid = (lo + hi) >> 1;
|
|
1962
|
+
if (regions[mid].end <= at) lo = mid + 1; else hi = mid;
|
|
1963
|
+
}
|
|
1964
|
+
return lo;
|
|
1965
|
+
};
|
|
1966
|
+
return groups.map((group, gi) => {
|
|
1967
|
+
const keep = analysisUnits(members[gi].map(i => regions[i]), 'calls').keep;
|
|
1968
|
+
const words = new Set();
|
|
1969
|
+
let length = 0;
|
|
1970
|
+
for (const i of members[gi]) {
|
|
1971
|
+
length += regions[i].layout.text.length;
|
|
1972
|
+
for (const word of outputs[i]) words.add(word);
|
|
1973
|
+
}
|
|
1974
|
+
// Expansions inside its kept headers (`impl T for m!() {..}`).
|
|
1975
|
+
for (const [start, end] of keep) {
|
|
1976
|
+
for (let i = firstEndingAfter(start); i < regions.length && regions[i].start < end; i++) {
|
|
1977
|
+
if (groupOf[i] !== gi) for (const word of outputs[i]) words.add(word);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
// Header code: the kept ranges outside the group's units.
|
|
1981
|
+
const headers = [];
|
|
1982
|
+
for (const [start, end] of keep) {
|
|
1983
|
+
let at = start;
|
|
1984
|
+
for (const unit of group.units) {
|
|
1985
|
+
if (unit.end <= at || unit.start >= end) continue;
|
|
1986
|
+
if (unit.start > at) headers.push([at, unit.start]);
|
|
1987
|
+
at = Math.max(at, unit.end);
|
|
1988
|
+
}
|
|
1989
|
+
if (at < end) headers.push([at, end]);
|
|
1990
|
+
}
|
|
1991
|
+
scanCodeWords(content, headers, nonCode, words);
|
|
1992
|
+
// Insertion order is the expansions' and the text's: deterministic.
|
|
1993
|
+
return [group.startLine, group.endLine, length, [...words].join(' ')];
|
|
1994
|
+
});
|
|
1658
1995
|
}
|
|
1659
1996
|
|
|
1660
1997
|
/** Whether the derived calls of a file pending derivation can bear `name`. */
|
|
@@ -1665,8 +2002,56 @@ function rustPendingMayBear(fileEntry, name) {
|
|
|
1665
2002
|
if (typeof rec.words !== 'string') return true;
|
|
1666
2003
|
let words = pendingWordSets.get(rec);
|
|
1667
2004
|
if (!words) pendingWordSets.set(rec, words = new Set(rec.words.split(' ')));
|
|
1668
|
-
|
|
1669
|
-
|
|
2005
|
+
return hasWord(words, name);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* Whether a recorded group's word list (space-separated) holds `name`, as
|
|
2010
|
+
* hasWord does for a set (fix #405b: one name is asked per group, so the
|
|
2011
|
+
* list is searched rather than split).
|
|
2012
|
+
*/
|
|
2013
|
+
function groupHasWord(list, name) {
|
|
2014
|
+
const text = String(name);
|
|
2015
|
+
const bare = text.replace(/^r#/, '');
|
|
2016
|
+
return listHasWord(list, text) || (bare !== text && listHasWord(list, bare)) || listHasWord(list, `r#${text}`);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
function listHasWord(list, word) {
|
|
2020
|
+
if (!word) return false;
|
|
2021
|
+
for (let at = list.indexOf(word); at >= 0; at = list.indexOf(word, at + 1)) {
|
|
2022
|
+
const end = at + word.length;
|
|
2023
|
+
if ((at === 0 || list.charCodeAt(at - 1) === 32) && (end === list.length || list.charCodeAt(end) === 32)) return true;
|
|
2024
|
+
}
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
const pendingFileLists = new WeakMap(); // operation content cache -> pending file paths
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Files whose macro expansions' calls are not derived yet and whose
|
|
2032
|
+
* expanded text can hold one of `names` (fix #405). The callee index lists
|
|
2033
|
+
* such a file under its parsed records' names and its expansions'
|
|
2034
|
+
* callee-position names, but a derived record can bear any word of the
|
|
2035
|
+
* expanded text (`self.view` passed as a value, `Self { .. }` named by its
|
|
2036
|
+
* impl): a query visits these files too, so the files it reads do not
|
|
2037
|
+
* depend on which files earlier queries derived.
|
|
2038
|
+
*/
|
|
2039
|
+
function rustPendingFilesBearing(index, names) {
|
|
2040
|
+
const scope = index._opContentCache;
|
|
2041
|
+
let files = scope ? pendingFileLists.get(scope) : undefined;
|
|
2042
|
+
if (!files) {
|
|
2043
|
+
files = [];
|
|
2044
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
2045
|
+
if (fileEntry.rustMacroExpansion?.callsPending) files.push(filePath);
|
|
2046
|
+
}
|
|
2047
|
+
if (scope) pendingFileLists.set(scope, files);
|
|
2048
|
+
}
|
|
2049
|
+
const bearing = [];
|
|
2050
|
+
for (const filePath of files) {
|
|
2051
|
+
const fileEntry = index.files.get(filePath);
|
|
2052
|
+
if (names.some(name => rustPendingMayBear(fileEntry, name))) bearing.push(filePath);
|
|
2053
|
+
}
|
|
2054
|
+
return bearing;
|
|
1670
2055
|
}
|
|
1671
2056
|
|
|
1672
2057
|
function tokenText(tokens) {
|
|
@@ -1824,7 +2209,10 @@ function applyRustMacroExpansion(index, pending, options = {}, started = undefin
|
|
|
1824
2209
|
...(rec.blind.length > 0 && { blind: rec.blind.sort((a, b) => a.line - b.line ||
|
|
1825
2210
|
codeUnitCompare(a.macro, b.macro) || codeUnitCompare(a.reason, b.reason)) }),
|
|
1826
2211
|
...(rec.expanded && { expanded: true }),
|
|
1827
|
-
...(rec.callsPending && {
|
|
2212
|
+
...(rec.callsPending && {
|
|
2213
|
+
callsPending: true, calleeNames: rec.calleeNames || [], words: rec.words, expansions: rec.expansions,
|
|
2214
|
+
...(rec.groups && { groups: rec.groups }),
|
|
2215
|
+
}),
|
|
1828
2216
|
};
|
|
1829
2217
|
}
|
|
1830
2218
|
return touched || false;
|
|
@@ -1978,7 +2366,10 @@ function expandForWorker(state, filePath) {
|
|
|
1978
2366
|
sites: rec.sites, blind: rec.blind, expanded: rec.expanded,
|
|
1979
2367
|
usedNames: [...rec.used.values()].map(def => def.name),
|
|
1980
2368
|
...(rec.symbols && { symbols: rec.symbols }),
|
|
1981
|
-
...(rec.callsPending && {
|
|
2369
|
+
...(rec.callsPending && {
|
|
2370
|
+
callsPending: true, calleeNames: rec.calleeNames, words: rec.words, expansions: rec.expansions,
|
|
2371
|
+
...(rec.groups && { groups: rec.groups }),
|
|
2372
|
+
}),
|
|
1982
2373
|
};
|
|
1983
2374
|
}
|
|
1984
2375
|
|
|
@@ -2051,20 +2442,538 @@ function rustExpandedCalls(index, filePath, rawCalls, { materialize = true } = {
|
|
|
2051
2442
|
if (!rec?.callsPending || !Array.isArray(rawCalls)) return rawCalls;
|
|
2052
2443
|
const hints = (rec.calleeNames || []).map(name => ({ name, line: 0, calleeHint: true }));
|
|
2053
2444
|
if (!materialize) return hints.length > 0 ? rawCalls.concat(hints) : rawCalls;
|
|
2054
|
-
|
|
2445
|
+
const ctx = pendingContext(index, filePath, { whole: true });
|
|
2055
2446
|
if (index.calleeIndex && hints.length > 0) index._removeFromCalleeIndex(filePath, hints);
|
|
2056
2447
|
rec.callsPending = false;
|
|
2057
2448
|
try {
|
|
2058
|
-
|
|
2449
|
+
if (ctx) composeCalls(index, ctx);
|
|
2059
2450
|
} catch (error) {
|
|
2060
2451
|
rec.callsPending = true;
|
|
2061
2452
|
throw error;
|
|
2062
2453
|
}
|
|
2454
|
+
index._rustMacroState.contexts.delete(filePath);
|
|
2063
2455
|
delete rec.words;
|
|
2456
|
+
delete rec.groups;
|
|
2457
|
+
delete rec.expansions;
|
|
2064
2458
|
index.macroExpansionDirty = true;
|
|
2065
2459
|
return index.callsCache.get(filePath)?.calls || rawCalls;
|
|
2066
2460
|
}
|
|
2067
2461
|
|
|
2462
|
+
// Derivation contexts whose trees are kept (fix #405); a context dropped
|
|
2463
|
+
// earlier keeps the records of the groups it read.
|
|
2464
|
+
const LIVE_CONTEXTS = 3;
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
* The calls-pass context of a file whose expansion calls are pending, or
|
|
2468
|
+
* null when nothing of it expands (its parsed records stand).
|
|
2469
|
+
*/
|
|
2470
|
+
function pendingContext(index, filePath, { whole = false } = {}) {
|
|
2471
|
+
if (!index._rustMacroState) index._rustMacroState = createState(index);
|
|
2472
|
+
const state = index._rustMacroState;
|
|
2473
|
+
const contexts = state.contexts;
|
|
2474
|
+
let ctx = contexts.get(filePath);
|
|
2475
|
+
if (ctx === null) return null;
|
|
2476
|
+
if (ctx && ctx.live) {
|
|
2477
|
+
contexts.delete(filePath);
|
|
2478
|
+
contexts.set(filePath, ctx);
|
|
2479
|
+
return ctx;
|
|
2480
|
+
}
|
|
2481
|
+
// A file derived whole before any part of it was read is read in one
|
|
2482
|
+
// pass (fix #405b): the groups apart cost the same expansions and parse
|
|
2483
|
+
// plus a second plan of the file's invocations.
|
|
2484
|
+
const fresh = whole && ctx === undefined
|
|
2485
|
+
? expandFile(state, filePath, 'calls')?.context || null
|
|
2486
|
+
: buildContext(state, filePath);
|
|
2487
|
+
if (fresh && ctx) {
|
|
2488
|
+
// Rebuilt: the groups the dropped context read keep their records.
|
|
2489
|
+
if (fresh.groups.length !== ctx.groups.length || !!fresh.isolated !== !!ctx.isolated) {
|
|
2490
|
+
throw new Error(`Rust macro expansion context changed: ${filePath}`);
|
|
2491
|
+
}
|
|
2492
|
+
for (let i = 0; i < fresh.groups.length; i++) {
|
|
2493
|
+
const kept = ctx.groups[i];
|
|
2494
|
+
if (!kept.records) continue;
|
|
2495
|
+
Object.assign(fresh.groups[i], { records: kept.records, units: kept.units, starts: kept.starts });
|
|
2496
|
+
}
|
|
2497
|
+
fresh.invocations = ctx.invocations;
|
|
2498
|
+
fresh.seen = ctx.seen;
|
|
2499
|
+
}
|
|
2500
|
+
contexts.delete(filePath);
|
|
2501
|
+
contexts.set(filePath, fresh);
|
|
2502
|
+
if (fresh) {
|
|
2503
|
+
let live = 0;
|
|
2504
|
+
for (const [file, other] of [...contexts].reverse()) {
|
|
2505
|
+
if (!other?.live) continue;
|
|
2506
|
+
if (++live <= LIVE_CONTEXTS || file === filePath) continue;
|
|
2507
|
+
contexts.set(file, releaseContext(other));
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return fresh;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* A file's calls-pass context: its groups read apart when the build's
|
|
2515
|
+
* record of its expansions still matches its invocations, else one pass
|
|
2516
|
+
* over the whole file. null when nothing of it expands.
|
|
2517
|
+
*/
|
|
2518
|
+
function buildContext(state, filePath) {
|
|
2519
|
+
const rec = state.index.files.get(filePath)?.rustMacroExpansion;
|
|
2520
|
+
// Expansions the build could not parse in context are read in one pass
|
|
2521
|
+
// (whose retries can also drop a neighbor).
|
|
2522
|
+
if (Array.isArray(rec?.expansions) && !(rec.blind || []).some(site => site.reason === 'parse-error')) {
|
|
2523
|
+
const isolated = isolatedContext(state, filePath, rec.expansions);
|
|
2524
|
+
if (isolated !== undefined) return isolated;
|
|
2525
|
+
}
|
|
2526
|
+
return expandFile(state, filePath, 'calls')?.context || null;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
/** What a dropped context keeps: the records it read. */
|
|
2530
|
+
function releaseContext(ctx) {
|
|
2531
|
+
return {
|
|
2532
|
+
isolated: ctx.isolated,
|
|
2533
|
+
groups: ctx.groups.map(group => ({ records: group.records, units: group.units, starts: group.starts })),
|
|
2534
|
+
invocations: ctx.invocations,
|
|
2535
|
+
seen: ctx.seen,
|
|
2536
|
+
live: false,
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
/**
|
|
2541
|
+
* A context that reads each group apart (fix #405). The build recorded, for
|
|
2542
|
+
* every invocation it expanded, its hygiene start and its expanded length:
|
|
2543
|
+
* a group expands alone with the numbering and the offsets of a pass over
|
|
2544
|
+
* the whole file, every other expansion standing in as a blank of its
|
|
2545
|
+
* length. undefined when that record no longer matches the file's
|
|
2546
|
+
* invocations or the file has parse errors (one pass reads it), null when
|
|
2547
|
+
* nothing of it expands.
|
|
2548
|
+
*/
|
|
2549
|
+
function isolatedContext(state, filePath, expansions) {
|
|
2550
|
+
const index = state.index;
|
|
2551
|
+
const content = readContent(state, filePath);
|
|
2552
|
+
if (!index.files.get(filePath) || content === null) return undefined;
|
|
2553
|
+
if (!fileMayExpand(state, filePath)) return null;
|
|
2554
|
+
const tree = safeParse(state.parser, content, undefined, PARSE_OPTIONS);
|
|
2555
|
+
if (tree.rootNode.hasError) return undefined;
|
|
2556
|
+
const plan = plannedInvocations(state, filePath, content, tree, { blind: [] });
|
|
2557
|
+
if (plan.length * 3 !== expansions.length) return undefined;
|
|
2558
|
+
const slots = [];
|
|
2559
|
+
for (let i = 0; i < plan.length; i++) {
|
|
2560
|
+
const item = plan[i];
|
|
2561
|
+
if (item.node.startIndex !== expansions[3 * i]) return undefined;
|
|
2562
|
+
item.hygiene = expansions[3 * i + 1];
|
|
2563
|
+
item.length = expansions[3 * i + 2];
|
|
2564
|
+
if (item.length < 0) continue;
|
|
2565
|
+
slots.push({
|
|
2566
|
+
item, node: item.node, start: item.node.startIndex, end: item.node.endIndex,
|
|
2567
|
+
startLine: item.line, endLine: item.node.endPosition.row + 1,
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
if (slots.length === 0) return null;
|
|
2571
|
+
slots.sort((a, b) => a.start - b.start);
|
|
2572
|
+
const { units } = analysisUnits(slots, 'calls');
|
|
2573
|
+
const lookups = unitLookups(units);
|
|
2574
|
+
const groups = groupsOf(units);
|
|
2575
|
+
for (const group of groups) group.slots = [];
|
|
2576
|
+
for (const slot of slots) groups[lookups.unitAtOffset(slot.start).group].slots.push(slot);
|
|
2577
|
+
return {
|
|
2578
|
+
isolated: true, filePath, content, tree, slots, groups,
|
|
2579
|
+
original: index.callsCache.get(filePath)?.calls || [],
|
|
2580
|
+
seen: new Set(), invocations: new Map(), parser: state.parser, state, live: true,
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
/** Units sharing a line form one group; units get their group's index. */
|
|
2585
|
+
function groupsOf(units) {
|
|
2586
|
+
const groups = [];
|
|
2587
|
+
for (const unit of units) {
|
|
2588
|
+
const last = groups[groups.length - 1];
|
|
2589
|
+
if (last && unit.startLine <= last.endLine) {
|
|
2590
|
+
last.units.push(unit);
|
|
2591
|
+
if (unit.endLine > last.endLine) last.endLine = unit.endLine;
|
|
2592
|
+
} else {
|
|
2593
|
+
groups.push({ units: [unit], startLine: unit.startLine, endLine: unit.endLine, records: null });
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
for (let i = 0; i < groups.length; i++) for (const unit of groups[i].units) unit.group = i;
|
|
2597
|
+
return groups;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
/**
|
|
2601
|
+
* Read groups of an isolated context. Groups whose expanded text parses
|
|
2602
|
+
* without an error read together as alone (each unit is an item of its
|
|
2603
|
+
* container); otherwise each group is read alone.
|
|
2604
|
+
*/
|
|
2605
|
+
function readIsolatedGroups(ctx, targets) {
|
|
2606
|
+
if (targets.length === 1) {
|
|
2607
|
+
readIsolatedGroup(ctx, targets[0]);
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
const rec = { blind: [], used: new Map() };
|
|
2611
|
+
const wanted = new Set(targets);
|
|
2612
|
+
const mine = new Set(targets.flatMap(i => ctx.groups[i].slots));
|
|
2613
|
+
const expand = slot => expandSlot(ctx, slot, rec);
|
|
2614
|
+
const live = ctx.slots.filter(slot => mine.has(slot)).map(expand);
|
|
2615
|
+
const keep = analysisUnits(live, 'calls').keep;
|
|
2616
|
+
const extras = ctx.slots.filter(slot => !mine.has(slot) &&
|
|
2617
|
+
keep.some(([start, end]) => slot.start < end && slot.end > start)).map(expand);
|
|
2618
|
+
const expanded = live.concat(extras);
|
|
2619
|
+
const bySlot = new Map(expanded.map(region => [region.slot, region]));
|
|
2620
|
+
const all = ctx.slots.map(slot => bySlot.get(slot) || placeholderOf(ctx, slot));
|
|
2621
|
+
const built = buildSynthetic(ctx.content, all, analysisUnits(expanded, 'calls').keep);
|
|
2622
|
+
const synthTree = parseSynthetic(ctx.parser, built.text, built.spans);
|
|
2623
|
+
if (collectErrorRanges(synthTree.rootNode, []).length > 0) {
|
|
2624
|
+
for (const i of targets) readIsolatedGroup(ctx, i);
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
const units = [];
|
|
2628
|
+
for (const i of targets) {
|
|
2629
|
+
const group = ctx.groups[i];
|
|
2630
|
+
const regions = live.filter(region => region.slot && ctx.groups[i].slots.includes(region.slot));
|
|
2631
|
+
group.units = analysisUnits(regions, 'calls').units;
|
|
2632
|
+
for (const unit of group.units) unit.group = i;
|
|
2633
|
+
group.starts = regions.map(region => region.start);
|
|
2634
|
+
group.records = [];
|
|
2635
|
+
units.push(...group.units);
|
|
2636
|
+
}
|
|
2637
|
+
units.sort((a, b) => a.start - b.start);
|
|
2638
|
+
const lookups = unitLookups(units);
|
|
2639
|
+
const originalSpanless = new Map();
|
|
2640
|
+
for (const record of ctx.original) {
|
|
2641
|
+
if (Number.isFinite(record.callStart) || !lookups.unitAtLine(record.line)) continue;
|
|
2642
|
+
const key = spanlessKey(record);
|
|
2643
|
+
originalSpanless.set(key, (originalSpanless.get(key) || 0) + 1);
|
|
2644
|
+
}
|
|
2645
|
+
const rust = require('../languages/rust');
|
|
2646
|
+
const synthCalls = rust.findCallsInCode(built.text, ctx.parser, {
|
|
2647
|
+
tree: synthTree, itemRecovery: rust.rustExpandedItemRecovery(built.text, ctx.parser, synthTree),
|
|
2648
|
+
});
|
|
2649
|
+
mergeRecords(ctx, synthCalls, {
|
|
2650
|
+
mapper: offsetMapper(all), lookups, regions: live, synthTree, originalSpanless,
|
|
2651
|
+
accept: unit => wanted.has(unit.group), push: (unit, call) => ctx.groups[unit.group].records.push(call),
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
/** Expand one invocation of an isolated context as the build did. */
|
|
2656
|
+
function expandSlot(ctx, slot, rec) {
|
|
2657
|
+
ctx.state.hygieneSeq = slot.item.hygiene;
|
|
2658
|
+
const region = expandPlanned(ctx.state, ctx.filePath, ctx.content, slot.item, rec);
|
|
2659
|
+
if (!region || region.layout.text.length !== slot.item.length) {
|
|
2660
|
+
throw new Error(`Rust macro expansion at ${ctx.filePath}:${slot.startLine} differs from the build's`);
|
|
2661
|
+
}
|
|
2662
|
+
region.slot = slot;
|
|
2663
|
+
return region;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
/** A blank of an expansion's length standing in for it (its newlines kept). */
|
|
2667
|
+
function placeholderOf(ctx, slot) {
|
|
2668
|
+
if (!ctx.placeholders) ctx.placeholders = new Map();
|
|
2669
|
+
let region = ctx.placeholders.get(slot);
|
|
2670
|
+
if (!region) {
|
|
2671
|
+
const breaks = slot.endLine - slot.startLine;
|
|
2672
|
+
region = {
|
|
2673
|
+
start: slot.start, end: slot.end, startLine: slot.startLine, endLine: slot.endLine, blank: true,
|
|
2674
|
+
layout: { text: ' '.repeat(slot.item.length - breaks) + '\n'.repeat(breaks), marks: [] },
|
|
2675
|
+
};
|
|
2676
|
+
ctx.placeholders.set(slot, region);
|
|
2677
|
+
}
|
|
2678
|
+
// Its offsets are set by the synthetic it stands in.
|
|
2679
|
+
return { ...region };
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
/** Read one group of an isolated context: its expansions, parsed alone. */
|
|
2683
|
+
function readIsolatedGroup(ctx, gi) {
|
|
2684
|
+
const group = ctx.groups[gi];
|
|
2685
|
+
const rec = { blind: [], used: new Map() };
|
|
2686
|
+
const expand = slot => expandSlot(ctx, slot, rec);
|
|
2687
|
+
const mine = new Set(group.slots);
|
|
2688
|
+
let live = group.slots.map(expand);
|
|
2689
|
+
// Expansions inside the kept headers of this group's units
|
|
2690
|
+
// (`impl T for m!() { .. }`) are part of its text.
|
|
2691
|
+
const headerKeep = analysisUnits(live, 'calls').keep;
|
|
2692
|
+
let extras = ctx.slots.filter(slot => !mine.has(slot) &&
|
|
2693
|
+
headerKeep.some(([start, end]) => slot.start < end && slot.end > start)).map(expand);
|
|
2694
|
+
let synthetic = null;
|
|
2695
|
+
let synthTree = null;
|
|
2696
|
+
let all = null;
|
|
2697
|
+
for (let attempt = 0; live.length > 0 && attempt <= ctx.slots.length; attempt++) {
|
|
2698
|
+
const expanded = live.concat(extras);
|
|
2699
|
+
const bySlot = new Map(expanded.map(region => [region.slot, region]));
|
|
2700
|
+
const dropped = new Set(group.slots.filter(slot => !bySlot.has(slot)));
|
|
2701
|
+
// A dropped expansion stays as written, as in one pass.
|
|
2702
|
+
all = ctx.slots.filter(slot => !dropped.has(slot)).map(slot => bySlot.get(slot) || placeholderOf(ctx, slot));
|
|
2703
|
+
const built = buildSynthetic(ctx.content, all, analysisUnits(expanded, 'calls').keep);
|
|
2704
|
+
synthetic = built.text;
|
|
2705
|
+
synthTree = parseSynthetic(ctx.parser, synthetic, built.spans);
|
|
2706
|
+
const errors = collectErrorRanges(synthTree.rootNode, []);
|
|
2707
|
+
const bad = new Set();
|
|
2708
|
+
for (const [start, end] of errors) {
|
|
2709
|
+
for (const region of expanded) {
|
|
2710
|
+
if (start < region.synthEnd && end > region.synthStart) bad.add(region);
|
|
2711
|
+
else if (start === end && start >= region.synthStart && start <= region.synthEnd) bad.add(region);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
if (bad.size === 0) break;
|
|
2715
|
+
live = live.filter(region => !bad.has(region));
|
|
2716
|
+
extras = extras.filter(region => !bad.has(region));
|
|
2717
|
+
}
|
|
2718
|
+
group.units = live.length > 0 ? analysisUnits(live, 'calls').units : [];
|
|
2719
|
+
group.starts = live.map(region => region.start);
|
|
2720
|
+
group.records = [];
|
|
2721
|
+
if (live.length === 0) return;
|
|
2722
|
+
const lookups = unitLookups(group.units);
|
|
2723
|
+
const mapper = offsetMapper(all);
|
|
2724
|
+
const originalSpanless = new Map();
|
|
2725
|
+
for (const record of ctx.original) {
|
|
2726
|
+
if (Number.isFinite(record.callStart) || !lookups.unitAtLine(record.line)) continue;
|
|
2727
|
+
const key = spanlessKey(record);
|
|
2728
|
+
originalSpanless.set(key, (originalSpanless.get(key) || 0) + 1);
|
|
2729
|
+
}
|
|
2730
|
+
const rust = require('../languages/rust');
|
|
2731
|
+
const synthCalls = rust.findCallsInCode(synthetic, ctx.parser, {
|
|
2732
|
+
tree: synthTree, itemRecovery: rust.rustExpandedItemRecovery(synthetic, ctx.parser, synthTree),
|
|
2733
|
+
});
|
|
2734
|
+
mergeRecords(ctx, synthCalls, {
|
|
2735
|
+
mapper, lookups, regions: live, synthTree, originalSpanless,
|
|
2736
|
+
accept: () => true, push: (unit, call) => group.records.push(call),
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
/**
|
|
2741
|
+
* Words the records of an isolated group can bear: its lines and its units'
|
|
2742
|
+
* kept headers in the file (literals and comments left out), the text of
|
|
2743
|
+
* every project macro its expansions can transcribe, and the crate names
|
|
2744
|
+
* `$crate` becomes.
|
|
2745
|
+
*/
|
|
2746
|
+
function isolatedGroupWords(ctx, group) {
|
|
2747
|
+
if (group.words) return group.words;
|
|
2748
|
+
const state = ctx.state;
|
|
2749
|
+
const words = new Set();
|
|
2750
|
+
const ranges = analysisUnits(group.slots, 'calls').keep.concat([[
|
|
2751
|
+
lineOffset(state, ctx.filePath, group.startLine), lineEndOffset(state, ctx.filePath, group.endLine),
|
|
2752
|
+
]]).sort((a, b) => a[0] - b[0]);
|
|
2753
|
+
if (!ctx.nonCode) ctx.nonCode = ctx.tree.rootNode.descendantsOfType(NON_CODE_TOKENS);
|
|
2754
|
+
const nonCode = ctx.nonCode;
|
|
2755
|
+
const scan = (start, end) => {
|
|
2756
|
+
if (end <= start) return;
|
|
2757
|
+
for (const word of ctx.content.slice(start, end).match(WORD) || []) words.add(word);
|
|
2758
|
+
};
|
|
2759
|
+
let next = 0;
|
|
2760
|
+
let done = 0;
|
|
2761
|
+
for (const [rangeStart, end] of ranges) {
|
|
2762
|
+
let at = Math.max(rangeStart, done);
|
|
2763
|
+
if (end <= at) continue;
|
|
2764
|
+
while (next < nonCode.length && nonCode[next].endIndex <= at) next++;
|
|
2765
|
+
for (let i = next; i < nonCode.length; i++) {
|
|
2766
|
+
const token = nonCode[i];
|
|
2767
|
+
if (token.startIndex >= end) break;
|
|
2768
|
+
scan(at, Math.min(token.startIndex, end));
|
|
2769
|
+
at = Math.max(at, token.endIndex);
|
|
2770
|
+
}
|
|
2771
|
+
scan(at, end);
|
|
2772
|
+
done = end;
|
|
2773
|
+
}
|
|
2774
|
+
for (const word of [...words]) if (state.inventory.has(word)) addTemplateWords(state, word, words);
|
|
2775
|
+
words.add('crate');
|
|
2776
|
+
group.words = words;
|
|
2777
|
+
return words;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/** Words of every definition of macro `name` and the macros they name, transitively. */
|
|
2781
|
+
function addTemplateWords(state, name, words) {
|
|
2782
|
+
if (!state.templateWords) state.templateWords = new Map();
|
|
2783
|
+
let own = state.templateWords.get(name);
|
|
2784
|
+
if (!own) {
|
|
2785
|
+
own = new Set();
|
|
2786
|
+
state.templateWords.set(name, own);
|
|
2787
|
+
const pending = [name];
|
|
2788
|
+
const visited = new Set();
|
|
2789
|
+
while (pending.length > 0) {
|
|
2790
|
+
const next = pending.pop();
|
|
2791
|
+
if (visited.has(next)) continue;
|
|
2792
|
+
visited.add(next);
|
|
2793
|
+
for (const def of state.inventory.get(next) || []) {
|
|
2794
|
+
const content = readContent(state, def.file);
|
|
2795
|
+
if (content === null) continue;
|
|
2796
|
+
const text = content.slice(lineOffset(state, def.file, def.startLine), lineOffset(state, def.file, def.endLine + 1));
|
|
2797
|
+
for (const word of text.match(WORD) || []) {
|
|
2798
|
+
own.add(word);
|
|
2799
|
+
if (state.inventory.has(word) && !visited.has(word)) pending.push(word);
|
|
2800
|
+
}
|
|
2801
|
+
const crate = packageOf(state, def.file)?.packageName;
|
|
2802
|
+
if (crate) own.add(crate);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
for (const word of own) words.add(word);
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
/**
|
|
2810
|
+
* The records of a file whose expansion calls are pending that a consumer
|
|
2811
|
+
* reading only part of them needs (fix #405), in the order the composed
|
|
2812
|
+
* calls hold them and as the same record objects. `name`: the records
|
|
2813
|
+
* `keep` accepts, each of which must bear the name as one of its own
|
|
2814
|
+
* identifier tokens (its name or written receiver); `lines`: [start, end],
|
|
2815
|
+
* the records on those lines; `lineSet`: the records on any of those lines. Only the groups that can hold such records
|
|
2816
|
+
* are read. undefined when the file's calls are not pending, `name` is not
|
|
2817
|
+
* an identifier, or every group is read (the file's calls are then derived
|
|
2818
|
+
* whole by the caller).
|
|
2819
|
+
*/
|
|
2820
|
+
const IDENTIFIER = /^(?:r#)?[\p{L}_][\p{L}\p{N}_]*$/u;
|
|
2821
|
+
// Tenths of a file's unread expanded text from which a view derives the file
|
|
2822
|
+
// whole (fix #405b).
|
|
2823
|
+
const WHOLE_FILE_SHARE = 9;
|
|
2824
|
+
|
|
2825
|
+
function rustPendingRecords(index, filePath, { name = null, keep = null, lines = null, lineSet = null } = {}) {
|
|
2826
|
+
const entry = index.files.get(filePath);
|
|
2827
|
+
if (!entry?.rustMacroExpansion?.callsPending) return undefined;
|
|
2828
|
+
// Hygiene renames locals a transcriber introduces (`x__ucn_h3`): no
|
|
2829
|
+
// source text holds such a name.
|
|
2830
|
+
if (name !== null && (!IDENTIFIER.test(name) || /__ucn_h\d+$/.test(name))) return undefined;
|
|
2831
|
+
const raw = index.callsCache.get(filePath)?.calls;
|
|
2832
|
+
if (!Array.isArray(raw)) return undefined;
|
|
2833
|
+
const matches = record => (!lines || (record.line >= lines[0] && record.line <= lines[1])) &&
|
|
2834
|
+
(!lineSet || lineSet.has(record.line)) && (!keep || keep(record));
|
|
2835
|
+
// No derived record bears a name outside the file's words (#375): the
|
|
2836
|
+
// parsed records then answer, unless one of them bears it.
|
|
2837
|
+
if (name !== null && !rustPendingMayBear(entry, name) && !raw.some(matches)) return [];
|
|
2838
|
+
// The groups recorded at build (fix #405b) are chosen before anything is
|
|
2839
|
+
// parsed: a group can hold a record `matches` accepts when a word its
|
|
2840
|
+
// expansions output or its headers hold is the name, or when a parsed
|
|
2841
|
+
// record on its lines is accepted (the units' own code is written there).
|
|
2842
|
+
const rec = entry.rustMacroExpansion;
|
|
2843
|
+
const recorded = Array.isArray(rec.groups) ? rec.groups : null;
|
|
2844
|
+
let planned = null;
|
|
2845
|
+
if (recorded) {
|
|
2846
|
+
const read = index._rustMacroState?.contexts?.get(filePath)?.groups || null;
|
|
2847
|
+
const parsedOn = name !== null ? parsedRecordLines(raw, matches) : null;
|
|
2848
|
+
planned = [];
|
|
2849
|
+
let wantedLength = 0;
|
|
2850
|
+
let unreadLength = 0;
|
|
2851
|
+
for (let i = 0; i < recorded.length; i++) {
|
|
2852
|
+
const [startLine, endLine, length] = recorded[i];
|
|
2853
|
+
const unread = !read?.[i]?.records;
|
|
2854
|
+
if (unread) unreadLength += length;
|
|
2855
|
+
if (lines && (endLine < lines[0] || startLine > lines[1])) continue;
|
|
2856
|
+
if (lineSet && ![...lineSet].some(line => line >= startLine && line <= endLine)) continue;
|
|
2857
|
+
if (name !== null && !groupHasWord(recorded[i][3], name) &&
|
|
2858
|
+
!parsedOn.some(line => line >= startLine && line <= endLine)) continue;
|
|
2859
|
+
planned.push(i);
|
|
2860
|
+
if (unread) wantedLength += length;
|
|
2861
|
+
}
|
|
2862
|
+
if (planned.length === 0) {
|
|
2863
|
+
// Every group read: the caller derives the file's calls whole.
|
|
2864
|
+
if (read && read.length > 0 && read.every(group => group.records)) return undefined;
|
|
2865
|
+
// No group can hold one: the accepted records are parsed ones
|
|
2866
|
+
// outside every unit, as the composed calls hold them.
|
|
2867
|
+
return raw.filter(matches).sort((a, b) => a.line - b.line);
|
|
2868
|
+
}
|
|
2869
|
+
// Reading most of the expanded text that is still unread costs more
|
|
2870
|
+
// than deriving the file whole (groups read together cost ~5-10%
|
|
2871
|
+
// over one pass), which then answers every later query of the file:
|
|
2872
|
+
// a cost decision, not a semantic one - either way the records are
|
|
2873
|
+
// the composed calls'.
|
|
2874
|
+
if (wantedLength > 0 && wantedLength * 10 >= unreadLength * WHOLE_FILE_SHARE) return undefined;
|
|
2875
|
+
}
|
|
2876
|
+
const ctx = pendingContext(index, filePath);
|
|
2877
|
+
if (!ctx) return raw.filter(matches);
|
|
2878
|
+
// Every group read: the caller derives the file's calls whole.
|
|
2879
|
+
if (ctx.groups.every(group => group.records)) return undefined;
|
|
2880
|
+
let wanted;
|
|
2881
|
+
if (planned && ctx.groups.length === recorded.length && ctx.groups.every((group, i) =>
|
|
2882
|
+
group.startLine === recorded[i][0] && group.endLine === recorded[i][1])) {
|
|
2883
|
+
wanted = planned;
|
|
2884
|
+
} else {
|
|
2885
|
+
wanted = [];
|
|
2886
|
+
for (let i = 0; i < ctx.groups.length; i++) {
|
|
2887
|
+
const group = ctx.groups[i];
|
|
2888
|
+
if (lines && (group.endLine < lines[0] || group.startLine > lines[1])) continue;
|
|
2889
|
+
if (lineSet && ![...lineSet].some(line => line >= group.startLine && line <= group.endLine)) continue;
|
|
2890
|
+
if (name !== null && !groupMayBear(ctx, group, name)) continue;
|
|
2891
|
+
wanted.push(i);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
readGroups(ctx, wanted);
|
|
2895
|
+
if (ctx.groups.every(group => group.records)) return undefined;
|
|
2896
|
+
const out = ctx.original.filter(record => matches(record) && !inReadUnit(ctx, record));
|
|
2897
|
+
for (const i of wanted) for (const record of ctx.groups[i].records) if (matches(record)) out.push(record);
|
|
2898
|
+
for (const record of invocationRecords(ctx)) if (matches(record)) out.push(record);
|
|
2899
|
+
return out.sort((a, b) => a.line - b.line);
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
/**
|
|
2903
|
+
* Whether a group's records can bear `name`: a word of the group's lines in
|
|
2904
|
+
* the expanded file or of the kept text outside every unit (headers: a
|
|
2905
|
+
* `Self` record bears its impl's type name).
|
|
2906
|
+
*/
|
|
2907
|
+
function groupMayBear(ctx, group, name) {
|
|
2908
|
+
if (ctx.isolated) return hasWord(isolatedGroupWords(ctx, group), name);
|
|
2909
|
+
if (!ctx.outsideWords) {
|
|
2910
|
+
const words = new Set();
|
|
2911
|
+
let at = 0;
|
|
2912
|
+
for (const unit of ctx.units) {
|
|
2913
|
+
for (const word of ctx.synthetic.slice(at, unit.synthStart).match(WORD) || []) words.add(word);
|
|
2914
|
+
at = Math.max(at, unit.synthEnd);
|
|
2915
|
+
}
|
|
2916
|
+
for (const word of ctx.synthetic.slice(at).match(WORD) || []) words.add(word);
|
|
2917
|
+
ctx.outsideWords = words;
|
|
2918
|
+
}
|
|
2919
|
+
if (hasWord(ctx.outsideWords, name)) return true;
|
|
2920
|
+
if (!group.words) {
|
|
2921
|
+
if (!ctx.synthLineStarts) {
|
|
2922
|
+
const starts = [0];
|
|
2923
|
+
const text = ctx.synthetic;
|
|
2924
|
+
for (let i = text.indexOf('\n'); i >= 0; i = text.indexOf('\n', i + 1)) starts.push(i + 1);
|
|
2925
|
+
ctx.synthLineStarts = starts;
|
|
2926
|
+
}
|
|
2927
|
+
const starts = ctx.synthLineStarts;
|
|
2928
|
+
const from = starts[Math.max(group.startLine - 1, 0)] ?? ctx.synthetic.length;
|
|
2929
|
+
const to = starts[group.endLine] ?? ctx.synthetic.length;
|
|
2930
|
+
group.words = new Set(ctx.synthetic.slice(from, to).match(WORD) || []);
|
|
2931
|
+
}
|
|
2932
|
+
return hasWord(group.words, name);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
/** Lines of the parsed records `matches` accepts. */
|
|
2936
|
+
function parsedRecordLines(raw, matches) {
|
|
2937
|
+
const out = [];
|
|
2938
|
+
for (const record of raw) if (matches(record)) out.push(record.line);
|
|
2939
|
+
return out;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
function hasWord(words, name) {
|
|
2943
|
+
// A raw identifier's token is `r#name`; text holds both parts.
|
|
2944
|
+
return words.has(name) || words.has(String(name).replace(/^r#/, '')) || words.has(`r#${name}`);
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
/**
|
|
2948
|
+
* Relative paths of the files whose expansion calls are pending with no
|
|
2949
|
+
* part derived yet, sorted.
|
|
2950
|
+
*/
|
|
2951
|
+
function rustUntouchedPendingFiles(index) {
|
|
2952
|
+
const contexts = index._rustMacroState?.contexts;
|
|
2953
|
+
const out = [];
|
|
2954
|
+
for (const [filePath, entry] of index.files) {
|
|
2955
|
+
if (!entry.rustMacroExpansion?.callsPending) continue;
|
|
2956
|
+
if (contexts?.get(filePath)?.groups?.some(group => group.records)) continue;
|
|
2957
|
+
out.push(entry.relativePath);
|
|
2958
|
+
}
|
|
2959
|
+
return out.sort(codeUnitCompare);
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
/** Derive whole every file whose expansion calls were derived in part. */
|
|
2963
|
+
function completeRustMacroCalls(index) {
|
|
2964
|
+
const contexts = index._rustMacroState?.contexts;
|
|
2965
|
+
if (!contexts || contexts.size === 0) return 0;
|
|
2966
|
+
let done = 0;
|
|
2967
|
+
for (const filePath of [...contexts.keys()].sort(codeUnitCompare)) {
|
|
2968
|
+
if (!index.files.get(filePath)?.rustMacroExpansion?.callsPending) continue;
|
|
2969
|
+
const cached = index.callsCache.get(filePath);
|
|
2970
|
+
if (!cached) continue;
|
|
2971
|
+
rustExpandedCalls(index, filePath, cached.calls);
|
|
2972
|
+
done++;
|
|
2973
|
+
}
|
|
2974
|
+
return done;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2068
2977
|
/** Derive every pending file's expansion calls (whole-program consumers). */
|
|
2069
2978
|
function materializeRustMacroCalls(index) {
|
|
2070
2979
|
for (const [filePath, entry] of index.files) {
|
|
@@ -2110,4 +3019,8 @@ module.exports = {
|
|
|
2110
3019
|
materializeRustMacroCalls,
|
|
2111
3020
|
rustMacroExpansionSummary,
|
|
2112
3021
|
rustPendingMayBear,
|
|
3022
|
+
rustPendingFilesBearing,
|
|
3023
|
+
rustPendingRecords,
|
|
3024
|
+
completeRustMacroCalls,
|
|
3025
|
+
rustUntouchedPendingFiles,
|
|
2113
3026
|
};
|