@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.
Files changed (108) hide show
  1. package/bin/.agentic-security/findings.json +320 -9
  2. package/bin/.agentic-security/last-scan.json +320 -9
  3. package/bin/.agentic-security/last-scan.json.sig +1 -1
  4. package/bin/.agentic-security/scan-history.json +17 -377
  5. package/bin/.agentic-security/streak.json +11 -16
  6. package/bin/agentic-security.js +33 -2
  7. package/dist/178.index.js +1 -1
  8. package/dist/384.index.js +1 -1
  9. package/dist/637.index.js +1 -1
  10. package/dist/718.index.js +106 -0
  11. package/dist/824.index.js +126 -0
  12. package/dist/838.index.js +1 -1
  13. package/dist/agentic-security.mjs +32 -32
  14. package/dist/agentic-security.mjs.sha256 +1 -1
  15. package/package.json +7 -7
  16. package/src/.agentic-security/findings.json +5731 -3933
  17. package/src/.agentic-security/last-scan.json +5731 -3933
  18. package/src/.agentic-security/last-scan.json.sig +1 -1
  19. package/src/.agentic-security/scan-history.json +2533 -887
  20. package/src/.agentic-security/streak.json +11 -16
  21. package/src/dataflow/.agentic-security/findings.json +52 -24
  22. package/src/dataflow/.agentic-security/last-scan.json +52 -24
  23. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
  24. package/src/dataflow/.agentic-security/scan-history.json +101 -134
  25. package/src/dataflow/.agentic-security/streak.json +8 -10
  26. package/src/dataflow/async-sequencing.js +16 -7
  27. package/src/dataflow/builtin-summaries.js +131 -0
  28. package/src/dataflow/catalog.js +107 -0
  29. package/src/dataflow/cross-repo.js +75 -1
  30. package/src/dataflow/engine.js +129 -0
  31. package/src/dataflow/implicit-flow.js +24 -6
  32. package/src/dataflow/stub-aware-filter.js +69 -11
  33. package/src/dataflow/summaries.js +28 -3
  34. package/src/engine-parallel.js +70 -0
  35. package/src/engine.js +165 -15
  36. package/src/ir/.agentic-security/findings.json +757 -16
  37. package/src/ir/.agentic-security/last-scan.json +757 -16
  38. package/src/ir/.agentic-security/last-scan.json.sig +1 -1
  39. package/src/ir/.agentic-security/scan-history.json +545 -138
  40. package/src/ir/.agentic-security/streak.json +11 -13
  41. package/src/ir/index.js +22 -1
  42. package/src/ir/parser-go.js +403 -0
  43. package/src/ir/parser-js.js +2 -0
  44. package/src/ir/parser-php.js +330 -0
  45. package/src/ir/parser-py.helper.py +137 -11
  46. package/src/ir/parser-rb.js +309 -0
  47. package/src/posture/.agentic-security/findings.json +407 -84
  48. package/src/posture/.agentic-security/last-scan.json +407 -84
  49. package/src/posture/.agentic-security/last-scan.json.sig +1 -1
  50. package/src/posture/.agentic-security/scan-history.json +16 -4923
  51. package/src/posture/.agentic-security/streak.json +10 -14
  52. package/src/posture/calibration.js +14 -0
  53. package/src/posture/triage.js +13 -0
  54. package/src/report/.agentic-security/findings.json +6 -5
  55. package/src/report/.agentic-security/last-scan.json +6 -5
  56. package/src/report/.agentic-security/last-scan.json.sig +1 -1
  57. package/src/report/.agentic-security/scan-history.json +3 -300
  58. package/src/report/.agentic-security/streak.json +7 -8
  59. package/src/report/index.js +23 -2
  60. package/src/sast/.agentic-security/findings.json +195 -56
  61. package/src/sast/.agentic-security/last-scan.json +195 -56
  62. package/src/sast/.agentic-security/last-scan.json.sig +1 -1
  63. package/src/sast/.agentic-security/scan-history.json +14 -394
  64. package/src/sast/.agentic-security/streak.json +10 -13
  65. package/src/sast/cache-poisoning.js +77 -0
  66. package/src/sast/comparison-safety.js +73 -0
  67. package/src/sast/db-taint.js +54 -0
  68. package/src/sast/graphql.js +127 -0
  69. package/src/sast/llm-stored-prompt.js +57 -0
  70. package/src/sast/mutation-xss.js +43 -0
  71. package/src/sast/nosql-injection.js +5 -0
  72. package/src/sast/null-byte-injection.js +76 -0
  73. package/src/sast/redos-nfa.js +338 -0
  74. package/src/sast/sensitive-data-logging.js +73 -0
  75. package/src/sast/weak-password-hash.js +77 -0
  76. package/src/sast/weak-randomness.js +100 -0
  77. package/src/sca/.agentic-security/findings.json +502 -11
  78. package/src/sca/.agentic-security/last-scan.json +502 -11
  79. package/src/sca/.agentic-security/last-scan.json.sig +1 -1
  80. package/src/sca/.agentic-security/scan-history.json +19 -1
  81. package/src/sca/.agentic-security/streak.json +6 -6
  82. package/src/sca/llm-function-extract.js +107 -0
  83. package/src/sca/vendor-detect.js +91 -0
  84. package/dist/218.index.js +0 -793
  85. package/dist/601.index.js +0 -1038
  86. package/dist/634.index.js +0 -1892
  87. package/src/integrations/.agentic-security/findings.json +0 -1504
  88. package/src/integrations/.agentic-security/last-scan.json +0 -1504
  89. package/src/integrations/.agentic-security/scan-history.json +0 -40
  90. package/src/integrations/.agentic-security/streak.json +0 -21
  91. package/src/llm-validator/.agentic-security/findings.json +0 -1891
  92. package/src/llm-validator/.agentic-security/last-scan.json +0 -1891
  93. package/src/llm-validator/.agentic-security/last-scan.json.sig +0 -1
  94. package/src/llm-validator/.agentic-security/scan-history.json +0 -168
  95. package/src/llm-validator/.agentic-security/streak.json +0 -20
  96. package/src/lsp/.agentic-security/findings.json +0 -28
  97. package/src/lsp/.agentic-security/last-scan.json +0 -28
  98. package/src/lsp/.agentic-security/scan-history.json +0 -79
  99. package/src/lsp/.agentic-security/streak.json +0 -22
  100. package/src/mcp/.agentic-security/findings.json +0 -8403
  101. package/src/mcp/.agentic-security/last-scan.json +0 -8403
  102. package/src/mcp/.agentic-security/last-scan.json.sig +0 -1
  103. package/src/mcp/.agentic-security/scan-history.json +0 -1182
  104. package/src/mcp/.agentic-security/streak.json +0 -22
  105. package/src/sast/bench-shape/.agentic-security/findings.json +0 -28
  106. package/src/sast/bench-shape/.agentic-security/last-scan.json +0 -28
  107. package/src/sast/bench-shape/.agentic-security/scan-history.json +0 -24
  108. package/src/sast/bench-shape/.agentic-security/streak.json +0 -22
