@portll/cobolwork 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +49 -18
  2. package/STABILITY.md +76 -0
  3. package/bin/cobolwork.mjs +30 -7
  4. package/lib/bms.mjs +21 -7
  5. package/lib/build.mjs +108 -53
  6. package/lib/capabilities.mjs +9 -10
  7. package/lib/cics-commands.mjs +501 -9
  8. package/lib/compliance.mjs +11 -0
  9. package/lib/consequence.mjs +13 -0
  10. package/lib/control-workers.mjs +161 -0
  11. package/lib/control.mjs +52 -3
  12. package/lib/dataflow.mjs +176 -125
  13. package/lib/db2/cursor.mjs +15 -0
  14. package/lib/db2/read.mjs +170 -0
  15. package/lib/db2/rules.mjs +77 -0
  16. package/lib/db2/stmt/alter.mjs +473 -0
  17. package/lib/db2/stmt/grant.mjs +125 -0
  18. package/lib/db2/stmt/index.mjs +141 -0
  19. package/lib/db2/stmt/misc.mjs +325 -0
  20. package/lib/db2/stmt/routine.mjs +564 -0
  21. package/lib/db2/stmt/storage.mjs +146 -0
  22. package/lib/db2/stmt/table.mjs +540 -0
  23. package/lib/db2/stmt/view.mjs +146 -0
  24. package/lib/diff.mjs +17 -5
  25. package/lib/evidence/cli.mjs +14 -3
  26. package/lib/evidence/record.mjs +1 -1
  27. package/lib/evidence/store.mjs +44 -31
  28. package/lib/exec-reading.mjs +51 -0
  29. package/lib/execution.mjs +3 -2
  30. package/lib/explain.mjs +2 -0
  31. package/lib/exploitability.mjs +11 -2
  32. package/lib/hlasm/asm/data.mjs +13 -1
  33. package/lib/hlasm/asm/sections.mjs +3 -1
  34. package/lib/hlasm/instr.mjs +25 -0
  35. package/lib/hlasm/macro/authorization.mjs +24 -4
  36. package/lib/hlasm/macro/datasets.mjs +30 -17
  37. package/lib/hlasm/macro/io.mjs +68 -45
  38. package/lib/hlasm/macro/le.mjs +2 -2
  39. package/lib/hlasm/macro/operator.mjs +32 -21
  40. package/lib/hlasm/macro/program.mjs +126 -84
  41. package/lib/hlasm/macro/recovery.mjs +10 -5
  42. package/lib/hlasm/macro/storage.mjs +64 -14
  43. package/lib/hlasm/macro/structured.mjs +1 -1
  44. package/lib/hlasm/model.mjs +35 -10
  45. package/lib/hlasm/mvs38.mjs +47 -0
  46. package/lib/hlasm/operands.mjs +10 -0
  47. package/lib/hlasm/optable.mjs +61 -0
  48. package/lib/hlasm/read.mjs +22 -8
  49. package/lib/hlasm.mjs +44 -7
  50. package/lib/ims/dli.mjs +37 -0
  51. package/lib/ims/macro/dbd.mjs +299 -0
  52. package/lib/ims/macro/psb.mjs +286 -0
  53. package/lib/ims/model.mjs +149 -0
  54. package/lib/ims/operands.mjs +23 -0
  55. package/lib/ims/read.mjs +37 -0
  56. package/lib/ims/rules.mjs +135 -0
  57. package/lib/ironwork.mjs +17 -13
  58. package/lib/kernel/pds-archive.mjs +256 -0
  59. package/lib/kernel/registry.mjs +14 -8
  60. package/lib/kernel/shared-pass.mjs +34 -9
  61. package/lib/kernel/source-tree.mjs +104 -36
  62. package/lib/kernel/version-key.mjs +17 -0
  63. package/lib/layout.mjs +26 -31
  64. package/lib/parser.mjs +136 -17
  65. package/lib/pli/cursor.mjs +15 -0
  66. package/lib/pli/expr.mjs +101 -0
  67. package/lib/pli/include.mjs +82 -0
  68. package/lib/pli/layout.mjs +125 -0
  69. package/lib/pli/lex.mjs +198 -0
  70. package/lib/pli/program.mjs +280 -0
  71. package/lib/pli/rules/based.mjs +95 -0
  72. package/lib/pli/rules/conditions.mjs +68 -0
  73. package/lib/pli/rules/entry.mjs +130 -0
  74. package/lib/pli/rules/index.mjs +24 -0
  75. package/lib/pli/rules/preprocessor.mjs +55 -0
  76. package/lib/pli/statements.mjs +130 -0
  77. package/lib/pli/stmt/alloc.mjs +45 -0
  78. package/lib/pli/stmt/assignment.mjs +56 -0
  79. package/lib/pli/stmt/call.mjs +104 -0
  80. package/lib/pli/stmt/conditions.mjs +94 -0
  81. package/lib/pli/stmt/control.mjs +219 -0
  82. package/lib/pli/stmt/declare.mjs +149 -0
  83. package/lib/pli/stmt/exec.mjs +55 -0
  84. package/lib/pli/stmt/io.mjs +239 -0
  85. package/lib/pli/stmt/misc.mjs +4 -0
  86. package/lib/pli/stmt/preprocessor.mjs +242 -0
  87. package/lib/pli/stmt/procedure.mjs +258 -0
  88. package/lib/pli/stmt/stream.mjs +283 -0
  89. package/lib/pli/storage.mjs +129 -0
  90. package/lib/precompile-check.mjs +124 -0
  91. package/lib/precompile-cics.mjs +7 -3
  92. package/lib/reach.mjs +11 -2
  93. package/lib/revision.json +1 -1
  94. package/lib/sarif.mjs +41 -3
  95. package/lib/scan.mjs +7 -1
  96. package/lib/sets/abend.mjs +16 -6
  97. package/lib/sets/cics.mjs +15 -35
  98. package/lib/sets/compile.mjs +41 -15
  99. package/lib/sets/crypto.mjs +5 -3
  100. package/lib/sets/ddl.mjs +36 -0
  101. package/lib/sets/flow.mjs +18 -1
  102. package/lib/sets/hidden.mjs +5 -3
  103. package/lib/sets/hlasm.mjs +72 -12
  104. package/lib/sets/ims.mjs +139 -0
  105. package/lib/sets/log.mjs +6 -6
  106. package/lib/sets/opaque.mjs +27 -7
  107. package/lib/sets/pli.mjs +40 -0
  108. package/lib/sets/recon.mjs +5 -3
  109. package/lib/sets/secrets.mjs +5 -3
  110. package/lib/sets/semantics.mjs +3 -0
  111. package/lib/sets/web.mjs +36 -22
  112. package/lib/site.mjs +10 -0
  113. package/lib/sources.mjs +80 -20
  114. package/lib/statement-cursor.mjs +67 -0
  115. package/lib/verify.mjs +3 -2
  116. package/lib/version.mjs +6 -0
  117. package/package.json +3 -2
  118. package/rules/compliance-cobit2019.json +432 -1
  119. package/rules/compliance-dora.json +414 -1
  120. package/rules/compliance-ffiec.json +414 -1
  121. package/rules/compliance-nist80053.json +466 -1
  122. package/rules/hlasm-optables.json +8024 -0
  123. package/schema/cobolwork-baseline.schema.json +36 -0
  124. package/schema/cobolwork-build-provenance.schema.json +187 -0
  125. package/schema/cobolwork-build.schema.json +382 -0
  126. package/schema/cobolwork-capabilities.schema.json +239 -0
  127. package/schema/cobolwork-diff.schema.json +217 -0
  128. package/schema/cobolwork-evidence.schema.json +161 -0
  129. package/schema/cobolwork-execution.schema.json +53 -0
  130. package/schema/cobolwork-explain.schema.json +360 -0
  131. package/schema/cobolwork-finding.schema.json +465 -0
  132. package/schema/cobolwork-flow.schema.json +465 -0
  133. package/schema/cobolwork-gate.schema.json +211 -0
  134. package/schema/cobolwork-inventory.schema.json +206 -0
  135. package/schema/cobolwork-parse.schema.json +105 -0
  136. package/schema/cobolwork-reach.schema.json +74 -0
  137. package/schema/cobolwork-report.schema.json +559 -0
  138. package/schema/cobolwork-witness.schema.json +107 -0
  139. package/schema/cobolwork.baseline.schema.json +101 -0
  140. package/schema/cobolwork.site.schema.json +116 -0
