@portll/cobolwork 0.4.0 → 0.6.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 (78) hide show
  1. package/README.md +53 -5
  2. package/bin/cobolwork.mjs +34 -9
  3. package/lib/baseline.mjs +9 -0
  4. package/lib/bms.mjs +8 -0
  5. package/lib/build.mjs +33 -8
  6. package/lib/capabilities.mjs +6 -3
  7. package/lib/consequence.mjs +17 -0
  8. package/lib/constants.mjs +60 -0
  9. package/lib/control-reuse.mjs +113 -0
  10. package/lib/control.mjs +86 -18
  11. package/lib/csd.mjs +6 -1
  12. package/lib/dataflow.mjs +343 -95
  13. package/lib/equivalence.mjs +44 -5
  14. package/lib/evidence/cli.mjs +13 -5
  15. package/lib/evidence/record.mjs +16 -6
  16. package/lib/evidence/seal.mjs +17 -0
  17. package/lib/evidence/store.mjs +30 -14
  18. package/lib/evidence/timestamp.mjs +83 -0
  19. package/lib/evidence/verify.mjs +120 -42
  20. package/lib/hlasm/asm/data.mjs +116 -0
  21. package/lib/hlasm/asm/listing.mjs +43 -0
  22. package/lib/hlasm/asm/output.mjs +45 -0
  23. package/lib/hlasm/asm/sections.mjs +159 -0
  24. package/lib/hlasm/asm/symbols.mjs +78 -0
  25. package/lib/hlasm/exec.mjs +27 -0
  26. package/lib/hlasm/expr.mjs +167 -0
  27. package/lib/hlasm/instr.mjs +48 -0
  28. package/lib/hlasm/locate.mjs +333 -0
  29. package/lib/hlasm/macro/authorization.mjs +126 -0
  30. package/lib/hlasm/macro/datasets.mjs +94 -0
  31. package/lib/hlasm/macro/io.mjs +139 -0
  32. package/lib/hlasm/macro/le.mjs +53 -0
  33. package/lib/hlasm/macro/linkage.mjs +143 -0
  34. package/lib/hlasm/macro/operator.mjs +66 -0
  35. package/lib/hlasm/macro/program.mjs +142 -0
  36. package/lib/hlasm/macro/recovery.mjs +80 -0
  37. package/lib/hlasm/macro/storage.mjs +117 -0
  38. package/lib/hlasm/macro/structured.mjs +131 -0
  39. package/lib/hlasm/model.mjs +72 -0
  40. package/lib/hlasm/operands.mjs +49 -0
  41. package/lib/hlasm/read.mjs +116 -0
  42. package/lib/hlasm.mjs +79 -0
  43. package/lib/inventory.mjs +10 -7
  44. package/lib/ironwork-ids.mjs +24 -0
  45. package/lib/ironwork.mjs +4 -1
  46. package/lib/kernel/registry.mjs +24 -17
  47. package/lib/kernel/shared-pass.mjs +138 -0
  48. package/lib/options.mjs +26 -4
  49. package/lib/policy.mjs +6 -0
  50. package/lib/precompile-cics.mjs +1 -1
  51. package/lib/revision.json +1 -1
  52. package/lib/sarif.mjs +3 -0
  53. package/lib/scan.mjs +21 -11
  54. package/lib/sets/abend.mjs +115 -13
  55. package/lib/sets/build.mjs +43 -37
  56. package/lib/sets/cics.mjs +5 -3
  57. package/lib/sets/compile.mjs +5 -3
  58. package/lib/sets/copybook.mjs +5 -3
  59. package/lib/sets/crypto.mjs +110 -0
  60. package/lib/sets/flow.mjs +32 -7
  61. package/lib/sets/hlasm.mjs +177 -0
  62. package/lib/sets/jcl.mjs +15 -1
  63. package/lib/sets/log.mjs +5 -3
  64. package/lib/sets/priv.mjs +5 -3
  65. package/lib/sets/secrets.mjs +110 -0
  66. package/lib/sets/semantics.mjs +5 -3
  67. package/lib/sets/web.mjs +104 -7
  68. package/lib/sources.mjs +12 -2
  69. package/package.json +1 -1
  70. package/rules/compliance-cobit2019.json +216 -0
  71. package/rules/compliance-dora.json +234 -0
  72. package/rules/compliance-ffiec.json +234 -0
  73. package/rules/compliance-nist80053.json +234 -0
  74. package/rules/gitleaks-mainframe.toml +61 -16
  75. package/rules/hlasm-instructions.json +2396 -0
  76. package/rules/hlasm-operations.json +451 -0
  77. package/rules/icsf-services.json +1178 -0
  78. package/schema/cobolwork.policy.schema.json +7 -0
