@clear-capabilities/agentic-security-scanner 0.76.1 → 0.78.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/bin/.agentic-security/findings.json +320 -9
- package/bin/.agentic-security/last-scan.json +320 -9
- package/bin/.agentic-security/last-scan.json.sig +1 -1
- package/bin/.agentic-security/scan-history.json +17 -377
- package/bin/.agentic-security/streak.json +11 -16
- package/bin/agentic-security.js +33 -2
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/718.index.js +106 -0
- package/dist/824.index.js +126 -0
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +32 -32
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +7 -7
- package/src/.agentic-security/findings.json +5731 -3933
- package/src/.agentic-security/last-scan.json +5731 -3933
- package/src/.agentic-security/last-scan.json.sig +1 -1
- package/src/.agentic-security/scan-history.json +2533 -887
- package/src/.agentic-security/streak.json +11 -16
- package/src/dataflow/.agentic-security/findings.json +52 -24
- package/src/dataflow/.agentic-security/last-scan.json +52 -24
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
- package/src/dataflow/.agentic-security/scan-history.json +101 -134
- package/src/dataflow/.agentic-security/streak.json +8 -10
- package/src/dataflow/async-sequencing.js +16 -7
- package/src/dataflow/builtin-summaries.js +131 -0
- package/src/dataflow/catalog.js +107 -0
- package/src/dataflow/cross-repo.js +75 -1
- package/src/dataflow/engine.js +129 -0
- package/src/dataflow/implicit-flow.js +24 -6
- package/src/dataflow/stub-aware-filter.js +69 -11
- package/src/dataflow/summaries.js +28 -3
- package/src/engine-parallel.js +70 -0
- package/src/engine.js +165 -15
- package/src/ir/.agentic-security/findings.json +757 -16
- package/src/ir/.agentic-security/last-scan.json +757 -16
- package/src/ir/.agentic-security/last-scan.json.sig +1 -1
- package/src/ir/.agentic-security/scan-history.json +545 -138
- package/src/ir/.agentic-security/streak.json +11 -13
- package/src/ir/index.js +22 -1
- package/src/ir/parser-go.js +403 -0
- package/src/ir/parser-js.js +2 -0
- package/src/ir/parser-php.js +330 -0
- package/src/ir/parser-py.helper.py +137 -11
- package/src/ir/parser-rb.js +309 -0
- package/src/posture/.agentic-security/findings.json +407 -84
- package/src/posture/.agentic-security/last-scan.json +407 -84
- package/src/posture/.agentic-security/last-scan.json.sig +1 -1
- package/src/posture/.agentic-security/scan-history.json +16 -4923
- package/src/posture/.agentic-security/streak.json +10 -14
- package/src/posture/calibration.js +14 -0
- package/src/posture/triage.js +13 -0
- package/src/report/.agentic-security/findings.json +6 -5
- package/src/report/.agentic-security/last-scan.json +6 -5
- package/src/report/.agentic-security/last-scan.json.sig +1 -1
- package/src/report/.agentic-security/scan-history.json +3 -300
- package/src/report/.agentic-security/streak.json +7 -8
- package/src/report/index.js +23 -2
- package/src/sast/.agentic-security/findings.json +195 -56
- package/src/sast/.agentic-security/last-scan.json +195 -56
- package/src/sast/.agentic-security/last-scan.json.sig +1 -1
- package/src/sast/.agentic-security/scan-history.json +14 -394
- package/src/sast/.agentic-security/streak.json +10 -13
- package/src/sast/cache-poisoning.js +77 -0
- package/src/sast/comparison-safety.js +73 -0
- package/src/sast/db-taint.js +54 -0
- package/src/sast/graphql.js +127 -0
- package/src/sast/llm-stored-prompt.js +57 -0
- package/src/sast/mutation-xss.js +43 -0
- package/src/sast/nosql-injection.js +5 -0
- package/src/sast/null-byte-injection.js +76 -0
- package/src/sast/redos-nfa.js +338 -0
- package/src/sast/sensitive-data-logging.js +73 -0
- package/src/sast/weak-password-hash.js +77 -0
- package/src/sast/weak-randomness.js +100 -0
- package/src/sca/.agentic-security/findings.json +502 -11
- package/src/sca/.agentic-security/last-scan.json +502 -11
- package/src/sca/.agentic-security/last-scan.json.sig +1 -1
- package/src/sca/.agentic-security/scan-history.json +19 -1
- package/src/sca/.agentic-security/streak.json +6 -6
- package/src/sca/llm-function-extract.js +107 -0
- package/src/sca/vendor-detect.js +91 -0
- package/dist/218.index.js +0 -793
- package/dist/601.index.js +0 -1038
- package/dist/634.index.js +0 -1892
- package/src/integrations/.agentic-security/findings.json +0 -1504
- package/src/integrations/.agentic-security/last-scan.json +0 -1504
- package/src/integrations/.agentic-security/scan-history.json +0 -40
- package/src/integrations/.agentic-security/streak.json +0 -21
- package/src/llm-validator/.agentic-security/findings.json +0 -1891
- package/src/llm-validator/.agentic-security/last-scan.json +0 -1891
- package/src/llm-validator/.agentic-security/last-scan.json.sig +0 -1
- package/src/llm-validator/.agentic-security/scan-history.json +0 -168
- package/src/llm-validator/.agentic-security/streak.json +0 -20
- package/src/lsp/.agentic-security/findings.json +0 -28
- package/src/lsp/.agentic-security/last-scan.json +0 -28
- package/src/lsp/.agentic-security/scan-history.json +0 -79
- package/src/lsp/.agentic-security/streak.json +0 -22
- package/src/mcp/.agentic-security/findings.json +0 -8403
- package/src/mcp/.agentic-security/last-scan.json +0 -8403
- package/src/mcp/.agentic-security/last-scan.json.sig +0 -1
- package/src/mcp/.agentic-security/scan-history.json +0 -1182
- package/src/mcp/.agentic-security/streak.json +0 -22
- package/src/sast/bench-shape/.agentic-security/findings.json +0 -28
- package/src/sast/bench-shape/.agentic-security/last-scan.json +0 -28
- package/src/sast/bench-shape/.agentic-security/scan-history.json +0 -24
- package/src/sast/bench-shape/.agentic-security/streak.json +0 -22
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Ruby IR frontend.
|
|
2
|
+
//
|
|
3
|
+
// Regex-based, follows the parser-cs.js / parser-go.js pattern. Focused on
|
|
4
|
+
// Rails params, ActiveRecord, Kernel methods surface area.
|
|
5
|
+
//
|
|
6
|
+
// What we model:
|
|
7
|
+
// - def / def self. method declarations
|
|
8
|
+
// - var = expr assignments
|
|
9
|
+
// - method calls: obj.method(args) and method(args)
|
|
10
|
+
// - return
|
|
11
|
+
// - each/map/select blocks as loop-header
|
|
12
|
+
//
|
|
13
|
+
// What we do NOT model:
|
|
14
|
+
// - blocks / procs / lambdas as first-class values
|
|
15
|
+
// - metaprogramming (define_method, method_missing)
|
|
16
|
+
// - module_function / protected / private method visibility scoping
|
|
17
|
+
// - control flow (if/unless/while/until/case) — body is straight-line
|
|
18
|
+
//
|
|
19
|
+
// Ruby body extraction: count def/class/module/do/if/unless/while/until/
|
|
20
|
+
// for/case/begin as openers and `end` as closers. Return null on balance
|
|
21
|
+
// failure (heredocs, multi-line strings can confuse the regex parser).
|
|
22
|
+
|
|
23
|
+
import * as crypto from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
const DEF_RE = /(?:^|\n)\s*def\s+(?:self\.)?(\w+[?!=]?)\s*(?:\(([^)]*)\))?/g;
|
|
26
|
+
|
|
27
|
+
function _extractRubyBody(src, defEnd) {
|
|
28
|
+
let depth = 1;
|
|
29
|
+
let i = defEnd;
|
|
30
|
+
let inStr = null;
|
|
31
|
+
let escape = false;
|
|
32
|
+
const openers = /\b(?:def|class|module|do|if|unless|while|until|for|case|begin)\b/;
|
|
33
|
+
while (i < src.length && depth > 0) {
|
|
34
|
+
const c = src[i];
|
|
35
|
+
if (escape) { escape = false; i++; continue; }
|
|
36
|
+
if (inStr) {
|
|
37
|
+
if (c === '\\') { escape = true; i++; continue; }
|
|
38
|
+
if (c === inStr) inStr = null;
|
|
39
|
+
i++; continue;
|
|
40
|
+
}
|
|
41
|
+
if (c === '"' || c === '\'') { inStr = c; i++; continue; }
|
|
42
|
+
if (c === '#') {
|
|
43
|
+
while (i < src.length && src[i] !== '\n') i++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Check for keyword boundaries
|
|
47
|
+
if (/[a-z]/i.test(c)) {
|
|
48
|
+
let word = '';
|
|
49
|
+
const start = i;
|
|
50
|
+
while (i < src.length && /\w/.test(src[i])) { word += src[i]; i++; }
|
|
51
|
+
if (word === 'end' && (start === 0 || /[^.\w]/.test(src[start - 1] || ' '))) {
|
|
52
|
+
depth--;
|
|
53
|
+
} else if (openers.test(word) && (start === 0 || /[^.\w]/.test(src[start - 1] || ' '))) {
|
|
54
|
+
// Only count as opener if not preceded by . (e.g., x.if would be wrong but rare)
|
|
55
|
+
depth++;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
if (depth !== 0) return null;
|
|
62
|
+
// `end` keyword ends at position `i`; body is between defEnd and the start of `end`
|
|
63
|
+
return { body: src.slice(defEnd, i - 3).trimEnd(), end: i };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const _RB_OPENERS = /^(?:if|unless|while|until|for|case|begin|do)\b/;
|
|
67
|
+
const _RB_BLOCK_KW = /\b(?:def|class|module|if|unless|while|until|for|case|begin|do)\b/;
|
|
68
|
+
|
|
69
|
+
function _splitStatements(body) {
|
|
70
|
+
const lines = body.split('\n');
|
|
71
|
+
const out = [];
|
|
72
|
+
let buf = '';
|
|
73
|
+
let depth = 0;
|
|
74
|
+
for (const rawLine of lines) {
|
|
75
|
+
const line = rawLine.trim();
|
|
76
|
+
if (!line || line.startsWith('#')) continue;
|
|
77
|
+
if (depth === 0 && _RB_OPENERS.test(line)) {
|
|
78
|
+
if (buf.trim()) out.push(buf.trim());
|
|
79
|
+
buf = line + '\n';
|
|
80
|
+
for (const m of line.matchAll(/\b(?:if|unless|while|until|for|case|begin|do|def|class|module)\b/g)) depth++;
|
|
81
|
+
if (/\bend\b/.test(line)) depth--;
|
|
82
|
+
if (depth <= 0) { depth = 0; out.push(buf.trim()); buf = ''; }
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (depth > 0) {
|
|
86
|
+
buf += line + '\n';
|
|
87
|
+
for (const m of line.matchAll(/\b(?:if|unless|while|until|for|case|begin|do|def|class|module)\b/g)) depth++;
|
|
88
|
+
const endMatches = line.match(/\bend\b/g);
|
|
89
|
+
if (endMatches) depth -= endMatches.length;
|
|
90
|
+
if (depth <= 0) { depth = 0; out.push(buf.trim()); buf = ''; }
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
out.push(line);
|
|
94
|
+
}
|
|
95
|
+
if (buf.trim()) out.push(buf.trim());
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _lowerExpr(text) {
|
|
100
|
+
const s = String(text || '').trim();
|
|
101
|
+
if (!s) return { kind: 'unknown' };
|
|
102
|
+
// String interpolation before plain literal check
|
|
103
|
+
if (/^".*#\{/.test(s)) {
|
|
104
|
+
const parts = [];
|
|
105
|
+
for (const m of s.matchAll(/#\{([^}]+)\}/g)) parts.push(_lowerExpr(m[1]));
|
|
106
|
+
if (parts.length) return { kind: 'tpl', parts };
|
|
107
|
+
}
|
|
108
|
+
if (/^['"]/.test(s)) return { kind: 'literal', value: s };
|
|
109
|
+
if (/^\d/.test(s)) return { kind: 'literal', value: s };
|
|
110
|
+
if (/^(true|false|nil)\b/.test(s)) return { kind: 'literal', value: s };
|
|
111
|
+
// Symbol
|
|
112
|
+
if (/^:\w+/.test(s)) return { kind: 'literal', value: s };
|
|
113
|
+
// Call: obj.method(args) or method(args)
|
|
114
|
+
const callMatch = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
|
|
115
|
+
if (callMatch) {
|
|
116
|
+
return { kind: 'call', callee: callMatch[1], args: _splitTopLevelCommas(callMatch[2]).map(_lowerExpr) };
|
|
117
|
+
}
|
|
118
|
+
// Method call without parens is very common in Ruby but hard to detect
|
|
119
|
+
// reliably with regex. We handle the explicit-paren form above.
|
|
120
|
+
// Dotted member: obj.prop
|
|
121
|
+
if (/^[A-Za-z_]\w*(?:\.\w+)+$/.test(s)) {
|
|
122
|
+
const parts = s.split('.');
|
|
123
|
+
let cur = { kind: 'ident', name: parts[0] };
|
|
124
|
+
for (let i = 1; i < parts.length; i++) cur = { kind: 'member', object: cur, prop: parts[i] };
|
|
125
|
+
return cur;
|
|
126
|
+
}
|
|
127
|
+
// Hash access: params[:key]
|
|
128
|
+
if (/^[A-Za-z_]\w*\[/.test(s)) {
|
|
129
|
+
const lb = s.indexOf('[');
|
|
130
|
+
const base = s.slice(0, lb);
|
|
131
|
+
return { kind: 'member', object: { kind: 'ident', name: base }, prop: '[]' };
|
|
132
|
+
}
|
|
133
|
+
// Simple ident
|
|
134
|
+
if (/^[A-Za-z_@]\w*$/.test(s)) return { kind: 'ident', name: s };
|
|
135
|
+
// Concat with +
|
|
136
|
+
if (s.includes('+')) {
|
|
137
|
+
const parts = s.split('+').map(p => _lowerExpr(p.trim()));
|
|
138
|
+
return { kind: 'tpl', parts };
|
|
139
|
+
}
|
|
140
|
+
return { kind: 'unknown' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _splitTopLevelCommas(s) {
|
|
144
|
+
const out = [];
|
|
145
|
+
let buf = '';
|
|
146
|
+
let depth = 0;
|
|
147
|
+
let inStr = null;
|
|
148
|
+
for (let i = 0; i < s.length; i++) {
|
|
149
|
+
const c = s[i];
|
|
150
|
+
if (inStr) {
|
|
151
|
+
buf += c;
|
|
152
|
+
if (c === '\\') { i++; buf += s[i] || ''; continue; }
|
|
153
|
+
if (c === inStr) inStr = null;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (c === '"' || c === '\'') { inStr = c; buf += c; continue; }
|
|
157
|
+
if (c === '(' || c === '{' || c === '[') depth++;
|
|
158
|
+
if (c === ')' || c === '}' || c === ']') depth--;
|
|
159
|
+
if (c === ',' && depth === 0) { out.push(buf.trim()); buf = ''; continue; }
|
|
160
|
+
buf += c;
|
|
161
|
+
}
|
|
162
|
+
if (buf.trim()) out.push(buf.trim());
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _lowerStmt(stmt, line) {
|
|
167
|
+
const s = stmt.trim();
|
|
168
|
+
if (!s || s.startsWith('#')) return null;
|
|
169
|
+
if (/^return\b/.test(s)) {
|
|
170
|
+
const rest = s.replace(/^return\s*/, '').trim();
|
|
171
|
+
return { kind: 'return', line, value: rest ? _lowerExpr(rest) : null };
|
|
172
|
+
}
|
|
173
|
+
if (/^raise\b/.test(s)) {
|
|
174
|
+
return { kind: 'throw', line, value: _lowerExpr(s.replace(/^raise\s*/, '')) };
|
|
175
|
+
}
|
|
176
|
+
// Assignment: var = expr
|
|
177
|
+
const assign = s.match(/^(@?\w+)\s*=\s*(.+)$/s);
|
|
178
|
+
if (assign && !/^={2}/.test(assign[2])) {
|
|
179
|
+
return { kind: 'assign', line, target: assign[1], source: _lowerExpr(assign[2]) };
|
|
180
|
+
}
|
|
181
|
+
// Statement-form call with parens
|
|
182
|
+
const call = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
|
|
183
|
+
if (call) {
|
|
184
|
+
return { kind: 'call', line, callee: call[1], args: _splitTopLevelCommas(call[2]).map(_lowerExpr) };
|
|
185
|
+
}
|
|
186
|
+
// Statement-form call without parens (common Ruby idiom): redirect_to expr
|
|
187
|
+
const bareCall = s.match(/^([a-z_]\w*)\s+(.+)$/s);
|
|
188
|
+
if (bareCall && /^[a-z_]/.test(bareCall[1]) && !/^(?:if|unless|while|until|for|case|when|elsif|else|end|return|raise|require|include|extend|attr_\w+)$/.test(bareCall[1])) {
|
|
189
|
+
return { kind: 'call', line, callee: bareCall[1], args: [_lowerExpr(bareCall[2])] };
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function _lineAt(src, idx) {
|
|
195
|
+
let line = 1;
|
|
196
|
+
for (let i = 0; i < idx && i < src.length; i++) if (src[i] === '\n') line++;
|
|
197
|
+
return line;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function _qid(file, name, line, body) {
|
|
201
|
+
const sha = crypto.createHash('sha256').update(body).digest('hex').slice(0, 8);
|
|
202
|
+
return `${file}::${name}@${line}#${sha}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let _nid = 0;
|
|
206
|
+
function _nextId() { return `rn${++_nid}`; }
|
|
207
|
+
|
|
208
|
+
function _addNode(nodes, node) {
|
|
209
|
+
const id = _nextId();
|
|
210
|
+
node.succ = node.succ || [];
|
|
211
|
+
node.pred = node.pred || [];
|
|
212
|
+
nodes[id] = node;
|
|
213
|
+
return id;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function _linkNodes(nodes, src, dst) {
|
|
217
|
+
if (!nodes[src] || !nodes[dst]) return;
|
|
218
|
+
if (!nodes[src].succ.includes(dst)) nodes[src].succ.push(dst);
|
|
219
|
+
if (!nodes[dst].pred.includes(src)) nodes[dst].pred.push(src);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function _extractRubyBlockBody(compound) {
|
|
223
|
+
const lines = compound.split('\n');
|
|
224
|
+
if (lines.length < 2) return '';
|
|
225
|
+
return lines.slice(1, -1).join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function _buildCfg(bodyText, nodes, prevId, startLine) {
|
|
229
|
+
const stmts = _splitStatements(bodyText);
|
|
230
|
+
let prev = prevId;
|
|
231
|
+
let line = startLine;
|
|
232
|
+
for (const stmt of stmts) {
|
|
233
|
+
const s = stmt.trim();
|
|
234
|
+
if (!s || s.startsWith('#')) { line++; continue; }
|
|
235
|
+
|
|
236
|
+
const ifMatch = s.match(/^(if|unless)\s+(.+)$/m);
|
|
237
|
+
if (ifMatch && /\bend\b\s*$/.test(s)) {
|
|
238
|
+
const condText = ifMatch[2].trim();
|
|
239
|
+
const innerBody = _extractRubyBlockBody(s);
|
|
240
|
+
const ifNode = _addNode(nodes, { kind: 'if', cond: _lowerExpr(condText), line });
|
|
241
|
+
_linkNodes(nodes, prev, ifNode);
|
|
242
|
+
const join = _addNode(nodes, { kind: 'noop', line });
|
|
243
|
+
const thenTail = _buildCfg(innerBody, nodes, ifNode, line + 1);
|
|
244
|
+
_linkNodes(nodes, thenTail, join);
|
|
245
|
+
_linkNodes(nodes, ifNode, join);
|
|
246
|
+
prev = join;
|
|
247
|
+
line += (s.match(/\n/g) || []).length + 1;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const whileMatch = s.match(/^(while|until)\s+(.+)$/m);
|
|
252
|
+
if (whileMatch && /\bend\b\s*$/.test(s)) {
|
|
253
|
+
const innerBody = _extractRubyBlockBody(s);
|
|
254
|
+
const header = _addNode(nodes, { kind: 'loop-header', line });
|
|
255
|
+
_linkNodes(nodes, prev, header);
|
|
256
|
+
const bodyTail = _buildCfg(innerBody, nodes, header, line + 1);
|
|
257
|
+
_linkNodes(nodes, bodyTail, header);
|
|
258
|
+
const join = _addNode(nodes, { kind: 'noop', line });
|
|
259
|
+
_linkNodes(nodes, header, join);
|
|
260
|
+
prev = join;
|
|
261
|
+
line += (s.match(/\n/g) || []).length + 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const node = _lowerStmt(s, line);
|
|
266
|
+
if (!node) { line += (s.match(/\n/g) || []).length + 1; continue; }
|
|
267
|
+
const id = _addNode(nodes, node);
|
|
268
|
+
_linkNodes(nodes, prev, id);
|
|
269
|
+
prev = id;
|
|
270
|
+
line += (s.match(/\n/g) || []).length + 1;
|
|
271
|
+
}
|
|
272
|
+
return prev;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function parseRubyFile(file, code) {
|
|
276
|
+
if (!file || typeof code !== 'string') return null;
|
|
277
|
+
if (!/\.rb$/i.test(file)) return null;
|
|
278
|
+
if (code.length > 1_000_000) return null;
|
|
279
|
+
|
|
280
|
+
const functions = [];
|
|
281
|
+
DEF_RE.lastIndex = 0;
|
|
282
|
+
_nid = 0;
|
|
283
|
+
let m;
|
|
284
|
+
while ((m = DEF_RE.exec(code)) !== null) {
|
|
285
|
+
const name = m[1];
|
|
286
|
+
const paramsText = m[2] || '';
|
|
287
|
+
const params = paramsText.split(',').map(p => {
|
|
288
|
+
const t = p.trim().replace(/\s*=\s*.*$/, '').replace(/^[*&]+/, '');
|
|
289
|
+
return t && /^\w+$/.test(t) ? t : null;
|
|
290
|
+
}).filter(Boolean);
|
|
291
|
+
const defLineEnd = code.indexOf('\n', m.index + m[0].length);
|
|
292
|
+
if (defLineEnd < 0) continue;
|
|
293
|
+
const extracted = _extractRubyBody(code, defLineEnd + 1);
|
|
294
|
+
if (!extracted) continue;
|
|
295
|
+
const startLine = _lineAt(code, m.index);
|
|
296
|
+
const nodes = {};
|
|
297
|
+
const entry = _addNode(nodes, { kind: 'entry', line: startLine });
|
|
298
|
+
const exit = _addNode(nodes, { kind: 'exit', line: startLine });
|
|
299
|
+
const tail = _buildCfg(extracted.body, nodes, entry, startLine + 1);
|
|
300
|
+
_linkNodes(nodes, tail, exit);
|
|
301
|
+
functions.push({
|
|
302
|
+
qid: _qid(file, name, startLine, extracted.body),
|
|
303
|
+
name, line: startLine, params, file,
|
|
304
|
+
cfg: { entry, exit, nodes },
|
|
305
|
+
});
|
|
306
|
+
DEF_RE.lastIndex = extracted.end;
|
|
307
|
+
}
|
|
308
|
+
return functions.length ? { file, functions, topLevel: null } : null;
|
|
309
|
+
}
|