@clear-capabilities/agentic-security-scanner 0.158.0 → 0.160.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 (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/1919.index.js +5 -2
  3. package/dist/2862.index.js +29 -9
  4. package/dist/3531.index.js +1 -1
  5. package/dist/4171.index.js +4 -4
  6. package/dist/{8551.index.js → 7033.index.js} +415 -22
  7. package/dist/9560.index.js +3 -1
  8. package/dist/agentic-security.mjs +15 -15
  9. package/dist/agentic-security.mjs.sha256 +1 -1
  10. package/package.json +11 -6
  11. package/src/engine.js +6 -1
  12. package/src/language/CLAUDE.md +48 -1
  13. package/src/language/assurance.js +21 -4
  14. package/src/language/context.js +2 -0
  15. package/src/language/cvss.js +91 -0
  16. package/src/language/fix-lifecycle.js +114 -21
  17. package/src/language/haskell-adapter.js +4 -3
  18. package/src/language/haskell-advisory-feed.js +15 -0
  19. package/src/language/haskell-boundaries.js +220 -0
  20. package/src/language/haskell-cpp.js +460 -0
  21. package/src/language/haskell-ir.js +6 -3
  22. package/src/language/haskell-models.js +4 -3
  23. package/src/language/haskell-parser.js +122 -38
  24. package/src/language/haskell-sca.js +10 -6
  25. package/src/language/haskell-supply.js +42 -8
  26. package/src/language/haskell-web.js +128 -23
  27. package/src/language/nix-advisory-feed.js +275 -0
  28. package/src/language/nix-fix.js +162 -24
  29. package/src/language/nix-ir.js +16 -12
  30. package/src/language/nix-sca.js +71 -12
  31. package/src/language/nixos-eval-forms.js +162 -0
  32. package/src/language/nixos-module-resolver.js +346 -51
  33. package/src/language/resolved-pass.js +57 -3
  34. package/src/lsp/server.js +5 -2
  35. package/src/mcp/tools.js +3 -1
  36. package/src/posture/fix-history.js +30 -3
  37. package/src/report/index.js +4 -1
  38. package/dist/1310.index.js +0 -3161
  39. package/dist/2376.index.js +0 -3151
  40. package/dist/3526.index.js +0 -1019
  41. package/dist/7310.index.js +0 -520
  42. package/dist/8520.index.js +0 -13
  43. package/dist/9091.index.js +0 -3135
@@ -0,0 +1,460 @@
1
+ // CPP conditional evaluation for Haskell sources (HS boundaries).
2
+ //
3
+ // Static and dependency-free: it never runs cpp, GHC or Cabal. It evaluates `#if` / `#ifdef` / `#ifndef` / `#elif` /
4
+ // `#else` chains with THREE-valued logic (true, false, unknown) so that:
5
+ // - a decidable conditional keeps only its live branch (the dead branch is overwritten with same-length spaces by the
6
+ // caller, so locations stay exact),
7
+ // - an undecidable one keeps EVERY possible branch (the union) and is reported as undecided,
8
+ // - nothing is ever invented: a macro that the file does not define and the project does not state is `unknown`, never 0.
9
+ //
10
+ // What decides a condition, and on what basis (each decision records its basis):
11
+ // file `#define` / `#undef` in the same file, integer literals, `#if 0` / `#if 1`, `defined(X)`.
12
+ // compiler:pinned __GLASGOW_HASKELL__, MIN_VERSION_GLASGOW_HASKELL and the boot-package MIN_VERSION_base, when the
13
+ // project PINS a compiler (`with-compiler: ghc-X` in cabal.project). Pinned means "this is what builds".
14
+ // compiler:tested-with the same, from `tested-with: GHC == X` lines. That is a claim by the project, not a pin, so a
15
+ // decision on this basis is only taken when EVERY listed version agrees, and it is disclosed as an assumption.
16
+ // cabal-bounds MIN_VERSION_<pkg>(a,b,c) judged against the hull of the `build-depends` ranges of every component in the
17
+ // project: a built version must satisfy its component's range, so it lies inside the hull.
18
+
19
+ import { parseCabalFile, parseVersion, compareVersions } from './haskell-manifests.js';
20
+
21
+ const UNKNOWN = null;
22
+
23
+ // ── tri-state helpers ──────────────────────────────────────────────────────
24
+
25
+ const truthy = (v) => (v === UNKNOWN ? UNKNOWN : v !== 0);
26
+
27
+ // ── versions ───────────────────────────────────────────────────────────────
28
+
29
+ const toNums = (v) => v.map((x) => Number(x));
30
+
31
+ /**
32
+ * Three-valued `v >= t` where `v` may have an unknown tail (a wildcard such as 9.6.* has no patch level): undefined
33
+ * components of v are unknown, so the answer is decided only by the components that exist.
34
+ */
35
+ function triGte(v, t) {
36
+ for (let i = 0; i < Math.max(v.length, t.length); i++) {
37
+ const p = v[i];
38
+ const q = t[i] ?? 0;
39
+ if (p === undefined) return t.slice(i).every((x) => x === 0) ? true : UNKNOWN;
40
+ if (p > q) return true;
41
+ if (p < q) return false;
42
+ }
43
+ return true;
44
+ }
45
+
46
+ // GHC ships a fixed `base`. Only major.minor is recorded: a patch level of base varies inside one GHC series.
47
+ const GHC_BASE = new Map([['8.6', [4, 12]], ['8.8', [4, 13]], ['8.10', [4, 14]], ['9.0', [4, 15]], ['9.2', [4, 16]], ['9.4', [4, 17]], ['9.6', [4, 18]], ['9.8', [4, 19]], ['9.10', [4, 20]], ['9.12', [4, 21]]]);
48
+
49
+ function baseRangeOf(ghc) {
50
+ const b = GHC_BASE.get(`${ghc[0]}.${ghc[1]}`);
51
+ return b ? { lo: b, hiExcl: [b[0], b[1] + 1] } : null;
52
+ }
53
+
54
+ /** Hull {lo, hiExcl, hiIncl} of a parsed version-range AST (null bounds are open). Over-approximates, never under. */
55
+ export function rangeHull(ast) {
56
+ const inter = (a, b) => ({
57
+ lo: !a.lo ? b.lo : !b.lo ? a.lo : (compareVersions(a.lo, b.lo) >= 0 ? a.lo : b.lo),
58
+ hiExcl: !a.hiExcl ? b.hiExcl : !b.hiExcl ? a.hiExcl : (compareVersions(a.hiExcl, b.hiExcl) <= 0 ? a.hiExcl : b.hiExcl),
59
+ hiIncl: !a.hiIncl ? b.hiIncl : !b.hiIncl ? a.hiIncl : (compareVersions(a.hiIncl, b.hiIncl) <= 0 ? a.hiIncl : b.hiIncl),
60
+ });
61
+ const open = { lo: null, hiExcl: null, hiIncl: null };
62
+ const walk = (n) => {
63
+ if (!n) return open;
64
+ switch (n.t) {
65
+ case 'and': return inter(walk(n.l), walk(n.r));
66
+ case 'or': {
67
+ const a = walk(n.l); const b = walk(n.r);
68
+ const hiOf = (x) => (x.hiExcl || x.hiIncl);
69
+ const unbounded = (x) => !x.hiExcl && !x.hiIncl;
70
+ let hi = { hiExcl: null, hiIncl: null };
71
+ if (!unbounded(a) && !unbounded(b)) {
72
+ // the larger of the two upper bounds; an inclusive bound outranks an exclusive one at the same version
73
+ const ha = hiOf(a); const hb = hiOf(b);
74
+ const c = compareVersions(ha, hb);
75
+ const pick = c > 0 ? a : c < 0 ? b : (a.hiIncl ? a : b);
76
+ hi = { hiExcl: pick.hiExcl && !pick.hiIncl ? pick.hiExcl : null, hiIncl: pick.hiIncl || null };
77
+ }
78
+ return { lo: !a.lo || !b.lo ? null : (compareVersions(a.lo, b.lo) <= 0 ? a.lo : b.lo), ...hi };
79
+ }
80
+ case 'cmp': {
81
+ const v = n.version;
82
+ if (n.op === '>=' || n.op === '>') return { ...open, lo: v };
83
+ if (n.op === '<') return { ...open, hiExcl: v };
84
+ if (n.op === '<=') return { ...open, hiIncl: v };
85
+ if (n.op === '^>=') { const up = [v[0], (v[1] ?? 0n) + 1n]; return { lo: v, hiExcl: up, hiIncl: null }; }
86
+ if (n.op === '==') {
87
+ if (n.wildcard) { const up = v.slice(); up[up.length - 1] += 1n; return { lo: v, hiExcl: up, hiIncl: null }; }
88
+ return { lo: v, hiExcl: null, hiIncl: v };
89
+ }
90
+ return open;
91
+ }
92
+ case 'none': return { lo: [1n], hiExcl: [0n], hiIncl: null };
93
+ default: return open;
94
+ }
95
+ };
96
+ return walk(ast);
97
+ }
98
+
99
+ /** Three-valued MIN_VERSION_x(a,b,c): is the installed version >= target, given that it lies in `r`? */
100
+ function minVersion(r, target) {
101
+ if (!r) return UNKNOWN;
102
+ const t = target.map((x) => BigInt(x));
103
+ if (r.lo && compareVersions(r.lo, t) >= 0) return true;
104
+ if (r.hiExcl && compareVersions(r.hiExcl, t) <= 0) return false;
105
+ if (r.hiIncl && compareVersions(r.hiIncl, t) < 0) return false;
106
+ return UNKNOWN;
107
+ }
108
+
109
+ const intersectHull = (a, b) => {
110
+ if (!a) return b; if (!b) return a;
111
+ return {
112
+ lo: !a.lo ? b.lo : !b.lo ? a.lo : (compareVersions(a.lo, b.lo) >= 0 ? a.lo : b.lo),
113
+ hiExcl: !a.hiExcl ? b.hiExcl : !b.hiExcl ? a.hiExcl : (compareVersions(a.hiExcl, b.hiExcl) <= 0 ? a.hiExcl : b.hiExcl),
114
+ hiIncl: !a.hiIncl ? b.hiIncl : !b.hiIncl ? a.hiIncl : (compareVersions(a.hiIncl, b.hiIncl) <= 0 ? a.hiIncl : b.hiIncl),
115
+ };
116
+ };
117
+
118
+ // ── project context ────────────────────────────────────────────────────────
119
+
120
+ /**
121
+ * Derive what the project itself states, from manifest files in `files` (path -> text). Returns
122
+ * { ghc: {versions:number[][], basis:'pinned'|'tested-with'}|null, packages: Map<name, hull> }. Never throws.
123
+ */
124
+ export function deriveCppContext(files = {}) {
125
+ const ctx = { ghc: null, packages: new Map() };
126
+ const pinned = [];
127
+ const tested = [];
128
+ let testedUsable = true;
129
+ const hulls = new Map();
130
+ for (const [file, text] of Object.entries(files || {})) {
131
+ if (typeof text !== 'string') continue;
132
+ try {
133
+ if (/(?:^|\/)cabal\.project(?:\.local)?$/i.test(file)) {
134
+ const m = /^\s*with-compiler\s*:\s*ghc-(\d+(?:\.\d+)+)\s*$/im.exec(text);
135
+ if (m) pinned.push(toNums(parseVersion(m[1])));
136
+ } else if (/\.cabal$/i.test(file)) {
137
+ const tw = /^tested-with\s*:([^\n]*(?:\n[ \t]+[^\n]*)*)/im.exec(text);
138
+ if (tw) {
139
+ const body = tw[1];
140
+ for (const part of body.split(',').map((s) => s.trim()).filter(Boolean)) {
141
+ const m = /^GHC\s*==\s*(\d+(?:\.\d+)*)(\.\*)?$/i.exec(part);
142
+ if (m) tested.push(toNums(parseVersion(m[1])));
143
+ else if (/^GHC\b/i.test(part)) testedUsable = false; // a range or an unparsable item: never guess a version from it
144
+ }
145
+ }
146
+ const parsed = parseCabalFile(text, { file });
147
+ for (const comp of parsed.components || []) {
148
+ for (const d of comp.dependencies || []) {
149
+ if (!d || !d.name) continue;
150
+ const h = d.range ? rangeHull(d.range) : { lo: null, hiExcl: null, hiIncl: null };
151
+ const prior = hulls.get(d.name);
152
+ // union of components: the lower of the lows, the larger of the highs; an open end stays open
153
+ hulls.set(d.name, !prior ? h : {
154
+ lo: !prior.lo || !h.lo ? null : (compareVersions(prior.lo, h.lo) <= 0 ? prior.lo : h.lo),
155
+ ...((!prior.hiExcl && !prior.hiIncl) || (!h.hiExcl && !h.hiIncl) ? { hiExcl: null, hiIncl: null } : (() => {
156
+ const pa = prior.hiExcl || prior.hiIncl; const pb = h.hiExcl || h.hiIncl;
157
+ const c = compareVersions(pa, pb);
158
+ const pick = c > 0 ? prior : c < 0 ? h : (prior.hiIncl ? prior : h);
159
+ return { hiExcl: pick.hiIncl ? null : pick.hiExcl, hiIncl: pick.hiIncl || null };
160
+ })()),
161
+ });
162
+ }
163
+ }
164
+ }
165
+ } catch { /* an unreadable manifest states nothing */ }
166
+ }
167
+ if (pinned.length) ctx.ghc = { versions: pinned, basis: 'pinned' };
168
+ else if (tested.length && testedUsable) ctx.ghc = { versions: tested, basis: 'tested-with' };
169
+ ctx.packages = hulls;
170
+ return ctx;
171
+ }
172
+
173
+ // ── expression evaluation ──────────────────────────────────────────────────
174
+
175
+ const TOKEN = /\s*(0[xX][0-9a-fA-F]+[uUlL]*|\d+[uUlL]*|[A-Za-z_]\w*|&&|\|\||==|!=|<=|>=|<<|>>|[-+*/%()!<>,~&|^?:])/y;
176
+
177
+ function tokenize(text) {
178
+ const s = text.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/\/\/.*$/, ' ').trim();
179
+ const out = [];
180
+ let pos = 0;
181
+ while (pos < s.length) {
182
+ TOKEN.lastIndex = pos;
183
+ const m = TOKEN.exec(s);
184
+ if (!m) return null;
185
+ out.push(m[1]);
186
+ pos = TOKEN.lastIndex;
187
+ if (out.length > 400) return null;
188
+ }
189
+ return out;
190
+ }
191
+
192
+ const isIdent = (t) => /^[A-Za-z_]/.test(t);
193
+
194
+ /**
195
+ * Evaluate an `#if` expression to a number, or UNKNOWN. `env` = { macros: Map, ghc: number[]|null, packages: Map }.
196
+ * `used` collects the bases the answer depended on.
197
+ */
198
+ function evalExpression(text, env, used) {
199
+ const toks = tokenize(text);
200
+ if (!toks || !toks.length) return UNKNOWN;
201
+ let i = 0;
202
+ let bad = false;
203
+ const macroValue = (name, depth) => {
204
+ const m = env.macros.get(name);
205
+ if (m === undefined) return UNKNOWN;
206
+ if (m.state === 'undefined') return 0;
207
+ if (m.state !== 'defined' || m.fn) return UNKNOWN;
208
+ if (m.value === '') return UNKNOWN;
209
+ if (depth > 8) return UNKNOWN;
210
+ const t2 = tokenize(m.value);
211
+ if (!t2 || t2.length !== 1) return UNKNOWN;
212
+ if (/^\d/.test(t2[0])) return parseInt(t2[0], /^0[xX]/.test(t2[0]) ? 16 : 10);
213
+ if (isIdent(t2[0])) return macroValue(t2[0], depth + 1);
214
+ return UNKNOWN;
215
+ };
216
+ const definedState = (name) => {
217
+ const m = env.macros.get(name);
218
+ if (m) return m.state === 'defined' ? true : m.state === 'undefined' ? false : UNKNOWN;
219
+ if (name === '__GLASGOW_HASKELL__' && env.ghc) { used.add(`compiler:${env.basis}`); return true; }
220
+ return UNKNOWN;
221
+ };
222
+ const num = () => {
223
+ const t = toks[i++];
224
+ if (t === undefined) { bad = true; return UNKNOWN; }
225
+ if (t === '(') { const v = ternary(); if (toks[i++] !== ')') bad = true; return v; }
226
+ if (t === '!') { const v = unary(); return v === UNKNOWN ? UNKNOWN : (v === 0 ? 1 : 0); }
227
+ if (t === '-') { const v = unary(); return v === UNKNOWN ? UNKNOWN : -v; }
228
+ if (t === '+') return unary();
229
+ if (t === '~') { const v = unary(); return v === UNKNOWN ? UNKNOWN : ~v; }
230
+ if (/^\d/.test(t)) return parseInt(t, /^0[xX]/.test(t) ? 16 : 10);
231
+ if (!isIdent(t)) { bad = true; return UNKNOWN; }
232
+ if (t === 'defined') {
233
+ let paren = false;
234
+ if (toks[i] === '(') { paren = true; i++; }
235
+ const name = toks[i++];
236
+ if (paren && toks[i++] !== ')') bad = true;
237
+ if (!name || !isIdent(name)) { bad = true; return UNKNOWN; }
238
+ const d = definedState(name);
239
+ return d === UNKNOWN ? UNKNOWN : (d ? 1 : 0);
240
+ }
241
+ if (toks[i] === '(') {
242
+ // function-like macro call: MIN_VERSION_*(a,b,c) is modelled, every other call is unknown
243
+ const args = [];
244
+ i++;
245
+ if (toks[i] !== ')') {
246
+ for (;;) {
247
+ const a = ternary();
248
+ args.push(a);
249
+ if (toks[i] === ',') { i++; continue; }
250
+ break;
251
+ }
252
+ }
253
+ if (toks[i++] !== ')') { bad = true; return UNKNOWN; }
254
+ const fnMacro = env.macros.get(t);
255
+ if (fnMacro) return UNKNOWN; // defined (or possibly defined) in the file: its body is not expanded
256
+ return minVersionCall(t, args);
257
+ }
258
+ if (t === '__GLASGOW_HASKELL__') {
259
+ const m = env.macros.get(t);
260
+ if (m) return macroValue(t, 0);
261
+ if (!env.ghc) return UNKNOWN;
262
+ used.add(`compiler:${env.basis}`);
263
+ return env.ghc[0] * 100 + (env.ghc[1] ?? 0);
264
+ }
265
+ return macroValue(t, 0);
266
+ };
267
+ const minVersionCall = (name, args) => {
268
+ const m = /^MIN_VERSION_(.+)$/.exec(name);
269
+ if (!m || args.some((a) => a === UNKNOWN || a < 0)) return UNKNOWN;
270
+ if (m[1] === 'GLASGOW_HASKELL') {
271
+ if (!env.ghc || args.length !== 4) return UNKNOWN;
272
+ used.add(`compiler:${env.basis}`);
273
+ const r = triGte(env.ghc, args);
274
+ return r === UNKNOWN ? UNKNOWN : (r ? 1 : 0);
275
+ }
276
+ if (args.length !== 3) return UNKNOWN;
277
+ const pkg = m[1].replace(/_/g, '-');
278
+ let hull = env.packages.get(pkg) || env.packages.get(m[1]) || null;
279
+ if (hull) used.add('cabal-bounds');
280
+ if (pkg === 'base' && env.ghc) {
281
+ const bt = baseRangeOf(env.ghc);
282
+ if (bt) {
283
+ used.add(`compiler:${env.basis}`);
284
+ hull = intersectHull(hull, { lo: bt.lo.map((x) => BigInt(x)), hiExcl: bt.hiExcl.map((x) => BigInt(x)), hiIncl: null });
285
+ }
286
+ }
287
+ const r = minVersion(hull, args);
288
+ return r === UNKNOWN ? UNKNOWN : (r ? 1 : 0);
289
+ };
290
+ const unary = () => num();
291
+ const bin = (next, ops) => () => {
292
+ let l = next();
293
+ while (ops.includes(toks[i])) {
294
+ const op = toks[i++];
295
+ const r = next();
296
+ l = apply(op, l, r);
297
+ }
298
+ return l;
299
+ };
300
+ const apply = (op, l, r) => {
301
+ if (op === '&&') { if (l === 0 || r === 0) return 0; if (l === UNKNOWN || r === UNKNOWN) return UNKNOWN; return 1; }
302
+ if (op === '||') { if ((l !== UNKNOWN && l !== 0) || (r !== UNKNOWN && r !== 0)) return 1; if (l === UNKNOWN || r === UNKNOWN) return UNKNOWN; return 0; }
303
+ if (l === UNKNOWN || r === UNKNOWN) return UNKNOWN;
304
+ switch (op) {
305
+ case '*': return l * r;
306
+ case '/': return r === 0 ? UNKNOWN : Math.trunc(l / r);
307
+ case '%': return r === 0 ? UNKNOWN : l % r;
308
+ case '+': return l + r;
309
+ case '-': return l - r;
310
+ case '<<': return r < 0 || r > 30 ? UNKNOWN : l << r;
311
+ case '>>': return r < 0 || r > 30 ? UNKNOWN : l >> r;
312
+ case '<': return l < r ? 1 : 0;
313
+ case '>': return l > r ? 1 : 0;
314
+ case '<=': return l <= r ? 1 : 0;
315
+ case '>=': return l >= r ? 1 : 0;
316
+ case '==': return l === r ? 1 : 0;
317
+ case '!=': return l !== r ? 1 : 0;
318
+ case '&': return l & r;
319
+ case '^': return l ^ r;
320
+ case '|': return l | r;
321
+ default: return UNKNOWN;
322
+ }
323
+ };
324
+ const mul = bin(unary, ['*', '/', '%']);
325
+ const add = bin(mul, ['+', '-']);
326
+ const shift = bin(add, ['<<', '>>']);
327
+ const rel = bin(shift, ['<', '>', '<=', '>=']);
328
+ const eq = bin(rel, ['==', '!=']);
329
+ const band = bin(eq, ['&']);
330
+ const bxor = bin(band, ['^']);
331
+ const bor = bin(bxor, ['|']);
332
+ const land = bin(bor, ['&&']);
333
+ const lor = bin(land, ['||']);
334
+ const ternary = () => {
335
+ const c = lor();
336
+ if (toks[i] === '?') {
337
+ i++;
338
+ const a = ternary();
339
+ if (toks[i++] !== ':') { bad = true; return UNKNOWN; }
340
+ const b = ternary();
341
+ if (c === UNKNOWN) return a === b ? a : UNKNOWN;
342
+ return c !== 0 ? a : b;
343
+ }
344
+ return c;
345
+ };
346
+ const v = ternary();
347
+ if (bad || i < toks.length) return UNKNOWN;
348
+ return v;
349
+ }
350
+
351
+ /** Tri-state truth of an `#if` expression, across every candidate compiler version (they must all agree). */
352
+ function decide(text, state, ctx) {
353
+ const ghcs = ctx && ctx.ghc && ctx.ghc.versions.length ? ctx.ghc.versions : [null];
354
+ const results = [];
355
+ const used = new Set();
356
+ for (const g of ghcs) {
357
+ const env = { macros: state.macros, ghc: g, basis: ctx && ctx.ghc ? ctx.ghc.basis : null, packages: (ctx && ctx.packages) || new Map() };
358
+ results.push(truthy(evalExpression(text, env, used)));
359
+ }
360
+ const first = results[0];
361
+ const value = results.every((r) => r === first) ? first : UNKNOWN;
362
+ return { value, basis: value === UNKNOWN ? null : (used.size ? [...used].sort() : ['file']) };
363
+ }
364
+
365
+ function decideDefined(name, state, ctx) {
366
+ const m = state.macros.get(name);
367
+ if (m) return { value: m.state === 'defined' ? true : m.state === 'undefined' ? false : UNKNOWN, basis: ['file'] };
368
+ if (name === '__GLASGOW_HASKELL__' && ctx && ctx.ghc) return { value: true, basis: [`compiler:${ctx.ghc.basis}`] };
369
+ return { value: UNKNOWN, basis: null };
370
+ }
371
+
372
+ const blank = (s) => s.replace(/[^\r\n]/g, ' ');
373
+
374
+ /**
375
+ * Evaluate the CPP conditionals of `code` line by line.
376
+ * @param {string[]} lines the source split on '\n' (modified copies are returned, lengths preserved)
377
+ * @param {{ghc?:object, packages?:Map, defines?:object}} [ctx]
378
+ * @returns {{lines:string[], maybeDepth:Int32Array, decided:object[], undecided:object[], deadLines:number}}
379
+ */
380
+ export function evaluateCppLines(lines, isDirective, ctx = {}) {
381
+ const state = { macros: new Map() };
382
+ for (const [k, v] of Object.entries((ctx && ctx.defines) || {})) state.macros.set(k, { state: 'defined', value: String(v), fn: false });
383
+ const out = lines.slice();
384
+ const maybeDepth = new Int32Array(lines.length + 2);
385
+ const stack = [];
386
+ const decided = [];
387
+ const undecided = [];
388
+ let deadLines = 0;
389
+ const region = () => {
390
+ let r = 'live';
391
+ for (const f of stack) { if (f.cur === 'dead') return 'dead'; if (f.cur === 'maybe') r = 'maybe'; }
392
+ return r;
393
+ };
394
+ const maybeCount = () => stack.filter((f) => f.cur === 'maybe').length;
395
+
396
+ for (let i = 0; i < lines.length; i++) {
397
+ const info = isDirective(i);
398
+ if (!info) {
399
+ // an ordinary line: a dead region is overwritten, a live or possible one is kept
400
+ if (region() === 'dead') { out[i] = blank(out[i]); deadLines++; }
401
+ maybeDepth[i + 1] = maybeCount();
402
+ continue;
403
+ }
404
+ // directive logical line (continuations already joined by the caller in info.text); the physical lines it spans
405
+ const { name, text, last } = info;
406
+ const parentRegion = region();
407
+ const rest = text.replace(/^#\s*[A-Za-z]+/, '').trim();
408
+ const lineNo = i + 1;
409
+ const note = (kind, value, basis) => {
410
+ const rec = { line: lineNo, directive: name, outcome: value === UNKNOWN ? 'undecided' : (value ? 'true' : 'false'), basis };
411
+ if (kind === 'decided') decided.push(rec); else undecided.push(rec);
412
+ };
413
+ if (name === 'if' || name === 'ifdef' || name === 'ifndef') {
414
+ if (parentRegion === 'dead') stack.push({ cur: 'dead', taken: 'yes', parentDead: true });
415
+ else {
416
+ let d;
417
+ if (name === 'if') d = decide(rest, state, ctx);
418
+ else {
419
+ const id = /^[A-Za-z_]\w*/.exec(rest);
420
+ d = id ? decideDefined(id[0], state, ctx) : { value: UNKNOWN, basis: null };
421
+ if (name === 'ifndef' && d.value !== UNKNOWN) d = { ...d, value: !d.value };
422
+ }
423
+ const cur = d.value === UNKNOWN ? 'maybe' : (d.value ? 'live' : 'dead');
424
+ stack.push({ cur, taken: cur === 'live' ? 'yes' : cur === 'maybe' ? 'maybe' : 'no', parentDead: false });
425
+ if (d.value === UNKNOWN) note('undecided', d.value, null); else note('decided', d.value, d.basis);
426
+ }
427
+ } else if (name === 'elif' || name === 'else') {
428
+ const f = stack[stack.length - 1];
429
+ if (f && !f.parentDead) {
430
+ let c;
431
+ let basis = null;
432
+ if (name === 'else') c = true;
433
+ else if (f.taken === 'yes') c = false; // an earlier branch was taken for certain: this one is dead whatever it says
434
+ else { const d = decide(rest, state, ctx); c = d.value; basis = d.basis; }
435
+ let cur;
436
+ if (f.taken === 'yes' || c === false) cur = 'dead';
437
+ else if (c === true) cur = f.taken === 'no' ? 'live' : 'maybe';
438
+ else cur = 'maybe';
439
+ if (cur === 'live') f.taken = 'yes'; else if (cur === 'maybe' && f.taken === 'no') f.taken = 'maybe';
440
+ f.cur = cur;
441
+ if (name === 'elif' && f.taken !== 'yes') { if (c === UNKNOWN) note('undecided', c, null); else note('decided', c, basis); }
442
+ }
443
+ } else if (name === 'endif') {
444
+ stack.pop();
445
+ } else if (name === 'define' || name === 'undef') {
446
+ const m = /^([A-Za-z_]\w*)(\()?\s*(.*)$/.exec(rest);
447
+ if (m && parentRegion !== 'dead') {
448
+ const maybe = parentRegion === 'maybe';
449
+ if (name === 'undef') state.macros.set(m[1], { state: maybe ? 'maybe' : 'undefined', value: '', fn: false });
450
+ else state.macros.set(m[1], { state: maybe ? 'maybe' : 'defined', value: m[2] ? '' : m[3].replace(/\\\s*$/, '').trim(), fn: !!m[2] });
451
+ }
452
+ }
453
+ // the directive lines themselves are always blanked by the caller; mark whether they sat in a possible region
454
+ const rg = region();
455
+ void rg;
456
+ for (let q = i; q <= last; q++) maybeDepth[q + 1] = maybeCount();
457
+ i = last;
458
+ }
459
+ return { lines: out, maybeDepth, decided, undecided, deadLines, unbalanced: stack.length > 0 };
460
+ }
@@ -25,6 +25,8 @@
25
25
 
26
26
  import { createHash } from 'node:crypto';
27
27
  import { parseSyntax, groupDecls, patternVars } from './haskell-syntax.js';
28
+ import { preprocessForSyntax } from './haskell-parser.js';
29
+ import { deriveCppContext } from './haskell-cpp.js';
28
30
  import { isKnownApi, isSourceApi, qualifyAmbiguous } from './haskell-models.js';
29
31
 
30
32
  export const HS_IR_LIMITS = Object.freeze({
@@ -98,8 +100,8 @@ function callerControlledParams(fn, sig, exported) {
98
100
  }
99
101
 
100
102
  // ── module table ─────────────────────────────────────────────────────────────
101
- function buildModuleInfo(file, src) {
102
- const parse = parseSyntax(src);
103
+ function buildModuleInfo(file, src, cpp) {
104
+ const parse = parseSyntax(preprocessForSyntax(src, { file, cpp }));
103
105
  const groups = groupDecls(parse.decls);
104
106
  const name = parse.module && parse.module.name ? parse.module.name : 'Main';
105
107
  const mod = {
@@ -1174,9 +1176,10 @@ export function buildHaskellIR(fileContents) {
1174
1176
  function buildHaskellIRUncached(fileContents) {
1175
1177
  const proj = new Project();
1176
1178
  const files = Object.keys(fileContents || {}).filter((f) => /\.hs$/i.test(f)).sort();
1179
+ const cppCtx = deriveCppContext(fileContents || {});
1177
1180
  for (const file of files) {
1178
1181
  try {
1179
- const mod = buildModuleInfo(file, fileContents[file]);
1182
+ const mod = buildModuleInfo(file, fileContents[file], cppCtx);
1180
1183
  proj.modules.set(mod.name === 'Main' && proj.modules.has('Main') ? `Main@${file}` : mod.name, mod);
1181
1184
  for (const e of mod.parse.errors) proj.diagnostics.push({ file, line: e.line, kind: e.kind, detail: e.detail });
1182
1185
  } catch (err) {
@@ -235,13 +235,14 @@ export const HS_WEB_FRAMEWORKS = Object.freeze({
235
235
  yesod: { modules: ['Yesod', 'Yesod.Core', 'Yesod.Auth'], package: 'yesod', tested: ['1.6'] },
236
236
  });
237
237
  export const HS_WEB_APIS = Object.freeze([
238
- ...E('Web.Scotty', ['get', 'post', 'put', 'delete', 'patch', 'options', 'addroute', 'matchAny', 'middleware', 'scotty', 'scottyApp', 'status', 'finish', 'raise', 'redirect', 'text', 'json', 'html', 'file', 'setHeader', 'header', 'headers', 'param', 'params', 'captureParam', 'queryParam', 'formParam', 'body', 'jsonData', 'files', 'rescue', 'liftIO', 'notFound']),
239
- ...E('Web.Scotty.Trans', ['get', 'post', 'put', 'delete', 'patch', 'middleware', 'scottyT', 'status', 'finish', 'raise', 'redirect', 'text', 'json', 'html', 'header', 'param', 'captureParam', 'queryParam', 'formParam', 'body', 'jsonData']),
238
+ ...E('Web.Scotty', ['get', 'post', 'put', 'delete', 'patch', 'options', 'addroute', 'matchAny', 'middleware', 'scotty', 'scottyApp', 'status', 'finish', 'raise', 'raiseStatus', 'redirect', 'text', 'json', 'html', 'file', 'setHeader', 'header', 'headers', 'param', 'params', 'captureParam', 'queryParam', 'formParam', 'body', 'jsonData', 'files', 'rescue', 'liftIO', 'notFound']),
239
+ ...E('Web.Scotty.Trans', ['get', 'post', 'put', 'delete', 'patch', 'middleware', 'scottyT', 'status', 'finish', 'raise', 'raiseStatus', 'redirect', 'text', 'json', 'html', 'header', 'param', 'captureParam', 'queryParam', 'formParam', 'body', 'jsonData']),
240
240
  ...E('Network.Wai', ['pathInfo', 'requestMethod', 'requestHeaders', 'responseLBS', 'queryString', 'rawPathInfo']),
241
241
  ...E('Network.Wai.Handler.Warp', ['run', 'runSettings']),
242
242
  ...E('Network.HTTP.Types', ['status200', 'status201', 'status400', 'status401', 'status403', 'status404', 'unauthorized401', 'forbidden403', 'methodGet', 'methodPost', 'methodPut', 'methodDelete', 'hAuthorization', 'hCookie']),
243
243
  ...E('Network.Wai.Middleware.HttpAuth', ['basicAuth', 'extractBasicAuth']),
244
- ...E('Yesod.Core', ['requireAuthId', 'requireAuth', 'maybeAuthId', 'maybeAuth', 'permissionDenied', 'notAuthenticated', 'lookupGetParam', 'lookupPostParam', 'runInputPost', 'runInputGet', 'getYesod', 'redirect']),
244
+ ...E('Yesod.Core', ['requireAuthId', 'requireAuth', 'maybeAuthId', 'maybeAuth', 'permissionDenied', 'notAuthenticated', 'lookupHeader', 'lookupBearerAuth', 'lookupBasicAuth', 'lookupGetParam', 'lookupPostParam', 'runInputPost', 'runInputGet', 'getYesod', 'redirect']),
245
+ ...E('Yesod', ['requireAuthId', 'requireAuth', 'maybeAuthId', 'maybeAuth', 'permissionDenied', 'notAuthenticated', 'lookupHeader', 'lookupBearerAuth', 'lookupBasicAuth']),
245
246
  ...E('Yesod.Auth', ['requireAuthId', 'requireAuth', 'maybeAuthId', 'maybeAuth', 'isAdmin']),
246
247
  ...E('Servant', ['throwError', 'err401', 'err403', 'err404']),
247
248
  ...E('Servant.Server', ['throwError', 'err401', 'err403', 'err404', 'serve']),