ucn 5.4.1 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +42 -16
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis-ext.js +6 -6
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +11 -2
- package/core/output/lines.js +8 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +316 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +25 -11
- package/package.json +1 -1
package/core/execute.js
CHANGED
|
@@ -17,6 +17,7 @@ const { cleanHtmlScriptTags, detectLanguage } = require('./parser');
|
|
|
17
17
|
const { renderExpandItem } = require('./expand-cache');
|
|
18
18
|
const { CANONICAL_COMMANDS } = require('./registry');
|
|
19
19
|
const { isAccessorDefinition } = require('./accessors');
|
|
20
|
+
const { describeError } = require('./errors');
|
|
20
21
|
|
|
21
22
|
// ============================================================================
|
|
22
23
|
// HELPERS
|
|
@@ -479,9 +480,14 @@ function validatePublicParams(command, p) {
|
|
|
479
480
|
}
|
|
480
481
|
const hasName = typeof p.name === 'string' && p.name.trim() !== '';
|
|
481
482
|
const gitScope = p.staged || (typeof p.base === 'string' && p.base.trim() !== '');
|
|
482
|
-
if (
|
|
483
|
+
if (command === 'impact' && hasName && gitScope) {
|
|
483
484
|
return `${command} accepts either a symbol target or Git diff scope (base/staged), not both.`;
|
|
484
485
|
}
|
|
486
|
+
// `check <symbol>` compares the declaration with its version at base;
|
|
487
|
+
// staged selects the target-less diff check.
|
|
488
|
+
if (command === 'check' && hasName && p.staged) {
|
|
489
|
+
return 'check accepts staged=true only without a symbol target; a symbol target compares with base.';
|
|
490
|
+
}
|
|
485
491
|
if ((command === 'impact' || command === 'check') && p.staged && p.base) {
|
|
486
492
|
return `${command} accepts either staged=true or base, not both.`;
|
|
487
493
|
}
|
|
@@ -1129,7 +1135,7 @@ const HANDLERS = {
|
|
|
1129
1135
|
addMode(result, 'diff');
|
|
1130
1136
|
return { ok: true, result };
|
|
1131
1137
|
} catch (e) {
|
|
1132
|
-
return { ok: false,
|
|
1138
|
+
return { ok: false, ...describeError(e) };
|
|
1133
1139
|
}
|
|
1134
1140
|
},
|
|
1135
1141
|
|
|
@@ -1280,17 +1286,18 @@ const HANDLERS = {
|
|
|
1280
1286
|
applyClassMethodSyntax(p);
|
|
1281
1287
|
const inErr = validateInFilter(index, p.in);
|
|
1282
1288
|
if (inErr) return { ok: false, error: inErr };
|
|
1283
|
-
|
|
1289
|
+
// The literal-name inventory lists every line the ACCOUNT counts,
|
|
1290
|
+
// test files included (fix #394); --exclude-tests hides them with a
|
|
1291
|
+
// disclosed count.
|
|
1292
|
+
const exclude = applyTestExclusions(p.exclude, !p.excludeTests);
|
|
1284
1293
|
const fileErr = checkFilePatternMatch(index, p.file);
|
|
1285
1294
|
if (fileErr) return { ok: false, error: fileErr };
|
|
1286
1295
|
if (p.className) {
|
|
1287
1296
|
const classErr = validateClassName(index, p.name, p.className);
|
|
1288
1297
|
if (classErr) return { ok: false, error: classErr };
|
|
1289
1298
|
}
|
|
1290
|
-
// Scan once WITHOUT the
|
|
1291
|
-
//
|
|
1292
|
-
// G2-java: usages silently hid test-file usages while search noted
|
|
1293
|
-
// them — a silently incomplete answer from the raw escape hatch).
|
|
1299
|
+
// Scan once WITHOUT the test exclusion, then filter in the handler —
|
|
1300
|
+
// the hidden count must be VISIBLE (fix #234).
|
|
1294
1301
|
// Normalize the user's exclude first (fix #239): MCP delivers a CSV
|
|
1295
1302
|
// STRING, and matchesFilters iterates its CHARACTERS — exclude=test
|
|
1296
1303
|
// emptied every TypeScript project ('t' matched the .ts extension).
|
|
@@ -1312,7 +1319,7 @@ const HANDLERS = {
|
|
|
1312
1319
|
if (exclude.length !== userExclude.length && Array.isArray(unfiltered)) {
|
|
1313
1320
|
result = unfiltered.filter(u => index.matchesFilters(u.relativePath, { exclude }));
|
|
1314
1321
|
const hidden = unfiltered.length - result.length;
|
|
1315
|
-
if (hidden > 0) notes.push(`${hidden} test-file usage(s) hidden by
|
|
1322
|
+
if (hidden > 0) notes.push(`${hidden} test-file usage(s) hidden by --exclude-tests; omit it to list them.`);
|
|
1316
1323
|
}
|
|
1317
1324
|
// Apply limit to total usages (result is a flat array). usages is the
|
|
1318
1325
|
// escape-hatch listing (fix #284): only an EXPLICIT limit caps it; the
|
|
@@ -2243,6 +2250,9 @@ const HANDLERS = {
|
|
|
2243
2250
|
// --no-include-methods.
|
|
2244
2251
|
...(p.includeMethods !== undefined && { includeMethods: p.includeMethods }),
|
|
2245
2252
|
...(p.includeUncertain !== undefined && { includeUncertain: p.includeUncertain }),
|
|
2253
|
+
// The revision whose declaration the call sites were written
|
|
2254
|
+
// against (default HEAD).
|
|
2255
|
+
...(typeof p.base === 'string' && p.base.trim() && { base: p.base.trim(), checkBaseRef: true }),
|
|
2246
2256
|
});
|
|
2247
2257
|
if (result && result.found === false) {
|
|
2248
2258
|
return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
@@ -2551,7 +2561,9 @@ function execute(index, command, params = {}) {
|
|
|
2551
2561
|
}
|
|
2552
2562
|
return response;
|
|
2553
2563
|
} catch (e) {
|
|
2554
|
-
|
|
2564
|
+
// A deliberate refusal keeps its message; an exception inside the
|
|
2565
|
+
// engine is marked internal on every surface (fix #394).
|
|
2566
|
+
return { ok: false, ...describeError(e) };
|
|
2555
2567
|
}
|
|
2556
2568
|
}
|
|
2557
2569
|
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* core/external-macros.js - C/C++ files read with the macro definitions of
|
|
5
|
+
* the project (fix #396).
|
|
6
|
+
*
|
|
7
|
+
* A file is parsed on its own, so a decoration or statement macro defined in
|
|
8
|
+
* another header (`SPDLOG_INLINE`, `JSON_INLINE_VARIABLE`, `LIB_TRY {`,
|
|
9
|
+
* `ATTR(3)` before a member) is an unknown name to the recovery, and the
|
|
10
|
+
* grammar's misreading can swallow a class body. The build reads the
|
|
11
|
+
* `#define` directives of every C/C++ header of the project (a translation
|
|
12
|
+
* unit's own definitions reach no other file; a parallel build reads them
|
|
13
|
+
* while its workers parse, and a worker waits for the result only when a
|
|
14
|
+
* file's recovery consults it) into a
|
|
15
|
+
* dictionary of the names whose every definition reads alike: object-like
|
|
16
|
+
* macros that expand to declaration specifiers (or nothing) or to a
|
|
17
|
+
* removable statement fragment (`try`, a complete `catch` handler), and
|
|
18
|
+
* function-like macros whose expansion reads as specifiers or a type. The
|
|
19
|
+
* recovery of a file the grammar could not read consults it for names the
|
|
20
|
+
* file does not define, exactly as it reads the file's own definitions, and
|
|
21
|
+
* records the names it used (`externalMacroNames`).
|
|
22
|
+
*
|
|
23
|
+
* After the include graph is built, a file whose recovery used a name that no
|
|
24
|
+
* file of its #include closure defines is read again without it (the
|
|
25
|
+
* preprocessor never saw that definition), and every file records the
|
|
26
|
+
* dictionary entries of the names its recovery used or could have used
|
|
27
|
+
* (`externalMacroKey`), so a later build re-reads it when they change.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const crypto = require('crypto');
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { langTraits, getParser, detectLanguage } = require('../languages');
|
|
34
|
+
const { codeUnitCompare } = require('./shared');
|
|
35
|
+
const { lexPP } = require('../languages/c-preprocessor');
|
|
36
|
+
|
|
37
|
+
// Translation-unit sources: what they define reaches no other file.
|
|
38
|
+
const TRANSLATION_UNIT_EXTENSIONS = new Set(['.c', '.cc', '.cpp', '.cxx', '.c++', '.cp', '.m', '.mm']);
|
|
39
|
+
|
|
40
|
+
// Object-like replacement lists longer than this are declarations or
|
|
41
|
+
// statement blocks, never a decoration (the #362 bound).
|
|
42
|
+
const MAX_OBJECT_BODY_CHARS = 256;
|
|
43
|
+
|
|
44
|
+
/** Object-like `#define NAME body` directives of a file, continuations joined. */
|
|
45
|
+
function objectDefinitions(code) {
|
|
46
|
+
const out = [];
|
|
47
|
+
const directive = /^[ \t]*#[ \t]*define[ \t]+([A-Za-z_][A-Za-z0-9_]*)(?![A-Za-z0-9_(])/gm;
|
|
48
|
+
for (let match = directive.exec(code); match; match = directive.exec(code)) {
|
|
49
|
+
let end = match.index + match[0].length;
|
|
50
|
+
let body = '';
|
|
51
|
+
for (;;) {
|
|
52
|
+
const lineEnd = code.indexOf('\n', end);
|
|
53
|
+
const line = code.slice(end, lineEnd < 0 ? code.length : lineEnd);
|
|
54
|
+
const continued = /\\[ \t\r]*$/.test(line);
|
|
55
|
+
body += ` ${continued ? line.replace(/\\[ \t\r]*$/, '') : line}`;
|
|
56
|
+
if (!continued || lineEnd < 0) break;
|
|
57
|
+
end = lineEnd + 1;
|
|
58
|
+
}
|
|
59
|
+
body = body.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, ' ').replace(/\s+/g, ' ').trim();
|
|
60
|
+
out.push({ name: match[1], body });
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A replacement list with each parameter replaced by a literal, pastes
|
|
66
|
+
// joined and stringizing made a string: what an invocation could expand to.
|
|
67
|
+
function substituteParams(template) {
|
|
68
|
+
const params = new Set([...(template.params || []), '__VA_ARGS__']);
|
|
69
|
+
const tokens = String(template.body || '').split(' ').filter(Boolean);
|
|
70
|
+
const out = [];
|
|
71
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
72
|
+
const token = tokens[i];
|
|
73
|
+
if (token === '#' && params.has(tokens[i + 1])) {
|
|
74
|
+
out.push('"0"');
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (token === '##') continue;
|
|
79
|
+
out.push(params.has(token) ? '0' : token);
|
|
80
|
+
}
|
|
81
|
+
return out.join(' ');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A replacement list that could read as declaration specifiers, a type, or
|
|
85
|
+
// a statement keyword: names, scopes, templates, attribute and declspec
|
|
86
|
+
// parentheses, pointers and references, but no statement or expression
|
|
87
|
+
// punctuation. Most macro bodies (numbers, strings, expressions, statement
|
|
88
|
+
// blocks) are set aside here without a probe parse (fix #396).
|
|
89
|
+
const NON_SPECIFIER_PUNCTUATION = new Set([';', '{', '}', '=', '?', '+', '-', '/', '%', '!', '|', '^', '~',
|
|
90
|
+
'.', '->', '++', '--', '+=', '-=', '==', '!=', '&&', '||', '<<', '>>', '<=', '>=', '#', '##']);
|
|
91
|
+
|
|
92
|
+
function maySpecify(body) {
|
|
93
|
+
const tokens = lexPP(body);
|
|
94
|
+
if (tokens.length === 0) return true;
|
|
95
|
+
const first = tokens[0];
|
|
96
|
+
if (first.k !== 'id' && first.v !== '::' && first.v !== '[') return false;
|
|
97
|
+
return !tokens.some(token => token.k === 'punct' && NON_SPECIFIER_PUNCTUATION.has(token.v));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A replacement list that begins by invoking another project function-like
|
|
101
|
+
// macro (`#define CHECK(x) DOCTEST_CHECK(x)`) reads as whatever that macro
|
|
102
|
+
// expands to, which a probe of the unexpanded text cannot show: no reading,
|
|
103
|
+
// and no probe parse. Keywords and reserved identifiers (`__attribute__`,
|
|
104
|
+
// `_Alignas`, `decltype`) a project may redefine for another compiler keep
|
|
105
|
+
// their probe.
|
|
106
|
+
const KEYWORD_INTRODUCERS = new Set(['alignas', 'alignof', 'asm', 'decltype', 'noexcept', 'requires',
|
|
107
|
+
'sizeof', 'static_assert', 'typeof']);
|
|
108
|
+
function leadsWithProjectMacro(body, perName) {
|
|
109
|
+
const [first, second] = lexPP(body);
|
|
110
|
+
if (!first || first.k !== 'id' || second?.v !== '(') return false;
|
|
111
|
+
if (/^_[_A-Z]/.test(first.v) || KEYWORD_INTRODUCERS.has(first.v)) return false;
|
|
112
|
+
return (perName.get(first.v)?.functions.size || 0) > 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// `catch (...) { }` / `try`: statement fragments are read by the probe
|
|
116
|
+
// even though they hold braces.
|
|
117
|
+
function mayBeStatementFragment(body) {
|
|
118
|
+
return /^(?:try|catch|if)\b/.test(body.trim());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The project's macro dictionary per language, or null: { key, byLanguage:
|
|
123
|
+
* { c|cpp: { objectBodies: [[name, bodies]], functionMacros: [[name,
|
|
124
|
+
* templates]] } } }. `files` are the files the build indexes.
|
|
125
|
+
*/
|
|
126
|
+
function projectMacroDictionary(index, files) {
|
|
127
|
+
const { directiveFunctionMacros } = require('../languages/c-macro-invocations');
|
|
128
|
+
const { objectMacroReading } = require('../languages/c-family');
|
|
129
|
+
const perName = new Map(); // name -> { objects: Set<body>, functions: Map<key, template> }
|
|
130
|
+
let any = false;
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
// Headers only: a source file's definitions are its own.
|
|
133
|
+
if (TRANSLATION_UNIT_EXTENSIONS.has(path.extname(file).toLowerCase())) continue;
|
|
134
|
+
const language = detectLanguage(file, index.root);
|
|
135
|
+
if (!langTraits(language)?.textualIncludes) continue;
|
|
136
|
+
let code;
|
|
137
|
+
try {
|
|
138
|
+
code = fs.readFileSync(file, 'utf-8');
|
|
139
|
+
} catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!code.includes('define')) continue;
|
|
143
|
+
for (const { name, body } of objectDefinitions(code)) {
|
|
144
|
+
if (!perName.has(name)) perName.set(name, { objects: new Set(), functions: new Map() });
|
|
145
|
+
perName.get(name).objects.add(body.length > MAX_OBJECT_BODY_CHARS ? null : body);
|
|
146
|
+
any = true;
|
|
147
|
+
}
|
|
148
|
+
const macros = directiveFunctionMacros(code);
|
|
149
|
+
for (const [name, defs] of macros?.defs || []) {
|
|
150
|
+
if (!perName.has(name)) perName.set(name, { objects: new Set(), functions: new Map() });
|
|
151
|
+
const slot = perName.get(name);
|
|
152
|
+
for (const def of defs) {
|
|
153
|
+
const template = { params: def.params, variadic: !!def.variadic, body: def.body };
|
|
154
|
+
slot.functions.set(JSON.stringify(template), template);
|
|
155
|
+
}
|
|
156
|
+
any = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!any) return null;
|
|
160
|
+
// One reading per replacement list, by the C++ grammar: the specifier,
|
|
161
|
+
// type and statement forms read here are the same in C.
|
|
162
|
+
const parser = getParser('cpp') || getParser('c');
|
|
163
|
+
if (!parser) return null;
|
|
164
|
+
const readingMemo = new Map();
|
|
165
|
+
const read = (key, compute) => {
|
|
166
|
+
if (!readingMemo.has(key)) readingMemo.set(key, compute());
|
|
167
|
+
return readingMemo.get(key);
|
|
168
|
+
};
|
|
169
|
+
const objectBodies = [];
|
|
170
|
+
const functionMacros = [];
|
|
171
|
+
for (const name of [...perName.keys()].sort(codeUnitCompare)) {
|
|
172
|
+
const slot = perName.get(name);
|
|
173
|
+
// Every definition of the name is one kind; object bodies are short.
|
|
174
|
+
if (slot.objects.size > 0 && slot.functions.size > 0) continue;
|
|
175
|
+
if (slot.objects.size > 0) {
|
|
176
|
+
if (slot.objects.has(null)) continue;
|
|
177
|
+
const bodies = [...slot.objects].sort(codeUnitCompare);
|
|
178
|
+
if (!bodies.every(body => !body || maySpecify(body) || mayBeStatementFragment(body))) continue;
|
|
179
|
+
if (bodies.some(body => leadsWithProjectMacro(body, perName))) continue;
|
|
180
|
+
const reading = read(`o\0${bodies.join('\u0001')}`, () => objectMacroReading(parser, bodies));
|
|
181
|
+
if (reading === 'decoration' || reading === 'statement' || reading === 'if') objectBodies.push([name, bodies]);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const templates = [...slot.functions.values()]
|
|
185
|
+
.sort((a, b) => codeUnitCompare(JSON.stringify(a), JSON.stringify(b)));
|
|
186
|
+
if (new Set(templates.map(template => template.params.length)).size !== 1) continue;
|
|
187
|
+
// Only an attribute or return-type macro (its expansion reads as
|
|
188
|
+
// specifiers or a type) can explain a misread declaration; statement
|
|
189
|
+
// macros such as test assertions read as calls.
|
|
190
|
+
const substituted = templates.map(substituteParams);
|
|
191
|
+
if (!substituted.every(body => !body || maySpecify(body))) continue;
|
|
192
|
+
if (substituted.some(body => leadsWithProjectMacro(body, perName))) continue;
|
|
193
|
+
const reading = read(`f\0${substituted.join('\u0001')}`, () => {
|
|
194
|
+
const readings = new Set(substituted.map(body => objectMacroReading(parser, [body])));
|
|
195
|
+
return readings.size === 1 ? [...readings][0] : null;
|
|
196
|
+
});
|
|
197
|
+
if (reading === 'decoration' || reading === 'type') functionMacros.push([name, templates]);
|
|
198
|
+
}
|
|
199
|
+
const byLanguage = {};
|
|
200
|
+
if (objectBodies.length > 0 || functionMacros.length > 0) {
|
|
201
|
+
const entries = { objectBodies, functionMacros };
|
|
202
|
+
byLanguage.c = entries;
|
|
203
|
+
byLanguage.cpp = entries;
|
|
204
|
+
}
|
|
205
|
+
if (Object.keys(byLanguage).length === 0) return null;
|
|
206
|
+
const key = crypto.createHash('sha1').update(JSON.stringify(byLanguage)).digest('base64url');
|
|
207
|
+
return { key, byLanguage };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The parse context of one file: the dictionary of its language, less the
|
|
212
|
+
* `excluded` names. Consulted names are collected by the recovery.
|
|
213
|
+
*/
|
|
214
|
+
const CONTEXT_MAPS = new WeakMap(); // dictionary -> language -> { objectBodies, functionMacros }
|
|
215
|
+
|
|
216
|
+
function contextFor(dictionary, language, excluded = null) {
|
|
217
|
+
const entry = dictionary?.byLanguage?.[language];
|
|
218
|
+
if (!entry) return null;
|
|
219
|
+
if (excluded && excluded.size > 0) {
|
|
220
|
+
const keep = ([name]) => !excluded.has(name);
|
|
221
|
+
return {
|
|
222
|
+
key: `${dictionary.key}-${[...excluded].sort(codeUnitCompare).join(',')}`,
|
|
223
|
+
objectBodies: new Map(entry.objectBodies.filter(keep)),
|
|
224
|
+
functionMacros: new Map(entry.functionMacros.filter(keep)),
|
|
225
|
+
consulted: new Set(),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// The maps are shared by every file of one build (read-only).
|
|
229
|
+
let byLanguage = CONTEXT_MAPS.get(dictionary);
|
|
230
|
+
if (!byLanguage) {
|
|
231
|
+
byLanguage = new Map();
|
|
232
|
+
CONTEXT_MAPS.set(dictionary, byLanguage);
|
|
233
|
+
}
|
|
234
|
+
let maps = byLanguage.get(language);
|
|
235
|
+
if (!maps) {
|
|
236
|
+
maps = { objectBodies: new Map(entry.objectBodies), functionMacros: new Map(entry.functionMacros) };
|
|
237
|
+
byLanguage.set(language, maps);
|
|
238
|
+
}
|
|
239
|
+
return { key: dictionary.key, ...maps, consulted: new Set() };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The dictionary entries of `names` for one language, as a stable key. */
|
|
243
|
+
function keyOfNames(dictionary, language, names) {
|
|
244
|
+
if (!names || names.length === 0) return null;
|
|
245
|
+
const entry = dictionary?.byLanguage?.[language];
|
|
246
|
+
const objects = new Map(entry?.objectBodies || []);
|
|
247
|
+
const functions = new Map(entry?.functionMacros || []);
|
|
248
|
+
const parts = [...new Set(names)].sort(codeUnitCompare)
|
|
249
|
+
.map(name => [name, objects.get(name) || null, functions.get(name) || null]);
|
|
250
|
+
return crypto.createHash('sha1').update(JSON.stringify(parts)).digest('base64url');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* After the include graph: a file whose recovery used a definition no file
|
|
255
|
+
* of its include closure (or the file itself) holds is re-read without it;
|
|
256
|
+
* a file whose recorded dictionary entries changed is re-read. Every C/C++
|
|
257
|
+
* file with a recovery record gets its key. Returns { files, names } of the
|
|
258
|
+
* re-indexed files, or null.
|
|
259
|
+
*/
|
|
260
|
+
function verifyExternalMacros(index, dictionary, { reindex }) {
|
|
261
|
+
const { _textualIncludeClosures } = require('./callers');
|
|
262
|
+
let definers = null; // name -> Set of files defining it
|
|
263
|
+
const definersOf = name => {
|
|
264
|
+
if (!definers) {
|
|
265
|
+
definers = new Map();
|
|
266
|
+
for (const defs of index.symbols.values()) {
|
|
267
|
+
for (const symbol of defs) {
|
|
268
|
+
if (symbol.type !== 'macro' || !symbol.file) continue;
|
|
269
|
+
if (!definers.has(symbol.name)) definers.set(symbol.name, new Set());
|
|
270
|
+
definers.get(symbol.name).add(symbol.file);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return definers.get(name) || new Set();
|
|
275
|
+
};
|
|
276
|
+
const files = new Set();
|
|
277
|
+
const names = new Set();
|
|
278
|
+
const candidates = [...index.files.keys()].sort(codeUnitCompare);
|
|
279
|
+
for (const file of candidates) {
|
|
280
|
+
const entry = index.files.get(file);
|
|
281
|
+
if (!langTraits(entry?.language)?.textualIncludes) continue;
|
|
282
|
+
const used = entry.externalMacroNames || [];
|
|
283
|
+
const watched = [...used, ...(entry.recoveryCandidates || [])];
|
|
284
|
+
if (watched.length === 0) {
|
|
285
|
+
if (entry.externalMacroKey) delete entry.externalMacroKey;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
// Names the recovery used that the file's translation unit never
|
|
289
|
+
// sees defined.
|
|
290
|
+
let unseen = null;
|
|
291
|
+
if (used.length > 0) {
|
|
292
|
+
const closure = _textualIncludeClosures(index, file).all;
|
|
293
|
+
for (const name of used) {
|
|
294
|
+
const where = definersOf(name);
|
|
295
|
+
if ([...where].some(definer => definer === file || closure.has(definer))) continue;
|
|
296
|
+
(unseen ||= new Set()).add(name);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const excluded = new Set([...(entry.externalMacroExcluded || []), ...(unseen || [])]);
|
|
300
|
+
const key = keyOfNames(dictionary, entry.language, watched.filter(name => !excluded.has(name)));
|
|
301
|
+
const stale = entry.externalMacroKey !== undefined && entry.externalMacroKey !== key;
|
|
302
|
+
if (!unseen && !stale) {
|
|
303
|
+
entry.externalMacroKey = key;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
for (const symbol of entry.symbols || []) names.add(symbol.name);
|
|
307
|
+
reindex(file, contextFor(dictionary, entry.language, excluded));
|
|
308
|
+
const next = index.files.get(file);
|
|
309
|
+
if (next) {
|
|
310
|
+
if (excluded.size > 0) next.externalMacroExcluded = [...excluded].sort(codeUnitCompare);
|
|
311
|
+
next.externalMacroKey = keyOfNames(dictionary, next.language,
|
|
312
|
+
[...(next.externalMacroNames || []), ...(next.recoveryCandidates || [])]
|
|
313
|
+
.filter(name => !excluded.has(name)));
|
|
314
|
+
for (const symbol of next.symbols || []) names.add(symbol.name);
|
|
315
|
+
}
|
|
316
|
+
files.add(file);
|
|
317
|
+
}
|
|
318
|
+
return files.size > 0 ? { files, names } : null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Serializable form for build workers (Maps as entry arrays). */
|
|
322
|
+
function serializeDictionary(dictionary) {
|
|
323
|
+
return dictionary ? { key: dictionary.key, byLanguage: dictionary.byLanguage } : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = { projectMacroDictionary, contextFor, verifyExternalMacros, serializeDictionary, keyOfNames };
|