@portll/cobolwork 0.0.1 → 0.2.76

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 (79) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSING.md +93 -0
  3. package/NOTICE +9 -0
  4. package/README.md +325 -3
  5. package/THIRD-PARTY-NOTICES.md +118 -0
  6. package/bin/cobolwork.mjs +354 -0
  7. package/lib/advisories.mjs +133 -0
  8. package/lib/baseline.mjs +154 -0
  9. package/lib/bms.mjs +453 -0
  10. package/lib/build.mjs +402 -0
  11. package/lib/capabilities.mjs +79 -0
  12. package/lib/cics-commands.mjs +281 -0
  13. package/lib/compliance.mjs +81 -0
  14. package/lib/consequence.mjs +139 -0
  15. package/lib/control.mjs +1515 -0
  16. package/lib/csd.mjs +77 -0
  17. package/lib/dataflow.mjs +1506 -0
  18. package/lib/diff.mjs +344 -0
  19. package/lib/explain.mjs +145 -0
  20. package/lib/gate.mjs +383 -0
  21. package/lib/index.mjs +6 -0
  22. package/lib/inventory.mjs +79 -0
  23. package/lib/jcl.mjs +478 -0
  24. package/lib/kernel/findings.mjs +94 -0
  25. package/lib/kernel/identity.mjs +216 -0
  26. package/lib/kernel/memory.mjs +217 -0
  27. package/lib/kernel/printable.mjs +6 -0
  28. package/lib/kernel/registry.mjs +79 -0
  29. package/lib/kernel/ruleset.mjs +72 -0
  30. package/lib/kernel/source-tree.mjs +159 -0
  31. package/lib/kev.mjs +27 -0
  32. package/lib/options.mjs +512 -0
  33. package/lib/packs.mjs +148 -0
  34. package/lib/parser.mjs +2055 -0
  35. package/lib/policy.mjs +163 -0
  36. package/lib/precompile-cics.mjs +169 -0
  37. package/lib/precompile.mjs +544 -0
  38. package/lib/reach.mjs +122 -0
  39. package/lib/revision.json +1 -0
  40. package/lib/revision.mjs +89 -0
  41. package/lib/sarif.mjs +222 -0
  42. package/lib/scan.mjs +272 -0
  43. package/lib/sets/build.mjs +234 -0
  44. package/lib/sets/cics.mjs +306 -0
  45. package/lib/sets/compile.mjs +187 -0
  46. package/lib/sets/copybook.mjs +174 -0
  47. package/lib/sets/flow.mjs +487 -0
  48. package/lib/sets/hidden.mjs +216 -0
  49. package/lib/sets/jcl.mjs +440 -0
  50. package/lib/sets/log.mjs +406 -0
  51. package/lib/sets/opaque.mjs +102 -0
  52. package/lib/sets/priv.mjs +322 -0
  53. package/lib/sets/recon.mjs +267 -0
  54. package/lib/sets/vendor.mjs +117 -0
  55. package/lib/sets/web.mjs +327 -0
  56. package/lib/site.mjs +164 -0
  57. package/lib/sources.mjs +156 -0
  58. package/lib/tui/app.mjs +325 -0
  59. package/lib/tui/keys.mjs +39 -0
  60. package/lib/tui/model.mjs +96 -0
  61. package/lib/tui/run.mjs +38 -0
  62. package/lib/tui/screen.mjs +59 -0
  63. package/lib/tui/terminal.mjs +46 -0
  64. package/lib/utilities.mjs +296 -0
  65. package/lib/version.mjs +15 -0
  66. package/lib/words.mjs +318 -0
  67. package/package.json +45 -6
  68. package/rules/advisories.json +264 -0
  69. package/rules/compliance-dora.json +2151 -0
  70. package/rules/compliance-ffiec.json +2134 -0
  71. package/rules/compliance-nist80053.json +2134 -0
  72. package/rules/gitleaks-mainframe.toml +57 -0
  73. package/rules/kev-ids.json +1729 -0
  74. package/rules/packs/broadcom.json +124 -0
  75. package/rules/packs/connectdirect.json +116 -0
  76. package/rules/packs/controlm.json +114 -0
  77. package/rules/system-layouts.json +28 -0
  78. package/schema/cobolwork-coverage.schema.json +65 -0
  79. package/schema/cobolwork.policy.schema.json +54 -0