@@ -1,23 +1,21 @@
1
1
  {
2
- "firstScanDate": "2026-05-18T18:07:37.263Z",
3
- "lastScanDate": "2026-05-20T17:01:27.572Z",
4
- "totalScans": 28,
5
- "daysCleanCritical": 3,
6
- "lastCleanDate": "2026-05-20",
2
+ "firstScanDate": "2026-05-26T15:14:23.942Z",
3
+ "lastScanDate": "2026-05-27T02:22:42.227Z",
4
+ "totalScans": 30,
5
+ "daysCleanCritical": 2,
6
+ "lastCleanDate": "2026-05-27",
7
7
  "lastCriticalDate": null,
8
8
  "hasEverHadCritical": false,
9
- "bestDaysCleanCritical": 3,
10
- "totalFindingsAtFirstScan": 0,
11
- "totalFindingsAtLastScan": 13,
9
+ "bestDaysCleanCritical": 2,
10
+ "totalFindingsAtFirstScan": 13,
11
+ "totalFindingsAtLastScan": 16,
12
12
  "totalFixesInferred": 0,
13
- "lastGrade": "A-",
14
- "bestGrade": "A+",
13
+ "lastGrade": "B",
14
+ "bestGrade": "A-",
15
15
  "launchCheckPassedAt": null,
16
16
  "achievements": [
17
17
  "first-scan",
18
- "grade-a",
19
- "grade-a-plus",
20
18
  "scan-veteran-25"
21
19
  ],
22
- "previousGrade": "A-"
20
+ "previousGrade": "B"
23
21
  }
