@clear-capabilities/agentic-security-scanner 0.136.2 → 0.137.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 +880 -0
- package/bin/agentic-security.js +189 -37
- package/dist/113.index.js +13 -4
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +5 -4
- package/dist/238.index.js +1 -1
- package/dist/317.index.js +36 -6
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +192 -15
- package/dist/444.index.js +20 -11
- package/dist/449.index.js +8 -1
- package/dist/526.index.js +3 -3
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +15 -15
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/nist-privacy-1-1.json +2 -2
- package/dist/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/package.json +21 -13
- package/src/dataflow/CLAUDE.md +12 -4
- package/src/dataflow/builtin-summaries.js +1 -1
- package/src/dataflow/catalog-expanded.js +1 -0
- package/src/dataflow/catalog.js +157 -31
- package/src/dataflow/engine.js +639 -112
- package/src/dataflow/implicit-flow.js +68 -36
- package/src/dataflow/incremental.js +18 -3
- package/src/dataflow/index.js +17 -1
- package/src/dataflow/points-to.js +19 -6
- package/src/dataflow/proven-clean.js +41 -0
- package/src/dataflow/sanitizer-gate.js +35 -9
- package/src/dataflow/sanitizer-proof.js +21 -3
- package/src/dataflow/stub-aware-filter.js +36 -13
- package/src/dataflow/summaries.js +21 -2
- package/src/engine.js +430 -196
- package/src/ir/CLAUDE.md +16 -2
- package/src/ir/balanced-call.js +55 -0
- package/src/ir/class-hierarchy.js +57 -11
- package/src/ir/index.js +14 -2
- package/src/ir/parser-cs.js +513 -40
- package/src/ir/parser-go.js +29 -11
- package/src/ir/parser-java.js +300 -20
- package/src/ir/parser-js.js +300 -22
- package/src/ir/parser-kt.js +436 -18
- package/src/ir/parser-php.js +631 -38
- package/src/ir/parser-py.helper.py +32 -2
- package/src/ir/parser-py.js +31 -4
- package/src/ir/parser-rb.js +161 -26
- package/src/ir/ssa.js +6 -1
- package/src/lsp/server.js +35 -3
- package/src/mcp/CLAUDE.md +9 -2
- package/src/mcp/redact.js +26 -0
- package/src/mcp/tools.js +164 -15
- package/src/posture/CLAUDE.md +19 -7
- package/src/posture/accuracy-scorecard.js +9 -1
- package/src/posture/aibom.js +12 -8
- package/src/posture/auditor-walkthrough.js +102 -3
- package/src/posture/autopilot.js +8 -1
- package/src/posture/calibration-drift.js +11 -5
- package/src/posture/calibration.js +24 -2
- package/src/posture/clustering.js +12 -1
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +2 -2
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/src/posture/compliance-policy.js +33 -1
- package/src/posture/confidence.js +44 -10
- package/src/posture/corpus-enroll.js +9 -5
- package/src/posture/corpus-match.js +19 -0
- package/src/posture/csharp-analysis.js +62 -3
- package/src/posture/deploy-platform.js +4 -1
- package/src/posture/drift.js +7 -1
- package/src/posture/epss.js +13 -1
- package/src/posture/evidence-bundle.js +36 -6
- package/src/posture/exploitability-probability.js +13 -1
- package/src/posture/falsification.js +23 -2
- package/src/posture/fix-metrics.js +1 -1
- package/src/posture/fix-verify-loop.js +10 -1
- package/src/posture/iac-reachability.js +14 -8
- package/src/posture/integrity.js +25 -7
- package/src/posture/model-rescan.js +65 -0
- package/src/posture/mttr.js +5 -0
- package/src/posture/poc-inprocess.js +27 -8
- package/src/posture/regression-test-gen.js +23 -8
- package/src/posture/reverse-blast-radius.js +5 -1
- package/src/posture/risk-dollars.js +18 -1
- package/src/posture/sbom.js +2 -2
- package/src/posture/secret-history.js +20 -11
- package/src/posture/security-trend.js +7 -1
- package/src/posture/stack-playbook.js +22 -1
- package/src/posture/threat-model-grounding.js +2 -2
- package/src/posture/validator-metrics.js +10 -3
- package/src/posture/verifier.js +32 -57
- package/src/report/index.js +183 -14
- package/src/runScan.js +1 -1
- package/src/sast/_comment-strip.js +15 -4
- package/src/sast/_secret-entropy.js +1 -1
- package/src/sast/authz.js +6 -4
- package/src/sast/bench-shape/index.js +2 -7
- package/src/sast/claude-md-prompt-injection.js +14 -3
- package/src/sast/cloud-iam.js +60 -7
- package/src/sast/cpp-bench-extras.js +1 -1
- package/src/sast/csrf.js +7 -5
- package/src/sast/env-hygiene.js +5 -2
- package/src/sast/iac-terraform.js +25 -0
- package/src/sast/java-bench-extras.js +1 -1
- package/src/sast/java-constant-fold.js +5 -5
- package/src/sast/llm-owasp.js +4 -2
- package/src/sast/mcp-audit.js +7 -0
- package/src/sast/pipeline.js +8 -0
- package/src/sast/prompt-template.js +8 -6
- package/src/sast/prototype-pollution.js +6 -2
- package/src/sast/redos-nfa.js +6 -6
- package/src/sast/secret-concat.js +13 -2
- package/src/sast/ssrf-cloud-metadata.js +6 -3
- package/src/sast/xss-reflected-multilang.js +1 -1
- package/src/sast/xxe.js +1 -1
- package/src/sca/CLAUDE.md +3 -4
- package/src/sca/container.js +35 -3
- package/src/sca/dep-confusion.js +7 -0
- package/src/sca/sarif-ingest.js +0 -187
package/src/ir/parser-cs.js
CHANGED
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
// - method calls (statement-form): `obj.Method(args);` / `Method(args);`
|
|
12
12
|
// - return: `return expr;`
|
|
13
13
|
// - ASP.NET source-like access: `Request.Form["x"]`, `Request.QueryString[...]`
|
|
14
|
+
// - control flow (R8): `if`/`else`/`else if`/`while`/`for`/`foreach`/
|
|
15
|
+
// `switch`/`do`/`try`/`catch`/`finally` bodies are recursed into by
|
|
16
|
+
// `_buildCfg` (ported from parser-cpp.js's proven keyword+balanced-scan
|
|
17
|
+
// pattern), so a sink several levels deep inside a braced body is
|
|
18
|
+
// reachable. A `for` header's init clause becomes a real assign node
|
|
19
|
+
// (not just its test clause as the condition); a `foreach` header
|
|
20
|
+
// binds its loop variable to the iterated collection before the body
|
|
21
|
+
// is recursed into, so the loop variable itself carries taint
|
|
22
|
+
// provenance. Line numbers through this recursion are computed via
|
|
23
|
+
// exact character-offset lookup (`_lineStarts`/`_lineForOffset`), not
|
|
24
|
+
// approximated — see `_buildCfg`'s header comment.
|
|
14
25
|
//
|
|
15
26
|
// What we do NOT model (regex-fallback class limits):
|
|
16
27
|
// - LINQ expressions (treated as opaque expression)
|
|
@@ -19,14 +30,24 @@
|
|
|
19
30
|
// - generics on declarations beyond Type<...> name
|
|
20
31
|
// - attributes (skipped)
|
|
21
32
|
// - destructuring / tuples
|
|
22
|
-
// - control
|
|
23
|
-
//
|
|
33
|
+
// - control-flow BRANCHING semantics: `if`/`else`, `switch` cases, and
|
|
34
|
+
// `try`/`catch`/`finally` clauses are each recursed into and linked
|
|
35
|
+
// SEQUENTIALLY (matching parser-cpp.js's own "linear but complete"
|
|
36
|
+
// approximation) rather than as alternative/exceptional paths — every
|
|
37
|
+
// branch's body is reachable in the CFG, which is what taint analysis
|
|
38
|
+
// needs, but the graph does not model that only one branch executes
|
|
39
|
+
// per run.
|
|
40
|
+
// - a comment appearing MID-statement (after real content has already
|
|
41
|
+
// started) is left as literal text, not stripped — only a comment
|
|
42
|
+
// that precedes a statement (the common real-world shape) is skipped
|
|
43
|
+
// by `_splitStatements`; see that function's header comment.
|
|
24
44
|
//
|
|
25
45
|
// This is a v1. Promoted to a Roslyn-backed CST parser (analogous to
|
|
26
46
|
// parser-py-cst.js) once we have a dotnet capability probe.
|
|
27
47
|
|
|
28
48
|
import * as crypto from 'node:crypto';
|
|
29
49
|
import { callSitesFromCfg } from './call-sites.js';
|
|
50
|
+
import { matchBalancedCall } from './balanced-call.js';
|
|
30
51
|
|
|
31
52
|
const METHOD_RE = new RegExp(
|
|
32
53
|
'(?:^|[\\s;{}])(?:public|private|protected|internal|static|virtual|override|async|sealed|abstract|new|readonly|partial)' +
|
|
@@ -36,30 +57,195 @@ const METHOD_RE = new RegExp(
|
|
|
36
57
|
'\\s*\\(([^)]*)\\)' + // params (group 3)
|
|
37
58
|
'\\s*\\{', 'g');
|
|
38
59
|
|
|
39
|
-
// Matches a top-level statement inside a method body.
|
|
40
|
-
//
|
|
60
|
+
// Matches a top-level statement inside a method body. Splits on `;` at
|
|
61
|
+
// brace-depth 0 (keeping simple lambdas inside calls intact), AND — R8 —
|
|
62
|
+
// also flushes on a `}` that returns the SAME shared depth counter to 0.
|
|
63
|
+
// That second trigger is what makes a braced control-flow body
|
|
64
|
+
// (`if (...) { ... }`) come back as its OWN statement, ready for
|
|
65
|
+
// `_buildCfg` to recurse into, instead of staying glued to whatever `;`
|
|
66
|
+
// happens to terminate the NEXT statement (the old, pre-R8 behavior,
|
|
67
|
+
// which is why control flow was invisible before this task).
|
|
68
|
+
//
|
|
69
|
+
// This is safe for C#'s `{}`-based collection/object initializers
|
|
70
|
+
// (`new Foo { X = 1 }`) and lambda bodies passed as call arguments
|
|
71
|
+
// (`xs.ForEach(x => { Process(x); })`) because `depth` here is ONE shared
|
|
72
|
+
// counter across `{`, `(` and `[` (matching this file's pre-existing
|
|
73
|
+
// convention) — a `}` only reaches depth 0 when EVERY enclosing brace,
|
|
74
|
+
// paren and bracket has also closed, so a collection initializer's `}`
|
|
75
|
+
// (which closes while the surrounding `(...)` of a call, or the
|
|
76
|
+
// surrounding `;`-terminated `var x = ...` is still "open" only in the
|
|
77
|
+
// sense of not yet having hit a flush point) or a lambda body's `}`
|
|
78
|
+
// (which closes while the outer call's `(` is still open, i.e. depth > 0)
|
|
79
|
+
// never trips this trigger. Only a `}` that is truly the LAST unmatched
|
|
80
|
+
// delimiter does — precisely the shape a control-flow body's closing
|
|
81
|
+
// brace has.
|
|
82
|
+
//
|
|
83
|
+
// Returns `{ text, start }[]` — `start` is the absolute character offset,
|
|
84
|
+
// within `body`, of the first REAL (non-whitespace) character of `text`.
|
|
85
|
+
// This offset is tracked directly against the ORIGINAL, untouched `body`
|
|
86
|
+
// string (never against a reconstructed/trimmed copy), so `_buildCfg` can
|
|
87
|
+
// compute exact line numbers via a single `_lineStarts`/`_lineForOffset`
|
|
88
|
+
// pair built once per function body — see that pairing's comment. This
|
|
89
|
+
// is the R8 lesson from the PHP task (3 fix rounds, all ultimately about
|
|
90
|
+
// line-number precision): approximate offsets computed by re-counting
|
|
91
|
+
// newlines in text that has already been trimmed or reconstructed are
|
|
92
|
+
// lossy (a stripped comment, a dropped blank line) in ways that only
|
|
93
|
+
// surface on real multi-line source; an exact offset into the pristine
|
|
94
|
+
// original text cannot drift.
|
|
95
|
+
//
|
|
96
|
+
// Also splits on a `:` at depth 0 that terminates a `switch` body's
|
|
97
|
+
// `case <expr>:` / `default:` label — those labels are not otherwise
|
|
98
|
+
// separated by `;` or `}`, and without this a label stays glued onto
|
|
99
|
+
// whatever real statement follows it, which then fails every shape
|
|
100
|
+
// `_lowerStmt` recognizes and silently drops that statement. Scoped
|
|
101
|
+
// tightly (the accumulated text so far must be EXACTLY `case <expr>` or
|
|
102
|
+
// `default`) so an ordinary ternary's `:` can't mis-fire — a ternary's
|
|
103
|
+
// left-hand accumulated text is never going to equal one of those two
|
|
104
|
+
// shapes. `::` (the `global::`/qualified-name operator) is excluded via
|
|
105
|
+
// the adjacent-character check so a qualified name's colon can never be
|
|
106
|
+
// mistaken for a label terminator.
|
|
107
|
+
//
|
|
108
|
+
// Comment handling: a `//` or `/* */` comment is skipped outright (not
|
|
109
|
+
// merely blanked) while NO real content has been accumulated yet for the
|
|
110
|
+
// statement currently being scanned (i.e. only whitespace so far this
|
|
111
|
+
// cycle) — the common real-world shape of a standalone comment line
|
|
112
|
+
// immediately before a statement. Skipping it here, rather than letting
|
|
113
|
+
// it become part of the statement text, matters because `_lowerStmt`
|
|
114
|
+
// refuses (`startsWith('//')`) any statement text that begins with a
|
|
115
|
+
// comment: without this, a comment on its own line right before e.g.
|
|
116
|
+
// `var cmd = ...;` would glue onto it and silently drop that statement
|
|
117
|
+
// from the CFG. A comment appearing mid-statement (after real content has
|
|
118
|
+
// already started) is left as literal text — this parser has never
|
|
119
|
+
// modelled comments in general, and fixing that broader gap is out of
|
|
120
|
+
// scope for this task.
|
|
41
121
|
function _splitStatements(body) {
|
|
42
122
|
const out = [];
|
|
43
123
|
let buf = '';
|
|
124
|
+
let contentStart = -1; // absolute offset of the first real char of the
|
|
125
|
+
// statement currently being accumulated, or -1
|
|
126
|
+
// if none seen yet.
|
|
44
127
|
let depth = 0;
|
|
45
|
-
let inString = null; // null | '"' | "'"
|
|
128
|
+
let inString = null; // null | '"' | "'"
|
|
46
129
|
let escape = false;
|
|
130
|
+
const push = (c, idx) => {
|
|
131
|
+
buf += c;
|
|
132
|
+
if (contentStart === -1 && !/\s/.test(c)) contentStart = idx;
|
|
133
|
+
};
|
|
134
|
+
const flush = () => {
|
|
135
|
+
const trimmed = buf.trim();
|
|
136
|
+
if (trimmed) out.push({ text: trimmed, start: contentStart });
|
|
137
|
+
buf = '';
|
|
138
|
+
contentStart = -1;
|
|
139
|
+
};
|
|
47
140
|
for (let i = 0; i < body.length; i++) {
|
|
48
141
|
const c = body[i];
|
|
49
|
-
if (
|
|
142
|
+
if (contentStart === -1) {
|
|
143
|
+
if (c === '/' && body[i + 1] === '/') {
|
|
144
|
+
while (i < body.length && body[i] !== '\n') i++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (c === '/' && body[i + 1] === '*') {
|
|
148
|
+
i += 2;
|
|
149
|
+
while (i < body.length && !(body[i] === '*' && body[i + 1] === '/')) i++;
|
|
150
|
+
if (i < body.length) i += 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (escape) { push(c, i); escape = false; continue; }
|
|
50
155
|
if (inString) {
|
|
51
|
-
|
|
156
|
+
push(c, i);
|
|
52
157
|
if (inString === '"' && c === '\\') { escape = true; continue; }
|
|
53
158
|
if (c === inString) inString = null;
|
|
54
159
|
continue;
|
|
55
160
|
}
|
|
56
|
-
if (c === '"' || c === "'") { inString = c;
|
|
57
|
-
if (c === '{' || c === '(' || c === '[') depth++;
|
|
58
|
-
if (c === '}' || c === ')' || c === ']')
|
|
161
|
+
if (c === '"' || c === "'") { inString = c; push(c, i); continue; }
|
|
162
|
+
if (c === '{' || c === '(' || c === '[') { depth++; push(c, i); continue; }
|
|
163
|
+
if (c === '}' || c === ')' || c === ']') {
|
|
164
|
+
depth--;
|
|
165
|
+
push(c, i);
|
|
166
|
+
if (c === '}' && depth === 0) flush();
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (c === ';' && depth === 0) { flush(); continue; }
|
|
170
|
+
if (c === ':' && depth === 0 && body[i + 1] !== ':' && body[i - 1] !== ':') {
|
|
171
|
+
const t = buf.trim();
|
|
172
|
+
if (/^case\s+[\s\S]+$/.test(t) || t === 'default') { flush(); continue; }
|
|
173
|
+
}
|
|
174
|
+
push(c, i);
|
|
175
|
+
}
|
|
176
|
+
flush();
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Build a sorted array of line-start offsets for `text` (index 0 holds the
|
|
181
|
+
// start of line 1, i.e. always 0). Paired with `_lineForOffset` to turn a
|
|
182
|
+
// character offset into an exact 1-based line number in O(log n) — ported
|
|
183
|
+
// verbatim from parser-cpp.js's `_lineStarts`/`_lineForOffset`, the
|
|
184
|
+
// proven reference for this exact-offset-based line computation pattern.
|
|
185
|
+
function _lineStarts(text) {
|
|
186
|
+
const starts = [0];
|
|
187
|
+
for (let i = 0; i < text.length; i++) {
|
|
188
|
+
if (text[i] === '\n') starts.push(i + 1);
|
|
189
|
+
}
|
|
190
|
+
return starts;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _lineForOffset(lineStarts, idx) {
|
|
194
|
+
let lo = 0, hi = lineStarts.length - 1;
|
|
195
|
+
while (lo < hi) {
|
|
196
|
+
const mid = (lo + hi + 1) >> 1;
|
|
197
|
+
if (lineStarts[mid] <= idx) lo = mid; else hi = mid - 1;
|
|
198
|
+
}
|
|
199
|
+
return lo + 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Find the index of the delimiter in `openCh`/`closeCh` that matches the
|
|
203
|
+
// one at `openIdx`, respecting nesting and skipping string-literal
|
|
204
|
+
// content. Returns -1 if unmatched.
|
|
205
|
+
function _matchDelim(text, openIdx, openCh, closeCh) {
|
|
206
|
+
let depth = 0;
|
|
207
|
+
let inStr = null;
|
|
208
|
+
let escape = false;
|
|
209
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
210
|
+
const c = text[i];
|
|
211
|
+
if (escape) { escape = false; continue; }
|
|
212
|
+
if (inStr) {
|
|
213
|
+
if (c === '\\') { escape = true; continue; }
|
|
214
|
+
if (c === inStr) inStr = null;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (c === '"' || c === "'") { inStr = c; continue; }
|
|
218
|
+
if (c === openCh) depth++;
|
|
219
|
+
else if (c === closeCh) { depth--; if (depth === 0) return i; }
|
|
220
|
+
}
|
|
221
|
+
return -1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Split a `for` header's `init; test; step` on top-level `;` (respecting
|
|
225
|
+
// nested parens/brackets/strings) so the init clause can be surfaced as a
|
|
226
|
+
// real assign node and the test clause used as the loop's condition —
|
|
227
|
+
// mirroring parser-cpp.js's `_splitTopLevelAligned` use for the same
|
|
228
|
+
// C-style for-loop shape.
|
|
229
|
+
function _splitTopLevelSemi(s) {
|
|
230
|
+
const out = [];
|
|
231
|
+
let buf = '';
|
|
232
|
+
let depth = 0;
|
|
233
|
+
let inStr = null;
|
|
234
|
+
for (let i = 0; i < s.length; i++) {
|
|
235
|
+
const c = s[i];
|
|
236
|
+
if (inStr) {
|
|
237
|
+
buf += c;
|
|
238
|
+
if (c === '\\') { i++; buf += s[i] || ''; continue; }
|
|
239
|
+
if (c === inStr) inStr = null;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (c === '"' || c === "'") { inStr = c; buf += c; continue; }
|
|
243
|
+
if (c === '(' || c === '{' || c === '[') depth++;
|
|
244
|
+
if (c === ')' || c === '}' || c === ']') depth--;
|
|
59
245
|
if (c === ';' && depth === 0) { out.push(buf.trim()); buf = ''; continue; }
|
|
60
246
|
buf += c;
|
|
61
247
|
}
|
|
62
|
-
|
|
248
|
+
out.push(buf.trim());
|
|
63
249
|
return out;
|
|
64
250
|
}
|
|
65
251
|
|
|
@@ -80,17 +266,69 @@ function _lowerExpr(text) {
|
|
|
80
266
|
if (parts.length === 1) return { kind: 'ident', name: parts[0] };
|
|
81
267
|
return _buildMemberChain(parts);
|
|
82
268
|
}
|
|
83
|
-
//
|
|
84
|
-
|
|
269
|
+
// Object creation: `new Type(args)` — lowered to a call so taint flows into
|
|
270
|
+
// constructor arguments. Without this branch the expression fell through to
|
|
271
|
+
// the concat heuristic below, and because the `+` sits INSIDE the parens
|
|
272
|
+
// `_splitTopLevelPlus` returned the input unchanged, so that branch recursed
|
|
273
|
+
// on the identical string until the stack blew. `buildProjectIR` catches
|
|
274
|
+
// per-file, so the crash surfaced only as "this file has no IR" — 12 of 21 C#
|
|
275
|
+
// corpus entries, and the catalog's own `cs-sqlcommand` rule ("SQL Injection
|
|
276
|
+
// (new SqlCommand with concatenated user input)") could never fire.
|
|
277
|
+
const newMatch = matchBalancedCall(s, /^new\s+([\w.]+)/);
|
|
278
|
+
if (newMatch) {
|
|
279
|
+
const callee = newMatch.callee.split('.').pop();
|
|
280
|
+
const args = _splitTopLevelCommas(newMatch.argsText).map(_lowerExpr);
|
|
281
|
+
return { kind: 'call', callee, args, isNew: true };
|
|
282
|
+
}
|
|
283
|
+
// Call: foo.bar(args) or Bar(args). matchBalancedCall finds the paren
|
|
284
|
+
// that actually balances the FIRST '(' — not the greedy-to-end-of-string
|
|
285
|
+
// match the old `/\((.*)\)\s*$/` used, which corrupted the argument text
|
|
286
|
+
// for a chained call (`Sanitize(x).Trim()` produced argsText="x).Trim(",
|
|
287
|
+
// which then fell through to {kind:'unknown'} and silently dropped x).
|
|
288
|
+
const callMatch = matchBalancedCall(s, /^([\w.]+)/);
|
|
85
289
|
if (callMatch) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
return { kind: 'call', callee, args };
|
|
290
|
+
const args = _splitTopLevelCommas(callMatch.argsText).map(_lowerExpr);
|
|
291
|
+
return { kind: 'call', callee: callMatch.callee, args };
|
|
89
292
|
}
|
|
90
293
|
// String concat / interpolation — heuristic.
|
|
294
|
+
//
|
|
295
|
+
// The `parts.length > 1` guard is load-bearing, not defensive tidiness: when
|
|
296
|
+
// the `+` is nested inside parens or brackets, `_splitTopLevelPlus` returns
|
|
297
|
+
// the input as a single part, and mapping `_lowerExpr` over it recurses on the
|
|
298
|
+
// identical string forever. Any future expression form that reaches here
|
|
299
|
+
// unsplit would otherwise reintroduce the same stack overflow.
|
|
91
300
|
if (s.includes('+') && /["']/.test(s)) {
|
|
92
|
-
const
|
|
93
|
-
return { kind: 'tpl', parts };
|
|
301
|
+
const rawParts = _splitTopLevelPlus(s);
|
|
302
|
+
if (rawParts.length > 1) return { kind: 'tpl', parts: rawParts.map(_lowerExpr) };
|
|
303
|
+
}
|
|
304
|
+
// Stage 3 correctness audit (detection depth, per-language-IR):
|
|
305
|
+
// interpolated strings ($"id={id}", $@"...", @$"...") were entirely
|
|
306
|
+
// unrecognized by every branch above — not even treated as an opaque
|
|
307
|
+
// literal, since nothing here tested for the leading `$` prefix — so
|
|
308
|
+
// they fell all the way through to {kind:'unknown'}, silently dropping
|
|
309
|
+
// any interpolated variable's taint. This is exactly
|
|
310
|
+
// `new SqlCommand($"SELECT ... WHERE id={id}", conn)`, one of the most
|
|
311
|
+
// common real C# SQL-injection shapes. `{expr}` / `{expr:format}` are
|
|
312
|
+
// lowered into a template, same shape as the `+`-concat branch above.
|
|
313
|
+
if (/^\$@?"/.test(s) || /^@\$"/.test(s)) {
|
|
314
|
+
const bodyStart = s.indexOf('"') + 1;
|
|
315
|
+
const inner = s.slice(bodyStart, -1);
|
|
316
|
+
const re = /\{([^{}:]+)(?::[^{}]*)?\}/g;
|
|
317
|
+
let lastIndex = 0;
|
|
318
|
+
const parts = [];
|
|
319
|
+
let matched = false;
|
|
320
|
+
let m;
|
|
321
|
+
while ((m = re.exec(inner)) !== null) {
|
|
322
|
+
matched = true;
|
|
323
|
+
if (m.index > lastIndex) parts.push({ kind: 'literal', value: inner.slice(lastIndex, m.index) });
|
|
324
|
+
parts.push(_lowerExpr(m[1].trim()));
|
|
325
|
+
lastIndex = re.lastIndex;
|
|
326
|
+
}
|
|
327
|
+
if (matched) {
|
|
328
|
+
if (lastIndex < inner.length) parts.push({ kind: 'literal', value: inner.slice(lastIndex) });
|
|
329
|
+
return { kind: 'tpl', parts };
|
|
330
|
+
}
|
|
331
|
+
return { kind: 'literal', value: s };
|
|
94
332
|
}
|
|
95
333
|
if (/^"|^@"/.test(s)) return { kind: 'literal', value: s };
|
|
96
334
|
if (/^\d/.test(s)) return { kind: 'literal', value: s };
|
|
@@ -169,9 +407,9 @@ function _lowerStmt(stmt, line) {
|
|
|
169
407
|
return { kind: 'assign', line, target, source: _lowerExpr(sourceText) };
|
|
170
408
|
}
|
|
171
409
|
// statement-form call
|
|
172
|
-
const cm = s
|
|
410
|
+
const cm = matchBalancedCall(s, /^([A-Za-z_][\w.]*)/);
|
|
173
411
|
if (cm) {
|
|
174
|
-
return { kind: 'call', line, callee: cm
|
|
412
|
+
return { kind: 'call', line, callee: cm.callee, args: _splitTopLevelCommas(cm.argsText).map(_lowerExpr) };
|
|
175
413
|
}
|
|
176
414
|
return { kind: 'unknown', line, text: s };
|
|
177
415
|
}
|
|
@@ -210,6 +448,185 @@ function _qid(file, name, line, body) {
|
|
|
210
448
|
return `${file}::${name}@${line}#${sha}`;
|
|
211
449
|
}
|
|
212
450
|
|
|
451
|
+
// Node-id counter for `_buildCfg`. Reset to 0 per function (see
|
|
452
|
+
// `parseCSharpFile`) so ids stay `n0`, `n1`, ... within a single
|
|
453
|
+
// function's `cfg.nodes` — matching the pre-R8 flat loop's `n${idx}`
|
|
454
|
+
// naming convention, just keyed off a running node COUNT now rather than
|
|
455
|
+
// the original statement array's index (a single top-level statement, an
|
|
456
|
+
// `if` block, can now expand into many CFG nodes, so id generation can no
|
|
457
|
+
// longer be tied to statement position). Confirmed by grep that nothing
|
|
458
|
+
// in this file, its tests, or the dataflow engine depends on the exact
|
|
459
|
+
// string shape of these ids.
|
|
460
|
+
let _csNid = 0;
|
|
461
|
+
function _nextNodeId() { return `n${_csNid++}`; }
|
|
462
|
+
|
|
463
|
+
function _addNode(nodes, node) {
|
|
464
|
+
const id = _nextNodeId();
|
|
465
|
+
node.succ = node.succ || [];
|
|
466
|
+
node.pred = node.pred || [];
|
|
467
|
+
nodes[id] = node;
|
|
468
|
+
return id;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function _linkNodes(nodes, src, dst) {
|
|
472
|
+
if (!nodes[src] || !nodes[dst]) return;
|
|
473
|
+
if (!nodes[src].succ.includes(dst)) nodes[src].succ.push(dst);
|
|
474
|
+
if (!nodes[dst].pred.includes(src)) nodes[dst].pred.push(src);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// R8: recursive statement-splitting + CFG builder, replacing the previous
|
|
478
|
+
// flat single-pass loop. Ported from parser-cpp.js's `emit()` — the
|
|
479
|
+
// already-proven, working reference for exactly this shape of problem in
|
|
480
|
+
// this codebase's hand-rolled-parser style: match a leading control-flow
|
|
481
|
+
// keyword, balanced-scan its condition and its `{...}` body, and recurse
|
|
482
|
+
// ONLY into that matched body. Every other `}` (a collection initializer's
|
|
483
|
+
// or a lambda's) is left alone by this mechanism — see `_splitStatements`'
|
|
484
|
+
// header comment for why those are never mistaken for a control-flow
|
|
485
|
+
// body's close.
|
|
486
|
+
//
|
|
487
|
+
// Line numbers are computed EXACTLY, not approximated: `lineStarts` is
|
|
488
|
+
// built once, by the caller, from the function's whole (untouched) body
|
|
489
|
+
// text, and `baseAbs` is threaded through every recursive call as the
|
|
490
|
+
// absolute offset — within that SAME body text — of `bodyText[0]`. Every
|
|
491
|
+
// statement's line is then `funcStartLine + _lineForOffset(lineStarts,
|
|
492
|
+
// baseAbs + stmt.start) - 1`. Because `baseAbs` and every sub-offset used
|
|
493
|
+
// below (`afterHeader`, `lead`, matched-delimiter indices) are all
|
|
494
|
+
// measured directly against the statement's own text — which
|
|
495
|
+
// `_splitStatements` guarantees is a byte-for-byte contiguous slice of
|
|
496
|
+
// the original body from `stmt.start` onward (see that function's
|
|
497
|
+
// comment) — this cannot drift the way a newline-recount over
|
|
498
|
+
// already-trimmed/reconstructed text can. That drift is exactly what cost
|
|
499
|
+
// the PHP port of this same task 3 fix rounds; getting the exact-offset
|
|
500
|
+
// version right from the start avoids repeating it here.
|
|
501
|
+
function _buildCfg(bodyText, nodes, prevId, funcStartLine, lineStarts, baseAbs, depth = 0) {
|
|
502
|
+
if (depth > 12) return prevId;
|
|
503
|
+
let prev = prevId;
|
|
504
|
+
for (const { text: s, start } of _splitStatements(bodyText)) {
|
|
505
|
+
if (!s) continue;
|
|
506
|
+
const absStart = baseAbs + start;
|
|
507
|
+
const line = funcStartLine + _lineForOffset(lineStarts, absStart) - 1;
|
|
508
|
+
|
|
509
|
+
// R8 fix round 1: `using (...) { }` and `lock (...) { }` were missing
|
|
510
|
+
// from this alternation entirely — both lowered to a bogus
|
|
511
|
+
// `call:using`/`call:lock` node via the generic `_lowerStmt` fallback,
|
|
512
|
+
// with their `{...}` body text (including a paren argument list that
|
|
513
|
+
// looks exactly like a call's) silently discarded. `using` is THE
|
|
514
|
+
// canonical C#/ADO.NET wrapper around the exact sinks this task
|
|
515
|
+
// targets (`SqlCommand`, `ExecuteReader`, file streams — anything
|
|
516
|
+
// `IDisposable`), so this was a significant real-world gap: a sink
|
|
517
|
+
// wrapped in `using` produced ZERO findings even after this task's
|
|
518
|
+
// main fix, same as if it were wrapped in `if` before this task
|
|
519
|
+
// existed at all. `using`/`lock` are deliberately NOT added to
|
|
520
|
+
// `needsCond` below — unlike `if`/`while`/`for`/`switch`, their
|
|
521
|
+
// parenthesised clause is a resource-acquisition declaration or a
|
|
522
|
+
// lock target, not a boolean expression, so lowering it via
|
|
523
|
+
// `_lowerExpr` would just produce `{kind:'unknown'}` and isn't worth
|
|
524
|
+
// a synthetic node; the body — where the real sink-bearing statements
|
|
525
|
+
// live — is what this fix makes reachable.
|
|
526
|
+
const hm = s.match(/^(if|while|for|foreach|switch|else\s+if|else|do|try|catch|finally|using|lock)\b/);
|
|
527
|
+
if (hm) {
|
|
528
|
+
const kwNorm = hm[1].replace(/\s+/g, ' ').trim();
|
|
529
|
+
let p = hm[0].length;
|
|
530
|
+
while (p < s.length && /\s/.test(s[p])) p++;
|
|
531
|
+
let condRaw = null, afterHeader = p;
|
|
532
|
+
if (s[p] === '(') {
|
|
533
|
+
const closeIdx = _matchDelim(s, p, '(', ')');
|
|
534
|
+
if (closeIdx !== -1) {
|
|
535
|
+
condRaw = s.slice(p + 1, closeIdx);
|
|
536
|
+
afterHeader = closeIdx + 1;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (kwNorm === 'foreach' && condRaw !== null) {
|
|
541
|
+
// `foreach (var x in xs)` / `foreach (Type x in xs)`. Unlike the
|
|
542
|
+
// other headers below, foreach's parenthesised clause is not an
|
|
543
|
+
// expression — it's a declaration — so it gets its own
|
|
544
|
+
// loop-header node (no `cond`) plus, R8 fix-round lesson from
|
|
545
|
+
// Java's for-each gap: a synthesized assign binding the loop
|
|
546
|
+
// variable to the iterated collection BEFORE the body is
|
|
547
|
+
// recursed into. Without this, the body is reachable but the
|
|
548
|
+
// loop variable itself carries no taint provenance, so
|
|
549
|
+
// `foreach (var id in ids) { sink(id); }` could never fire even
|
|
550
|
+
// though `sink(x) { ... }` shapes elsewhere in the same function
|
|
551
|
+
// do.
|
|
552
|
+
const headerId = _addNode(nodes, { kind: 'loop-header', line });
|
|
553
|
+
_linkNodes(nodes, prev, headerId);
|
|
554
|
+
prev = headerId;
|
|
555
|
+
const fm = condRaw.match(/^([\s\S]+?)\s+in\s+([\s\S]+)$/);
|
|
556
|
+
if (fm) {
|
|
557
|
+
const declPart = fm[1].trim();
|
|
558
|
+
const loopVar = declPart.split(/\s+/).pop();
|
|
559
|
+
const iterExpr = fm[2].trim();
|
|
560
|
+
if (loopVar && /^[A-Za-z_]\w*$/.test(loopVar)) {
|
|
561
|
+
const assignId = _addNode(nodes, { kind: 'assign', line, target: loopVar, source: _lowerExpr(iterExpr) });
|
|
562
|
+
_linkNodes(nodes, prev, assignId);
|
|
563
|
+
prev = assignId;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
const needsCond = /^(?:if|while|for|switch|else if|catch)$/.test(kwNorm);
|
|
568
|
+
if (needsCond && condRaw !== null) {
|
|
569
|
+
let condForNode = condRaw;
|
|
570
|
+
let initRaw = null;
|
|
571
|
+
if (kwNorm === 'for') {
|
|
572
|
+
// `for (init; test; step)` — surface the test as the
|
|
573
|
+
// condition and the init as a leading assign node (context
|
|
574
|
+
// (a): a C# for-loop commonly initializes a loop variable
|
|
575
|
+
// that the body then reads, e.g. `for (int i = 0; ...)`
|
|
576
|
+
// followed by `arr[i]` inside the body — without this the
|
|
577
|
+
// loop var's taint provenance is never established).
|
|
578
|
+
const parts = _splitTopLevelSemi(condRaw);
|
|
579
|
+
if (parts.length > 1) {
|
|
580
|
+
initRaw = parts[0];
|
|
581
|
+
condForNode = parts[1];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const ifId = _addNode(nodes, { kind: 'if', line, cond: _lowerExpr(condForNode) });
|
|
585
|
+
_linkNodes(nodes, prev, ifId);
|
|
586
|
+
prev = ifId;
|
|
587
|
+
if (kwNorm === 'for' && initRaw && initRaw.trim()) {
|
|
588
|
+
const initNode = _lowerStmt(initRaw.trim(), line);
|
|
589
|
+
if (initNode && initNode.kind === 'assign') {
|
|
590
|
+
const initId = _addNode(nodes, initNode);
|
|
591
|
+
_linkNodes(nodes, prev, initId);
|
|
592
|
+
prev = initId;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const rest = s.slice(afterHeader);
|
|
599
|
+
const lead = rest.match(/^\s*/)[0].length;
|
|
600
|
+
if (rest[lead] === '{') {
|
|
601
|
+
const closeRel = _matchDelim(rest, lead, '{', '}');
|
|
602
|
+
if (closeRel !== -1) {
|
|
603
|
+
const innerBaseAbs = absStart + afterHeader + lead + 1;
|
|
604
|
+
prev = _buildCfg(rest.slice(lead + 1, closeRel), nodes, prev, funcStartLine, lineStarts, innerBaseAbs, depth + 1);
|
|
605
|
+
}
|
|
606
|
+
} else if (rest.trim()) {
|
|
607
|
+
const innerBaseAbs = absStart + afterHeader;
|
|
608
|
+
prev = _buildCfg(rest, nodes, prev, funcStartLine, lineStarts, innerBaseAbs, depth + 1);
|
|
609
|
+
}
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// A bare nested block `{ ... }` with no leading keyword.
|
|
614
|
+
const bare = s.match(/^\{([\s\S]*)\}$/);
|
|
615
|
+
if (bare) {
|
|
616
|
+
const innerBaseAbs = absStart + 1;
|
|
617
|
+
prev = _buildCfg(bare[1], nodes, prev, funcStartLine, lineStarts, innerBaseAbs, depth + 1);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const node = _lowerStmt(s, line);
|
|
622
|
+
if (!node) continue;
|
|
623
|
+
const id = _addNode(nodes, node);
|
|
624
|
+
_linkNodes(nodes, prev, id);
|
|
625
|
+
prev = id;
|
|
626
|
+
}
|
|
627
|
+
return prev;
|
|
628
|
+
}
|
|
629
|
+
|
|
213
630
|
export function parseCSharpFile(file, code) {
|
|
214
631
|
if (!file || typeof code !== 'string') return null;
|
|
215
632
|
const functions = [];
|
|
@@ -218,44 +635,100 @@ export function parseCSharpFile(file, code) {
|
|
|
218
635
|
while ((m = METHOD_RE.exec(code)) !== null) {
|
|
219
636
|
const name = m[2];
|
|
220
637
|
const paramsText = m[3] || '';
|
|
221
|
-
const
|
|
638
|
+
const paramAnnotations = [];
|
|
639
|
+
// `keptIdx` tracks the parameter's position in the FILTERED array — the
|
|
640
|
+
// same array `fn.params` ends up being — not the raw pre-filter split
|
|
641
|
+
// position (`idx` below). It only advances when a fragment actually
|
|
642
|
+
// yields a kept parameter name, so an empty/unparseable comma fragment
|
|
643
|
+
// ahead of an annotated parameter doesn't shift that parameter's
|
|
644
|
+
// recorded `index` off of its real position in `fn.params`. R14(a)
|
|
645
|
+
// final-review fix: Java (`parser-java.js`) and JS/TS (`parser-js.js`)
|
|
646
|
+
// both already compute `index` this way (position in the final
|
|
647
|
+
// `fn.params`, per the plan's Global Constraints); C# was the only one
|
|
648
|
+
// of the three using the raw split index, which silently diverges
|
|
649
|
+
// whenever an earlier fragment is filtered out. Nothing reads
|
|
650
|
+
// `paramAnnotations[i].index` today (confirmed by grep), so this was
|
|
651
|
+
// latent — but the field is kept for future k>1 call-string work, so a
|
|
652
|
+
// silently-wrong producer here would become a real bug once something
|
|
653
|
+
// starts consuming it.
|
|
654
|
+
let keptIdx = 0;
|
|
655
|
+
const params = paramsText.split(',').map((p, idx) => {
|
|
222
656
|
const t = p.trim();
|
|
223
657
|
if (!t) return null;
|
|
658
|
+
// Extract ALL leading [AttributeName] or [AttributeName(...)] patterns (stacked or not).
|
|
659
|
+
// R14(a) Task 6 fix round 1: an earlier version of this regex used one
|
|
660
|
+
// optional `(?:\(...\))?` group with a `\s*` on each side, which left
|
|
661
|
+
// two independent quantifiers both able to consume the same
|
|
662
|
+
// whitespace run on a failed match (no closing `]`) — classic
|
|
663
|
+
// adjacent-quantifier ReDoS, confirmed quadratic (n=32000 chars took
|
|
664
|
+
// ~600ms; caught by this repo's own self-scan gate against its own
|
|
665
|
+
// new code). Rather than accept the engine's own `safe-regex`-backed
|
|
666
|
+
// heuristic flagging a merely-restructured-but-still-single-group
|
|
667
|
+
// version (it does — confirmed empirically linear but still flagged),
|
|
668
|
+
// this instead follows this repo's own precedent (commit `6bd394c`,
|
|
669
|
+
// `class-hierarchy.js`'s qid-tail-stripping fix): split into two
|
|
670
|
+
// alternatives — no-args and with-args — so no quantifier has two
|
|
671
|
+
// ways to consume the same text. Each alternative independently
|
|
672
|
+
// passes `safe-regex`, avoiding reliance on any one detector's
|
|
673
|
+
// judgment call. Verified O(n) (n=256000 chars in well under 1ms)
|
|
674
|
+
// with identical matches on every real attribute shape (stacked,
|
|
675
|
+
// spaced, empty-arg) against the prior single-group version.
|
|
676
|
+
const attrRegex = /^\[\s*([A-Za-z_]\w*)\s*\]|^\[\s*([A-Za-z_]\w*)\s*\([^)]*\)\s*\]/;
|
|
677
|
+
let remaining = t;
|
|
678
|
+
let match;
|
|
679
|
+
const decorators = [];
|
|
680
|
+
while ((match = attrRegex.exec(remaining)) !== null) {
|
|
681
|
+
decorators.push(match[1] || match[2]);
|
|
682
|
+
remaining = remaining.slice(match[0].length).trim();
|
|
683
|
+
}
|
|
224
684
|
// "Type name" → name. "Type<T> name" → name. "Type[] name = default" → name.
|
|
225
|
-
const last =
|
|
226
|
-
|
|
685
|
+
const last = remaining.replace(/=.*$/, '').trim().split(/\s+/).pop();
|
|
686
|
+
const paramName = last && /^[A-Za-z_][\w]*$/.test(last) ? last : null;
|
|
687
|
+
// Add an entry for each decorator found, indexed by position in the
|
|
688
|
+
// FILTERED params array (see `keptIdx` comment above) — not `idx`,
|
|
689
|
+
// the raw pre-filter split position.
|
|
690
|
+
if (paramName) {
|
|
691
|
+
for (const decorator of decorators) {
|
|
692
|
+
paramAnnotations.push({ index: keptIdx, name: paramName, decorator });
|
|
693
|
+
}
|
|
694
|
+
keptIdx++;
|
|
695
|
+
}
|
|
696
|
+
return paramName;
|
|
227
697
|
}).filter(Boolean);
|
|
228
698
|
const braceIdx = code.indexOf('{', m.index + m[0].length - 1);
|
|
229
699
|
if (braceIdx < 0) continue;
|
|
230
700
|
const extracted = _extractBody(code, braceIdx);
|
|
231
701
|
if (!extracted) continue;
|
|
232
702
|
const startLine = _lineAt(code, m.index);
|
|
233
|
-
|
|
234
|
-
//
|
|
703
|
+
// R8 (lesson learned from the PHP port of this task, fix round 3): the
|
|
704
|
+
// body's own start line must be derived from `braceIdx` — the
|
|
705
|
+
// method's ACTUAL opening `{` — not approximated as `startLine + 1`.
|
|
706
|
+
// `startLine` (above) is the line of the METHOD DECLARATION match,
|
|
707
|
+
// which only happens to be one line before the body for a same-line
|
|
708
|
+
// signature; a multi-line signature or Allman brace style would make
|
|
709
|
+
// that approximation wrong by however many lines the signature spans.
|
|
710
|
+
const bodyStartLine = _lineAt(code, braceIdx + 1);
|
|
711
|
+
// Built once per function body; `_buildCfg` looks up every node's
|
|
712
|
+
// line in O(log n) via `_lineForOffset` against this SAME array — see
|
|
713
|
+
// `_buildCfg`'s header comment for why this (not per-statement
|
|
714
|
+
// newline-recounting) is what keeps line numbers exact through
|
|
715
|
+
// arbitrarily deep recursion.
|
|
716
|
+
const lineStarts = _lineStarts(extracted.body);
|
|
717
|
+
// Build the CFG: entry → (recursive statement/control-flow walk) → exit.
|
|
235
718
|
const nodes = {};
|
|
236
719
|
nodes.entry = { kind: 'entry', line: startLine, succ: [], pred: [] };
|
|
237
720
|
nodes.exit = { kind: 'exit', line: startLine, succ: [], pred: [] };
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (!node) continue;
|
|
243
|
-
const id = `n${idx}`;
|
|
244
|
-
nodes[id] = { ...node, succ: [], pred: [prev] };
|
|
245
|
-
nodes[prev].succ.push(id);
|
|
246
|
-
prev = id;
|
|
247
|
-
// Approximate per-statement line advance by counting '\n' in source.
|
|
248
|
-
// (Cheap, good-enough for finding line attribution.)
|
|
249
|
-
stmtLine += (stmts[idx].match(/\n/g) || []).length + 1;
|
|
250
|
-
}
|
|
251
|
-
nodes[prev].succ.push('exit');
|
|
252
|
-
nodes.exit.pred.push(prev);
|
|
721
|
+
_csNid = 0;
|
|
722
|
+
const tail = _buildCfg(extracted.body, nodes, 'entry', bodyStartLine, lineStarts, 0, 0);
|
|
723
|
+
nodes[tail].succ.push('exit');
|
|
724
|
+
nodes.exit.pred.push(tail);
|
|
253
725
|
const cfg = { entry: 'entry', exit: 'exit', nodes };
|
|
254
726
|
functions.push({
|
|
255
727
|
qid: _qid(file, name, startLine, extracted.body),
|
|
256
728
|
name, line: startLine, params, file,
|
|
257
729
|
cfg,
|
|
258
730
|
calls: callSitesFromCfg(cfg),
|
|
731
|
+
...(paramAnnotations.length ? { paramAnnotations } : {}),
|
|
259
732
|
});
|
|
260
733
|
METHOD_RE.lastIndex = extracted.end + 1;
|
|
261
734
|
}
|