ucn 5.5.1 → 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 +30 -21
- package/cli/index.js +1 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- 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 +217 -112
- 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 +151 -19
- package/core/rust-modules.js +1 -1
- 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 +124 -37
- 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 +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
package/core/search.js
CHANGED
|
@@ -72,28 +72,28 @@ function appendTextComplements(index, {
|
|
|
72
72
|
.filter(usage => usage.file === filePath)
|
|
73
73
|
.map(usage => usage.line));
|
|
74
74
|
const pattern = literalNameRegex(name);
|
|
75
|
+
let syntax;
|
|
75
76
|
for (let idx = 0; idx < lines.length; idx++) {
|
|
76
77
|
const line = lines[idx];
|
|
77
78
|
const match = pattern.exec(line);
|
|
78
79
|
if (!match || represented.has(idx + 1)) continue;
|
|
79
80
|
const lineNum = idx + 1;
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
syntax ||= require('./text-usage-syntax').textUsageSyntax(index, filePath, fileEntry, content, name);
|
|
82
|
+
const textKind = syntax.kind(lineNum, match.index);
|
|
82
83
|
// A literal match that no AST identifier record claimed and that is
|
|
83
84
|
// not inside a comment or string is non-code text: JSX children,
|
|
84
85
|
// HTML markup and attributes, regex bodies. The ACCOUNT counts these
|
|
85
86
|
// lines as other-text; the escape hatch must list them too. A line
|
|
86
87
|
// the AST scan classified and then deliberately dropped (Rust enum
|
|
87
88
|
// variants against a struct pin, #234) is code, not text: skip it.
|
|
88
|
-
if (
|
|
89
|
-
index.isIdentifierAtPosition(content, lineNum, match.index, filePath, name)) continue;
|
|
89
|
+
if (textKind === 'identifier') continue;
|
|
90
90
|
const usage = {
|
|
91
91
|
file: filePath,
|
|
92
92
|
relativePath: fileEntry.relativePath,
|
|
93
93
|
line: lineNum,
|
|
94
94
|
content: line,
|
|
95
95
|
usageType: 'text',
|
|
96
|
-
textKind
|
|
96
|
+
textKind,
|
|
97
97
|
isDefinition: false,
|
|
98
98
|
};
|
|
99
99
|
if (context > 0) {
|
|
@@ -106,6 +106,7 @@ function appendTextComplements(index, {
|
|
|
106
106
|
}
|
|
107
107
|
usagesList.push(usage);
|
|
108
108
|
}
|
|
109
|
+
syntax?.save();
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/**
|
|
@@ -339,9 +340,11 @@ function usages(index, name, options = {}) {
|
|
|
339
340
|
options.internalEvidence ? { lexicalScopes: true } : undefined);
|
|
340
341
|
if (astUsages !== null) {
|
|
341
342
|
for (const u of astUsages) {
|
|
342
|
-
//
|
|
343
|
+
// Public usages groups declaration lines into their definition
|
|
344
|
+
// entry. Refactoring needs every other AST token on that line.
|
|
343
345
|
if (definitions.some(d => d.file === filePath &&
|
|
344
|
-
(d.startLine === u.line || d.nameLine === u.line))
|
|
346
|
+
(d.startLine === u.line || d.nameLine === u.line)) &&
|
|
347
|
+
(!options.internalEvidence || u.usageType === 'definition' || u.declarationName)) {
|
|
345
348
|
continue;
|
|
346
349
|
}
|
|
347
350
|
|
|
@@ -375,13 +378,22 @@ function usages(index, name, options = {}) {
|
|
|
375
378
|
// plan gains precision without changing that surface.
|
|
376
379
|
...(options.internalEvidence && {
|
|
377
380
|
...(Number.isInteger(u.column) && { column: u.column }),
|
|
381
|
+
...(u.receiverType && {
|
|
382
|
+
receiverType: u.receiverType,
|
|
383
|
+
...(u.receiverTypeQualifier && { receiverTypeQualifier: u.receiverTypeQualifier }),
|
|
384
|
+
...(Number.isInteger(u.receiverTypeArgs) && { receiverTypeArgs: u.receiverTypeArgs }),
|
|
385
|
+
}),
|
|
378
386
|
...(u.receiverIsModule && { receiverIsModule: true }),
|
|
379
387
|
...(u.receiverLocalBinding && { receiverLocalBinding: true }),
|
|
388
|
+
// A path-qualified reference (`T::m`, fix #422).
|
|
389
|
+
...(u.scopedReference && { scopedReference: true }),
|
|
380
390
|
...(u.namespace && { namespace: u.namespace }),
|
|
381
391
|
...(u.scopeBinding && { scopeBinding: u.scopeBinding }),
|
|
382
392
|
...(u.scopeDefLines && { scopeDefLines: u.scopeDefLines }),
|
|
383
393
|
...(u.shorthandProperty && { shorthandProperty: true }),
|
|
384
394
|
...(u.importAlias && { importAlias: true }),
|
|
395
|
+
...(u.modulePath && { modulePath: true }),
|
|
396
|
+
...(u.importSource && { importSource: true }),
|
|
385
397
|
}),
|
|
386
398
|
...(callerSym && {
|
|
387
399
|
callerName: callerSym.name,
|
|
@@ -587,7 +599,7 @@ function search(index, term, options = {}) {
|
|
|
587
599
|
|
|
588
600
|
// Use AST-based filtering for codeOnly mode when language is supported
|
|
589
601
|
if (options.codeOnly) {
|
|
590
|
-
const language = detectLanguage(filePath);
|
|
602
|
+
const language = fileEntry.language || detectLanguage(filePath, index.root);
|
|
591
603
|
if (language) {
|
|
592
604
|
try {
|
|
593
605
|
const parser = getParser(language);
|
|
@@ -1323,6 +1335,11 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1323
1335
|
} else if (!isFilePath) {
|
|
1324
1336
|
targetDefs = index.find(nameOrFile, { exact: true, className: options.className });
|
|
1325
1337
|
}
|
|
1338
|
+
// A pinned definition among same-name ones (an overload, a constructor
|
|
1339
|
+
// beside its class and siblings, fix #430): calls are this definition's
|
|
1340
|
+
// only when the caller engine attributes them to it.
|
|
1341
|
+
const pinnedFamily = !isFilePath && options.definition && targetDefs.length > 1;
|
|
1342
|
+
if (!isFilePath && options.definition) targetDefs = [options.definition];
|
|
1326
1343
|
|
|
1327
1344
|
// Find all test files
|
|
1328
1345
|
const testFiles = [];
|
|
@@ -1522,6 +1539,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1522
1539
|
// own definition (fix #246).
|
|
1523
1540
|
if (localShadow && !usage.receiver && usage.usageType !== 'import') continue;
|
|
1524
1541
|
|
|
1542
|
+
if (pinnedFamily && usage.usageType === 'call') continue;
|
|
1525
1543
|
// A line is claimed only by a usage that is recorded: two
|
|
1526
1544
|
// same-kind tokens on one line (`a.m = b.m`) are judged
|
|
1527
1545
|
// separately, so a rejected first token never hides a
|
|
@@ -1550,10 +1568,14 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1550
1568
|
// className scoping for references: require class-associated receiver
|
|
1551
1569
|
if (className && (matchType === 'reference' || matchType === 'string-ref' ||
|
|
1552
1570
|
matchType === 'unverified-reference')) {
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
|
|
1556
|
-
|
|
1571
|
+
// A bare name can bind an earlier class-body method.
|
|
1572
|
+
// Other references need a receiver matching the target.
|
|
1573
|
+
if (!usage.receiver && matchType !== 'unverified-reference') {
|
|
1574
|
+
if (!classBodyReferenceTargets(index, testPath, entry.language, usage, targetDefs)) continue;
|
|
1575
|
+
seenLines.add(lineKey);
|
|
1576
|
+
matches.push({ line: usage.line, content: lineContent.trim(), matchType: 'reference' });
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1557
1579
|
if (!usage.receiver && matchType === 'unverified-reference') {
|
|
1558
1580
|
seenLines.add(lineKey);
|
|
1559
1581
|
matches.push({
|
|
@@ -1685,7 +1707,9 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1685
1707
|
if (!isFilePath && targetDefs.length > 0) {
|
|
1686
1708
|
const testInfo = new Map(testFiles.map(t => [t.path, t]));
|
|
1687
1709
|
const callerSites = [];
|
|
1688
|
-
|
|
1710
|
+
// A pinned definition's unverified sites were adjudicated against it
|
|
1711
|
+
// alone, like a globally unique name's (fix #430).
|
|
1712
|
+
const globallyUniqueTarget = !!options.definition || index.find(searchTerm, { exact: true }).length === 1;
|
|
1689
1713
|
for (const def of targetDefs) {
|
|
1690
1714
|
const raw = index.findCallers(searchTerm, {
|
|
1691
1715
|
targetDefinitions: [def],
|
|
@@ -1750,15 +1774,27 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1750
1774
|
// receiver identity the accessor engine proves are test links; the
|
|
1751
1775
|
// unverified ones join only for a globally unique name, as above.
|
|
1752
1776
|
const accessorDefs = targetDefs.filter(isAccessorDefinition);
|
|
1753
|
-
|
|
1777
|
+
// A nested type's annotations and qualifiers are type references,
|
|
1778
|
+
// not instance-member accesses of its containing class. Reuse the
|
|
1779
|
+
// same language lookup as impact instead of requiring a receiver.
|
|
1780
|
+
const nestedTypeDefs = targetDefs.filter(def => def.enclosingType &&
|
|
1781
|
+
['class', 'struct', 'interface', 'type', 'enum', 'trait', 'record', 'union'].includes(def.type));
|
|
1782
|
+
if (accessorDefs.length > 0 || nestedTypeDefs.length > 0) {
|
|
1754
1783
|
const testPaths = new Set(testFiles.map(t => t.path));
|
|
1755
1784
|
const globallyUnique = index.find(searchTerm, { exact: true }).length === 1;
|
|
1756
|
-
for (const def of accessorDefs) {
|
|
1757
|
-
const
|
|
1785
|
+
for (const def of [...accessorDefs, ...nestedTypeDefs]) {
|
|
1786
|
+
const types = nestedTypeDefs.includes(def)
|
|
1787
|
+
? require('./analysis').findTypeReferences(index, searchTerm, def, { files: testPaths }) : null;
|
|
1788
|
+
const typeSite = site => ({ ...site, absoluteFile: path.join(index.root, site.file) });
|
|
1789
|
+
const refs = types ? {
|
|
1790
|
+
confirmed: types.byFile.flatMap(group => group.sites).map(typeSite),
|
|
1791
|
+
unverified: types.unverifiedSites.map(typeSite),
|
|
1792
|
+
} : findAccessorReferences(index, searchTerm, def, { files: testPaths });
|
|
1758
1793
|
if (!refs) continue;
|
|
1759
1794
|
const rows = [
|
|
1760
1795
|
...refs.confirmed.map(r => ({ ...r, tierName: 'confirmed' })),
|
|
1761
|
-
...(globallyUnique
|
|
1796
|
+
...(globallyUnique || types && options.definition
|
|
1797
|
+
? refs.unverified.map(r => ({ ...r, tierName: 'unverified' })) : []),
|
|
1762
1798
|
];
|
|
1763
1799
|
for (const ref of rows) {
|
|
1764
1800
|
const info = testInfo.get(ref.absoluteFile);
|
|
@@ -2092,6 +2128,23 @@ function _buildInstanceTypeMap(index, filePath, content, targetClassName) {
|
|
|
2092
2128
|
return typeMap;
|
|
2093
2129
|
}
|
|
2094
2130
|
|
|
2131
|
+
/** A bare name bound by one preceding declaration in a Python class body. */
|
|
2132
|
+
function classBodyReferenceTargets(index, file, language, usage, targets) {
|
|
2133
|
+
if (!langTraits(language)?.assignmentDeclaresLocal || !targets.length) return false;
|
|
2134
|
+
const tree = index._getParsedTree(file, index._readFile(file), language);
|
|
2135
|
+
if (!tree) return false;
|
|
2136
|
+
const node = tree.rootNode.namedDescendantForPosition({ row: usage.line - 1, column: usage.column });
|
|
2137
|
+
const owner = require('./value-references').pythonClassBodyOf(node);
|
|
2138
|
+
if (!owner) return false;
|
|
2139
|
+
const { pythonBindingSites, pythonClassBodyOrder } = require('../languages/lexical-scope');
|
|
2140
|
+
const bindings = pythonBindingSites(owner, node.text);
|
|
2141
|
+
if (bindings.length !== 1 || pythonClassBodyOrder(owner, node, node.text) !== 'class') return false;
|
|
2142
|
+
const binding = bindings[0];
|
|
2143
|
+
return targets.some(def => def.file === file && def.name === node.text &&
|
|
2144
|
+
def.startLine >= owner.startPosition.row + 1 && def.endLine <= owner.endPosition.row + 1 &&
|
|
2145
|
+
binding.startPosition.row + 1 === (def.nameLine || def.startLine));
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2095
2148
|
/**
|
|
2096
2149
|
* Check if a usage's receiver matches the target className.
|
|
2097
2150
|
* Uses direct receiver check and instance type map for indirect bindings.
|
package/core/shared.js
CHANGED
|
@@ -7,6 +7,11 @@ const path = require('path');
|
|
|
7
7
|
const { isTestFile } = require('./discovery');
|
|
8
8
|
const { detectLanguage } = require('./parser');
|
|
9
9
|
|
|
10
|
+
// Modules read lazily (load cycles), each resolved once: a require() per
|
|
11
|
+
// call in hot paths was measurable (fix #436).
|
|
12
|
+
let lazyLanguagesModuleCache = null;
|
|
13
|
+
const lazyLanguagesModule = () => lazyLanguagesModuleCache || (lazyLanguagesModuleCache = require('../languages'));
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* Security containment check (fix #285): is `candidate` inside `root`?
|
|
12
17
|
*
|
|
@@ -63,7 +68,7 @@ function isTestPath(rp) {
|
|
|
63
68
|
// has a `.spec` form, only C's patterns name `.h`): no header-language
|
|
64
69
|
// detection, which walks the project directory (fix #396).
|
|
65
70
|
const language = path.extname(rp).toLowerCase() === '.h' ? 'c' : detectLanguage(rp);
|
|
66
|
-
const { langTraits } =
|
|
71
|
+
const { langTraits } = lazyLanguagesModule();
|
|
67
72
|
const specConvention = langTraits(language)?.testFileCandidates?.('sample', '')
|
|
68
73
|
.some(candidate => candidate.includes('.spec'));
|
|
69
74
|
return isTestFile(rp, language) ||
|
|
@@ -148,6 +153,22 @@ function escapeRegExp(text) {
|
|
|
148
153
|
|
|
149
154
|
// Symbol types that are not callable (used to filter class/struct/type declarations from call analysis)
|
|
150
155
|
const NON_CALLABLE_TYPES = new Set(['class', 'struct', 'interface', 'type', 'enum', 'trait', 'state', 'impl', 'field']);
|
|
156
|
+
const VALUE_SYMBOL_KINDS = new Set(['field', 'private field', 'constant', 'state', 'variant']);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A member a call can name (fix #423): a callable kind, or a class field the
|
|
160
|
+
* parser read as a function (`reset = () => {..}`, `isMethod`), which
|
|
161
|
+
* `form.reset()` and `this.reset()` call like a method.
|
|
162
|
+
*/
|
|
163
|
+
function isCallableMember(definition) {
|
|
164
|
+
return !!definition && (!NON_CALLABLE_TYPES.has(definition.type) ||
|
|
165
|
+
(definition.type === 'field' && definition.isMethod === true));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isValueDefinition(definition) {
|
|
169
|
+
return !!definition && !definition.isMethod &&
|
|
170
|
+
(VALUE_SYMBOL_KINDS.has(definition.type) || VALUE_SYMBOL_KINDS.has(definition.memberType));
|
|
171
|
+
}
|
|
151
172
|
|
|
152
173
|
/**
|
|
153
174
|
* Every function-shaped symbol kind across the parsers (fix #251 — stats
|
|
@@ -501,7 +522,81 @@ function isMacroNamespaceDefinition(def) {
|
|
|
501
522
|
modifiers.includes('proc_macro_derive');
|
|
502
523
|
}
|
|
503
524
|
|
|
525
|
+
/** The names the inverted call index records, in first-occurrence order. */
|
|
526
|
+
function calleeNames(calls) {
|
|
527
|
+
const names = new Set();
|
|
528
|
+
for (const call of calls || []) {
|
|
529
|
+
names.add(call.name);
|
|
530
|
+
if (call.resolvedName) names.add(call.resolvedName);
|
|
531
|
+
for (const name of call.resolvedNames || []) names.add(name);
|
|
532
|
+
if (call.ownClass) names.add(call.ownClass.name);
|
|
533
|
+
if (call.receiverIsTypeQualified && call.receiver) names.add(call.receiver);
|
|
534
|
+
}
|
|
535
|
+
return names;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Declarations whose line extent holds a line, by symbol list (fix #418:
|
|
539
|
+
// per-site scans of a whole symbol table made construction and namespace
|
|
540
|
+
// lookups quadratic in large files). An index belongs to one list; a list
|
|
541
|
+
// whose length or ends changed gets a new one.
|
|
542
|
+
const SPAN_INDEXES = new WeakMap();
|
|
543
|
+
|
|
544
|
+
function spanIndexOf(symbols) {
|
|
545
|
+
const known = SPAN_INDEXES.get(symbols);
|
|
546
|
+
if (known && known.length === symbols.length && known.first === symbols[0] &&
|
|
547
|
+
known.last === symbols[symbols.length - 1]) return known;
|
|
548
|
+
const regular = [], irregular = [];
|
|
549
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
550
|
+
const symbol = symbols[i];
|
|
551
|
+
if (Number.isFinite(symbol?.startLine) && Number.isFinite(symbol?.endLine)) regular.push(i);
|
|
552
|
+
else irregular.push(i);
|
|
553
|
+
}
|
|
554
|
+
regular.sort((a, b) => symbols[a].startLine - symbols[b].startLine || a - b);
|
|
555
|
+
const starts = regular.map(i => symbols[i].startLine), ends = regular.map(i => symbols[i].endLine);
|
|
556
|
+
const maxEnd = new Array(regular.length);
|
|
557
|
+
const build = (lo, hi) => {
|
|
558
|
+
if (lo >= hi) return -Infinity;
|
|
559
|
+
const mid = (lo + hi) >> 1;
|
|
560
|
+
return (maxEnd[mid] = Math.max(ends[mid], build(lo, mid), build(mid + 1, hi)));
|
|
561
|
+
};
|
|
562
|
+
build(0, regular.length);
|
|
563
|
+
const index = { length: symbols.length, first: symbols[0], last: symbols[symbols.length - 1],
|
|
564
|
+
regular, starts, ends, maxEnd, irregular };
|
|
565
|
+
SPAN_INDEXES.set(symbols, index);
|
|
566
|
+
return index;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The symbols of `symbols` whose extent holds `line` (the comparisons
|
|
571
|
+
* `!(startLine > line) && !(endLine < line)`), in list order.
|
|
572
|
+
*/
|
|
573
|
+
function symbolsSpanning(symbols, line) {
|
|
574
|
+
if (!Array.isArray(symbols) || symbols.length === 0) return [];
|
|
575
|
+
const index = spanIndexOf(symbols);
|
|
576
|
+
const found = [];
|
|
577
|
+
const visit = (lo, hi) => {
|
|
578
|
+
if (lo >= hi) return;
|
|
579
|
+
const mid = (lo + hi) >> 1;
|
|
580
|
+
if (index.maxEnd[mid] < line) return;
|
|
581
|
+
visit(lo, mid);
|
|
582
|
+
if (index.starts[mid] > line) return;
|
|
583
|
+
if (index.ends[mid] >= line) found.push(index.regular[mid]);
|
|
584
|
+
visit(mid + 1, hi);
|
|
585
|
+
};
|
|
586
|
+
if (Number.isFinite(line)) visit(0, index.regular.length);
|
|
587
|
+
else for (let i = 0; i < index.regular.length; i++) {
|
|
588
|
+
if (!(index.starts[i] > line) && !(index.ends[i] < line)) found.push(index.regular[i]);
|
|
589
|
+
}
|
|
590
|
+
for (const i of index.irregular) {
|
|
591
|
+
const symbol = symbols[i];
|
|
592
|
+
if (!(symbol?.startLine > line) && !(symbol?.endLine < line)) found.push(i);
|
|
593
|
+
}
|
|
594
|
+
return found.sort((a, b) => a - b).map(i => symbols[i]);
|
|
595
|
+
}
|
|
596
|
+
|
|
504
597
|
module.exports = { literalNameRegex,
|
|
598
|
+
symbolsSpanning,
|
|
599
|
+
calleeNames,
|
|
505
600
|
pickBestDefinition,
|
|
506
601
|
addTestExclusions,
|
|
507
602
|
escapeRegExp,
|
|
@@ -511,6 +606,8 @@ module.exports = { literalNameRegex,
|
|
|
511
606
|
classDispatchNames,
|
|
512
607
|
maskBlockComments,
|
|
513
608
|
NON_CALLABLE_TYPES,
|
|
609
|
+
isCallableMember,
|
|
610
|
+
isValueDefinition,
|
|
514
611
|
CALLABLE_SYMBOL_KINDS,
|
|
515
612
|
formatSymbolHandle,
|
|
516
613
|
parseSymbolHandle,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
// Bounded memos and source identities shared by the query-time syntax
|
|
7
|
+
// caches (fix #418). A memo keys a value by the exact input that determines
|
|
8
|
+
// it (a source text, a parsed view), never by a binding or ownership verdict.
|
|
9
|
+
|
|
10
|
+
/** A least-recently-used map bounded by entry count and by total weight. */
|
|
11
|
+
class BoundedMap {
|
|
12
|
+
constructor({ maxEntries = Infinity, maxWeight = Infinity, weigh = null } = {}) {
|
|
13
|
+
this.maxEntries = maxEntries;
|
|
14
|
+
this.maxWeight = maxWeight;
|
|
15
|
+
this.weigh = weigh;
|
|
16
|
+
this.entries = new Map();
|
|
17
|
+
this.weight = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get size() { return this.entries.size; }
|
|
21
|
+
|
|
22
|
+
has(key) { return this.entries.has(key); }
|
|
23
|
+
|
|
24
|
+
/** The value, made most recently used; undefined when absent. */
|
|
25
|
+
get(key) {
|
|
26
|
+
const entry = this.entries.get(key);
|
|
27
|
+
if (entry === undefined) return undefined;
|
|
28
|
+
this.entries.delete(key);
|
|
29
|
+
this.entries.set(key, entry);
|
|
30
|
+
return entry.value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Store a value; one heavier than the whole bound is not kept. */
|
|
34
|
+
set(key, value) {
|
|
35
|
+
const weight = this.weigh ? this.weigh(value, key) : 0;
|
|
36
|
+
this.delete(key);
|
|
37
|
+
if (weight > this.maxWeight) return false;
|
|
38
|
+
while (this.entries.size > 0 && (this.entries.size >= this.maxEntries ||
|
|
39
|
+
this.weight + weight > this.maxWeight)) {
|
|
40
|
+
this.delete(this.entries.keys().next().value);
|
|
41
|
+
}
|
|
42
|
+
this.entries.set(key, { value, weight });
|
|
43
|
+
this.weight += weight;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
delete(key) {
|
|
48
|
+
const entry = this.entries.get(key);
|
|
49
|
+
if (entry === undefined) return false;
|
|
50
|
+
this.entries.delete(key);
|
|
51
|
+
this.weight -= entry.weight;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
clear() {
|
|
56
|
+
this.entries.clear();
|
|
57
|
+
this.weight = 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
*[Symbol.iterator]() {
|
|
61
|
+
for (const [key, entry] of this.entries) yield [key, entry.value];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A memo of facts derived from a source text in one language. The text is
|
|
67
|
+
* the key itself, so a lookup never builds a key string as long as the
|
|
68
|
+
* source; its weight counts the retained text.
|
|
69
|
+
*/
|
|
70
|
+
class SourceMemo {
|
|
71
|
+
constructor({ maxEntries = 256, maxWeight = 32 * 1024 * 1024, weigh = () => 0 } = {}) {
|
|
72
|
+
this.memo = new BoundedMap({ maxEntries, maxWeight,
|
|
73
|
+
weigh: (byLanguage, source) => source.length * 2 + [...byLanguage.values()].reduce(
|
|
74
|
+
(sum, value) => sum + weigh(value), 0) });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get(language, source) {
|
|
78
|
+
return this.memo.get(source)?.get(language);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
set(language, source, value) {
|
|
82
|
+
const byLanguage = new Map(this.memo.get(source) || []);
|
|
83
|
+
byLanguage.set(language, value);
|
|
84
|
+
this.memo.set(source, byLanguage);
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parsed trees of recently read sources, by grammar. A tree is immutable
|
|
90
|
+
// and collected by the runtime; a reader never edits one. Readers that ask
|
|
91
|
+
// for whole-file syntax at many sites of the same few files share a parse
|
|
92
|
+
// (fix #418: an overload check parsed both of its files for every argument).
|
|
93
|
+
const parsedSources = new SourceMemo({ maxEntries: 32, maxWeight: 64 * 1024 * 1024 });
|
|
94
|
+
|
|
95
|
+
/** The tree of `source` in the grammar `language`, or null when it cannot parse. */
|
|
96
|
+
function parsedSource(language, source) {
|
|
97
|
+
if (typeof source !== 'string') return null;
|
|
98
|
+
let tree = parsedSources.get(language, source);
|
|
99
|
+
if (tree === undefined) {
|
|
100
|
+
const { getParser, safeParse } = require('../languages');
|
|
101
|
+
tree = safeParse(getParser(language), source) || null;
|
|
102
|
+
parsedSources.set(language, source, tree);
|
|
103
|
+
}
|
|
104
|
+
return tree;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// One stat per file per operation (fix #365), for every reader of the
|
|
108
|
+
// operation (fix #418: the call-name cache stated a file per query).
|
|
109
|
+
const OPERATION_STATS = new WeakMap(); // operation content cache -> Map<file, mtimeMs>
|
|
110
|
+
|
|
111
|
+
/** The file's mtime, read once per operation; throws as statSync does. */
|
|
112
|
+
function fileMtime(index, file) {
|
|
113
|
+
const scope = index._opContentCache;
|
|
114
|
+
let memo = scope ? OPERATION_STATS.get(scope) : null;
|
|
115
|
+
if (scope && !memo) OPERATION_STATS.set(scope, memo = new Map());
|
|
116
|
+
let mtime = memo?.get(file);
|
|
117
|
+
if (mtime === undefined) {
|
|
118
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
119
|
+
memo?.set(file, mtime);
|
|
120
|
+
}
|
|
121
|
+
return mtime;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The SHA-256 of the JSON of `parts`: a view or option identity. */
|
|
125
|
+
function digestOf(parts) {
|
|
126
|
+
return crypto.createHash('sha256').update(JSON.stringify(parts)).digest('hex');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Does `content` equal the source the index read for `file`? One hash per
|
|
131
|
+
* file and content in an operation (every syntax cache of an operation
|
|
132
|
+
* asks it of the same files).
|
|
133
|
+
*/
|
|
134
|
+
function indexedContent(index, file, entry, content) {
|
|
135
|
+
if (!entry?.hash || typeof content !== 'string') return false;
|
|
136
|
+
const memo = !index.isProvenanceFactIndex && typeof index._opMemo === 'function'
|
|
137
|
+
? index._opMemo('indexedContent', () => new Map()) : null;
|
|
138
|
+
const saved = memo?.get(file);
|
|
139
|
+
if (saved && saved.content === content && saved.hash === entry.hash) return saved.fresh;
|
|
140
|
+
const fresh = crypto.createHash('md5').update(content).digest('hex') === entry.hash;
|
|
141
|
+
memo?.set(file, { content, hash: entry.hash, fresh });
|
|
142
|
+
return fresh;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Are the type names written in `file` usable as the index read them (trait
|
|
147
|
+
* `typeNameSourceCanonical` of `language`)? The index's own parse of the
|
|
148
|
+
* same content tells whether it had errors, so most sources need no second
|
|
149
|
+
* parse (fix #418). `lines` (`{ startLine, endLine }`) asks about the names
|
|
150
|
+
* of that line range only: a parse error confined to another member does not
|
|
151
|
+
* reach them (fix #421).
|
|
152
|
+
*/
|
|
153
|
+
function typeNamesCanonical(index, file, canonical, language, lines = null) {
|
|
154
|
+
const source = index._readFile(file);
|
|
155
|
+
// The verdict is the same with or without the hint, so a witness view
|
|
156
|
+
// takes it unrecorded from its live index; its replay derives it again.
|
|
157
|
+
const hint = live => {
|
|
158
|
+
const entry = live.files.get(file);
|
|
159
|
+
return entry?.language === language && indexedContent(live, file, entry, source)
|
|
160
|
+
? !!entry.parseRecovery : undefined;
|
|
161
|
+
};
|
|
162
|
+
return canonical(source, () => index.isProvenanceFactIndex ? index.unrecordedHint(hint) : hint(index), lines);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The parsed views of a file: grammar, configured views, recovery blanks and external macro scope. */
|
|
166
|
+
function parseViewParts(entry, language = entry.language) {
|
|
167
|
+
return [language, entry.conditionalViews, entry.recoveryBlanks, entry.externalMacroKey];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Persisted source-only syntax caches (fix #415): each family's mode
|
|
172
|
+
* prefix and the weight of one stored value in the usage-result LRU.
|
|
173
|
+
* A producer names its mode through `syntaxMode`, so a version bump in a
|
|
174
|
+
* prefix keeps its weight.
|
|
175
|
+
*/
|
|
176
|
+
const SYNTAX_FAMILIES = Object.freeze({
|
|
177
|
+
callToken: { prefix: 'call-token-v1', weight: 'rows' },
|
|
178
|
+
declarationToken: { prefix: 'declaration-token-v1', weight: 'rows' },
|
|
179
|
+
contractSyntax: { prefix: 'contract-syntax-v1', weight: 'json' },
|
|
180
|
+
usageText: { prefix: 'usage-text-v2', weight: 'json' },
|
|
181
|
+
enumSyntax: { prefix: 'enum-syntax-v2', weight: 'json' },
|
|
182
|
+
valueLocal: { prefix: 'value-local-v3', weight: 'json' },
|
|
183
|
+
valueBodies: { prefix: 'value-bodies-v4', weight: 'json' },
|
|
184
|
+
classFields: { prefix: 'class-fields-v1', weight: 'json' },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/** A family's mode: its prefix, an optional qualifier and the digest of `parts`. */
|
|
188
|
+
function syntaxMode(family, parts, qualifier = null) {
|
|
189
|
+
return `${SYNTAX_FAMILIES[family].prefix}:${qualifier === null ? '' : `${qualifier}:`}${digestOf(parts)}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function syntaxWeightKind(mode) {
|
|
193
|
+
const prefix = String(mode).split(':', 1)[0];
|
|
194
|
+
for (const family of Object.values(SYNTAX_FAMILIES)) {
|
|
195
|
+
if (family.prefix === prefix) return family.weight;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { BoundedMap, SourceMemo, parsedSource, fileMtime, digestOf, indexedContent, typeNamesCanonical, parseViewParts,
|
|
201
|
+
SYNTAX_FAMILIES, syntaxMode, syntaxWeightKind };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const cache = require('./cache');
|
|
4
|
+
const { indexedContent, parseViewParts, syntaxMode } = require('./source-memo');
|
|
5
|
+
|
|
6
|
+
/** Source positions only; no binding or ownership decisions are retained. */
|
|
7
|
+
function textUsageSyntax(index, file, entry, content, name) {
|
|
8
|
+
const eligible = entry.hash && index._usageResultCache && !index.isProvenanceFactIndex &&
|
|
9
|
+
indexedContent(index, file, entry, content);
|
|
10
|
+
const mode = eligible ? syntaxMode('usageText', parseViewParts(entry)) : null;
|
|
11
|
+
const stored = eligible ? cache.getUsageResult(index, file, name, mode) : null;
|
|
12
|
+
const facts = new Map();
|
|
13
|
+
if (Array.isArray(stored) && stored.every(fact => Array.isArray(fact) && fact.length === 3 &&
|
|
14
|
+
Number.isInteger(fact[0]) && fact[0] > 0 && Number.isInteger(fact[1]) && fact[1] >= 0 &&
|
|
15
|
+
['identifier', 'comment-or-string', 'markup-or-text'].includes(fact[2]))) {
|
|
16
|
+
for (const fact of stored) facts.set(fact[0], fact);
|
|
17
|
+
}
|
|
18
|
+
let dirty = false;
|
|
19
|
+
return {
|
|
20
|
+
kind(line, column) {
|
|
21
|
+
const saved = facts.get(line);
|
|
22
|
+
if (saved?.[1] === column) return saved[2];
|
|
23
|
+
const kind = index.isCommentOrStringAtPosition(content, line, column, file)
|
|
24
|
+
? 'comment-or-string'
|
|
25
|
+
: index.isIdentifierAtPosition(content, line, column, file, name)
|
|
26
|
+
? 'identifier' : 'markup-or-text';
|
|
27
|
+
facts.set(line, [line, column, kind]);
|
|
28
|
+
dirty = true;
|
|
29
|
+
return kind;
|
|
30
|
+
},
|
|
31
|
+
save() {
|
|
32
|
+
if (eligible && dirty) cache.storeUsageResult(index, file, name, mode,
|
|
33
|
+
[...facts.values()].sort((a, b) => a[0] - b[0]));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { textUsageSyntax };
|
package/core/tracing.js
CHANGED
|
@@ -231,6 +231,11 @@ function trace(index, name, options = {}) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
// A construction whose constructor body is undecided (fix #421):
|
|
235
|
+
// its class edge is a child, the review a leaf beside it.
|
|
236
|
+
if (callees.initializerReviews && callees.initializerReviews.length > 0) {
|
|
237
|
+
node.initializerReviews = callees.initializerReviews;
|
|
238
|
+
}
|
|
234
239
|
for (const callee of callees.slice(0, maxChildren)) {
|
|
235
240
|
// callee already has the best-matched definition from findCallees
|
|
236
241
|
const childTree = buildTree(callee, currentDepth + 1, 'down');
|
|
@@ -238,7 +243,8 @@ function trace(index, name, options = {}) {
|
|
|
238
243
|
node.children.push({
|
|
239
244
|
...childTree,
|
|
240
245
|
callCount: callee.callCount,
|
|
241
|
-
weight: callee.weight
|
|
246
|
+
weight: callee.weight,
|
|
247
|
+
...(callee.initializerOf && { constructs: callee.initializerOf }),
|
|
242
248
|
});
|
|
243
249
|
}
|
|
244
250
|
}
|