package/src/ir/index.js CHANGED
@@ -13,6 +13,9 @@ import {
13
13
  probePythonAvailable,
14
14
  } from './parser-py-cst.js';
15
15
  import { parseJavaFile } from './parser-java.js';
16
+ import { parseGoFile } from './parser-go.js';
17
+ import { parsePhpFile } from './parser-php.js';
18
+ import { parseRubyFile } from './parser-rb.js';
16
19
  import { buildCallGraph } from './callgraph.js';
17
20
  import { buildClassHierarchy } from './class-hierarchy.js';
18
21
  import { computeSSA, isSSAEnabled } from './ssa.js';
@@ -76,6 +79,15 @@ export function buildProjectIR(fileContents) {
76
79
  } else if (/\.kt$/i.test(file)) {
77
80
  const ir = parseKotlinFile(file, code);
78
81
  if (ir) perFile[file] = ir;
82
+ } else if (/\.go$/i.test(file)) {
83
+ const ir = parseGoFile(file, code);
84
+ if (ir) perFile[file] = ir;
85
+ } else if (/\.(?:php|phtml)$/i.test(file)) {
86
+ const ir = parsePhpFile(file, code);
87
+ if (ir) perFile[file] = ir;
88
+ } else if (/\.rb$/i.test(file)) {
89
+ const ir = parseRubyFile(file, code);
90
+ if (ir) perFile[file] = ir;
79
91
  }
80
92
  }
81
93
  if (pyBatch.length) {
@@ -116,6 +128,15 @@ export async function buildProjectIRAsync(fileContents) {
116
128
  const ir = await parseJavaFile(file, code);
117
129
  if (ir) perFile[file] = ir;
118
130
  } catch { /* skip */ }
131
+ } else if (/\.go$/i.test(file)) {
132
+ const ir = parseGoFile(file, code);
133
+ if (ir) perFile[file] = ir;
134
+ } else if (/\.(?:php|phtml)$/i.test(file)) {
135
+ const ir = parsePhpFile(file, code);
136
+ if (ir) perFile[file] = ir;
137
+ } else if (/\.rb$/i.test(file)) {
138
+ const ir = parseRubyFile(file, code);
139
+ if (ir) perFile[file] = ir;
119
140
  }
120
141
  }
