ucn 5.4.2 → 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 +40 -14
- 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.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 +7 -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 +305 -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 +22 -8
- package/package.json +1 -1
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Token-level expansion of C/C++ function-like macro invocations (fix #362).
|
|
5
|
+
*
|
|
6
|
+
* `#define XX(uc, lc) case UV_FS_##uc: fs__##lc(req); break;` followed by
|
|
7
|
+
* `XX(STAT, stat)` calls `fs__stat`, but no AST node spells that name: the
|
|
8
|
+
* call target exists only after the preprocessor substitutes arguments and
|
|
9
|
+
* pastes tokens. This module performs that substitution for invocations of
|
|
10
|
+
* project macros, parses the substituted token stream with the language's own
|
|
11
|
+
* tree-sitter grammar, and reports the identifiers the expansion produced:
|
|
12
|
+
*
|
|
13
|
+
* calls - pasted (`fs__##lc`) or argument-substituted (`CALL(foo)` ->
|
|
14
|
+
* `foo()`) identifiers in call position, attributed to the
|
|
15
|
+
* invocation site with `macroExpansion` provenance
|
|
16
|
+
* refs - pasted identifiers in value position (X-macro tables:
|
|
17
|
+
* `{ #name, handle_##name }`), used as deadcode liveness
|
|
18
|
+
* blind - invocations whose expansion cannot be computed (budget,
|
|
19
|
+
* unbalanced arguments, a pasted operand that is an unexpanded
|
|
20
|
+
* object-like project macro); disclosed, never guessed
|
|
21
|
+
*
|
|
22
|
+
* Nothing here is persisted: macro templates are indexed per file
|
|
23
|
+
* (`ppParams`/`ppBody` on macro symbols) and expansions are derived from the
|
|
24
|
+
* current index on demand, so a header edit or an incremental rebuild never
|
|
25
|
+
* leaves stale expansion edges behind. Work is proportional to invocations of
|
|
26
|
+
* macros that can produce names (pasting or higher-order macros, or macros
|
|
27
|
+
* that invoke them); a project without such macros pays one symbol scan.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const crypto = require('crypto');
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { langTraits, getParser, safeParse, PARSE_OPTIONS } = require('../languages');
|
|
34
|
+
const { lexPP, splitArguments, pastePatterns } = require('../languages/c-preprocessor');
|
|
35
|
+
const { codeUnitCompare } = require('./shared');
|
|
36
|
+
|
|
37
|
+
const MAX_DEPTH = 64;
|
|
38
|
+
const MAX_TOKENS = 20000;
|
|
39
|
+
const MAX_VARIANTS = 4;
|
|
40
|
+
|
|
41
|
+
class BlindExpansion extends Error {
|
|
42
|
+
constructor(reason) {
|
|
43
|
+
super(reason);
|
|
44
|
+
this.reason = reason;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const WRAPPERS = {
|
|
49
|
+
statement: ['void __ucn_macro_expansion__(void) { switch (0) {\n', '\n; } }\n'],
|
|
50
|
+
file: ['', '\n;\n'],
|
|
51
|
+
initializer: ['static int __ucn_macro_expansion__[] = {\n', '\n};\n'],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Per-build memo; reset with the other definition-derived memos. */
|
|
55
|
+
function expansionState(index) {
|
|
56
|
+
if (index._macroExpansion) return index._macroExpansion;
|
|
57
|
+
const templates = new Map(); // name -> function-like macro defs with a template
|
|
58
|
+
const macroDefs = new Map(); // name -> every macro def (object- and function-like)
|
|
59
|
+
const defsByFile = new Map(); // file -> function-like template defs
|
|
60
|
+
for (const [name, defs] of index.symbols) {
|
|
61
|
+
for (const def of defs) {
|
|
62
|
+
if (def.type !== 'macro') continue;
|
|
63
|
+
if (!langTraits(index.files.get(def.file)?.language)?.textualIncludes) continue;
|
|
64
|
+
if (!macroDefs.has(name)) macroDefs.set(name, []);
|
|
65
|
+
macroDefs.get(name).push(def);
|
|
66
|
+
if (!def.functionLike || typeof def.ppBody !== 'string') continue;
|
|
67
|
+
if (!templates.has(name)) templates.set(name, []);
|
|
68
|
+
templates.get(name).push(def);
|
|
69
|
+
if (!defsByFile.has(def.file)) defsByFile.set(def.file, []);
|
|
70
|
+
defsByFile.get(def.file).push(def);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const bodyTokens = new Map();
|
|
74
|
+
const tokensOf = def => {
|
|
75
|
+
let tokens = bodyTokens.get(def);
|
|
76
|
+
if (!tokens) {
|
|
77
|
+
tokens = lexPP(def.ppBody);
|
|
78
|
+
bodyTokens.set(def, tokens);
|
|
79
|
+
}
|
|
80
|
+
return tokens;
|
|
81
|
+
};
|
|
82
|
+
const relevant = productiveMacros(index, templates, tokensOf);
|
|
83
|
+
const state = {
|
|
84
|
+
templates, macroDefs, defsByFile, relevant, tokensOf,
|
|
85
|
+
byFile: new Map(), summary: null, includeTokens: new Map(),
|
|
86
|
+
words: new Map(), visibleMemo: new Map(), persistOut: new Map(), signature: null,
|
|
87
|
+
};
|
|
88
|
+
index._macroExpansion = state;
|
|
89
|
+
return state;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Macros whose invocations can produce a project name the source does not
|
|
94
|
+
* spell (the invocations worth expanding). Parameter flow, per definition:
|
|
95
|
+
* a parameter is productive when its argument can become a paste operand,
|
|
96
|
+
* the callee of a call (`p(...)`, higher-order `LIST(X)`), or an argument
|
|
97
|
+
* at a productive position of a macro the body invokes. A literal-only
|
|
98
|
+
* paste that spells a project name makes the macro productive by itself.
|
|
99
|
+
* gtest-style assertion macros (arguments only ever in value position,
|
|
100
|
+
* pastes only of `__LINE__`-built locals) are not expanded at all.
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* Can a paste pattern (literal pieces, null for a parameter wildcard that may
|
|
104
|
+
* be empty) spell a name the project defines? Memoized per build.
|
|
105
|
+
*/
|
|
106
|
+
function patternSpellsProjectName(index, pieces) {
|
|
107
|
+
const state = index._macroSpellable || (index._macroSpellable = { names: null, memo: new Map() });
|
|
108
|
+
const key = pieces.map(piece => piece === null ? '\u0000' : piece).join('\u0001');
|
|
109
|
+
if (state.memo.has(key)) return state.memo.get(key);
|
|
110
|
+
const literal = pieces.filter(piece => piece !== null);
|
|
111
|
+
let result = true;
|
|
112
|
+
if (literal.length > 0) {
|
|
113
|
+
if (!state.names) state.names = [...index.symbols.keys()];
|
|
114
|
+
const pattern = new RegExp('^' + pieces.map(piece => piece === null ? '[A-Za-z0-9_$]*'
|
|
115
|
+
: piece.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('') + '$');
|
|
116
|
+
const longest = literal.reduce((a, b) => (b.length > a.length ? b : a));
|
|
117
|
+
result = state.names.some(candidate => candidate.includes(longest) && pattern.test(candidate));
|
|
118
|
+
}
|
|
119
|
+
state.memo.set(key, result);
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function productiveMacros(index, templates, tokensOf) {
|
|
124
|
+
// Can a paste of these operands spell a project name? Parameter operands
|
|
125
|
+
// are wildcards (possibly empty: an empty argument is a placemarker);
|
|
126
|
+
// a chain of literal operands only is checked by name directly below.
|
|
127
|
+
const pasteCanSpell = (operands, paramIndex) => {
|
|
128
|
+
if (operands.some(op => op.k !== 'id' && op.k !== 'num')) return true;
|
|
129
|
+
return patternSpellsProjectName(index,
|
|
130
|
+
operands.map(op => op.k === 'id' && paramIndex.has(op.v) ? null : op.v));
|
|
131
|
+
};
|
|
132
|
+
// Does some definition of `callee` copy its parameter at `argIndex` into
|
|
133
|
+
// its output as written (not only pasted or stringized)?
|
|
134
|
+
const passthrough = new Map();
|
|
135
|
+
const passesThrough = (callee, argIndex) => {
|
|
136
|
+
const key = `${callee}\0${argIndex}`;
|
|
137
|
+
if (passthrough.has(key)) return passthrough.get(key);
|
|
138
|
+
let result = false;
|
|
139
|
+
for (const def of templates.get(callee) || []) {
|
|
140
|
+
const params = def.ppParams || [];
|
|
141
|
+
const position = def.ppVariadic && argIndex >= params.length - 1 ? params.length - 1 : argIndex;
|
|
142
|
+
const param = params[position];
|
|
143
|
+
if (param === undefined) continue;
|
|
144
|
+
const body = tokensOf(def);
|
|
145
|
+
if (body.some((token, i) => token.k === 'id' && token.v === param &&
|
|
146
|
+
body[i - 1]?.v !== '#' && body[i - 1]?.v !== '##' && body[i + 1]?.v !== '##')) {
|
|
147
|
+
result = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
passthrough.set(key, result);
|
|
152
|
+
return result;
|
|
153
|
+
};
|
|
154
|
+
const productive = new Map(); // name -> Set of productive param indices
|
|
155
|
+
const fixed = new Set(); // names with a literal paste spelling a project name
|
|
156
|
+
const deps = []; // [name, paramIndex, calleeName, argIndex]
|
|
157
|
+
const mark = (name, i) => {
|
|
158
|
+
if (!productive.has(name)) productive.set(name, new Set());
|
|
159
|
+
const set = productive.get(name);
|
|
160
|
+
if (set.has(i)) return false;
|
|
161
|
+
set.add(i);
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
const variadicOf = new Map();
|
|
165
|
+
for (const [name, defs] of templates) {
|
|
166
|
+
for (const def of defs) {
|
|
167
|
+
const tokens = tokensOf(def);
|
|
168
|
+
const params = def.ppParams || [];
|
|
169
|
+
if (def.ppVariadic) variadicOf.set(name, params.length - 1);
|
|
170
|
+
const paramIndex = new Map(params.map((param, i) => [param, i]));
|
|
171
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
172
|
+
const t = tokens[i];
|
|
173
|
+
if (t.k === 'punct' && t.v === '##') continue;
|
|
174
|
+
if (tokens[i + 1]?.v === '##' && tokens[i + 1].k === 'punct') {
|
|
175
|
+
const operands = [t];
|
|
176
|
+
let j = i + 1;
|
|
177
|
+
while (tokens[j]?.v === '##' && tokens[j + 1]) {
|
|
178
|
+
operands.push(tokens[j + 1]);
|
|
179
|
+
j += 2;
|
|
180
|
+
}
|
|
181
|
+
const paramOperands = operands.filter(op => op.k === 'id' && paramIndex.has(op.v));
|
|
182
|
+
// A paste whose literal pieces no project name can
|
|
183
|
+
// contain (`suite##_##name##_Test` in a project with no
|
|
184
|
+
// `*_*_Test` symbol) produces no name worth expanding
|
|
185
|
+
// for (fix #385: gtest TEST(...) sites, a third of fmt's
|
|
186
|
+
// cold-build expansion work, produced nothing).
|
|
187
|
+
if (paramOperands.length > 0 && !pasteCanSpell(operands, paramIndex)) {
|
|
188
|
+
i = j - 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
for (const op of paramOperands) mark(name, paramIndex.get(op.v));
|
|
192
|
+
if (paramOperands.length === 0 && index.symbols.has(operands.map(op => op.v).join(''))) {
|
|
193
|
+
fixed.add(name);
|
|
194
|
+
}
|
|
195
|
+
i = j - 1;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (t.k !== 'id') continue;
|
|
199
|
+
if (paramIndex.has(t.v)) {
|
|
200
|
+
const next = tokens[i + 1];
|
|
201
|
+
if (next?.v === '(' || (next?.k === 'id' && paramIndex.has(next.v))) {
|
|
202
|
+
mark(name, paramIndex.get(t.v));
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (tokens[i + 1]?.v !== '(' || !templates.has(t.v)) continue;
|
|
207
|
+
const split = splitArguments(tokens, i + 1);
|
|
208
|
+
if (!split) continue;
|
|
209
|
+
split.args.forEach((arg, argIndex) => {
|
|
210
|
+
// A parameter inside a nested invocation in this argument
|
|
211
|
+
// (`OUTER(INNER(p))`) reaches OUTER's argument only when
|
|
212
|
+
// INNER passes that parameter through to its output; a
|
|
213
|
+
// pasted-only one (`INNER(a) = a##_Test`) does not (fix
|
|
214
|
+
// #385). Its own dependency on INNER is recorded when the
|
|
215
|
+
// scan reaches INNER.
|
|
216
|
+
const nestedAt = nestedInvocationSlots(arg, templates);
|
|
217
|
+
arg.forEach((token, k) => {
|
|
218
|
+
if (token.k !== 'id' || !paramIndex.has(token.v)) return;
|
|
219
|
+
const slot = nestedAt[k];
|
|
220
|
+
if (slot && !passesThrough(slot.name, slot.argIndex)) return;
|
|
221
|
+
deps.push([name, paramIndex.get(token.v), t.v, argIndex]);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
deps.push([name, -1, t.v, -1]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (let changed = true; changed;) {
|
|
229
|
+
changed = false;
|
|
230
|
+
for (const [name, param, callee, argIndex] of deps) {
|
|
231
|
+
if (param < 0) {
|
|
232
|
+
if (fixed.has(callee) && !fixed.has(name)) {
|
|
233
|
+
fixed.add(name);
|
|
234
|
+
changed = true;
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const calleeParams = productive.get(callee);
|
|
239
|
+
if (!calleeParams) continue;
|
|
240
|
+
const variadic = variadicOf.get(callee);
|
|
241
|
+
const position = variadic != null && argIndex > variadic ? variadic : argIndex;
|
|
242
|
+
if (calleeParams.has(position) && mark(name, param)) changed = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return new Set([...productive.keys(), ...fixed]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* For each token of a macro argument, the outermost template-macro
|
|
250
|
+
* invocation within the argument that contains it: { name, argIndex }, or
|
|
251
|
+
* undefined for a token at the argument's own level.
|
|
252
|
+
*/
|
|
253
|
+
function nestedInvocationSlots(arg, templates) {
|
|
254
|
+
const slots = new Array(arg.length);
|
|
255
|
+
for (let i = 0; i < arg.length; i++) {
|
|
256
|
+
const token = arg[i];
|
|
257
|
+
if (token.k !== 'id' || !templates.has(token.v) || arg[i + 1]?.v !== '(') continue;
|
|
258
|
+
const split = splitArguments(arg, i + 1);
|
|
259
|
+
if (!split) continue;
|
|
260
|
+
let argIndex = 0;
|
|
261
|
+
let depth = 0;
|
|
262
|
+
for (let k = i + 2; k < split.close; k++) {
|
|
263
|
+
const inner = arg[k];
|
|
264
|
+
if (inner.k === 'punct' && inner.v === '(') depth++;
|
|
265
|
+
else if (inner.k === 'punct' && inner.v === ')') depth--;
|
|
266
|
+
else if (depth === 0 && inner.k === 'punct' && inner.v === ',') {
|
|
267
|
+
argIndex++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
slots[k] = { name: token.v, argIndex };
|
|
271
|
+
}
|
|
272
|
+
i = split.close;
|
|
273
|
+
}
|
|
274
|
+
return slots;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function defKey(def) {
|
|
278
|
+
return `${def.relativePath || def.file}:${def.startLine}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** `#undef NAME` lines of a file (directive tokens, not text matching). */
|
|
282
|
+
function undefLines(fileState, lines) {
|
|
283
|
+
if (fileState.undefs) return fileState.undefs;
|
|
284
|
+
const undefs = new Map();
|
|
285
|
+
for (let row = 0; row < lines.length; row++) {
|
|
286
|
+
const line = lines[row];
|
|
287
|
+
const hash = line.indexOf('#');
|
|
288
|
+
if (hash < 0 || line.slice(0, hash).trim() !== '') continue;
|
|
289
|
+
const tokens = lexPP(line);
|
|
290
|
+
if (tokens[0]?.v === '#' && tokens[1]?.v === 'undef' && tokens[2]?.k === 'id') {
|
|
291
|
+
if (!undefs.has(tokens[2].v)) undefs.set(tokens[2].v, []);
|
|
292
|
+
undefs.get(tokens[2].v).push(row + 1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
fileState.undefs = undefs;
|
|
296
|
+
return undefs;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Macro definitions of `name` in effect at (file, line). Same-file
|
|
301
|
+
* definitions win (nearest preceding; an earlier one with no `#undef`
|
|
302
|
+
* between is a conditional alternative); otherwise definitions visible
|
|
303
|
+
* through the file's #include closure. Returns { defs, ambiguous }.
|
|
304
|
+
*/
|
|
305
|
+
function definitionsInScope(index, state, site, name) {
|
|
306
|
+
const memoKey = `${name}\0${site.line}`;
|
|
307
|
+
const hit = site.lookupMemo.get(memoKey);
|
|
308
|
+
if (hit) return hit;
|
|
309
|
+
const all = state.macroDefs.get(name) || [];
|
|
310
|
+
let result = { defs: [], ambiguous: false };
|
|
311
|
+
if (all.length > 0) {
|
|
312
|
+
const sameFile = all.filter(def => def.file === site.file && def.startLine < site.line)
|
|
313
|
+
.sort((a, b) => b.startLine - a.startLine);
|
|
314
|
+
if (sameFile.length > 0) {
|
|
315
|
+
const undefs = undefLines(site.fileState, site.lines).get(name) || [];
|
|
316
|
+
const chosen = [sameFile[0]];
|
|
317
|
+
for (let i = 1; i < sameFile.length; i++) {
|
|
318
|
+
const later = chosen[chosen.length - 1];
|
|
319
|
+
if (undefs.some(line => line > sameFile[i].endLine && line < later.startLine)) break;
|
|
320
|
+
chosen.push(sameFile[i]);
|
|
321
|
+
}
|
|
322
|
+
result = { defs: chosen, ambiguous: false };
|
|
323
|
+
} else {
|
|
324
|
+
const { _textualIncludeClosures } = require('./callers');
|
|
325
|
+
const closure = _textualIncludeClosures(index, site.file).all;
|
|
326
|
+
const visible = all.filter(def => def.file !== site.file && closure.has(def.file))
|
|
327
|
+
.sort((a, b) => codeUnitCompare(defKey(a), defKey(b)));
|
|
328
|
+
result = { defs: visible, ambiguous: false };
|
|
329
|
+
}
|
|
330
|
+
// Identical redefinitions are one definition; different bodies are
|
|
331
|
+
// alternatives the preprocessor chooses between by configuration.
|
|
332
|
+
const distinct = new Map();
|
|
333
|
+
for (const def of result.defs) {
|
|
334
|
+
const key = def.functionLike
|
|
335
|
+
? `f\0${(def.ppParams || []).join(',')}\0${def.ppVariadic ? 1 : 0}\0${def.ppBody}`
|
|
336
|
+
: `o\0${def.startLine}\0${def.file}`;
|
|
337
|
+
if (!distinct.has(key)) distinct.set(key, def);
|
|
338
|
+
}
|
|
339
|
+
const defs = [...distinct.values()];
|
|
340
|
+
result = {
|
|
341
|
+
defs: defs.slice(0, MAX_VARIANTS),
|
|
342
|
+
ambiguous: defs.length > 1,
|
|
343
|
+
truncated: defs.length > MAX_VARIANTS,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
site.lookupMemo.set(memoKey, result);
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Preprocessor macro definitions of `name` in effect at a C/C++ source site
|
|
352
|
+
* (fix #377): same-file definitions above the line (an intervening `#undef`
|
|
353
|
+
* ends one), else definitions in the file's #include closure. A call spelled
|
|
354
|
+
* NAME( there invokes the macro, not a same-named function. Returns
|
|
355
|
+
* { defs, ambiguous, reach } with reach 'file' | 'include', or null when the
|
|
356
|
+
* name has no macro definition or the file cannot be read.
|
|
357
|
+
*/
|
|
358
|
+
function macroDefinitionsAt(index, filePath, line, name) {
|
|
359
|
+
const state = expansionState(index);
|
|
360
|
+
const all = state.macroDefs.get(name);
|
|
361
|
+
if (!all || all.length === 0) return null;
|
|
362
|
+
let lines;
|
|
363
|
+
try {
|
|
364
|
+
lines = index._getFileLines(filePath);
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
if (!state.shadowFileStates) state.shadowFileStates = new Map();
|
|
369
|
+
let fileState = state.shadowFileStates.get(filePath);
|
|
370
|
+
if (!fileState) {
|
|
371
|
+
fileState = {};
|
|
372
|
+
state.shadowFileStates.set(filePath, fileState);
|
|
373
|
+
}
|
|
374
|
+
const site = { file: filePath, line, lines, fileState, lookupMemo: new Map() };
|
|
375
|
+
const result = definitionsInScope(index, state, site, name);
|
|
376
|
+
if (result.defs.length === 0) return null;
|
|
377
|
+
const sameFile = result.defs.every(def => def.file === filePath);
|
|
378
|
+
return { ...result, reach: sameFile ? 'file' : 'include' };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Tokens of a macro's replacement list (function- or object-like). */
|
|
382
|
+
function macroBodyTokens(index, def) {
|
|
383
|
+
if (typeof def.ppBody !== 'string') return null;
|
|
384
|
+
return expansionState(index).tokensOf(def);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Does an invocation with `args` fit the macro's parameter list? */
|
|
388
|
+
function arityFits(def, args) {
|
|
389
|
+
const params = def.ppParams || [];
|
|
390
|
+
const count = params.length === 0 && args.length === 1 && args[0].length === 0 ? 0 : args.length;
|
|
391
|
+
if (def.ppVariadic) return count >= params.length - 1;
|
|
392
|
+
return count === params.length;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Expand one invocation of `def` with argument token lists `args`. */
|
|
396
|
+
function substitute(index, state, site, def, args, hide, depth) {
|
|
397
|
+
if (depth > MAX_DEPTH) throw new BlindExpansion('expansion-depth');
|
|
398
|
+
const params = def.ppParams || [];
|
|
399
|
+
if (typeof def.ppBody !== 'string') throw new BlindExpansion('no-template');
|
|
400
|
+
let actual = args;
|
|
401
|
+
if (params.length === 0 && actual.length === 1 && actual[0].length === 0) actual = [];
|
|
402
|
+
if (def.ppVariadic) {
|
|
403
|
+
// The last parameter is the variadic one (`...` as __VA_ARGS__, or
|
|
404
|
+
// GNU `name...`); it takes every remaining argument with its commas.
|
|
405
|
+
const namedCount = params.length - 1;
|
|
406
|
+
if (actual.length < namedCount) throw new BlindExpansion('argument-count');
|
|
407
|
+
const joined = [];
|
|
408
|
+
actual.slice(namedCount).forEach((arg, i) => {
|
|
409
|
+
if (i > 0) joined.push({ v: ',', k: 'punct', o: 'arg' });
|
|
410
|
+
joined.push(...arg);
|
|
411
|
+
});
|
|
412
|
+
actual = [...actual.slice(0, namedCount), joined];
|
|
413
|
+
} else if (actual.length !== params.length) {
|
|
414
|
+
throw new BlindExpansion('argument-count');
|
|
415
|
+
}
|
|
416
|
+
const paramIndex = new Map(params.map((p, i) => [p, i]));
|
|
417
|
+
const body = state.tokensOf(def);
|
|
418
|
+
const expandedArgs = new Map();
|
|
419
|
+
const expandedArg = i => {
|
|
420
|
+
if (!expandedArgs.has(i)) {
|
|
421
|
+
expandedArgs.set(i, expandTokens(index, state, site, actual[i], hide, depth + 1));
|
|
422
|
+
}
|
|
423
|
+
return expandedArgs.get(i);
|
|
424
|
+
};
|
|
425
|
+
const out = [];
|
|
426
|
+
for (let i = 0; i < body.length; i++) {
|
|
427
|
+
const t = body[i];
|
|
428
|
+
if (t.k === 'punct' && t.v === '#' && body[i + 1]?.k === 'id' && paramIndex.has(body[i + 1].v)) {
|
|
429
|
+
const arg = actual[paramIndex.get(body[i + 1].v)];
|
|
430
|
+
out.push({ v: JSON.stringify(arg.map(a => a.v).join(' ')), k: 'str', o: 'str' });
|
|
431
|
+
i++;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (t.k === 'id' && t.v === '__VA_OPT__' && def.ppVariadic && body[i + 1]?.v === '(') {
|
|
435
|
+
const split = splitArguments(body, i + 1);
|
|
436
|
+
if (!split) throw new BlindExpansion('va-opt');
|
|
437
|
+
const hasRest = actual[actual.length - 1].length > 0;
|
|
438
|
+
if (hasRest) {
|
|
439
|
+
const inner = body.slice(i + 2, split.close);
|
|
440
|
+
for (const token of inner) out.push({ ...token, o: 'body' });
|
|
441
|
+
}
|
|
442
|
+
i = split.close;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (t.k === 'id' && paramIndex.has(t.v)) {
|
|
446
|
+
const pasted = body[i - 1]?.v === '##' || body[i + 1]?.v === '##';
|
|
447
|
+
const arg = pasted ? actual[paramIndex.get(t.v)] : expandedArg(paramIndex.get(t.v));
|
|
448
|
+
if (arg.length === 0 && pasted) out.push({ v: '', k: 'placemarker', o: 'body' });
|
|
449
|
+
for (const token of arg) out.push(token);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (t.k === 'punct' && t.v === '##') {
|
|
453
|
+
out.push({ v: '##', k: 'paste-op', o: 'body' });
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
out.push({ v: t.v, k: t.k, o: 'body' });
|
|
457
|
+
}
|
|
458
|
+
// Paste pass: `##` from the replacement list (never one that arrived in
|
|
459
|
+
// an argument) joins its neighbours into one token.
|
|
460
|
+
const pasted = [];
|
|
461
|
+
for (let i = 0; i < out.length; i++) {
|
|
462
|
+
const t = out[i];
|
|
463
|
+
if (t.k !== 'paste-op') {
|
|
464
|
+
pasted.push(t);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const left = pasted.pop();
|
|
468
|
+
const right = out[i + 1];
|
|
469
|
+
i++;
|
|
470
|
+
if (!left || !right) throw new BlindExpansion('paste-operand');
|
|
471
|
+
if (left.k === 'placemarker') {
|
|
472
|
+
pasted.push(right);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (right.k === 'placemarker') {
|
|
476
|
+
pasted.push(left);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (left.opaque || right.opaque) throw new BlindExpansion('opaque-paste-operand');
|
|
480
|
+
const text = left.v + right.v;
|
|
481
|
+
const relexed = lexPP(text);
|
|
482
|
+
const token = relexed.length === 1 && relexed[0].v === text
|
|
483
|
+
? { v: text, k: relexed[0].k, o: 'paste' }
|
|
484
|
+
// An invalid paste is undefined behavior; keep the spelling so
|
|
485
|
+
// no name is invented from it.
|
|
486
|
+
: { v: text, k: 'invalid', o: 'paste' };
|
|
487
|
+
pasted.push(token);
|
|
488
|
+
}
|
|
489
|
+
const result = pasted.filter(t => t.k !== 'placemarker');
|
|
490
|
+
site.budget -= result.length;
|
|
491
|
+
if (site.budget < 0) throw new BlindExpansion('expansion-budget');
|
|
492
|
+
const inner = new Set(hide);
|
|
493
|
+
inner.add(def.name);
|
|
494
|
+
return expandTokens(index, state, site, result, inner, depth + 1);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Rescan a token list, expanding invocations of project function-like macros. */
|
|
498
|
+
function expandTokens(index, state, site, tokens, hide, depth) {
|
|
499
|
+
if (depth > MAX_DEPTH) throw new BlindExpansion('expansion-depth');
|
|
500
|
+
const out = [];
|
|
501
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
502
|
+
const t = tokens[i];
|
|
503
|
+
if (t.k !== 'id' || t.noexpand || !state.macroDefs.has(t.v)) {
|
|
504
|
+
out.push(t);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (hide.has(t.v)) {
|
|
508
|
+
out.push({ ...t, noexpand: true });
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const scope = definitionsInScope(index, state, site, t.v);
|
|
512
|
+
let def = scope.defs[0];
|
|
513
|
+
if (!def) {
|
|
514
|
+
out.push(t);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (!def.functionLike) {
|
|
518
|
+
// An object-like macro expands to its replacement list when every
|
|
519
|
+
// definition in scope agrees on it; otherwise (or without a kept
|
|
520
|
+
// list) the token stays as spelled and may not take part in a
|
|
521
|
+
// paste. Tokens replacing an argument-spelled name are produced,
|
|
522
|
+
// not spelled at the site.
|
|
523
|
+
const bodies = new Set(scope.defs.map(other =>
|
|
524
|
+
!other.functionLike && typeof other.ppBody === 'string' ? other.ppBody : null));
|
|
525
|
+
if (scope.truncated || bodies.size !== 1 || bodies.has(null)) {
|
|
526
|
+
out.push({ ...t, opaque: true });
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const origin = t.o === 'arg' || t.o === 'paste' ? 'paste' : t.o;
|
|
530
|
+
const replaced = lexPP(def.ppBody).map(token => ({ v: token.v, k: token.k, o: origin }));
|
|
531
|
+
site.budget -= replaced.length;
|
|
532
|
+
if (site.budget < 0) throw new BlindExpansion('expansion-budget');
|
|
533
|
+
const inner = new Set(hide);
|
|
534
|
+
inner.add(t.v);
|
|
535
|
+
out.push(...expandTokens(index, state, site, replaced, inner, depth + 1));
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (tokens[i + 1]?.v !== '(') {
|
|
539
|
+
out.push(t);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const split = splitArguments(tokens, i + 1);
|
|
543
|
+
if (!split) throw new BlindExpansion('unbalanced-arguments');
|
|
544
|
+
// A configuration alternative whose parameter list cannot take these
|
|
545
|
+
// arguments is not the definition in effect (it would not compile).
|
|
546
|
+
if (!arityFits(def, split.args)) {
|
|
547
|
+
def = scope.defs.find(other => other.functionLike && arityFits(other, split.args)) || def;
|
|
548
|
+
}
|
|
549
|
+
// Only alternatives of a macro that is actually expanded can change
|
|
550
|
+
// the produced names (an ambiguous object-like attribute cannot).
|
|
551
|
+
if (scope.ambiguous) site.ambiguous = true;
|
|
552
|
+
site.usedDefs.add(defKey(def));
|
|
553
|
+
out.push(...substitute(index, state, site, def, split.args, hide, depth + 1));
|
|
554
|
+
i = split.close;
|
|
555
|
+
}
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function countErrors(root) {
|
|
560
|
+
if (!root.hasError) return 0;
|
|
561
|
+
let count = 0;
|
|
562
|
+
const stack = [root];
|
|
563
|
+
while (stack.length) {
|
|
564
|
+
const node = stack.pop();
|
|
565
|
+
if (node.type === 'ERROR' || node.isMissing) count++;
|
|
566
|
+
if (!node.hasError && node.type !== 'ERROR') continue;
|
|
567
|
+
for (let i = 0; i < node.childCount; i++) stack.push(node.child(i));
|
|
568
|
+
}
|
|
569
|
+
return count;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const DECLARATOR_PARENTS = new Set([
|
|
573
|
+
'function_declarator', 'init_declarator', 'pointer_declarator',
|
|
574
|
+
'array_declarator', 'parameter_declaration', 'declaration',
|
|
575
|
+
'field_declaration', 'reference_declarator', 'parenthesized_declarator',
|
|
576
|
+
'optional_parameter_declaration', 'variadic_declarator',
|
|
577
|
+
]);
|
|
578
|
+
|
|
579
|
+
function calleeNameNode(fn) {
|
|
580
|
+
let node = fn;
|
|
581
|
+
for (let guard = 0; node && guard < 8; guard++) {
|
|
582
|
+
if (node.type === 'identifier') return node;
|
|
583
|
+
if (node.type === 'qualified_identifier' || node.type === 'template_function') {
|
|
584
|
+
node = node.childForFieldName('name');
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function producesProjectName(index, tokens) {
|
|
593
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
594
|
+
const t = tokens[i];
|
|
595
|
+
if (t.k !== 'id' || !index.symbols.has(t.v)) continue;
|
|
596
|
+
if (t.o === 'paste' || t.foreign) return true;
|
|
597
|
+
if (t.o === 'arg' && !t.srcCall && tokens[i + 1]?.v === '(') return true;
|
|
598
|
+
}
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Parse the substituted token stream and classify the identifiers the
|
|
604
|
+
* expansion produced (not the replacement list's own literal tokens, which
|
|
605
|
+
* the macro-body scan already attributes to the definition).
|
|
606
|
+
*/
|
|
607
|
+
function classifyExpansion(parser, tokens, preferred) {
|
|
608
|
+
const pieces = [];
|
|
609
|
+
const spans = [];
|
|
610
|
+
let offset = 0;
|
|
611
|
+
for (const t of tokens) {
|
|
612
|
+
if (t.k === 'placemarker') continue;
|
|
613
|
+
if (pieces.length) offset += 1;
|
|
614
|
+
spans.push({ start: offset, token: t });
|
|
615
|
+
pieces.push(t.v);
|
|
616
|
+
offset += t.v.length;
|
|
617
|
+
}
|
|
618
|
+
const text = pieces.join(' ');
|
|
619
|
+
let best = null;
|
|
620
|
+
for (const wrapper of preferred) {
|
|
621
|
+
const [prefix, suffix] = WRAPPERS[wrapper];
|
|
622
|
+
const tree = safeParse(parser, prefix + text + suffix, undefined, PARSE_OPTIONS);
|
|
623
|
+
const errors = countErrors(tree.rootNode);
|
|
624
|
+
if (!best || errors < best.errors) {
|
|
625
|
+
best?.tree.delete?.();
|
|
626
|
+
best = { tree, prefix, errors };
|
|
627
|
+
} else {
|
|
628
|
+
tree.delete?.();
|
|
629
|
+
}
|
|
630
|
+
if (errors === 0) break;
|
|
631
|
+
}
|
|
632
|
+
const tokenAt = new Map(spans.map(span => [span.start, span.token]));
|
|
633
|
+
const calls = [];
|
|
634
|
+
const refs = [];
|
|
635
|
+
const produced = (node) => {
|
|
636
|
+
const token = tokenAt.get(node.startIndex - best.prefix.length);
|
|
637
|
+
if (!token || token.v !== node.text) return null;
|
|
638
|
+
return token.o === 'paste' || token.o === 'arg' ? token : null;
|
|
639
|
+
};
|
|
640
|
+
const callees = new Set();
|
|
641
|
+
const stack = [best.tree.rootNode];
|
|
642
|
+
const found = [];
|
|
643
|
+
while (stack.length) {
|
|
644
|
+
const node = stack.pop();
|
|
645
|
+
if (node.type === 'call_expression') {
|
|
646
|
+
const nameNode = calleeNameNode(node.childForFieldName('function'));
|
|
647
|
+
if (nameNode) {
|
|
648
|
+
callees.add(nameNode.startIndex);
|
|
649
|
+
const token = produced(nameNode);
|
|
650
|
+
if (token) {
|
|
651
|
+
const args = node.childForFieldName('arguments');
|
|
652
|
+
const argCount = (args?.namedChildren || [])
|
|
653
|
+
.filter(child => child.type !== 'comment').length;
|
|
654
|
+
found.push({ kind: 'call', token, argCount, at: nameNode.startIndex });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} else if (node.type === 'identifier' && !callees.has(node.startIndex)) {
|
|
658
|
+
const parent = node.parent;
|
|
659
|
+
const declarator = parent && DECLARATOR_PARENTS.has(parent.type) &&
|
|
660
|
+
parent.childForFieldName('declarator')?.startIndex === node.startIndex;
|
|
661
|
+
const token = !declarator && produced(node);
|
|
662
|
+
if (token && (token.o === 'paste' || token.foreign)) {
|
|
663
|
+
found.push({ kind: 'ref', token, at: node.startIndex });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
for (let i = node.childCount - 1; i >= 0; i--) stack.push(node.child(i));
|
|
667
|
+
}
|
|
668
|
+
for (const item of found) {
|
|
669
|
+
if (item.kind === 'call') calls.push(item);
|
|
670
|
+
else if (!callees.has(item.at)) refs.push(item);
|
|
671
|
+
}
|
|
672
|
+
best.tree.delete?.();
|
|
673
|
+
return { calls, refs };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* classifyExpansion memoized per build (fix #365): the classification
|
|
678
|
+
* depends only on the stream's token texts and origins and the wrapper
|
|
679
|
+
* order, and invocations repeat (`cJSON_Add...ToObject(...)`, list
|
|
680
|
+
* entries). Results are stored as stream positions and rebound to the
|
|
681
|
+
* current tokens, whose source offsets differ per invocation.
|
|
682
|
+
*/
|
|
683
|
+
function classifyExpansionMemo(state, parser, tokens, preferred) {
|
|
684
|
+
const kept = tokens.filter(t => t.k !== 'placemarker');
|
|
685
|
+
const key = preferred.join(',') + '\u0002' + kept.map(t =>
|
|
686
|
+
`${t.v}\u0001${t.o || ''}\u0001${t.foreign ? 1 : 0}`).join('\u0002');
|
|
687
|
+
if (!state.classifyMemo) state.classifyMemo = new Map();
|
|
688
|
+
let memo = state.classifyMemo.get(key);
|
|
689
|
+
if (!memo) {
|
|
690
|
+
const result = classifyExpansion(parser, tokens, preferred);
|
|
691
|
+
const position = new Map(kept.map((t, i) => [t, i]));
|
|
692
|
+
memo = {
|
|
693
|
+
calls: result.calls.map(c => ({ ...c, token: position.get(c.token) })),
|
|
694
|
+
refs: result.refs.map(r => ({ ...r, token: position.get(r.token) })),
|
|
695
|
+
};
|
|
696
|
+
state.classifyMemo.set(key, memo);
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
calls: memo.calls.map(c => ({ ...c, token: kept[c.token] })),
|
|
700
|
+
refs: memo.refs.map(r => ({ ...r, token: kept[r.token] })),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function lineStarts(content) {
|
|
705
|
+
const starts = [0];
|
|
706
|
+
for (let i = 0; i < content.length; i++) {
|
|
707
|
+
if (content.charCodeAt(i) === 10) starts.push(i + 1);
|
|
708
|
+
}
|
|
709
|
+
return starts;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function positionOf(starts, offset) {
|
|
713
|
+
let lo = 0;
|
|
714
|
+
let hi = starts.length - 1;
|
|
715
|
+
while (lo < hi) {
|
|
716
|
+
const mid = (lo + hi + 1) >> 1;
|
|
717
|
+
if (starts[mid] <= offset) lo = mid;
|
|
718
|
+
else hi = mid - 1;
|
|
719
|
+
}
|
|
720
|
+
return { line: lo + 1, column: offset - starts[lo] };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Names of relevant (name-producing) macros a file can invoke: its own
|
|
725
|
+
* definitions and those of every file in its #include closure.
|
|
726
|
+
*/
|
|
727
|
+
/** Identifier-shaped words of a file (a prefilter; invocations are token-checked). */
|
|
728
|
+
function wordsOf(state, filePath, text) {
|
|
729
|
+
let words = state.words.get(filePath);
|
|
730
|
+
if (!words) {
|
|
731
|
+
words = new Set(text.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []);
|
|
732
|
+
state.words.set(filePath, words);
|
|
733
|
+
}
|
|
734
|
+
return words;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* The visible relevant macro names a file spells as whole identifiers: one
|
|
739
|
+
* alternation scan instead of collecting every identifier of the file into a
|
|
740
|
+
* set (fix #365; same identifier boundaries as wordsOf).
|
|
741
|
+
*/
|
|
742
|
+
function namesSpelled(state, filePath, content, names) {
|
|
743
|
+
const cached = state.words.get(filePath);
|
|
744
|
+
if (cached) return new Set(names.filter(name => cached.has(name)));
|
|
745
|
+
if (names.length === 0) return new Set();
|
|
746
|
+
// A word is the part of a maximal [A-Za-z0-9_] run from its first
|
|
747
|
+
// non-digit on (`12ab` spells `ab`), exactly as wordsOf tokenizes.
|
|
748
|
+
const isWord = code => (code >= 48 && code <= 57) || (code >= 65 && code <= 90) ||
|
|
749
|
+
(code >= 97 && code <= 122) || code === 95;
|
|
750
|
+
const isDigit = code => code >= 48 && code <= 57;
|
|
751
|
+
const found = new Set();
|
|
752
|
+
for (const name of names) {
|
|
753
|
+
for (let at = content.indexOf(name); at >= 0; at = content.indexOf(name, at + 1)) {
|
|
754
|
+
const after = at + name.length;
|
|
755
|
+
if (after < content.length && isWord(content.charCodeAt(after))) continue;
|
|
756
|
+
let before = at - 1;
|
|
757
|
+
while (before >= 0 && isDigit(content.charCodeAt(before))) before--;
|
|
758
|
+
if (before >= 0 && isWord(content.charCodeAt(before))) continue;
|
|
759
|
+
found.add(name);
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return new Set(names.filter(name => found.has(name)));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function relevantNamesVisibleFrom(index, state, filePath) {
|
|
767
|
+
const memo = state.visibleMemo.get(filePath);
|
|
768
|
+
if (memo) return memo;
|
|
769
|
+
const result = relevantNamesVisibleFromUncached(index, state, filePath);
|
|
770
|
+
state.visibleMemo.set(filePath, result);
|
|
771
|
+
return result;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function relevantNamesVisibleFromUncached(index, state, filePath) {
|
|
775
|
+
if (!state.relevantFiles) {
|
|
776
|
+
state.relevantFiles = new Map();
|
|
777
|
+
for (const name of state.relevant) {
|
|
778
|
+
for (const def of state.templates.get(name) || []) {
|
|
779
|
+
if (!state.relevantFiles.has(def.file)) state.relevantFiles.set(def.file, new Set());
|
|
780
|
+
state.relevantFiles.get(def.file).add(name);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (state.relevantFiles.size === 0) return [];
|
|
785
|
+
const names = new Set(state.relevantFiles.get(filePath) || []);
|
|
786
|
+
const { _textualIncludeClosures } = require('./callers');
|
|
787
|
+
for (const file of _textualIncludeClosures(index, filePath).all) {
|
|
788
|
+
for (const name of state.relevantFiles.get(file) || []) names.add(name);
|
|
789
|
+
}
|
|
790
|
+
return [...names].sort(codeUnitCompare);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** Preprocessing tokens of a file outside directive lines. */
|
|
794
|
+
function codeTokens(content, lines) {
|
|
795
|
+
const directive = new Uint8Array(lines.length + 1);
|
|
796
|
+
for (let row = 0; row < lines.length; row++) {
|
|
797
|
+
const line = lines[row];
|
|
798
|
+
let k = 0;
|
|
799
|
+
while (k < line.length && (line[k] === ' ' || line[k] === '\t')) k++;
|
|
800
|
+
if (line[k] !== '#') continue;
|
|
801
|
+
directive[row] = 1;
|
|
802
|
+
while (row + 1 < lines.length && /\\\s*$/.test(lines[row])) directive[++row] = 1;
|
|
803
|
+
}
|
|
804
|
+
const starts = [0];
|
|
805
|
+
for (let row = 0; row < lines.length; row++) starts.push(starts[row] + lines[row].length + 1);
|
|
806
|
+
const tokens = [];
|
|
807
|
+
let row = 0;
|
|
808
|
+
for (const token of lexPP(content)) {
|
|
809
|
+
while (row + 1 < starts.length && starts[row + 1] <= token.start) row++;
|
|
810
|
+
if (!directive[row]) tokens.push(token);
|
|
811
|
+
}
|
|
812
|
+
return tokens;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function enclosingFunctionAt(fileEntry, line) {
|
|
816
|
+
let best = null;
|
|
817
|
+
for (const symbol of fileEntry?.symbols || []) {
|
|
818
|
+
if (symbol.type === 'macro' || symbol.startLine > line || symbol.endLine < line) continue;
|
|
819
|
+
if (!['function', 'method', 'constructor', 'destructor', 'static'].includes(symbol.type) &&
|
|
820
|
+
!symbol.isMethod) continue;
|
|
821
|
+
if (!best || symbol.startLine >= best.startLine) best = symbol;
|
|
822
|
+
}
|
|
823
|
+
return best ? {
|
|
824
|
+
name: best.name, startLine: best.startLine, endLine: best.endLine,
|
|
825
|
+
...(best.className && { className: best.className }),
|
|
826
|
+
} : null;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Expand one invocation (tokens of `NAME ( args )`) in the given site
|
|
831
|
+
* context. Returns { calls, refs, defs } or throws BlindExpansion.
|
|
832
|
+
*/
|
|
833
|
+
function expandInvocation(index, state, site, tokens, parser, preferred, trailing = []) {
|
|
834
|
+
const scope = definitionsInScope(index, state, site, tokens[0].v);
|
|
835
|
+
const functionDefs = scope.defs.filter(def => def.functionLike);
|
|
836
|
+
if (functionDefs.length === 0) return null;
|
|
837
|
+
const split = splitArguments(tokens, 1);
|
|
838
|
+
if (!split) throw new BlindExpansion('unbalanced-arguments');
|
|
839
|
+
// Alternatives whose parameter list cannot take these arguments are not
|
|
840
|
+
// the definition in effect; no fitting alternative is a blind site.
|
|
841
|
+
const fitting = functionDefs.filter(def => arityFits(def, split.args));
|
|
842
|
+
if (fitting.length === 0) throw new BlindExpansion('argument-count');
|
|
843
|
+
const variants = [];
|
|
844
|
+
for (const def of fitting) {
|
|
845
|
+
site.budget = MAX_TOKENS;
|
|
846
|
+
site.usedDefs.add(defKey(def));
|
|
847
|
+
const expanded = substitute(index, state, site, def, split.args, new Set(), 0);
|
|
848
|
+
// Parsing is the expensive step. Only an expansion that produced a
|
|
849
|
+
// project name by pasting, or moved an argument identifier into
|
|
850
|
+
// call position, can yield an edge the source does not spell.
|
|
851
|
+
// The preprocessor rescans the expansion together with the source
|
|
852
|
+
// that follows it: `CAT(uv_, run)()` calls uv_run with the `()`
|
|
853
|
+
// written after the invocation.
|
|
854
|
+
const stream = trailing.length > 0 ? expanded.concat(trailing) : expanded;
|
|
855
|
+
variants.push({ def, ...(producesProjectName(index, stream)
|
|
856
|
+
? classifyExpansionMemo(state, parser, stream, preferred) : { calls: [], refs: [] }) });
|
|
857
|
+
}
|
|
858
|
+
// Top-level alternatives are all expanded; a name every alternative
|
|
859
|
+
// produces is unambiguous. Unexpanded alternatives (object-like, over
|
|
860
|
+
// the variant cap) or ambiguous nested macros leave every name open.
|
|
861
|
+
const ambiguous = scope.truncated || functionDefs.length !== scope.defs.length || site.ambiguous;
|
|
862
|
+
return { variants, ambiguous };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function fileExpansion(index, filePath, rawCalls) {
|
|
866
|
+
const state = expansionState(index);
|
|
867
|
+
let fileState = state.byFile.get(filePath);
|
|
868
|
+
if (fileState && fileState.raw === rawCalls) return fileState;
|
|
869
|
+
fileState = {
|
|
870
|
+
raw: rawCalls, calls: rawCalls, refs: [], blind: [], usedDefs: new Set(), sites: 0,
|
|
871
|
+
records: [], targets: [],
|
|
872
|
+
};
|
|
873
|
+
state.byFile.set(filePath, fileState);
|
|
874
|
+
state.summary = null;
|
|
875
|
+
if (state.relevant.size === 0 || !Array.isArray(rawCalls)) return fileState;
|
|
876
|
+
const fileEntry = index.files.get(filePath);
|
|
877
|
+
if (!langTraits(fileEntry?.language)?.textualIncludes) return fileState;
|
|
878
|
+
const reused = persistedFileExpansion(index, state, fileEntry);
|
|
879
|
+
if (reused) {
|
|
880
|
+
Object.assign(fileState, reused, {
|
|
881
|
+
calls: reused.records.length > 0 ? rawCalls.concat(reused.records) : rawCalls,
|
|
882
|
+
});
|
|
883
|
+
state.persistOut.set(fileEntry.relativePath, persistRecord(fileEntry, fileState));
|
|
884
|
+
return fileState;
|
|
885
|
+
}
|
|
886
|
+
computeFileExpansion(index, state, filePath, rawCalls, fileState, fileEntry);
|
|
887
|
+
state.persistOut.set(fileEntry.relativePath, persistRecord(fileEntry, fileState));
|
|
888
|
+
index.macroExpansionDirty = true;
|
|
889
|
+
return fileState;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** Persisted shape of one file's expansion (index cache, fix #362). */
|
|
893
|
+
function persistRecord(fileEntry, fileState) {
|
|
894
|
+
return {
|
|
895
|
+
hash: fileEntry.hash,
|
|
896
|
+
targets: fileState.targets,
|
|
897
|
+
records: fileState.records,
|
|
898
|
+
refs: fileState.refs,
|
|
899
|
+
blind: fileState.blind,
|
|
900
|
+
usedDefs: [...fileState.usedDefs].sort(codeUnitCompare),
|
|
901
|
+
sites: fileState.sites,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** Version token of an include target: indexed content hash, else size+mtime. */
|
|
906
|
+
function targetVersion(index, target) {
|
|
907
|
+
const entry = index.files.get(target);
|
|
908
|
+
if (entry) return `h:${entry.hash}`;
|
|
909
|
+
try {
|
|
910
|
+
const stat = fs.statSync(target);
|
|
911
|
+
return `s:${stat.size}:${stat.mtimeMs}`;
|
|
912
|
+
} catch {
|
|
913
|
+
return 'missing';
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* A persisted expansion is reused only when every input is unchanged: the
|
|
919
|
+
* file's content hash, each include target it read, and the project-wide
|
|
920
|
+
* signature of macro definitions and the include graph.
|
|
921
|
+
*/
|
|
922
|
+
function persistedFileExpansion(index, state, fileEntry) {
|
|
923
|
+
const persisted = index._macroExpansionPersisted;
|
|
924
|
+
if (!persisted || persisted.signature !== expansionSignature(index, state)) return null;
|
|
925
|
+
const entry = persisted.byFile?.[fileEntry.relativePath];
|
|
926
|
+
if (!entry || entry.hash !== fileEntry.hash) return null;
|
|
927
|
+
for (const [rel, version] of entry.targets || []) {
|
|
928
|
+
if (targetVersion(index, path.join(index.root, rel)) !== version) return null;
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
records: entry.records || [],
|
|
932
|
+
refs: entry.refs || [],
|
|
933
|
+
blind: entry.blind || [],
|
|
934
|
+
usedDefs: new Set(entry.usedDefs || []),
|
|
935
|
+
sites: entry.sites || 0,
|
|
936
|
+
targets: entry.targets || [],
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** Hash of every C/C++ macro definition and the C/C++ include graph. */
|
|
941
|
+
function expansionSignature(index, state) {
|
|
942
|
+
if (state.signature) return state.signature;
|
|
943
|
+
const hash = crypto.createHash('md5');
|
|
944
|
+
for (const name of [...state.macroDefs.keys()].sort(codeUnitCompare)) {
|
|
945
|
+
for (const def of state.macroDefs.get(name)) {
|
|
946
|
+
hash.update(`${name}\0${def.relativePath}\0${def.startLine}\0${def.endLine}\0` +
|
|
947
|
+
`${def.functionLike ? 1 : 0}\0${(def.ppParams || []).join(',')}\0` +
|
|
948
|
+
`${def.ppVariadic ? 1 : 0}\0${def.ppBody ?? ''}\n`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const files = [...index.files.values()]
|
|
952
|
+
.filter(entry => langTraits(entry.language)?.textualIncludes)
|
|
953
|
+
.map(entry => entry.path).sort(codeUnitCompare);
|
|
954
|
+
for (const file of files) {
|
|
955
|
+
const edges = [...(index.importGraph?.get(file) || [])].sort(codeUnitCompare)
|
|
956
|
+
.map(target => path.relative(index.root, target));
|
|
957
|
+
hash.update(`${path.relative(index.root, file)}>${edges.join(',')}\n`);
|
|
958
|
+
}
|
|
959
|
+
state.signature = hash.digest('hex');
|
|
960
|
+
return state.signature;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Index-cache payload: every file expansion known this session. */
|
|
964
|
+
function persistableMacroExpansion(index) {
|
|
965
|
+
const state = index._macroExpansion;
|
|
966
|
+
const persisted = index._macroExpansionPersisted;
|
|
967
|
+
if (!state) return persisted || null;
|
|
968
|
+
const byFile = {};
|
|
969
|
+
// Carry forward persisted entries this session did not touch; their
|
|
970
|
+
// own validity checks run when they are next used.
|
|
971
|
+
if (persisted && persisted.signature === expansionSignature(index, state)) {
|
|
972
|
+
for (const [rel, entry] of Object.entries(persisted.byFile || {})) byFile[rel] = entry;
|
|
973
|
+
}
|
|
974
|
+
for (const [rel, entry] of state.persistOut) byFile[rel] = entry;
|
|
975
|
+
return { signature: expansionSignature(index, state), byFile };
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function computeFileExpansion(index, state, filePath, rawCalls, fileState, fileEntry) {
|
|
979
|
+
// Invocation recognition is the preprocessor's own: a macro name token
|
|
980
|
+
// followed by `(` outside directive lines. Call records are not enough:
|
|
981
|
+
// statement-position invocations (`switch (e) { LIST(X) default: ... }`)
|
|
982
|
+
// are recovered as declarations or blanked by macro recovery.
|
|
983
|
+
const visibleRelevant = relevantNamesVisibleFrom(index, state, filePath);
|
|
984
|
+
// Quoted includes whose text may invoke a macro this file can see but
|
|
985
|
+
// the included file cannot (`#define X ...` + `#include "list.def"`, or
|
|
986
|
+
// a list header included after the header defining its macro).
|
|
987
|
+
const includeSites = visibleRelevant.length === 0 ? [] : (fileEntry.importDetails || [])
|
|
988
|
+
.filter(detail => detail.type === 'include' && Number.isInteger(detail.line));
|
|
989
|
+
if (visibleRelevant.length === 0 && includeSites.length === 0) return fileState;
|
|
990
|
+
|
|
991
|
+
let content;
|
|
992
|
+
try {
|
|
993
|
+
content = index._readFile(filePath);
|
|
994
|
+
} catch {
|
|
995
|
+
return fileState;
|
|
996
|
+
}
|
|
997
|
+
// Own words are needed once; only include targets are revisited.
|
|
998
|
+
const namesHere = namesSpelled(state, filePath, content, visibleRelevant);
|
|
999
|
+
if (namesHere.size === 0 && includeSites.length === 0) return fileState;
|
|
1000
|
+
const lines = content.split('\n');
|
|
1001
|
+
const starts = lineStarts(content);
|
|
1002
|
+
const parser = getParser(fileEntry.language);
|
|
1003
|
+
const records = [];
|
|
1004
|
+
// An argument-substituted call the parser already recorded at its own
|
|
1005
|
+
// spelling (`X(CLOSE, uv__fs_close(fd))`) is not a second call.
|
|
1006
|
+
let seenKeys = null; // built on first use: most files emit nothing (fix #365)
|
|
1007
|
+
const seen = {
|
|
1008
|
+
has: key => (seenKeys ||= new Set(rawCalls.map(call =>
|
|
1009
|
+
`${call.name}\0${call.line}\0${call.column}`))).has(key),
|
|
1010
|
+
add: key => (seenKeys ||= new Set(rawCalls.map(call =>
|
|
1011
|
+
`${call.name}\0${call.line}\0${call.column}`))).add(key),
|
|
1012
|
+
};
|
|
1013
|
+
const makeSite = line => ({
|
|
1014
|
+
file: filePath, line, lines, fileState, lookupMemo: new Map(),
|
|
1015
|
+
usedDefs: fileState.usedDefs, budget: MAX_TOKENS, ambiguous: false,
|
|
1016
|
+
});
|
|
1017
|
+
const emit = (result, invocationName, anchor, enclosingFunction) => {
|
|
1018
|
+
const byName = new Map();
|
|
1019
|
+
for (const variant of result.variants) {
|
|
1020
|
+
for (const call of variant.calls) {
|
|
1021
|
+
const token = call.token;
|
|
1022
|
+
const position = token.o === 'arg' && Number.isInteger(token.pos)
|
|
1023
|
+
? positionOf(starts, token.pos) : anchor;
|
|
1024
|
+
const key = `${token.v}\0${position.line}\0${position.column}`;
|
|
1025
|
+
if (!byName.has(key)) {
|
|
1026
|
+
byName.set(key, { call, token, position, defs: new Set(), count: 0 });
|
|
1027
|
+
}
|
|
1028
|
+
const entry = byName.get(key);
|
|
1029
|
+
if (!entry.defs.has(variant.def)) entry.count++;
|
|
1030
|
+
entry.defs.add(variant.def);
|
|
1031
|
+
}
|
|
1032
|
+
for (const ref of variant.refs) fileState.refs.push(ref.token.v);
|
|
1033
|
+
}
|
|
1034
|
+
for (const [key, entry] of byName) {
|
|
1035
|
+
if (seen.has(key)) continue;
|
|
1036
|
+
// A macro name the expansion could not expand further is an
|
|
1037
|
+
// unresolved invocation, not a call of a function.
|
|
1038
|
+
if (!entry.token.noexpand && state.macroDefs.has(entry.token.v) &&
|
|
1039
|
+
(index.symbols.get(entry.token.v) || []).every(def => def.type === 'macro')) continue;
|
|
1040
|
+
seen.add(key);
|
|
1041
|
+
const def = [...entry.defs][0];
|
|
1042
|
+
const ambiguous = result.ambiguous || entry.count !== result.variants.length;
|
|
1043
|
+
records.push({
|
|
1044
|
+
name: entry.token.v,
|
|
1045
|
+
line: entry.position.line,
|
|
1046
|
+
column: entry.position.column,
|
|
1047
|
+
isMethod: false,
|
|
1048
|
+
argCount: entry.call.argCount,
|
|
1049
|
+
...(enclosingFunction && { enclosingFunction }),
|
|
1050
|
+
macroExpansion: {
|
|
1051
|
+
macro: invocationName,
|
|
1052
|
+
definition: defKey(def),
|
|
1053
|
+
origin: entry.token.o === 'arg' ? 'argument' : 'paste',
|
|
1054
|
+
...(ambiguous && { ambiguous: true }),
|
|
1055
|
+
},
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
if (namesHere.size > 0) {
|
|
1060
|
+
const tokens = codeTokens(content, lines);
|
|
1061
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1062
|
+
const t = tokens[i];
|
|
1063
|
+
if (t.k !== 'id' || !namesHere.has(t.v) || tokens[i + 1]?.v !== '(') continue;
|
|
1064
|
+
// A macro invoked inside another invocation's arguments is
|
|
1065
|
+
// expanded with that invocation (argument pre-expansion).
|
|
1066
|
+
const split = splitArguments(tokens, i + 1);
|
|
1067
|
+
const anchor = positionOf(starts, t.start);
|
|
1068
|
+
if (!split) {
|
|
1069
|
+
fileState.blind.push({ line: anchor.line, macro: t.v, reason: 'unbalanced-arguments' });
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const invocation = [];
|
|
1073
|
+
for (let j = i; j <= split.close; j++) {
|
|
1074
|
+
const token = tokens[j];
|
|
1075
|
+
invocation.push({
|
|
1076
|
+
v: token.v, k: token.k, o: j === i ? 'site' : 'arg', pos: token.start,
|
|
1077
|
+
// Already spelled as a call where it is written: the
|
|
1078
|
+
// parser recorded that call at this position.
|
|
1079
|
+
...(token.k === 'id' && tokens[j + 1]?.v === '(' && { srcCall: true }),
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
const site = makeSite(anchor.line);
|
|
1083
|
+
const enclosingFunction = enclosingFunctionAt(fileEntry, anchor.line);
|
|
1084
|
+
try {
|
|
1085
|
+
const after = splitArguments(tokens, split.close + 1);
|
|
1086
|
+
const trailing = after
|
|
1087
|
+
? tokens.slice(split.close + 1, after.close + 1).map(token =>
|
|
1088
|
+
({ v: token.v, k: token.k, o: 'trail' }))
|
|
1089
|
+
: [];
|
|
1090
|
+
const result = expandInvocation(index, state, site, invocation, parser,
|
|
1091
|
+
enclosingFunction ? ['statement', 'initializer', 'file']
|
|
1092
|
+
: ['file', 'initializer', 'statement'], trailing);
|
|
1093
|
+
if (!result) continue;
|
|
1094
|
+
fileState.sites++;
|
|
1095
|
+
emit(result, t.v, anchor, enclosingFunction);
|
|
1096
|
+
i = split.close;
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
if (!(error instanceof BlindExpansion)) throw error;
|
|
1099
|
+
fileState.blind.push({ line: anchor.line, macro: t.v, reason: error.reason });
|
|
1100
|
+
i = split.close;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Includer-defined macros: `#define X(n) {#n, handle_##n},` then
|
|
1106
|
+
// `#include "list.def"` expands the included file's `X(...)` lines with
|
|
1107
|
+
// the includer's definition (the X-macro table pattern).
|
|
1108
|
+
for (const detail of includeSites) {
|
|
1109
|
+
const resolvedRel = fileEntry.moduleResolved?.[detail.module];
|
|
1110
|
+
const target = resolvedRel ? path.join(index.root, resolvedRel)
|
|
1111
|
+
: path.resolve(path.dirname(filePath), detail.module);
|
|
1112
|
+
if (!target.startsWith(index.root + path.sep) || target === filePath) continue;
|
|
1113
|
+
let text;
|
|
1114
|
+
try {
|
|
1115
|
+
text = index.files.has(target) ? index._readFile(target) : fs.readFileSync(target, 'utf-8');
|
|
1116
|
+
} catch {
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
fileState.targets.push([path.relative(index.root, target), targetVersion(index, target)]);
|
|
1120
|
+
const targetWords = wordsOf(state, target, text);
|
|
1121
|
+
const candidates = visibleRelevant.filter(name => targetWords.has(name));
|
|
1122
|
+
if (candidates.length === 0) continue;
|
|
1123
|
+
const ownNames = new Set(relevantNamesVisibleFrom(index, state, target));
|
|
1124
|
+
const names = new Set(candidates.filter(name => !ownNames.has(name)));
|
|
1125
|
+
if (names.size === 0) continue;
|
|
1126
|
+
let tokens = state.includeTokens.get(target);
|
|
1127
|
+
if (!tokens) {
|
|
1128
|
+
tokens = codeTokens(text, text.split('\n'));
|
|
1129
|
+
state.includeTokens.set(target, tokens);
|
|
1130
|
+
}
|
|
1131
|
+
const enclosingFunction = enclosingFunctionAt(fileEntry, detail.line);
|
|
1132
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1133
|
+
const t = tokens[i];
|
|
1134
|
+
if (t.k !== 'id' || !names.has(t.v) || tokens[i + 1]?.v !== '(') continue;
|
|
1135
|
+
const split = splitArguments(tokens, i + 1);
|
|
1136
|
+
const site = makeSite(detail.line);
|
|
1137
|
+
if (!split) {
|
|
1138
|
+
fileState.blind.push({ line: detail.line, macro: t.v, reason: 'unbalanced-arguments' });
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
// Tokens of an unindexed included file (`list.def`) are seen by no
|
|
1142
|
+
// other scan, so identifiers it passes in value position count
|
|
1143
|
+
// as produced references too.
|
|
1144
|
+
const foreign = !index.files.has(target);
|
|
1145
|
+
const invocation = [];
|
|
1146
|
+
for (let j = i; j <= split.close; j++) {
|
|
1147
|
+
const token = tokens[j];
|
|
1148
|
+
invocation.push(j === i ? { ...token, o: 'site' } : {
|
|
1149
|
+
...token, o: 'arg',
|
|
1150
|
+
...(foreign && { foreign: true }),
|
|
1151
|
+
...(!foreign && token.k === 'id' && tokens[j + 1]?.v === '(' && { srcCall: true }),
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
try {
|
|
1155
|
+
const result = expandInvocation(index, state, site, invocation, parser,
|
|
1156
|
+
enclosingFunction ? ['statement', 'initializer', 'file']
|
|
1157
|
+
: ['initializer', 'file', 'statement']);
|
|
1158
|
+
if (result) {
|
|
1159
|
+
fileState.sites++;
|
|
1160
|
+
emit(result, t.v, { line: detail.line, column: 0 }, enclosingFunction);
|
|
1161
|
+
}
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
if (!(error instanceof BlindExpansion)) throw error;
|
|
1164
|
+
fileState.blind.push({ line: detail.line, macro: t.v, reason: error.reason });
|
|
1165
|
+
}
|
|
1166
|
+
i = split.close;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (records.length > 0) {
|
|
1170
|
+
records.sort((a, b) => a.line - b.line || a.column - b.column ||
|
|
1171
|
+
codeUnitCompare(a.name, b.name));
|
|
1172
|
+
fileState.records = records;
|
|
1173
|
+
fileState.calls = rawCalls.concat(records);
|
|
1174
|
+
}
|
|
1175
|
+
fileState.refs = [...new Set(fileState.refs)].sort(codeUnitCompare);
|
|
1176
|
+
return fileState;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Paste chains of a function-like macro: each `a ## b ## c` run that
|
|
1181
|
+
* involves a parameter, as operands `{ param }` / `{ literal }`.
|
|
1182
|
+
*/
|
|
1183
|
+
function pasteChains(params, body) {
|
|
1184
|
+
const tokens = lexPP(body);
|
|
1185
|
+
const paramSet = new Set(params);
|
|
1186
|
+
const chains = [];
|
|
1187
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1188
|
+
if (tokens[i + 1]?.v !== '##') continue;
|
|
1189
|
+
const operands = [tokens[i]];
|
|
1190
|
+
let j = i + 1;
|
|
1191
|
+
while (tokens[j]?.v === '##' && tokens[j + 1]) {
|
|
1192
|
+
operands.push(tokens[j + 1]);
|
|
1193
|
+
j += 2;
|
|
1194
|
+
}
|
|
1195
|
+
i = j - 1;
|
|
1196
|
+
if (operands.some(op => op.k !== 'id' && op.k !== 'num')) continue;
|
|
1197
|
+
if (!operands.some(op => op.k === 'id' && paramSet.has(op.v))) continue;
|
|
1198
|
+
chains.push(operands.map(op => (op.k === 'id' && paramSet.has(op.v)
|
|
1199
|
+
? { param: op.v } : { literal: op.v })));
|
|
1200
|
+
}
|
|
1201
|
+
return chains;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Arguments of the invocation whose `(` is at `open`, read from the raw
|
|
1206
|
+
* text: [argText] with whitespace removed, or null when the text holds a
|
|
1207
|
+
* literal or a comment there (the caller then lexes the file) or the list
|
|
1208
|
+
* does not close within a bound.
|
|
1209
|
+
*/
|
|
1210
|
+
function rawInvocationArguments(content, open) {
|
|
1211
|
+
const args = [];
|
|
1212
|
+
let depth = 0;
|
|
1213
|
+
let current = '';
|
|
1214
|
+
for (let k = open + 1; k < content.length && k - open < 4096; k++) {
|
|
1215
|
+
const ch = content[k];
|
|
1216
|
+
if (ch === '"' || ch === "'" || (ch === '/' && (content[k + 1] === '/' || content[k + 1] === '*'))) return null;
|
|
1217
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
1218
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
1219
|
+
if (depth === 0) {
|
|
1220
|
+
if (ch !== ')') return null;
|
|
1221
|
+
args.push(current);
|
|
1222
|
+
return args;
|
|
1223
|
+
}
|
|
1224
|
+
depth--;
|
|
1225
|
+
} else if (ch === ',' && depth === 0) {
|
|
1226
|
+
args.push(current);
|
|
1227
|
+
current = '';
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
if (!/\s/.test(ch)) current += ch;
|
|
1231
|
+
}
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** May an invocation in `content` of a macro in `byName` paste `name`? */
|
|
1236
|
+
function mayPasteName(content, byName, name) {
|
|
1237
|
+
for (const [macroName, macros] of byName) {
|
|
1238
|
+
for (let at = content.indexOf(macroName); at >= 0; at = content.indexOf(macroName, at + 1)) {
|
|
1239
|
+
if (at > 0 && /[A-Za-z0-9_]/.test(content[at - 1])) continue;
|
|
1240
|
+
let k = at + macroName.length;
|
|
1241
|
+
if (/[A-Za-z0-9_]/.test(content[k] || '')) continue;
|
|
1242
|
+
while (k < content.length && (content[k] === ' ' || content[k] === '\t')) k++;
|
|
1243
|
+
if (content[k] !== '(') continue;
|
|
1244
|
+
const args = rawInvocationArguments(content, k);
|
|
1245
|
+
if (!args) return true;
|
|
1246
|
+
for (const macro of macros) {
|
|
1247
|
+
const params = macro.symbol.ppParams;
|
|
1248
|
+
const argText = param => {
|
|
1249
|
+
const index = params.indexOf(param);
|
|
1250
|
+
return index >= 0 && args[index] ? args[index] : '';
|
|
1251
|
+
};
|
|
1252
|
+
if (macro.chains.some(chain => chain.map(op => op.literal ?? argText(op.param)).join('') === name)) {
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* fix #396: where token pasting can spell `name` without writing it. Returns
|
|
1263
|
+
* { macros: [{ symbol, spelling }], sites: [{ file, line, macro }] }: the
|
|
1264
|
+
* project function-like macros whose replacement list pastes a parameter
|
|
1265
|
+
* into a token that can be `name` (`struct sdshdr##T`), and their
|
|
1266
|
+
* invocations (outside directive lines) whose arguments make it exactly
|
|
1267
|
+
* `name` (`SDS_HDR_VAR(8, s)`). Arguments next to `##` are pasted as
|
|
1268
|
+
* written (C11 6.10.3.3), so no expansion is needed. Memoized per operation.
|
|
1269
|
+
*/
|
|
1270
|
+
function pastedNameSites(index, name) {
|
|
1271
|
+
const memo = index._opMemo?.('pastedNameSites', () => new Map());
|
|
1272
|
+
if (memo?.has(name)) return memo.get(name);
|
|
1273
|
+
const { matchesPastePattern } = require('../languages/c-preprocessor');
|
|
1274
|
+
const macros = [];
|
|
1275
|
+
for (const defs of index.symbols.values()) {
|
|
1276
|
+
for (const symbol of defs) {
|
|
1277
|
+
if (symbol.type !== 'macro' || !Array.isArray(symbol.ppParams) ||
|
|
1278
|
+
typeof symbol.ppBody !== 'string' || !symbol.ppBody.includes('##')) continue;
|
|
1279
|
+
const chains = pasteChains(symbol.ppParams, symbol.ppBody).filter(chain =>
|
|
1280
|
+
matchesPastePattern(chain.map(op => op.literal ?? null), name));
|
|
1281
|
+
if (chains.length === 0) continue;
|
|
1282
|
+
macros.push({
|
|
1283
|
+
symbol, chains,
|
|
1284
|
+
spelling: chains[0].map(op => op.literal ?? op.param).join(' ## '),
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
macros.sort((a, b) => codeUnitCompare(a.symbol.file, b.symbol.file) ||
|
|
1289
|
+
a.symbol.startLine - b.symbol.startLine);
|
|
1290
|
+
const sites = [];
|
|
1291
|
+
const byName = new Map();
|
|
1292
|
+
for (const macro of macros) {
|
|
1293
|
+
if (!byName.has(macro.symbol.name)) byName.set(macro.symbol.name, []);
|
|
1294
|
+
byName.get(macro.symbol.name).push(macro);
|
|
1295
|
+
}
|
|
1296
|
+
if (byName.size > 0) {
|
|
1297
|
+
const files = [...index.files.keys()].sort(codeUnitCompare);
|
|
1298
|
+
for (const file of files) {
|
|
1299
|
+
const entry = index.files.get(file);
|
|
1300
|
+
if (!langTraits(entry?.language)?.textualIncludes) continue;
|
|
1301
|
+
let content;
|
|
1302
|
+
try {
|
|
1303
|
+
content = index._readFile(file);
|
|
1304
|
+
} catch {
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
if (![...byName.keys()].some(macroName => content.includes(macroName))) continue;
|
|
1308
|
+
// Lex the file only when an invocation there may paste `name`:
|
|
1309
|
+
// most files invoke a generic pasting macro with other arguments.
|
|
1310
|
+
if (!mayPasteName(content, byName, name)) continue;
|
|
1311
|
+
const lines = content.split('\n');
|
|
1312
|
+
const starts = lineStarts(content);
|
|
1313
|
+
const tokens = codeTokens(content, lines);
|
|
1314
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1315
|
+
const candidates = byName.get(tokens[i].v);
|
|
1316
|
+
if (!candidates || tokens[i + 1]?.v !== '(') continue;
|
|
1317
|
+
const split = splitArguments(tokens, i + 1);
|
|
1318
|
+
if (!split) continue;
|
|
1319
|
+
const line = positionOf(starts, tokens[i].start).line;
|
|
1320
|
+
for (const macro of candidates) {
|
|
1321
|
+
const params = macro.symbol.ppParams;
|
|
1322
|
+
const argText = param => {
|
|
1323
|
+
const at = params.indexOf(param);
|
|
1324
|
+
return at >= 0 && split.args[at] ? split.args[at].map(token => token.v).join('') : '';
|
|
1325
|
+
};
|
|
1326
|
+
if (macro.chains.some(chain => chain.map(op => op.literal ?? argText(op.param)).join('') === name)) {
|
|
1327
|
+
sites.push({ file, line, macro: macro.symbol.name });
|
|
1328
|
+
break;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
i = split.close;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const result = { macros, sites };
|
|
1336
|
+
memo?.set(name, result);
|
|
1337
|
+
return result;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** Calls of a file including the call records its macro invocations expand to. */
|
|
1341
|
+
function withMacroExpansion(index, filePath, rawCalls, options = {}) {
|
|
1342
|
+
if (!Array.isArray(rawCalls) || !index?.symbols) return rawCalls;
|
|
1343
|
+
// Rust macro_rules! expansions (fix #374) derive their calls on first use.
|
|
1344
|
+
if (index.files.get(filePath)?.rustMacroExpansion?.callsPending) {
|
|
1345
|
+
return require('./rust-macro-expansion').rustExpandedCalls(index, filePath, rawCalls, options);
|
|
1346
|
+
}
|
|
1347
|
+
if (!langTraits(index.files.get(filePath)?.language)?.textualIncludes) return rawCalls;
|
|
1348
|
+
return fileExpansion(index, filePath, rawCalls).calls;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Project-wide inventory for deadcode and health: expanded invocation sites,
|
|
1353
|
+
* names produced in value position, blind invocations, and the paste
|
|
1354
|
+
* patterns whose produced names cannot be enumerated (a pasting macro with
|
|
1355
|
+
* a blind invocation or with no expandable invocation at all).
|
|
1356
|
+
*/
|
|
1357
|
+
function macroExpansionSummary(index) {
|
|
1358
|
+
const state = expansionState(index);
|
|
1359
|
+
if (state.summary) return state.summary;
|
|
1360
|
+
const summary = {
|
|
1361
|
+
sites: 0, calls: 0, refNames: new Set(),
|
|
1362
|
+
blind: { count: 0, fileCount: 0, files: [], sample: [] },
|
|
1363
|
+
patterns: [],
|
|
1364
|
+
};
|
|
1365
|
+
if (state.relevant.size === 0) {
|
|
1366
|
+
state.summary = summary;
|
|
1367
|
+
return summary;
|
|
1368
|
+
}
|
|
1369
|
+
const { getCachedCalls } = require('./callers');
|
|
1370
|
+
const usedDefs = new Set();
|
|
1371
|
+
const blindDefs = new Set();
|
|
1372
|
+
const files = [...index.files.keys()].sort(codeUnitCompare);
|
|
1373
|
+
for (const filePath of files) {
|
|
1374
|
+
const fileEntry = index.files.get(filePath);
|
|
1375
|
+
if (!langTraits(fileEntry?.language)?.textualIncludes) continue;
|
|
1376
|
+
getCachedCalls(index, filePath);
|
|
1377
|
+
const fileState = state.byFile.get(filePath);
|
|
1378
|
+
if (!fileState) continue;
|
|
1379
|
+
summary.sites += fileState.sites;
|
|
1380
|
+
summary.calls += fileState.calls.length - fileState.raw.length;
|
|
1381
|
+
for (const name of fileState.refs) summary.refNames.add(name);
|
|
1382
|
+
for (const key of fileState.usedDefs) usedDefs.add(key);
|
|
1383
|
+
if (fileState.blind.length > 0) {
|
|
1384
|
+
summary.blind.count += fileState.blind.length;
|
|
1385
|
+
summary.blind.fileCount++;
|
|
1386
|
+
if (summary.blind.files.length < 10) summary.blind.files.push(fileEntry.relativePath);
|
|
1387
|
+
for (const site of fileState.blind) {
|
|
1388
|
+
blindDefs.add(site.macro);
|
|
1389
|
+
if (summary.blind.sample.length < 10) {
|
|
1390
|
+
summary.blind.sample.push({ file: fileEntry.relativePath, ...site });
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
for (const [name, defs] of state.templates) {
|
|
1396
|
+
for (const def of defs) {
|
|
1397
|
+
// A pattern no project name matches withholds nothing.
|
|
1398
|
+
const patterns = pastePatterns({ params: def.ppParams || [], body: def.ppBody })
|
|
1399
|
+
.filter(pattern => patternSpellsProjectName(index, pattern));
|
|
1400
|
+
if (patterns.length === 0) continue;
|
|
1401
|
+
const unexpanded = !usedDefs.has(defKey(def));
|
|
1402
|
+
if (!unexpanded && !blindDefs.has(name)) continue;
|
|
1403
|
+
for (const pattern of patterns) {
|
|
1404
|
+
summary.patterns.push({
|
|
1405
|
+
pattern, macro: name, file: def.relativePath, line: def.startLine,
|
|
1406
|
+
reason: unexpanded ? 'no-expanded-invocation' : 'blind-invocation',
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
state.summary = summary;
|
|
1412
|
+
return summary;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
module.exports = { withMacroExpansion, macroExpansionSummary, expansionState, persistableMacroExpansion,
|
|
1416
|
+
macroDefinitionsAt, macroBodyTokens, codeTokens, pastedNameSites };
|