package/lib/bms.mjs ADDED
@@ -0,0 +1,453 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // BMS maps, read the way the assembler reads them: DFHMSD opens a mapset, DFHMDI a map in it,
3
+ // DFHMDF a field in that, and DFHMSD TYPE=FINAL closes the set.
4
+ //
5
+ // COSGN00 DFHMSD LANG=COBOL,MODE=INOUT,TYPE=&&SYSPARM
6
+ // COSGN0A DFHMDI SIZE=(24,80)
7
+ // PASSWD DFHMDF POS=(20,43),LENGTH=8,ATTRB=(DRK,FSET,UNPROT)
8
+ //
9
+ // A field's attributes are enforced by the 3270 emulator, not by CICS: a modified client writes a
10
+ // PROT field and reads a DRK one, which makes them what a hidden field is to a web form. So ATTRB
11
+ // is kept as the set of keywords the map declares, and each named field is tied to the names a
12
+ // COBOL program uses for it (PASSWDI, PASSWDL, PASSWDA and the rest, in COSGN0AI and COSGN0AO),
13
+ // because a program never uses the DFHMDF label itself. A DFHMDF with no label is a constant on
14
+ // the screen and has no names; it is kept, with name null, so that it cannot pass for a data field.
15
+ //
16
+ // This reads text and opens nothing; the caller reads the file through the source tree, so
17
+ // containment stays where it is enforced.
18
+ import { parseOperands, splitOperands } from './jcl.mjs';
19
+
20
+ // Column 72 marks a continuation and 73 to 80 are the sequence field, so a statement ends at 71.
21
+ const END_COLUMN = 71;
22
+ const CONTINUE_COLUMN = 16;
23
+
24
+ const MACROS = new Set(['DFHMSD', 'DFHMDI', 'DFHMDF']);
25
+
26
+ // Assembler instructions a map source carries around its macros. Anything else in the operation
27
+ // field is a statement this reader does not know, which is a diagnostic rather than a silent skip.
28
+ const ASSEMBLER = new Set(['TITLE', 'PRINT', 'EJECT', 'SPACE', 'PUNCH', 'ICTL', 'ISEQ', 'COPY', 'END']);
29
+
30
+ const PROTECTION = ['ASKIP', 'PROT', 'UNPROT'];
31
+ const INTENSITY = ['BRT', 'NORM', 'DRK'];
32
+ const ATTRB_KEYWORDS = new Set([...PROTECTION, ...INTENSITY, 'NUM', 'DET', 'IC', 'FSET']);
33
+
34
+ // Each extended attribute DSATTS can give a subfield of its own, with the letter that subfield
35
+ // takes, in the order generated copybooks lay them out. The order of C, P, H, V, U and M is read
36
+ // from copybooks CICS generated; TRANSP last is an assumption, since no copybook seen carries it.
37
+ // https://www.ibm.com/docs/en/cics-ts/6.x?topic=output-setting-display-characteristics
38
+ const EXTENDED = [['COLOR', 'C'], ['PS', 'P'], ['HILIGHT', 'H'], ['VALIDN', 'V'], ['OUTLINE', 'U'], ['SOSI', 'M'], ['TRANSP', 'T']];
39
+ const LETTER = new Map(EXTENDED);
40
+ const EXTATT_YES = ['COLOR', 'HILIGHT', 'PS', 'VALIDN'];
41
+
42
+ const upper = (v) => (v == null ? null : String(v).toUpperCase());
43
+ const integer = (v) => (/^\d+$/.test(v ?? '') ? Number(v) : null);
44
+
45
+ // 'Tom''s' is Tom's: a quote or an ampersand inside a literal is written twice.
46
+ const unquote = (v) => {
47
+ const m = /^'([\s\S]*)'$/.exec(v ?? '');
48
+ return m ? m[1].replace(/''/g, "'").replace(/&&/g, '&') : (v ?? null);
49
+ };
50
+
51
+ // (ASKIP,NORM) and ASKIP are both lists; the parentheses are optional around one entry.
52
+ const sublist = (v) => {
53
+ if (v == null) return null;
54
+ const inner = /^\(([\s\S]*)\)$/.exec(v);
55
+ return (inner ? splitOperands(inner[1]) : [v]).map((x) => x.trim()).filter(Boolean);
56
+ };
57
+
58
+ // Folds physical lines into statements. A map continues a statement in two ways, often both in one
59
+ // file. An operand that fills the line to column 71 carries on in column 16 of the next line, which
60
+ // is how a long INITIAL literal is split. A comma followed by a blank ends the line's operands and
61
+ // leaves the rest of it as a remark, which is how a map written one operand per line reads. An
62
+ // operand written after that blank, or starting anywhere but column 16, is a remark to the
63
+ // assembler, so it is reported rather than read.
64
+ // https://www.ibm.com/docs/en/SSLTBW_2.1.0/com.ibm.zos.v2r1.asma400/cl.htm
65
+ // https://www.ibm.com/docs/en/SSLTBW_2.1.0/com.ibm.zos.v2r1.asma400/altwmac.htm
66
+ export function foldStatements(src) {
67
+ const phys = String(src).replace(/\r\n?/g, '\n').split('\n');
68
+ const statements = [];
69
+ const diags = [];
70
+ let open = null; // { st, reading, quoted }: the statement the next line continues
71
+ let ended = false; // the assembler reads nothing after END
72
+
73
+ const remark = (text, line, why) => {
74
+ if (/^[A-Z][A-Z0-9]*=/i.test(text)) diags.push({ sev: 'warn', line, text: `'${text}' ${why}, so the assembler reads it as a remark, not an operand` });
75
+ };
76
+
77
+ const read = (o, text, from, line) => {
78
+ for (let i = from; i < text.length; i++) {
79
+ const c = text[i];
80
+ if (o.quoted) { o.st.field += c; if (c === "'") o.quoted = false; continue; }
81
+ if (c === "'") { o.quoted = true; o.st.field += c; continue; }
82
+ if (c === ' ' || c === '\t') {
83
+ o.reading = o.st.field.endsWith(',');
84
+ remark(text.slice(i).trim(), line, o.reading ? 'follows a comma and a blank' : 'follows the last operand');
85
+ return;
86
+ }
87
+ o.st.field += c;
88
+ }
89
+ o.reading = o.quoted || text.length === END_COLUMN;
90
+ };
91
+
92
+ const finish = (o) => {
93
+ if (o.quoted) diags.push({ sev: 'error', line: o.st.line, text: 'a quoted string is not closed' });
94
+ statements.push(o.st);
95
+ };
96
+
97
+ for (let i = 0; i < phys.length && !ended; i++) {
98
+ const line = i + 1;
99
+ const raw = phys[i];
100
+ if (raw.length > 80) diags.push({ sev: 'warn', line, text: 'line is longer than 80 columns' });
101
+ const text = raw.slice(0, END_COLUMN);
102
+ const continues = raw.length > END_COLUMN && raw[END_COLUMN] !== ' ';
103
+
104
+ if (open) {
105
+ if (/\S/.test(text.slice(0, CONTINUE_COLUMN - 1))) {
106
+ // A comment banner that runs into column 72 is common and harmless; a statement is not.
107
+ if (open.st.kind === 'statement') diags.push({ sev: 'error', line, text: `a continuation must leave columns 1 to ${CONTINUE_COLUMN - 1} blank, so this line was read as a new statement` });
108
+ finish(open);
109
+ open = null;
110
+ } else {
111
+ open.st.endLine = line;
112
+ open.st.lines.push(line);
113
+ if (open.reading) {
114
+ if (!open.quoted && /^\s|^$/.test(text.slice(CONTINUE_COLUMN - 1))) {
115
+ remark(text.trim(), line, `does not start in column ${CONTINUE_COLUMN}`);
116
+ open.reading = false;
117
+ } else read(open, text, CONTINUE_COLUMN - 1, line);
118
+ }
119
+ if (!continues) { finish(open); open = null; }
120
+ continue;
121
+ }
122
+ }
123
+
124
+ if (!text.trim()) continue;
125
+ if (/^\*|^\.\*/.test(text)) {
126
+ const st = { kind: 'comment', line, endLine: line, lines: [line], text: text.slice(1) };
127
+ if (continues) open = { st, reading: false, quoted: false };
128
+ else statements.push(st);
129
+ continue;
130
+ }
131
+ const m = /^(\S*)[ \t]+(\S+)/.exec(text);
132
+ if (!m) {
133
+ diags.push({ sev: 'error', line, text: 'no operation field' });
134
+ statements.push({ kind: 'unreadable', line, endLine: line, lines: [line], text });
135
+ continue;
136
+ }
137
+ const st = { kind: 'statement', name: m[1] || null, operation: m[2].toUpperCase(), field: '', line, endLine: line, lines: [line] };
138
+ const o = { st, reading: true, quoted: false };
139
+ let at = m[0].length;
140
+ while (text[at] === ' ' || text[at] === '\t') at++;
141
+ // An operation with nothing after it on its line takes its operands from the next line.
142
+ if (at < text.length) read(o, text, at, line);
143
+ if (continues) open = o;
144
+ else finish(o);
145
+ if (st.operation === 'END') ended = true;
146
+ }
147
+
148
+ if (open) {
149
+ if (open.st.kind === 'statement') diags.push({ sev: 'error', line: open.st.line, text: 'statement ends on a continuation with nothing continuing it' });
150
+ finish(open);
151
+ }
152
+ return { statements, diags };
153
+ }
154
+
155
+ // The attributes BMS gives a field. IBM: "If no ATTRB parameters are specified, ASKIP and NORM are
156
+ // assumed. If any parameter is specified, UNPROT and NORM are assumed for that field unless
157
+ // overridden by a specified parameter." So ATTRB=(BRT) is an unprotected field, which is not what
158
+ // it looks like. https://www.ibm.com/docs/en/cics-ts/6.x?topic=macros-dfhmdf
159
+ function effectiveAttrb(declared) {
160
+ if (!declared) return new Set(['ASKIP', 'NORM']);
161
+ const set = new Set(declared);
162
+ if (!PROTECTION.some((k) => set.has(k))) set.add('UNPROT');
163
+ if (!INTENSITY.some((k) => set.has(k))) set.add('NORM');
164
+ return set;
165
+ }
166
+
167
+ // POS=(line,column) counts from one; POS=n is a displacement from zero into the map, so it needs
168
+ // the map's width to become a line and a column.
169
+ function position(v, size) {
170
+ const list = sublist(v);
171
+ if (!list) return null;
172
+ if (list.length === 2 && list.every((x) => integer(x) !== null)) return { line: Number(list[0]), column: Number(list[1]) };
173
+ const offset = list.length === 1 ? integer(list[0]) : null;
174
+ if (offset === null) return undefined;
175
+ if (!size) return { line: null, column: null, offset };
176
+ return { line: Math.floor(offset / size.columns) + 1, column: (offset % size.columns) + 1, offset };
177
+ }
178
+
179
+ const lineOrColumn = (v) => (v == null ? null : integer(v) ?? upper(v));
180
+
181
+ // DSATTS names the extended attributes that get a subfield in the symbolic map, and EXTATT=YES is
182
+ // the older spelling of four of them. Given together they add up: every CBSA map gives both, and
183
+ // CBSA's generated copybooks carry V, which only EXTATT names, beside U and M, which only DSATTS
184
+ // does. A map that states either is taken to replace what its mapset stated; IBM does not say, and
185
+ // no copybook seen settles it. https://www.ibm.com/docs/en/cics-ts/6.x?topic=macros-dfhmsd
186
+ function extendedAttributes(keywords) {
187
+ const extatt = upper(keywords.get('EXTATT'));
188
+ if (!keywords.has('DSATTS') && extatt === null) return null;
189
+ const types = new Set((sublist(keywords.get('DSATTS')) || []).map(upper));
190
+ if (extatt === 'YES') for (const t of EXTATT_YES) types.add(t);
191
+ return EXTENDED.map(([t]) => t).filter((t) => types.has(t));
192
+ }
193
+
194
+ // The records a COBOL program copies for a map: mapI for input and mapO for output, as MODE asks,
195
+ // OUT when it does not say. SUFFIX leaves them alone: IBM describes one symbolic map shared by every
196
+ // suffixed version of a mapset, the suffix choosing among physical maps only.
197
+ // https://www.ibm.com/docs/en/cics-ts/6.x?topic=map-device-dependent-maps
198
+ function symbolicMap(name, mapset, keywords) {
199
+ const mode = mapset.options.mode || 'OUT';
200
+ const base = upper(name);
201
+ return {
202
+ input: base && (mode === 'IN' || mode === 'INOUT') ? `${base}I` : null,
203
+ output: base && (mode === 'OUT' || mode === 'INOUT') ? `${base}O` : null,
204
+ attributes: extendedAttributes(keywords) ?? extendedAttributes(mapset.operands) ?? [],
205
+ // OCCURS fields in the mapset's earlier maps, which the numbering of output arrays continues.
206
+ occursBefore: mapset.maps.reduce((n, m) => n + m.fields.filter((f) => f.occurs > 1).length, 0),
207
+ };
208
+ }
209
+
210
+ const where = (st) => ({ line: st.line, endLine: st.endLine, lines: st.lines });
211
+
212
+ function newMapset(st, keywords) {
213
+ return {
214
+ name: st?.name ?? null,
215
+ ...(st ? where(st) : { line: null, endLine: null, lines: [] }),
216
+ // As written. TYPE=MAP builds the screen and TYPE=DSECT the copybook; &SYSPARM, or &&SYSPARM,
217
+ // leaves the choice to the assembly, which is usually run once for each.
218
+ options: {
219
+ type: upper(keywords.get('TYPE')),
220
+ mode: upper(keywords.get('MODE')),
221
+ lang: upper(keywords.get('LANG')),
222
+ suffix: keywords.get('SUFFIX') ?? null,
223
+ storage: upper(keywords.get('STORAGE')),
224
+ base: keywords.get('BASE') ?? null,
225
+ tioapfx: upper(keywords.get('TIOAPFX')),
226
+ extatt: upper(keywords.get('EXTATT')),
227
+ dsatts: sublist(keywords.get('DSATTS'))?.map(upper) ?? null,
228
+ mapatts: sublist(keywords.get('MAPATTS'))?.map(upper) ?? null,
229
+ },
230
+ operands: keywords,
231
+ maps: [],
232
+ finalLine: null,
233
+ };
234
+ }
235
+
236
+ function newMap(st, keywords, mapset, diags) {
237
+ const size = sublist(keywords.get('SIZE'));
238
+ const [lines, columns] = (size || []).map(integer);
239
+ if (size && !(size.length === 2 && lines && columns)) diags.push({ sev: 'warn', line: st?.line ?? null, text: `SIZE=${keywords.get('SIZE')} is not (lines,columns)` });
240
+ return {
241
+ name: st?.name ?? null,
242
+ mapset: mapset.name,
243
+ ...(st ? where(st) : { line: null, endLine: null, lines: [] }),
244
+ options: {
245
+ size: lines && columns && size.length === 2 ? { lines, columns } : null,
246
+ line: lineOrColumn(keywords.get('LINE')),
247
+ column: lineOrColumn(keywords.get('COLUMN')),
248
+ fields: upper(keywords.get('FIELDS')),
249
+ justify: sublist(keywords.get('JUSTIFY'))?.map(upper) ?? null,
250
+ extatt: upper(keywords.get('EXTATT')),
251
+ dsatts: sublist(keywords.get('DSATTS'))?.map(upper) ?? null,
252
+ mapatts: sublist(keywords.get('MAPATTS'))?.map(upper) ?? null,
253
+ },
254
+ operands: keywords,
255
+ symbolic: symbolicMap(st?.name, mapset, keywords),
256
+ fields: [],
257
+ };
258
+ }
259
+
260
+ // { mapsets: [{ name, line, options: { type, mode, lang, suffix, storage, ... }, operands, maps,
261
+ // finalLine }], statements, diags }, a map being { name, mapset, line, options: { size, line,
262
+ // column, fields, ... }, operands, symbolic: { input, output, attributes, occursBefore }, fields }
263
+ // and a field { name, map, mapset, line, pos: { line, column[, offset] }, length, attrb, effective,
264
+ // picin, picout, initial, occurs, grpname, operands }. `operands` holds every keyword as written;
265
+ // `attrb` is the ATTRB operand as a set, null when it is not coded, and `effective` the set BMS
266
+ // applies.
267
+ export function parseBms(text) {
268
+ const { statements, diags } = foldStatements(text);
269
+ const mapsets = [];
270
+ let mapset = null;
271
+ let map = null;
272
+ let group = null; // { name, leader }: the GRPNAME group the previous field belonged to
273
+ let groupsEnded = new Set();
274
+
275
+ const implicitMapset = (st, what) => {
276
+ diags.push({ sev: 'error', line: st.line, text: `${what} before any DFHMSD` });
277
+ mapset = newMapset(null, new Map());
278
+ mapsets.push(mapset);
279
+ };
280
+
281
+ for (const st of statements) {
282
+ if (st.kind !== 'statement') continue;
283
+ const { positional, keywords } = parseOperands(st.field);
284
+ if (MACROS.has(st.operation) && positional.length) {
285
+ diags.push({ sev: 'warn', line: st.line, text: `${st.operation} takes keyword operands only, so '${positional.join(',')}' was not read` });
286
+ }
287
+
288
+ switch (st.operation) {
289
+ case 'DFHMSD': {
290
+ if (upper(keywords.get('TYPE')) === 'FINAL') {
291
+ if (mapset) mapset.finalLine = st.line;
292
+ else diags.push({ sev: 'warn', line: st.line, text: 'DFHMSD TYPE=FINAL with no mapset open' });
293
+ mapset = null;
294
+ map = null;
295
+ break;
296
+ }
297
+ if (mapset?.line) diags.push({ sev: 'warn', line: st.line, text: `mapset ${mapset.name} has no DFHMSD TYPE=FINAL before the next DFHMSD` });
298
+ if (!st.name) diags.push({ sev: 'error', line: st.line, text: 'DFHMSD has no mapset name' });
299
+ mapset = newMapset(st, keywords);
300
+ mapsets.push(mapset);
301
+ map = null;
302
+ break;
303
+ }
304
+
305
+ case 'DFHMDI': {
306
+ if (!mapset) implicitMapset(st, 'DFHMDI');
307
+ if (!st.name) diags.push({ sev: 'error', line: st.line, text: 'DFHMDI has no map name, so its fields have no symbolic names' });
308
+ map = newMap(st, keywords, mapset, diags);
309
+ mapset.maps.push(map);
310
+ group = null;
311
+ groupsEnded = new Set();
312
+ break;
313
+ }
314
+
315
+ case 'DFHMDF': {
316
+ if (!map) {
317
+ if (!mapset) implicitMapset(st, 'DFHMDF');
318
+ diags.push({ sev: 'error', line: st.line, text: 'DFHMDF before any DFHMDI' });
319
+ map = newMap(null, new Map(), mapset, diags);
320
+ mapset.maps.push(map);
321
+ }
322
+ const coded = keywords.get('ATTRB');
323
+ const attrb = coded ? new Set(sublist(coded).map(upper)) : null;
324
+ for (const k of attrb || []) {
325
+ if (!ATTRB_KEYWORDS.has(k)) diags.push({ sev: 'warn', line: st.line, text: `ATTRB keyword '${k}' is not one BMS defines` });
326
+ }
327
+ const pos = position(keywords.get('POS'), map.options.size);
328
+ if (pos === undefined) diags.push({ sev: 'warn', line: st.line, text: `POS=${keywords.get('POS')} is neither (line,column) nor a number` });
329
+ else if (pos && pos.line === null) diags.push({ sev: 'warn', line: st.line, text: `POS=${pos.offset} is an offset, and the map gives no SIZE to turn it into a line and a column` });
330
+
331
+ const field = {
332
+ name: st.name,
333
+ map: map.name,
334
+ mapset: map.mapset,
335
+ ...where(st),
336
+ pos: pos ?? null,
337
+ length: integer(keywords.get('LENGTH')),
338
+ attrb,
339
+ effective: effectiveAttrb(attrb),
340
+ picin: unquote(keywords.get('PICIN')),
341
+ picout: unquote(keywords.get('PICOUT')),
342
+ initial: unquote(keywords.get('INITIAL')),
343
+ occurs: integer(keywords.get('OCCURS')),
344
+ grpname: keywords.get('GRPNAME') ?? null,
345
+ operands: keywords,
346
+ };
347
+
348
+ // A group has one attribute byte, so the first field's ATTRB applies to all of it, and its
349
+ // fields must follow one another. IBM, DFHMDF: "The ATTRB operand specified on the first
350
+ // field of the group applies to all of the fields within the group."
351
+ // https://www.ibm.com/docs/en/cics-ts/6.x?topic=macros-dfhmdf
352
+ // https://www.ibm.com/docs/en/cics-ts/6.x?topic=map-using-complex-fields
353
+ const g = upper(field.grpname);
354
+ if (g && group?.name === g) {
355
+ if (attrb) diags.push({ sev: 'warn', line: st.line, text: `ATTRB on a later field of group ${g} is not used; the first field's applies to the group` });
356
+ field.effective = new Set(group.leader.effective);
357
+ } else {
358
+ if (group) groupsEnded.add(group.name);
359
+ if (g && groupsEnded.has(g)) diags.push({ sev: 'error', line: st.line, text: `GRPNAME=${g} resumes after other fields; a group's fields must follow one another` });
360
+ group = g ? { name: g, leader: field } : null;
361
+ }
362
+ if (g && !st.name) diags.push({ sev: 'error', line: st.line, text: `every field of group ${g} needs a name` });
363
+ if (g && field.occurs !== null) diags.push({ sev: 'error', line: st.line, text: 'OCCURS and GRPNAME cannot both be given' });
364
+ if (st.name && field.length === 0) diags.push({ sev: 'warn', line: st.line, text: `field ${st.name} has LENGTH=0, which BMS allows only on a field with no name` });
365
+ if (st.name && map.fields.some((f) => upper(f.name) === upper(st.name))) diags.push({ sev: 'warn', line: st.line, text: `field ${st.name} appears twice in map ${map.name}` });
366
+ map.fields.push(field);
367
+ break;
368
+ }
369
+
370
+ default:
371
+ if (st.operation === 'COPY') diags.push({ sev: 'warn', line: st.line, text: `COPY ${st.field} was not resolved, so its statements were not read` });
372
+ else if (!ASSEMBLER.has(st.operation)) diags.push({ sev: 'info', line: st.line, text: `operation '${st.operation}' is not a BMS macro, so the statement was not read` });
373
+ break;
374
+ }
375
+ }
376
+
377
+ // A mapset this reader opened for a stray statement has already been reported.
378
+ if (mapset?.line) diags.push({ sev: 'warn', line: mapset.line, text: `mapset ${mapset.name} has no DFHMSD TYPE=FINAL` });
379
+ return { mapsets, statements, diags };
380
+ }
381
+
382
+ // The COBOL names the symbolic map gives a field, each the field name and one letter: L the length
383
+ // of what was keyed, F its flag, A the attribute byte, I the data received, O the data sent, and a
384
+ // letter for each extended attribute. What a program reads from the terminal arrives in NAMEI.
385
+ // Returns [{ name, suffix, record[, occurs][, group] }] in record order, empty for a field with no
386
+ // name. https://www.ibm.com/docs/en/cics-ts/5.6.0?topic=example-symbolic-input-map
387
+ //
388
+ // With MODE=INOUT, A redefines F in the input record and the extended attributes precede O in the
389
+ // output record, as CardDemo's copybooks show. MODE=OUT puts A in the output record, as IBM's
390
+ // examples do; MODE=IN is taken to keep it beside F, which no copybook seen shows.
391
+ //
392
+ // OCCURS follows a copybook CICS TS 6.2 generated: the input side is an array NAMED of L, F and I,
393
+ // and the output side an array DFHMSn of A, the extended attributes and O. IBM's example names the
394
+ // output array TELNOG, after its field TELNO, which is not what the macros generate now. That
395
+ // copybook numbers two arrays in one map; that the count runs on through the mapset rather than
396
+ // starting again at each map is an assumption.
397
+ //
398
+ // In a GRPNAME group only the first field has L, F, A and the extended attributes, the others I and
399
+ // O, and the group name is an item in each record. That rests on IBM's output example alone; no
400
+ // generated group was available to check it.
401
+ // https://www.ibm.com/docs/en/cics-ts/6.x?topic=map-using-complex-fields
402
+ export function symbolicNames(map, field) {
403
+ if (!field.name || !map.symbolic) return [];
404
+ const { input, output, attributes, occursBefore } = map.symbolic;
405
+ const base = upper(field.name);
406
+ const extended = attributes.map((t) => LETTER.get(t));
407
+ const group = upper(field.grpname);
408
+ const leader = !group || map.fields.find((f) => upper(f.grpname) === group)?.line === field.line;
409
+ const occurs = field.occurs > 1 ? field.occurs : null;
410
+
411
+ const names = [];
412
+ const lay = (record, head, suffixes) => {
413
+ if (!record) return;
414
+ if (head) names.push({ ...head, record });
415
+ for (const suffix of suffixes) names.push({ name: base + suffix, suffix, record, ...(occurs ? { occurs } : {}) });
416
+ };
417
+ if (occurs) {
418
+ const n = occursBefore + map.fields.filter((f) => f.occurs > 1 && f.line <= field.line).length;
419
+ lay(input, { name: `${base}D`, suffix: 'D', occurs }, ['L', 'F', 'I']);
420
+ lay(output, { name: `DFHMS${n}`, suffix: null, occurs }, ['A', ...extended, 'O']);
421
+ } else if (!leader) {
422
+ lay(input, null, ['I']);
423
+ lay(output, null, ['O']);
424
+ } else {
425
+ const head = group ? { name: group, suffix: null, group } : null;
426
+ if (input) {
427
+ lay(input, head, ['L', 'F', 'A', 'I']);
428
+ lay(output, head, [...extended, 'O']);
429
+ } else lay(output, head, ['A', ...extended, 'O']);
430
+ }
431
+ return names;
432
+ }
433
+
434
+ // From a COBOL name back to what it stands for. One name can stand for several fields - TRNNAMEI
435
+ // is in every CardDemo map - so each hit carries the record it sits in, which is how a program
436
+ // qualifies it (USERIDI OF COSGN0AI). A record name itself is a hit with field null.
437
+ // Map(NAME -> [{ mapset, map, field, suffix, record[, occurs][, group] }])
438
+ export function symbolIndex(...parsed) {
439
+ const index = new Map();
440
+ const note = (name, hit) => {
441
+ if (!index.has(name)) index.set(name, []);
442
+ index.get(name).push(hit);
443
+ };
444
+ for (const bms of parsed) for (const ms of bms.mapsets) for (const map of ms.maps) {
445
+ const at = { mapset: ms.name, map: map.name };
446
+ if (map.symbolic.input) note(map.symbolic.input, { ...at, field: null, suffix: 'I', record: map.symbolic.input });
447
+ if (map.symbolic.output) note(map.symbolic.output, { ...at, field: null, suffix: 'O', record: map.symbolic.output });
448
+ for (const f of map.fields) {
449
+ for (const { name, ...hit } of symbolicNames(map, f)) note(name, { ...at, field: hit.group ? null : f.name, ...hit });
450
+ }
451
+ }
452
+ return index;
453
+ }