@portll/cobolwork 0.2.140 → 0.2.150
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 +13 -1
- package/THIRD-PARTY-NOTICES.md +3 -2
- package/bin/cobolwork.mjs +16 -1
- package/lib/baseline.mjs +8 -6
- package/lib/bms.mjs +1 -1
- package/lib/capabilities.mjs +2 -1
- package/lib/cards.mjs +66 -0
- package/lib/compliance.mjs +6 -5
- package/lib/control.mjs +177 -5
- package/lib/dataflow.mjs +5 -4
- package/lib/diff.mjs +65 -89
- package/lib/equivalence.mjs +30 -12
- package/lib/evidence/record.mjs +3 -1
- package/lib/evidence/slsa.mjs +17 -2
- package/lib/execution.mjs +77 -0
- package/lib/explain.mjs +1 -1
- package/lib/ftp.mjs +1 -1
- package/lib/inventory.mjs +8 -9
- package/lib/jcl.mjs +3 -48
- package/lib/kernel/git.mjs +60 -0
- package/lib/kernel/identity.mjs +7 -6
- package/lib/kernel/source-tree.mjs +204 -34
- package/lib/parser.mjs +6 -4
- package/lib/revision.json +1 -1
- package/lib/sbom.mjs +33 -9
- package/lib/scan.mjs +11 -4
- package/lib/sets/build.mjs +1 -1
- package/lib/sets/cics.mjs +27 -3
- package/lib/sets/copybook.mjs +1 -1
- package/lib/sets/hidden.mjs +1 -1
- package/lib/sets/jcl.mjs +1 -1
- package/lib/sets/priv.mjs +1 -1
- package/lib/sets/recon.mjs +2 -2
- package/lib/sets/semantics.mjs +1 -1
- package/lib/sets/vendor.mjs +1 -1
- package/lib/site.mjs +17 -7
- package/lib/sources.mjs +82 -38
- package/lib/utilities.mjs +1 -1
- package/package.json +1 -1
- package/rules/compliance-cobit2019.json +2438 -0
- package/lib/card.mjs +0 -18
package/README.md
CHANGED
|
@@ -187,6 +187,11 @@ since a test that missed the route does not show it safe.
|
|
|
187
187
|
`summary.handling`, because it is then a list of what to attack first. Upload it only where the RACF
|
|
188
188
|
facts behind it may go. [docs/spec/reach.md](docs/spec/reach.md) §9 is the full specification.
|
|
189
189
|
|
|
190
|
+
`COBOLWORK_EXECUTION` names reports `ironwork run --coverage` wrote from runs of the estate's own
|
|
191
|
+
tests. A finding inside a paragraph they cover carries `executed`, the paragraph and how often the
|
|
192
|
+
runs entered it, and `summary.byExecution` counts the findings in paragraphs entered and never
|
|
193
|
+
entered ([docs/spec/evidence.md](docs/spec/evidence.md) §13.5).
|
|
194
|
+
|
|
190
195
|
### What each finding lets someone do, and the fix
|
|
191
196
|
|
|
192
197
|
A defect finding carries two more facts, the same for every finding of its rule: what someone can do
|
|
@@ -339,13 +344,20 @@ the guard off, it just stops the host's load from deciding. A spawned scan inher
|
|
|
339
344
|
|
|
340
345
|
## Compliance
|
|
341
346
|
|
|
342
|
-
Every rule is mapped to the clause of each framework that makes it an obligation, quoted verbatim
|
|
347
|
+
Every rule is mapped to the clause of each framework that makes it an obligation, quoted verbatim,
|
|
348
|
+
and to a COBIT 2019 practice by identifier alone:
|
|
343
349
|
|
|
344
350
|
| File | Instrument | Rules mapped |
|
|
345
351
|
|---|---|---|
|
|
346
352
|
| `rules/compliance-dora.json` | Regulation (EU) 2022/2554 (DORA) | 203 |
|
|
347
353
|
| `rules/compliance-ffiec.json` | FFIEC IT Examination Handbook | 201, and 2 recorded as unmapped |
|
|
348
354
|
| `rules/compliance-nist80053.json` | NIST SP 800-53 Rev. 5.2.0 | 201, and 2 recorded as unmapped |
|
|
355
|
+
| `rules/compliance-cobit2019.json` | COBIT 2019 (ISACA), identifiers only | 201, and 2 recorded as unmapped |
|
|
356
|
+
|
|
357
|
+
A COBIT 2019 row names the objective or practice and gives this project's own rationale; no ISACA
|
|
358
|
+
text is reproduced, so reading what a practice says needs a copy of the framework. The practice is
|
|
359
|
+
chosen at the NIST control, through a crosswalk in `diag/map-compliance.mjs`, so it cannot drift
|
|
360
|
+
from the rule's NIST clause.
|
|
349
361
|
|
|
350
362
|
`node diag/map-compliance.mjs` refuses to write a quote the cached instrument does not contain, and
|
|
351
363
|
every scan carries the mapping as `ruleCompliance`. The clause choice is a judgement that no
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -89,8 +89,9 @@ its source URL and retrieval date:
|
|
|
89
89
|
- **NIST SP 800-53 Rev. 5** — a work of the US Government.
|
|
90
90
|
- **FFIEC IT Examination Handbook** — a work of the US Government.
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
COBIT 2019 appears as objective and practice identifiers only, with this project's own rationale;
|
|
93
|
+
no text of COBIT, which is ISACA's, is reproduced. PCI DSS is deliberately absent: it may not be
|
|
94
|
+
redistributed. Full instrument texts are never
|
|
94
95
|
committed (`feed/sources/` is ignored); the feed ships citations, not text.
|
|
95
96
|
|
|
96
97
|
## Vulnerability data
|
package/bin/cobolwork.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import { build, buildSarif, buildSummaryLine } from '../lib/build.mjs';
|
|
|
16
16
|
import { capabilities } from '../lib/capabilities.mjs';
|
|
17
17
|
import { commitAt, revisionOf, toolRevision } from '../lib/revision.mjs';
|
|
18
18
|
import { stampFingerprints } from '../lib/kernel/identity.mjs';
|
|
19
|
+
import { pdsExportTree } from '../lib/kernel/source-tree.mjs';
|
|
20
|
+
import { SITE_FILE } from '../lib/site.mjs';
|
|
19
21
|
import { tally } from '../lib/kernel/findings.mjs';
|
|
20
22
|
import { loadBaseline, applyBaseline, baselineEntries, BASELINE_FILE, SUPPRESSING } from '../lib/baseline.mjs';
|
|
21
23
|
import { readReport } from '../lib/tui/model.mjs';
|
|
@@ -86,6 +88,9 @@ Options
|
|
|
86
88
|
baseline: the judgement each new entry records, and which rules it covers
|
|
87
89
|
--advisories <file>[,<file>] a customer's own advisory extract (JSON), loaded for this scan only;
|
|
88
90
|
never kept in the tree
|
|
91
|
+
--pds-export scan: the directory holds partitioned data sets' members as files, as
|
|
92
|
+
zowe zos-files download all-members writes them (hlq/llq/member.txt) or in a
|
|
93
|
+
directory named for each data set; findings name DATA.SET/MEMBER
|
|
89
94
|
--copylib <dir>[,<dir>] copy libraries the estate keeps outside the repository, searched after the
|
|
90
95
|
tree's own copybooks, as COBCPY is; a copybook found there is read, not reported missing
|
|
91
96
|
--report <file> tui, explain: read a stored scan report instead of scanning
|
|
@@ -150,6 +155,7 @@ function parseArgs(argv) {
|
|
|
150
155
|
else if (a === '--action') opts.action = value();
|
|
151
156
|
else if (a === '--rule') opts.rule = list();
|
|
152
157
|
else if (a === '--advisories') opts.advisoryFeeds = [...new Set(list().map(x => resolve(x)))];
|
|
158
|
+
else if (a === '--pds-export') opts.pdsExport = true;
|
|
153
159
|
else if (a === '--copylib') opts.copylib = [...new Set(list().map(x => resolve(x)))];
|
|
154
160
|
else if (a === '--report') opts.report = value();
|
|
155
161
|
else if (a === '--keys') opts.keys = value();
|
|
@@ -198,6 +204,7 @@ function warnCoverage(report) {
|
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
const isLink = (p) => { try { return lstatSync(p).isSymbolicLink(); } catch { return false; } };
|
|
207
|
+
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
201
208
|
|
|
202
209
|
let journal = null;
|
|
203
210
|
|
|
@@ -262,6 +269,8 @@ if (opts.copylib && opts._.length && !COPYLIB_COMMANDS.includes(opts._[0])) {
|
|
|
262
269
|
const notDirs = (opts.copylib || []).filter((d) => { try { return !statSync(d).isDirectory(); } catch { return true; } });
|
|
263
270
|
if (notDirs.length) { process.stderr.write(`cobolwork: --copylib ${notDirs.join(', ')} is not a directory\n`); process.exit(2); }
|
|
264
271
|
const systemDirs = opts.copylib || [];
|
|
272
|
+
if (opts.pdsExport && opts._.length && opts._[0] !== 'scan') { process.stderr.write('cobolwork: --pds-export is for scan only\n'); process.exit(2); }
|
|
273
|
+
if (opts.pdsExport && opts.repos) { process.stderr.write('cobolwork: --pds-export reads one export; --repos does not apply\n'); process.exit(2); }
|
|
265
274
|
if (opts.json && opts._.length && opts._[0] !== 'capabilities') {
|
|
266
275
|
process.stderr.write(`cobolwork: --json is for capabilities; every other command writes JSON unless --format says otherwise\n`);
|
|
267
276
|
process.exit(2);
|
|
@@ -332,7 +341,13 @@ try {
|
|
|
332
341
|
process.exitCode = evidenceCommand(target, opts, { toolVersion: VERSION, write: (s) => process.stdout.write(s) });
|
|
333
342
|
} else if (command === 'scan' || command === 'flow') {
|
|
334
343
|
const flowOpts = { repos, fullTrace: opts.fullTrace === true, systemDirs };
|
|
335
|
-
|
|
344
|
+
// The site file sits beside the members and is not one of them, so it is named rather than found.
|
|
345
|
+
const site = resolve(root, SITE_FILE);
|
|
346
|
+
const pds = opts.pdsExport ? pdsExportTree(root, { systemDirs }) : null;
|
|
347
|
+
const report = command === 'flow' ? scanFlow(root, flowOpts)
|
|
348
|
+
: pds ? scanAll(pds.root, { ...flowOpts, only: opts.only, advisoryFeeds: opts.advisoryFeeds, tree: pds, feedRoot: root, site: isFile(site) && !isLink(site) ? site : null })
|
|
349
|
+
: scanAll(root, { ...flowOpts, only: opts.only, advisoryFeeds: opts.advisoryFeeds });
|
|
350
|
+
if (pds) report.summary.pdsExport = pds.export;
|
|
336
351
|
if (command === 'flow') {
|
|
337
352
|
report.summary.identity = stampFingerprints(report.findings, { root });
|
|
338
353
|
stampFingerprints(report.checked, { root });
|
package/lib/baseline.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// because the finding it would hide is evidence that the tree is not to be trusted. A baseline
|
|
17
17
|
// passed from outside the tree can.
|
|
18
18
|
import { readFileSync, existsSync } from 'node:fs';
|
|
19
|
-
import { join } from 'node:path';
|
|
19
|
+
import { join, resolve } from 'node:path';
|
|
20
20
|
import { tally } from './kernel/findings.mjs';
|
|
21
21
|
import { resolvesInside } from './kernel/source-tree.mjs';
|
|
22
22
|
import { printable } from './kernel/printable.mjs';
|
|
@@ -54,22 +54,24 @@ export function validateEntry(e) {
|
|
|
54
54
|
|
|
55
55
|
// { path, source, entries, problems }. An absent file is an empty baseline, not an error; an
|
|
56
56
|
// unreadable one is a problem the report carries, never a silent empty.
|
|
57
|
-
|
|
57
|
+
// A source tree held in memory (a git revision) supplies its own baseline file.
|
|
58
|
+
export function loadBaseline(root, { explicit = null, use = true, mayBeAbsent = false, tree = null } = {}) {
|
|
58
59
|
if (!use) return null;
|
|
59
|
-
const
|
|
60
|
-
|
|
60
|
+
const held = !explicit && tree && tree.kind !== 'directory';
|
|
61
|
+
const path = explicit || (held ? resolve(tree.root, BASELINE_FILE) : join(root, BASELINE_FILE));
|
|
62
|
+
if (held ? !tree.contains(path) : !existsSync(path)) {
|
|
61
63
|
if (!explicit) return null;
|
|
62
64
|
if (mayBeAbsent) return { path, source: 'explicit', entries: [], problems: [] };
|
|
63
65
|
return { path, source: 'explicit', entries: [], problems: [`${path} does not exist, so nothing was suppressed`] };
|
|
64
66
|
}
|
|
65
67
|
const out = { path, source: explicit ? 'explicit' : 'tree', entries: [], problems: [] };
|
|
66
68
|
// The tree's own baseline is the tree's, and a link out of it is not followed.
|
|
67
|
-
if (!explicit && !resolvesInside(root, path)) {
|
|
69
|
+
if (!explicit && !held && !resolvesInside(root, path)) {
|
|
68
70
|
out.problems.push(`${BASELINE_FILE} is a link that leads outside the tree, so nothing was suppressed`);
|
|
69
71
|
return out;
|
|
70
72
|
}
|
|
71
73
|
let raw;
|
|
72
|
-
try { raw = JSON.parse(readFileSync(path, 'utf8')); } catch (err) {
|
|
74
|
+
try { raw = JSON.parse(held ? tree.bytes(path).toString('utf8') : readFileSync(path, 'utf8')); } catch (err) {
|
|
73
75
|
out.problems.push(`${path} is not readable as JSON: ${printable(err.message, 120)}`);
|
|
74
76
|
return out;
|
|
75
77
|
}
|
package/lib/bms.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// This reads text and opens nothing; the caller reads the file through the source tree, so
|
|
17
17
|
// containment stays where it is enforced.
|
|
18
|
-
import { parseOperands, splitOperands } from './
|
|
18
|
+
import { parseOperands, splitOperands } from './cards.mjs';
|
|
19
19
|
|
|
20
20
|
// Column 72 marks a continuation and 73 to 80 are the sequence field, so a statement ends at 71.
|
|
21
21
|
const END_COLUMN = 71;
|
package/lib/capabilities.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export const CAPABILITIES_SCHEMA_VERSION = 1;
|
|
|
17
17
|
|
|
18
18
|
// Each command, what it takes and what it writes.
|
|
19
19
|
export const COMMANDS = {
|
|
20
|
-
scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--baseline', '--no-baseline', '--advisories', '--copylib', '--evidence'], documents: ['cobolwork', 'sarif'] },
|
|
20
|
+
scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--baseline', '--no-baseline', '--advisories', '--copylib', '--pds-export', '--evidence'], documents: ['cobolwork', 'sarif'] },
|
|
21
21
|
flow: { args: ['<path>'], options: ['--out', '--repos', '--quiet', '--full-trace', '--baseline', '--no-baseline', '--copylib', '--evidence'], documents: ['cobolwork-flow'] },
|
|
22
22
|
inventory: { args: ['<path>'], options: ['--out', '--quiet', '--copylib', '--evidence'], documents: ['cobolwork-inventory'] },
|
|
23
23
|
parse: { args: ['<file>'], options: ['--out'], documents: ['cobolwork-parse'] },
|
|
@@ -68,6 +68,7 @@ export function capabilities() {
|
|
|
68
68
|
jcl: JCL_EXT,
|
|
69
69
|
bms: BMS_EXT,
|
|
70
70
|
extensionless: 'sniffed by content: a file with no extension is classified by what it holds',
|
|
71
|
+
pdsExport: 'scan --pds-export: every member classified by content, whatever extension the download gave it; text, --binary and --record downloads',
|
|
71
72
|
zowe: ['zowe.config.json', 'zowe.config.user.json', '.mcp.json', '.vscode/mcp.json', '.cursor/mcp.json', '.claude/settings.json', '.claude/settings.local.json', '.gemini/settings.json', 'claude_desktop_config.json', '.vscode/settings.json', 'systems.json under a mock directory'],
|
|
72
73
|
},
|
|
73
74
|
evidenceKinds: Object.keys(EVIDENCE),
|
package/lib/cards.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Card-image source: where a line's fields sit on the card, and how an operand field splits. COBOL
|
|
3
|
+
// in fixed or variable reference format has a sequence area (columns 1-6), an indicator (7), program
|
|
4
|
+
// text (8-72, or 8-250 in variable format) and what follows, which the compiler ignores. A JCL or
|
|
5
|
+
// utility control statement is columns 1-72, with its sequence number after. Free-format COBOL is
|
|
6
|
+
// text from end to end.
|
|
7
|
+
|
|
8
|
+
export const TEXT_END = { fixed: 72, variable: 250 };
|
|
9
|
+
export const STATEMENT_END = 72;
|
|
10
|
+
|
|
11
|
+
export function cobolCard(line, format) {
|
|
12
|
+
const end = TEXT_END[format];
|
|
13
|
+
if (!end) return { sequence: '', indicator: '', text: line, ignored: '' };
|
|
14
|
+
return { sequence: line.slice(0, 6), indicator: line[6] || ' ', text: line.slice(7, end), ignored: line.slice(end) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function statementCard(line) {
|
|
18
|
+
return { text: line.slice(0, STATEMENT_END), sequence: line.slice(STATEMENT_END) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Splits an operand field on commas that are not inside parentheses or quotes. JCL, BMS and HLASM nest
|
|
22
|
+
// both, and a naive split on comma turns DISP=(NEW,CATLG,DELETE) into three operands.
|
|
23
|
+
export function splitOperands(s) {
|
|
24
|
+
const out = [];
|
|
25
|
+
let depth = 0, quoted = false, start = 0;
|
|
26
|
+
for (let i = 0; i < s.length; i++) {
|
|
27
|
+
const c = s[i];
|
|
28
|
+
if (quoted) { if (c === "'") { if (s[i + 1] === "'") i++; else quoted = false; } continue; }
|
|
29
|
+
if (c === "'") quoted = true;
|
|
30
|
+
else if (c === '(') depth++;
|
|
31
|
+
else if (c === ')') depth = Math.max(0, depth - 1);
|
|
32
|
+
else if (c === ',' && depth === 0) { out.push(s.slice(start, i)); start = i + 1; }
|
|
33
|
+
}
|
|
34
|
+
out.push(s.slice(start));
|
|
35
|
+
return out.filter((x, i, a) => x.length || i < a.length - 1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Operands are positional until the first KEY=VALUE, and keyword after it: JCL's EXEC takes its
|
|
39
|
+
// procedure name positionally, and everything interesting on a DD is a keyword.
|
|
40
|
+
export function parseOperands(field) {
|
|
41
|
+
const positional = [];
|
|
42
|
+
const keywords = new Map();
|
|
43
|
+
for (const raw of splitOperands(field)) {
|
|
44
|
+
const part = raw.trim();
|
|
45
|
+
if (!part) continue;
|
|
46
|
+
const eq = keywordSplit(part);
|
|
47
|
+
if (eq < 0) positional.push(part);
|
|
48
|
+
else keywords.set(part.slice(0, eq).toUpperCase(), part.slice(eq + 1));
|
|
49
|
+
}
|
|
50
|
+
return { positional, keywords };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The first '=' that is not inside parentheses or quotes. DCB=(RECFM=FB,LRECL=80) is one keyword
|
|
54
|
+
// whose value happens to contain more of them.
|
|
55
|
+
function keywordSplit(part) {
|
|
56
|
+
let depth = 0, quoted = false;
|
|
57
|
+
for (let i = 0; i < part.length; i++) {
|
|
58
|
+
const c = part[i];
|
|
59
|
+
if (quoted) { if (c === "'") { if (part[i + 1] === "'") i++; else quoted = false; } continue; }
|
|
60
|
+
if (c === "'") quoted = true;
|
|
61
|
+
else if (c === '(') depth++;
|
|
62
|
+
else if (c === ')') depth = Math.max(0, depth - 1);
|
|
63
|
+
else if (c === '=' && depth === 0) return i;
|
|
64
|
+
}
|
|
65
|
+
return -1;
|
|
66
|
+
}
|
package/lib/compliance.mjs
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// against each. The difference between them is this file, and it is the difference between a
|
|
6
6
|
// tooling budget and a compliance budget.
|
|
7
7
|
//
|
|
8
|
-
// DORA, NIST SP 800-53 and the FFIEC IT Examination Handbook ship
|
|
9
|
-
// may be reproduced: one is EU law, the other two US Government works.
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// DORA, NIST SP 800-53 and the FFIEC IT Examination Handbook ship with quotes because their
|
|
9
|
+
// instruments may be reproduced: one is EU law, the other two US Government works. COBIT 2019 ships
|
|
10
|
+
// as identifiers with this project's own rationale and no ISACA text. PCI DSS may not be
|
|
11
|
+
// redistributed and is not mapped.
|
|
12
12
|
//
|
|
13
13
|
// A framework may decline to cover a rule. Neither NIST nor FFIEC has a control that genuinely
|
|
14
14
|
// covers committing an LPAR name to a repository, and that is recorded as an unmapped rule with a
|
|
@@ -19,6 +19,7 @@ const FRAMEWORKS = [
|
|
|
19
19
|
{ id: 'dora', file: '../rules/compliance-dora.json' },
|
|
20
20
|
{ id: 'nist-800-53r5', file: '../rules/compliance-nist80053.json' },
|
|
21
21
|
{ id: 'ffiec', file: '../rules/compliance-ffiec.json' },
|
|
22
|
+
{ id: 'cobit-2019', file: '../rules/compliance-cobit2019.json' },
|
|
22
23
|
];
|
|
23
24
|
|
|
24
25
|
// A framework that will not parse is a framework this scan cannot map against, which is the same
|
|
@@ -64,7 +65,7 @@ const byRule = new Map();
|
|
|
64
65
|
for (const d of loaded) {
|
|
65
66
|
for (const r of d.rows) {
|
|
66
67
|
if (!byRule.has(r.ruleId)) byRule.set(r.ruleId, []);
|
|
67
|
-
byRule.get(r.ruleId).push({ framework: r.framework, clause: r.clause, title: r.title });
|
|
68
|
+
byRule.get(r.ruleId).push({ framework: r.framework, clause: r.clause, ...(r.title ? { title: r.title } : {}) });
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
package/lib/control.mjs
CHANGED
|
@@ -251,6 +251,12 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
251
251
|
const loopBody = new Map();
|
|
252
252
|
const stmtAt = new Map();
|
|
253
253
|
for (const st of prog.statements) if (st.at != null && st.verb !== 'WHEN') stmtAt.set(st.at, st);
|
|
254
|
+
const unpassedBy = new Map();
|
|
255
|
+
for (const c of prog.calls || []) {
|
|
256
|
+
const st = prog.statements[c.stmtIndex];
|
|
257
|
+
if (st) unpassedBy.set(st, new Set([c.targetTok, ...c.using.filter((a) => a.mode !== 'REFERENCE' && a.tok).map((a) => a.tok)]));
|
|
258
|
+
}
|
|
259
|
+
const external = (it) => { for (let a = it; a; a = a.parent) if (a.external) return true; return false; };
|
|
254
260
|
const labelAt = new Map();
|
|
255
261
|
for (const l of prog.labels) if (l.at != null) labelAt.set(l.at, l);
|
|
256
262
|
const labelNames = new Set(prog.labels.map((l) => l.name));
|
|
@@ -640,11 +646,14 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
640
646
|
for (const t of n.st.targets || []) add(t);
|
|
641
647
|
if (n.st.verb === 'CALL' || n.st.verb === 'READ' || n.st.verb === 'RETURN') {
|
|
642
648
|
// A callee may write what it is passed by reference, and a READ fills the file's records.
|
|
649
|
+
// The field naming the program called, and what goes BY CONTENT or BY VALUE, is not passed
|
|
650
|
+
// for the callee to write - unless it is EXTERNAL, which any program can.
|
|
651
|
+
const unpassed = n.st.verb === 'CALL' ? unpassedBy.get(n.st) : null;
|
|
643
652
|
for (const t of n.st.sources || []) {
|
|
644
653
|
const f = resolve(t);
|
|
645
654
|
if (!f) continue;
|
|
646
655
|
if (f.records) for (const r of f.records) out.push(r);
|
|
647
|
-
else if (n.st.verb === 'CALL') out.push(f);
|
|
656
|
+
else if (n.st.verb === 'CALL' && !(unpassed && unpassed.has(t) && !external(f))) out.push(f);
|
|
648
657
|
}
|
|
649
658
|
}
|
|
650
659
|
}
|
|
@@ -1083,7 +1092,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1083
1092
|
const bounds = { lo: 1, hi };
|
|
1084
1093
|
const c = { id: checks.length, key, field, file: list[0].n.st.file, line: list[0].n.st.line, tCons: { numeric: true, lo: 1, loInc: true, hi, hiInc: true }, fCons: null, restricts: false, assigned: true, bounds };
|
|
1085
1094
|
checks.push(c);
|
|
1086
|
-
|
|
1095
|
+
// "At most v" alone, which a decrement that cannot go below 0 leaves standing.
|
|
1096
|
+
const top = { id: checks.length, key, field, file: c.file, line: c.line, tCons: { numeric: true, hi, hiInc: true }, fCons: null, restricts: false, assigned: true, bounds: { hi } };
|
|
1097
|
+
checks.push(top);
|
|
1098
|
+
for (const a of list) if (Number(a.value) <= hi) (a.upTo ||= []).push(c, top);
|
|
1087
1099
|
}
|
|
1088
1100
|
}
|
|
1089
1101
|
// Fact numbers: for each check, `x` that it ran (only a restricting one), `t` and `f` that it came
|
|
@@ -1419,6 +1431,40 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1419
1431
|
(a.n.genStmt ||= []).push(newLo(a.field, v, a.n.st).t);
|
|
1420
1432
|
}
|
|
1421
1433
|
// An increment by at least m turns "at least k" into "at least k + m", where the sum fits the field.
|
|
1434
|
+
// The fields a statement only ever lowers, by a constant, and by how much at most.
|
|
1435
|
+
const decrements = (st) => {
|
|
1436
|
+
const seg = segOf(st);
|
|
1437
|
+
let fields = [];
|
|
1438
|
+
let max = 0;
|
|
1439
|
+
const amount = (t) => { const v = t && (t.t === 'word' || t.t === 'num') ? numberOf(constValue([t])) : null; return v != null && Number.isInteger(v) && v >= 0 ? v : null; };
|
|
1440
|
+
if (st.verb === 'SUBTRACT') {
|
|
1441
|
+
const from = seg.findIndex((t) => WORD(t, 'FROM'));
|
|
1442
|
+
if (from < 1 || seg.some((t) => WORD(t, 'GIVING') || WORD(t, 'CORRESPONDING') || WORD(t, 'CORR'))) return null;
|
|
1443
|
+
for (const t of seg.slice(0, from)) { const v = amount(t); if (v == null) return null; max += v; }
|
|
1444
|
+
const end = seg.findIndex((t, i) => i > from && PHRASE_AT(t));
|
|
1445
|
+
if (seg.slice(from + 1, end < 0 ? seg.length : end).some((t) => t.t === 'sep')) return null;
|
|
1446
|
+
fields = (st.targets || []).map((t) => resolve(t));
|
|
1447
|
+
} else if (st.verb === 'SET') {
|
|
1448
|
+
const down = seg.findIndex((t) => WORD(t, 'DOWN'));
|
|
1449
|
+
if (down < 1 || !WORD(seg[down + 1], 'BY') || seg.length !== down + 3 || seg.slice(0, down).some((t) => t.t !== 'word')) return null;
|
|
1450
|
+
max = amount(seg[down + 2]);
|
|
1451
|
+
if (max == null) return null;
|
|
1452
|
+
fields = (st.targets || []).map((t) => resolve(t));
|
|
1453
|
+
} else if (st.verb === 'COMPUTE') {
|
|
1454
|
+
const eq = seg.findIndex((t) => t.t === 'op' && t.v === '=');
|
|
1455
|
+
const lhs = seg.slice(0, eq).filter((t) => !WORD(t, 'ROUNDED'));
|
|
1456
|
+
const end = seg.findIndex((t, i) => i > eq && PHRASE_AT(t));
|
|
1457
|
+
const rhs = seg.slice(eq + 1, end < 0 ? seg.length : end);
|
|
1458
|
+
if (eq < 1 || lhs.length !== 1 || lhs[0].t !== 'word' || rhs.length !== 3 || rhs[1].t !== 'op' || rhs[1].v !== '-') return null;
|
|
1459
|
+
const target = resolve(lhs[0]);
|
|
1460
|
+
if (rhs[0].t !== 'word' || resolve(rhs[0]) !== target) return null;
|
|
1461
|
+
max = amount(rhs[2]);
|
|
1462
|
+
if (max == null) return null;
|
|
1463
|
+
fields = [target];
|
|
1464
|
+
} else return null;
|
|
1465
|
+
fields = [...new Set(fields.filter((f) => intCap(f) != null))];
|
|
1466
|
+
return fields.length ? { fields, max } : null;
|
|
1467
|
+
};
|
|
1422
1468
|
const lifted = new Map();
|
|
1423
1469
|
for (const [id, inc] of raisers) {
|
|
1424
1470
|
const list = [];
|
|
@@ -1712,7 +1758,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1712
1758
|
const r = rangeOfField(leaf.f);
|
|
1713
1759
|
const base = { needs: [], lo: r.hold.lo, hi: r.hold.hi };
|
|
1714
1760
|
if (!leaf.plain) return [base];
|
|
1715
|
-
const cv = numberOf(constValue([leaf.tok]));
|
|
1761
|
+
const cv = leaf.tok ? numberOf(constValue([leaf.tok])) : null;
|
|
1716
1762
|
if (cv != null && Number.isInteger(cv)) return [{ needs: [], lo: BigInt(cv), hi: BigInt(cv) }];
|
|
1717
1763
|
if (!heldBits) return [base];
|
|
1718
1764
|
let lo = null;
|
|
@@ -1781,9 +1827,19 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1781
1827
|
}
|
|
1782
1828
|
return out;
|
|
1783
1829
|
};
|
|
1830
|
+
// The derived intervals that hold every whole number from lo to hi, an open end holding nothing.
|
|
1831
|
+
const containing = (key, lo, hi) => {
|
|
1832
|
+
const out = [];
|
|
1833
|
+
for (const c of factsByKey.get(key) || []) {
|
|
1834
|
+
if (!c.derived) continue;
|
|
1835
|
+
const e = wholeEnds(c.tCons);
|
|
1836
|
+
if (e && (e.lo == null || (lo != null && e.lo <= lo)) && (e.hi == null || (hi != null && e.hi >= hi))) out.push(c.t);
|
|
1837
|
+
}
|
|
1838
|
+
return out;
|
|
1839
|
+
};
|
|
1784
1840
|
const expandRules = () => {
|
|
1785
1841
|
indexFacts();
|
|
1786
|
-
for (const n of nodes) { n.derive = undefined; n.genArith = undefined; }
|
|
1842
|
+
for (const n of nodes) { n.derive = undefined; n.genArith = undefined; n.genArithOn = undefined; }
|
|
1787
1843
|
for (const r of rules) {
|
|
1788
1844
|
const bits = withLooser(r.fact);
|
|
1789
1845
|
const at = r.bucket == null ? r.n : nodes[r.n.test];
|
|
@@ -1792,6 +1848,26 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1792
1848
|
else ((at.genArithOn ||= { true: [], false: [] })[r.bucket]).push(...bits);
|
|
1793
1849
|
} else (r.n.derive ||= []).push({ needs: r.needs, to: bits });
|
|
1794
1850
|
}
|
|
1851
|
+
// A constant moved, and a check's outcome, are values too: routes that reached an interval by
|
|
1852
|
+
// arithmetic and routes that reached it by a move or a test meet at it.
|
|
1853
|
+
for (const a of assigns) {
|
|
1854
|
+
const v = numberOf(a.value);
|
|
1855
|
+
if (v == null || !Number.isInteger(v)) continue;
|
|
1856
|
+
const bits = containing(fieldKey(a.field), BigInt(v), BigInt(v));
|
|
1857
|
+
if (bits.length) (a.n.genArith ||= []).push(...bits);
|
|
1858
|
+
}
|
|
1859
|
+
for (const id of tests) {
|
|
1860
|
+
const n = nodes[id];
|
|
1861
|
+
for (const c of n.checks || []) {
|
|
1862
|
+
for (const [cons, bucket] of [[c.tCons, 'true'], [c.fCons, 'false']]) {
|
|
1863
|
+
if (!cons || (cons.set && cons.set.size)) continue;
|
|
1864
|
+
const e = wholeEnds(cons);
|
|
1865
|
+
if (!e || (e.lo == null && e.hi == null)) continue;
|
|
1866
|
+
const bits = containing(c.key, e.lo, e.hi);
|
|
1867
|
+
if (bits.length) ((n.genArithOn ||= { true: [], false: [] })[bucket]).push(...bits);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1795
1871
|
};
|
|
1796
1872
|
|
|
1797
1873
|
const ARITH_VERBS = new Set(['COMPUTE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'MOVE']);
|
|
@@ -1985,6 +2061,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1985
2061
|
const before = relevant.size;
|
|
1986
2062
|
for (const a of arith) if (a.targets.some((r) => r.f && relevant.has(r.f))) for (const tree of a.trees) for (const l of tree ? leaves(tree) : []) relevant.add(l.f);
|
|
1987
2063
|
for (const r of relations.values()) if (relevant.has(r.f) || relevant.has(r.g)) { relevant.add(r.f); relevant.add(r.g); }
|
|
2064
|
+
// A loop condition that bounds a relevant counter through another field makes that field relevant.
|
|
2065
|
+
for (const c of checks) { if (c.pseudo || !c.field || !relevant.has(c.field)) continue; for (const d of [c.tDep, c.fDep]) if (d?.g) relevant.add(d.g); }
|
|
1988
2066
|
more = relevant.size > before;
|
|
1989
2067
|
}
|
|
1990
2068
|
|
|
@@ -1996,6 +2074,20 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1996
2074
|
let grew = false;
|
|
1997
2075
|
try {
|
|
1998
2076
|
indexFacts();
|
|
2077
|
+
// A check's outcome as an interval of its own, which arithmetic's intervals can widen into at a join.
|
|
2078
|
+
for (const id of tests) {
|
|
2079
|
+
for (const c of nodes[id].checks || []) {
|
|
2080
|
+
if (!relevant.has(c.field) || !rangeOfField(c.field) || inTable(c.field)) continue;
|
|
2081
|
+
for (const cons of [c.tCons, c.fCons]) {
|
|
2082
|
+
if (!cons || (cons.set && cons.set.size)) continue;
|
|
2083
|
+
const e = wholeEnds(cons);
|
|
2084
|
+
if (!e || (e.lo == null && e.hi == null)) continue;
|
|
2085
|
+
const had = derivedOf.get(c.field)?.size || 0;
|
|
2086
|
+
derivedFact(c.field, e.lo, e.hi, nodes[id].at || nodes[id].st);
|
|
2087
|
+
if ((derivedOf.get(c.field)?.size || 0) > had) grew = true;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
1999
2091
|
for (const a of arith) {
|
|
2000
2092
|
a.trees.forEach((tree, i) => {
|
|
2001
2093
|
const r = a.targets[i];
|
|
@@ -2032,6 +2124,27 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2032
2124
|
}
|
|
2033
2125
|
}
|
|
2034
2126
|
}
|
|
2127
|
+
// A bound over an expression in one other field, X <= G - 1, takes the interval G holds at the test.
|
|
2128
|
+
for (const c of checks) {
|
|
2129
|
+
if (c.pseudo || c.test == null || !relevant.has(c.field) || !rangeOfField(c.field) || inTable(c.field)) continue;
|
|
2130
|
+
for (const o of [true, false]) {
|
|
2131
|
+
const dep = o ? c.tDep : c.fDep;
|
|
2132
|
+
const at = dep && outcomeAt.get(`${c.test}|${o}`);
|
|
2133
|
+
if (!at || !rangeOfField(dep.g) || inTable(dep.g) || !Number.isInteger(dep.m) || !Number.isInteger(dep.b)) continue;
|
|
2134
|
+
const hold = rangeOfField(c.field).hold;
|
|
2135
|
+
heldBits = prev ? prev.get(c.test) || null : null;
|
|
2136
|
+
for (const cand of leafCands({ f: dep.g, tok: null, plain: true })) {
|
|
2137
|
+
const v = BigInt(dep.m) * (dep.needs === 'hi' ? cand.hi : cand.lo) + BigInt(dep.b);
|
|
2138
|
+
let lo = null, hi = null;
|
|
2139
|
+
if (dep.side === 'hi') hi = dep.inc ? v : v - 1n; else lo = dep.inc ? v : v + 1n;
|
|
2140
|
+
if (lo != null && lo <= hold.lo) lo = null;
|
|
2141
|
+
if (hi != null && hi >= hold.hi) hi = null;
|
|
2142
|
+
if ((lo == null && hi == null) || (hi != null && hi < hold.lo) || (lo != null && lo > hold.hi)) continue;
|
|
2143
|
+
const fact = derivedFact(c.field, lo, hi, at.st || nodes[c.test].at || nodes[c.test].st);
|
|
2144
|
+
if (fact && derive(at, cand.needs, fact, String(o))) grew = true;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2035
2148
|
// A relation holds after the outcome of its test that makes it true, and the fields it bounds hold
|
|
2036
2149
|
// what the other side could hold.
|
|
2037
2150
|
for (const id of tests) {
|
|
@@ -2161,6 +2274,31 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2161
2274
|
} else if (n.kind === 'step' && stepped.has(id)) {
|
|
2162
2275
|
keepAcross(n, [{ ...stepped.get(id), sizeError: false }]);
|
|
2163
2276
|
}
|
|
2277
|
+
// An upper bound survives a decrement that cannot go below 0: one where the field was at least the
|
|
2278
|
+
// amount before it, so nothing is stored as an absolute value or wraps.
|
|
2279
|
+
if (n.kind === 'stmt' && n.st && n.st.at != null) {
|
|
2280
|
+
const dec = decrements(n.st);
|
|
2281
|
+
const wrote = dec ? writtenBy(n) : [];
|
|
2282
|
+
for (const field of dec ? dec.fields : []) {
|
|
2283
|
+
if (wrote.some((w) => w !== field && overlaps(w, field))) continue;
|
|
2284
|
+
const key = fieldKey(field);
|
|
2285
|
+
const keep = [];
|
|
2286
|
+
const guards = [];
|
|
2287
|
+
for (const c of checks) {
|
|
2288
|
+
if (c.key !== key || c.pseudo) continue;
|
|
2289
|
+
for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
2290
|
+
if (bit == null || !cons || cons.dep || (cons.set && cons.set.size)) continue;
|
|
2291
|
+
const r = asRange(cons);
|
|
2292
|
+
if (r.hi != null && r.lo == null) keep.push(bit);
|
|
2293
|
+
if (r.lo != null && (r.loInc ? r.lo : Math.floor(r.lo) + 1) >= dec.max) guards.push(bit);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
if (!keep.length || !guards.length) continue;
|
|
2297
|
+
const bits = new Uint32Array(words);
|
|
2298
|
+
for (const b of keep) bits[b >>> 5] |= 1 << (b & 31);
|
|
2299
|
+
(n.keeps ||= []).push({ bits, guards });
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2164
2302
|
if (n.kind === 'step' && keptOnStep.has(id)) {
|
|
2165
2303
|
const bits = new Uint32Array(words);
|
|
2166
2304
|
for (const b of keptOnStep.get(id)) bits[b >>> 5] |= 1 << (b & 31);
|
|
@@ -2440,6 +2578,39 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2440
2578
|
const posOf = new Map();
|
|
2441
2579
|
const places = [];
|
|
2442
2580
|
let placeFacts = nFacts;
|
|
2581
|
+
// The tightest end of a field's value the facts `bits` hold, or its picture's.
|
|
2582
|
+
const boundAt = (g, needs, bits) => {
|
|
2583
|
+
const hold = rangeOfField(g)?.hold;
|
|
2584
|
+
let best = hold ? Number(needs === 'hi' ? hold.hi : hold.lo) : null;
|
|
2585
|
+
if (best != null && !Number.isFinite(best)) best = null;
|
|
2586
|
+
for (const c of checks) {
|
|
2587
|
+
if (c.pseudo || c.key !== fieldKey(g)) continue;
|
|
2588
|
+
for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
2589
|
+
if (!cons || !hasFact(bits, bit)) continue;
|
|
2590
|
+
const e = wholeEnds(cons);
|
|
2591
|
+
const v = e && (needs === 'hi' ? e.hi : e.lo);
|
|
2592
|
+
if (v == null) continue;
|
|
2593
|
+
const n = Number(v);
|
|
2594
|
+
if (best == null || (needs === 'hi' ? n < best : n > best)) best = n;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
return best;
|
|
2598
|
+
};
|
|
2599
|
+
// A relation over an expression, X <= G - 1, read with what G held at the test.
|
|
2600
|
+
const resolveDeps = (map, bits) => {
|
|
2601
|
+
const out = new Map();
|
|
2602
|
+
for (const [key, c] of map) {
|
|
2603
|
+
const dep = c && c.dep;
|
|
2604
|
+
const g = dep ? boundAt(dep.g, dep.needs, bits) : null;
|
|
2605
|
+
if (g == null || !Number.isInteger(dep.m) || !Number.isInteger(dep.b)) { out.set(key, c); continue; }
|
|
2606
|
+
const v = dep.m * g + dep.b;
|
|
2607
|
+
const plain = plainCons(c) || {};
|
|
2608
|
+
const had = plain[dep.side];
|
|
2609
|
+
const tighter = had == null || (dep.side === 'hi' ? v < had : v > had);
|
|
2610
|
+
out.set(key, tighter ? { ...plain, [dep.side]: v, [`${dep.side}Inc`]: dep.inc } : plain);
|
|
2611
|
+
}
|
|
2612
|
+
return out;
|
|
2613
|
+
};
|
|
2443
2614
|
for (const id of tests) {
|
|
2444
2615
|
const n = nodes[id];
|
|
2445
2616
|
if (!n.st || !n.st.indexes || !n.tree || n.evaluate || !(n.st.verb === 'IF' || n.loop)) continue;
|
|
@@ -2454,7 +2625,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2454
2625
|
for (const g of guards || []) {
|
|
2455
2626
|
let cons = new Map();
|
|
2456
2627
|
const r = new Map();
|
|
2457
|
-
|
|
2628
|
+
const atTest = facts.get(id);
|
|
2629
|
+
for (const [sub, outcome] of g) { cons = meetMaps(cons, atTest ? resolveDeps(consOf(sub, outcome), atTest) : consOf(sub, outcome)); testedWhen(sub, outcome, r); }
|
|
2458
2630
|
said = said ? joinMaps(said, cons) : cons;
|
|
2459
2631
|
ran = ran ? new Map([...ran].filter(([k]) => r.has(k))) : r;
|
|
2460
2632
|
}
|
package/lib/dataflow.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
import { inScope, isProgram, isBms,
|
|
2
|
+
import { inScope, isProgram, isBms, relPath } from './sources.mjs';
|
|
3
3
|
import { parseBms, symbolicNames } from './bms.mjs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { parseSource, buildFileIndex } from './parser.mjs';
|
|
@@ -197,6 +197,7 @@ export function analyze(root, opts = {}) {
|
|
|
197
197
|
const nodes = [];
|
|
198
198
|
const programs = [];
|
|
199
199
|
const jclPending = [];
|
|
200
|
+
const jclTrees = new Map();
|
|
200
201
|
// CICS definitions, from CSD extracts and from DFHCSDUP job input, and the estate's own statement
|
|
201
202
|
// of which DDs and queues reach the internal reader.
|
|
202
203
|
const csd = { tdqueues: new Map(), transactions: new Map(), urimaps: new Map() };
|
|
@@ -209,7 +210,7 @@ export function analyze(root, opts = {}) {
|
|
|
209
210
|
const jobSteps = [];
|
|
210
211
|
// The maps of the repository being read, by MAPSET/MAP and by MAP alone.
|
|
211
212
|
const bmsMaps = new Map();
|
|
212
|
-
const site = loadSite(root, opts.site || null);
|
|
213
|
+
const site = loadSite(root, opts.site || null, opts.tree);
|
|
213
214
|
const relOf = new Map();
|
|
214
215
|
const rel = (p) => { let r = relOf.get(p); if (r === undefined) { r = relPath(root, p); relOf.set(p, r); } return r; };
|
|
215
216
|
// `at` is the statement an edge that is not a statement's own is read at: the CALL or LINK that
|
|
@@ -959,7 +960,7 @@ export function analyze(root, opts = {}) {
|
|
|
959
960
|
const tree = (!repo && opts.tree) ? opts.tree : directoryTree(base, opts);
|
|
960
961
|
const idx = tree.index;
|
|
961
962
|
const files = tree.list().filter(isProgram).filter(inScope(opts));
|
|
962
|
-
for (const f of tree.list().filter(isJcl).filter(inScope(opts))) jclPending.push(f);
|
|
963
|
+
for (const f of tree.list().filter(isJcl).filter(inScope(opts))) { jclPending.push(f); jclTrees.set(f, tree); }
|
|
963
964
|
for (const f of tree.list().filter((p) => /\.csd$/i.test(p)).filter(inScope(opts))) {
|
|
964
965
|
try { addCsd(tree.text(f).text, f); } catch (e) { stats.unreadable++; unread.push(`${tree.rel(f)}: ${e.code || e.name}`); }
|
|
965
966
|
}
|
|
@@ -1088,7 +1089,7 @@ export function analyze(root, opts = {}) {
|
|
|
1088
1089
|
// ACCEPT FROM COMMAND-LINE is untrusted.
|
|
1089
1090
|
for (const f of jclPending) {
|
|
1090
1091
|
let src;
|
|
1091
|
-
try { src =
|
|
1092
|
+
try { src = jclTrees.get(f).text(f).text; } catch (e) { stats.unreadable++; unread.push(`${rel(f)}: ${e.code || e.name}`); continue; }
|
|
1092
1093
|
let job;
|
|
1093
1094
|
try { job = parseJcl(src, f); } catch (e) { stats.threw++; unparsed.push(`${rel(f)}: ${e.code || e.name}`); continue; }
|
|
1094
1095
|
stats.jclFiles++;
|