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
@@ -0,0 +1,380 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Function-like macro invocations the C/C++ grammar misreads as declarations
5
+ * (fix #385).
6
+ *
7
+ * A function-like macro name followed by `(` is an invocation wherever the
8
+ * definition is in effect (C11 6.10.3p10). tree-sitter knows no macros, so an
9
+ * invocation in declaration or statement position is parsed as a
10
+ * declaration: `ERROR_DEF(Base, Name)` in a class body became a method named
11
+ * ERROR_DEF (and, without a trailing `;`, swallowed the class head),
12
+ * `LOG_ONCE(init());` in a function body declared a function `init`
13
+ * returning LOG_ONCE, `CATCH_ALL(...) { }` a nested function definition.
14
+ *
15
+ * This module reads the file's own `#define NAME(...)` / `#undef NAME`
16
+ * directives (the preprocessor's line grammar), finds the invocations of
17
+ * macros in effect that the tree holds in those declaration shapes, and
18
+ * gives recovery the byte ranges to blank:
19
+ *
20
+ * statement position the macro name only when a `;` follows, so its
21
+ * arguments stay expressions (their calls stay
22
+ * calls); the whole invocation otherwise
23
+ * declaration/member the whole invocation; its declarations are derived
24
+ * from the expansion (the replacement list is known)
25
+ * file-scope with body `NAME(args) { ... }` is left as parsed (the body
26
+ * belongs to whatever the expansion declares); the
27
+ * definition is marked as a macro invocation
28
+ *
29
+ * Invocations of macros defined in other files are recognized at index time
30
+ * from the same shapes (see the `macroInvocation` fact).
31
+ */
32
+
33
+ const { lexPP, splitArguments, functionMacroTemplate } = require('./c-preprocessor');
34
+
35
+ const DECLARATION_CONTAINERS = new Set(['declaration', 'field_declaration', 'function_definition']);
36
+ const SCOPE_BOUNDARIES = new Set([
37
+ 'translation_unit', 'field_declaration_list', 'declaration_list', 'compound_statement',
38
+ ]);
39
+ const MAX_EXPANSION_CHARS = 64 * 1024;
40
+ const EXPRESSION_CONTINUATION = new Set(['=', '(', ',', '?', '!', '&', '|', '+', '-', '*', '/', '%',
41
+ '<', '>', '[', '^', '~', '.']);
42
+
43
+ /**
44
+ * Function-like macro definitions and #undef lines of a file, read from its
45
+ * directive lines. Returns null when the file defines no function-like macro.
46
+ */
47
+ function directiveFunctionMacros(code) {
48
+ if (!code.includes('define')) return null;
49
+ const defs = new Map();
50
+ const undefs = new Map();
51
+ const directive = /^[ \t]*#[ \t]*(define|undef)[ \t]+([A-Za-z_][A-Za-z0-9_]*)/gm;
52
+ let row = 1;
53
+ let counted = 0;
54
+ const rowAt = offset => {
55
+ for (let i = code.indexOf('\n', counted); i !== -1 && i < offset; i = code.indexOf('\n', i + 1)) row++;
56
+ counted = offset;
57
+ return row;
58
+ };
59
+ for (let match = directive.exec(code); match; match = directive.exec(code)) {
60
+ const line = rowAt(match.index);
61
+ const name = match[2];
62
+ if (match[1] === 'undef') {
63
+ if (!undefs.has(name)) undefs.set(name, []);
64
+ undefs.get(name).push(line);
65
+ continue;
66
+ }
67
+ const afterName = match.index + match[0].length;
68
+ if (code[afterName] !== '(') continue;
69
+ // The directive with its continuation lines.
70
+ let end = code.indexOf('\n', afterName);
71
+ let endLine = line;
72
+ while (end !== -1 && /\\[ \t\r]*$/.test(code.slice(Math.max(afterName, end - 64), end))) {
73
+ const next = code.indexOf('\n', end + 1);
74
+ endLine++;
75
+ end = next;
76
+ }
77
+ const text = code.slice(afterName, end === -1 ? code.length : end)
78
+ .replace(/\\[ \t\r]*\n/g, ' \n');
79
+ const template = functionMacroTemplate(text);
80
+ if (!template) continue;
81
+ if (!defs.has(name)) defs.set(name, []);
82
+ defs.get(name).push({ name, line, endLine, ...template });
83
+ }
84
+ return defs.size > 0 ? { defs, undefs } : null;
85
+ }
86
+
87
+ /** The definition of `name` in effect at `line` (nearest above, no #undef between). */
88
+ function definitionInEffect(macros, name, line) {
89
+ const defs = macros.defs.get(name);
90
+ if (!defs) return null;
91
+ let best = null;
92
+ for (const def of defs) {
93
+ if (def.endLine < line && (!best || def.line > best.line)) best = def;
94
+ }
95
+ if (!best) return null;
96
+ const undone = (macros.undefs.get(name) || []).some(undef => undef > best.endLine && undef < line);
97
+ return undone ? null : best;
98
+ }
99
+
100
+ function isWordChar(code) {
101
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) ||
102
+ (code >= 97 && code <= 122) || code === 95;
103
+ }
104
+
105
+ function lineOfOffset(lineStarts, offset) {
106
+ let lo = 0;
107
+ let hi = lineStarts.length - 1;
108
+ while (lo < hi) {
109
+ const mid = (lo + hi + 1) >> 1;
110
+ if (lineStarts[mid] <= offset) lo = mid;
111
+ else hi = mid - 1;
112
+ }
113
+ return lo + 1;
114
+ }
115
+
116
+ /**
117
+ * Offset after the parenthesis closing the one at `open` (string and
118
+ * character literals and comments skipped), or -1.
119
+ */
120
+ function closingParenthesis(code, open) {
121
+ const limit = Math.min(code.length, open + MAX_EXPANSION_CHARS);
122
+ let depth = 0;
123
+ for (let i = open; i < limit; i++) {
124
+ const ch = code[i];
125
+ if (ch === '"' || ch === '\'') {
126
+ for (i++; i < limit && code[i] !== ch; i++) if (code[i] === '\\') i++;
127
+ continue;
128
+ }
129
+ if (ch === '/' && code[i + 1] === '/') {
130
+ while (i < limit && code[i] !== '\n') i++;
131
+ continue;
132
+ }
133
+ if (ch === '/' && code[i + 1] === '*') {
134
+ const close = code.indexOf('*/', i + 2);
135
+ if (close < 0) return -1;
136
+ i = close + 1;
137
+ continue;
138
+ }
139
+ if (ch === '(') depth++;
140
+ else if (ch === ')' && --depth === 0) return i + 1;
141
+ }
142
+ return -1;
143
+ }
144
+
145
+ /** End offset (after the closing parenthesis) and arguments of the list at `open`. */
146
+ function argumentListEnd(code, open) {
147
+ const end = closingParenthesis(code, open);
148
+ if (end < 0) return null;
149
+ const tokens = lexPP(code.slice(open, end));
150
+ const split = splitArguments(tokens, 0);
151
+ if (!split) return null;
152
+ return { end: open + tokens[split.close].start + 1, args: split.args };
153
+ }
154
+
155
+ function sameNode(a, b) {
156
+ return !!a && !!b && (a === b || a.id === b.id);
157
+ }
158
+
159
+ /** Scope of a declaration container: 'statement', 'member' or 'declaration'. */
160
+ function scopeOf(container) {
161
+ for (let up = container.parent; up; up = up.parent) {
162
+ if (!SCOPE_BOUNDARIES.has(up.type)) continue;
163
+ if (up.type === 'compound_statement') return 'statement';
164
+ if (up.type === 'field_declaration_list') return 'member';
165
+ return 'declaration';
166
+ }
167
+ return 'declaration';
168
+ }
169
+
170
+ /**
171
+ * The declaration a macro-name node was read into, and how: 'typeless' when
172
+ * it is the name of a function declarator in a declaration with no type
173
+ * (`NAME(args);`, `NAME(args) { }`), 'typed' when it is the type of a
174
+ * declaration (`NAME(x());` read as a function x returning NAME),
175
+ * 'type-specifier' for `NAME(args) value;` read as a macro type specifier,
176
+ * 'error' for any other non-call reading inside a syntax error.
177
+ */
178
+ function misreadShape(node) {
179
+ const parent = node.parent;
180
+ if (!parent) return null;
181
+ if ((node.type === 'identifier' || node.type === 'field_identifier') &&
182
+ parent.type === 'function_declarator' &&
183
+ sameNode(parent.childForFieldName('declarator'), node)) {
184
+ const container = parent.parent;
185
+ if (container && DECLARATION_CONTAINERS.has(container.type) &&
186
+ sameNode(container.childForFieldName('declarator'), parent) &&
187
+ !container.childForFieldName('type')) {
188
+ return { shape: 'typeless', container };
189
+ }
190
+ }
191
+ if (node.type === 'type_identifier' && DECLARATION_CONTAINERS.has(parent.type) &&
192
+ sameNode(parent.childForFieldName('type'), node)) {
193
+ return { shape: 'typed', container: parent };
194
+ }
195
+ // `NAME(args)` read as a macro type specifier in a declaration's type:
196
+ // right only when the expansion is a type (checked by the caller).
197
+ if (parent.type === 'macro_type_specifier' && sameNode(parent.childForFieldName('name'), node) &&
198
+ parent.parent && DECLARATION_CONTAINERS.has(parent.parent.type) &&
199
+ sameNode(parent.parent.childForFieldName('type'), parent)) {
200
+ return { shape: 'type-specifier', container: parent.parent };
201
+ }
202
+ // Inside a syntax error the grammar already failed around the
203
+ // invocation: any reading of the name other than a call's function
204
+ // (`LIST(X)` statements with no `;`, read as a macro type specifier or
205
+ // a stray declarator) is the misread invocation.
206
+ if (parent.type === 'call_expression' && sameNode(parent.childForFieldName('function'), node)) {
207
+ return null;
208
+ }
209
+ for (let up = parent; up; up = up.parent) {
210
+ if (up.type === 'ERROR') return { shape: 'error', container: up };
211
+ if (SCOPE_BOUNDARIES.has(up.type)) break;
212
+ }
213
+ return null;
214
+ }
215
+
216
+ /** Does this invocation's expansion read as a type (`TYPE(x) value;`)? */
217
+ function expandsToType(parser, definition, args) {
218
+ const text = substituteInvocation(definition, args);
219
+ if (text == null || !parser) return true;
220
+ const probe = parser.parse(`typedef ${text} __ucn_type_probe__;\n`);
221
+ return !probe.rootNode.hasError;
222
+ }
223
+
224
+ /**
225
+ * Invocations of the file's own function-like macros that the tree holds in
226
+ * a declaration shape. Each: { name, definition, line, start, nameEnd, end,
227
+ * args, scope, hasBody, blank: [start, end] | null }.
228
+ */
229
+ function misreadMacroInvocations(tree, code, macros, parser = null) {
230
+ const invocations = [];
231
+ if (!macros) return invocations;
232
+ const lineStarts = [0];
233
+ for (let i = 0; i < code.length; i++) if (code.charCodeAt(i) === 10) lineStarts.push(i + 1);
234
+ // Directive lines with their continuations: a macro body is never
235
+ // blanked, even where the grammar read it as code.
236
+ const directive = new Uint8Array(lineStarts.length + 1);
237
+ for (let row = 0; row < lineStarts.length; row++) {
238
+ let k = lineStarts[row];
239
+ while (k < code.length && (code[k] === ' ' || code[k] === '\t')) k++;
240
+ if (code[k] !== '#') continue;
241
+ for (;;) {
242
+ directive[row + 1] = 1;
243
+ const end = row + 1 < lineStarts.length ? lineStarts[row + 1] - 1 : code.length;
244
+ if (!/\\\s*$/.test(code.slice(lineStarts[row], end)) || row + 1 >= lineStarts.length) break;
245
+ row++;
246
+ }
247
+ }
248
+ const directiveRow = row => directive[row] === 1;
249
+ const root = tree.rootNode;
250
+ // One scan for identifiers followed by `(` (a prefilter: every hit is
251
+ // checked against the tree), instead of one text search per macro.
252
+ const candidate = /[A-Za-z_][A-Za-z0-9_]*[ \t]*\(/g;
253
+ for (let match = candidate.exec(code); match; match = candidate.exec(code)) {
254
+ const at = match.index;
255
+ if (at > 0 && isWordChar(code.charCodeAt(at - 1))) continue;
256
+ let nameEnd = at;
257
+ while (nameEnd < code.length && isWordChar(code.charCodeAt(nameEnd))) nameEnd++;
258
+ const name = code.slice(at, nameEnd);
259
+ if (!macros.defs.has(name)) continue;
260
+ // An invocation continuing an expression or a parameter list
261
+ // (`= M(x)`, `(M(x)`, `, M(x)`, `->M(x)`) is never read as a
262
+ // declaration of its own; skip the tree lookup.
263
+ let before = at - 1;
264
+ while (before >= 0 && (code[before] === ' ' || code[before] === '\t')) before--;
265
+ if (before >= 0 && EXPRESSION_CONTINUATION.has(code[before])) continue;
266
+ const open = match.index + match[0].length - 1;
267
+ {
268
+ const line = lineOfOffset(lineStarts, at);
269
+ if (directiveRow(line)) continue;
270
+ const definition = definitionInEffect(macros, name, line);
271
+ if (!definition) continue;
272
+ const node = root.descendantForIndex(at, nameEnd);
273
+ if (!node || node.startIndex !== at || node.endIndex !== nameEnd) continue;
274
+ const misread = misreadShape(node);
275
+ if (!misread) continue;
276
+ // Inside a syntax error only an invocation that begins its line
277
+ // stands in statement or declaration position (`LIST(X)` lines);
278
+ // one inside an expression (`if (LIKELY(x))`) is not misread.
279
+ if (misread.shape === 'error' &&
280
+ code.slice(lineStarts[line - 1], at).trim() !== '') continue;
281
+ const extent = argumentListEnd(code, open);
282
+ if (!extent) continue;
283
+ if (misread.shape === 'type-specifier' && expandsToType(parser, definition, extent.args)) continue;
284
+ const scope = scopeOf(misread.container);
285
+ const hasBody = misread.container.type === 'function_definition';
286
+ let after = extent.end;
287
+ while (after < code.length && (code[after] === ' ' || code[after] === '\t')) after++;
288
+ // A `typed` reading makes the invocation a prefix of the real
289
+ // declaration that follows (an attribute macro before a member,
290
+ // `M(x());` in a body); a `typeless` one with a body is a
291
+ // definition the expansion declares (`TEST(a, b) { }`), left as
292
+ // parsed and marked.
293
+ // Blanking never removes call syntax the source spells: an
294
+ // invocation whose arguments hold a parenthesis (a call,
295
+ // `f<T>(x)`, a cast) keeps them (only its name is blanked where
296
+ // a `;` makes the rest an expression statement, otherwise it is
297
+ // left as parsed).
298
+ const argumentsCall = extent.args.some(arg => arg.some(token => token.v === '('));
299
+ let blank = null;
300
+ if (scope === 'statement' && code[after] === ';' &&
301
+ !(hasBody && misread.shape === 'typeless')) {
302
+ blank = [at, nameEnd];
303
+ } else if (!argumentsCall && (scope === 'statement' || !hasBody || misread.shape !== 'typeless')) {
304
+ blank = [at, extent.end];
305
+ }
306
+ invocations.push({
307
+ name, definition, line, start: at, nameEnd, end: extent.end,
308
+ args: extent.args, scope, hasBody, blank,
309
+ });
310
+ }
311
+ }
312
+ return invocations.sort((a, b) => a.start - b.start);
313
+ }
314
+
315
+ /**
316
+ * The replacement of one invocation of a same-file macro: parameters
317
+ * substituted (arguments as written), `#param` stringized, `##` pasted.
318
+ * Macros the replacement invokes are not expanded again here; a nested
319
+ * invocation stays as written. Returns null when the arguments do not fit.
320
+ */
321
+ function substituteInvocation(definition, args) {
322
+ const params = definition.params || [];
323
+ let actual = args;
324
+ if (params.length === 0 && actual.length === 1 && actual[0].length === 0) actual = [];
325
+ if (definition.variadic) {
326
+ const named = params.length - 1;
327
+ if (actual.length < named) return null;
328
+ const rest = [];
329
+ actual.slice(named).forEach((arg, i) => {
330
+ if (i > 0) rest.push({ v: ',', k: 'punct' });
331
+ rest.push(...arg);
332
+ });
333
+ actual = [...actual.slice(0, named), rest];
334
+ } else if (actual.length !== params.length) {
335
+ return null;
336
+ }
337
+ const index = new Map(params.map((param, i) => [param, i]));
338
+ const body = lexPP(definition.body || '');
339
+ const out = [];
340
+ for (let i = 0; i < body.length; i++) {
341
+ const token = body[i];
342
+ if (token.v === '#' && body[i + 1]?.k === 'id' && index.has(body[i + 1].v)) {
343
+ out.push({ v: JSON.stringify(actual[index.get(body[i + 1].v)].map(t => t.v).join(' ')), k: 'str' });
344
+ i++;
345
+ continue;
346
+ }
347
+ if (token.k === 'id' && index.has(token.v)) {
348
+ out.push(...actual[index.get(token.v)]);
349
+ continue;
350
+ }
351
+ out.push(token);
352
+ }
353
+ const pasted = [];
354
+ for (let i = 0; i < out.length; i++) {
355
+ if (out[i].v === '##' && pasted.length > 0 && out[i + 1]) {
356
+ const left = pasted.pop();
357
+ pasted.push({ v: left.v + out[i + 1].v, k: 'id' });
358
+ i++;
359
+ continue;
360
+ }
361
+ pasted.push(out[i]);
362
+ }
363
+ // Spaces only where two tokens would otherwise merge (two words, two
364
+ // punctuators), so types read as written (`std::string`, `T*`).
365
+ let text = '';
366
+ for (let i = 0; i < pasted.length; i++) {
367
+ const word = pasted[i].k !== 'punct';
368
+ if (i > 0 && word === (pasted[i - 1].k !== 'punct')) text += ' ';
369
+ else if (i > 0 && pasted[i - 1].v === ',') text += ' ';
370
+ text += pasted[i].v;
371
+ }
372
+ return text.length > MAX_EXPANSION_CHARS ? null : text;
373
+ }
374
+
375
+ module.exports = {
376
+ directiveFunctionMacros,
377
+ definitionInEffect,
378
+ misreadMacroInvocations,
379
+ substituteInvocation,
380
+ };
@@ -0,0 +1,254 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * C/C++ preprocessing tokens (C11 6.4 / C++ [lex.pptoken]).
5
+ *
6
+ * Tree-sitter keeps a macro replacement list as one opaque `preproc_arg`, and
7
+ * `##` / `#` are not expression syntax, so a function-like macro that builds a
8
+ * call target by token pasting (`fs__##lc(req)`) is invisible to the AST. The
9
+ * preprocessor itself is defined over this token grammar, not over the AST:
10
+ * substituting arguments and pasting operands is a token-sequence operation.
11
+ * This module is that token layer. It never classifies code; callers parse
12
+ * the substituted token stream with tree-sitter to decide what is a call,
13
+ * a declaration, or a value reference.
14
+ */
15
+
16
+ const PUNCTUATORS = [
17
+ '%:%:', '...', '<<=', '>>=', '->*', '<=>',
18
+ '##', '->', '++', '--', '<<', '>>', '<=', '>=', '==', '!=', '&&', '||',
19
+ '*=', '/=', '%=', '+=', '-=', '&=', '^=', '|=', '::', '.*', '<:', ':>',
20
+ '<%', '%>', '%:',
21
+ ];
22
+
23
+ // Characters that never begin a multi-character punctuator.
24
+ const SINGLE_PUNCTUATORS = new Set(['(', ')', '{', '}', '[', ']', ';', ',', '?', '~']);
25
+
26
+ function isIdentStart(ch) {
27
+ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' ||
28
+ ch === '$' || ch > '\x7f';
29
+ }
30
+
31
+ function isIdentPart(ch) {
32
+ return isIdentStart(ch) || (ch >= '0' && ch <= '9');
33
+ }
34
+
35
+ /**
36
+ * Tokenize `text` into preprocessing tokens. Comments and whitespace are
37
+ * dropped; a line splice (`\` + newline) joins lines. Each token records its
38
+ * offset in `text` (`start`) and its kind: 'id', 'num', 'str', 'chr', 'punct'.
39
+ *
40
+ * One sticky scanner handles the common token classes (identifiers,
41
+ * whitespace, comments, numbers); literals and punctuators fall through to
42
+ * the exact per-character rules below.
43
+ */
44
+ const FAST_TOKEN = /([ \t\r\n\f\v]+|\\\r?\n)|(\/\/[^\n]*)|(\/\*[\s\S]*?(?:\*\/|$))|([A-Za-z_$\u0080-\uffff][A-Za-z0-9_$\u0080-\uffff]*)(?![A-Za-z0-9_$\u0080-\uffff"'])|((?:\.?[0-9])(?:[eEpP][+-]|[A-Za-z0-9_.]|'(?=[A-Za-z0-9_]))*)/y;
45
+
46
+ function lexPP(text) {
47
+ const tokens = [];
48
+ const n = text.length;
49
+ let i = 0;
50
+ while (i < n) {
51
+ FAST_TOKEN.lastIndex = i;
52
+ const match = FAST_TOKEN.exec(text);
53
+ if (match) {
54
+ if (match[4] !== undefined) {
55
+ tokens.push({ v: match[4], k: 'id', start: i });
56
+ } else if (match[5] !== undefined) {
57
+ tokens.push({ v: match[5], k: 'num', start: i });
58
+ }
59
+ i = FAST_TOKEN.lastIndex;
60
+ continue;
61
+ }
62
+ const ch = text[i];
63
+ const start = i;
64
+ if (isIdentStart(ch)) {
65
+ // Identifier directly followed by a quote: an encoding prefix
66
+ // (L"", u8"", R"(...)") belongs to the literal.
67
+ while (i < n && isIdentPart(text[i])) i++;
68
+ const prefix = text.slice(start, i);
69
+ if (/^(?:L|u8|u|U|R|LR|u8R|uR|UR)$/.test(prefix)) {
70
+ i = scanQuoted(text, i, prefix.endsWith('R'));
71
+ tokens.push({ v: text.slice(start, i), k: text[start + prefix.length] === '\'' ? 'chr' : 'str', start });
72
+ } else {
73
+ tokens.push({ v: prefix, k: 'id', start });
74
+ }
75
+ continue;
76
+ }
77
+ if (ch === '"' || ch === '\'') {
78
+ i = scanQuoted(text, i, false);
79
+ tokens.push({ v: text.slice(start, i), k: ch === '"' ? 'str' : 'chr', start });
80
+ continue;
81
+ }
82
+ if (SINGLE_PUNCTUATORS.has(ch)) {
83
+ tokens.push({ v: ch, k: 'punct', start });
84
+ i++;
85
+ continue;
86
+ }
87
+ let punct = null;
88
+ for (const p of PUNCTUATORS) {
89
+ if (text.startsWith(p, i)) {
90
+ punct = p;
91
+ break;
92
+ }
93
+ }
94
+ if (!punct) punct = ch;
95
+ i += punct.length;
96
+ tokens.push({ v: punct === '%:%:' ? '##' : punct === '%:' ? '#' : punct, k: 'punct', start });
97
+ }
98
+ return tokens;
99
+ }
100
+
101
+ function scanQuoted(text, i, raw) {
102
+ const quote = text[i];
103
+ if (raw && quote === '"') {
104
+ const open = text.indexOf('(', i);
105
+ if (open > 0) {
106
+ const delimiter = text.slice(i + 1, open);
107
+ const close = text.indexOf(`)${delimiter}"`, open);
108
+ if (close >= 0) return close + delimiter.length + 2;
109
+ }
110
+ }
111
+ i++;
112
+ while (i < text.length) {
113
+ const c = text[i];
114
+ if (c === '\\') { i += 2; continue; }
115
+ if (c === quote) return i + 1;
116
+ if (c === '\n') return i;
117
+ i++;
118
+ }
119
+ return i;
120
+ }
121
+
122
+ /**
123
+ * Split the tokens of a macro invocation `NAME ( a , b )` (starting at
124
+ * `openIndex`, the `(` token) into argument token lists. Returns
125
+ * { args, close } or null when the parentheses do not balance.
126
+ */
127
+ function splitArguments(tokens, openIndex) {
128
+ if (tokens[openIndex]?.v !== '(') return null;
129
+ const args = [[]];
130
+ let depth = 0;
131
+ for (let i = openIndex; i < tokens.length; i++) {
132
+ const t = tokens[i];
133
+ if (t.k === 'punct' && t.v === '(') {
134
+ depth++;
135
+ if (depth === 1) continue;
136
+ } else if (t.k === 'punct' && t.v === ')') {
137
+ depth--;
138
+ if (depth === 0) {
139
+ // `F()` has one empty argument; the caller reconciles that
140
+ // with a zero-parameter macro.
141
+ return { args, close: i };
142
+ }
143
+ } else if (depth === 1 && t.k === 'punct' && t.v === ',') {
144
+ args.push([]);
145
+ continue;
146
+ }
147
+ args[args.length - 1].push(t);
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Replacement-list template of a function-like macro, read from its
154
+ * logical `#define` line (text after the macro name). Returns
155
+ * { params, variadic, body } or null. `params` lists parameter names;
156
+ * a trailing `...` is exposed as `__VA_ARGS__`, GNU `name...` as `name`.
157
+ * `body` is the replacement list as space-joined preprocessing tokens.
158
+ */
159
+ function functionMacroTemplate(afterName) {
160
+ const tokens = lexPP(afterName);
161
+ if (tokens[0]?.v !== '(') return null;
162
+ const params = [];
163
+ let variadic = false;
164
+ let i = 1;
165
+ let expectName = true;
166
+ for (; i < tokens.length; i++) {
167
+ const t = tokens[i];
168
+ if (t.v === ')') break;
169
+ if (t.v === ',') {
170
+ expectName = true;
171
+ continue;
172
+ }
173
+ if (t.v === '...') {
174
+ variadic = true;
175
+ if (expectName) params.push('__VA_ARGS__');
176
+ continue;
177
+ }
178
+ if (t.k === 'id' && expectName) {
179
+ params.push(t.v);
180
+ expectName = false;
181
+ continue;
182
+ }
183
+ return null;
184
+ }
185
+ if (tokens[i]?.v !== ')') return null;
186
+ const body = tokens.slice(i + 1).map(t => t.v).join(' ');
187
+ return { params, variadic, body };
188
+ }
189
+
190
+ // Object-like replacement lists longer than this are not kept (they are
191
+ // declarations or statement blocks, never a name fragment for a paste).
192
+ const MAX_OBJECT_MACRO_CHARS = 256;
193
+
194
+ /** Replacement list of an object-like macro (text after its name), or null. */
195
+ function objectMacroBody(afterName) {
196
+ const tokens = lexPP(afterName);
197
+ const body = tokens.map(t => t.v).join(' ');
198
+ return body.length <= MAX_OBJECT_MACRO_CHARS ? body : null;
199
+ }
200
+
201
+ /**
202
+ * Pasting patterns of a template: each `a ## b ## c` chain that involves a
203
+ * parameter, as a sequence of literal strings and parameter wildcards
204
+ * (null). A chain of only literals pastes a fixed token and is ignored.
205
+ */
206
+ function pastePatterns(template) {
207
+ if (!template || !template.body.includes('##')) return [];
208
+ const tokens = lexPP(template.body);
209
+ const params = new Set(template.params);
210
+ const patterns = [];
211
+ for (let i = 0; i < tokens.length; i++) {
212
+ if (tokens[i + 1]?.v !== '##') continue;
213
+ const operands = [tokens[i]];
214
+ let j = i + 1;
215
+ while (tokens[j]?.v === '##' && tokens[j + 1]) {
216
+ operands.push(tokens[j + 1]);
217
+ j += 2;
218
+ }
219
+ i = j - 1;
220
+ if (!operands.some(op => op.k === 'id' && params.has(op.v))) continue;
221
+ // A chain whose operands are not identifier-shaped (`, ## __VA_ARGS__`
222
+ // comma elision) never produces a name.
223
+ if (operands.some(op => op.k !== 'id' && op.k !== 'num')) continue;
224
+ patterns.push(operands.map(op => op.k === 'id' && params.has(op.v) ? null : op.v));
225
+ }
226
+ return patterns;
227
+ }
228
+
229
+ /** Does `name` fit a paste pattern (literal pieces in order, wildcards between)? */
230
+ function matchesPastePattern(pattern, name) {
231
+ let position = 0;
232
+ for (let i = 0; i < pattern.length; i++) {
233
+ const piece = pattern[i];
234
+ if (piece === null) continue;
235
+ if (i === 0) {
236
+ if (!name.startsWith(piece)) return false;
237
+ position = piece.length;
238
+ continue;
239
+ }
240
+ if (i === pattern.length - 1) {
241
+ return name.length - piece.length >= position && name.endsWith(piece) &&
242
+ (pattern[i - 1] !== null || name.length - piece.length === position);
243
+ }
244
+ const found = name.indexOf(piece, position + (pattern[i - 1] === null ? 1 : 0));
245
+ if (found < 0 || (pattern[i - 1] !== null && found !== position)) return false;
246
+ position = found + piece.length;
247
+ }
248
+ // A trailing wildcard must produce at least one character.
249
+ return pattern[pattern.length - 1] !== null || name.length > position;
250
+ }
251
+
252
+ module.exports = {
253
+ lexPP, splitArguments, functionMacroTemplate, objectMacroBody, pastePatterns, matchesPastePattern,
254
+ };