@clear-capabilities/agentic-security-scanner 0.151.3 → 0.152.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/CHANGELOG.md +71 -0
- package/dist/agentic-security.mjs +80 -80
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +5 -5
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +802 -13
- package/src/dataflow/engine.js +852 -89
- package/src/dataflow/incremental.js +13 -0
- package/src/dataflow/index.js +18 -5
- package/src/dataflow/path-feasibility.js +86 -9
- package/src/dataflow/string-domain.js +132 -0
- package/src/dataflow/summaries.js +69 -8
- package/src/engine.js +108 -3
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/balanced-call.js +33 -1
- package/src/ir/callgraph.js +26 -2
- package/src/ir/class-hierarchy.js +101 -6
- package/src/ir/csharp-ir.js +97 -8
- package/src/ir/index.js +21 -1
- package/src/ir/parser-cs.js +706 -24
- package/src/ir/parser-java.js +498 -17
- package/src/ir/parser-js.js +22 -0
- package/src/ir/parser-php.js +446 -43
- package/src/ir/parser-rust.js +1338 -0
- package/src/ir/php-include-merge.js +190 -0
- package/src/lineage/sink-registry.js +18 -0
- package/src/lineage/source-registry.js +23 -0
- package/src/posture/csharp-analysis.js +341 -43
- package/src/posture/finding-defaults.js +5 -1
- package/src/posture/fix-coverage.js +1 -0
- package/src/posture/proof-coverage.js +1 -0
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/authz.js +144 -1
- package/src/sast/crypto-protocol.js +65 -7
- package/src/sast/csharp-cross-file.js +125 -0
- package/src/sast/csharp-structural.js +78 -2
- package/src/sast/csharp.js +574 -42
- package/src/sast/java-ast-folding.js +268 -11
- package/src/sast/java-bench-extras.js +530 -2
- package/src/sast/java-deserialization.js +31 -2
- package/src/sast/java-structural-cross-file.js +146 -0
- package/src/sast/java-structural.js +499 -6
- package/src/sast/ldap-injection.js +133 -3
- package/src/sast/rust.js +57 -0
- package/src/sast/xss-reflected-multilang.js +252 -3
|
@@ -0,0 +1,1338 @@
|
|
|
1
|
+
// Rust IR frontend.
|
|
2
|
+
//
|
|
3
|
+
// Hand-rolled, following the parser-go.js / parser-cs.js pattern: one
|
|
4
|
+
// preprocessing pass normalizes the source (comments blanked with line
|
|
5
|
+
// structure preserved, `::` paths flattened to dots, turbofish/`?`/`.await`
|
|
6
|
+
// removed), then a statement splitter and a recursive CFG builder lower each
|
|
7
|
+
// function body into the IR contract documented in ./CLAUDE.md.
|
|
8
|
+
//
|
|
9
|
+
// What we model:
|
|
10
|
+
// - fn items (free, impl, trait default) with pub/async/unsafe/const/extern
|
|
11
|
+
// qualifiers, generics, where clauses; impl methods are named `Type.method`
|
|
12
|
+
// - let / let mut / destructuring let / let-else; assignments; compound
|
|
13
|
+
// assignments (`+=` lowers to a template of old value + rhs)
|
|
14
|
+
// - calls and method chains, dot-joined, args accumulated innermost-first
|
|
15
|
+
// (source order), so `Command::new("sh").arg("-c").arg(x)` has the literal
|
|
16
|
+
// program name at arg 0
|
|
17
|
+
// - macros: format!-family lower to templates (inline `{ident}` args and
|
|
18
|
+
// positional args), vec! to arrays, every other macro to a call whose
|
|
19
|
+
// callee keeps its `!` (so `sqlx::query!` never matches the `query` sink)
|
|
20
|
+
// - if / else if / else, if let, while, while let, loop, for, match arms
|
|
21
|
+
// (pattern bindings assigned from the scrutinee), unsafe/bare/async blocks
|
|
22
|
+
// - control flow in expression position (`let x = match … { … }`) yields a
|
|
23
|
+
// union of the branch tails
|
|
24
|
+
// - closures passed as call arguments are inlined, parameters bound to the
|
|
25
|
+
// call's receiver; extractor-typed closure params become synthetic sources
|
|
26
|
+
// - fn.paramAnnotations for framework extractor types (Query/Path/Json/Form/
|
|
27
|
+
// HeaderMap/…) and for route-attributed handlers (`#[get("/<x>")]`)
|
|
28
|
+
//
|
|
29
|
+
// What we do NOT model:
|
|
30
|
+
// - trait dispatch, generics as types, lifetimes (stripped as text)
|
|
31
|
+
// - closures stored in variables and invoked later
|
|
32
|
+
// - struct field taint through method receivers beyond simple dotted paths
|
|
33
|
+
// - labeled breaks, `?` early-return control flow (the `?` is stripped, the
|
|
34
|
+
// happy path is what taint needs)
|
|
35
|
+
|
|
36
|
+
import * as crypto from 'node:crypto';
|
|
37
|
+
import { callSitesFromCfg } from './call-sites.js';
|
|
38
|
+
|
|
39
|
+
const U = { kind: 'unknown' };
|
|
40
|
+
|
|
41
|
+
// ─── literal scanning ────────────────────────────────────────────────────────
|
|
42
|
+
// Returns the index just past a string/char literal starting at `i`, or `i`
|
|
43
|
+
// when no literal starts there. Every splitter below routes through this so
|
|
44
|
+
// braces, commas and semicolons inside literals never count.
|
|
45
|
+
function _literalEnd(s, i) {
|
|
46
|
+
const c = s[i];
|
|
47
|
+
const prevIsWord = i > 0 && /[\w]/.test(s[i - 1]);
|
|
48
|
+
if (c === '"') return _scanDq(s, i + 1);
|
|
49
|
+
if (c === '\'') {
|
|
50
|
+
if (s[i + 1] === '\\') {
|
|
51
|
+
// '\n', '\'', '\u{1F600}'
|
|
52
|
+
let j = i + 2;
|
|
53
|
+
if (s[j] === 'u' && s[j + 1] === '{') { j = s.indexOf('}', j); if (j < 0) return i; j++; } else j++;
|
|
54
|
+
return s[j] === '\'' ? j + 1 : i;
|
|
55
|
+
}
|
|
56
|
+
const cp = s.codePointAt(i + 1);
|
|
57
|
+
const w = cp > 0xffff ? 2 : 1;
|
|
58
|
+
if (s[i + 1 + w] === '\'' && s[i + 1] !== '\'') return i + 2 + w;
|
|
59
|
+
return i; // lifetime or label
|
|
60
|
+
}
|
|
61
|
+
if (prevIsWord) return i;
|
|
62
|
+
// raw / byte / c-string prefixes
|
|
63
|
+
const m = /^(?:br|cr|r)(#*)"/.exec(s.slice(i, i + 12));
|
|
64
|
+
if (m) {
|
|
65
|
+
const hashes = m[1];
|
|
66
|
+
const close = '"' + hashes;
|
|
67
|
+
const j = s.indexOf(close, i + m[0].length);
|
|
68
|
+
return j < 0 ? s.length : j + close.length;
|
|
69
|
+
}
|
|
70
|
+
if ((c === 'b' || c === 'c') && s[i + 1] === '"') return _scanDq(s, i + 2);
|
|
71
|
+
if (c === 'b' && s[i + 1] === '\'') {
|
|
72
|
+
const e = _literalEnd(s, i + 1);
|
|
73
|
+
return e > i + 1 ? e : i;
|
|
74
|
+
}
|
|
75
|
+
return i;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _scanDq(s, i) {
|
|
79
|
+
for (; i < s.length; i++) {
|
|
80
|
+
if (s[i] === '\\') { i++; continue; }
|
|
81
|
+
if (s[i] === '"') return i + 1;
|
|
82
|
+
}
|
|
83
|
+
return s.length;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── preprocessing ───────────────────────────────────────────────────────────
|
|
87
|
+
// Newlines are preserved exactly so every offset in the preprocessed text maps
|
|
88
|
+
// to the same line in the original file.
|
|
89
|
+
function _preprocess(src) {
|
|
90
|
+
let out = '';
|
|
91
|
+
let i = 0;
|
|
92
|
+
const n = src.length;
|
|
93
|
+
while (i < n) {
|
|
94
|
+
const c = src[i];
|
|
95
|
+
const d = src[i + 1];
|
|
96
|
+
if (c === '/' && d === '/') {
|
|
97
|
+
while (i < n && src[i] !== '\n') { out += ' '; i++; }
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (c === '/' && d === '*') {
|
|
101
|
+
let depth = 1;
|
|
102
|
+
out += ' '; i += 2;
|
|
103
|
+
while (i < n && depth > 0) {
|
|
104
|
+
if (src[i] === '/' && src[i + 1] === '*') { depth++; out += ' '; i += 2; continue; }
|
|
105
|
+
if (src[i] === '*' && src[i + 1] === '/') { depth--; out += ' '; i += 2; continue; }
|
|
106
|
+
out += src[i] === '\n' ? '\n' : ' ';
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const lit = _literalEnd(src, i);
|
|
112
|
+
if (lit > i) { out += src.slice(i, lit); i = lit; continue; }
|
|
113
|
+
if (c === ':' && d === ':') {
|
|
114
|
+
if (src[i + 2] === '<') {
|
|
115
|
+
// turbofish `::<T>`: drop entirely, keep any newlines inside
|
|
116
|
+
let j = i + 2;
|
|
117
|
+
let depth = 0;
|
|
118
|
+
for (; j < n; j++) {
|
|
119
|
+
if (src[j] === '<') depth++;
|
|
120
|
+
else if (src[j] === '>') { depth--; if (depth === 0) { j++; break; } }
|
|
121
|
+
else if (src[j] === '\n') out += '\n';
|
|
122
|
+
}
|
|
123
|
+
i = j;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
out += '.'; i += 2;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (c === '.' && src.startsWith('.await', i) && !/\w/.test(src[i + 6] || '')) { i += 6; continue; }
|
|
130
|
+
if (c === '?') { i++; continue; }
|
|
131
|
+
out += c; i++;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── generic scanners ────────────────────────────────────────────────────────
|
|
137
|
+
function _matchingClose(s, openIdx) {
|
|
138
|
+
const open = s[openIdx];
|
|
139
|
+
const close = open === '(' ? ')' : open === '[' ? ']' : open === '{' ? '}' : null;
|
|
140
|
+
if (!close) return -1;
|
|
141
|
+
let depth = 0;
|
|
142
|
+
for (let i = openIdx; i < s.length;) {
|
|
143
|
+
const e = _literalEnd(s, i);
|
|
144
|
+
if (e > i) { i = e; continue; }
|
|
145
|
+
const c = s[i];
|
|
146
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
147
|
+
else if (c === ')' || c === ']' || c === '}') { depth--; if (depth === 0) return i; }
|
|
148
|
+
i++;
|
|
149
|
+
}
|
|
150
|
+
return -1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Split on `sep` at depth 0 (parens/brackets/braces, and optionally angle
|
|
154
|
+
// brackets for type contexts). Returns [{text, start}] with `start` = offset of
|
|
155
|
+
// the part's first non-space char within `s`.
|
|
156
|
+
function _splitTopLevel(s, sep, { angles = false } = {}) {
|
|
157
|
+
const out = [];
|
|
158
|
+
let depth = 0;
|
|
159
|
+
let angle = 0;
|
|
160
|
+
let partStart = 0;
|
|
161
|
+
const push = (end) => {
|
|
162
|
+
let a = partStart;
|
|
163
|
+
while (a < end && /\s/.test(s[a])) a++;
|
|
164
|
+
let b = end;
|
|
165
|
+
while (b > a && /\s/.test(s[b - 1])) b--;
|
|
166
|
+
out.push({ text: s.slice(a, b), start: a });
|
|
167
|
+
};
|
|
168
|
+
for (let i = 0; i < s.length;) {
|
|
169
|
+
const e = _literalEnd(s, i);
|
|
170
|
+
if (e > i) { i = e; continue; }
|
|
171
|
+
const c = s[i];
|
|
172
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
173
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
174
|
+
else if (angles && c === '<' && depth >= 0) angle++;
|
|
175
|
+
else if (angles && c === '>' && s[i - 1] !== '-' && angle > 0) angle--;
|
|
176
|
+
else if (c === sep && depth === 0 && angle === 0) { push(i); partStart = i + 1; i++; continue; }
|
|
177
|
+
i++;
|
|
178
|
+
}
|
|
179
|
+
push(s.length);
|
|
180
|
+
if (out.length === 1 && !out[0].text) return [];
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// `+` at depth 0, not part of `+=`, `->`, or a unary sign after an operator.
|
|
185
|
+
function _splitTopLevelPlus(s) {
|
|
186
|
+
const out = [];
|
|
187
|
+
let depth = 0;
|
|
188
|
+
let partStart = 0;
|
|
189
|
+
for (let i = 0; i < s.length;) {
|
|
190
|
+
const e = _literalEnd(s, i);
|
|
191
|
+
if (e > i) { i = e; continue; }
|
|
192
|
+
const c = s[i];
|
|
193
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
194
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
195
|
+
else if (c === '+' && depth === 0 && s[i + 1] !== '=') {
|
|
196
|
+
const t = s.slice(partStart, i).trim();
|
|
197
|
+
if (t) { out.push(t); partStart = i + 1; }
|
|
198
|
+
}
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
const last = s.slice(partStart).trim();
|
|
202
|
+
if (last) out.push(last);
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _extractBody(src, openBrace) {
|
|
207
|
+
const end = _matchingClose(src, openBrace);
|
|
208
|
+
if (end < 0) return null;
|
|
209
|
+
return { body: src.slice(openBrace + 1, end), end };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Statement splitter. Flushes on `;` at depth 0 and on a `}` that closes a
|
|
213
|
+
// depth-0 block, unless the block is continued by `else`, a method call, a
|
|
214
|
+
// cast, or is itself an argument. Returns [{text, start, terminated}].
|
|
215
|
+
const _BLOCK_CONTINUERS = /^(?:else\b|as\b|\.|,|\)|\]|;|\}|\|\||&&|==|!=|[-+*\/%]?=|\?)/;
|
|
216
|
+
function _splitStatements(body) {
|
|
217
|
+
const out = [];
|
|
218
|
+
let depth = 0;
|
|
219
|
+
let stmtStart = 0;
|
|
220
|
+
const flush = (end, terminated) => {
|
|
221
|
+
let a = stmtStart;
|
|
222
|
+
while (a < end && /\s/.test(body[a])) a++;
|
|
223
|
+
let b = end;
|
|
224
|
+
while (b > a && /\s/.test(body[b - 1])) b--;
|
|
225
|
+
if (b > a) out.push({ text: body.slice(a, b), start: a, terminated });
|
|
226
|
+
};
|
|
227
|
+
for (let i = 0; i < body.length;) {
|
|
228
|
+
const e = _literalEnd(body, i);
|
|
229
|
+
if (e > i) { i = e; continue; }
|
|
230
|
+
const c = body[i];
|
|
231
|
+
if (c === '(' || c === '[' || c === '{') { depth++; i++; continue; }
|
|
232
|
+
if (c === ')' || c === ']') { depth--; i++; continue; }
|
|
233
|
+
if (c === '}') {
|
|
234
|
+
depth--;
|
|
235
|
+
i++;
|
|
236
|
+
if (depth === 0) {
|
|
237
|
+
let j = i;
|
|
238
|
+
while (j < body.length && /\s/.test(body[j])) j++;
|
|
239
|
+
if (j >= body.length || !_BLOCK_CONTINUERS.test(body.slice(j, j + 4))) {
|
|
240
|
+
flush(i, false);
|
|
241
|
+
stmtStart = i;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (c === ';' && depth === 0) { flush(i, true); stmtStart = i + 1; i++; continue; }
|
|
247
|
+
i++;
|
|
248
|
+
}
|
|
249
|
+
flush(body.length, false);
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── chain parsing ───────────────────────────────────────────────────────────
|
|
254
|
+
// Parses `base . seg (args) [idx] …` into a flat op list. `base` is an ident
|
|
255
|
+
// path (`a.b.c`), a literal, a paren group, or a macro invocation.
|
|
256
|
+
const _IDENT_PATH_RE = /^[A-Za-z_][\w.]*/;
|
|
257
|
+
const _MACRO_RE = /^([A-Za-z_][\w.]*)!\s*([(\[{])/;
|
|
258
|
+
|
|
259
|
+
function _parseChain(s) {
|
|
260
|
+
let i = 0;
|
|
261
|
+
let base = null;
|
|
262
|
+
const lit = _literalEnd(s, 0);
|
|
263
|
+
if (lit > 0) {
|
|
264
|
+
base = { type: 'literal', text: s.slice(0, lit) };
|
|
265
|
+
i = lit;
|
|
266
|
+
} else if (s[0] === '(') {
|
|
267
|
+
const close = _matchingClose(s, 0);
|
|
268
|
+
if (close < 0) return null;
|
|
269
|
+
base = { type: 'paren', text: s.slice(1, close) };
|
|
270
|
+
i = close + 1;
|
|
271
|
+
} else {
|
|
272
|
+
const mac = _MACRO_RE.exec(s);
|
|
273
|
+
if (mac) {
|
|
274
|
+
const open = mac[0].length - 1;
|
|
275
|
+
const close = _matchingClose(s, open);
|
|
276
|
+
if (close < 0) return null;
|
|
277
|
+
base = { type: 'macro', name: mac[1], argsText: s.slice(open + 1, close), argsStart: open + 1 };
|
|
278
|
+
i = close + 1;
|
|
279
|
+
} else {
|
|
280
|
+
const id = _IDENT_PATH_RE.exec(s);
|
|
281
|
+
if (!id) return null;
|
|
282
|
+
base = { type: 'path', text: id[0] };
|
|
283
|
+
i = id[0].length;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const ops = [];
|
|
287
|
+
for (;;) {
|
|
288
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
289
|
+
if (i >= s.length) break;
|
|
290
|
+
const c = s[i];
|
|
291
|
+
if (c === '(' && (base.type !== 'literal' || ops.length)) {
|
|
292
|
+
const close = _matchingClose(s, i);
|
|
293
|
+
if (close < 0) return null;
|
|
294
|
+
ops.push({ kind: 'call', argsText: s.slice(i + 1, close), argsStart: i + 1 });
|
|
295
|
+
i = close + 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (c === '[') {
|
|
299
|
+
const close = _matchingClose(s, i);
|
|
300
|
+
if (close < 0) return null;
|
|
301
|
+
ops.push({ kind: 'index', text: s.slice(i + 1, close) });
|
|
302
|
+
i = close + 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (c === '.') {
|
|
306
|
+
const m = /^\.\s*([A-Za-z_]\w*|\d+)/.exec(s.slice(i));
|
|
307
|
+
if (!m) break;
|
|
308
|
+
ops.push({ kind: 'member', name: m[1] });
|
|
309
|
+
i += m[0].length;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
return { base, ops, end: i };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── expression lowering ─────────────────────────────────────────────────────
|
|
318
|
+
const _FORMAT_MACROS = new Set(['format', 'print', 'println', 'eprint', 'eprintln', 'panic', 'format_args', 'write', 'writeln', 'assert', 'unreachable', 'todo', 'concat']);
|
|
319
|
+
const _WRITER_FIRST = new Set(['write', 'writeln']);
|
|
320
|
+
const _CLOSURE_RE = /^(?:move\s+)?\|([^|]*)\|\s*(?:async\s+(?:move\s+)?)?/;
|
|
321
|
+
const _ASYNC_BLOCK_RE = /^async\s+(?:move\s+)?\{/;
|
|
322
|
+
|
|
323
|
+
function _stripCast(s) {
|
|
324
|
+
// drop a trailing top-level ` as Type`
|
|
325
|
+
let depth = 0;
|
|
326
|
+
let cut = -1;
|
|
327
|
+
for (let i = 0; i < s.length;) {
|
|
328
|
+
const e = _literalEnd(s, i);
|
|
329
|
+
if (e > i) { i = e; continue; }
|
|
330
|
+
const c = s[i];
|
|
331
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
332
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
333
|
+
else if (depth === 0 && c === 'a' && s[i + 1] === 's' && /\s/.test(s[i + 2] || '') && /\s/.test(s[i - 1] || '')) cut = i;
|
|
334
|
+
i++;
|
|
335
|
+
}
|
|
336
|
+
return cut > 0 ? s.slice(0, cut).trim() : s;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function _lowerFormatArgs(argsText, dropFirst) {
|
|
340
|
+
const parts = [];
|
|
341
|
+
let args = _splitTopLevel(argsText, ',').map(p => p.text);
|
|
342
|
+
if (dropFirst) args = args.slice(1);
|
|
343
|
+
if (!args.length) return { kind: 'tpl', parts };
|
|
344
|
+
const fmt = args[0];
|
|
345
|
+
const positional = args.slice(1);
|
|
346
|
+
const named = new Map();
|
|
347
|
+
const positionalExprs = [];
|
|
348
|
+
for (const a of positional) {
|
|
349
|
+
const nm = /^([A-Za-z_]\w*)\s*=\s*([\s\S]+)$/.exec(a);
|
|
350
|
+
if (nm) named.set(nm[1], _lowerExpr(nm[2]));
|
|
351
|
+
else positionalExprs.push(_lowerExpr(a));
|
|
352
|
+
}
|
|
353
|
+
if (_literalEnd(fmt, 0) === fmt.length && fmt.length > 1) {
|
|
354
|
+
parts.push({ kind: 'literal', value: fmt });
|
|
355
|
+
let next = 0;
|
|
356
|
+
const used = new Set();
|
|
357
|
+
const re = /\{\{|\}\}|\{([^{}]*)\}/g;
|
|
358
|
+
let m;
|
|
359
|
+
while ((m = re.exec(fmt)) !== null) {
|
|
360
|
+
if (m[0] === '{{' || m[0] === '}}') continue;
|
|
361
|
+
const inner = (m[1] || '').split(':')[0].trim();
|
|
362
|
+
if (!inner) {
|
|
363
|
+
if (next < positionalExprs.length) { parts.push(positionalExprs[next]); used.add(next); }
|
|
364
|
+
next++;
|
|
365
|
+
} else if (/^\d+$/.test(inner)) {
|
|
366
|
+
const idx = Number(inner);
|
|
367
|
+
if (idx < positionalExprs.length) { parts.push(positionalExprs[idx]); used.add(idx); }
|
|
368
|
+
} else if (named.has(inner)) {
|
|
369
|
+
parts.push(named.get(inner));
|
|
370
|
+
} else if (/^[A-Za-z_]\w*$/.test(inner)) {
|
|
371
|
+
parts.push({ kind: 'ident', name: inner });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
positionalExprs.forEach((e, idx) => { if (!used.has(idx)) parts.push(e); });
|
|
375
|
+
for (const [, e] of named) if (!parts.includes(e)) parts.push(e);
|
|
376
|
+
} else {
|
|
377
|
+
parts.push(_lowerExpr(fmt), ...positionalExprs, ...named.values());
|
|
378
|
+
}
|
|
379
|
+
return { kind: 'tpl', parts };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// `format!`/`format_args!`/`concat!` ARE the value they build, so they lower
|
|
383
|
+
// straight to a template. Every other format-family macro (`println!`,
|
|
384
|
+
// `write!`, `panic!`, and log macros later) is a statement-shaped call that
|
|
385
|
+
// consumes a template: it lowers to a call whose callee keeps the `!`, with
|
|
386
|
+
// the template as its single argument, so a catalog sink can key on the
|
|
387
|
+
// macro name (`info!` for log injection) exactly like a function.
|
|
388
|
+
const _VALUE_MACROS = new Set(['format', 'format_args', 'concat']);
|
|
389
|
+
function _lowerMacro(name, argsText) {
|
|
390
|
+
const bare = name.includes('.') ? name.slice(name.lastIndexOf('.') + 1) : name;
|
|
391
|
+
if (_FORMAT_MACROS.has(bare)) {
|
|
392
|
+
const tpl = _lowerFormatArgs(argsText, _WRITER_FIRST.has(bare));
|
|
393
|
+
if (_VALUE_MACROS.has(bare)) return tpl;
|
|
394
|
+
return { kind: 'call', callee: `${name}!`, args: [tpl] };
|
|
395
|
+
}
|
|
396
|
+
if (bare === 'vec') {
|
|
397
|
+
const semi = _splitTopLevel(argsText, ';');
|
|
398
|
+
const items = semi.length === 2 ? [semi[0].text] : _splitTopLevel(argsText, ',').map(p => p.text);
|
|
399
|
+
return { kind: 'array', elements: items.map(_lowerExpr) };
|
|
400
|
+
}
|
|
401
|
+
return { kind: 'call', callee: `${name}!`, args: _splitTopLevel(argsText, ',').map(p => _lowerExpr(p.text)) };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const _PASSTHROUGH_CALLS = new Set(['unwrap', 'expect']);
|
|
405
|
+
|
|
406
|
+
function _memberChain(segs) {
|
|
407
|
+
let cur = { kind: 'ident', name: segs[0] };
|
|
408
|
+
for (let i = 1; i < segs.length; i++) cur = { kind: 'member', object: cur, prop: segs[i] };
|
|
409
|
+
return cur;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function _lowerChain(chain) {
|
|
413
|
+
const { base, ops } = chain;
|
|
414
|
+
let segs;
|
|
415
|
+
let args = [];
|
|
416
|
+
let _baseIsCall = false;
|
|
417
|
+
if (base.type === 'path') segs = base.text.split('.');
|
|
418
|
+
else if (base.type === 'macro') {
|
|
419
|
+
const m = _lowerMacro(base.name, base.argsText);
|
|
420
|
+
if (!ops.length) return m;
|
|
421
|
+
// A macro starting a chain keeps its own name (with the `!`) as the
|
|
422
|
+
// chain root so `sqlx::query!(…).fetch_one(pool)` reads as
|
|
423
|
+
// `sqlx.query!.fetch_one`, and its lowered value is the innermost arg.
|
|
424
|
+
segs = `${base.name}!`.split('.');
|
|
425
|
+
args = m.kind === 'call' ? m.args.slice() : [m];
|
|
426
|
+
// A call-shaped macro (`write!`, `println!`, …) IS the call for this
|
|
427
|
+
// statement even if every op that follows is a stripped passthrough
|
|
428
|
+
// (`write!(w, "…", x).unwrap();`) — without this, a macro-only
|
|
429
|
+
// statement whose sole continuation is `.unwrap()` would fall through
|
|
430
|
+
// to `if (!hasCall)` below and silently stop being a call node at all.
|
|
431
|
+
_baseIsCall = m.kind === 'call';
|
|
432
|
+
} else if (base.type === 'literal') {
|
|
433
|
+
const l = { kind: 'literal', value: base.text };
|
|
434
|
+
if (!ops.length) return l;
|
|
435
|
+
segs = ['_lit'];
|
|
436
|
+
args = [l];
|
|
437
|
+
} else { // paren
|
|
438
|
+
const inner = _lowerExpr(base.text);
|
|
439
|
+
if (!ops.length) return inner;
|
|
440
|
+
segs = ['_expr'];
|
|
441
|
+
args = [inner];
|
|
442
|
+
}
|
|
443
|
+
let hasCall = _baseIsCall;
|
|
444
|
+
// `.unwrap()`/`.expect(msg)` are Result/Option combinators that neither
|
|
445
|
+
// change WHAT the value is nor take a taint-relevant argument — they exist
|
|
446
|
+
// purely to panic on an error path. Left in the chain, they shift the
|
|
447
|
+
// TERMINAL segment away from the real sink/method name (the same "terminal
|
|
448
|
+
// segment shift" defect class documented for Go/Java/Kotlin in ir/CLAUDE.md),
|
|
449
|
+
// so `sqlx::query(&sql).fetch_all(pool).await.unwrap()` would otherwise end
|
|
450
|
+
// in `.unwrap`, never `.fetch_all`, and no sink catalog entry could ever
|
|
451
|
+
// match it — nearly every real async Rust call site ends this way. Popped
|
|
452
|
+
// (segment AND args) as soon as the CALL that follows them is seen, so the
|
|
453
|
+
// chain reads exactly as if they were never written.
|
|
454
|
+
let pendingPassthrough = false;
|
|
455
|
+
for (const op of ops) {
|
|
456
|
+
if (op.kind === 'member') {
|
|
457
|
+
pendingPassthrough = _PASSTHROUGH_CALLS.has(op.name);
|
|
458
|
+
segs.push(op.name);
|
|
459
|
+
} else if (op.kind === 'index') {
|
|
460
|
+
pendingPassthrough = false;
|
|
461
|
+
segs.push('[]');
|
|
462
|
+
} else if (pendingPassthrough) {
|
|
463
|
+
pendingPassthrough = false;
|
|
464
|
+
segs.pop();
|
|
465
|
+
} else {
|
|
466
|
+
hasCall = true;
|
|
467
|
+
const lowered = _splitTopLevel(op.argsText, ',').map(p => _lowerExpr(p.text));
|
|
468
|
+
args = args.concat(lowered);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (!hasCall) {
|
|
472
|
+
if (base.type === 'path') return _memberChain(segs);
|
|
473
|
+
return args[0] || U;
|
|
474
|
+
}
|
|
475
|
+
return { kind: 'call', callee: segs.join('.'), args };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function _lowerExpr(text) {
|
|
479
|
+
let s = String(text || '').trim();
|
|
480
|
+
if (!s) return U;
|
|
481
|
+
// fully parenthesized
|
|
482
|
+
while (s.startsWith('(') && _matchingClose(s, 0) === s.length - 1) {
|
|
483
|
+
const inner = s.slice(1, -1).trim();
|
|
484
|
+
if (_splitTopLevel(inner, ',').length > 1) return { kind: 'array', elements: _splitTopLevel(inner, ',').map(p => _lowerExpr(p.text)) };
|
|
485
|
+
s = inner;
|
|
486
|
+
if (!s) return U;
|
|
487
|
+
}
|
|
488
|
+
const pre = /^(?:&\s*mut\s+|&\s*|\*\s*|mut\s+|!\s*|-\s*)/.exec(s);
|
|
489
|
+
if (pre && pre[0].length < s.length) return _lowerExpr(s.slice(pre[0].length));
|
|
490
|
+
s = _stripCast(s);
|
|
491
|
+
// closure as a value: its body expression is what flows onward
|
|
492
|
+
const cl = _CLOSURE_RE.exec(s);
|
|
493
|
+
if (cl) {
|
|
494
|
+
const body = s.slice(cl[0].length).trim();
|
|
495
|
+
if (body.startsWith('{')) return U;
|
|
496
|
+
return _lowerExpr(body);
|
|
497
|
+
}
|
|
498
|
+
if (/^(?:if|match|loop|while|for|unsafe|async)\b/.test(s) || s.startsWith('{')) return U;
|
|
499
|
+
// array literal
|
|
500
|
+
if (s.startsWith('[') && _matchingClose(s, 0) === s.length - 1) {
|
|
501
|
+
const inner = s.slice(1, -1);
|
|
502
|
+
const semi = _splitTopLevel(inner, ';');
|
|
503
|
+
const items = semi.length === 2 ? [semi[0].text] : _splitTopLevel(inner, ',').map(p => p.text);
|
|
504
|
+
return { kind: 'array', elements: items.map(_lowerExpr) };
|
|
505
|
+
}
|
|
506
|
+
// string concat with +
|
|
507
|
+
const plusParts = _splitTopLevelPlus(s);
|
|
508
|
+
if (plusParts.length > 1) return { kind: 'tpl', parts: plusParts.map(_lowerExpr) };
|
|
509
|
+
// comparison / logical operators at top level: keep both sides visible
|
|
510
|
+
const bin = _splitBinary(s);
|
|
511
|
+
if (bin) return { kind: 'logical', op: bin.op, left: _lowerExpr(bin.left), right: _lowerExpr(bin.right) };
|
|
512
|
+
// range `a..b`
|
|
513
|
+
const range = /^([\s\S]*?)\.\.=?([\s\S]*)$/.exec(s);
|
|
514
|
+
if (range && !s.startsWith('.') && range[1].trim() && !/[("[]/.test(range[1])) {
|
|
515
|
+
return { kind: 'array', elements: [_lowerExpr(range[1]), _lowerExpr(range[2])].filter(e => e !== U) };
|
|
516
|
+
}
|
|
517
|
+
if (/^\d/.test(s)) return { kind: 'literal', value: s };
|
|
518
|
+
if (/^(?:true|false|None|Self|self)$/.test(s)) return s === 'self' || s === 'Self' ? { kind: 'ident', name: s } : { kind: 'literal', value: s };
|
|
519
|
+
// struct literal `Type { a: x, b }` / `Type.Variant { .. }`
|
|
520
|
+
const sl = /^([A-Za-z_][\w.]*)\s*\{/.exec(s);
|
|
521
|
+
if (sl && _matchingClose(s, sl[0].length - 1) === s.length - 1) {
|
|
522
|
+
const inner = s.slice(sl[0].length, -1);
|
|
523
|
+
const props = [];
|
|
524
|
+
for (const f of _splitTopLevel(inner, ',')) {
|
|
525
|
+
const t = f.text;
|
|
526
|
+
if (!t) continue;
|
|
527
|
+
if (t.startsWith('..')) { props.push({ value: _lowerExpr(t.slice(2)) }); continue; }
|
|
528
|
+
const kv = /^([A-Za-z_]\w*)\s*:\s*([\s\S]+)$/.exec(t);
|
|
529
|
+
props.push({ value: _lowerExpr(kv ? kv[2] : t) });
|
|
530
|
+
}
|
|
531
|
+
return { kind: 'object', props };
|
|
532
|
+
}
|
|
533
|
+
const chain = _parseChain(s);
|
|
534
|
+
if (chain && chain.end >= s.length) return _lowerChain(chain);
|
|
535
|
+
if (chain && chain.base.type === 'path' && !chain.ops.length) return _memberChain(chain.base.text.split('.'));
|
|
536
|
+
return U;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Top-level `==`, `!=`, `<`, `>`, `<=`, `>=`, `&&`, `||`: returns the split or
|
|
540
|
+
// null. Kept simple: first top-level operator found wins.
|
|
541
|
+
function _splitBinary(s) {
|
|
542
|
+
let depth = 0;
|
|
543
|
+
for (let i = 0; i < s.length;) {
|
|
544
|
+
const e = _literalEnd(s, i);
|
|
545
|
+
if (e > i) { i = e; continue; }
|
|
546
|
+
const c = s[i];
|
|
547
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
548
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
549
|
+
else if (depth === 0) {
|
|
550
|
+
const two = s.slice(i, i + 2);
|
|
551
|
+
if (two === '&&' || two === '||' || two === '==' || two === '!=' || two === '<=' || two === '>=') {
|
|
552
|
+
return { op: two, left: s.slice(0, i), right: s.slice(i + 2) };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
i++;
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ─── patterns ────────────────────────────────────────────────────────────────
|
|
561
|
+
const _PAT_KEYWORDS = new Set(['ref', 'mut', 'true', 'false', 'box', 'if', 'in', 'as', 'self', 'Self', 'crate', 'super']);
|
|
562
|
+
|
|
563
|
+
// Lowercase-initial identifiers bound by a pattern. Enum variants and struct
|
|
564
|
+
// names are capitalized in idiomatic Rust; field names before `:` are keys,
|
|
565
|
+
// not bindings.
|
|
566
|
+
function _patternIdents(pat) {
|
|
567
|
+
const out = [];
|
|
568
|
+
const p = String(pat || '');
|
|
569
|
+
const re = /[A-Za-z_]\w*/g;
|
|
570
|
+
let m;
|
|
571
|
+
while ((m = re.exec(p)) !== null) {
|
|
572
|
+
const id = m[0];
|
|
573
|
+
if (_PAT_KEYWORDS.has(id) || id === '_') continue;
|
|
574
|
+
if (!/^[a-z_]/.test(id)) continue;
|
|
575
|
+
const after = p.slice(m.index + id.length);
|
|
576
|
+
if (/^\s*:(?!:)/.test(after)) continue; // `field: pat`
|
|
577
|
+
if (/^\s*\(/.test(after)) continue; // tuple-struct / call-like
|
|
578
|
+
if (/^\s*!/.test(after)) continue; // macro
|
|
579
|
+
if (m.index > 0 && p[m.index - 1] === '.') continue; // path segment
|
|
580
|
+
if (/^\s*\./.test(after)) continue; // path prefix
|
|
581
|
+
if (!out.includes(id)) out.push(id);
|
|
582
|
+
}
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function _stripGuard(patText) {
|
|
587
|
+
// `Some(x) if x > 3` → `Some(x)`
|
|
588
|
+
let depth = 0;
|
|
589
|
+
for (let i = 0; i < patText.length;) {
|
|
590
|
+
const e = _literalEnd(patText, i);
|
|
591
|
+
if (e > i) { i = e; continue; }
|
|
592
|
+
const c = patText[i];
|
|
593
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
594
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
595
|
+
else if (depth === 0 && /\s/.test(patText[i - 1] || ' ') && patText.startsWith('if', i) && /\s/.test(patText[i + 2] || '')) {
|
|
596
|
+
return patText.slice(0, i).trim();
|
|
597
|
+
}
|
|
598
|
+
i++;
|
|
599
|
+
}
|
|
600
|
+
return patText.trim();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ─── params ──────────────────────────────────────────────────────────────────
|
|
604
|
+
const _EXTRACTORS = new Set([
|
|
605
|
+
'Query', 'Path', 'Json', 'Form', 'HeaderMap', 'TypedHeader', 'RawQuery', 'RawForm',
|
|
606
|
+
'Multipart', 'MultipartForm', 'HttpRequest', 'Request', 'CookieJar', 'Cookies', 'Host',
|
|
607
|
+
'OriginalUri', 'RawPathParams', 'Bytes',
|
|
608
|
+
]);
|
|
609
|
+
const _ROUTE_ATTR_RE = /#\[\s*(?:rocket\.|actix_web\.|salvo\.)?(?:get|post|put|delete|patch|head|options|route|handler)\s*[(\]]/;
|
|
610
|
+
|
|
611
|
+
function _typeRoot(typeText) {
|
|
612
|
+
const t = String(typeText || '').replace(/^\s*(?:&\s*(?:'\w+\s+)?(?:mut\s+)?|mut\s+)/, '').trim();
|
|
613
|
+
const head = t.split('<')[0].trim();
|
|
614
|
+
const last = head.includes('.') ? head.slice(head.lastIndexOf('.') + 1) : head;
|
|
615
|
+
return last;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// `(?:#\[[^\]]*\]\s*)+` (here, in `_buildCfg`'s attribute-skip below, and
|
|
619
|
+
// inside `_FN_RE`) matches this repo's self-scan gate's "optional/quantified
|
|
620
|
+
// group flanked by `\s*`" ReDoS heuristic, but each iteration is bounded by
|
|
621
|
+
// the MANDATORY literal `#[`…`]` — there is no shared character class between
|
|
622
|
+
// the outer `+` and the inner `\s*`/`[^\]]*` for the engine to backtrack
|
|
623
|
+
// across, so the shape is linear by construction, not merely linear on the
|
|
624
|
+
// inputs tried. Confirmed by direct timing (this repo's own standard for
|
|
625
|
+
// this exact judgment call, per `ir/CLAUDE.md`'s ReDoS notes): 50 000
|
|
626
|
+
// back-to-back `#[a]` attributes and 200 000 non-matching whitespace
|
|
627
|
+
// characters both resolve in single-digit milliseconds.
|
|
628
|
+
function _parseParams(paramsText, hasRouteAttr) {
|
|
629
|
+
const params = [];
|
|
630
|
+
const paramAnnotations = [];
|
|
631
|
+
for (const part of _splitTopLevel(paramsText, ',', { angles: true })) {
|
|
632
|
+
let t = part.text.replace(/^(?:#\[[^\]]*\]\s*)+/, '').trim();
|
|
633
|
+
if (!t) continue;
|
|
634
|
+
if (/^(?:&\s*(?:'\w+\s+)?(?:mut\s+)?|mut\s+)?self\b/.test(t)) { params.push('self'); continue; }
|
|
635
|
+
// split `pattern: Type` at the first top-level colon
|
|
636
|
+
let colon = -1;
|
|
637
|
+
let depth = 0;
|
|
638
|
+
for (let i = 0; i < t.length; i++) {
|
|
639
|
+
const c = t[i];
|
|
640
|
+
if (c === '(' || c === '[' || c === '{' || c === '<') depth++;
|
|
641
|
+
else if (c === ')' || c === ']' || c === '}' || c === '>') depth--;
|
|
642
|
+
else if (c === ':' && depth === 0) { colon = i; break; }
|
|
643
|
+
}
|
|
644
|
+
const pat = colon >= 0 ? t.slice(0, colon).trim() : t;
|
|
645
|
+
const type = colon >= 0 ? t.slice(colon + 1).trim() : '';
|
|
646
|
+
const wrapper = /^([A-Z]\w*)\s*\(/.exec(pat);
|
|
647
|
+
const idents = _patternIdents(pat.replace(/^mut\s+/, ''));
|
|
648
|
+
const root = _typeRoot(type);
|
|
649
|
+
let decorator = null;
|
|
650
|
+
if (wrapper && _EXTRACTORS.has(wrapper[1])) decorator = wrapper[1];
|
|
651
|
+
else if (_EXTRACTORS.has(root)) decorator = root;
|
|
652
|
+
else if (hasRouteAttr) decorator = 'RouteParam';
|
|
653
|
+
for (const id of idents) {
|
|
654
|
+
if (decorator) paramAnnotations.push({ index: params.length, name: id, decorator });
|
|
655
|
+
params.push(id);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return { params, paramAnnotations };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Skip backward over `fn`'s qualifier keywords (pub/pub(...)/const/async/
|
|
662
|
+
// unsafe/default/extern "…") so `_precedingAttributes` (below) can still see
|
|
663
|
+
// an attribute that precedes them, e.g. `#[get("/x")] async fn handler()` —
|
|
664
|
+
// the common shape for every Rust web-framework route macro. Plain bounded
|
|
665
|
+
// literal comparisons, not a regex: this is exactly the kind of fixed,
|
|
666
|
+
// small, disjoint-keyword set that is safe to skip character-by-character
|
|
667
|
+
// with no backtracking possible by construction (same reasoning
|
|
668
|
+
// `_precedingAttributes` itself already relies on for `#[...]`/`]`).
|
|
669
|
+
const _FN_QUALIFIER_KEYWORDS = ['pub', 'const', 'async', 'unsafe', 'default'];
|
|
670
|
+
function _skipFnQualifiersBackward(src, idx) {
|
|
671
|
+
let pos = idx;
|
|
672
|
+
for (;;) {
|
|
673
|
+
while (pos > 0 && /\s/.test(src[pos - 1])) pos--;
|
|
674
|
+
if (src[pos - 1] === ')') {
|
|
675
|
+
// pub(crate)/pub(super)/pub(in ...) — find the matching '(' and check
|
|
676
|
+
// it's immediately preceded by "pub".
|
|
677
|
+
let depth = 0;
|
|
678
|
+
let j = pos - 1;
|
|
679
|
+
for (; j >= 0; j--) {
|
|
680
|
+
if (src[j] === ')') depth++;
|
|
681
|
+
else if (src[j] === '(') { depth--; if (depth === 0) break; }
|
|
682
|
+
}
|
|
683
|
+
if (j >= 3 && src.slice(j - 3, j) === 'pub' && !/\w/.test(src[j - 4] || '')) { pos = j - 3; continue; }
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
if (src[pos - 1] === '"') {
|
|
687
|
+
// extern "C"/"Rust"/etc.
|
|
688
|
+
let j = pos - 2;
|
|
689
|
+
while (j >= 0 && src[j] !== '"') j--;
|
|
690
|
+
if (j <= 0) break;
|
|
691
|
+
const before = src.slice(0, j);
|
|
692
|
+
const m = /extern\s*$/.exec(before);
|
|
693
|
+
if (m) { pos = m.index; continue; }
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
let matched = false;
|
|
697
|
+
for (const kw of _FN_QUALIFIER_KEYWORDS) {
|
|
698
|
+
const start = pos - kw.length;
|
|
699
|
+
if (start >= 0 && src.slice(start, pos) === kw && (start === 0 || !/\w/.test(src[start - 1]))) {
|
|
700
|
+
pos = start;
|
|
701
|
+
matched = true;
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!matched) break;
|
|
706
|
+
}
|
|
707
|
+
return pos;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Attributes immediately preceding `idx`, scanned backwards over `#[…]` groups.
|
|
711
|
+
function _precedingAttributes(src, idx) {
|
|
712
|
+
let pos = idx;
|
|
713
|
+
let attrs = '';
|
|
714
|
+
for (;;) {
|
|
715
|
+
while (pos > 0 && /\s/.test(src[pos - 1])) pos--;
|
|
716
|
+
if (src[pos - 1] !== ']') break;
|
|
717
|
+
let depth = 0;
|
|
718
|
+
let j = pos - 1;
|
|
719
|
+
for (; j >= 0; j--) {
|
|
720
|
+
if (src[j] === ']') depth++;
|
|
721
|
+
else if (src[j] === '[') { depth--; if (depth === 0) break; }
|
|
722
|
+
}
|
|
723
|
+
if (j <= 0 || src[j - 1] !== '#') break;
|
|
724
|
+
attrs = src.slice(j - 1, pos) + '\n' + attrs;
|
|
725
|
+
pos = j - 1;
|
|
726
|
+
}
|
|
727
|
+
return attrs;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ─── CFG construction ────────────────────────────────────────────────────────
|
|
731
|
+
let _nid = 0;
|
|
732
|
+
let _src = '';
|
|
733
|
+
let _lineStarts = [];
|
|
734
|
+
|
|
735
|
+
function _computeLineStarts(s) {
|
|
736
|
+
const starts = [0];
|
|
737
|
+
for (let i = 0; i < s.length; i++) if (s[i] === '\n') starts.push(i + 1);
|
|
738
|
+
return starts;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function _lineForOffset(off) {
|
|
742
|
+
let lo = 0;
|
|
743
|
+
let hi = _lineStarts.length - 1;
|
|
744
|
+
while (lo < hi) {
|
|
745
|
+
const mid = (lo + hi + 1) >> 1;
|
|
746
|
+
if (_lineStarts[mid] <= off) lo = mid; else hi = mid - 1;
|
|
747
|
+
}
|
|
748
|
+
return lo + 1;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function _addNode(nodes, node) {
|
|
752
|
+
const id = `rn${++_nid}`;
|
|
753
|
+
node.succ = node.succ || [];
|
|
754
|
+
node.pred = node.pred || [];
|
|
755
|
+
nodes[id] = node;
|
|
756
|
+
return id;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function _link(nodes, src, dst) {
|
|
760
|
+
if (!nodes[src] || !nodes[dst]) return;
|
|
761
|
+
if (!nodes[src].succ.includes(dst)) nodes[src].succ.push(dst);
|
|
762
|
+
if (!nodes[dst].pred.includes(src)) nodes[dst].pred.push(src);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const _MAX_DEPTH = 60;
|
|
766
|
+
const _DECL_RE = /^(?:pub(?:\([^)]*\))?\s+)?(?:(?:const|async|unsafe|extern\s+"[^"]*")\s+)*(?:fn|struct|enum|impl|trait|use|mod|type|macro_rules)\b/;
|
|
767
|
+
const _CTRL_RE = /^(?:if|while|loop|for|match|unsafe|async)\b|^\{/;
|
|
768
|
+
|
|
769
|
+
function _bindPattern(nodes, prev, patText, sourceExpr, line) {
|
|
770
|
+
let cur = prev;
|
|
771
|
+
for (const id of _patternIdents(patText)) {
|
|
772
|
+
const n = _addNode(nodes, { kind: 'assign', target: id, source: sourceExpr, line });
|
|
773
|
+
_link(nodes, cur, n);
|
|
774
|
+
cur = n;
|
|
775
|
+
}
|
|
776
|
+
return cur;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Where the `{` of a control-flow header is: first `{` at depth 0 in s (after
|
|
780
|
+
// the keyword), literal-aware.
|
|
781
|
+
function _headerBrace(s, from) {
|
|
782
|
+
let depth = 0;
|
|
783
|
+
for (let i = from; i < s.length;) {
|
|
784
|
+
const e = _literalEnd(s, i);
|
|
785
|
+
if (e > i) { i = e; continue; }
|
|
786
|
+
const c = s[i];
|
|
787
|
+
if (c === '(' || c === '[') depth++;
|
|
788
|
+
else if (c === ')' || c === ']') depth--;
|
|
789
|
+
else if (c === '{' && depth === 0) return i;
|
|
790
|
+
i++;
|
|
791
|
+
}
|
|
792
|
+
return -1;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Inline every closure / async block found among a chain's call args.
|
|
796
|
+
function _inlineClosures(chain, stmtText, stmtStart, nodes, prev, depth) {
|
|
797
|
+
let cur = prev;
|
|
798
|
+
const segsSoFar = chain.base.type === 'path' ? chain.base.text.split('.') : ['_expr'];
|
|
799
|
+
for (const op of chain.ops) {
|
|
800
|
+
if (op.kind === 'member') { segsSoFar.push(op.name); continue; }
|
|
801
|
+
if (op.kind === 'index') { segsSoFar.push('[]'); continue; }
|
|
802
|
+
const receiverSegs = segsSoFar.slice(0, -1);
|
|
803
|
+
for (const part of _splitTopLevel(op.argsText, ',')) {
|
|
804
|
+
const t = part.text;
|
|
805
|
+
const absStart = stmtStart + op.argsStart + part.start;
|
|
806
|
+
const cl = _CLOSURE_RE.exec(t);
|
|
807
|
+
let bodyText = null;
|
|
808
|
+
let bodyAbs = 0;
|
|
809
|
+
if (cl) {
|
|
810
|
+
const rest = t.slice(cl[0].length);
|
|
811
|
+
const line = _lineForOffset(absStart);
|
|
812
|
+
for (const p of _splitTopLevel(cl[1], ',', { angles: true })) {
|
|
813
|
+
const pt = p.text.trim();
|
|
814
|
+
if (!pt) continue;
|
|
815
|
+
let colon = -1;
|
|
816
|
+
let d = 0;
|
|
817
|
+
for (let i = 0; i < pt.length; i++) {
|
|
818
|
+
const c = pt[i];
|
|
819
|
+
if (c === '(' || c === '[' || c === '{' || c === '<') d++;
|
|
820
|
+
else if (c === ')' || c === ']' || c === '}' || c === '>') d--;
|
|
821
|
+
else if (c === ':' && d === 0) { colon = i; break; }
|
|
822
|
+
}
|
|
823
|
+
const pat = colon >= 0 ? pt.slice(0, colon) : pt;
|
|
824
|
+
const type = colon >= 0 ? pt.slice(colon + 1) : '';
|
|
825
|
+
const root = _typeRoot(type);
|
|
826
|
+
const wrapper = /^([A-Z]\w*)\s*\(/.exec(pat.trim());
|
|
827
|
+
const extractor = wrapper && _EXTRACTORS.has(wrapper[1]) ? wrapper[1] : (_EXTRACTORS.has(root) ? root : null);
|
|
828
|
+
let source;
|
|
829
|
+
if (extractor) source = { kind: 'call', callee: `__rs_extract__.${extractor}`, args: [] };
|
|
830
|
+
else if (receiverSegs.length && receiverSegs[0] !== '_expr') source = _memberChain(receiverSegs);
|
|
831
|
+
else source = null;
|
|
832
|
+
if (source) cur = _bindPattern(nodes, cur, pat, source, line);
|
|
833
|
+
}
|
|
834
|
+
if (rest.startsWith('{')) {
|
|
835
|
+
const ex = _extractBody(rest, 0);
|
|
836
|
+
if (!ex) continue;
|
|
837
|
+
bodyText = ex.body;
|
|
838
|
+
bodyAbs = absStart + cl[0].length + 1;
|
|
839
|
+
} else {
|
|
840
|
+
bodyText = rest;
|
|
841
|
+
bodyAbs = absStart + cl[0].length;
|
|
842
|
+
}
|
|
843
|
+
} else if (_ASYNC_BLOCK_RE.test(t)) {
|
|
844
|
+
const open = t.indexOf('{');
|
|
845
|
+
const ex = _extractBody(t, open);
|
|
846
|
+
if (!ex) continue;
|
|
847
|
+
bodyText = ex.body;
|
|
848
|
+
bodyAbs = absStart + open + 1;
|
|
849
|
+
}
|
|
850
|
+
if (bodyText != null) cur = _buildCfg(bodyText, bodyAbs, nodes, cur, null, depth + 1);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return cur;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function _hasClosureArg(chain) {
|
|
857
|
+
return chain.ops.some(op => op.kind === 'call' && _splitTopLevel(op.argsText, ',').some(p => _CLOSURE_RE.test(p.text) || _ASYNC_BLOCK_RE.test(p.text)));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Lower a chain, dropping closure/async-block args (they are inlined as CFG
|
|
861
|
+
// nodes instead of being passed as opaque unknowns).
|
|
862
|
+
function _lowerChainNoClosures(chain) {
|
|
863
|
+
const clone = { base: chain.base, ops: chain.ops.map(op => op.kind !== 'call' ? op : {
|
|
864
|
+
...op,
|
|
865
|
+
argsText: _splitTopLevel(op.argsText, ',').filter(p => !_CLOSURE_RE.test(p.text) && !_ASYNC_BLOCK_RE.test(p.text)).map(p => p.text).join(', '),
|
|
866
|
+
}) };
|
|
867
|
+
return _lowerChain(clone);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Builds a control-flow construct that starts at `s` (statement text) whose
|
|
871
|
+
// absolute start is `start`. `tails` collects branch tail expressions when the
|
|
872
|
+
// construct is in expression position. Returns the join node id.
|
|
873
|
+
function _buildControl(s, start, nodes, prev, tails, depth) {
|
|
874
|
+
const line = _lineForOffset(start);
|
|
875
|
+
// if / if let … else if … else
|
|
876
|
+
if (/^if\b/.test(s)) {
|
|
877
|
+
const brace = _headerBrace(s, 2);
|
|
878
|
+
if (brace < 0) return prev;
|
|
879
|
+
const header = s.slice(2, brace).trim();
|
|
880
|
+
const ex = _extractBody(s, brace);
|
|
881
|
+
if (!ex) return prev;
|
|
882
|
+
const ifLet = /^let\s+([\s\S]+?)\s*=\s*([\s\S]+)$/.exec(header);
|
|
883
|
+
const condExpr = _lowerExpr(ifLet ? ifLet[2] : header);
|
|
884
|
+
const ifNode = _addNode(nodes, { kind: 'if', cond: condExpr, line });
|
|
885
|
+
_link(nodes, prev, ifNode);
|
|
886
|
+
const join = _addNode(nodes, { kind: 'noop', line });
|
|
887
|
+
let thenPrev = ifNode;
|
|
888
|
+
if (ifLet) thenPrev = _bindPattern(nodes, ifNode, ifLet[1], condExpr, line);
|
|
889
|
+
const thenTail = _buildCfg(ex.body, start + brace + 1, nodes, thenPrev, tails, depth + 1);
|
|
890
|
+
_link(nodes, thenTail, join);
|
|
891
|
+
const rest = s.slice(ex.end + 1).trim();
|
|
892
|
+
const restStart = start + ex.end + 1 + (s.slice(ex.end + 1).length - s.slice(ex.end + 1).trimStart().length);
|
|
893
|
+
const elseM = /^else\b\s*/.exec(rest);
|
|
894
|
+
if (elseM) {
|
|
895
|
+
const after = rest.slice(elseM[0].length);
|
|
896
|
+
const afterStart = restStart + elseM[0].length;
|
|
897
|
+
if (/^if\b/.test(after)) {
|
|
898
|
+
const t = _buildControl(after, afterStart, nodes, ifNode, tails, depth + 1);
|
|
899
|
+
_link(nodes, t, join);
|
|
900
|
+
} else if (after.startsWith('{')) {
|
|
901
|
+
const eb = _extractBody(after, 0);
|
|
902
|
+
if (eb) {
|
|
903
|
+
const t = _buildCfg(eb.body, afterStart + 1, nodes, ifNode, tails, depth + 1);
|
|
904
|
+
_link(nodes, t, join);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
_link(nodes, ifNode, join);
|
|
909
|
+
}
|
|
910
|
+
return join;
|
|
911
|
+
}
|
|
912
|
+
// while / while let
|
|
913
|
+
if (/^while\b/.test(s)) {
|
|
914
|
+
const brace = _headerBrace(s, 5);
|
|
915
|
+
if (brace < 0) return prev;
|
|
916
|
+
const header = s.slice(5, brace).trim();
|
|
917
|
+
const ex = _extractBody(s, brace);
|
|
918
|
+
if (!ex) return prev;
|
|
919
|
+
const whileLet = /^let\s+([\s\S]+?)\s*=\s*([\s\S]+)$/.exec(header);
|
|
920
|
+
const condExpr = _lowerExpr(whileLet ? whileLet[2] : header);
|
|
921
|
+
const head = _addNode(nodes, { kind: 'loop-header', line });
|
|
922
|
+
_link(nodes, prev, head);
|
|
923
|
+
const ifNode = _addNode(nodes, { kind: 'if', cond: condExpr, line });
|
|
924
|
+
_link(nodes, head, ifNode);
|
|
925
|
+
let bodyPrev = ifNode;
|
|
926
|
+
if (whileLet) bodyPrev = _bindPattern(nodes, ifNode, whileLet[1], condExpr, line);
|
|
927
|
+
const tail = _buildCfg(ex.body, start + brace + 1, nodes, bodyPrev, null, depth + 1);
|
|
928
|
+
_link(nodes, tail, head);
|
|
929
|
+
const join = _addNode(nodes, { kind: 'noop', line });
|
|
930
|
+
_link(nodes, ifNode, join);
|
|
931
|
+
return join;
|
|
932
|
+
}
|
|
933
|
+
// loop
|
|
934
|
+
if (/^loop\b/.test(s)) {
|
|
935
|
+
const brace = _headerBrace(s, 4);
|
|
936
|
+
if (brace < 0) return prev;
|
|
937
|
+
const ex = _extractBody(s, brace);
|
|
938
|
+
if (!ex) return prev;
|
|
939
|
+
const head = _addNode(nodes, { kind: 'loop-header', line });
|
|
940
|
+
_link(nodes, prev, head);
|
|
941
|
+
const tail = _buildCfg(ex.body, start + brace + 1, nodes, head, tails, depth + 1);
|
|
942
|
+
_link(nodes, tail, head);
|
|
943
|
+
const join = _addNode(nodes, { kind: 'noop', line });
|
|
944
|
+
_link(nodes, head, join);
|
|
945
|
+
_link(nodes, tail, join);
|
|
946
|
+
return join;
|
|
947
|
+
}
|
|
948
|
+
// for PAT in EXPR
|
|
949
|
+
if (/^for\b/.test(s)) {
|
|
950
|
+
const brace = _headerBrace(s, 3);
|
|
951
|
+
if (brace < 0) return prev;
|
|
952
|
+
const header = s.slice(3, brace).trim();
|
|
953
|
+
const ex = _extractBody(s, brace);
|
|
954
|
+
if (!ex) return prev;
|
|
955
|
+
const m = /^([\s\S]+?)\s+in\s+([\s\S]+)$/.exec(header);
|
|
956
|
+
const head = _addNode(nodes, { kind: 'loop-header', line });
|
|
957
|
+
_link(nodes, prev, head);
|
|
958
|
+
let bodyPrev = head;
|
|
959
|
+
if (m) bodyPrev = _bindPattern(nodes, head, m[1], _lowerExpr(m[2]), line);
|
|
960
|
+
const tail = _buildCfg(ex.body, start + brace + 1, nodes, bodyPrev, null, depth + 1);
|
|
961
|
+
_link(nodes, tail, head);
|
|
962
|
+
const join = _addNode(nodes, { kind: 'noop', line });
|
|
963
|
+
_link(nodes, head, join);
|
|
964
|
+
return join;
|
|
965
|
+
}
|
|
966
|
+
// match EXPR { arms }
|
|
967
|
+
if (/^match\b/.test(s)) {
|
|
968
|
+
const brace = _headerBrace(s, 5);
|
|
969
|
+
if (brace < 0) return prev;
|
|
970
|
+
const scrutinee = _lowerExpr(s.slice(5, brace).trim());
|
|
971
|
+
const ex = _extractBody(s, brace);
|
|
972
|
+
if (!ex) return prev;
|
|
973
|
+
const matchNode = _addNode(nodes, { kind: 'if', cond: scrutinee, line });
|
|
974
|
+
_link(nodes, prev, matchNode);
|
|
975
|
+
const join = _addNode(nodes, { kind: 'noop', line });
|
|
976
|
+
const body = ex.body;
|
|
977
|
+
const bodyAbs = start + brace + 1;
|
|
978
|
+
let i = 0;
|
|
979
|
+
let armCount = 0;
|
|
980
|
+
while (i < body.length) {
|
|
981
|
+
// find `=>` at depth 0
|
|
982
|
+
let d = 0;
|
|
983
|
+
let arrow = -1;
|
|
984
|
+
let j = i;
|
|
985
|
+
for (; j < body.length;) {
|
|
986
|
+
const e = _literalEnd(body, j);
|
|
987
|
+
if (e > j) { j = e; continue; }
|
|
988
|
+
const c = body[j];
|
|
989
|
+
if (c === '(' || c === '[' || c === '{') d++;
|
|
990
|
+
else if (c === ')' || c === ']' || c === '}') d--;
|
|
991
|
+
else if (d === 0 && c === '=' && body[j + 1] === '>') { arrow = j; break; }
|
|
992
|
+
j++;
|
|
993
|
+
}
|
|
994
|
+
if (arrow < 0) break;
|
|
995
|
+
const patText = _stripGuard(body.slice(i, arrow).replace(/^\s*,?\s*/, ''));
|
|
996
|
+
let k = arrow + 2;
|
|
997
|
+
while (k < body.length && /\s/.test(body[k])) k++;
|
|
998
|
+
let armBody;
|
|
999
|
+
let armAbs;
|
|
1000
|
+
let next;
|
|
1001
|
+
if (body[k] === '{') {
|
|
1002
|
+
const eb = _extractBody(body, k);
|
|
1003
|
+
if (!eb) break;
|
|
1004
|
+
armBody = eb.body;
|
|
1005
|
+
armAbs = bodyAbs + k + 1;
|
|
1006
|
+
next = eb.end + 1;
|
|
1007
|
+
} else {
|
|
1008
|
+
// expression arm: up to the next top-level comma
|
|
1009
|
+
let dd = 0;
|
|
1010
|
+
let q = k;
|
|
1011
|
+
for (; q < body.length;) {
|
|
1012
|
+
const e = _literalEnd(body, q);
|
|
1013
|
+
if (e > q) { q = e; continue; }
|
|
1014
|
+
const c = body[q];
|
|
1015
|
+
if (c === '(' || c === '[' || c === '{') dd++;
|
|
1016
|
+
else if (c === ')' || c === ']' || c === '}') dd--;
|
|
1017
|
+
else if (c === ',' && dd === 0) break;
|
|
1018
|
+
q++;
|
|
1019
|
+
}
|
|
1020
|
+
armBody = body.slice(k, q);
|
|
1021
|
+
armAbs = bodyAbs + k;
|
|
1022
|
+
next = q;
|
|
1023
|
+
}
|
|
1024
|
+
const armLine = _lineForOffset(bodyAbs + i + (body.slice(i).length - body.slice(i).trimStart().length));
|
|
1025
|
+
const armPrev = _bindPattern(nodes, matchNode, patText, scrutinee, armLine);
|
|
1026
|
+
const tail = _buildCfg(armBody, armAbs, nodes, armPrev, tails, depth + 1);
|
|
1027
|
+
_link(nodes, tail, join);
|
|
1028
|
+
armCount++;
|
|
1029
|
+
i = next;
|
|
1030
|
+
while (i < body.length && (/\s/.test(body[i]) || body[i] === ',')) i++;
|
|
1031
|
+
}
|
|
1032
|
+
if (!armCount) _link(nodes, matchNode, join);
|
|
1033
|
+
return join;
|
|
1034
|
+
}
|
|
1035
|
+
// unsafe { } / async { } / async move { } / bare { }
|
|
1036
|
+
const open = s.indexOf('{');
|
|
1037
|
+
if (open >= 0) {
|
|
1038
|
+
const ex = _extractBody(s, open);
|
|
1039
|
+
if (!ex) return prev;
|
|
1040
|
+
return _buildCfg(ex.body, start + open + 1, nodes, prev, tails, depth + 1);
|
|
1041
|
+
}
|
|
1042
|
+
return prev;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function _lowerStmt(s, line) {
|
|
1046
|
+
if (/^return\b/.test(s)) {
|
|
1047
|
+
const rest = s.replace(/^return\b\s*/, '').trim();
|
|
1048
|
+
return { kind: 'return', line, value: rest ? _lowerExpr(rest) : null };
|
|
1049
|
+
}
|
|
1050
|
+
if (/^(?:break|continue)\b/.test(s)) return { kind: 'noop', line };
|
|
1051
|
+
// index write: base[idx] = rhs
|
|
1052
|
+
const idxWrite = /^([A-Za-z_][\w.]*)\s*\[([\s\S]*?)\]\s*(\+|-|\*|\/)?=(?!=)\s*([\s\S]+)$/.exec(s);
|
|
1053
|
+
if (idxWrite && _matchingClose(s, s.indexOf('[')) === s.indexOf(']', s.indexOf('['))) {
|
|
1054
|
+
return { kind: 'call', line, callee: `${idxWrite[1]}.__setitem__`, args: [_lowerExpr(idxWrite[2]), _lowerExpr(idxWrite[4])] };
|
|
1055
|
+
}
|
|
1056
|
+
const assign = /^(\*?[A-Za-z_][\w.]*)\s*(\+|-|\*|\/|%|&|\||\^)?=(?!=)\s*([\s\S]+)$/.exec(s);
|
|
1057
|
+
if (assign && !/^(?:if|while|for|match|let)\b/.test(s)) {
|
|
1058
|
+
const target = assign[1].replace(/^\*/, '');
|
|
1059
|
+
const rhs = _lowerExpr(assign[3]);
|
|
1060
|
+
if (assign[2]) return { kind: 'assign', line, target, source: { kind: 'tpl', parts: [_memberChain(target.split('.')), rhs] } };
|
|
1061
|
+
return { kind: 'assign', line, target, source: rhs };
|
|
1062
|
+
}
|
|
1063
|
+
const expr = _lowerExpr(s);
|
|
1064
|
+
if (expr.kind === 'call') return { kind: 'call', line, callee: expr.callee, args: expr.args };
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function _buildCfg(bodyText, bodyStart, nodes, prevId, tails, depth) {
|
|
1069
|
+
if (depth > _MAX_DEPTH) return prevId;
|
|
1070
|
+
const stmts = _splitStatements(bodyText);
|
|
1071
|
+
let prev = prevId;
|
|
1072
|
+
for (let si = 0; si < stmts.length; si++) {
|
|
1073
|
+
const { text: raw, start: localStart, terminated } = stmts[si];
|
|
1074
|
+
const isLast = si === stmts.length - 1;
|
|
1075
|
+
let s = raw;
|
|
1076
|
+
let start = bodyStart + localStart;
|
|
1077
|
+
// leading attributes on a statement
|
|
1078
|
+
const attr = /^(?:#\[[^\]]*\]\s*)+/.exec(s);
|
|
1079
|
+
if (attr) { s = s.slice(attr[0].length); start += attr[0].length; }
|
|
1080
|
+
if (!s) continue;
|
|
1081
|
+
const line = _lineForOffset(start);
|
|
1082
|
+
if (_DECL_RE.test(s) && !/^(?:const|static)\s+[A-Z_]\w*\s*:/.test(s)) continue; // nested items handled by the top-level scan
|
|
1083
|
+
// const/static locals
|
|
1084
|
+
const cst = /^(?:pub\s+)?(?:const|static)\s+(?:mut\s+)?([A-Za-z_]\w*)\s*:[^=]*=\s*([\s\S]+)$/.exec(s);
|
|
1085
|
+
if (cst) {
|
|
1086
|
+
const n = _addNode(nodes, { kind: 'assign', target: cst[1], source: _lowerExpr(cst[2]), line });
|
|
1087
|
+
_link(nodes, prev, n);
|
|
1088
|
+
prev = n;
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
// let
|
|
1092
|
+
const letM = /^let\s+([\s\S]+?)\s*=\s*([\s\S]+)$/.exec(s);
|
|
1093
|
+
if (letM || /^let\b/.test(s)) {
|
|
1094
|
+
if (!letM) continue; // `let x;` declares without a value
|
|
1095
|
+
let patText = letM[1];
|
|
1096
|
+
const typeIdx = _topLevelColon(patText);
|
|
1097
|
+
if (typeIdx >= 0) patText = patText.slice(0, typeIdx);
|
|
1098
|
+
let rhs = letM[2].trim();
|
|
1099
|
+
const rhsStart = start + s.indexOf(letM[2], letM[0].length - letM[2].length);
|
|
1100
|
+
// let-else: `let PAT = EXPR else { … };`
|
|
1101
|
+
const letElse = /^([\s\S]+?)\s+else\s*\{[\s\S]*\}$/.exec(rhs);
|
|
1102
|
+
let elseBody = null;
|
|
1103
|
+
if (letElse && !/^(?:if|match)\b/.test(rhs)) {
|
|
1104
|
+
const ob = rhs.lastIndexOf('else');
|
|
1105
|
+
const eb = _extractBody(rhs, rhs.indexOf('{', ob));
|
|
1106
|
+
if (eb) { elseBody = { text: eb.body, abs: rhsStart + rhs.indexOf('{', ob) + 1 }; rhs = letElse[1].trim(); }
|
|
1107
|
+
}
|
|
1108
|
+
let sourceExpr;
|
|
1109
|
+
if (_CTRL_RE.test(rhs)) {
|
|
1110
|
+
const branchTails = [];
|
|
1111
|
+
prev = _buildControl(rhs, rhsStart, nodes, prev, branchTails, depth + 1);
|
|
1112
|
+
const branches = branchTails.filter(b => b && b.kind !== 'unknown');
|
|
1113
|
+
sourceExpr = branches.length === 1 ? branches[0] : branches.length ? { kind: 'union', branches } : U;
|
|
1114
|
+
} else if (_CLOSURE_RE.test(rhs)) {
|
|
1115
|
+
continue; // closures stored for later invocation are not modeled
|
|
1116
|
+
} else {
|
|
1117
|
+
const chain = _parseChain(rhs);
|
|
1118
|
+
if (chain && chain.end >= rhs.length && _hasClosureArg(chain)) {
|
|
1119
|
+
sourceExpr = _lowerChainNoClosures(chain);
|
|
1120
|
+
prev = _inlineClosures(chain, rhs, rhsStart, nodes, prev, depth);
|
|
1121
|
+
} else {
|
|
1122
|
+
sourceExpr = _lowerExpr(rhs);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
prev = _bindPattern(nodes, prev, patText, sourceExpr, line);
|
|
1126
|
+
if (elseBody) prev = _buildCfg(elseBody.text, elseBody.abs, nodes, prev, null, depth + 1);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
// control flow at statement position
|
|
1130
|
+
if (_CTRL_RE.test(s)) {
|
|
1131
|
+
const isTail = isLast && !terminated && tails !== undefined;
|
|
1132
|
+
const branchTails = isTail ? [] : null;
|
|
1133
|
+
prev = _buildControl(s, start, nodes, prev, isTail ? (tails || branchTails) : null, depth + 1);
|
|
1134
|
+
if (isTail && tails === null) {
|
|
1135
|
+
const branches = branchTails.filter(b => b && b.kind !== 'unknown');
|
|
1136
|
+
if (branches.length) {
|
|
1137
|
+
const r = _addNode(nodes, { kind: 'return', line, value: branches.length === 1 ? branches[0] : { kind: 'union', branches } });
|
|
1138
|
+
_link(nodes, prev, r);
|
|
1139
|
+
prev = r;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
// expression statement with closure args: keep the call, inline bodies
|
|
1145
|
+
const chain = _parseChain(s);
|
|
1146
|
+
if (chain && chain.end >= s.length && _hasClosureArg(chain)) {
|
|
1147
|
+
const lowered = _lowerChainNoClosures(chain);
|
|
1148
|
+
if (lowered.kind === 'call') {
|
|
1149
|
+
const n = _addNode(nodes, { kind: 'call', line, callee: lowered.callee, args: lowered.args });
|
|
1150
|
+
_link(nodes, prev, n);
|
|
1151
|
+
prev = n;
|
|
1152
|
+
}
|
|
1153
|
+
prev = _inlineClosures(chain, s, start, nodes, prev, depth);
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
// tail expression (no `;`): the block's value
|
|
1157
|
+
if (isLast && !terminated && !/^return\b/.test(s)) {
|
|
1158
|
+
const expr = _lowerExpr(s);
|
|
1159
|
+
if (tails) { tails.push(expr); continue; }
|
|
1160
|
+
if (tails === null) {
|
|
1161
|
+
if (expr.kind === 'call') {
|
|
1162
|
+
const c = _addNode(nodes, { kind: 'call', line, callee: expr.callee, args: expr.args });
|
|
1163
|
+
_link(nodes, prev, c);
|
|
1164
|
+
prev = c;
|
|
1165
|
+
}
|
|
1166
|
+
const r = _addNode(nodes, { kind: 'return', line, value: expr });
|
|
1167
|
+
_link(nodes, prev, r);
|
|
1168
|
+
prev = r;
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const node = _lowerStmt(s, line);
|
|
1173
|
+
if (!node) continue;
|
|
1174
|
+
const id = _addNode(nodes, node);
|
|
1175
|
+
_link(nodes, prev, id);
|
|
1176
|
+
prev = id;
|
|
1177
|
+
}
|
|
1178
|
+
return prev;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function _topLevelColon(s) {
|
|
1182
|
+
let depth = 0;
|
|
1183
|
+
for (let i = 0; i < s.length; i++) {
|
|
1184
|
+
const c = s[i];
|
|
1185
|
+
if (c === '(' || c === '[' || c === '{' || c === '<') depth++;
|
|
1186
|
+
else if (c === ')' || c === ']' || c === '}' || c === '>') depth--;
|
|
1187
|
+
else if (c === ':' && depth === 0) return i;
|
|
1188
|
+
}
|
|
1189
|
+
return -1;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// ─── items ───────────────────────────────────────────────────────────────────
|
|
1193
|
+
// A REAL ReDoS, found via this repo's own self-scan gate after this file
|
|
1194
|
+
// merged (confirmed by direct timing, not the heuristic alone: ~50s on
|
|
1195
|
+
// 1000 repeated `#[a] ` attributes with no trailing `fn`, i.e. the ordinary
|
|
1196
|
+
// shape of a file this parser is scanning that simply never reaches a match).
|
|
1197
|
+
// The original version of this regex chained THREE separately-quantified
|
|
1198
|
+
// constructs before `fn` — an attribute-skip star, an optional `pub`, and a
|
|
1199
|
+
// qualifier-skip star — each one individually timed as linear in isolation
|
|
1200
|
+
// (see the file-header note this repo's own review left on the sibling
|
|
1201
|
+
// `(?:#\[[^\]]*\]\s*)+` shape in `_parseParams`), but a backtracking engine
|
|
1202
|
+
// does not know the alternatives inside a `(?:A|B|C)*` are mutually
|
|
1203
|
+
// exclusive on real input: on eventual overall-match failure (no `fn` ever
|
|
1204
|
+
// reached) it explores every way the star COULD have divided its iterations
|
|
1205
|
+
// among A/B/C, which is exponential regardless of how "obviously
|
|
1206
|
+
// unambiguous" the alternation looks to a human reader — confirmed by
|
|
1207
|
+
// re-timing a version that merged all three into one alternation-based star
|
|
1208
|
+
// and finding it was STILL exponential (100ms+ on just 20 attributes).
|
|
1209
|
+
// The actual fix is not a smarter regex at all: none of the skipped prefix
|
|
1210
|
+
// text was ever used for anything. `_precedingAttributes` (below) already
|
|
1211
|
+
// independently, safely (plain character-by-character backward scan, no
|
|
1212
|
+
// regex) recovers real attribute text for route detection, and neither
|
|
1213
|
+
// `fnKw` nor `name` extraction in the scan loop needs the qualifiers
|
|
1214
|
+
// matched — `\bfn\b` already can't match inside a longer identifier
|
|
1215
|
+
// (`myfn`, `fn_pointer`) on its own, since `\w` includes `_` and a word
|
|
1216
|
+
// boundary requires a transition. Verified linear up to 500 000 repeated
|
|
1217
|
+
// attributes+qualifiers with no trailing `fn` (single-digit ms).
|
|
1218
|
+
const _FN_RE = /\bfn\s+([A-Za-z_]\w*)/g;
|
|
1219
|
+
const _IMPL_RE = /(?:^|[\n;{}])\s*(?:unsafe\s+)?(?:impl|trait)\b/g;
|
|
1220
|
+
|
|
1221
|
+
function _findImplRanges(src) {
|
|
1222
|
+
const ranges = [];
|
|
1223
|
+
_IMPL_RE.lastIndex = 0;
|
|
1224
|
+
let m;
|
|
1225
|
+
while ((m = _IMPL_RE.exec(src)) !== null) {
|
|
1226
|
+
const kwIdx = m.index + m[0].search(/(?:impl|trait)\b/);
|
|
1227
|
+
const brace = _headerBrace(src, kwIdx);
|
|
1228
|
+
if (brace < 0) continue;
|
|
1229
|
+
const ex = _extractBody(src, brace);
|
|
1230
|
+
if (!ex) continue;
|
|
1231
|
+
let header = src.slice(kwIdx, brace).replace(/^(?:impl|trait)\s*/, '');
|
|
1232
|
+
header = header.replace(/^<[^>]*>\s*/, '');
|
|
1233
|
+
const forIdx = header.search(/\bfor\b/);
|
|
1234
|
+
if (forIdx >= 0) header = header.slice(forIdx + 3);
|
|
1235
|
+
const whereIdx = header.search(/\bwhere\b/);
|
|
1236
|
+
if (whereIdx >= 0) header = header.slice(0, whereIdx);
|
|
1237
|
+
header = header.replace(/^\s*&\s*(?:'\w+\s+)?(?:mut\s+)?/, '').trim();
|
|
1238
|
+
const name = _typeRoot(header);
|
|
1239
|
+
ranges.push({ start: brace, end: ex.end, name });
|
|
1240
|
+
_IMPL_RE.lastIndex = brace + 1;
|
|
1241
|
+
}
|
|
1242
|
+
return ranges;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function _enclosingImpl(ranges, idx) {
|
|
1246
|
+
let best = null;
|
|
1247
|
+
for (const r of ranges) {
|
|
1248
|
+
if (idx > r.start && idx < r.end && (!best || r.start > best.start)) best = r;
|
|
1249
|
+
}
|
|
1250
|
+
return best ? best.name : null;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function _qid(file, name, line, body) {
|
|
1254
|
+
const sha = crypto.createHash('sha256').update(body).digest('hex').slice(0, 8);
|
|
1255
|
+
return `${file}::${name}@${line}#${sha}`;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
export function parseRustFile(file, code) {
|
|
1259
|
+
if (!file || typeof code !== 'string') return null;
|
|
1260
|
+
if (!/\.rs$/i.test(file)) return null;
|
|
1261
|
+
if (!code.trim() || code.length > 1_000_000) return null;
|
|
1262
|
+
|
|
1263
|
+
const src = _preprocess(code);
|
|
1264
|
+
_src = src;
|
|
1265
|
+
_lineStarts = _computeLineStarts(src);
|
|
1266
|
+
_nid = 0;
|
|
1267
|
+
const implRanges = _findImplRanges(src);
|
|
1268
|
+
const functions = [];
|
|
1269
|
+
_FN_RE.lastIndex = 0;
|
|
1270
|
+
let m;
|
|
1271
|
+
while ((m = _FN_RE.exec(src)) !== null) {
|
|
1272
|
+
const name = m[1];
|
|
1273
|
+
// _FN_RE is now the bare `\bfn\s+(name)` pattern (see its own header
|
|
1274
|
+
// comment for why), so the match always starts exactly at "fn" itself —
|
|
1275
|
+
// no leading anchor/whitespace/qualifier text is ever consumed into
|
|
1276
|
+
// m[0] anymore.
|
|
1277
|
+
const fnKw = m.index;
|
|
1278
|
+
let i = m.index + m[0].length;
|
|
1279
|
+
// generics
|
|
1280
|
+
while (i < src.length && /\s/.test(src[i])) i++;
|
|
1281
|
+
if (src[i] === '<') {
|
|
1282
|
+
let d = 0;
|
|
1283
|
+
for (; i < src.length; i++) {
|
|
1284
|
+
if (src[i] === '<') d++;
|
|
1285
|
+
else if (src[i] === '>' && src[i - 1] !== '-') { d--; if (d === 0) { i++; break; } }
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
while (i < src.length && /\s/.test(src[i])) i++;
|
|
1289
|
+
if (src[i] !== '(') continue;
|
|
1290
|
+
const paramsClose = _matchingClose(src, i);
|
|
1291
|
+
if (paramsClose < 0) continue;
|
|
1292
|
+
const paramsText = src.slice(i + 1, paramsClose);
|
|
1293
|
+
// find body `{` or a declaration `;`
|
|
1294
|
+
let j = paramsClose + 1;
|
|
1295
|
+
let brace = -1;
|
|
1296
|
+
let d = 0;
|
|
1297
|
+
for (; j < src.length;) {
|
|
1298
|
+
const e = _literalEnd(src, j);
|
|
1299
|
+
if (e > j) { j = e; continue; }
|
|
1300
|
+
const c = src[j];
|
|
1301
|
+
if (c === '(' || c === '[') d++;
|
|
1302
|
+
else if (c === ')' || c === ']') d--;
|
|
1303
|
+
else if (c === ';' && d === 0) break;
|
|
1304
|
+
else if (c === '{' && d === 0) { brace = j; break; }
|
|
1305
|
+
j++;
|
|
1306
|
+
}
|
|
1307
|
+
if (brace < 0) continue;
|
|
1308
|
+
const extracted = _extractBody(src, brace);
|
|
1309
|
+
if (!extracted) continue;
|
|
1310
|
+
const attrs = _precedingAttributes(src, _skipFnQualifiersBackward(src, fnKw));
|
|
1311
|
+
const hasRouteAttr = _ROUTE_ATTR_RE.test(attrs);
|
|
1312
|
+
const { params, paramAnnotations } = _parseParams(paramsText, hasRouteAttr);
|
|
1313
|
+
const startLine = _lineForOffset(fnKw);
|
|
1314
|
+
const className = _enclosingImpl(implRanges, fnKw);
|
|
1315
|
+
const nodes = {};
|
|
1316
|
+
const entry = _addNode(nodes, { kind: 'entry', line: startLine });
|
|
1317
|
+
const exit = _addNode(nodes, { kind: 'exit', line: startLine });
|
|
1318
|
+
let tail;
|
|
1319
|
+
try {
|
|
1320
|
+
tail = _buildCfg(extracted.body, brace + 1, nodes, entry, null, 0);
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
if (err instanceof RangeError) { tail = entry; } else throw err;
|
|
1323
|
+
}
|
|
1324
|
+
_link(nodes, tail, exit);
|
|
1325
|
+
const cfg = { entry, exit, nodes };
|
|
1326
|
+
const qualified = className ? `${className}.${name}` : name;
|
|
1327
|
+
functions.push({
|
|
1328
|
+
qid: _qid(file, qualified, startLine, extracted.body),
|
|
1329
|
+
name: qualified, line: startLine, params, file,
|
|
1330
|
+
cfg,
|
|
1331
|
+
calls: callSitesFromCfg(cfg),
|
|
1332
|
+
...(paramAnnotations.length ? { paramAnnotations } : {}),
|
|
1333
|
+
});
|
|
1334
|
+
// nested fns are found by continuing the scan from inside the body
|
|
1335
|
+
_FN_RE.lastIndex = brace + 1;
|
|
1336
|
+
}
|
|
1337
|
+
return functions.length ? { file, functions, topLevel: null } : null;
|
|
1338
|
+
}
|