121
142
  if (pyBatch.length) {
@@ -149,4 +170,4 @@ export function parsePythonFile(file, code) {
149
170
  return parsePythonFileRegex(file, code);
150
171
  }
151
172
 
152
- export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable };
173
+ export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, parseGoFile, parsePhpFile, parseRubyFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable };
@@ -0,0 +1,403 @@
1
+ // Go IR frontend.
2
+ //
3
+ // Regex-based, follows the parser-cs.js / parser-kt.js pattern. Focused on
4
+ // net/http, gin, echo, chi, gorm, database/sql surface area.
5
+ //
6
+ // What we model:
7
+ // - func declarations: top-level, method receivers, closures (top-level only)
8
+ // - := short declarations and = assignments
9
+ // - method / function calls
10
+ // - return
11
+ // - if / for / switch as linear blocks (body treated as straight-line)
12
+ // - defer / go as call nodes
13
+ // - fmt.Sprintf as template literal
14
+ // - multi-return first-target tracking: a, err := f()
15
+ //
16
+ // What we do NOT model:
17
+ // - goroutine channel taint (send/receive)
18
+ // - interface dispatch (dynamic method resolution)
19
+ // - generics (type params)
20
+ // - select statements (treated as noop)
21
+ // - struct field assignments (x.Field = val) beyond simple dotted targets
22
+
23
+ import * as crypto from 'node:crypto';
24
+
25
+ const FUNC_RE = new RegExp(
26
+ '(?:^|[\\n;{}])\\s*func\\s+' +
27
+ '(?:\\(\\s*(\\w+)\\s+\\*?([A-Za-z_]\\w*)\\s*\\)\\s+)?' + // optional receiver (g1=name, g2=type)
28
+ '([A-Za-z_]\\w*)' + // func name (g3)
29
+ '\\s*\\(([^)]*)\\)' + // params (g4)
30
+ '(?:\\s*(?:\\([^)]*\\)|[A-Za-z_*\\[\\]\\w.,\\s]*))?' + // optional return type(s)
31
+ '\\s*\\{', 'g');
32
+
33
+ function _splitStatements(body) {
34
+ const out = [];
35
+ let buf = '';
36
+ let depth = 0;
37
+ let inStr = null;
38
+ let inRaw = false;
39
+ let escape = false;
40
+ for (let i = 0; i < body.length; i++) {
41
+ const c = body[i];
42
+ if (escape) { buf += c; escape = false; continue; }
43
+ if (inStr) {
44
+ buf += c;
45
+ if (c === '\\' && inStr === '"') { escape = true; continue; }
46
+ if (c === inStr) inStr = null;
47
+ continue;
48
+ }
49
+ if (inRaw) {
50
+ buf += c;
51
+ if (c === '`') inRaw = false;
52
+ continue;
53
+ }
54
+ if (c === '"' || c === '\'') { inStr = c; buf += c; continue; }
55
+ if (c === '`') { inRaw = true; buf += c; continue; }
56
+ if (c === '/' && body[i + 1] === '/') {
57
+ while (i < body.length && body[i] !== '\n') i++;
58
+ continue;
59
+ }
60
+ if (c === '{' || c === '(' || c === '[') depth++;
61
+ if (c === '}' || c === ')' || c === ']') depth--;
62
+ if ((c === '\n' || c === ';') && depth === 0) {
63
+ const t = buf.trim();
64
+ if (t) out.push(t);
65
+ buf = '';
66
+ continue;
67
+ }
68
+ buf += c;
69
+ }
70
+ if (buf.trim()) out.push(buf.trim());
71
+ return out;
72
+ }
73
+
74
+ function _lowerExpr(text) {
75
+ const s = String(text || '').trim();
76
+ if (!s) return { kind: 'unknown' };
77
+ if (/^fmt\.Sprintf\s*\(/.test(s)) {
78
+ const inner = s.slice(s.indexOf('(') + 1, s.lastIndexOf(')'));
79
+ const parts = _splitTopLevelCommas(inner).map(_lowerExpr);
80
+ return { kind: 'tpl', parts };
81
+ }
82
+ if (/^"/.test(s) || /^`/.test(s)) return { kind: 'literal', value: s };
83
+ if (/^\d/.test(s)) return { kind: 'literal', value: s };
84
+ if (/^(true|false|nil)\b/.test(s)) return { kind: 'literal', value: s };
85
+ // Call: foo.Bar(args) or Bar(args)
86
+ const callMatch = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
87
+ if (callMatch) {
88
+ const callee = callMatch[1];
89
+ const args = _splitTopLevelCommas(callMatch[2]).map(_lowerExpr);
90
+ return { kind: 'call', callee, args };
91
+ }
92
+ // String concat with +
93
+ if (s.includes('+') && /["'`]/.test(s)) {
94
+ const parts = _splitTopLevelPlus(s).map(_lowerExpr);
95
+ return { kind: 'tpl', parts };
96
+ }
97
+ // Member: a.b.c
98
+ if (/^[A-Za-z_][\w.]*$/.test(s)) {
99
+ const parts = s.split('.');
100
+ if (parts.length === 1) return { kind: 'ident', name: parts[0] };
101
+ let cur = { kind: 'ident', name: parts[0] };
102
+ for (let i = 1; i < parts.length; i++) cur = { kind: 'member', object: cur, prop: parts[i] };
103
+ return cur;
104
+ }
105
+ // Indexing: a[b] or a["key"]
106
+ if (/^[A-Za-z_][\w.]*\[/.test(s)) {
107
+ const lb = s.indexOf('[');
108
+ const base = s.slice(0, lb);
109
+ const parts = base.split('.');
110
+ let cur = { kind: 'ident', name: parts[0] };
111
+ for (let i = 1; i < parts.length; i++) cur = { kind: 'member', object: cur, prop: parts[i] };
112
+ return { kind: 'member', object: cur, prop: '[]' };
113
+ }
114
+ // Struct literal: Type{...}
115
+ if (/^[A-Za-z_]\w*\s*\{/.test(s)) {
116
+ return { kind: 'object', props: [] };
117
+ }
118
+ // Address-of / dereference
119
+ if (s.startsWith('&') || s.startsWith('*')) return _lowerExpr(s.slice(1));
120
+ return { kind: 'unknown' };
121
+ }
122
+
123
+ function _splitTopLevelCommas(s) {
124
+ const out = [];
125
+ let buf = '';
126
+ let depth = 0;
127
+ let inStr = null;
128
+ let inRaw = false;
129
+ for (let i = 0; i < s.length; i++) {
130
+ const c = s[i];
131
+ if (inStr) {
132
+ buf += c;
133
+ if (c === '\\' && inStr === '"') { i++; buf += s[i] || ''; continue; }
134
+ if (c === inStr) inStr = null;
135
+ continue;
136
+ }
137
+ if (inRaw) { buf += c; if (c === '`') inRaw = false; continue; }
138
+ if (c === '"') { inStr = c; buf += c; continue; }
139
+ if (c === '`') { inRaw = true; buf += c; continue; }
140
+ if (c === '(' || c === '{' || c === '[') depth++;
141
+ if (c === ')' || c === '}' || c === ']') depth--;
142
+ if (c === ',' && depth === 0) { out.push(buf.trim()); buf = ''; continue; }
143
+ buf += c;
144
+ }
145
+ if (buf.trim()) out.push(buf.trim());
146
+ return out;
147
+ }
148
+
149
+ function _splitTopLevelPlus(s) {
150
+ const out = [];
151
+ let buf = '';
152
+ let depth = 0;
153
+ let inStr = null;
154
+ for (let i = 0; i < s.length; i++) {
155
+ const c = s[i];
156
+ if (inStr) {
157
+ buf += c;
158
+ if (c === '\\' && inStr === '"') { i++; buf += s[i] || ''; continue; }
159
+ if (c === inStr) inStr = null;
160
+ continue;
161
+ }
162
+ if (c === '"' || c === '`') { inStr = c; buf += c; continue; }
163
+ if (c === '(' || c === '{' || c === '[') depth++;
164
+ if (c === ')' || c === '}' || c === ']') depth--;
165
+ if (c === '+' && depth === 0) { out.push(buf.trim()); buf = ''; continue; }
166
+ buf += c;
167
+ }
168
+ if (buf.trim()) out.push(buf.trim());
169
+ return out;
170
+ }
171
+
172
+ function _lowerStmt(stmt, line) {
173
+ const s = stmt.trim();
174
+ if (!s || s.startsWith('//')) return null;
175
+ // return
176
+ if (/^return\b/.test(s)) {
177
+ const rest = s.replace(/^return\s*/, '').trim();
178
+ // Multi-return: return a, b → take the first
179
+ const parts = _splitTopLevelCommas(rest);
180
+ const value = parts.length ? _lowerExpr(parts[0]) : null;
181
+ return { kind: 'return', line, value };
182
+ }
183
+ // defer / go: treat as call
184
+ if (/^(?:defer|go)\s+/.test(s)) {
185
+ const rest = s.replace(/^(?:defer|go)\s+/, '').trim();
186
+ const cm = rest.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
187
+ if (cm) {
188
+ return { kind: 'call', line, callee: cm[1], args: _splitTopLevelCommas(cm[2]).map(_lowerExpr) };
189
+ }
190
+ return { kind: 'noop', line };
191
+ }
192
+ // Short variable declaration: a, b := expr or a := expr
193
+ const shortDecl = s.match(/^(\w+(?:\s*,\s*\w+)*)\s*:=\s*(.+)$/s);
194
+ if (shortDecl) {
195
+ const targets = shortDecl[1].split(',').map(t => t.trim());
196
+ const rhs = shortDecl[2].trim();
197
+ if (targets.length === 1) {
198
+ return { kind: 'assign', line, target: targets[0], source: _lowerExpr(rhs) };
199
+ }
200
+ // Multi-return: a, err := f() → assign first target
201
+ return { kind: 'assign', line, target: targets[0], source: _lowerExpr(rhs) };
202
+ }
203
+ // Regular assignment: a = expr or a.b = expr
204
+ const assign = s.match(/^([A-Za-z_][\w.]*)\s*=\s*(.+)$/s);
205
+ if (assign) {
206
+ return { kind: 'assign', line, target: assign[1], source: _lowerExpr(assign[2]) };
207
+ }
208
+ // var declaration: var name Type = expr or var name = expr
209
+ const varDecl = s.match(/^var\s+(\w+)\s+(?:\w[\w.*[\]]*\s*)?=\s*(.+)$/s);
210
+ if (varDecl) {
211
+ return { kind: 'assign', line, target: varDecl[1], source: _lowerExpr(varDecl[2]) };
212
+ }
213
+ // Statement-form call: obj.Method(args) or Method(args)
214
+ const cm = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
215
+ if (cm) {
216
+ return { kind: 'call', line, callee: cm[1], args: _splitTopLevelCommas(cm[2]).map(_lowerExpr) };
217
+ }
218
+ return null;
219
+ }
220
+
221
+ function _extractBody(src, openBrace) {
222
+ let depth = 1;
223
+ let i = openBrace + 1;
224
+ let inStr = null;
225
+ let inRaw = false;
226
+ let escape = false;
227
+ while (i < src.length && depth > 0) {
228
+ const c = src[i];
229
+ if (escape) { escape = false; i++; continue; }
230
+ if (inStr) {
231
+ if (c === '\\' && inStr === '"') { escape = true; i++; continue; }
232
+ if (c === inStr) inStr = null;
233
+ i++; continue;
234
+ }
235
+ if (inRaw) { if (c === '`') inRaw = false; i++; continue; }
236
+ if (c === '"') { inStr = c; i++; continue; }
237
+ if (c === '`') { inRaw = true; i++; continue; }
238
+ if (c === '{') depth++;
239
+ else if (c === '}') depth--;
240
+ if (depth === 0) return { body: src.slice(openBrace + 1, i), end: i };
241
+ i++;
242
+ }
243
+ return null;
244
+ }
245
+
246
+ function _lineAt(src, idx) {
247
+ let line = 1;
248
+ for (let i = 0; i < idx && i < src.length; i++) if (src[i] === '\n') line++;
249
+ return line;
250
+ }
251
+
252
+ function _qid(file, name, line, body) {
253
+ const sha = crypto.createHash('sha256').update(body).digest('hex').slice(0, 8);
254
+ return `${file}::${name}@${line}#${sha}`;
255
+ }
256
+
257
+ function _parseGoParams(paramsText) {
258
+ if (!paramsText.trim()) return [];
259
+ const parts = _splitTopLevelCommas(paramsText);
260
+ const params = [];
261
+ for (const p of parts) {
262
+ const t = p.trim();
263
+ if (!t) continue;
264
+ // "name Type" or "name, name2 Type" or just "Type" (unnamed)
265
+ const tokens = t.split(/\s+/);
266
+ if (tokens.length >= 2) {
267
+ // Could be "name Type" or "name *Type" or "name ...Type"
268
+ const name = tokens[0].replace(/^\*/, '');
269
+ if (/^[a-z_]\w*$/i.test(name) && !/^(?:func|chan|map|interface|struct)$/.test(name)) {
270
+ params.push(name);
271
+ }
272
+ } else if (tokens.length === 1) {
273
+ // Single token — could be just a type (unnamed param) or a name
274
+ // In Go, if it looks like a lowercase identifier, it's likely a name
275
+ const t0 = tokens[0].replace(/^\*/, '').replace(/^\.\.\./, '');
276
+ if (/^[a-z_]\w*$/.test(t0) && !/^(?:int|string|bool|byte|rune|float32|float64|error|any|interface)$/.test(t0)) {
277
+ params.push(t0);
278
+ }
279
+ }
280
+ }
281
+ return params;
282
+ }
283
+
284
+ let _nid = 0;
285
+ function _nextId() { return `gn${++_nid}`; }
286
+
287
+ function _addNode(nodes, node) {
288
+ const id = _nextId();
289
+ node.succ = node.succ || [];
290
+ node.pred = node.pred || [];
291
+ nodes[id] = node;
292
+ return id;
293
+ }
294
+
295
+ function _link(nodes, src, dst) {
296
+ if (!nodes[src] || !nodes[dst]) return;
297
+ if (!nodes[src].succ.includes(dst)) nodes[src].succ.push(dst);
298
+ if (!nodes[dst].pred.includes(src)) nodes[dst].pred.push(src);
299
+ }
300
+
301
+ function _buildCfg(bodyText, nodes, prevId, startLine) {
302
+ const stmts = _splitStatements(bodyText);
303
+ let prev = prevId;
304
+ let line = startLine;
305
+ for (const stmt of stmts) {
306
+ const s = stmt.trim();
307
+ if (!s || s.startsWith('//')) { line++; continue; }
308
+
309
+ // if statement with brace body
310
+ const ifMatch = s.match(/^if\s+([\s\S]+?)\s*\{([\s\S]*)\}(?:\s*else\s*\{([\s\S]*)\})?\s*$/s) ||
311
+ s.match(/^if\s+([\s\S]+?)\s*\{([\s\S]*)\}\s*$/s);
312
+ if (ifMatch) {
313
+ const condText = ifMatch[1].replace(/;[^;]*$/, '').trim();
314
+ const thenBody = ifMatch[2];
315
+ const elseBody = ifMatch[3] || null;
316
+ const ifNode = _addNode(nodes, { kind: 'if', cond: _lowerExpr(condText), line });
317
+ _link(nodes, prev, ifNode);
318
+ const join = _addNode(nodes, { kind: 'noop', line });
319
+ const thenTail = _buildCfg(thenBody, nodes, ifNode, line + 1);
320
+ _link(nodes, thenTail, join);
321
+ if (elseBody) {
322
+ const elseTail = _buildCfg(elseBody, nodes, ifNode, line + 1);
323
+ _link(nodes, elseTail, join);
324
+ } else {
325
+ _link(nodes, ifNode, join);
326
+ }
327
+ prev = join;
328
+ line += (s.match(/\n/g) || []).length + 1;
329
+ continue;
330
+ }
331
+
332
+ // for loop with brace body
333
+ const forMatch = s.match(/^for\s+([\s\S]*?)\s*\{([\s\S]*)\}\s*$/s);
334
+ if (forMatch) {
335
+ const header = _addNode(nodes, { kind: 'loop-header', line });
336
+ _link(nodes, prev, header);
337
+ const loopBody = forMatch[2];
338
+ // for-range: extract loop variable assignment
339
+ const rangeMatch = forMatch[1].match(/^(\w+)(?:\s*,\s*(\w+))?\s*:=\s*range\s+(.+)$/s);
340
+ let bodyPrev = header;
341
+ if (rangeMatch) {
342
+ const loopVar = rangeMatch[2] || rangeMatch[1];
343
+ const iterExpr = rangeMatch[3];
344
+ const assignId = _addNode(nodes, { kind: 'assign', target: loopVar, source: _lowerExpr(iterExpr), line });
345
+ _link(nodes, header, assignId);
346
+ bodyPrev = assignId;
347
+ }
348
+ const bodyTail = _buildCfg(loopBody, nodes, bodyPrev, line + 1);
349
+ _link(nodes, bodyTail, header);
350
+ const join = _addNode(nodes, { kind: 'noop', line });
351
+ _link(nodes, header, join);
352
+ prev = join;
353
+ line += (s.match(/\n/g) || []).length + 1;
354
+ continue;
355
+ }
356
+
357
+ // Regular statement
358
+ const node = _lowerStmt(s, line);
359
+ if (!node) { line++; continue; }
360
+ const id = _addNode(nodes, node);
361
+ _link(nodes, prev, id);
362
+ prev = id;
363
+ line += (s.match(/\n/g) || []).length + 1;
364
+ }
365
+ return prev;
366
+ }
367
+
368
+ export function parseGoFile(file, code) {
369
+ if (!file || typeof code !== 'string') return null;
370
+ if (!/\.go$/i.test(file)) return null;
371
+ if (code.length > 1_000_000) return null;
372
+
373
+ const functions = [];
374
+ FUNC_RE.lastIndex = 0;
375
+ _nid = 0;
376
+ let m;
377
+ while ((m = FUNC_RE.exec(code)) !== null) {
378
+ const receiverName = m[1] || null;
379
+ const name = m[3];
380
+ const paramsText = m[4] || '';
381
+ const params = _parseGoParams(paramsText);
382
+ if (receiverName && !params.includes(receiverName)) {
383
+ params.unshift(receiverName);
384
+ }
385
+ const braceIdx = code.indexOf('{', m.index + m[0].length - 1);
386
+ if (braceIdx < 0) continue;
387
+ const extracted = _extractBody(code, braceIdx);
388
+ if (!extracted) continue;
389
+ const startLine = _lineAt(code, m.index);
390
+ const nodes = {};
391
+ const entry = _addNode(nodes, { kind: 'entry', line: startLine });
392
+ const exit = _addNode(nodes, { kind: 'exit', line: startLine });
393
+ const tail = _buildCfg(extracted.body, nodes, entry, startLine + 1);
394
+ _link(nodes, tail, exit);
395
+ functions.push({
396
+ qid: _qid(file, name, startLine, extracted.body),
397
+ name, line: startLine, params, file,
398
+ cfg: { entry, exit, nodes },
399
+ });
400
+ FUNC_RE.lastIndex = extracted.end + 1;
401
+ }
402
+ return functions.length ? { file, functions, topLevel: null } : null;
403
+ }
@@ -86,6 +86,7 @@ function exprOf(n) {
86
86
  };
87
87
  case 'ArrayExpression': return { kind: 'array', elements: (n.elements || []).map(exprOf) };
88
88
  case 'SpreadElement': return exprOf(n.argument);
89
+ case 'ThisExpression': return { kind: 'ident', name: '_this_' };
89
90
  default: return { kind: 'unknown' };
90
91
  }
91
92
  }
@@ -94,6 +95,7 @@ function exprOf(n) {
94
95
  function lhsPath(n) {
95
96
  if (!n) return null;
96
97
  if (n.type === 'Identifier') return n.name;
98
+ if (n.type === 'ThisExpression') return '_this_';
97
99
  if (n.type === 'MemberExpression') {
98
100
  const base = lhsPath(n.object);
99
101
  const prop = n.computed ? '*' : (n.property?.name || '*');