ucn 5.4.2 → 5.5.1
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 +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- 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 +529 -31
- package/core/callers.js +12228 -921
- 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 +472 -66
- 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 +3026 -0
- package/core/rust-modules.js +203 -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 +1185 -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 +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- 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
|
+
};
|