@portll/cobolwork 0.2.150 → 0.4.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/README.md +20 -10
- package/lib/cics-commands.mjs +308 -42
- package/lib/consequence.mjs +6 -0
- package/lib/control.mjs +225 -14
- package/lib/dataflow.mjs +82 -28
- package/lib/explain.mjs +31 -5
- package/lib/exploitability.mjs +49 -4
- package/lib/gate.mjs +31 -9
- package/lib/kernel/findings.mjs +2 -0
- package/lib/kernel/registry.mjs +2 -0
- package/lib/kernel/source-tree.mjs +1 -0
- package/lib/options.mjs +6 -2
- package/lib/packs.mjs +4 -0
- package/lib/parser.mjs +92 -23
- package/lib/precompile-cics.mjs +9 -3
- package/lib/precompile.mjs +3 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +3 -2
- package/lib/scan.mjs +2 -1
- package/lib/sets/abend.mjs +148 -0
- package/lib/sets/compile.mjs +2 -1
- package/lib/sets/hidden.mjs +1 -1
- package/lib/sets/jcl.mjs +1 -0
- package/lib/sets/recon.mjs +1 -0
- package/lib/utilities.mjs +2 -0
- package/lib/verify.mjs +2 -1
- package/lib/words.mjs +377 -17
- package/package.json +2 -2
- package/rules/compliance-cobit2019.json +48 -0
- package/rules/compliance-dora.json +52 -0
- package/rules/compliance-ffiec.json +52 -0
- package/rules/compliance-nist80053.json +52 -0
package/lib/control.mjs
CHANGED
|
@@ -1775,7 +1775,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1775
1775
|
const needs = [...new Set([lo && lo.bit, hi && hi.bit].filter((x) => x != null))].sort((x, y) => x - y);
|
|
1776
1776
|
const from = lo ? lo.v : base.lo;
|
|
1777
1777
|
const to = hi ? hi.v : base.hi;
|
|
1778
|
-
|
|
1778
|
+
if (!needs.length || from > to) return [base];
|
|
1779
|
+
// Each end alone as well, so a result that needs only one of them does not wait on the other.
|
|
1780
|
+
const ends = lo && hi && lo.bit !== hi.bit ? [{ needs: [lo.bit], lo: lo.v, hi: base.hi }, { needs: [hi.bit], lo: base.lo, hi: hi.v }] : [];
|
|
1781
|
+
return [base, { needs, lo: from, hi: to }, ...ends];
|
|
1779
1782
|
};
|
|
1780
1783
|
// A fact "the field holds a whole number from lo to hi" (either end may be open), one per field and interval.
|
|
1781
1784
|
const derivedOf = new Map();
|
|
@@ -1815,6 +1818,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1815
1818
|
// is not among them: the interval is what the data holds, not a test that ran.
|
|
1816
1819
|
const withLooser = (fact) => {
|
|
1817
1820
|
const out = [fact.t];
|
|
1821
|
+
if (fact.pair) {
|
|
1822
|
+
for (const [k, other] of pairFacts.get(fact.pairKey)) if (k > fact.k && (fact.pair === 'span' || other.a === fact.a)) out.push(other.t);
|
|
1823
|
+
return out;
|
|
1824
|
+
}
|
|
1818
1825
|
const own = fact.bounds;
|
|
1819
1826
|
for (const c of factsByKey.get(fact.key) || []) {
|
|
1820
1827
|
if (c === fact || !c.derived) continue;
|
|
@@ -2047,6 +2054,108 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2047
2054
|
relations.clear();
|
|
2048
2055
|
}
|
|
2049
2056
|
|
|
2057
|
+
// Facts over two whole-number fields, killed by a write to either: "A - B <= k" (an order) and
|
|
2058
|
+
// "A + B <= k" (a span, which keeps a reference modification X(A:B) inside X). They are drawn
|
|
2059
|
+
// only for the start and length of a reference modification and the fields a length is computed from.
|
|
2060
|
+
const spanPairs = new Map();
|
|
2061
|
+
for (const st of prog.statements) {
|
|
2062
|
+
for (const x of st.indexes || []) {
|
|
2063
|
+
if (x.kind !== 'refmod-length' || !x.from || !x.from.tok) continue;
|
|
2064
|
+
const len = x.tok && x.tok.t === 'word' ? resolve(x.tok) : null;
|
|
2065
|
+
const start = resolve(x.from.tok);
|
|
2066
|
+
if (!len || !start || len === start || !rangeOfField(len) || !rangeOfField(start) || inTable(len) || inTable(start)) continue;
|
|
2067
|
+
if (!spanPairs.has(len)) spanPairs.set(len, new Set());
|
|
2068
|
+
spanPairs.get(len).add(start);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
const starts = new Set([...spanPairs.values()].flatMap((s) => [...s]));
|
|
2072
|
+
const pairIds = new Map();
|
|
2073
|
+
const pairId = (f) => { if (!pairIds.has(f)) pairIds.set(f, pairIds.size); return pairIds.get(f); };
|
|
2074
|
+
const pairFacts = new Map();
|
|
2075
|
+
const pairKey = (kind, a, b) => (kind === 'span' && pairId(a) > pairId(b) ? `${kind}|${pairId(b)}|${pairId(a)}` : `${kind}|${pairId(a)}|${pairId(b)}`);
|
|
2076
|
+
const pairFact = (kind, a, b, k, at) => {
|
|
2077
|
+
const key = pairKey(kind, a, b);
|
|
2078
|
+
if (!pairFacts.has(key)) pairFacts.set(key, new Map());
|
|
2079
|
+
const mine = pairFacts.get(key);
|
|
2080
|
+
if (mine.has(k)) return mine.get(k);
|
|
2081
|
+
if (!arithBudget()) return null;
|
|
2082
|
+
const fact = { pair: kind, pairKey: key, a, b, k, t: nFacts++, file: at.file, line: at.line };
|
|
2083
|
+
for (const f of [a, b]) {
|
|
2084
|
+
const fk = fieldKey(f);
|
|
2085
|
+
noteField(fk, f);
|
|
2086
|
+
checks.push({ pseudo: true, key: fk, field: f, x: null, t: fact.t, f: null });
|
|
2087
|
+
}
|
|
2088
|
+
mine.set(k, fact);
|
|
2089
|
+
return fact;
|
|
2090
|
+
};
|
|
2091
|
+
// What an outcome makes true of two fields, as order and span facts: S <= E, S + L - 1 <= 80.
|
|
2092
|
+
const pairsOf = (rel, pol) => {
|
|
2093
|
+
const op = pol ? rel.op : NEGATE[rel.op];
|
|
2094
|
+
if (!['<', '<=', '>', '>=', '='].includes(op)) return [];
|
|
2095
|
+
const l = linearOf(rel.left), r = linearOf(rel.right);
|
|
2096
|
+
const e = l && r ? linAdd(l, r, -1) : null;
|
|
2097
|
+
if (!e || e.terms.size !== 2 || e.lo !== e.hi || !Number.isInteger(e.lo)) return [];
|
|
2098
|
+
const out = [];
|
|
2099
|
+
// e op 0 as one or two facts "sum <= k" over the terms, each with its signs.
|
|
2100
|
+
const below = (sign, strict) => {
|
|
2101
|
+
const terms = [...e.terms].map(([f, c]) => [f, sign * c]);
|
|
2102
|
+
if (terms.some(([f, c]) => Math.abs(c) !== 1 || !rangeOfField(f) || inTable(f))) return;
|
|
2103
|
+
const k = -sign * e.lo - (strict ? 1 : 0);
|
|
2104
|
+
const [[a, ca], [b, cb]] = terms;
|
|
2105
|
+
if (ca > 0 && cb > 0) { if ((spanPairs.get(a)?.has(b)) || (spanPairs.get(b)?.has(a))) out.push(['span', a, b, k]); }
|
|
2106
|
+
else if (ca > 0 && cb < 0) { if (starts.has(a)) out.push(['order', a, b, k]); }
|
|
2107
|
+
else if (ca < 0 && cb > 0) { if (starts.has(b)) out.push(['order', b, a, k]); }
|
|
2108
|
+
};
|
|
2109
|
+
if (op === '<' || op === '<=' || op === '=') below(1, op === '<');
|
|
2110
|
+
if (op === '>' || op === '>=' || op === '=') below(-1, op === '>');
|
|
2111
|
+
return out;
|
|
2112
|
+
};
|
|
2113
|
+
if (spanPairs.size) {
|
|
2114
|
+
for (const id of tests) {
|
|
2115
|
+
const n = nodes[id];
|
|
2116
|
+
if (!n.tree) continue;
|
|
2117
|
+
for (const o of [true, false]) {
|
|
2118
|
+
const at = outcomeAt.get(`${id}|${o}`);
|
|
2119
|
+
if (!at) continue;
|
|
2120
|
+
for (const { rel, pol } of relationsOf(n.tree, o)) {
|
|
2121
|
+
for (const [kind, a, b, k] of pairsOf(rel, pol)) {
|
|
2122
|
+
const fact = pairFact(kind, a, b, k, n.at || n.st);
|
|
2123
|
+
if (fact) derive(at, [], fact, String(o));
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
// The statements that only raise a field of a span, by at most `max`.
|
|
2130
|
+
const spanRaisers = [];
|
|
2131
|
+
if (spanPairs.size) {
|
|
2132
|
+
const spanFields = new Set([...spanPairs.keys(), ...starts]);
|
|
2133
|
+
nodes.forEach((n, id) => {
|
|
2134
|
+
if (n.kind !== 'stmt' || !n.st || n.st.at == null) return;
|
|
2135
|
+
const inc = increments(n.st);
|
|
2136
|
+
if (inc && inc.fields.some((f) => spanFields.has(f))) spanRaisers.push({ id, inc, wrote: writtenBy(n) });
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
const spanLifted = new Set();
|
|
2140
|
+
// An arithmetic tree as whole-number coefficients per plain field plus a whole constant, or null.
|
|
2141
|
+
const linearTree = (t) => {
|
|
2142
|
+
switch (t.k) {
|
|
2143
|
+
case 'const': return t.v.d === 1n ? { terms: new Map(), c: Number(t.v.n) } : null;
|
|
2144
|
+
case 'field': return t.plain ? { terms: new Map([[t.f, 1]]), c: 0 } : null;
|
|
2145
|
+
case 'neg': { const a = linearTree(t.a); return a && { terms: new Map([...a.terms].map(([f, c]) => [f, -c])), c: -a.c }; }
|
|
2146
|
+
case 'bin': {
|
|
2147
|
+
if (t.op !== '+' && t.op !== '-') return null;
|
|
2148
|
+
const a = linearTree(t.a), b = linearTree(t.b);
|
|
2149
|
+
if (!a || !b) return null;
|
|
2150
|
+
const sign = t.op === '+' ? 1 : -1;
|
|
2151
|
+
const terms = new Map(a.terms);
|
|
2152
|
+
for (const [f, c] of b.terms) { const v = (terms.get(f) || 0) + sign * c; if (v) terms.set(f, v); else terms.delete(f); }
|
|
2153
|
+
return { terms, c: a.c + sign * b.c };
|
|
2154
|
+
}
|
|
2155
|
+
default: return null;
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2050
2159
|
// The fields that matter: those an index reads, and those that flow into them through the
|
|
2051
2160
|
// statements above and the relations between fields.
|
|
2052
2161
|
const relevant = new Set();
|
|
@@ -2092,16 +2201,59 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2092
2201
|
a.trees.forEach((tree, i) => {
|
|
2093
2202
|
const r = a.targets[i];
|
|
2094
2203
|
if (!tree || !r.f || !rangeOfField(r.f) || inTable(r.f) || !relevant.has(r.f)) return;
|
|
2095
|
-
const fit = rangeOfField(r.f)
|
|
2204
|
+
const { fit, hold } = rangeOfField(r.f);
|
|
2096
2205
|
heldBits = prev ? prev.get(a.n.id) || null : null;
|
|
2097
2206
|
for (const c of candidates(tree, leafCands, 64)) {
|
|
2098
2207
|
const s = storedRange(c.iv, r.rounded);
|
|
2099
|
-
if (s.lo
|
|
2100
|
-
|
|
2208
|
+
if (s.lo === fit.lo && s.hi === fit.hi) continue;
|
|
2209
|
+
// A result past one end of what the field keeps loses high digits and keeps its sign, and no
|
|
2210
|
+
// result past what the storage holds is added, so the other end still bounds it.
|
|
2211
|
+
const whole = s.lo >= fit.lo && s.hi <= fit.hi;
|
|
2212
|
+
const top = whole || (s.lo >= hold.lo && s.hi <= fit.hi && (s.hi >= 0n || s.lo >= fit.lo));
|
|
2213
|
+
const bottom = whole || (s.hi <= hold.hi && s.lo >= fit.lo && (s.lo <= 0n || s.hi <= fit.hi));
|
|
2214
|
+
if (!top && !bottom) continue;
|
|
2215
|
+
const fact = derivedFact(r.f, bottom ? s.lo : null, top ? s.hi : null, a.n.st);
|
|
2101
2216
|
if (fact && derive(a.n, c.needs, fact)) grew = true;
|
|
2102
2217
|
}
|
|
2103
2218
|
});
|
|
2104
2219
|
}
|
|
2220
|
+
// A length computed as E - S + c from its start S, where S <= E + d held, is at least c - d, which
|
|
2221
|
+
// intervals alone cannot see; and S + L is then E + c, at most what E could hold plus c.
|
|
2222
|
+
for (const a of arith) {
|
|
2223
|
+
a.trees.forEach((tree, i) => {
|
|
2224
|
+
const r = a.targets[i];
|
|
2225
|
+
const mates = r.f && spanPairs.get(r.f);
|
|
2226
|
+
const lin = mates && tree ? linearTree(tree) : null;
|
|
2227
|
+
if (!lin) return;
|
|
2228
|
+
const fit = rangeOfField(r.f).fit;
|
|
2229
|
+
heldBits = prev ? prev.get(a.n.id) || null : null;
|
|
2230
|
+
for (const [s, cs] of lin.terms) {
|
|
2231
|
+
if (cs !== -1 || !mates.has(s) || s === r.f) continue;
|
|
2232
|
+
const rest = [...lin.terms].filter(([f]) => f !== s);
|
|
2233
|
+
if (rest.length > 1 || (rest.length && (rest[0][1] !== 1 || rest[0][0] === r.f))) continue;
|
|
2234
|
+
const e = rest.length ? rest[0][0] : null;
|
|
2235
|
+
const orders = e ? [...(pairFacts.get(pairKey('order', s, e))?.values() || [])] : [];
|
|
2236
|
+
const eCands = e ? leafCands({ f: e, tok: null, plain: true }) : [{ needs: [], lo: 0n, hi: 0n }];
|
|
2237
|
+
const sCands = leafCands({ f: s, tok: null, plain: true });
|
|
2238
|
+
// With no E, the lowest L is c less S's highest, which an interval of S already gives.
|
|
2239
|
+
const floors = e ? orders.map((o) => ({ needs: [o.t], lo: BigInt(lin.c - o.k) })) : sCands.map((sc) => ({ needs: sc.needs, lo: BigInt(lin.c) - sc.hi }));
|
|
2240
|
+
for (const fl of floors) {
|
|
2241
|
+
if (fl.lo < fit.lo) continue;
|
|
2242
|
+
for (const ec of eCands) {
|
|
2243
|
+
for (const sc of sCands) {
|
|
2244
|
+
const hi = ec.hi - sc.lo + BigInt(lin.c);
|
|
2245
|
+
if (hi > fit.hi || hi < fl.lo) continue;
|
|
2246
|
+
const needs = [...new Set([...fl.needs, ...ec.needs, ...sc.needs])].sort((x, y) => x - y);
|
|
2247
|
+
const fact = derivedFact(r.f, fl.lo, hi, a.n.st);
|
|
2248
|
+
if (fact && derive(a.n, needs, fact)) grew = true;
|
|
2249
|
+
const span = pairFact('span', s, r.f, Number(ec.hi) + lin.c, a.n.st);
|
|
2250
|
+
if (span && derive(a.n, needs, span)) grew = true;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2105
2257
|
for (const rel of relations.values()) {
|
|
2106
2258
|
if (!relevant.has(rel.f) && !relevant.has(rel.g)) continue;
|
|
2107
2259
|
const at = nodes[rel.test].at || nodes[rel.test].st;
|
|
@@ -2124,6 +2276,24 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2124
2276
|
}
|
|
2125
2277
|
}
|
|
2126
2278
|
}
|
|
2279
|
+
// An increment of a span's field by at most n leaves the span n looser: the sum grows by n at most,
|
|
2280
|
+
// and a sum that wraps or is cut short is only lower.
|
|
2281
|
+
for (const { id, inc, wrote } of spanRaisers) {
|
|
2282
|
+
const held = prev ? prev.get(id) : null;
|
|
2283
|
+
if (!held) continue;
|
|
2284
|
+
for (const mine of pairFacts.values()) {
|
|
2285
|
+
for (const f of [...mine.values()]) {
|
|
2286
|
+
if (f.pair !== 'span' || !hasFact(held, f.t)) continue;
|
|
2287
|
+
const count = [f.a, f.b].filter((x) => inc.fields.includes(x)).length;
|
|
2288
|
+
if (!count || wrote.some((w) => !inc.fields.includes(w) && (overlaps(w, f.a) || overlaps(w, f.b)))) continue;
|
|
2289
|
+
const to = pairFact('span', f.a, f.b, f.k + count * inc.max, nodes[id].st);
|
|
2290
|
+
if (!to || spanLifted.has(`${id}|${f.t}|${to.t}`)) continue;
|
|
2291
|
+
spanLifted.add(`${id}|${f.t}|${to.t}`);
|
|
2292
|
+
(nodes[id].spanLifts ||= []).push([f.t, to.t]);
|
|
2293
|
+
grew = true;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2127
2297
|
// A bound over an expression in one other field, X <= G - 1, takes the interval G holds at the test.
|
|
2128
2298
|
for (const c of checks) {
|
|
2129
2299
|
if (c.pseudo || c.test == null || !relevant.has(c.field) || !rangeOfField(c.field) || inTable(c.field)) continue;
|
|
@@ -2340,6 +2510,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2340
2510
|
const killScratch = new Uint32Array(words);
|
|
2341
2511
|
// A summary is joined to whatever enters the range, so it cannot assume a guard the caller lacks.
|
|
2342
2512
|
let summarising = false;
|
|
2513
|
+
// While summarising, a fact that depends on others is taken only where the range's own statements
|
|
2514
|
+
// make those others on every route: then it holds whatever the caller brings.
|
|
2515
|
+
let approved = null;
|
|
2516
|
+
const has = (bits, x) => (bits[x >>> 5] & (1 << (x & 31))) !== 0;
|
|
2343
2517
|
// What holds after a node, in a buffer reused by every call: the caller merges it into the next
|
|
2344
2518
|
// node's facts before calling again. Null where the route ends here.
|
|
2345
2519
|
const transfer = (n, input) => {
|
|
@@ -2362,7 +2536,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2362
2536
|
kill = killScratch;
|
|
2363
2537
|
kill.set(n.kill);
|
|
2364
2538
|
for (const k of n.keeps) {
|
|
2365
|
-
if (k.guards && (summarising
|
|
2539
|
+
if (k.guards && (summarising ? !approved.has(k) : !k.guards.some((g) => has(input, g)))) continue;
|
|
2366
2540
|
for (let i = 0; i < words; i++) kill[i] &= ~k.bits[i];
|
|
2367
2541
|
}
|
|
2368
2542
|
}
|
|
@@ -2377,26 +2551,36 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2377
2551
|
// An increment that cannot wrap raises the lower bounds that held before it.
|
|
2378
2552
|
if (n.lifts) {
|
|
2379
2553
|
for (const { pairs, guards } of n.lifts) {
|
|
2380
|
-
if (summarising
|
|
2381
|
-
for (const
|
|
2382
|
-
|
|
2554
|
+
if (!summarising && !guards.some((g) => has(input, g))) continue;
|
|
2555
|
+
for (const pair of pairs) {
|
|
2556
|
+
const [from, to] = pair;
|
|
2557
|
+
if (summarising ? !approved.has(pair) : !has(input, from)) continue;
|
|
2383
2558
|
if (out === input) { scratch.set(input); out = scratch; }
|
|
2384
2559
|
out[to >>> 5] |= 1 << (to & 31);
|
|
2385
2560
|
}
|
|
2386
2561
|
}
|
|
2387
2562
|
}
|
|
2563
|
+
if (n.spanLifts) {
|
|
2564
|
+
for (const pair of n.spanLifts) {
|
|
2565
|
+
const [from, to] = pair;
|
|
2566
|
+
if (summarising ? !approved.has(pair) : !has(input, from)) continue;
|
|
2567
|
+
if (out === input) { scratch.set(input); out = scratch; }
|
|
2568
|
+
out[to >>> 5] |= 1 << (to & 31);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2388
2571
|
// What a statement or a test leaves in a field, where the facts its operands needed held before it.
|
|
2389
|
-
if (n.derive
|
|
2572
|
+
if (n.derive) {
|
|
2390
2573
|
for (const d of n.derive) {
|
|
2391
|
-
if (!d.needs.every((x) => input
|
|
2574
|
+
if (summarising ? !approved.has(d) : !d.needs.every((x) => has(input, x))) continue;
|
|
2392
2575
|
if (out === input) { scratch.set(input); out = scratch; }
|
|
2393
2576
|
for (const t of d.to) out[t >>> 5] |= 1 << (t & 31);
|
|
2394
2577
|
}
|
|
2395
2578
|
}
|
|
2396
2579
|
// A flag test's outcome turns what held before it into the bound the flag stood for.
|
|
2397
2580
|
if (n.implies) {
|
|
2398
|
-
for (const
|
|
2399
|
-
|
|
2581
|
+
for (const entry of n.implies) {
|
|
2582
|
+
const [from, to, soft] = entry;
|
|
2583
|
+
if (soft && summarising && !approved.has(entry)) continue;
|
|
2400
2584
|
if (!(input[from >>> 5] & (1 << (from & 31)))) continue;
|
|
2401
2585
|
if (out === input) { scratch.set(input); out = scratch; }
|
|
2402
2586
|
out[to >>> 5] |= 1 << (to & 31);
|
|
@@ -2477,8 +2661,22 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2477
2661
|
const r = pending.shift();
|
|
2478
2662
|
queued.delete(r.key);
|
|
2479
2663
|
const calls = new Set();
|
|
2664
|
+
// From no facts, every fact the run holds was made inside the range, so what depends on it is sound as it stands.
|
|
2665
|
+
summarising = false;
|
|
2666
|
+
const fromNone = run(r.entry, ZERO, r.end);
|
|
2667
|
+
summarising = true;
|
|
2668
|
+
const bot = fromNone.exitOut;
|
|
2669
|
+
approved = new Set();
|
|
2670
|
+
for (const [id, bits] of fromNone.IN) {
|
|
2671
|
+
const n = nodes[id];
|
|
2672
|
+
for (const k of n.keeps || []) if (k.guards && k.guards.some((g) => has(bits, g))) approved.add(k);
|
|
2673
|
+
for (const { pairs, guards } of n.lifts || []) if (guards.some((g) => has(bits, g))) for (const pair of pairs) if (has(bits, pair[0])) approved.add(pair);
|
|
2674
|
+
for (const pair of n.spanLifts || []) if (has(bits, pair[0])) approved.add(pair);
|
|
2675
|
+
for (const d of n.derive || []) if (d.needs.every((x) => has(bits, x))) approved.add(d);
|
|
2676
|
+
for (const entry of n.implies || []) if (entry[2] && has(bits, entry[0])) approved.add(entry);
|
|
2677
|
+
}
|
|
2480
2678
|
const top = run(r.entry, ONES, r.end, calls).exitOut;
|
|
2481
|
-
|
|
2679
|
+
approved = null;
|
|
2482
2680
|
for (const k of calls) { if (!callers.has(k)) callers.set(k, new Set()); callers.get(k).add(r); }
|
|
2483
2681
|
const old = summaries.get(r.key);
|
|
2484
2682
|
const nextS = top && bot ? { top, bot } : { top: null, bot: null };
|
|
@@ -2673,7 +2871,20 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2673
2871
|
// the caller has let the parse tree go. A statement no context reaches has no facts, and credits
|
|
2674
2872
|
// nothing. One `reached` does not mark does not run, unless `partial` says the analysis stopped
|
|
2675
2873
|
// short or met a paragraph the parse did not find.
|
|
2676
|
-
|
|
2874
|
+
const spans = [];
|
|
2875
|
+
for (const mine of pairFacts.values()) for (const f of mine.values()) if (f.pair === 'span') spans.push({ a: f.a, b: f.b, k: f.k, t: f.t });
|
|
2876
|
+
return { loopBody, nodeOf, posOf, checks: checks.filter((c) => !c.pseudo), spans, words, nodes: nodes.length, facts, reached: reachedAt, extraFacts, partial };
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
// The most the facts at a point let a node's value be, as a whole number, or null for no upper end.
|
|
2880
|
+
export function topOf(checks, bits) {
|
|
2881
|
+
let cons = null;
|
|
2882
|
+
for (const c of checks || []) for (const [bit, said] of [[c.t, c.tCons], [c.f, c.fCons]]) if (hasFact(bits, bit)) cons = meet(cons, said);
|
|
2883
|
+
return cons ? wholeTop(cons) : null;
|
|
2884
|
+
}
|
|
2885
|
+
export function wholeTop(said) {
|
|
2886
|
+
const r = asRange(said);
|
|
2887
|
+
return r.hi == null ? null : r.hiInc ? Math.floor(r.hi) : Math.ceil(r.hi) - 1;
|
|
2677
2888
|
}
|
|
2678
2889
|
|
|
2679
2890
|
// For each token a condition reads, what each reading of it waits on: the operands to its left at
|
package/lib/dataflow.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { eachWithinMemory, watchMemoryBuffer } from './kernel/memory.mjs';
|
|
|
15
15
|
// A flow finding is located by source and sink rather than by one path and line, so it keeps its
|
|
16
16
|
// own comparator. The text comparison underneath it is the shared one.
|
|
17
17
|
import { byText } from './kernel/findings.mjs';
|
|
18
|
-
import { buildControl, creditOf, hasFact, within } from './control.mjs';
|
|
18
|
+
import { buildControl, creditOf, hasFact, topOf, within, wholeTop } from './control.mjs';
|
|
19
19
|
import { directoryTree } from './kernel/source-tree.mjs';
|
|
20
20
|
|
|
21
21
|
const OS_COMMAND_ROUTINE = /^(SYSTEM|C\$SYSTEM|CBL_EXEC_RUN_UNIT|CBL_GC_HOSTED|BXPSYSTM)$/i;
|
|
@@ -87,6 +87,7 @@ function literalNames(prog) {
|
|
|
87
87
|
// The value of the last NAME(value) option in a list: the last one wins, at either level.
|
|
88
88
|
function lastOption(name, list) {
|
|
89
89
|
for (let k = (list || []).length - 1; k >= 0; k--) {
|
|
90
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- name is a literal option name
|
|
90
91
|
const m = new RegExp(`^${name}\\(([A-Z]+)\\)$`).exec(list[k]);
|
|
91
92
|
if (m) return m[1];
|
|
92
93
|
}
|
|
@@ -215,11 +216,13 @@ export function analyze(root, opts = {}) {
|
|
|
215
216
|
const rel = (p) => { let r = relOf.get(p); if (r === undefined) { r = relPath(root, p); relOf.set(p, r); } return r; };
|
|
216
217
|
// `at` is the statement an edge that is not a statement's own is read at: the CALL or LINK that
|
|
217
218
|
// passes an argument, the PUT or GET of a container.
|
|
218
|
-
|
|
219
|
+
// `link` marks an edge into a callee at a call site ({ call }) or back out to it ({ ret }).
|
|
220
|
+
const edge = (a, b, why, dir, at, link) => {
|
|
219
221
|
if (!a || !b || a === b) return;
|
|
220
|
-
a.edgesOut.push(
|
|
222
|
+
a.edgesOut.push({ to: b, why, dir, ...(at != null ? { at } : {}), ...link });
|
|
221
223
|
stats.edges++;
|
|
222
224
|
};
|
|
225
|
+
let callSites = 0;
|
|
223
226
|
|
|
224
227
|
// Everything the graph needs from one program is taken while its parse tree is in hand, and the
|
|
225
228
|
// tree is then released. Holding every tree until the end is what exhausted a 4 GB heap on an
|
|
@@ -236,7 +239,9 @@ export function analyze(root, opts = {}) {
|
|
|
236
239
|
let repeats = false;
|
|
237
240
|
let table = null;
|
|
238
241
|
for (let a = item; a; a = a.parent) if ((a.occurs || 1) > 1) { repeats = true; table = a; }
|
|
239
|
-
|
|
242
|
+
let top = item;
|
|
243
|
+
while (top.parent) top = top.parent;
|
|
244
|
+
n = { id: nodes.length, pk, program: prog.id, file, name: item.name, off: item.offset ?? null, size: item.size ?? null, repeats, edgesOut: [], sources: [], sinks: [], ...(top.section === 'LINKAGE' ? { linkage: true } : {}) };
|
|
240
245
|
// Where the outermost table holding this item sits in the record: every occurrence of the
|
|
241
246
|
// item is somewhere inside it. A group's size already counts its occurrences; an
|
|
242
247
|
// elementary item's is one element.
|
|
@@ -406,6 +411,13 @@ export function analyze(root, opts = {}) {
|
|
|
406
411
|
// A whole number no sign is written on, which no value put in it can leave below 0.
|
|
407
412
|
const unsignedWhole = (it) => it.level !== 88 && !it.index && !(it.children || []).length
|
|
408
413
|
&& /^9+$/.test(String(it.picture || '').toUpperCase().replace(/(\w)\((\d+)\)/g, (_, ch, n) => ch.repeat(Number(n))));
|
|
414
|
+
// Where a length's reference starts: the start's node, a constant it is moved by, and the span
|
|
415
|
+
// facts that keep start and length together inside the item.
|
|
416
|
+
const startOf = (from, len, size) => {
|
|
417
|
+
const s = from.tok ? itemOfToken(from.tok) : null;
|
|
418
|
+
const spans = ctl && ctl.spans && s && len ? ctl.spans.filter((f) => ((f.a === s && f.b === len) || (f.a === len && f.b === s)) && f.k + from.offset <= size + 1).map((f) => f.t) : [];
|
|
419
|
+
return { node: from.tok ? nodeOfToken(from.tok) : null, offset: from.offset, size, spans };
|
|
420
|
+
};
|
|
409
421
|
const indexed = new Set();
|
|
410
422
|
const subscripting = new Map();
|
|
411
423
|
// Each subscript use of a name, with the other indices of the same reference: the loop-bound
|
|
@@ -438,8 +450,9 @@ export function analyze(root, opts = {}) {
|
|
|
438
450
|
const use = `${key}|${point ?? `s${st.at}`}|${offset}`;
|
|
439
451
|
if (indexed.has(use)) continue;
|
|
440
452
|
indexed.add(use);
|
|
453
|
+
const start = kind === 'reference-modification' && x.from && base != null ? startOf(x.from, idx, base) : null;
|
|
441
454
|
at.sinks.push({
|
|
442
|
-
kind, onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point, group: `${pk}|${key}`, limit, ...(offset ? { offset } : {}), ...(offset > 0 && idx && unsignedWhole(idx) ? { nonNeg: true } : {}), ...(ssrange ? { ssrange } : {}),
|
|
455
|
+
kind, onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point, group: `${pk}|${key}`, limit, ...(start ? { start } : {}), ...(offset ? { offset } : {}), ...(offset > 0 && idx && unsignedWhole(idx) ? { nonNeg: true } : {}), ...(ssrange ? { ssrange } : {}),
|
|
443
456
|
detail: kind === 'subscript'
|
|
444
457
|
? `${at.name} subscripts ${host.name}, a table of ${table.occurs}`
|
|
445
458
|
: `${at.name} sets the ${x.kind === 'refmod-offset' ? 'start' : 'length'} of a reference to ${host.name}, which is ${host.size} bytes`,
|
|
@@ -1032,21 +1045,23 @@ export function analyze(root, opts = {}) {
|
|
|
1032
1045
|
for (const p of programs) {
|
|
1033
1046
|
for (const t of p.transfers) {
|
|
1034
1047
|
const r = calleesOf(t.callee, p.file);
|
|
1048
|
+
const site = ++callSites;
|
|
1035
1049
|
for (const callee of r.list) {
|
|
1036
1050
|
if (!callee.commarea) continue;
|
|
1037
|
-
edge(t.argNode, callee.commarea, `EXEC CICS ${t.verb} COMMAREA to ${t.callee}${t.through ? `, the program ${t.through} names` : ''}${ofN(r, t.callee)}`, undefined, t.point);
|
|
1038
|
-
if (t.verb === 'LINK') edge(callee.commarea, t.argNode, `COMMAREA returned from ${t.callee}${ofN(r, t.callee)}
|
|
1051
|
+
edge(t.argNode, callee.commarea, `EXEC CICS ${t.verb} COMMAREA to ${t.callee}${t.through ? `, the program ${t.through} names` : ''}${ofN(r, t.callee)}`, undefined, t.point, { call: site });
|
|
1052
|
+
if (t.verb === 'LINK') edge(callee.commarea, t.argNode, `COMMAREA returned from ${t.callee}${ofN(r, t.callee)}`, undefined, undefined, { ret: site });
|
|
1039
1053
|
}
|
|
1040
1054
|
}
|
|
1041
1055
|
for (const c of p.callSites) {
|
|
1042
1056
|
const r = calleesOf(c.name, p.file);
|
|
1057
|
+
const site = ++callSites;
|
|
1043
1058
|
for (const callee of r.list) {
|
|
1044
1059
|
if (!callee.params) continue;
|
|
1045
1060
|
c.args.forEach((arg, i) => {
|
|
1046
1061
|
const param = callee.params[i];
|
|
1047
1062
|
if (!param || !arg.node || !param.node) return;
|
|
1048
|
-
edge(arg.node, param.node, `CALL '${c.display}' argument ${i + 1} at ${rel(c.file)}:${c.line}${ofN(r, c.display)}`, undefined, c.point);
|
|
1049
|
-
if (arg.mode === 'REFERENCE' && param.mode === 'REFERENCE') edge(param.node, arg.node, `CALL '${c.display}' argument ${i + 1} written back${ofN(r, c.display)}
|
|
1063
|
+
edge(arg.node, param.node, `CALL '${c.display}' argument ${i + 1} at ${rel(c.file)}:${c.line}${ofN(r, c.display)}`, undefined, c.point, { call: site });
|
|
1064
|
+
if (arg.mode === 'REFERENCE' && param.mode === 'REFERENCE') edge(param.node, arg.node, `CALL '${c.display}' argument ${i + 1} written back${ofN(r, c.display)}`, undefined, undefined, { ret: site });
|
|
1050
1065
|
});
|
|
1051
1066
|
}
|
|
1052
1067
|
const callee = r.list.length === 1 ? r.list[0] : null;
|
|
@@ -1283,6 +1298,18 @@ export function analyze(root, opts = {}) {
|
|
|
1283
1298
|
const bits = p.facts.get(point);
|
|
1284
1299
|
return bits ? creditOf(n.checks, bits, kind, limit, offset, nonNeg) : NONE;
|
|
1285
1300
|
}
|
|
1301
|
+
// A length is judged with its start: the last byte, S + L - 1, stays inside the item where the
|
|
1302
|
+
// start at its highest leaves room for the length, or where a fact bounds the two together.
|
|
1303
|
+
function spanned(c, sink, n) {
|
|
1304
|
+
if (c.level < 2 || !sink.start) return c;
|
|
1305
|
+
const p = programs[n.pk];
|
|
1306
|
+
const bits = p && p.ordered && sink.point != null ? p.facts.get(sink.point) : null;
|
|
1307
|
+
const s = sink.start;
|
|
1308
|
+
if (bits && s.spans.some((t) => hasFact(bits, t))) return c;
|
|
1309
|
+
const top = bits && s.node ? topOf(s.node.checks, bits) : null;
|
|
1310
|
+
const len = c.cons ? wholeTop(c.cons) : null;
|
|
1311
|
+
return top != null && len != null && top + s.offset + len - 1 <= s.size ? c : { level: 1, check: c.check };
|
|
1312
|
+
}
|
|
1286
1313
|
// A loop bound cannot push its counter out of the table when the loop's own condition or the
|
|
1287
1314
|
// body's checks keep every subscript the counter makes, and each index beside it, in range.
|
|
1288
1315
|
// That holds whatever the bound is, so it does not depend on the route the input took.
|
|
@@ -1312,7 +1339,7 @@ export function analyze(root, opts = {}) {
|
|
|
1312
1339
|
let point = sink.point;
|
|
1313
1340
|
for (let h = hops.length - 1; h >= 0; h--) {
|
|
1314
1341
|
const n = hops[h].node;
|
|
1315
|
-
let c = creditAt(n, point, sink.kind, sink.limit, sink.offset, sink.nonNeg);
|
|
1342
|
+
let c = spanned(creditAt(n, point, sink.kind, sink.limit, sink.offset, sink.nonNeg), sink, n);
|
|
1316
1343
|
// A count checked against a number above the table's maximum has been checked, not kept in range.
|
|
1317
1344
|
if (c.level === 2 && sink.top != null && !within(c.cons, sink.top, false)) c = { level: 1, check: c.check };
|
|
1318
1345
|
if (c.level > best.level) { best = c; by = n; }
|
|
@@ -1327,6 +1354,7 @@ export function analyze(root, opts = {}) {
|
|
|
1327
1354
|
// last moved - at a point where the credit reaches `level`.
|
|
1328
1355
|
function refuse(start, reach, credited, level, kind, shift = 0) {
|
|
1329
1356
|
walk++;
|
|
1357
|
+
stampCtx.clear();
|
|
1330
1358
|
const want = credited.filter((c) => c.level >= level && c.sink.kind === kind && (c.sink.offset || 0) === shift);
|
|
1331
1359
|
// Past the budget no route can be ruled out, so every credit in question is lost: a finding
|
|
1332
1360
|
// may be reported unchecked, never dropped.
|
|
@@ -1334,7 +1362,10 @@ export function analyze(root, opts = {}) {
|
|
|
1334
1362
|
const at = new Map();
|
|
1335
1363
|
for (const c of want) { if (!at.has(c.node.id)) at.set(c.node.id, []); at.get(c.node.id).push(c); }
|
|
1336
1364
|
// Along the way no sink's limit applies, so a bound that holds only against one blocks nothing.
|
|
1337
|
-
const holds = (since, point, k, limit = null, offset = 0, nonNeg = false) => since.some((n) =>
|
|
1365
|
+
const holds = (since, point, k, limit = null, offset = 0, nonNeg = false, sink = null) => since.some((n) => {
|
|
1366
|
+
const c = creditAt(n, point, k, limit, offset, nonNeg);
|
|
1367
|
+
return (sink ? spanned(c, sink, n) : c).level >= level;
|
|
1368
|
+
});
|
|
1338
1369
|
const lost = new Set();
|
|
1339
1370
|
const dirty = new Map();
|
|
1340
1371
|
const parts = new Map();
|
|
@@ -1343,7 +1374,7 @@ export function analyze(root, opts = {}) {
|
|
|
1343
1374
|
const began = edgesWalked;
|
|
1344
1375
|
for (let i = 0; i < open.length; i++) {
|
|
1345
1376
|
const st = open[i];
|
|
1346
|
-
for (const c of at.get(st.node.id) || []) if (!holds(st.since, c.sink.point, c.sink.kind, c.sink.limit, c.sink.offset, c.sink.nonNeg)) lost.add(c);
|
|
1377
|
+
for (const c of at.get(st.node.id) || []) if (!holds(st.since, c.sink.point, c.sink.kind, c.sink.limit, c.sink.offset, c.sink.nonNeg, c.sink)) lost.add(c);
|
|
1347
1378
|
for (const e of st.node.edgesOut) {
|
|
1348
1379
|
edgesWalked++;
|
|
1349
1380
|
const to = e.to;
|
|
@@ -1357,17 +1388,15 @@ export function analyze(root, opts = {}) {
|
|
|
1357
1388
|
// stand in for it.
|
|
1358
1389
|
const guarded = since.some((n) => n.checks || n.guard);
|
|
1359
1390
|
if (t.partial) {
|
|
1360
|
-
const key = `${to.id}|${guarded}`;
|
|
1391
|
+
const key = `${to.id}|${guarded}|${t.ctx}`;
|
|
1361
1392
|
const held = parts.get(key);
|
|
1362
1393
|
if (held && held.some((r) => (r[2] ? t.vague : !t.vague && r[0] <= t.lo && t.hi <= r[1]))) continue;
|
|
1363
1394
|
if (held) held.push([t.lo, t.hi, t.vague]); else parts.set(key, [[t.lo, t.hi, t.vague]]);
|
|
1364
1395
|
} else if (guarded) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
stampWhole[to.id] = walk;
|
|
1370
|
-
}
|
|
1396
|
+
const dk = `${to.id}|${t.ctx}`;
|
|
1397
|
+
if (stampWhole[to.id] === walk || dirty.get(dk) === walk) continue;
|
|
1398
|
+
dirty.set(dk, walk);
|
|
1399
|
+
} else if (!firstVisit(to.id, t.ctx)) continue;
|
|
1371
1400
|
open.push({ node: to, ...t, since });
|
|
1372
1401
|
}
|
|
1373
1402
|
if (edgesWalked - began > walkBudget) { stats.creditUndecided++; return new Set(want); }
|
|
@@ -1386,6 +1415,16 @@ export function analyze(root, opts = {}) {
|
|
|
1386
1415
|
// Visit stamps instead of a per-walk Set: one walk per source, and a Set of string keys per walk
|
|
1387
1416
|
// was most of the time spent on large repositories.
|
|
1388
1417
|
const stampWhole = new Int32Array(nodes.length);
|
|
1418
|
+
// A whole node reached with a call context, per walk. Reached with none, it stands in for every context.
|
|
1419
|
+
const stampCtx = new Map();
|
|
1420
|
+
const firstVisit = (id, ctx) => {
|
|
1421
|
+
if (stampWhole[id] === walk) return false;
|
|
1422
|
+
if (!ctx) { stampWhole[id] = walk; return true; }
|
|
1423
|
+
const key = `${id}|${ctx}`;
|
|
1424
|
+
if (stampCtx.get(key) === walk) return false;
|
|
1425
|
+
stampCtx.set(key, walk);
|
|
1426
|
+
return true;
|
|
1427
|
+
};
|
|
1389
1428
|
let walk = 0;
|
|
1390
1429
|
// Every walk is bounded, and so is the analysis. One source of an estate whose programs share
|
|
1391
1430
|
// their records through thousands of copybooks reached millions of nodes, and 22,905 such walks
|
|
@@ -1402,9 +1441,25 @@ export function analyze(root, opts = {}) {
|
|
|
1402
1441
|
// it does not. Both walks below cross edges through this, so neither can take a route the other
|
|
1403
1442
|
// would refuse.
|
|
1404
1443
|
const known = (n) => n.off != null && n.size != null && !n.repeats;
|
|
1444
|
+
// The call sites a value entered its callees through, innermost last, as a string; empty where any
|
|
1445
|
+
// caller may be returned to: a value born in the callee, or one that rested in storage a program
|
|
1446
|
+
// keeps between calls, which a later call from any caller can read.
|
|
1447
|
+
const CONTEXT_DEPTH = 4;
|
|
1448
|
+
function contextOver(state, e) {
|
|
1449
|
+
const ctx = state.ctx || '';
|
|
1450
|
+
if (e.ret != null) {
|
|
1451
|
+
if (!ctx) return '';
|
|
1452
|
+
const sites = ctx.split('/');
|
|
1453
|
+
return Number(sites[sites.length - 1]) === e.ret ? sites.slice(0, -1).join('/') : null;
|
|
1454
|
+
}
|
|
1455
|
+
if (e.call != null) return [...(ctx ? ctx.split('/') : []), String(e.call)].slice(-CONTEXT_DEPTH).join('/');
|
|
1456
|
+
return e.to.linkage ? ctx : '';
|
|
1457
|
+
}
|
|
1405
1458
|
function cross(state, e) {
|
|
1406
1459
|
const cur = state.node;
|
|
1407
1460
|
const to = e.to;
|
|
1461
|
+
const ctx = contextOver(state, e);
|
|
1462
|
+
if (ctx === null) return null;
|
|
1408
1463
|
let partial = false, lo = 0, hi = 0, vague = false;
|
|
1409
1464
|
if (opts.legacyGroupEdges) { /* every edge carries the whole item */ }
|
|
1410
1465
|
else if (e.dir === 'up') {
|
|
@@ -1441,7 +1496,7 @@ export function analyze(root, opts = {}) {
|
|
|
1441
1496
|
if (!vague && to.size != null) { hi = Math.min(hi, to.size); if (hi <= lo) return null; }
|
|
1442
1497
|
}
|
|
1443
1498
|
}
|
|
1444
|
-
return { partial, lo, hi, vague };
|
|
1499
|
+
return { partial, lo, hi, vague, ctx };
|
|
1445
1500
|
}
|
|
1446
1501
|
for (const start of nodes) {
|
|
1447
1502
|
if (!start.sources.length || !canReach[start.id]) continue;
|
|
@@ -1450,6 +1505,7 @@ export function analyze(root, opts = {}) {
|
|
|
1450
1505
|
if (!(canReach[start.id] & reach)) continue;
|
|
1451
1506
|
if (edgesWalked >= totalBudget) { stats.sourcesNotWalked++; continue; }
|
|
1452
1507
|
walk++;
|
|
1508
|
+
stampCtx.clear();
|
|
1453
1509
|
const credited = [];
|
|
1454
1510
|
// A state is a node plus which of its bytes are tainted. Going up from a child taints only
|
|
1455
1511
|
// that child's bytes of the group; going down reaches a child only if those bytes overlap it,
|
|
@@ -1516,16 +1572,14 @@ export function analyze(root, opts = {}) {
|
|
|
1516
1572
|
const to = e.to;
|
|
1517
1573
|
const crossed = cross(state, e);
|
|
1518
1574
|
if (!crossed) continue;
|
|
1519
|
-
const { partial, lo, hi, vague } = crossed;
|
|
1575
|
+
const { partial, lo, hi, vague, ctx } = crossed;
|
|
1520
1576
|
if (partial) {
|
|
1521
|
-
const
|
|
1577
|
+
const pk = `${to.id}|${ctx}`;
|
|
1578
|
+
const held = partSeen.get(pk);
|
|
1522
1579
|
if (held && held.some(r => (r[2] ? vague : !vague && r[0] <= lo && hi <= r[1]))) continue;
|
|
1523
|
-
if (held) held.push([lo, hi, vague]); else partSeen.set(
|
|
1524
|
-
} else
|
|
1525
|
-
|
|
1526
|
-
stampWhole[to.id] = walk;
|
|
1527
|
-
}
|
|
1528
|
-
queue.push({ node: to, partial, lo, hi, vague, prev: state, why: e.why, dir: e.dir, point: pointOfEdge(e), moves: moves(e) });
|
|
1580
|
+
if (held) held.push([lo, hi, vague]); else partSeen.set(pk, [[lo, hi, vague]]);
|
|
1581
|
+
} else if (!firstVisit(to.id, ctx)) continue;
|
|
1582
|
+
queue.push({ node: to, partial, lo, hi, vague, ctx, prev: state, why: e.why, dir: e.dir, point: pointOfEdge(e), moves: moves(e) });
|
|
1529
1583
|
}
|
|
1530
1584
|
}
|
|
1531
1585
|
if (cut) stats.walksCut++;
|
package/lib/explain.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { resolve, relative, sep, dirname } from 'node:path';
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
4
|
import { readSource, isProgram, isCopybook, isJcl } from './sources.mjs';
|
|
5
|
-
import { parseFile, buildFileIndex, detectFormat } from './parser.mjs';
|
|
5
|
+
import { parseFile, buildFileIndex, detectFormat, normalize, tokenize, segmentEnd } from './parser.mjs';
|
|
6
6
|
import { EVIDENCE } from './kernel/findings.mjs';
|
|
7
7
|
import { REGISTRY } from './kernel/registry.mjs';
|
|
8
8
|
import { WHO_ACTS } from './tui/model.mjs';
|
|
@@ -34,6 +34,21 @@ function contained(root, rel) {
|
|
|
34
34
|
return { abs };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// The last line of the COBOL statement that starts on `line`, its period or END- terminator included,
|
|
38
|
+
// so a drafter shown one line of a statement knows where the statement ends. Past MAX_EXTENT lines
|
|
39
|
+
// nothing is claimed.
|
|
40
|
+
const MAX_EXTENT = 30;
|
|
41
|
+
function statementEnd(file, line) {
|
|
42
|
+
file.tokens ??= (() => { try { return tokenize(normalize(file.text, file.format), '').tokens; } catch { return []; } })();
|
|
43
|
+
const t = file.tokens;
|
|
44
|
+
const i = t.findIndex((x) => x.line === line);
|
|
45
|
+
if (i < 0 || t[i].t !== 'word') return line;
|
|
46
|
+
const e = segmentEnd(t, i, t.length);
|
|
47
|
+
const next = t[e];
|
|
48
|
+
const end = next && next.line >= line && (next.t === 'period' || (next.t === 'word' && next.u === `END-${t[i].u}`)) ? next.line : t[e - 1].line;
|
|
49
|
+
return end > line && end - line < MAX_EXTENT ? end : line;
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
// A hop's location: from `via` ("MOVE at pos/P1.cbl:9"), else the related entry for its file.
|
|
38
53
|
function hopAt(hop, finding) {
|
|
39
54
|
const m = /\bat (.+):(\d+)$/.exec(hop.via || '');
|
|
@@ -62,7 +77,7 @@ export function explainFinding(report, fingerprint, { root }) {
|
|
|
62
77
|
else {
|
|
63
78
|
try {
|
|
64
79
|
const text = readSource(c.abs).text;
|
|
65
|
-
file = { kind: kindOf(c.abs), format: detectFormat(text), lines: text.split(/\r?\n/) };
|
|
80
|
+
file = { kind: kindOf(c.abs), format: detectFormat(text), text, lines: text.split(/\r?\n/) };
|
|
66
81
|
} catch (e) { unread.push({ path, why: `it could not be read: ${e.message}` }); }
|
|
67
82
|
}
|
|
68
83
|
files.set(path, file);
|
|
@@ -72,15 +87,26 @@ export function explainFinding(report, fingerprint, { root }) {
|
|
|
72
87
|
for (const x of [...report.findings, ...(report.suppressed || [])]) {
|
|
73
88
|
if (HIDDEN_RULES.has(x.rule) && x.path && x.line) flagged.set(`${x.path}:${x.line}`, x.rule);
|
|
74
89
|
}
|
|
75
|
-
const
|
|
90
|
+
const quoteLine = (path, file, line) => {
|
|
76
91
|
const withheld = flagged.get(`${path}:${line}`);
|
|
77
92
|
if (withheld) return { code: null, withheld };
|
|
78
|
-
const file = fileOf(path);
|
|
79
|
-
if (!file || !(line > 0 && line <= file.lines.length)) return { code: null };
|
|
80
93
|
const area = codeArea(file.lines[line - 1], file.kind, file.format);
|
|
81
94
|
const dropped = area.rest.trim() !== '' || /[^\d\s]/.test(area.seq);
|
|
82
95
|
return { code: area.text.trimEnd(), ...(dropped ? { dropped } : {}) };
|
|
83
96
|
};
|
|
97
|
+
// A statement that runs on past its first line carries endLine and the lines after the first.
|
|
98
|
+
const quote = (path, line) => {
|
|
99
|
+
const withheld = flagged.get(`${path}:${line}`);
|
|
100
|
+
if (withheld) return { code: null, withheld };
|
|
101
|
+
const file = fileOf(path);
|
|
102
|
+
if (!file || !(line > 0 && line <= file.lines.length)) return { code: null };
|
|
103
|
+
const first = quoteLine(path, file, line);
|
|
104
|
+
const endLine = file.kind === 'cobol' ? statementEnd(file, line) : line;
|
|
105
|
+
if (endLine === line) return first;
|
|
106
|
+
const rest = [];
|
|
107
|
+
for (let n = line + 1; n <= endLine; n++) rest.push({ line: n, ...quoteLine(path, file, n) });
|
|
108
|
+
return { ...first, endLine, rest };
|
|
109
|
+
};
|
|
84
110
|
|
|
85
111
|
const hops = (f.trace || []).map((h, i) => {
|
|
86
112
|
if (h.elided) return { n: i + 1, elided: h.elided, via: h.via };
|