@portll/cobolwork 0.5.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 (163) hide show
  1. package/README.md +89 -26
  2. package/STABILITY.md +76 -0
  3. package/bin/cobolwork.mjs +56 -14
  4. package/lib/baseline.mjs +9 -0
  5. package/lib/bms.mjs +21 -7
  6. package/lib/build.mjs +133 -53
  7. package/lib/capabilities.mjs +13 -11
  8. package/lib/cics-commands.mjs +501 -9
  9. package/lib/compliance.mjs +11 -0
  10. package/lib/consequence.mjs +17 -0
  11. package/lib/control-reuse.mjs +113 -0
  12. package/lib/control-workers.mjs +161 -0
  13. package/lib/control.mjs +77 -4
  14. package/lib/dataflow.mjs +402 -152
  15. package/lib/db2/cursor.mjs +15 -0
  16. package/lib/db2/read.mjs +170 -0
  17. package/lib/db2/rules.mjs +77 -0
  18. package/lib/db2/stmt/alter.mjs +473 -0
  19. package/lib/db2/stmt/grant.mjs +125 -0
  20. package/lib/db2/stmt/index.mjs +141 -0
  21. package/lib/db2/stmt/misc.mjs +325 -0
  22. package/lib/db2/stmt/routine.mjs +564 -0
  23. package/lib/db2/stmt/storage.mjs +146 -0
  24. package/lib/db2/stmt/table.mjs +540 -0
  25. package/lib/db2/stmt/view.mjs +146 -0
  26. package/lib/diff.mjs +17 -5
  27. package/lib/equivalence.mjs +44 -5
  28. package/lib/evidence/cli.mjs +26 -7
  29. package/lib/evidence/record.mjs +16 -6
  30. package/lib/evidence/seal.mjs +17 -0
  31. package/lib/evidence/store.mjs +44 -15
  32. package/lib/evidence/timestamp.mjs +83 -0
  33. package/lib/evidence/verify.mjs +120 -42
  34. package/lib/exec-reading.mjs +51 -0
  35. package/lib/execution.mjs +3 -2
  36. package/lib/explain.mjs +2 -0
  37. package/lib/exploitability.mjs +11 -2
  38. package/lib/hlasm/asm/data.mjs +128 -0
  39. package/lib/hlasm/asm/listing.mjs +43 -0
  40. package/lib/hlasm/asm/output.mjs +45 -0
  41. package/lib/hlasm/asm/sections.mjs +161 -0
  42. package/lib/hlasm/asm/symbols.mjs +78 -0
  43. package/lib/hlasm/exec.mjs +27 -0
  44. package/lib/hlasm/expr.mjs +167 -0
  45. package/lib/hlasm/instr.mjs +73 -0
  46. package/lib/hlasm/locate.mjs +333 -0
  47. package/lib/hlasm/macro/authorization.mjs +146 -0
  48. package/lib/hlasm/macro/datasets.mjs +107 -0
  49. package/lib/hlasm/macro/io.mjs +162 -0
  50. package/lib/hlasm/macro/le.mjs +53 -0
  51. package/lib/hlasm/macro/linkage.mjs +143 -0
  52. package/lib/hlasm/macro/operator.mjs +77 -0
  53. package/lib/hlasm/macro/program.mjs +184 -0
  54. package/lib/hlasm/macro/recovery.mjs +85 -0
  55. package/lib/hlasm/macro/storage.mjs +167 -0
  56. package/lib/hlasm/macro/structured.mjs +131 -0
  57. package/lib/hlasm/model.mjs +97 -0
  58. package/lib/hlasm/mvs38.mjs +47 -0
  59. package/lib/hlasm/operands.mjs +59 -0
  60. package/lib/hlasm/optable.mjs +61 -0
  61. package/lib/hlasm/read.mjs +130 -0
  62. package/lib/hlasm.mjs +44 -7
  63. package/lib/ims/dli.mjs +37 -0
  64. package/lib/ims/macro/dbd.mjs +299 -0
  65. package/lib/ims/macro/psb.mjs +286 -0
  66. package/lib/ims/model.mjs +149 -0
  67. package/lib/ims/operands.mjs +23 -0
  68. package/lib/ims/read.mjs +37 -0
  69. package/lib/ims/rules.mjs +135 -0
  70. package/lib/inventory.mjs +10 -7
  71. package/lib/ironwork-ids.mjs +24 -0
  72. package/lib/ironwork.mjs +20 -13
  73. package/lib/kernel/pds-archive.mjs +256 -0
  74. package/lib/kernel/registry.mjs +32 -23
  75. package/lib/kernel/shared-pass.mjs +163 -0
  76. package/lib/kernel/source-tree.mjs +104 -36
  77. package/lib/kernel/version-key.mjs +17 -0
  78. package/lib/layout.mjs +26 -31
  79. package/lib/options.mjs +26 -4
  80. package/lib/parser.mjs +136 -17
  81. package/lib/pli/cursor.mjs +15 -0
  82. package/lib/pli/expr.mjs +101 -0
  83. package/lib/pli/include.mjs +82 -0
  84. package/lib/pli/layout.mjs +125 -0
  85. package/lib/pli/lex.mjs +198 -0
  86. package/lib/pli/program.mjs +280 -0
  87. package/lib/pli/rules/based.mjs +95 -0
  88. package/lib/pli/rules/conditions.mjs +68 -0
  89. package/lib/pli/rules/entry.mjs +130 -0
  90. package/lib/pli/rules/index.mjs +24 -0
  91. package/lib/pli/rules/preprocessor.mjs +55 -0
  92. package/lib/pli/statements.mjs +130 -0
  93. package/lib/pli/stmt/alloc.mjs +45 -0
  94. package/lib/pli/stmt/assignment.mjs +56 -0
  95. package/lib/pli/stmt/call.mjs +104 -0
  96. package/lib/pli/stmt/conditions.mjs +94 -0
  97. package/lib/pli/stmt/control.mjs +219 -0
  98. package/lib/pli/stmt/declare.mjs +149 -0
  99. package/lib/pli/stmt/exec.mjs +55 -0
  100. package/lib/pli/stmt/io.mjs +239 -0
  101. package/lib/pli/stmt/misc.mjs +4 -0
  102. package/lib/pli/stmt/preprocessor.mjs +242 -0
  103. package/lib/pli/stmt/procedure.mjs +258 -0
  104. package/lib/pli/stmt/stream.mjs +283 -0
  105. package/lib/pli/storage.mjs +129 -0
  106. package/lib/policy.mjs +6 -0
  107. package/lib/precompile-check.mjs +124 -0
  108. package/lib/precompile-cics.mjs +8 -4
  109. package/lib/reach.mjs +11 -2
  110. package/lib/revision.json +1 -1
  111. package/lib/sarif.mjs +44 -3
  112. package/lib/scan.mjs +28 -12
  113. package/lib/sets/abend.mjs +82 -8
  114. package/lib/sets/build.mjs +43 -37
  115. package/lib/sets/cics.mjs +20 -38
  116. package/lib/sets/compile.mjs +46 -18
  117. package/lib/sets/copybook.mjs +5 -3
  118. package/lib/sets/crypto.mjs +5 -3
  119. package/lib/sets/ddl.mjs +36 -0
  120. package/lib/sets/flow.mjs +32 -8
  121. package/lib/sets/hidden.mjs +5 -3
  122. package/lib/sets/hlasm.mjs +124 -8
  123. package/lib/sets/ims.mjs +139 -0
  124. package/lib/sets/log.mjs +11 -9
  125. package/lib/sets/opaque.mjs +27 -7
  126. package/lib/sets/pli.mjs +40 -0
  127. package/lib/sets/priv.mjs +5 -3
  128. package/lib/sets/recon.mjs +5 -3
  129. package/lib/sets/secrets.mjs +112 -0
  130. package/lib/sets/semantics.mjs +8 -3
  131. package/lib/sets/web.mjs +39 -23
  132. package/lib/site.mjs +10 -0
  133. package/lib/sources.mjs +80 -20
  134. package/lib/statement-cursor.mjs +67 -0
  135. package/lib/verify.mjs +3 -2
  136. package/lib/version.mjs +6 -0
  137. package/package.json +3 -2
  138. package/rules/compliance-cobit2019.json +520 -5
  139. package/rules/compliance-dora.json +509 -5
  140. package/rules/compliance-ffiec.json +505 -1
  141. package/rules/compliance-nist80053.json +557 -1
  142. package/rules/gitleaks-mainframe.toml +46 -15
  143. package/rules/hlasm-instructions.json +2396 -0
  144. package/rules/hlasm-optables.json +8024 -0
  145. package/schema/cobolwork-baseline.schema.json +36 -0
  146. package/schema/cobolwork-build-provenance.schema.json +187 -0
  147. package/schema/cobolwork-build.schema.json +382 -0
  148. package/schema/cobolwork-capabilities.schema.json +239 -0
  149. package/schema/cobolwork-diff.schema.json +217 -0
  150. package/schema/cobolwork-evidence.schema.json +161 -0
  151. package/schema/cobolwork-execution.schema.json +53 -0
  152. package/schema/cobolwork-explain.schema.json +360 -0
  153. package/schema/cobolwork-finding.schema.json +465 -0
  154. package/schema/cobolwork-flow.schema.json +465 -0
  155. package/schema/cobolwork-gate.schema.json +211 -0
  156. package/schema/cobolwork-inventory.schema.json +206 -0
  157. package/schema/cobolwork-parse.schema.json +105 -0
  158. package/schema/cobolwork-reach.schema.json +74 -0
  159. package/schema/cobolwork-report.schema.json +559 -0
  160. package/schema/cobolwork-witness.schema.json +107 -0
  161. package/schema/cobolwork.baseline.schema.json +101 -0
  162. package/schema/cobolwork.policy.schema.json +7 -0
  163. package/schema/cobolwork.site.schema.json +116 -0
