@clear-capabilities/agentic-security-scanner 0.158.0 → 0.159.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 +29 -0
- package/dist/1919.index.js +5 -2
- package/dist/2862.index.js +29 -9
- package/dist/3531.index.js +1 -1
- package/dist/4171.index.js +4 -4
- package/dist/{8551.index.js → 7033.index.js} +415 -22
- package/dist/9560.index.js +3 -1
- package/dist/agentic-security.mjs +15 -15
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -4
- package/src/engine.js +6 -1
- package/src/language/CLAUDE.md +48 -1
- package/src/language/assurance.js +21 -4
- package/src/language/context.js +2 -0
- package/src/language/cvss.js +91 -0
- package/src/language/fix-lifecycle.js +114 -21
- package/src/language/haskell-adapter.js +4 -3
- package/src/language/haskell-advisory-feed.js +15 -0
- package/src/language/haskell-boundaries.js +220 -0
- package/src/language/haskell-cpp.js +460 -0
- package/src/language/haskell-ir.js +6 -3
- package/src/language/haskell-models.js +4 -3
- package/src/language/haskell-parser.js +122 -38
- package/src/language/haskell-sca.js +10 -6
- package/src/language/haskell-supply.js +42 -8
- package/src/language/haskell-web.js +128 -23
- package/src/language/nix-advisory-feed.js +275 -0
- package/src/language/nix-fix.js +162 -24
- package/src/language/nix-ir.js +16 -12
- package/src/language/nix-sca.js +71 -12
- package/src/language/nixos-eval-forms.js +162 -0
- package/src/language/nixos-module-resolver.js +346 -51
- package/src/language/resolved-pass.js +57 -3
- package/src/lsp/server.js +5 -2
- package/src/mcp/tools.js +3 -1
- package/src/posture/fix-history.js +30 -3
- package/src/report/index.js +4 -1
- package/dist/1310.index.js +0 -3161
- package/dist/2376.index.js +0 -3151
- package/dist/3526.index.js +0 -1019
- package/dist/7310.index.js +0 -520
- package/dist/8520.index.js +0 -13
- package/dist/9091.index.js +0 -3135
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Safe Template Haskell declarations and inert quasi-quotes (HS boundaries).
|
|
2
|
+
//
|
|
3
|
+
// The parser treats a Template Haskell splice or a quasi-quote as an opaque boundary: it cannot see the code the
|
|
4
|
+
// compiler will generate, so the file is `unresolved` in scan health. A small, closed set of constructs is different:
|
|
5
|
+
// a well-known generator applied to nothing but NAMES and LITERALS (`makeLenses ''T`, `deriveJSON defaultOptions ''T`,
|
|
6
|
+
// `share [mkPersist sqlSettings] [persistLowerCase| ... |]`) defines names, executes no project-controlled string and
|
|
7
|
+
// introduces no source, sink or sanitizer; and a raw-string quasi-quote (`[r|...|]`, or `[i|...|]` with no
|
|
8
|
+
// interpolation) is a string literal. Those are recognised here and reported as a DISCLOSED ASSUMPTION instead of an
|
|
9
|
+
// opaque boundary.
|
|
10
|
+
//
|
|
11
|
+
// Soundness rules (each one is pinned by a test in both directions):
|
|
12
|
+
// - The generator must be IMPORTED from the upstream module that defines it, with the import actually exposing the name,
|
|
13
|
+
// and must not be defined in the file. A local `makeLenses` stays opaque.
|
|
14
|
+
// - Every token of the declaration must be accounted for. An argument that is a variable, a call, a lambda, a string
|
|
15
|
+
// built from an expression, a nested splice or any other quasi-quote leaves the whole declaration opaque.
|
|
16
|
+
// - Nothing here executes anything. This is a closed-form match over tokens, not an evaluation.
|
|
17
|
+
// - The assumption is stated on the boundary: the generator is taken to be the upstream package's, and what it
|
|
18
|
+
// generates (lenses, instances, persistent entities) is not modelled.
|
|
19
|
+
|
|
20
|
+
// generator name -> modules that export it
|
|
21
|
+
const GENERATORS = new Map([
|
|
22
|
+
['makeLenses', ['Control.Lens', 'Control.Lens.TH', 'Lens.Micro.TH', 'Lens.Micro.Platform']],
|
|
23
|
+
['makeLensesWith', ['Control.Lens', 'Control.Lens.TH']],
|
|
24
|
+
['makeClassy', ['Control.Lens', 'Control.Lens.TH', 'Lens.Micro.TH', 'Lens.Micro.Platform']],
|
|
25
|
+
['makeClassyPrisms', ['Control.Lens', 'Control.Lens.TH']],
|
|
26
|
+
['makePrisms', ['Control.Lens', 'Control.Lens.TH']],
|
|
27
|
+
['makeFields', ['Control.Lens', 'Control.Lens.TH']],
|
|
28
|
+
['makeWrapped', ['Control.Lens', 'Control.Lens.TH', 'Control.Lens.Wrapped']],
|
|
29
|
+
['deriveJSON', ['Data.Aeson.TH']],
|
|
30
|
+
['deriveToJSON', ['Data.Aeson.TH']],
|
|
31
|
+
['deriveFromJSON', ['Data.Aeson.TH']],
|
|
32
|
+
['deriveSafeCopy', ['Data.SafeCopy']],
|
|
33
|
+
['deriveSafeCopySimple', ['Data.SafeCopy']],
|
|
34
|
+
['mkPersist', ['Database.Persist.TH']],
|
|
35
|
+
['mkMigrate', ['Database.Persist.TH']],
|
|
36
|
+
['mkSave', ['Database.Persist.TH']],
|
|
37
|
+
['mkDeleteCascade', ['Database.Persist.TH']],
|
|
38
|
+
['share', ['Database.Persist.TH']],
|
|
39
|
+
['makeAcidic', ['Data.Acid', 'Data.Acid.TH']],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// plain identifiers that may appear as an argument: option records and settings values the generators document
|
|
43
|
+
const SAFE_VALUES = new Map([
|
|
44
|
+
['defaultOptions', ['Data.Aeson.TH', 'Data.Aeson']],
|
|
45
|
+
['lensRules', ['Control.Lens', 'Control.Lens.TH']],
|
|
46
|
+
['camelCaseFields', ['Control.Lens', 'Control.Lens.TH']],
|
|
47
|
+
['abbreviatedFields', ['Control.Lens', 'Control.Lens.TH']],
|
|
48
|
+
['underscoreFields', ['Control.Lens', 'Control.Lens.TH']],
|
|
49
|
+
['sqlSettings', ['Database.Persist.TH']],
|
|
50
|
+
['mkPersistSettings', ['Database.Persist.TH']],
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// quasi-quoters accepted inside a safe declaration
|
|
54
|
+
const SAFE_DECL_QUOTERS = new Map([
|
|
55
|
+
['persistLowerCase', ['Database.Persist.TH']],
|
|
56
|
+
['persistUpperCase', ['Database.Persist.TH']],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// Option-record update (`defaultOptions { fieldLabelModifier = drop 4 }`): field names and the pure helpers a value may use.
|
|
60
|
+
const OPTION_FIELDS = new Set(['fieldLabelModifier', 'constructorTagModifier', 'omitNothingFields', 'allNullaryToStringTag', 'sumEncoding', 'unwrapUnaryRecords', 'tagSingleConstructors', 'rejectUnknownFields']);
|
|
61
|
+
const PURE_HELPERS = new Set(['drop', 'take', 'map', 'toLower', 'toUpper', 'id', 'camelTo2', 'tail', 'init']);
|
|
62
|
+
|
|
63
|
+
// inert quasi-quoters: name -> { modules, interpolating }. `interpolating` quoters are inert only when the body has no
|
|
64
|
+
// interpolation marker; a raw-string quoter is inert whatever the body says.
|
|
65
|
+
const INERT_QUOTERS = new Map([
|
|
66
|
+
['r', { modules: ['Text.RawString.QQ'], check: false }],
|
|
67
|
+
['here', { modules: ['Data.String.Here', 'Data.String.Here.Uninterpolated', 'Data.String.Here.Interpolated'], check: true }],
|
|
68
|
+
['hereLit', { modules: ['Data.String.Here', 'Data.String.Here.Uninterpolated'], check: true }],
|
|
69
|
+
['i', { modules: ['Data.String.Interpolate', 'Data.String.Interpolate.IsString', 'Data.String.Here', 'Data.String.Here.Interpolated'], check: true }],
|
|
70
|
+
['iii', { modules: ['Data.String.Interpolate'], check: true }],
|
|
71
|
+
['__i', { modules: ['Data.String.Interpolate'], check: true }],
|
|
72
|
+
['iTrim', { modules: ['Data.String.Here', 'Data.String.Here.Interpolated'], check: true }],
|
|
73
|
+
]);
|
|
74
|
+
const INTERPOLATION_MARK = /#\{|\$\{|\$\(|\$[A-Za-z_]/;
|
|
75
|
+
|
|
76
|
+
function splitQuoter(q) {
|
|
77
|
+
const i = q.lastIndexOf('.');
|
|
78
|
+
return i < 0 ? { qual: null, name: q } : { qual: q.slice(0, i), name: q.slice(i + 1) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Is `name` (optionally qualified by `qual`) exported by one of `modules` through a visible import, and not shadowed locally? */
|
|
82
|
+
function importedFrom(name, qual, modules, imports, defined) {
|
|
83
|
+
if (defined.has(name) && !qual) return false;
|
|
84
|
+
for (const imp of imports) {
|
|
85
|
+
if (!imp || !imp.module || !modules.includes(imp.module)) continue;
|
|
86
|
+
if (qual) { if ((imp.as || imp.module) !== qual) continue; }
|
|
87
|
+
else if (imp.qualified) continue;
|
|
88
|
+
if (imp.items) {
|
|
89
|
+
const listed = imp.items.includes(name);
|
|
90
|
+
if (imp.hiding ? listed : !listed) continue;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Classify one top-level splice item as a safe generative declaration.
|
|
99
|
+
* @returns {{generators:string[], modules:string[]}|null} null leaves it opaque
|
|
100
|
+
*/
|
|
101
|
+
export function classifySafeSplice(T, a, b, match, imports, defined) {
|
|
102
|
+
const generators = [];
|
|
103
|
+
const modules = new Set();
|
|
104
|
+
const tok = (i) => T[i];
|
|
105
|
+
const sameWord = (t) => t.k === 'varid' || t.k === 'qvarid';
|
|
106
|
+
|
|
107
|
+
// value: a single argument starting at p, returns the next index or -1
|
|
108
|
+
const parseValue = (p, end) => {
|
|
109
|
+
const t = tok(p);
|
|
110
|
+
if (!t) return -1;
|
|
111
|
+
if (t.k === 'thname' || t.k === 'string' || t.k === 'int' || t.k === 'conid' || t.k === 'qconid') {
|
|
112
|
+
let q = p + 1;
|
|
113
|
+
// a record update right after an atom: defaultOptions { field = value, ... }
|
|
114
|
+
if (q < end && tok(q).k === '{' && match[q] > q && match[q] < end) {
|
|
115
|
+
if (!parseRecordUpdate(q, match[q])) return -1;
|
|
116
|
+
q = match[q] + 1;
|
|
117
|
+
}
|
|
118
|
+
return q;
|
|
119
|
+
}
|
|
120
|
+
if (sameWord(t)) {
|
|
121
|
+
const sv = SAFE_VALUES.get(t.name || t.v);
|
|
122
|
+
if (!sv || !importedFrom(t.name || t.v, t.qual || null, sv, imports, defined)) return -1;
|
|
123
|
+
let q = p + 1;
|
|
124
|
+
if (q < end && tok(q).k === '{' && match[q] > q && match[q] < end) {
|
|
125
|
+
if (!parseRecordUpdate(q, match[q])) return -1;
|
|
126
|
+
q = match[q] + 1;
|
|
127
|
+
}
|
|
128
|
+
return q;
|
|
129
|
+
}
|
|
130
|
+
if (t.k === 'quasiquote') {
|
|
131
|
+
const { qual, name } = splitQuoter(t.quoter || '');
|
|
132
|
+
const mods = SAFE_DECL_QUOTERS.get(name);
|
|
133
|
+
if (!mods || !importedFrom(name, qual, mods, imports, defined)) return -1;
|
|
134
|
+
return p + 1;
|
|
135
|
+
}
|
|
136
|
+
if (t.k === '(' && match[p] > p && match[p] < end) {
|
|
137
|
+
if (!parseApp(p + 1, match[p], true)) return -1;
|
|
138
|
+
return match[p] + 1;
|
|
139
|
+
}
|
|
140
|
+
if (t.k === '[' && match[p] > p && match[p] < end) {
|
|
141
|
+
// a list of applications or atoms, separated by top-level commas
|
|
142
|
+
let q = p + 1;
|
|
143
|
+
const close = match[p];
|
|
144
|
+
if (q === close) return close + 1;
|
|
145
|
+
for (;;) {
|
|
146
|
+
let e = q;
|
|
147
|
+
while (e < close && tok(e).k !== ',') e = (['(', '[', '{'].includes(tok(e).k) && match[e] > e) ? match[e] + 1 : e + 1;
|
|
148
|
+
if (e === q) return -1;
|
|
149
|
+
if (!parseApp(q, e, true)) return -1;
|
|
150
|
+
if (e >= close) break;
|
|
151
|
+
q = e + 1;
|
|
152
|
+
}
|
|
153
|
+
return close + 1;
|
|
154
|
+
}
|
|
155
|
+
return -1;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const parseRecordUpdate = (open, close) => {
|
|
159
|
+
let p = open + 1;
|
|
160
|
+
if (p === close) return true;
|
|
161
|
+
for (;;) {
|
|
162
|
+
const f = tok(p);
|
|
163
|
+
if (!f || f.k !== 'varid' || !OPTION_FIELDS.has(f.v)) return false;
|
|
164
|
+
if (!tok(p + 1) || tok(p + 1).k !== 'rop' || tok(p + 1).v !== '=') return false;
|
|
165
|
+
p += 2;
|
|
166
|
+
let valueEnd = p;
|
|
167
|
+
while (valueEnd < close && tok(valueEnd).k !== ',') valueEnd++;
|
|
168
|
+
if (valueEnd === p) return false;
|
|
169
|
+
for (let q = p; q < valueEnd; q++) {
|
|
170
|
+
const t = tok(q);
|
|
171
|
+
const ok = t.k === 'int' || t.k === 'string' || t.k === 'conid' || t.k === 'qconid' || t.k === '(' || t.k === ')'
|
|
172
|
+
|| (t.k === 'op' && t.v === '.') || (t.k === 'varid' && PURE_HELPERS.has(t.v));
|
|
173
|
+
if (!ok) return false;
|
|
174
|
+
}
|
|
175
|
+
if (valueEnd >= close) return true;
|
|
176
|
+
p = valueEnd + 1;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// application: generator followed by values. `nested` allows a generator head inside parentheses or lists.
|
|
181
|
+
const parseApp = (p, end, nested) => {
|
|
182
|
+
const h = tok(p);
|
|
183
|
+
if (!h || !sameWord(h)) {
|
|
184
|
+
// a bare value inside a list (`[mkPersist sqlSettings, mkMigrate "m"]` elements are applications, but a quoter or name may stand alone)
|
|
185
|
+
if (nested && p < end) { const n = parseValue(p, end); return n === end; }
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const name = h.name || h.v;
|
|
189
|
+
const mods = GENERATORS.get(name);
|
|
190
|
+
if (!mods && nested) return parseValue(p, end) === end;
|
|
191
|
+
if (!mods || !importedFrom(name, h.qual || null, mods, imports, defined)) return false;
|
|
192
|
+
generators.push(name);
|
|
193
|
+
for (const m of mods) modules.add(m);
|
|
194
|
+
let q = p + 1;
|
|
195
|
+
while (q < end) {
|
|
196
|
+
const n = parseValue(q, end);
|
|
197
|
+
if (n < 0) return false;
|
|
198
|
+
q = n;
|
|
199
|
+
}
|
|
200
|
+
return q === end;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// optional outer splice wrapper: $( ... )
|
|
204
|
+
let s = a;
|
|
205
|
+
let e = b;
|
|
206
|
+
if (tok(a).k === 'op' && tok(a).v === '$' && tok(a + 1) && tok(a + 1).k === '(' && tok(a).e === tok(a + 1).s && match[a + 1] === b - 1) { s = a + 2; e = b - 1; }
|
|
207
|
+
if (s >= e) return null;
|
|
208
|
+
if (!parseApp(s, e, false)) return null;
|
|
209
|
+
return { generators, modules: [...modules].sort() };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Classify one quasi-quote token as an inert string. @returns {{quoter:string, module:string}|null} */
|
|
213
|
+
export function classifyInertQuasiQuote(quoter, body, imports, defined) {
|
|
214
|
+
if (!quoter) return null;
|
|
215
|
+
const { qual, name } = splitQuoter(quoter);
|
|
216
|
+
const q = INERT_QUOTERS.get(name);
|
|
217
|
+
if (!q || !importedFrom(name, qual, q.modules, imports, defined)) return null;
|
|
218
|
+
if (q.check && INTERPOLATION_MARK.test(body)) return null;
|
|
219
|
+
return { quoter: name, modules: q.modules };
|
|
220
|
+
}
|
|
@@ -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
|
+
}
|