@@ -0,0 +1,280 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // A PL/I source file as the program the flow engine reads (lib/dataflow.mjs summarise): data items
3
+ // with offsets and sizes from lib/pli/layout.mjs, statements as the names they read and write,
4
+ // external calls with their arguments, EXEC blocks as the engine's tokens, and the MAIN procedure's
5
+ // parameters. The engine itself is unchanged.
6
+ import { basename } from 'node:path';
7
+ import { readPli } from './lex.mjs';
8
+ import { readPliExpanded } from './include.mjs';
9
+ import { parseStatement } from './statements.mjs';
10
+ import { layout } from './layout.mjs';
11
+
12
+ // The token naming a reference: its last name at parenthesis depth 0.
13
+ function nameToken(ref) {
14
+ let depth = 0;
15
+ let last = null;
16
+ for (const t of ref.toks) {
17
+ if (t.t === 'op' && t.v === '(') depth++;
18
+ else if (t.t === 'op' && t.v === ')') depth--;
19
+ else if (depth === 0 && t.t === 'word') last = t;
20
+ }
21
+ return last;
22
+ }
23
+
24
+ // EXEC block tokens as lib/parser.mjs gives them: parentheses as separators, commas dropped, a
25
+ // period between two names joined.
26
+ function execTokens(toks) {
27
+ const out = [];
28
+ for (let k = 0; k < toks.length; k++) {
29
+ const t = toks[k];
30
+ if (t.t === 'op' && (t.v === '(' || t.v === ')')) out.push({ ...t, t: 'sep' });
31
+ else if (t.t === 'op' && t.v === ',') continue;
32
+ else if (t.t === 'op' && t.v === '.') out.push({ ...t, t: 'period', joined: !!(toks[k - 1]?.t === 'word' && toks[k + 1]?.t === 'word' && toks[k + 1].col === t.col + 1) });
33
+ else out.push(t);
34
+ }
35
+ return out;
36
+ }
37
+
38
+ const ARITHMETIC_OPS = new Set(['+', '-', '*', '/', '**', 'prefix-', 'prefix+']);
39
+
40
+ // What the Db2 precompiler writes for EXEC SQL INCLUDE SQLCA in a PL/I program (Db2 13 SQL
41
+ // Reference, "The included SQLCA").
42
+ const SQLCA_PLI = ` DECLARE
43
+ 1 SQLCA,
44
+ 2 SQLCAID CHAR(8),
45
+ 2 SQLCABC FIXED(31) BINARY,
46
+ 2 SQLCODE FIXED(31) BINARY,
47
+ 2 SQLERRM CHAR(70) VAR,
48
+ 2 SQLERRP CHAR(8),
49
+ 2 SQLERRD(6) FIXED(31) BINARY,
50
+ 2 SQLWARN,
51
+ 3 SQLWARN0 CHAR(1),
52
+ 3 SQLWARN1 CHAR(1),
53
+ 3 SQLWARN2 CHAR(1),
54
+ 3 SQLWARN3 CHAR(1),
55
+ 3 SQLWARN4 CHAR(1),
56
+ 3 SQLWARN5 CHAR(1),
57
+ 3 SQLWARN6 CHAR(1),
58
+ 3 SQLWARN7 CHAR(1),
59
+ 2 SQLEXT,
60
+ 3 SQLWARN8 CHAR(1),
61
+ 3 SQLWARN9 CHAR(1),
62
+ 3 SQLWARNA CHAR(1),
63
+ 3 SQLSTATE CHAR(5);`;
64
+
65
+ // readMember(name, fromFile) -> { path, text } | null splices %INCLUDE members in; without it the
66
+ // directives stay as statements and what they would have declared is missing.
67
+ export function pliProgram(text, file, { readMember = null } = {}) {
68
+ const read = readMember ? readPliExpanded(text, { file, readMember }) : readPli(text, { file });
69
+ const statements = read.statements.map((st) => ({ st, r: parseStatement(st) }));
70
+ const declared = [];
71
+ for (const { st, r } of statements) if (r.status === 'parsed' && r.kind === 'DECLARE') declared.push(...r.node.items.map((it) => ({ ...it, file: st.file || file })));
72
+ const sqlcaAt = statements.find(({ st, r }) => r.kind === 'EXEC' && r.node?.processor === 'SQL' && r.node.sql?.verb === 'INCLUDE' && st.toks[3]?.u === 'SQLCA')?.st;
73
+ if (sqlcaAt && !declared.some((it) => it.name === 'SQLCA')) {
74
+ for (const st of readPli(SQLCA_PLI, { file }).statements) {
75
+ const r = parseStatement(st);
76
+ if (r.kind === 'DECLARE') declared.push(...r.node.items.map((it) => ({ ...it, file: sqlcaAt.file || file, line: sqlcaAt.line })));
77
+ }
78
+ }
79
+ const { roots } = layout(declared);
80
+
81
+ const items = [];
82
+ const flatten = (node, parent) => {
83
+ const item = {
84
+ name: node.name, level: node.logical, parent, children: [], file: node.file || file, line: node.line, values: [],
85
+ offset: node.offset, occurs: node.occurs || 1,
86
+ size: node.children.length ? node.size * Math.max(1, node.occurs || 1) : node.size,
87
+ attributes: node.attributes.map((a) => a.name), storage: node.storage || null,
88
+ };
89
+ items.push(item);
90
+ for (const ch of node.children) item.children.push(flatten(ch, item));
91
+ return item;
92
+ };
93
+ for (const r of roots) flatten(r, null);
94
+ const byName = new Map();
95
+ for (const it of items) { if (!byName.has(it.name)) byName.set(it.name, []); byName.get(it.name).push(it); }
96
+ const builtin = new Set(items.filter((it) => it.attributes.includes('BUILTIN') || it.attributes.includes('GENERIC')).map((it) => it.name));
97
+
98
+ // A qualified reference A.B.C names the C whose containing structures include B and then A; after
99
+ // a locator, P->X.Y, only the part after the arrow qualifies the data.
100
+ const resolved = new Map();
101
+ const resolve = (ref) => {
102
+ const qual = ref.path.slice(ref.locators.length ? ref.locators[ref.locators.length - 1] : 0);
103
+ const cands = (byName.get(ref.name) || []).filter((it) => !builtin.has(it.name));
104
+ const fits = cands.filter((it) => {
105
+ let k = qual.length - 2;
106
+ for (let a = it.parent; a && k >= 0; a = a.parent) if (a.name === qual[k]) k--;
107
+ return k < 0;
108
+ });
109
+ const item = fits[0] || null;
110
+ const tok = nameToken(ref);
111
+ if (item && tok) resolved.set(tok, item);
112
+ return item ? tok : null;
113
+ };
114
+
115
+ // The names an expression reads: references to declared data; a reference that names no declared
116
+ // item and has arguments is a function, read through its arguments.
117
+ // A subscript names a table element; SUBSTR's start and length bound a reference into its
118
+ // first argument. Both are what the engine's subscript and reference-modification sinks read.
119
+ const tableOf = (it) => { for (let a = it; a; a = a.parent) if ((a.occurs || 1) > 1) return a; return null; };
120
+ const indexOf = (a) => {
121
+ const t = a && a.tree;
122
+ if (!t) return null;
123
+ if (t.t === 'ref') { const tok = resolve(t); return tok ? { tok, offset: 0 } : null; }
124
+ if ((t.op === '+' || t.op === '-') && t.args[0]?.t === 'ref' && t.args[1]?.t === 'num' && /^\d+$/.test(t.args[1].tok.v)) {
125
+ const tok = resolve(t.args[0]);
126
+ return tok ? { tok, offset: (t.op === '+' ? 1 : -1) * Number(t.args[1].tok.v) } : null;
127
+ }
128
+ return null;
129
+ };
130
+ const noteIndexes = (n, hostTok, indexes) => {
131
+ if (!indexes) return;
132
+ const item = resolved.get(hostTok);
133
+ if (item && tableOf(item)) {
134
+ for (const list of n.args) for (const a of list) { const x = indexOf(a); if (x) indexes.push({ host: hostTok, tok: x.tok, kind: 'subscript', ...(x.offset ? { offset: x.offset } : {}) }); }
135
+ } else if (!item && n.name === 'SUBSTR' && n.args[0]?.[0]?.tree?.t === 'ref') {
136
+ const host = resolve(n.args[0][0].tree);
137
+ const [, from, len] = n.args[0].map(indexOf);
138
+ if (host && from) indexes.push({ host, tok: from.tok, kind: 'refmod-offset', ...(from.offset ? { offset: from.offset } : {}) });
139
+ if (host && len) indexes.push({ host, tok: len.tok, kind: 'refmod-length' });
140
+ }
141
+ };
142
+ const reads = (expr, out = [], indexes = null) => {
143
+ const walk = (n) => {
144
+ if (!n) return;
145
+ if (n.t === 'ref') {
146
+ const tok = resolve(n);
147
+ if (tok) out.push(tok);
148
+ noteIndexes(n, tok, indexes);
149
+ if (!tok) for (const list of n.args) for (const a of list) if (a.tree) walk(a.tree);
150
+ for (const at of n.locators) { const p = byName.get(n.path[at - 1]); if (p) { const t = n.toks.find((x) => x.t === 'word' && x.u === n.path[at - 1]); if (t) { resolved.set(t, p[0]); out.push(t); } } }
151
+ } else if (n.t === 'paren') walk(n.expr.tree);
152
+ else if (n.t === 'lit' && n.factor) walk(n.factor.tree);
153
+ else if (n.args) n.args.forEach(walk);
154
+ };
155
+ walk(expr.tree ?? expr);
156
+ return out;
157
+ };
158
+ const hasOp = (tree, ops) => !!tree && (ops.has(tree.op) || (tree.args || []).some((a) => hasOp(a, ops)));
159
+
160
+ const prog = { id: null, line: 1, file, items, resolved, statements: [], execs: [], calls: [], paramTokens: [], labels: [], proc: null, files: [], fds: [], accepts: [], unresolvedRefs: [] };
161
+ const procs = new Map();
162
+ for (const { st, r } of statements) {
163
+ if (r.status !== 'parsed' || r.kind !== 'PROCEDURE') continue;
164
+ procs.set(r.node.name, { node: r.node, st });
165
+ if (!prog.id) {
166
+ prog.id = r.node.name;
167
+ prog.line = st.line;
168
+ }
169
+ if (r.node.options.some((o) => o.name === 'MAIN') && !prog.paramTokens.length) {
170
+ for (const p of r.node.params) {
171
+ const tok = st.toks.find((t) => t.t === 'word' && t.u === p);
172
+ const item = (byName.get(p) || [])[0];
173
+ if (tok && item) {
174
+ resolved.set(tok, item);
175
+ prog.paramTokens.push({ tok, mode: 'REFERENCE' });
176
+ // A MAIN procedure's parameter is the PARM string, or the command line off z/OS.
177
+ prog.accepts.push({ target: p, targetTok: tok, from: 'COMMAND-LINE', line: st.line, file: st.file || file });
178
+ }
179
+ }
180
+ }
181
+ }
182
+ if (!prog.id) prog.id = basename(file).replace(/\.[^.]*$/, '').toUpperCase();
183
+
184
+ const add = (st, verb, sources, targets, extra = {}) => prog.statements.push({ verb, sources, targets, file: st.file || file, line: st.line, ...extra });
185
+ const visit = (st, r, top = true) => {
186
+ if (r.status !== 'parsed' && r.status !== 'unbuilt') return;
187
+ const n = r.node;
188
+ if (!n) return;
189
+ if (r.kind === 'ASSIGNMENT') {
190
+ const indexes = [];
191
+ const sources = reads(n.value, [], indexes);
192
+ const targets = [];
193
+ for (const t of n.targets) {
194
+ const tok = resolve(t);
195
+ noteIndexes(t, tok, indexes);
196
+ if (tok) { targets.push(tok); continue; }
197
+ // A pseudovariable such as SUBSTR(X, 1, 2) = ... writes its first argument.
198
+ const first = t.args[0]?.[0];
199
+ if (first?.tree?.t === 'ref') { const a = resolve(first.tree); if (a) targets.push(a); }
200
+ }
201
+ const verb = n.value.tree?.t === 'ref' ? 'MOVE' : hasOp(n.value.tree, new Set(['||'])) ? 'STRING' : hasOp(n.value.tree, ARITHMETIC_OPS) ? 'COMPUTE' : 'MOVE';
202
+ add(st, verb, sources, targets, indexes.length ? { indexes } : {});
203
+ } else if (r.kind === 'IF' || r.kind === 'WHEN' || (r.kind === 'DO' && (n.while || n.until))) {
204
+ // A condition is what the engine credits as a check on the names it compares.
205
+ const conds = r.kind === 'IF' ? [n.cond] : r.kind === 'WHEN' ? n.values : [n.while, n.until].filter(Boolean);
206
+ const indexes = [];
207
+ const sources = conds.flatMap((c) => reads(c, [], indexes));
208
+ const ops = [];
209
+ const walk = (t) => { if (!t) return; if (t.op) ops.push(t.op); (t.args || []).forEach(walk); if (t.t === 'paren') walk(t.expr.tree); };
210
+ conds.forEach((c) => walk(c.tree));
211
+ add(st, r.kind === 'WHEN' ? 'WHEN' : 'IF', sources, [], { ops, ...(indexes.length ? { indexes } : {}) });
212
+ } else if (r.kind === 'READ') {
213
+ const into = n.options?.INTO;
214
+ const recTok = into ? resolve(into) : null;
215
+ add(st, 'READ', [], recTok ? [recTok] : []);
216
+ // A PL/I file's DD name is its own name unless OPEN gives it a TITLE; the record it reads
217
+ // into is what a job's DD for that name supplies.
218
+ const fname = n.file?.name;
219
+ if (fname && recTok) {
220
+ let fd = prog.fds.find((x) => x.name === fname);
221
+ if (!fd) { fd = { name: fname, records: [], file: st.file || file, line: st.line }; prog.fds.push(fd); prog.files.push({ name: fname, assign: { t: 'lit', v: fname }, file: st.file || file, line: st.line }); }
222
+ const rec = resolved.get(recTok);
223
+ if (rec && !fd.records.includes(rec)) fd.records.push(rec);
224
+ }
225
+ } else if (r.kind === 'WRITE' || r.kind === 'REWRITE') {
226
+ const from = n.options?.FROM;
227
+ add(st, 'WRITE', from ? [resolve(from)].filter(Boolean) : [], []);
228
+ } else if (r.kind === 'CALL') {
229
+ const internal = procs.get(n.name);
230
+ const argToks = n.args.map((a) => (a.tree?.t === 'ref' ? resolve(a.tree) : null));
231
+ if (internal) {
232
+ // Arguments pass by reference: an internal procedure's parameter is the argument's storage.
233
+ internal.node.params.forEach((p, k) => {
234
+ const pTok = internal.st.toks.find((t) => t.t === 'word' && t.u === p);
235
+ const pItem = (byName.get(p) || [])[0];
236
+ if (!pTok || !pItem || !argToks[k]) return;
237
+ resolved.set(pTok, pItem);
238
+ add(st, 'MOVE', [argToks[k]], [pTok]);
239
+ add(st, 'MOVE', [pTok], [argToks[k]]);
240
+ });
241
+ } else {
242
+ const variable = (byName.get(n.callee.name) || []).some((it) => it.attributes.includes('ENTRY') && it.attributes.includes('VARIABLE'));
243
+ const targetTok = variable ? resolve(n.callee) : nameToken(n.callee);
244
+ add(st, 'CALL', [], []);
245
+ prog.calls.push({
246
+ kind: variable ? 'I' : 'L', name: n.name, line: st.line, file: st.file || file, targetTok,
247
+ using: n.args.map((a, k) => ({ tok: argToks[k], word: argToks[k]?.u ?? null, mode: 'REFERENCE' })),
248
+ stmtIndex: prog.statements.length - 1,
249
+ });
250
+ }
251
+ } else if (r.kind === 'FETCH') {
252
+ // A TITLE that is a variable names the module loaded at run time.
253
+ for (const e of n.entries) {
254
+ const t = e.title && e.title.tree;
255
+ const tok = t && t.t === 'ref' ? resolve(t) : null;
256
+ if (!tok) continue;
257
+ add(st, 'CALL', [], []);
258
+ prog.calls.push({ kind: 'I', name: tok.u, line: st.line, file: st.file || file, targetTok: tok, using: [], stmtIndex: prog.statements.length - 1 });
259
+ }
260
+ } else if (r.kind === 'OPEN') {
261
+ // A TITLE that is a variable names the dataset opened, as SELECT ... ASSIGN TO a data item does.
262
+ for (const f of n.files) {
263
+ const t = f.options && f.options.TITLE && f.options.TITLE.tree;
264
+ if (t && t.t === 'ref' && resolve(t)) prog.files.push({ name: f.file.name, assign: { t: 'word', v: t.name }, file: st.file || file, line: st.line });
265
+ }
266
+ } else if (r.kind === 'EXEC' && top) {
267
+ prog.execs.push({ t: 'exec', kind: n.processor, toks: execTokens(st.toks.slice(2)), line: st.line, file: st.file || file });
268
+ for (const t of st.toks) if (t.t === 'word' && byName.has(t.u)) resolved.set(t, byName.get(t.u)[0]);
269
+ }
270
+ for (const u of n.units || []) if (u.node) visit(st, u, false);
271
+ };
272
+ for (const { st, r } of statements) visit(st, r);
273
+ prog.includes = { included: read.included || [], unresolved: read.unresolved || [], cycles: read.cycles || [] };
274
+ return prog;
275
+ }
276
+
277
+ // The engine's parse result for one PL/I source file.
278
+ export function parsePliSource(text, file, opts = {}) {
279
+ return { file, format: 'pli', programs: [pliProgram(text, file, opts)], options: [], diags: [], copies: [] };
280
+ }
@@ -0,0 +1,95 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Rule: BASED storage addressed through a pointer.
3
+
4
+ export const RULES = {
5
+ 'pli-based-storage-addressing': {
6
+ sev: 'info',
7
+ evidence: 'coverage',
8
+ cwe: 'CWE-119',
9
+ text: 'A program addresses storage through a pointer, so data flow through it is not followed'
10
+ }
11
+ };
12
+
13
+ const ADDRESS_ARITHMETIC = new Set(['POINTERADD', 'PTRADD', 'POINTERVALUE', 'PTRVALUE', 'POINTERSUBTRACT', 'PTRSUBTRACT']);
14
+
15
+ // Whether an expression computes an address rather than copying one.
16
+ function computed(tree) {
17
+ if (!tree) return false;
18
+ if (tree.t === 'ref') return ADDRESS_ARITHMETIC.has(tree.name) || tree.args.some((list) => list.some((a) => computed(a.tree)));
19
+ if (tree.op === '+' || tree.op === '-') return true;
20
+ if (tree.t === 'paren') return computed(tree.expr.tree);
21
+ return (tree.args || []).some(computed);
22
+ }
23
+
24
+ export function check(program) {
25
+ const basedVars = [];
26
+ const pointerVars = new Set();
27
+ let firstBasedLine = null;
28
+ let firstPointerAssign = null;
29
+
30
+ const walk = (units) => {
31
+ if (!Array.isArray(units)) return;
32
+ for (const unit of units) {
33
+ if (!unit || unit.status !== 'parsed' || !unit.node) continue;
34
+ const node = unit.node;
35
+
36
+ if (node.kind === 'DECLARE') {
37
+ for (const item of node.items || []) {
38
+ const name = item.name;
39
+ if (!name) continue;
40
+ const attrs = item.attributes || [];
41
+ const basedAttr = attrs.find((a) => a.name === 'BASED');
42
+ if (basedAttr) {
43
+ basedVars.push({ name, args: basedAttr.args || [] });
44
+ if (firstBasedLine === null) firstBasedLine = unit.line;
45
+ }
46
+ if (attrs.some((a) => a.name === 'POINTER')) {
47
+ pointerVars.add(name.toUpperCase());
48
+ }
49
+ }
50
+ } else if (node.kind === 'ASSIGNMENT') {
51
+ const targets = node.targets || [];
52
+ for (const target of targets) {
53
+ if (target && target.t === 'ref' && target.name && pointerVars.has(target.name.toUpperCase())) {
54
+ if (computed(node.value && node.value.tree) && !firstPointerAssign) firstPointerAssign = { name: target.name, line: unit.line };
55
+ }
56
+ }
57
+ }
58
+
59
+ if (node.units) walk(node.units.map((u) => ({ ...u, line: unit.line })));
60
+ }
61
+ };
62
+
63
+ walk(program.statements.map((st) => ({ ...st.parsed, line: st.line })));
64
+
65
+ if (basedVars.length === 0) return [];
66
+
67
+ const names = basedVars.slice(0, 3).map((v) => v.name);
68
+ const namesStr = names.join(', ');
69
+ const count = basedVars.length;
70
+
71
+ let detail = `${count} variable${count === 1 ? '' : 's'} declared BASED (${namesStr})`;
72
+
73
+ const overlay = (v) => v.args[0] && v.args[0].t === 'word' && v.args[0].u === 'ADDR';
74
+ const hasAddr = basedVars.some(overlay);
75
+ const hasPlain = basedVars.some((v) => !overlay(v));
76
+
77
+ if (hasAddr && hasPlain) {
78
+ detail += ', some using ADDR() of declared variables (overlays) and some plain pointers';
79
+ } else if (hasAddr) {
80
+ detail += ', using ADDR() of declared variables (overlays)';
81
+ } else if (hasPlain) {
82
+ detail += ', plain pointers addressing whatever the pointer holds';
83
+ }
84
+
85
+ if (firstPointerAssign) {
86
+ detail += `; first computed address assignment to ${firstPointerAssign.name} at line ${firstPointerAssign.line}`;
87
+ }
88
+
89
+ return [{
90
+ rule: 'pli-based-storage-addressing',
91
+ path: program.path,
92
+ line: firstBasedLine,
93
+ detail
94
+ }];
95
+ }
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // PL/I rules on condition handling.
3
+
4
+ export const RULES = {
5
+ 'pli-error-condition-ignored': {
6
+ sev: 'low',
7
+ evidence: 'construct',
8
+ cwe: 'CWE-390',
9
+ text: 'An ON unit for a condition does nothing with it, so the condition is silently ignored',
10
+ impact: 'A failure the condition reports, such as a conversion error, a division by zero, a subscript out of range or a file that would not open, reaches an ON unit that does nothing with it, so no message or log says the operation failed',
11
+ remedy: 'Handle the condition in its ON unit: record it and recover, or end the run with a message; where a condition is expected, test for it and act rather than leave the unit empty'
12
+ }
13
+ };
14
+
15
+ const IGNORED_CONDITIONS = new Set([
16
+ 'ERROR', 'ANYCONDITION', 'ANYCOND', 'CONVERSION', 'CONV',
17
+ 'ZERODIVIDE', 'ZDIV', 'FIXEDOVERFLOW', 'FOFL', 'SIZE',
18
+ 'STRINGRANGE', 'STRG', 'SUBSCRIPTRANGE', 'SUBRG', 'KEY',
19
+ 'RECORD', 'TRANSMIT', 'UNDEFINEDFILE', 'UNDF'
20
+ ]);
21
+
22
+ const BLOCK_OPENERS = new Set(['BEGIN', 'DO', 'SELECT', 'PROCEDURE', 'PACKAGE']);
23
+
24
+ // Whether a statement opens a block that a later END closes: BEGIN, DO, SELECT or a procedure,
25
+ // itself or as the unit of an IF, ELSE, WHEN, OTHERWISE or ON.
26
+ function opens(r) {
27
+ if (!r || r.status !== 'parsed' || !r.node) return false;
28
+ if (BLOCK_OPENERS.has(r.kind)) return true;
29
+ const units = r.node.units || [];
30
+ return units.length > 0 && opens(units[units.length - 1]);
31
+ }
32
+
33
+ // The statements of the block a statement at `at` opens, up to and not including its END.
34
+ function blockBody(statements, at) {
35
+ let depth = 1;
36
+ for (let k = at + 1; k < statements.length; k++) {
37
+ const r = statements[k].parsed;
38
+ if (r.kind === 'END') { if (--depth === 0) return statements.slice(at + 1, k); continue; }
39
+ if (opens(r)) depth++;
40
+ }
41
+ return statements.slice(at + 1);
42
+ }
43
+
44
+ // Doing nothing is a null unit, a block that is empty, or only jumping away.
45
+ const inert = (r) => r.kind === 'NULL' || r.kind === 'GOTO';
46
+
47
+ export function check(program) {
48
+ const findings = [];
49
+ const statements = program.statements;
50
+ statements.forEach((st, at) => {
51
+ const r = st.parsed;
52
+ if (r.status !== 'parsed' || r.kind !== 'ON' || r.node.system) return;
53
+ const names = r.node.conditions.filter((c) => IGNORED_CONDITIONS.has(c.name)).map((c) => c.name);
54
+ if (!names.length) return;
55
+ const unit = r.node.units[0];
56
+ const on = `ON ${names.join(', ')}`;
57
+ let detail = null;
58
+ if (!unit) detail = `${on} has a null unit, so the condition is raised and dropped`;
59
+ else if (unit.kind === 'GOTO') detail = `${on} only does GO TO ${unit.node.target.path.join('.')}`;
60
+ else if (unit.kind === 'BEGIN') {
61
+ const body = blockBody(statements, at).map((s) => s.parsed);
62
+ if (!body.length) detail = `${on} has an empty BEGIN block`;
63
+ else if (body.every(inert)) detail = `${on} has a BEGIN block that only does GO TO ${body.filter((b) => b.kind === 'GOTO').map((b) => b.node.target.path.join('.')).join(', ')}`;
64
+ }
65
+ if (detail) findings.push({ rule: 'pli-error-condition-ignored', path: program.path, line: st.line, detail });
66
+ });
67
+ return findings;
68
+ }
@@ -0,0 +1,130 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // PL/I rules on calls through entry variables and FETCH of a non-literal title.
3
+
4
+ export const RULES = {
5
+ 'pli-entry-variable-call': {
6
+ sev: 'info',
7
+ evidence: 'coverage',
8
+ cwe: 'CWE-470',
9
+ text: 'A CALL through an entry variable dispatches to a procedure chosen at run time, so the callee is not known statically',
10
+ },
11
+ 'pli-fetch-title-variable': {
12
+ sev: 'med',
13
+ evidence: 'construct',
14
+ cwe: 'CWE-494',
15
+ text: 'A FETCH whose TITLE is not a literal names the module to load at run time',
16
+ impact: 'FETCH loads the load module whose name the TITLE value holds when the statement runs, so whatever sets that value chooses the code the program then calls, with the program\'s authority',
17
+ remedy: 'Give FETCH a literal TITLE where the module is fixed; where it must vary, check the name against a list of the modules the program may load before the FETCH',
18
+ },
19
+ };
20
+
21
+ function walkUnits(units, visit) {
22
+ if (!Array.isArray(units)) return;
23
+ for (const u of units) {
24
+ if (!u || typeof u !== 'object') continue;
25
+ visit(u);
26
+ if (u.node && typeof u.node === 'object') walkUnits(u.node.units, visit);
27
+ }
28
+ }
29
+
30
+ function eachParsed(program, visit) {
31
+ for (const st of program.statements) {
32
+ if (!st || !st.parsed || st.parsed.status !== 'parsed') continue;
33
+ visit(st);
34
+ walkUnits(st.parsed.node && st.parsed.node.units, (u) => {
35
+ if (u && u.status === 'parsed') visit({ line: st.line, parsed: u });
36
+ });
37
+ }
38
+ }
39
+
40
+ function isEntryVariable(item) {
41
+ if (!item || !Array.isArray(item.attributes)) return false;
42
+ const names = new Set(item.attributes.map((a) => (a && a.name ? String(a.name).toUpperCase() : '')));
43
+ return names.has('ENTRY') && names.has('VARIABLE');
44
+ }
45
+
46
+ function isEntryMember(item) {
47
+ if (!item || !Array.isArray(item.attributes)) return false;
48
+ const names = new Set(item.attributes.map((a) => (a && a.name ? String(a.name).toUpperCase() : '')));
49
+ return names.has('ENTRY') && !names.has('VARIABLE');
50
+ }
51
+
52
+ function isProcedure(name, procedures) {
53
+ return procedures.has(String(name || '').toUpperCase());
54
+ }
55
+
56
+ const isLiteralTitle = (title) => !!title && title.tree && title.tree.t === 'lit';
57
+
58
+ export function check(program) {
59
+ const findings = [];
60
+ if (!program || !Array.isArray(program.statements)) return findings;
61
+
62
+ const entryVariables = new Map();
63
+ const entryMembers = new Set();
64
+ const procedures = new Set();
65
+
66
+ eachParsed(program, (st) => {
67
+ const node = st.parsed && st.parsed.node;
68
+ if (!node || typeof node !== 'object') return;
69
+ if (node.kind === 'DECLARE' && Array.isArray(node.items)) {
70
+ for (const item of node.items) {
71
+ if (!item || !item.name) continue;
72
+ const key = String(item.name).toUpperCase();
73
+ // A member of a structure is a variable whatever it declares.
74
+ if (isEntryVariable(item) || (item.level > 1 && isEntryMember(item))) entryVariables.set(key, item.name);
75
+ else if (isEntryMember(item)) entryMembers.add(key);
76
+ }
77
+ } else if (node.kind === 'PROCEDURE' && node.name) {
78
+ procedures.add(String(node.name).toUpperCase());
79
+ }
80
+ });
81
+
82
+ const assignedConstants = new Map();
83
+ eachParsed(program, (st) => {
84
+ const node = st.parsed && st.parsed.node;
85
+ if (!node || node.kind !== 'ASSIGNMENT') return;
86
+ const ref = node.value && node.value.tree;
87
+ if (!Array.isArray(node.targets) || !ref || ref.t !== 'ref' || !ref.name) return;
88
+ const valueName = String(ref.name).toUpperCase();
89
+ if (!procedures.has(valueName) && !entryMembers.has(valueName)) return;
90
+ for (const target of node.targets) {
91
+ if (!target || target.t !== 'ref' || !target.name) continue;
92
+ const key = String(target.name).toUpperCase();
93
+ if (!entryVariables.has(key)) continue;
94
+ if (!assignedConstants.has(key)) assignedConstants.set(key, new Set());
95
+ assignedConstants.get(key).add(ref.name);
96
+ }
97
+ });
98
+
99
+ eachParsed(program, (st) => {
100
+ const node = st.parsed && st.parsed.node;
101
+ if (!node || typeof node !== 'object') return;
102
+ if (node.kind === 'CALL') {
103
+ const callee = node.callee;
104
+ if (!callee || callee.t !== 'ref' || !callee.name) return;
105
+ const key = String(callee.name).toUpperCase();
106
+ if (entryVariables.has(key)) {
107
+ const varName = entryVariables.get(key);
108
+ const constants = assignedConstants.get(key);
109
+ const detail = constants && constants.size
110
+ ? `CALL ${callee.name} dispatches through an entry variable assigned ${[...constants].join(', ')}`
111
+ : `CALL ${callee.name} dispatches through an entry variable with no entry constant assigned in this program`;
112
+ findings.push({ rule: 'pli-entry-variable-call', path: program.path, line: st.line, detail });
113
+ }
114
+ } else if (node.kind === 'FETCH' && Array.isArray(node.entries)) {
115
+ for (const entry of node.entries) {
116
+ if (!entry || entry.title === null || entry.title === undefined) continue;
117
+ if (isLiteralTitle(entry.title)) continue;
118
+ const name = entry.name || 'FETCH';
119
+ findings.push({
120
+ rule: 'pli-fetch-title-variable',
121
+ path: program.path,
122
+ line: st.line,
123
+ detail: `FETCH ${name} loads a module named at run time because its TITLE is not a literal`,
124
+ });
125
+ }
126
+ }
127
+ });
128
+
129
+ return findings;
130
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // The PL/I rules: each module checks one parsed program and returns findings. A program is
3
+ // { path, statements } where each statement is as readPli gives it with `parsed`, its parseStatement
4
+ // result, attached.
5
+ import { readPli } from '../lex.mjs';
6
+ import { parseStatement } from '../statements.mjs';
7
+ import * as based from './based.mjs';
8
+ import * as entry from './entry.mjs';
9
+ import * as preprocessor from './preprocessor.mjs';
10
+ import * as conditions from './conditions.mjs';
11
+
12
+ const MODULES = [based, entry, preprocessor, conditions];
13
+
14
+ export const PLI_RULES = Object.freeze(Object.assign({}, ...MODULES.map((m) => m.RULES)));
15
+
16
+ export function programOf(path, text) {
17
+ const statements = readPli(text, { file: path }).statements.map((st) => ({ ...st, parsed: parseStatement(st) }));
18
+ return { path, statements };
19
+ }
20
+
21
+ // Findings { rule, path, line, detail } for one program, in source order.
22
+ export function checkProgram(program) {
23
+ return MODULES.flatMap((m) => m.check(program)).sort((a, b) => a.line - b.line || (a.rule < b.rule ? -1 : 1));
24
+ }
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // PL/I rules on preprocessor use.
3
+
4
+ export const RULES = {
5
+ 'pli-preprocessor-in-use': {
6
+ sev: 'info',
7
+ evidence: 'coverage',
8
+ cwe: 'CWE-1059',
9
+ text: 'A program uses PL/I preprocessor directives, so the source text is not the code that is compiled'
10
+ }
11
+ };
12
+
13
+ // Directives that change the text compiled; INCLUDE is read through and listing directives change nothing.
14
+ const DIRECTIVES = new Set(['IF', 'DO', 'DCL', 'DECLARE', 'ASSIGN', 'ACT', 'ACTIVATE', 'DEACT', 'DEACTIVATE', 'PROC', 'PROCEDURE', 'SELECT', 'REPLACE', 'GO', 'GOTO']);
15
+
16
+ function collectDirectives(node, set) {
17
+ if (!node) return;
18
+ if (node.kind === 'PREPROCESSOR' && typeof node.directive === 'string') {
19
+ const d = node.directive.toUpperCase();
20
+ if (DIRECTIVES.has(d)) set.add(d);
21
+ }
22
+ if (Array.isArray(node.units)) {
23
+ for (const u of node.units) {
24
+ if (u && u.node) collectDirectives(u.node, set);
25
+ }
26
+ }
27
+ }
28
+
29
+ export function check(program) {
30
+ const findings = [];
31
+ const directives = new Set();
32
+ let firstLine = null;
33
+
34
+ for (const st of program.statements) {
35
+ if (!st.parsed || st.parsed.status !== 'parsed') continue;
36
+ const node = st.parsed.node;
37
+ if (!node) continue;
38
+ collectDirectives(node, directives);
39
+ if (directives.size > 0 && firstLine === null) {
40
+ firstLine = st.line;
41
+ }
42
+ }
43
+
44
+ if (firstLine !== null) {
45
+ const list = [...directives].sort().join(', ');
46
+ findings.push({
47
+ rule: 'pli-preprocessor-in-use',
48
+ path: program.path,
49
+ line: firstLine,
50
+ detail: `preprocessor directives ${list} alter the source before compilation`
51
+ });
52
+ }
53
+
54
+ return findings;
55
+ }