@@ -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
+ }
@@ -0,0 +1,130 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Which kind each PL/I statement is, and its parse by that kind's parser. PL/I reserves no words, so
3
+ // a statement is an assignment whenever it reads as references followed by '=', and a keyword
4
+ // statement only otherwise: `IF = 1;` assigns to a variable named IF.
5
+ import { cursor, PliSyntax } from './cursor.mjs';
6
+ import { parsers as declare } from './stmt/declare.mjs';
7
+ import { parsers as procedure } from './stmt/procedure.mjs';
8
+ import { parsers as control } from './stmt/control.mjs';
9
+ import { parsers as call } from './stmt/call.mjs';
10
+ import { parsers as assignment } from './stmt/assignment.mjs';
11
+ import { parsers as io } from './stmt/io.mjs';
12
+ import { parsers as stream } from './stmt/stream.mjs';
13
+ import { parsers as conditions } from './stmt/conditions.mjs';
14
+ import { parsers as alloc } from './stmt/alloc.mjs';
15
+ import { parsers as exec } from './stmt/exec.mjs';
16
+ import { parsers as preprocessor } from './stmt/preprocessor.mjs';
17
+ import { parsers as misc } from './stmt/misc.mjs';
18
+
19
+ export const PARSERS = Object.freeze({ ...declare, ...procedure, ...control, ...call, ...assignment, ...io, ...stream, ...conditions, ...alloc, ...exec, ...preprocessor, ...misc });
20
+
21
+ const KEYWORDS = new Map(Object.entries({
22
+ DECLARE: 'DECLARE', DCL: 'DECLARE', DEFAULT: 'DEFAULT', DFT: 'DEFAULT', DEFINE: 'DEFINE',
23
+ PROCEDURE: 'PROCEDURE', PROC: 'PROCEDURE', ENTRY: 'ENTRY', PACKAGE: 'PACKAGE', BEGIN: 'BEGIN', END: 'END',
24
+ DO: 'DO', IF: 'IF', ELSE: 'ELSE', SELECT: 'SELECT', WHEN: 'WHEN', OTHERWISE: 'OTHERWISE', OTHER: 'OTHERWISE',
25
+ LEAVE: 'LEAVE', ITERATE: 'ITERATE', GO: 'GOTO', GOTO: 'GOTO', RETURN: 'RETURN', STOP: 'STOP', EXIT: 'EXIT',
26
+ CALL: 'CALL', FETCH: 'FETCH', RELEASE: 'RELEASE',
27
+ OPEN: 'OPEN', CLOSE: 'CLOSE', READ: 'READ', WRITE: 'WRITE', REWRITE: 'REWRITE', DELETE: 'DELETE', LOCATE: 'LOCATE',
28
+ UNLOCK: 'UNLOCK', GET: 'GET', PUT: 'PUT', DISPLAY: 'DISPLAY', FORMAT: 'FORMAT', FLUSH: 'FLUSH',
29
+ ON: 'ON', SIGNAL: 'SIGNAL', REVERT: 'REVERT', RESIGNAL: 'RESIGNAL',
30
+ ALLOCATE: 'ALLOCATE', ALLOC: 'ALLOCATE', FREE: 'FREE',
31
+ EXEC: 'EXEC', EXECUTE: 'EXEC',
32
+ WAIT: 'WAIT', DELAY: 'DELAY', ATTACH: 'ATTACH', DETACH: 'DETACH', ASSERT: 'ASSERT', QUALIFY: 'QUALIFY',
33
+ }));
34
+
35
+ const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '|=', '&=', '||=', '**=']);
36
+
37
+ // Where a reference starting at `i` ends: name, then any mix of (subscripts), '.' name and
38
+ // '->' or '=>' name. -1 when `i` does not start one.
39
+ function referenceEnd(toks, i) {
40
+ if (!toks[i] || toks[i].t !== 'word') return -1;
41
+ let k = i + 1;
42
+ for (;;) {
43
+ const t = toks[k];
44
+ if (t && t.t === 'op' && t.v === '(') {
45
+ let depth = 0;
46
+ for (; k < toks.length; k++) {
47
+ if (toks[k].t !== 'op') continue;
48
+ if (toks[k].v === '(') depth++;
49
+ else if (toks[k].v === ')' && --depth === 0) break;
50
+ }
51
+ if (k >= toks.length) return -1;
52
+ k++;
53
+ continue;
54
+ }
55
+ if (t && t.t === 'op' && (t.v === '.' || t.v === '->' || t.v === '=>') && toks[k + 1] && toks[k + 1].t === 'word') { k += 2; continue; }
56
+ return k;
57
+ }
58
+ }
59
+
60
+ export function isAssignment(toks) {
61
+ let i = 0;
62
+ for (;;) {
63
+ const end = referenceEnd(toks, i);
64
+ if (end < 0) return false;
65
+ const t = toks[end];
66
+ if (t && t.t === 'op' && ASSIGN_OPS.has(t.v)) return true;
67
+ if (t && t.t === 'op' && t.v === ',') { i = end + 1; continue; }
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function thenFollows(toks) {
73
+ let depth = 0;
74
+ for (const t of toks.slice(1)) {
75
+ if (t.t === 'op' && t.v === '(') depth++;
76
+ else if (t.t === 'op' && t.v === ')') depth--;
77
+ else if (depth === 0 && t.t === 'word' && t.u === 'THEN') return true;
78
+ }
79
+ return false;
80
+ }
81
+
82
+ export function classify(stmt) {
83
+ const toks = stmt.toks;
84
+ if (!toks.length) return 'NULL';
85
+ if (toks[0].t === 'op' && toks[0].v === '%') return 'PREPROCESSOR';
86
+ // PL/I reserves no words, so IF (X) = 0 could assign to an array IF; a THEN at depth 0 makes it
87
+ // the IF statement, as the compiler reads it.
88
+ if (toks[0].t === 'word' && toks[0].u === 'IF' && thenFollows(toks)) return 'IF';
89
+ if (isAssignment(toks)) return 'ASSIGNMENT';
90
+ // An %INCLUDE member holding the middle of a structure starts at a level number.
91
+ if (toks[0].t === 'num' && /^\d+$/.test(toks[0].v) && toks[1] && toks[1].t === 'word') return 'DECLARE_FRAGMENT';
92
+ if (toks[0].t !== 'word') return 'UNKNOWN';
93
+ return KEYWORDS.get(toks[0].u) || 'UNKNOWN';
94
+ }
95
+
96
+ // A statement built from tokens inside another, such as the unit of an IF, ON or WHEN, with its own
97
+ // prefixes taken off as the splitter takes them off a statement of its own.
98
+ export function subStatement(parent, toks) {
99
+ const labels = [];
100
+ let i = 0;
101
+ while (toks[i] && toks[i].t === 'word' && toks[i + 1] && toks[i + 1].t === 'op' && toks[i + 1].v === ':') { labels.push({ name: toks[i].u, line: toks[i].line }); i += 2; }
102
+ const first = toks[i] || toks[0];
103
+ return { labels, conditions: [], toks: toks.slice(i), line: first ? first.line : parent.line, endLine: parent.endLine, ...(parent.file ? { file: parent.file } : {}) };
104
+ }
105
+
106
+ const RANK = { parsed: 0, unbuilt: 1, unparsed: 2, unknown: 3 };
107
+
108
+ // { kind, status, node?, reason? }. status is parsed (every token accounted for, and every nested
109
+ // unit parsed), unbuilt (the kind has no parser yet), unparsed (the parser refused it or left tokens)
110
+ // or unknown (no kind fits). A parser receives a cursor on the statement's tokens after its prefixes
111
+ // and returns a node; a nested statement it parses through ctx.parseStatement goes in node.units.
112
+ export function parseStatement(stmt) {
113
+ const kind = classify(stmt);
114
+ if (kind === 'NULL') return { kind, status: 'parsed', node: { kind } };
115
+ if (kind === 'UNKNOWN') return { kind, status: 'unknown', reason: 'no statement kind fits' };
116
+ const parser = PARSERS[kind];
117
+ if (!parser) return { kind, status: 'unbuilt' };
118
+ const c = cursor(stmt.toks);
119
+ let node;
120
+ try {
121
+ node = parser(c, stmt, { parseStatement, subStatement });
122
+ } catch (e) {
123
+ if (e instanceof PliSyntax) return { kind, status: 'unparsed', reason: e.message, line: e.line ?? stmt.line };
124
+ return { kind, status: 'unparsed', reason: `parser error: ${e && e.message}`, crash: true, line: stmt.line };
125
+ }
126
+ if (!c.done()) return { kind, status: 'unparsed', reason: `tokens left at '${c.peek().v ?? c.peek().t}'`, line: c.peek().line, node };
127
+ let status = 'parsed';
128
+ for (const u of (node && node.units) || []) if (RANK[u.status] > RANK[status]) status = u.status;
129
+ return status === 'parsed' ? { kind, status, node } : { kind, status, node, reason: 'a nested statement is not parsed' };
130
+ }
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // PL/I ALLOCATE and FREE: each names one or more controlled or based variables, an ALLOCATE with
3
+ // the area it takes storage IN, the locator it SETs and any attributes it gives a controlled one.
4
+
5
+ import { parseReference } from '../expr.mjs';
6
+
7
+ const ALLOCATE_ATTRIBUTES = new Set(['CHARACTER', 'CHAR', 'BIT', 'GRAPHIC', 'WIDECHAR', 'AREA', 'INITIAL', 'INIT', 'DIMENSION', 'DIM']);
8
+
9
+ function readParenthesised(c, read) {
10
+ c.expectOp('(');
11
+ const value = read(c);
12
+ c.expectOp(')');
13
+ return value;
14
+ }
15
+
16
+ function readAllocateItem(c) {
17
+ const level = c.peek() && c.peek().t === 'num' ? Number(c.next().v) : null;
18
+ const item = { level, variable: parseReference(c), in: null, set: null, attributes: [] };
19
+ while (!c.done() && !c.isOp(',')) {
20
+ if (c.isWord('IN')) { c.next(); item.in = readParenthesised(c, parseReference); }
21
+ else if (c.isWord('SET')) { c.next(); item.set = readParenthesised(c, parseReference); }
22
+ else if (c.isWord([...ALLOCATE_ATTRIBUTES])) {
23
+ const name = c.next().u;
24
+ item.attributes.push({ name, args: c.isOp('(') ? c.group() : null });
25
+ } else c.fail('IN, SET, an attribute or a comma');
26
+ }
27
+ return item;
28
+ }
29
+
30
+ function readFreeItem(c) {
31
+ const item = { variable: parseReference(c), in: null };
32
+ if (c.isWord('IN')) { c.next(); item.in = readParenthesised(c, parseReference); }
33
+ return item;
34
+ }
35
+
36
+ function readList(c, read) {
37
+ const items = [read(c)];
38
+ while (c.isOp(',')) { c.next(); items.push(read(c)); }
39
+ return items;
40
+ }
41
+
42
+ export const parsers = {
43
+ ALLOCATE: (c) => { c.expectWord(['ALLOCATE', 'ALLOC']); return { kind: 'ALLOCATE', items: readList(c, readAllocateItem) }; },
44
+ FREE: (c) => { c.expectWord('FREE'); return { kind: 'FREE', items: readList(c, readFreeItem) }; },
45
+ };
@@ -0,0 +1,56 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // PL/I assignment statements.
3
+
4
+ import { parseExpression, parseReference } from '../expr.mjs';
5
+
6
+ const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '|=', '&=', '||=', '**=']);
7
+
8
+ function parseAssignment(c, stmt, ctx) {
9
+ const targets = [];
10
+ for (;;) {
11
+ const ref = parseReference(c);
12
+ targets.push(ref);
13
+ if (!c.isOp(',')) break;
14
+ c.next();
15
+ }
16
+
17
+ const opTok = c.peek();
18
+ if (!opTok || opTok.t !== 'op' || !ASSIGN_OPS.has(opTok.v)) {
19
+ c.fail('an assignment operator');
20
+ }
21
+ const op = c.next().v;
22
+
23
+ const value = parseExpression(c);
24
+
25
+ let byName = false;
26
+ if (c.isOp(',')) {
27
+ c.next();
28
+ if (c.isWord('BY')) {
29
+ c.next();
30
+ if (c.isWord('NAME')) {
31
+ c.next();
32
+ byName = true;
33
+ } else {
34
+ c.fail("'NAME'");
35
+ }
36
+ } else {
37
+ c.fail("'BY'");
38
+ }
39
+ }
40
+
41
+ if (!c.done()) {
42
+ c.fail('the end of the statement');
43
+ }
44
+
45
+ return {
46
+ kind: 'ASSIGNMENT',
47
+ targets,
48
+ op,
49
+ value,
50
+ byName,
51
+ };
52
+ }
53
+
54
+ export const parsers = {
55
+ ASSIGNMENT: parseAssignment,
56
+ };
@@ -0,0 +1,104 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // PL/I CALL, FETCH and RELEASE statements.
3
+
4
+ import { cursor } from '../cursor.mjs';
5
+ import { parseExpression, parseReference } from '../expr.mjs';
6
+
7
+ function parseCall(c, stmt, ctx) {
8
+ c.expectWord('CALL');
9
+ const callee = parseReference(c);
10
+ const args = callee.args.length ? callee.args[callee.args.length - 1] : [];
11
+ const options = [];
12
+
13
+ while (!c.done()) {
14
+ if (c.isWord('TASK')) {
15
+ c.next();
16
+ const inner = c.group();
17
+ const s = cursor(inner);
18
+ const e = parseExpression(s);
19
+ if (!s.done()) s.fail('the end of the argument');
20
+ options.push({ name: 'TASK', args: [e] });
21
+ } else if (c.isWord('EVENT')) {
22
+ c.next();
23
+ const inner = c.group();
24
+ const s = cursor(inner);
25
+ const e = parseExpression(s);
26
+ if (!s.done()) s.fail('the end of the argument');
27
+ options.push({ name: 'EVENT', args: [e] });
28
+ } else if (c.isWord('PRIORITY')) {
29
+ c.next();
30
+ const inner = c.group();
31
+ const s = cursor(inner);
32
+ const e = parseExpression(s);
33
+ if (!s.done()) s.fail('the end of the argument');
34
+ options.push({ name: 'PRIORITY', args: [e] });
35
+ } else {
36
+ c.fail('end of statement or TASK, EVENT, or PRIORITY');
37
+ }
38
+ }
39
+
40
+ return { kind: 'CALL', callee, name: callee.path.join('.'), args, options };
41
+ }
42
+
43
+ function parseFetch(c, stmt, ctx) {
44
+ c.expectWord('FETCH');
45
+ const entries = [];
46
+
47
+ while (true) {
48
+ const name = c.expectWord(undefined).u;
49
+ let set = null;
50
+ let title = null;
51
+
52
+ // SET and TITLE may both appear, in either order.
53
+ while (c.isWord(['SET', 'TITLE']) && c.isOp('(', 1)) {
54
+ const option = c.next().u;
55
+ const s = cursor(c.group());
56
+ if (option === 'SET') set = parseReference(s);
57
+ else title = parseExpression(s);
58
+ if (!s.done()) s.fail('the end of the argument');
59
+ }
60
+
61
+ entries.push({ name, set, title });
62
+
63
+ if (c.isOp(',')) {
64
+ c.next();
65
+ } else {
66
+ break;
67
+ }
68
+ }
69
+
70
+ if (!c.done()) c.fail('end of statement');
71
+
72
+ return { kind: 'FETCH', entries };
73
+ }
74
+
75
+ function parseRelease(c, stmt, ctx) {
76
+ c.expectWord('RELEASE');
77
+ const entries = [];
78
+
79
+ if (c.isOp('*')) {
80
+ c.next();
81
+ entries.push('*');
82
+ } else {
83
+ while (true) {
84
+ const name = c.expectWord(undefined).u;
85
+ entries.push(name);
86
+
87
+ if (c.isOp(',')) {
88
+ c.next();
89
+ } else {
90
+ break;
91
+ }
92
+ }
93
+ }
94
+
95
+ if (!c.done()) c.fail('end of statement');
96
+
97
+ return { kind: 'RELEASE', entries };
98
+ }
99
+
100
+ export const parsers = {
101
+ CALL: parseCall,
102
+ FETCH: parseFetch,
103
+ RELEASE: parseRelease,
104
+ };
@@ -0,0 +1,94 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Parsers for ON, SIGNAL, REVERT and RESIGNAL statements.
3
+
4
+ const CONDITION_NAMES = new Set([
5
+ 'ENDFILE', 'ENDPAGE', 'KEY', 'RECORD', 'TRANSMIT', 'UNDEFINEDFILE', 'UNDF',
6
+ 'NAME', 'CONDITION', 'COND',
7
+ 'ERROR', 'FINISH', 'CONVERSION', 'CONV', 'FIXEDOVERFLOW', 'FOFL',
8
+ 'OVERFLOW', 'OFL', 'UNDERFLOW', 'UFL', 'ZERODIVIDE', 'ZDIV',
9
+ 'SIZE', 'STRINGRANGE', 'STRG', 'STRINGSIZE', 'STRZ',
10
+ 'SUBSCRIPTRANGE', 'SUBRG', 'AREA', 'STORAGE', 'ATTENTION', 'ATTN',
11
+ 'INVALIDOP', 'ANYCONDITION', 'ANYCOND'
12
+ ]);
13
+
14
+ function parseCondition(c) {
15
+ const nameTok = c.next();
16
+ if (!nameTok || nameTok.t !== 'word' || !CONDITION_NAMES.has(nameTok.u)) {
17
+ c.fail('a condition name');
18
+ }
19
+ const name = nameTok.u;
20
+ let arg = null;
21
+ if (c.isOp('(')) {
22
+ const inner = c.group();
23
+ arg = inner;
24
+ }
25
+ return { name, arg };
26
+ }
27
+
28
+ function parseConditions(c) {
29
+ const conditions = [];
30
+ const first = parseCondition(c);
31
+ conditions.push(first);
32
+
33
+ while (c.isOp(',')) {
34
+ c.next();
35
+ const next = parseCondition(c);
36
+ conditions.push(next);
37
+ }
38
+
39
+ return conditions;
40
+ }
41
+
42
+ function parseOn(c, stmt, ctx) {
43
+ c.expectWord('ON');
44
+
45
+ const conditions = parseConditions(c);
46
+
47
+ let snap = false;
48
+ if (c.isWord('SNAP')) {
49
+ c.next();
50
+ snap = true;
51
+ }
52
+
53
+ let system = false;
54
+ if (c.isWord('SYSTEM')) {
55
+ c.next();
56
+ system = true;
57
+ }
58
+
59
+ const units = [];
60
+ if (!system) {
61
+ const rest = c.drain();
62
+ if (rest.length > 0) {
63
+ const sub = ctx.subStatement(stmt, rest);
64
+ const result = ctx.parseStatement(sub);
65
+ units.push(result);
66
+ }
67
+ }
68
+
69
+ return { kind: 'ON', conditions, snap, system, units };
70
+ }
71
+
72
+ function parseSignal(c, stmt, ctx) {
73
+ c.expectWord('SIGNAL');
74
+ const condition = parseCondition(c);
75
+ return { kind: 'SIGNAL', condition, units: [] };
76
+ }
77
+
78
+ function parseRevert(c, stmt, ctx) {
79
+ c.expectWord('REVERT');
80
+ const condition = parseCondition(c);
81
+ return { kind: 'REVERT', condition, units: [] };
82
+ }
83
+
84
+ function parseResignal(c, stmt, ctx) {
85
+ c.expectWord('RESIGNAL');
86
+ return { kind: 'RESIGNAL', units: [] };
87
+ }
88
+
89
+ export const parsers = {
90
+ ON: parseOn,
91
+ SIGNAL: parseSignal,
92
+ REVERT: parseRevert,
93
+ RESIGNAL: parseResignal
94
+ };