@@ -0,0 +1,117 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Parser for z/OS virtual storage macros: STORAGE, GETMAIN, and FREEMAIN.
3
+ import { HlasmSyntax } from '../operands.mjs';
4
+
5
+ const GETMAIN_MODES = new Set(['R', 'RU', 'RC', 'VU', 'VC', 'EU', 'EC', 'LU', 'LC', 'VRC', 'VRU']);
6
+ const FREEMAIN_MODES = new Set(['R', 'RU', 'RC', 'VU', 'VC', 'V', 'EU', 'EC', 'E', 'LU', 'LC', 'L']);
7
+
8
+ function getKeyword(keywords, key) {
9
+ const val = keywords.get(key);
10
+ return val === undefined ? null : val;
11
+ }
12
+
13
+ function parseStorage(st, ops) {
14
+ const { positional, keywords } = ops;
15
+ if (positional.length < 1) {
16
+ throw new HlasmSyntax('STORAGE requires a request type (OBTAIN or RELEASE)');
17
+ }
18
+ const request = positional[0].trim().toUpperCase();
19
+ if (request !== 'OBTAIN' && request !== 'RELEASE') {
20
+ throw new HlasmSyntax(`STORAGE request must be OBTAIN or RELEASE, got ${positional[0]}`);
21
+ }
22
+ if (positional.length > 1) {
23
+ throw new HlasmSyntax(`STORAGE does not accept extra positional operands: ${positional[1]}`);
24
+ }
25
+
26
+ const allowed = new Set(['LENGTH', 'ADDR', 'INADDR', 'SP', 'BNDRY', 'CONTBDY', 'STARTBDY', 'KEY', 'CALLRKY', 'LOC', 'LINKAGE', 'RTCD', 'COND', 'CHECKZERO', 'BACK', 'FIX', 'EXECUTABLE', 'RELATED']);
27
+ for (const key of keywords.keys()) {
28
+ if (!allowed.has(key)) {
29
+ throw new HlasmSyntax(`STORAGE does not accept keyword ${key}`);
30
+ }
31
+ }
32
+
33
+ return {
34
+ kind: 'STORAGE',
35
+ request,
36
+ length: getKeyword(keywords, 'LENGTH'),
37
+ address: getKeyword(keywords, 'ADDR'),
38
+ subpool: getKeyword(keywords, 'SP'),
39
+ key: getKeyword(keywords, 'KEY'),
40
+ cond: getKeyword(keywords, 'COND'),
41
+ keywords: Object.fromEntries(keywords)
42
+ };
43
+ }
44
+
45
+ function parseGetmain(st, ops) {
46
+ const { positional, keywords } = ops;
47
+ if (positional.length < 1) {
48
+ throw new HlasmSyntax('GETMAIN requires a request type');
49
+ }
50
+ const mode = positional[0].trim().toUpperCase();
51
+ if (!GETMAIN_MODES.has(mode)) {
52
+ throw new HlasmSyntax(`GETMAIN request type must be one of ${[...GETMAIN_MODES].join(', ')}, got ${positional[0]}`);
53
+ }
54
+ if (positional.length > 1) {
55
+ throw new HlasmSyntax(`GETMAIN does not accept extra positional operands: ${positional[1]}`);
56
+ }
57
+
58
+ const allowed = new Set(['LA', 'LV', 'A', 'SP', 'BNDRY', 'CONTBDY', 'STARTBDY', 'KEY', 'LOC', 'INADDR', 'CHECKZERO', 'RELATED']);
59
+ for (const key of keywords.keys()) {
60
+ if (!allowed.has(key)) {
61
+ throw new HlasmSyntax(`GETMAIN does not accept keyword ${key}`);
62
+ }
63
+ }
64
+
65
+ const length = getKeyword(keywords, 'LV') ?? getKeyword(keywords, 'LA');
66
+ const address = getKeyword(keywords, 'A');
67
+ const subpool = getKeyword(keywords, 'SP');
68
+
69
+ return {
70
+ kind: 'GETMAIN',
71
+ mode,
72
+ length,
73
+ address,
74
+ subpool,
75
+ keywords: Object.fromEntries(keywords)
76
+ };
77
+ }
78
+
79
+ function parseFreemain(st, ops) {
80
+ const { positional, keywords } = ops;
81
+ if (positional.length < 1) {
82
+ throw new HlasmSyntax('FREEMAIN requires a request type');
83
+ }
84
+ const mode = positional[0].trim().toUpperCase();
85
+ if (!FREEMAIN_MODES.has(mode)) {
86
+ throw new HlasmSyntax(`FREEMAIN request type must be one of ${[...FREEMAIN_MODES].join(', ')}, got ${positional[0]}`);
87
+ }
88
+ if (positional.length > 1) {
89
+ throw new HlasmSyntax(`FREEMAIN does not accept extra positional operands: ${positional[1]}`);
90
+ }
91
+
92
+ const allowed = new Set(['LA', 'LV', 'A', 'SP', 'KEY', 'RELATED']);
93
+ for (const key of keywords.keys()) {
94
+ if (!allowed.has(key)) {
95
+ throw new HlasmSyntax(`FREEMAIN does not accept keyword ${key}`);
96
+ }
97
+ }
98
+
99
+ const length = getKeyword(keywords, 'LV') ?? getKeyword(keywords, 'LA');
100
+ const address = getKeyword(keywords, 'A');
101
+ const subpool = getKeyword(keywords, 'SP');
102
+
103
+ return {
104
+ kind: 'FREEMAIN',
105
+ mode,
106
+ length,
107
+ address,
108
+ subpool,
109
+ keywords: Object.fromEntries(keywords)
110
+ };
111
+ }
112
+
113
+ export const parsers = {
114
+ STORAGE: parseStorage,
115
+ GETMAIN: parseGetmain,
116
+ FREEMAIN: parseFreemain
117
+ };
@@ -0,0 +1,131 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Parsers for the HLASM Toolkit structured programming macros (IF, DO, SELECT, etc.).
3
+ import { HlasmSyntax, NAME, splitOperands } from '../operands.mjs';
4
+
5
+ const CC_MNEMONICS = new Set(['H', 'L', 'E', 'Z', 'O', 'M', 'P', 'NH', 'NL', 'NE', 'NZ', 'NO', 'NM', 'NP', 'EQ', 'GT', 'LT', 'GE', 'LE',
6
+ 'HE', 'LE', 'NHE', 'NLE']);
7
+ const CONNECTORS = new Set(['AND', 'OR', 'ANDIF', 'ORIF']);
8
+
9
+ // (Z) or (8): a condition-code mnemonic or a mask; (CLI,0(R1),EQ,C'A') or (LTR,R15,R15,NZ): an
10
+ // instruction and its operands with the condition among them.
11
+ function parseCondition(text) {
12
+ const t = text.trim();
13
+ if (!t.startsWith('(') || !t.endsWith(')')) {
14
+ if (CC_MNEMONICS.has(t.toUpperCase())) return t;
15
+ throw new HlasmSyntax(`unrecognised condition: ${t}`);
16
+ }
17
+ const parts = splitOperands(t.slice(1, -1)).map((x) => x.trim());
18
+ if (parts.length === 1) {
19
+ if (CC_MNEMONICS.has(parts[0].toUpperCase()) || /^(?:CC=)?(?:[1-9]|1[0-4])$/i.test(parts[0])) return t;
20
+ throw new HlasmSyntax(`unrecognised condition: ${t}`);
21
+ }
22
+ if (!NAME.test(parts[0])) throw new HlasmSyntax(`condition ${t} does not start with an instruction`);
23
+ if (!parts.slice(1).some((x) => CC_MNEMONICS.has(x.toUpperCase()))) throw new HlasmSyntax(`unrecognised condition: ${t} names no condition mnemonic`);
24
+ return t;
25
+ }
26
+
27
+ // Conditions joined by AND, OR, ANDIF and ORIF, each operand of the macro one condition or joiner.
28
+ function parsePredicate(positional) {
29
+ const items = positional.map((x) => x.trim()).filter(Boolean);
30
+ if (!items.length) throw new HlasmSyntax('requires a predicate');
31
+ const conditions = [];
32
+ const joins = [];
33
+ items.forEach((x, i) => {
34
+ if (i % 2) {
35
+ if (!CONNECTORS.has(x.toUpperCase())) throw new HlasmSyntax(`expected AND, OR, ANDIF or ORIF, not ${x}`);
36
+ joins.push(x.toUpperCase());
37
+ } else conditions.push(parseCondition(x));
38
+ });
39
+ if (joins.length === conditions.length) throw new HlasmSyntax('a predicate cannot end with a joiner');
40
+ return { conditions, joins };
41
+ }
42
+
43
+ function parseIF(st, ops) {
44
+ const { conditions, joins } = parsePredicate(ops.positional);
45
+ return { kind: 'IF', conditions, joins, keywords: Object.fromEntries(ops.keywords) };
46
+ }
47
+
48
+ function parseELSEIF(st, ops) {
49
+ const { conditions, joins } = parsePredicate(ops.positional);
50
+ return { kind: 'ELSEIF', conditions, joins, keywords: Object.fromEntries(ops.keywords) };
51
+ }
52
+
53
+ function parseDO(st, ops) {
54
+ const pos = ops.positional[0] || '';
55
+ const kw = ops.keywords;
56
+ let form = 'SIMPLE';
57
+ if (pos) {
58
+ const upper = pos.toUpperCase();
59
+ if (upper === 'ONCE') form = 'ONCE';
60
+ else if (upper === 'INF') form = 'INF';
61
+ else if (upper === 'BXH' || upper === 'BXLE' || upper === 'BCT' || upper === 'BCTR') form = 'COUNT';
62
+ else if (pos.startsWith('(')) form = 'COUNT';
63
+ else if (upper === 'WHILE' || upper === 'UNTIL') {
64
+ if (kw.has(upper)) form = upper;
65
+ else throw new HlasmSyntax(`unknown DO positional operand: ${pos}`);
66
+ }
67
+ else throw new HlasmSyntax(`unknown DO positional operand: ${pos}`);
68
+ }
69
+ if (kw.has('FROM')) form = 'FROM';
70
+ else if (kw.has('WHILE')) form = 'WHILE';
71
+ else if (kw.has('UNTIL')) form = 'UNTIL';
72
+ return { kind: 'DO', form, keywords: Object.fromEntries(kw) };
73
+ }
74
+
75
+ function parseDOEXIT(st, ops) {
76
+ const pos = ops.positional[0] || '';
77
+ if (pos) {
78
+ parseCondition(pos);
79
+ }
80
+ return { kind: 'DOEXIT', operands: [pos], keywords: Object.fromEntries(ops.keywords) };
81
+ }
82
+
83
+ function parseITERATE(st, ops) {
84
+ const pos = ops.positional[0] || '';
85
+ if (pos) {
86
+ if (!NAME.test(pos)) throw new HlasmSyntax(`ITERATE operand must be a label: ${pos}`);
87
+ }
88
+ return { kind: 'ITERATE', operands: [pos], keywords: Object.fromEntries(ops.keywords) };
89
+ }
90
+
91
+ function parseASMLEAVE(st, ops) {
92
+ const pos = ops.positional[0] || '';
93
+ if (pos && !NAME.test(pos)) throw new HlasmSyntax(`ASMLEAVE operand must be a label: ${pos}`);
94
+ return { kind: 'ASMLEAVE', operands: [pos], keywords: Object.fromEntries(ops.keywords) };
95
+ }
96
+
97
+ // SELECT alone, or SELECT instruction,operand,condition: CLI,0(R6),EQ compares each WHEN's values.
98
+ function parseSELECT(st, ops) {
99
+ const parts = ops.positional.map((x) => x.trim()).filter(Boolean);
100
+ if (parts.length && (parts.length < 3 || !CC_MNEMONICS.has(parts[2].toUpperCase()))) throw new HlasmSyntax(`SELECT ${parts.join(',')} is not instruction,operand,condition`);
101
+ return { kind: 'SELECT', operands: [parts.join(',')].filter(Boolean), keywords: Object.fromEntries(ops.keywords) };
102
+ }
103
+
104
+ // WHEN takes a condition, or after a comparing SELECT a parenthesised list of values: (X'20'), (1,5,13).
105
+ function parseWHEN(st, ops) {
106
+ const parts = ops.positional.map((x) => x.trim()).filter(Boolean);
107
+ if (!parts.length) throw new HlasmSyntax('WHEN needs a condition or values');
108
+ for (const p of parts) if (!(p.startsWith('(') && p.endsWith(')')) && !CONNECTORS.has(p.toUpperCase())) throw new HlasmSyntax(`WHEN operand ${p} is not in parentheses`);
109
+ return { kind: 'WHEN', operands: parts, keywords: Object.fromEntries(ops.keywords) };
110
+ }
111
+
112
+ function parseNoOperand(st, ops) {
113
+ if (ops.positional.length > 0) throw new HlasmSyntax(`${st.operation} does not accept positional operands`);
114
+ return { kind: st.operation, keywords: Object.fromEntries(ops.keywords) };
115
+ }
116
+
117
+ export const parsers = {
118
+ IF: parseIF,
119
+ ELSEIF: parseELSEIF,
120
+ ELSE: parseNoOperand,
121
+ ENDIF: parseNoOperand,
122
+ DO: parseDO,
123
+ ENDDO: parseNoOperand,
124
+ DOEXIT: parseDOEXIT,
125
+ ITERATE: parseITERATE,
126
+ ASMLEAVE: parseASMLEAVE,
127
+ SELECT: parseSELECT,
128
+ WHEN: parseWHEN,
129
+ OTHRWISE: parseNoOperand,
130
+ ENDSEL: parseNoOperand
131
+ };
@@ -0,0 +1,72 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Macro prototypes, model statements and open-code statements that hold variable symbols. A model
3
+ // statement is read as the statement it is: each variable symbol in its operands stands in for an
4
+ // ordinary symbol, or for the letter X inside a quoted string, and the result goes to the parser for
5
+ // its operation. An operation that is itself a variable symbol is decided at expansion and refused.
6
+ import { HlasmSyntax, NAME, splitOperands } from './operands.mjs';
7
+ import { parseHlasmStatement } from './read.mjs';
8
+
9
+ const VARIABLE = /^&[A-Z$#@_][A-Z0-9$#@_]*$/i;
10
+ // System variable symbols whose value has a fixed length, which is all a constant's length needs.
11
+ // https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=symbols-sysdate-system-variable-symbol
12
+ const FIXED_LENGTH = { '&SYSDATE': 8, '&SYSDATC': 8, '&SYSTIME': 5 };
13
+
14
+ // &NAME, with an optional subscript (&A(1), &A(&I+1)) and an optional period that joins it to what
15
+ // follows. && is one ampersand, not a variable symbol. Inside a quoted string a variable becomes as
16
+ // many X as its value has characters when that is fixed, and one X otherwise, counted as unsized.
17
+ function substitute(text) {
18
+ let out = '';
19
+ let quoted = false;
20
+ let n = 0;
21
+ let unsized = 0;
22
+ for (let i = 0; i < text.length; i++) {
23
+ const c = text[i];
24
+ if (c === "'") { quoted = !quoted; out += c; continue; }
25
+ if (c !== '&') { out += c; continue; }
26
+ if (text[i + 1] === '&') { out += '&&'; i++; continue; }
27
+ const m = /^&[A-Z$#@_][A-Z0-9$#@_]*/i.exec(text.slice(i));
28
+ if (!m) { out += c; continue; }
29
+ i += m[0].length;
30
+ if (text[i] === '(') {
31
+ let depth = 0;
32
+ for (; i < text.length; i++) { if (text[i] === '(') depth++; else if (text[i] === ')' && --depth === 0) { i++; break; } }
33
+ }
34
+ if (text[i] === '.') i++;
35
+ i--;
36
+ const fixed = FIXED_LENGTH[m[0].toUpperCase()];
37
+ if (quoted && !fixed) unsized++;
38
+ out += quoted ? 'X'.repeat(fixed || 1) : `VAR${++n}`;
39
+ }
40
+ return { text: out, unsized };
41
+ }
42
+
43
+ function read(kind, st) {
44
+ if (/&[A-Z$#@_]/i.test(st.operation)) throw new HlasmSyntax(`the operation ${st.operation} is decided when the macro is expanded`);
45
+ const name = st.name && !st.name.startsWith('&') && !st.name.startsWith('.') ? st.name : null;
46
+ const { text, unsized } = substitute(st.field || '');
47
+ const res = parseHlasmStatement({ ...st, name, field: text, inMacro: false, prototype: false });
48
+ // exact: no quoted string holds a variable of unknown length, so the node's lengths are the source's.
49
+ if (res.status === 'parsed') return { kind, as: res.kind, node: res.node, exact: unsized === 0 };
50
+ if (res.status === 'unbuilt') throw new HlasmSyntax(`no parser yet for ${res.kind}`);
51
+ throw new HlasmSyntax(res.reason || `${res.kind} ${res.status}`);
52
+ }
53
+
54
+ function prototype(st) {
55
+ if (st.name && !VARIABLE.test(st.name)) throw new HlasmSyntax(`a prototype's name field is a variable symbol or empty, not ${st.name}`);
56
+ if (!NAME.test(st.operation)) throw new HlasmSyntax(`${st.operation} is not a macro name`);
57
+ const positional = [];
58
+ const keywords = {};
59
+ for (const part of splitOperands(st.field || '')) {
60
+ const m = /^(&[A-Z$#@_][A-Z0-9$#@_]*)=([\s\S]*)$/i.exec(part);
61
+ if (m) keywords[m[1].toUpperCase()] = m[2];
62
+ else if (VARIABLE.test(part)) positional.push(part.toUpperCase());
63
+ else if (part !== '') throw new HlasmSyntax(`prototype operand ${part} is not a variable symbol`);
64
+ }
65
+ return { kind: 'PROTOTYPE', macro: st.operation, label: st.name || null, positional, keywords };
66
+ }
67
+
68
+ export const parsers = {
69
+ PROTOTYPE: prototype,
70
+ MODEL: (st) => read('MODEL', st),
71
+ SUBSTITUTED: (st) => read('SUBSTITUTED', st),
72
+ };
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // What every HLASM statement parser shares: the refusal it throws, and the split of an operand field
3
+ // into operands at the commas outside parentheses and quotes.
4
+
5
+ export class HlasmSyntax extends Error {
6
+ constructor(message) { super(message); this.name = 'HlasmSyntax'; }
7
+ }
8
+
9
+ export const NAME = /^[A-Z$#@_][A-Z0-9$#@_]{0,62}$/i;
10
+ export const ATTRIBUTES = 'LTSIKNDO';
11
+
12
+ // A quote after an attribute letter that stands alone, before a name, a variable symbol, * or a
13
+ // literal, is an attribute reference (L'FIELD), not the start of a string. The card reader
14
+ // (lib/bms.mjs) folds continuations by the same rule.
15
+ export const isAttributeQuote = (text, i) => i > 0 && ATTRIBUTES.includes(text[i - 1].toUpperCase())
16
+ && !/[A-Z0-9$#@_&]/i.test(text[i - 2] || '') && /[A-Z$#@_&*=]/i.test(text[i + 1] || '');
17
+
18
+ export function splitOperands(field) {
19
+ const out = [];
20
+ let depth = 0, quoted = false, start = 0;
21
+ for (let i = 0; i < field.length; i++) {
22
+ const c = field[i];
23
+ if (quoted) {
24
+ if (c === "'") { if (field[i + 1] === "'") i++; else quoted = false; }
25
+ continue;
26
+ }
27
+ if (c === "'") { if (!isAttributeQuote(field, i)) quoted = true; }
28
+ else if (c === '(') depth++;
29
+ else if (c === ')') { if (--depth < 0) throw new HlasmSyntax(`a closing parenthesis has no opening one at column ${i + 1} of the operands`); }
30
+ else if (c === ',' && depth === 0) { out.push(field.slice(start, i)); start = i + 1; }
31
+ }
32
+ if (quoted) throw new HlasmSyntax('a quoted string in the operands is not closed');
33
+ if (depth) throw new HlasmSyntax('a parenthesis in the operands is not closed');
34
+ out.push(field.slice(start));
35
+ return field === '' ? [] : out;
36
+ }
37
+
38
+ // Positional operands until the first KEY=VALUE, as a macro call is written. A literal (=F'1') is a
39
+ // positional operand, not a keyword with an empty name.
40
+ export function macroOperands(field) {
41
+ const positional = [];
42
+ const keywords = new Map();
43
+ for (const part of splitOperands(field)) {
44
+ const m = /^([A-Z$#@_][A-Z0-9$#@_]*)=([\s\S]*)$/i.exec(part);
45
+ if (m) keywords.set(m[1].toUpperCase(), m[2]);
46
+ else positional.push(part);
47
+ }
48
+ return { positional, keywords };
49
+ }
@@ -0,0 +1,116 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // HLASM source read statement by statement: the cards fold as the assembler folds them (lib/bms.mjs),
3
+ // and each statement goes to the parser for what its operation is. A machine instruction is matched
4
+ // against IBM's operand syntax, an assembler instruction and a system macro each have their own
5
+ // parser, and any other macro call is split into its operands. Conditional assembly and macro
6
+ // definitions are counted and not evaluated: nothing here expands a macro.
7
+ import { foldStatements } from '../bms.mjs';
8
+ import { HlasmSyntax, NAME, macroOperands } from './operands.mjs';
9
+ import { INSTRUCTIONS, parseInstruction, formatFamily } from './instr.mjs';
10
+ import { parsers as data } from './asm/data.mjs';
11
+ import { parsers as sections } from './asm/sections.mjs';
12
+ import { parsers as symbols } from './asm/symbols.mjs';
13
+ import { parsers as listing } from './asm/listing.mjs';
14
+ import { parsers as output } from './asm/output.mjs';
15
+ import { parsers as program } from './macro/program.mjs';
16
+ import { parsers as linkage } from './macro/linkage.mjs';
17
+ import { parsers as storage } from './macro/storage.mjs';
18
+ import { parsers as authorization } from './macro/authorization.mjs';
19
+ import { parsers as operator } from './macro/operator.mjs';
20
+ import { parsers as datasets } from './macro/datasets.mjs';
21
+ import { parsers as io } from './macro/io.mjs';
22
+ import { parsers as recovery } from './macro/recovery.mjs';
23
+ import { parsers as le } from './macro/le.mjs';
24
+ import { parsers as structured } from './macro/structured.mjs';
25
+ import { parsers as exec } from './exec.mjs';
26
+ import { parsers as model } from './model.mjs';
27
+
28
+ // The assembler instructions of HLASM V1R6 (Language Reference SC26-4940-09, chapters 5 and 9).
29
+ export const ASSEMBLER = new Set(['ACONTROL', 'ADATA', 'AINSERT', 'ALIAS', 'AMODE', 'CATTR', 'CCW', 'CCW0', 'CCW1',
30
+ 'CEJECT', 'CNOP', 'COM', 'COPY', 'CSECT', 'CXD', 'DC', 'DROP', 'DS', 'DSECT', 'DXD', 'EJECT', 'END', 'ENTRY', 'EQU',
31
+ 'EXITCTL', 'EXTRN', 'ICTL', 'ISEQ', 'LOCTR', 'LTORG', 'MNOTE', 'OPSYN', 'ORG', 'POP', 'PRINT', 'PUNCH', 'PUSH',
32
+ 'REPRO', 'RMODE', 'RSECT', 'SPACE', 'START', 'TITLE', 'USING', 'WXTRN', 'XATTR']);
33
+ export const CONDITIONAL = new Set(['ACTR', 'AEJECT', 'AGO', 'AGOB', 'AIF', 'AIFB', 'ANOP', 'AREAD', 'ASPACE',
34
+ 'GBLA', 'GBLB', 'GBLC', 'LCLA', 'LCLB', 'LCLC', 'MEXIT', 'MHELP', 'SETA', 'SETAF', 'SETB', 'SETC', 'SETCF']);
35
+ // The z/OS macros the rules read, each parsed against the keywords IBM lists for it.
36
+ export const SYSTEM_MACROS = new Set(['LINK', 'XCTL', 'LOAD', 'ATTACH', 'CALL', 'SAVE', 'RETURN', 'MODESET',
37
+ 'STORAGE', 'GETMAIN', 'FREEMAIN', 'RACROUTE', 'TESTAUTH', 'WTO', 'WTOR', 'OPEN', 'CLOSE', 'DCB', 'GET', 'PUT',
38
+ 'READ', 'WRITE', 'ESTAE', 'ESPIE', 'ABEND', 'SNAP']);
39
+ // Language Environment's prolog, epilog and mapping macros, and the HLASM Toolkit's structured
40
+ // programming macros, each with its own parser.
41
+ export const LE_MACROS = new Set(['CEEENTRY', 'CEETERM', 'CEEPPA', 'CEECAA', 'CEEDSA']);
42
+ export const TOOLKIT_MACROS = new Set(['IF', 'ELSE', 'ELSEIF', 'ENDIF', 'DO', 'ENDDO', 'DOEXIT', 'ASMLEAVE', 'ITERATE',
43
+ 'SELECT', 'WHEN', 'NEXTWHEN', 'OTHRWISE', 'ENDSEL', 'STRTSRCH', 'EXITIF', 'ORELSE', 'ENDLOOP', 'ENDSRCH',
44
+ 'CASENTRY', 'CASE', 'ENDCASE', 'ASMMREL']);
45
+ export const NAMED_MACROS = new Set([...SYSTEM_MACROS, ...LE_MACROS, ...TOOLKIT_MACROS]);
46
+
47
+ const NO_OPERANDS = new Set(['CSECT', 'DSECT', 'RSECT', 'COM', 'LOCTR', 'LTORG', 'EJECT', 'REPRO', 'CXD']);
48
+
49
+ export const PARSERS = Object.freeze({ ...data, ...sections, ...symbols, ...listing, ...output });
50
+ export const MACRO_PARSERS = Object.freeze({ ...program, ...linkage, ...storage, ...authorization, ...operator, ...datasets, ...io, ...recovery, ...le, ...structured });
51
+
52
+ // Statements other than comments, each marked inMacro when it is part of a macro definition, the
53
+ // prototype (the statement after MACRO) marked as such.
54
+ // EXEC CICS and EXEC SQL are written for their translators, with blanks between the words: their
55
+ // field is the cards' text after EXEC, columns 16 to 71 of each continuation, not the operands up
56
+ // to the first blank as an assembler statement's are.
57
+ function execField(phys, st) {
58
+ return st.lines.map((n, i) => (i === 0 ? phys[n - 1].slice(0, 71).replace(/^\S*\s+EXEC\b/i, '') : phys[n - 1].slice(15, 71))).join(' ').replace(/\s+/g, ' ').trim();
59
+ }
60
+
61
+ export function readHlasmStatements(text) {
62
+ const { statements, diags } = foldStatements(text);
63
+ const phys = String(text).replace(/\r\n?/g, '\n').split('\n');
64
+ const out = [];
65
+ let depth = 0;
66
+ let prototypeNext = false;
67
+ for (const s of statements) {
68
+ if (s.kind === 'comment') continue;
69
+ const st = { ...s, inMacro: depth > 0, prototype: false };
70
+ if (s.kind === 'statement' && s.operation === 'EXEC') st.field = execField(phys, s);
71
+ if (s.kind === 'statement') {
72
+ if (s.operation === 'MACRO') { depth++; prototypeNext = true; st.inMacro = true; }
73
+ else if (prototypeNext) { st.prototype = true; prototypeNext = false; }
74
+ if (s.operation === 'MEND') depth = Math.max(0, depth - 1);
75
+ }
76
+ out.push(st);
77
+ }
78
+ return { statements: out, diags };
79
+ }
80
+
81
+ function run(kind, parser, st, ...args) {
82
+ if (!parser) return { kind, status: 'unbuilt' };
83
+ try {
84
+ return { kind, status: 'parsed', node: parser(st, ...args) };
85
+ } catch (e) {
86
+ if (e instanceof HlasmSyntax) return { kind, status: 'unparsed', reason: e.message };
87
+ return { kind, status: 'unparsed', reason: `parser error: ${e && e.message}`, crash: true };
88
+ }
89
+ }
90
+
91
+ // { kind, status, node?, reason?, crash? }: parsed, unbuilt (no parser for the kind yet), unparsed
92
+ // (the parser refused it) or unknown (not an operation HLASM has).
93
+ export function parseHlasmStatement(st) {
94
+ if (st.kind !== 'statement') return { kind: 'UNREADABLE', status: 'unknown', reason: 'no operation field' };
95
+ const op = st.operation;
96
+ if (op === 'MACRO' || op === 'MEND') return { kind: 'MACRO DEFINITION', status: 'parsed', node: { kind: op } };
97
+ if (CONDITIONAL.has(op)) return { kind: 'CONDITIONAL', status: 'unbuilt' };
98
+ if (st.prototype) return run('PROTOTYPE', model.PROTOTYPE, st);
99
+ if (st.inMacro) return run('MODEL', model.MODEL, st);
100
+ if (/&[A-Z$#@_]/i.test(`${st.name || ''} ${op} ${st.field || ''}`.replace(/&&/g, ''))) return run('SUBSTITUTED', model.SUBSTITUTED, st);
101
+ // A lone comma is the convention for no operands with remarks after it (CSECT , remark); an
102
+ // instruction that takes no operands reads whatever follows it as remarks.
103
+ if (ASSEMBLER.has(op)) return run(op, PARSERS[op], st.field === ',' || NO_OPERANDS.has(op) ? { ...st, field: '' } : st);
104
+ const row = INSTRUCTIONS.get(op);
105
+ if (row) return run(formatFamily(row.format), parseInstruction, st, row);
106
+ if (op === 'EXEC') {
107
+ const kind = `EXEC ${(/^\S+/.exec(st.field || '') || ['?'])[0].toUpperCase()}`;
108
+ return run(kind, exec[kind], st);
109
+ }
110
+ if (!NAME.test(op)) return { kind: 'UNKNOWN', status: 'unknown', reason: `${op} is not an operation name` };
111
+ if (NAMED_MACROS.has(op)) {
112
+ const parser = MACRO_PARSERS[op];
113
+ return run(op, parser && ((s) => parser(s, macroOperands(s.field === ',' ? '' : s.field || ''))), st);
114
+ }
115
+ return run('MACRO CALL', (s) => ({ kind: 'MACRO CALL', macro: op, ...macroOperands(s.field || '') }), st);
116
+ }
package/lib/hlasm.mjs ADDED
@@ -0,0 +1,79 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // HLASM source, read and not assembled. The cards fold into statements as the assembler folds them
3
+ // (lib/bms.mjs), and each statement's operation is looked up in rules/hlasm-operations.json, the
4
+ // operations that matter to a reviewer, each with the IBM document that defines it. Macros are not
5
+ // expanded and conditional assembly is not evaluated, so an operation is reported where it is
6
+ // written: in a macro definition, or in a branch AIF jumps over, as much as anywhere else.
7
+ import { readFileSync } from 'node:fs';
8
+ import { foldStatements } from './bms.mjs';
9
+ import { parseOperands, splitOperands } from './cards.mjs';
10
+
11
+ const TABLE = JSON.parse(readFileSync(new URL('../rules/hlasm-operations.json', import.meta.url), 'utf8'));
12
+ export const OPERATIONS = new Map(TABLE.operations.map((o) => [o.name, o]));
13
+
14
+ // Operations only assembler source holds: sections, base registers, the instructions every
15
+ // program's linkage uses, and macro definitions. x86 or 6502 source written in a .asm file has none.
16
+ // The weak ones are words another assembler's source can hold (a 6502 `zproc start`), so it takes
17
+ // two different ones to count. USING counts only with the comma between its base and register: a
18
+ // comment line written `C Using a ...`, as GMP's are, reads as label C and operation USING.
19
+ const ASSEMBLER = new Set(['CSECT', 'DSECT', 'RSECT', 'USING', 'BALR', 'BASR', 'STM', 'STMG', 'LM', 'LMG', 'MVC',
20
+ 'MACRO', 'MEND', 'AMODE', 'RMODE', 'LTORG', 'CNOP']);
21
+ const WEAK = new Set(['START', 'DC', 'DS', 'BAL', 'BAS', 'DROP']);
22
+ const strong = (s) => ASSEMBLER.has(s.operation) && (s.operation !== 'USING' || s.field.includes(','));
23
+
24
+ // GNU assembler source, run through m4 as GMP's is, holds s390 instructions too, and is not HLASM.
25
+ const GNU = /^dnl\b|^\s*\.(?:text|globl|global|section|align|file)\b|\b(?:PROLOGUE|EPILOGUE|ASM_START)\(/m;
26
+
27
+ // The macros that make a file a BMS map or an IMS database or program definition, which their own
28
+ // readers take.
29
+ const BMS = new Set(['DFHMSD', 'DFHMDI', 'DFHMDF']);
30
+ const IMS = new Set(['DBD', 'DBDGEN', 'SEGM', 'PSBGEN', 'PCB', 'SENSEG']);
31
+
32
+ // MODESET changes the PSW key or the state; these operand values make it key zero or supervisor
33
+ // state, and the opposite values (KEY=NZERO, MODE=PROB) return from it. EXTKEY is the inline form,
34
+ // which sets only the PSW key.
35
+ const STATE_CHANGE = { KEY: ['ZERO'], MODE: ['SUP'], EXTKEY: ['ZERO'] };
36
+
37
+ const nameList = (field) => splitOperands(field || '').map((s) => s.trim().toUpperCase()).filter((s) => /^[A-Z$#@_][A-Z0-9$#@_]*$/.test(s));
38
+
39
+ function stateChange(field) {
40
+ let ops;
41
+ try { ops = parseOperands(field || ''); } catch { return null; }
42
+ for (const [key, values] of Object.entries(STATE_CHANGE)) {
43
+ const v = ops.keywords.get(key);
44
+ if (v && values.includes(String(v).trim().toUpperCase())) return `${key}=${String(v).trim().toUpperCase()}`;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ // What the file is, and in it: each operation the table names with where it is written; the
50
+ // sections and entry points it defines, which a CALL or a job step can name; and the names it
51
+ // leaves to the binder.
52
+ export function readHlasm(text) {
53
+ const { statements, diags } = foldStatements(text);
54
+ const ops = statements.filter((s) => s.kind === 'statement');
55
+ const has = (set) => ops.some((s) => set.has(s.operation));
56
+ const weak = new Set(ops.filter((s) => WEAK.has(s.operation)).map((s) => s.operation)).size;
57
+ const assembler = !GNU.test(text) && (ops.some(strong) || weak >= 2);
58
+ const kind = has(BMS) ? 'bms' : has(IMS) ? 'ims' : assembler ? 'hlasm' : 'unrecognised';
59
+ const out = { kind, statements: ops.length, operations: [], defines: [], external: [], diags };
60
+ if (kind !== 'hlasm') return out;
61
+
62
+ let macroDepth = 0;
63
+ for (const s of ops) {
64
+ if (s.operation === 'MACRO') { macroDepth++; continue; }
65
+ if (s.operation === 'MEND') { macroDepth = Math.max(0, macroDepth - 1); continue; }
66
+ const inMacro = macroDepth > 0;
67
+ if (!inMacro && ['CSECT', 'RSECT', 'START'].includes(s.operation) && s.name && !s.name.startsWith('&')) {
68
+ out.defines.push({ name: s.name.toUpperCase(), how: s.operation, line: s.line });
69
+ }
70
+ if (!inMacro && s.operation === 'ENTRY') for (const name of nameList(s.field)) out.defines.push({ name, how: 'ENTRY', line: s.line });
71
+ if (!inMacro && (s.operation === 'EXTRN' || s.operation === 'WXTRN')) for (const name of nameList(s.field)) out.external.push({ name, how: s.operation, line: s.line });
72
+ const op = OPERATIONS.get(s.operation);
73
+ if (!op || op.class === 'section' || op.class === 'linkage') continue;
74
+ const use = { name: op.name, class: op.class, line: s.line, inMacro, field: s.field };
75
+ if (op.name === 'MODESET') use.stateChange = stateChange(s.field);
76
+ out.operations.push(use);
77
+ }
78
+ return out;
79
+ }
package/lib/inventory.mjs CHANGED
@@ -3,12 +3,13 @@ import { detectFormat } from './parser.mjs';
3
3
  import { inScope, isCopybook, isJcl, isProgram, relPath } from './sources.mjs';
4
4
  import { SCHEMA_VERSION, TOOL_VERSION } from './version.mjs';
5
5
  import { treeFor } from './kernel/source-tree.mjs';
6
+ import { drive, loopOver } from './kernel/shared-pass.mjs';
6
7
 
7
8
  const SYSTEM_COPY = /^(?:(?:DFH|DSN|CEE|IGZ|EZA|BPX|CSQ|CMQ|DLI)[A-Z0-9$#@]{0,5}|SQLCA|SQLDA|ATTRIB)$/i;
8
9
 
9
10
  // What the repository contains and, as importantly, what could not be read: a scan over a tree
10
11
  // whose copybooks are missing has not examined what it appears to have examined.
11
- export function inventory(root, opts = {}) {
12
+ export function* inventorySteps(root, opts = {}) {
12
13
  const tree = treeFor(root, opts);
13
14
  const idx = tree.index;
14
15
  const files = tree.list().filter(inScope(opts));
@@ -28,27 +29,27 @@ export function inventory(root, opts = {}) {
28
29
  ebcdic: [],
29
30
  recursiveCopybooks: {},
30
31
  };
31
- for (const f of files) {
32
+ yield loopOver(files, (f) => {
32
33
  if (isCopybook(f)) out.summary.copybookFiles++;
33
34
  if (isJcl(f)) out.summary.jclFiles++;
34
- if (!isProgram(f)) continue;
35
+ if (!isProgram(f)) return;
35
36
  out.summary.programFiles++;
36
37
  let src;
37
38
  try {
38
39
  const s = tree.text(f);
39
40
  src = s.text;
40
41
  if (s.encoding === 'ebcdic') { out.summary.filesEbcdic++; out.ebcdic.push(tree.rel(f)); }
41
- } catch (e) { out.summary.filesUnreadable++; out.unreadable.push(`${tree.rel(f)}: ${e.code || e.name}`); continue; }
42
+ } catch (e) { out.summary.filesUnreadable++; out.unreadable.push(`${tree.rel(f)}: ${e.code || e.name}`); return; }
42
43
  // A file with a program extension and no program in it is usually a copybook named .cbl; it is
43
44
  // counted so the difference between program files and programs read is accounted for.
44
- if (!/PROCEDURE\s+DIVISION|PROGRAM-ID/i.test(src)) { out.summary.notPrograms++; continue; }
45
+ if (!/PROCEDURE\s+DIVISION|PROGRAM-ID/i.test(src)) { out.summary.notPrograms++; return; }
45
46
  let res;
46
47
  try {
47
48
  res = tree.parse(f, src);
48
49
  } catch (e) {
49
50
  out.summary.filesUnreadable++;
50
51
  out.unreadable.push(`${tree.rel(f)}: ${e.code || e.name}`);
51
- continue;
52
+ return;
52
53
  }
53
54
  out.summary.filesScanned++;
54
55
  out.summary.programs += res.programs.length;
@@ -71,8 +72,10 @@ export function inventory(root, opts = {}) {
71
72
  out.missingCopybooks[k] = (out.missingCopybooks[k] || 0) + 1;
72
73
  }
73
74
  }
74
- }
75
+ }, { guarded: false, label: 'inventory' });
75
76
  out.summary.nosrc = out.summary.filesScanned === 0;
76
77
  out.summary.coverageIncomplete = out.summary.copiesMissing > 0 || out.summary.copiesRecursive > 0 || (out.summary.copiesOverLimit || 0) > 0 || out.summary.filesUnreadable > 0 || out.summary.copiesRefused > 0 || out.summary.dirsUnreadable > 0 || out.summary.symlinks.outside > 0;
77
78
  return out;
78
79
  }
80
+
81
+ export const inventory = (root, opts = {}) => drive(inventorySteps(root, opts));
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // What cobolwork reads from ironwork, and from which ironwork release: the formats it keys on, and
3
+ // how ironwork says it ended a run. The run endings are ironwork's docs/run-endings.tsv; a test holds
4
+ // them to the copy in test/fixtures/ironwork, and CI holds that copy to ironwork's.
5
+
6
+ // Each format by the id its reader keys on, with the first ironwork release that writes all of what
7
+ // cobolwork reads from it. A reader refuses an id it does not know, so a later format is not misread.
8
+ export const IRONWORK_FORMATS = {
9
+ 'ironwork-fuzz/v1': '0.3.0',
10
+ 'ironwork-fuzz-interface/v1': '0.5.0',
11
+ 'cobolwork-evidence/v1': '0.4.0',
12
+ 'https://github.com/Portll/ironwork/blob/main/docs/evidence.md#equivalence-v1': '0.1.2',
13
+ 'run-endings': '0.4.1',
14
+ };
15
+
16
+ // The first ironwork release whose output cobolwork reads throughout; a format listed with a later
17
+ // release is read from that release on.
18
+ export const IRONWORK_MINIMUM = '0.4.1';
19
+
20
+ // How ironwork ends a run other than with its RETURN-CODE, by exit status.
21
+ export const RUN_ENDINGS = { 240: 'abend', 241: 'refused', 242: 'not-generated', 243: 'stopped', 244: 'not-run', 245: 'unreadable', 246: 'usage', 255: 'internal' };
22
+
23
+ // The abend codes that say a run reached what ironwork does not run.
24
+ export const NOT_RUN_ABENDS = ['IRONWORK', 'EXEC', 'JAVA'];