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.
Files changed (87) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +18 -12
  6. package/cli/index.js +42 -16
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +522 -31
  16. package/core/callers.js +12053 -1047
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis-ext.js +6 -6
  38. package/core/output/analysis.js +18 -2
  39. package/core/output/brief.js +3 -0
  40. package/core/output/doctor.js +4 -0
  41. package/core/output/endpoints.js +15 -6
  42. package/core/output/find.js +11 -2
  43. package/core/output/lines.js +8 -2
  44. package/core/output/refactoring.js +121 -14
  45. package/core/output/reporting.js +31 -3
  46. package/core/output/shared.js +14 -0
  47. package/core/output-budget.js +316 -122
  48. package/core/parallel-build.js +124 -47
  49. package/core/parser.js +3 -2
  50. package/core/project.js +457 -60
  51. package/core/provenance-facts.js +156 -12
  52. package/core/provenance-overload.js +30 -3
  53. package/core/provenance.js +104 -30
  54. package/core/pytest-fixtures.js +434 -0
  55. package/core/python-fixture-flow.js +17 -3
  56. package/core/python-modules.js +173 -0
  57. package/core/reflection.js +877 -0
  58. package/core/registry.js +1 -1
  59. package/core/reporting.js +234 -52
  60. package/core/request-helpers.js +561 -0
  61. package/core/route-graph.js +1922 -0
  62. package/core/route-lists.js +2414 -0
  63. package/core/rust-macro-expansion.js +2113 -0
  64. package/core/rust-modules.js +185 -0
  65. package/core/search.js +121 -29
  66. package/core/shared.js +19 -1
  67. package/core/test-name-bindings.js +256 -0
  68. package/core/type-denotation.js +1161 -0
  69. package/core/type-references.js +3229 -0
  70. package/core/verify.js +3360 -672
  71. package/languages/adapter.js +31 -5
  72. package/languages/c-family.js +4061 -212
  73. package/languages/c-macro-invocations.js +380 -0
  74. package/languages/c-preprocessor.js +254 -0
  75. package/languages/csharp.js +1406 -49
  76. package/languages/go.js +1243 -70
  77. package/languages/index.js +728 -6
  78. package/languages/java.js +473 -61
  79. package/languages/javascript.js +1270 -197
  80. package/languages/lexical-scope.js +1280 -0
  81. package/languages/python.js +1557 -72
  82. package/languages/rust-macro-rules.js +1020 -0
  83. package/languages/rust-value-flow.js +416 -0
  84. package/languages/rust.js +2237 -115
  85. package/languages/utils.js +298 -4
  86. package/mcp/server.js +25 -11
  87. 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 ((command === 'impact' || command === 'check') && hasName && gitScope) {
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, error: e && e.message ? e.message : String(e) };
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
- const exclude = applyTestExclusions(p.exclude, p.includeTests);
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 default test exclusion, then filter in the
1291
- // handler — the hidden count must be VISIBLE (fix #234, campaign
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 default — pass --include-tests to include them.`);
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
- return { ok: false, error: e.message };
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 };