@portll/cobolwork 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -18
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +30 -7
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +108 -53
- package/lib/capabilities.mjs +9 -10
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +13 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +52 -3
- package/lib/dataflow.mjs +176 -125
- package/lib/db2/cursor.mjs +15 -0
- package/lib/db2/read.mjs +170 -0
- package/lib/db2/rules.mjs +77 -0
- package/lib/db2/stmt/alter.mjs +473 -0
- package/lib/db2/stmt/grant.mjs +125 -0
- package/lib/db2/stmt/index.mjs +141 -0
- package/lib/db2/stmt/misc.mjs +325 -0
- package/lib/db2/stmt/routine.mjs +564 -0
- package/lib/db2/stmt/storage.mjs +146 -0
- package/lib/db2/stmt/table.mjs +540 -0
- package/lib/db2/stmt/view.mjs +146 -0
- package/lib/diff.mjs +17 -5
- package/lib/evidence/cli.mjs +14 -3
- package/lib/evidence/record.mjs +1 -1
- package/lib/evidence/store.mjs +44 -31
- package/lib/exec-reading.mjs +51 -0
- package/lib/execution.mjs +3 -2
- package/lib/explain.mjs +2 -0
- package/lib/exploitability.mjs +11 -2
- package/lib/hlasm/asm/data.mjs +13 -1
- package/lib/hlasm/asm/sections.mjs +3 -1
- package/lib/hlasm/instr.mjs +25 -0
- package/lib/hlasm/macro/authorization.mjs +24 -4
- package/lib/hlasm/macro/datasets.mjs +30 -17
- package/lib/hlasm/macro/io.mjs +68 -45
- package/lib/hlasm/macro/le.mjs +2 -2
- package/lib/hlasm/macro/operator.mjs +32 -21
- package/lib/hlasm/macro/program.mjs +126 -84
- package/lib/hlasm/macro/recovery.mjs +10 -5
- package/lib/hlasm/macro/storage.mjs +64 -14
- package/lib/hlasm/macro/structured.mjs +1 -1
- package/lib/hlasm/model.mjs +35 -10
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +10 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +22 -8
- package/lib/hlasm.mjs +44 -7
- package/lib/ims/dli.mjs +37 -0
- package/lib/ims/macro/dbd.mjs +299 -0
- package/lib/ims/macro/psb.mjs +286 -0
- package/lib/ims/model.mjs +149 -0
- package/lib/ims/operands.mjs +23 -0
- package/lib/ims/read.mjs +37 -0
- package/lib/ims/rules.mjs +135 -0
- package/lib/ironwork.mjs +17 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +14 -8
- package/lib/kernel/shared-pass.mjs +34 -9
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- package/lib/parser.mjs +136 -17
- package/lib/pli/cursor.mjs +15 -0
- package/lib/pli/expr.mjs +101 -0
- package/lib/pli/include.mjs +82 -0
- package/lib/pli/layout.mjs +125 -0
- package/lib/pli/lex.mjs +198 -0
- package/lib/pli/program.mjs +280 -0
- package/lib/pli/rules/based.mjs +95 -0
- package/lib/pli/rules/conditions.mjs +68 -0
- package/lib/pli/rules/entry.mjs +130 -0
- package/lib/pli/rules/index.mjs +24 -0
- package/lib/pli/rules/preprocessor.mjs +55 -0
- package/lib/pli/statements.mjs +130 -0
- package/lib/pli/stmt/alloc.mjs +45 -0
- package/lib/pli/stmt/assignment.mjs +56 -0
- package/lib/pli/stmt/call.mjs +104 -0
- package/lib/pli/stmt/conditions.mjs +94 -0
- package/lib/pli/stmt/control.mjs +219 -0
- package/lib/pli/stmt/declare.mjs +149 -0
- package/lib/pli/stmt/exec.mjs +55 -0
- package/lib/pli/stmt/io.mjs +239 -0
- package/lib/pli/stmt/misc.mjs +4 -0
- package/lib/pli/stmt/preprocessor.mjs +242 -0
- package/lib/pli/stmt/procedure.mjs +258 -0
- package/lib/pli/stmt/stream.mjs +283 -0
- package/lib/pli/storage.mjs +129 -0
- package/lib/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +7 -3
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +41 -3
- package/lib/scan.mjs +7 -1
- package/lib/sets/abend.mjs +16 -6
- package/lib/sets/cics.mjs +15 -35
- package/lib/sets/compile.mjs +41 -15
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +18 -1
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +72 -12
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +6 -6
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +5 -3
- package/lib/sets/semantics.mjs +3 -0
- package/lib/sets/web.mjs +36 -22
- package/lib/site.mjs +10 -0
- package/lib/sources.mjs +80 -20
- package/lib/statement-cursor.mjs +67 -0
- package/lib/verify.mjs +3 -2
- package/lib/version.mjs +6 -0
- package/package.json +3 -2
- package/rules/compliance-cobit2019.json +432 -1
- package/rules/compliance-dora.json +414 -1
- package/rules/compliance-ffiec.json +414 -1
- package/rules/compliance-nist80053.json +466 -1
- package/rules/hlasm-optables.json +8024 -0
- package/schema/cobolwork-baseline.schema.json +36 -0
- package/schema/cobolwork-build-provenance.schema.json +187 -0
- package/schema/cobolwork-build.schema.json +382 -0
- package/schema/cobolwork-capabilities.schema.json +239 -0
- package/schema/cobolwork-diff.schema.json +217 -0
- package/schema/cobolwork-evidence.schema.json +161 -0
- package/schema/cobolwork-execution.schema.json +53 -0
- package/schema/cobolwork-explain.schema.json +360 -0
- package/schema/cobolwork-finding.schema.json +465 -0
- package/schema/cobolwork-flow.schema.json +465 -0
- package/schema/cobolwork-gate.schema.json +211 -0
- package/schema/cobolwork-inventory.schema.json +206 -0
- package/schema/cobolwork-parse.schema.json +105 -0
- package/schema/cobolwork-reach.schema.json +74 -0
- package/schema/cobolwork-report.schema.json +559 -0
- package/schema/cobolwork-witness.schema.json +107 -0
- package/schema/cobolwork.baseline.schema.json +101 -0
- package/schema/cobolwork.site.schema.json +116 -0
|
@@ -36,6 +36,7 @@ import { basename, dirname, relative, resolve, sep } from 'node:path';
|
|
|
36
36
|
import { buildFileIndex, parseSource } from '../parser.mjs';
|
|
37
37
|
import { classifyUnder, decodeSource, diskClassifier, heldClassifier, kindOfBytes, looksEbcdic, readSource, relPath } from '../sources.mjs';
|
|
38
38
|
import { revisionBlobs, treeOf, treePathParts } from './git.mjs';
|
|
39
|
+
import { archiveKind, readArchive } from './pds-archive.mjs';
|
|
39
40
|
|
|
40
41
|
// The shape every adapter answers to. Checked rather than documented, because an adapter missing a
|
|
41
42
|
// method fails at the first rule set that happens to call it rather than at the boundary.
|
|
@@ -70,6 +71,7 @@ export function directoryTree(root, opts = {}) {
|
|
|
70
71
|
const kindOf = diskClassifier();
|
|
71
72
|
classifyUnder(root, kindOf);
|
|
72
73
|
if (top !== root) classifyUnder(top, kindOf);
|
|
74
|
+
const parseSpec = { copyDirs: idx.copyDirs, fileIndex: idx.index, indexRoot: idx.index.root, systemDirs };
|
|
73
75
|
|
|
74
76
|
return {
|
|
75
77
|
kind: 'directory',
|
|
@@ -81,6 +83,7 @@ export function directoryTree(root, opts = {}) {
|
|
|
81
83
|
list: () => [...idx.index.values()].sort(),
|
|
82
84
|
bytes: (p) => readFileSync(p),
|
|
83
85
|
text: (p) => readSource(p),
|
|
86
|
+
decode: decodeSource,
|
|
84
87
|
rel: (p) => relPath(root, p),
|
|
85
88
|
// The same prefix test buildFileIndex applies while walking. It is not the containment
|
|
86
89
|
// mechanism - the walk is, and it resolves symlinks before testing - it is the answer to
|
|
@@ -88,17 +91,26 @@ export function directoryTree(root, opts = {}) {
|
|
|
88
91
|
contains: (p) => { const r = resolve(p); return r === top || r.startsWith(top + sep); },
|
|
89
92
|
// One parse configuration, in one place. `text` is optional: a caller that has already read
|
|
90
93
|
// the source passes it rather than reading twice.
|
|
91
|
-
parse: (file, text) =>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
fileIndex: idx.index,
|
|
95
|
-
mainDir: dirname(file),
|
|
96
|
-
copyFormat: 'auto',
|
|
97
|
-
systemDirs,
|
|
98
|
-
}),
|
|
94
|
+
parse: (file, text) => parseInDirectory(parseSpec, file, text),
|
|
95
|
+
// The same configuration as data, for a worker thread to parse as this tree does.
|
|
96
|
+
parseSpec,
|
|
99
97
|
};
|
|
100
98
|
}
|
|
101
99
|
|
|
100
|
+
// A directory tree's parse, from its parseSpec. A spec cloned into a worker loses the index's root,
|
|
101
|
+
// which the parser reads to refuse a COPY that leaves the tree, so it is put back.
|
|
102
|
+
export function parseInDirectory(spec, file, text) {
|
|
103
|
+
if (spec.fileIndex.root !== spec.indexRoot) spec.fileIndex.root = spec.indexRoot;
|
|
104
|
+
return parseSource(text ?? readSource(file).text, file, {
|
|
105
|
+
format: 'auto',
|
|
106
|
+
includeDirs: spec.copyDirs,
|
|
107
|
+
fileIndex: spec.fileIndex,
|
|
108
|
+
mainDir: dirname(file),
|
|
109
|
+
copyFormat: 'auto',
|
|
110
|
+
systemDirs: spec.systemDirs,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
// A tree whose files are held rather than on disk. The parser finds copybooks in the index built
|
|
103
115
|
// here and reads them through `readText`, so a COPY resolves to a held file or to a system copy
|
|
104
116
|
// library outside the tree, never to a file on disk that happens to sit under this tree's root.
|
|
@@ -135,6 +147,7 @@ function heldTree({ kind, root, store, rel, systemDirs = [], copyDirs = null, cl
|
|
|
135
147
|
list: () => [...store.keys()].sort(),
|
|
136
148
|
bytes,
|
|
137
149
|
text,
|
|
150
|
+
decode: decodeSource,
|
|
138
151
|
rel,
|
|
139
152
|
// Containment by construction: a path this tree does not hold is a path outside it.
|
|
140
153
|
contains: (p) => store.has(p),
|
|
@@ -206,24 +219,26 @@ export function unframeRecords(buf) {
|
|
|
206
219
|
return looksEbcdic(ebcdic) ? ebcdic : lines(0x0A);
|
|
207
220
|
}
|
|
208
221
|
|
|
209
|
-
// A
|
|
210
|
-
// not a regular one, is refused rather than followed or waited on.
|
|
211
|
-
function
|
|
222
|
+
// A file the walk found, read as it is or its first `limit` bytes. A link put in its place since, or
|
|
223
|
+
// a file that is not a regular one, is refused rather than followed or waited on.
|
|
224
|
+
function readFound(file, limit = Infinity) {
|
|
212
225
|
const fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
213
226
|
try {
|
|
214
227
|
const st = fstatSync(fd);
|
|
215
228
|
if (!st.isFile()) throw Object.assign(new Error(`not a regular file: ${file}`), { code: 'ENOTFILE' });
|
|
216
|
-
const
|
|
229
|
+
const size = Math.min(st.size, limit);
|
|
230
|
+
const buf = Buffer.alloc(size);
|
|
217
231
|
let n = 0;
|
|
218
|
-
while (n <
|
|
219
|
-
const r = readSync(fd, buf, n,
|
|
232
|
+
while (n < size) {
|
|
233
|
+
const r = readSync(fd, buf, n, size - n, n);
|
|
220
234
|
if (!r) break;
|
|
221
235
|
n += r;
|
|
222
236
|
}
|
|
223
|
-
return
|
|
237
|
+
return buf.subarray(0, n);
|
|
224
238
|
} finally { closeSync(fd); }
|
|
225
239
|
}
|
|
226
240
|
|
|
241
|
+
|
|
227
242
|
const QUALIFIER = /^[A-Z@#$][A-Z0-9@#$-]{0,7}$/;
|
|
228
243
|
const MEMBER = /^([A-Z@#$][A-Z0-9@#$]{0,7})(?:\.[^.]*)?$/i;
|
|
229
244
|
const MAX_LISTED = 8;
|
|
@@ -243,58 +258,111 @@ const MAX_LISTED = 8;
|
|
|
243
258
|
export function pdsExportTree(dir, opts = {}) {
|
|
244
259
|
const idx = buildFileIndex(dir);
|
|
245
260
|
const top = realpathSync(dir);
|
|
246
|
-
const
|
|
261
|
+
const files = [...idx.index.values()].sort().map((file) => {
|
|
262
|
+
let real;
|
|
263
|
+
try { real = realpathSync(file); } catch { real = file; }
|
|
264
|
+
return { rel: relPath(dir, file), escapes: real !== top && !real.startsWith(top + sep), read: (limit) => readFound(real, limit) };
|
|
265
|
+
});
|
|
266
|
+
return exportOf({ dir, root: `${resolve(dir)}@pds`, files, systemDirs: opts.systemDirs, symlinks: idx.symlinks,
|
|
267
|
+
unreadableDirs: idx.unreadableDirs.map((d) => relative(dir, d)) });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// The same export as a git revision holds it, read with plumbing into memory, as gitTree reads one.
|
|
271
|
+
export function pdsExportRevision(repo, ref, opts = {}) {
|
|
272
|
+
const oid = treeOf(repo, ref);
|
|
273
|
+
const { blobs } = revisionBlobs(repo, oid, ref);
|
|
274
|
+
const files = blobs.filter((b) => treePathParts(b.path)).map((b) => ({ rel: b.path, read: (limit = Infinity) => b.bytes.subarray(0, limit) }));
|
|
275
|
+
const at = `${resolve(repo)}@${oid.slice(0, 12)}`;
|
|
276
|
+
return Object.assign(exportOf({ dir: at, root: `${at}@pds`, files, systemDirs: opts.systemDirs }), { ref, oid });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// An export's files, each { rel, read(limit), escapes }, held as members: a file whose directory is a
|
|
280
|
+
// data set name and whose name a member name, or an XMIT file or IEBCOPY unload read into its data
|
|
281
|
+
// set. `rel` is the file's path from the export's top, with forward slashes.
|
|
282
|
+
function exportOf({ dir, root, files, systemDirs = [], symlinks = { followed: 0, outside: 0, broken: 0 }, unreadableDirs = [] }) {
|
|
247
283
|
const origins = new Map();
|
|
248
284
|
const found = new Map();
|
|
249
285
|
const notMembers = [];
|
|
250
286
|
const duplicates = [];
|
|
251
287
|
const dataSets = new Set();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
288
|
+
const archived = new Map();
|
|
289
|
+
const archives = { files: 0, members: 0, aliases: 0, notRead: [] };
|
|
290
|
+
for (const { rel, read, escapes } of files) {
|
|
291
|
+
const parts = rel.split('/');
|
|
292
|
+
const file = parts.pop();
|
|
293
|
+
const dsn = parts.join('.').toUpperCase();
|
|
294
|
+
let head = null;
|
|
295
|
+
try { head = archiveKind(read(16)); } catch { head = null; }
|
|
296
|
+
if (head) {
|
|
297
|
+
if (escapes) { notMembers.push(rel); continue; }
|
|
298
|
+
let a;
|
|
299
|
+
try { a = readArchive(read()); } catch (e) { a = { why: reasonOf(e) }; }
|
|
300
|
+
const name = a.dataSet || file.replace(/\.[^.]*$/, '').toUpperCase();
|
|
301
|
+
if (!a.why && !(name.length <= 44 && name.split('.').every((q) => QUALIFIER.test(q)))) a.why = `${name} is not a data set name`;
|
|
302
|
+
if (a.why) { archives.notRead.push(`${rel}: ${a.why}`); continue; }
|
|
303
|
+
archives.files++;
|
|
304
|
+
archives.aliases += a.aliases.length;
|
|
305
|
+
if (a.unmatched) archives.notRead.push(`${rel}: ${a.unmatched} member(s) whose blocks no directory entry names`);
|
|
306
|
+
if (a.missing) archives.notRead.push(`${rel}: no data for ${a.missing.join(', ')}`);
|
|
307
|
+
for (const m of a.members) {
|
|
308
|
+
const at = m.name === null ? resolve(root, name) : resolve(root, name, m.name);
|
|
309
|
+
const origin = m.name === null ? rel : `${rel}(${m.name})`;
|
|
310
|
+
if (found.has(at)) { duplicates.push(`${origin}: ${relPath(root, at)} is ${found.get(at)}`); continue; }
|
|
311
|
+
archived.set(at, m.bytes);
|
|
312
|
+
found.set(at, origin);
|
|
313
|
+
dataSets.add(m.name === null ? at : dirname(at));
|
|
314
|
+
archives.members++;
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const member = MEMBER.exec(file);
|
|
256
319
|
if (!member || dsn.length > 44 || (dsn && !dsn.split('.').every((q) => QUALIFIER.test(q)))) {
|
|
257
|
-
notMembers.push(
|
|
320
|
+
notMembers.push(rel);
|
|
258
321
|
continue;
|
|
259
322
|
}
|
|
260
323
|
const at = resolve(root, ...(dsn ? [dsn] : []), member[1].toUpperCase());
|
|
261
|
-
if (found.has(at)) { duplicates.push(`${
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
origins.set(at, real);
|
|
266
|
-
found.set(at, relPath(dir, file));
|
|
324
|
+
if (found.has(at)) { duplicates.push(`${rel}: ${relPath(root, at)} is ${found.get(at)}`); continue; }
|
|
325
|
+
if (escapes) { notMembers.push(rel); continue; }
|
|
326
|
+
origins.set(at, read);
|
|
327
|
+
found.set(at, rel);
|
|
267
328
|
dataSets.add(dirname(at));
|
|
268
329
|
}
|
|
269
330
|
const kinds = new Map();
|
|
270
331
|
const byKind = {};
|
|
271
332
|
const copyDirs = new Set();
|
|
272
|
-
|
|
333
|
+
const bytesOf = (at) => archived.get(at) ?? unframeRecords(origins.get(at)());
|
|
334
|
+
for (const at of [...origins.keys(), ...archived.keys()]) {
|
|
273
335
|
let kind = null;
|
|
274
|
-
try { kind = kindOfBytes(
|
|
336
|
+
try { kind = kindOfBytes(bytesOf(at)); } catch { kind = null; }
|
|
275
337
|
kinds.set(at, kind);
|
|
276
338
|
byKind[kind || 'other'] = (byKind[kind || 'other'] || 0) + 1;
|
|
277
339
|
if (kind !== 'jcl' && kind !== 'bms') copyDirs.add(dirname(at));
|
|
278
340
|
}
|
|
279
341
|
const store = {
|
|
280
|
-
keys: () => origins.keys(),
|
|
281
|
-
has: (p) => origins.has(p),
|
|
282
|
-
get: (p) => (origins.has(p)
|
|
342
|
+
keys: () => [...origins.keys(), ...archived.keys()].values(),
|
|
343
|
+
has: (p) => origins.has(p) || archived.has(p),
|
|
344
|
+
get: (p) => (origins.has(p) || archived.has(p) ? bytesOf(p) : undefined),
|
|
283
345
|
};
|
|
284
346
|
const tree = heldTree({
|
|
285
|
-
kind: 'pds-export', root, store, rel: (p) => relPath(root, p), systemDirs
|
|
286
|
-
copyDirs: [...copyDirs], classify: () => (p) => kinds.get(p) ?? null, symlinks
|
|
287
|
-
unreadableDirs:
|
|
347
|
+
kind: 'pds-export', root, store, rel: (p) => relPath(root, p), systemDirs,
|
|
348
|
+
copyDirs: [...copyDirs], classify: () => (p) => kinds.get(p) ?? null, symlinks,
|
|
349
|
+
unreadableDirs: unreadableDirs.map((d) => resolve(root, d)),
|
|
288
350
|
});
|
|
289
351
|
return Object.assign(tree, {
|
|
290
352
|
dir,
|
|
291
353
|
origin: (p) => found.get(p) ?? null,
|
|
292
354
|
export: {
|
|
293
355
|
dataSets: dataSets.size,
|
|
294
|
-
members: origins.size,
|
|
356
|
+
members: origins.size + archived.size,
|
|
295
357
|
byKind,
|
|
296
358
|
...(notMembers.length ? { notMembers: notMembers.length, notMembersListed: notMembers.slice(0, MAX_LISTED) } : {}),
|
|
297
359
|
...(duplicates.length ? { duplicates: duplicates.length, duplicatesListed: duplicates.slice(0, MAX_LISTED) } : {}),
|
|
360
|
+
...(archives.files || archives.notRead.length ? {
|
|
361
|
+
archives: {
|
|
362
|
+
files: archives.files, members: archives.members, aliases: archives.aliases,
|
|
363
|
+
...(archives.notRead.length ? { notRead: archives.notRead.length, notReadListed: archives.notRead.slice(0, MAX_LISTED) } : {}),
|
|
364
|
+
},
|
|
365
|
+
} : {}),
|
|
298
366
|
},
|
|
299
367
|
});
|
|
300
368
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// The version key and unknown keys of a document cobolwork reads. A document without the key
|
|
3
|
+
// predates versioning and is read as version 0, which holds the same keys as version 1.
|
|
4
|
+
import { printable } from './printable.mjs';
|
|
5
|
+
|
|
6
|
+
// Why the document cannot be read, as a clause to follow its name, or null when it can.
|
|
7
|
+
export function versionProblem(raw, current) {
|
|
8
|
+
const version = raw.version === undefined ? 0 : raw.version;
|
|
9
|
+
if (!Number.isInteger(version) || version < 0) return `has version ${printable(JSON.stringify(version), 40)}, which is not a whole number from 0`;
|
|
10
|
+
if (version > current) return `is version ${version}, and this cobolwork reads up to version ${current}; upgrade cobolwork`;
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// A key opening with an underscore is a note for people, as diag/propose-site.mjs writes them.
|
|
15
|
+
export const unknownKeys = (raw, known) => Object.keys(raw).filter((k) => !known.has(k) && !k.startsWith('_')).sort();
|
|
16
|
+
|
|
17
|
+
export const ignoredKey = (where, key) => `${where}: ${printable(JSON.stringify(key), 80)} is not a key this cobolwork reads, so it was ignored`;
|
package/lib/layout.mjs
CHANGED
|
@@ -22,6 +22,8 @@ function picInfo(pic, constants) {
|
|
|
22
22
|
if (ch === 'V' || ch === 'P') continue;
|
|
23
23
|
if (ch === '9') { info.digits += n; info.display += n; continue; }
|
|
24
24
|
if (ch === 'N' || ch === 'G') { info.national = true; info.display += 2 * n; continue; }
|
|
25
|
+
// A U is one UTF-8 character, for which IBM reserves four bytes (Defining UTF-8 data items).
|
|
26
|
+
if (ch === 'U') { info.utf8 = true; info.display += 4 * n; continue; }
|
|
25
27
|
if (ch === 'X' || ch === 'A') info.alphanumeric = true;
|
|
26
28
|
info.display += n;
|
|
27
29
|
}
|
|
@@ -45,9 +47,13 @@ function binaryBytes(digits, scheme) {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function elementarySize(item, scheme, constants) {
|
|
50
|
+
if (item.byteLength) return item.byteLength;
|
|
48
51
|
if (!item.picture && !item.usage) {
|
|
49
52
|
const constBytes = constants && constants.textBytes;
|
|
50
|
-
const
|
|
53
|
+
const bytes = (v) => (v.t === 'lit' ? literalBytes(v) : v.t === 'word' && constBytes && constBytes.has(v.u) ? constBytes.get(v.u) : 0);
|
|
54
|
+
// A screen item shows a numeric VALUE as written, one position per character.
|
|
55
|
+
const digits = (v) => v.t === 'num' || (v.t === 'word' && /^\d+$/.test(v.v));
|
|
56
|
+
const fromValue = item.values.reduce((n, v) => n + (item.section === 'SCREEN' && digits(v) ? v.v.length : bytes(v)), 0);
|
|
51
57
|
if (item.section === 'SCREEN' && !fromValue && item.screenRefItem) return item.screenRefItem.size || 0;
|
|
52
58
|
if (item.section === 'SCREEN') return Math.max(1, fromValue);
|
|
53
59
|
if (fromValue) return fromValue;
|
|
@@ -147,47 +153,36 @@ export function computeSizes(roots, scheme, constants) {
|
|
|
147
153
|
for (const r of roots) place(r, 0);
|
|
148
154
|
}
|
|
149
155
|
|
|
150
|
-
// A report group is
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// to, is as large as the largest group.
|
|
156
|
+
// A report group is as large as the larger of its items laid end to end and the column its
|
|
157
|
+
// rightmost elementary item ends in, COLUMN PLUS counting on from the end of the item before; an
|
|
158
|
+
// elementary item keeps its picture's size. Every 01 group of a report, like the file the report is
|
|
159
|
+
// written to, is as large as the largest group. These are GnuCOBOL's sizes: IBM's Report Writer is a
|
|
160
|
+
// precompiler that writes its own records.
|
|
154
161
|
export function layoutReport(rd) {
|
|
155
162
|
const structural = (x) => x.children.filter(c => c.level !== 88 && c.level !== 66 && c.level !== 78);
|
|
156
163
|
const mark = (x) => { x.rd = rd; for (const c of x.children) mark(c); };
|
|
157
164
|
for (const g of rd.groups) mark(g);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const lineWidth = (line) => {
|
|
165
|
+
const sizeOf = (x) => {
|
|
166
|
+
const kids = structural(x);
|
|
167
|
+
if (!kids.length) return x.size || 0;
|
|
162
168
|
let last = 0;
|
|
163
169
|
let right = 0;
|
|
164
170
|
let total = 0;
|
|
165
|
-
const
|
|
166
|
-
const len = c
|
|
171
|
+
for (const c of kids) {
|
|
172
|
+
const len = sizeOf(c) * (c.occurs || 1);
|
|
173
|
+
total += len;
|
|
174
|
+
if (structural(c).length) continue;
|
|
167
175
|
const start = c.rwColumn ? (c.rwColumn.at != null ? c.rwColumn.at : last + c.rwColumn.plus) : last + 1;
|
|
168
176
|
last = start + len - 1;
|
|
169
177
|
right = Math.max(right, last);
|
|
170
|
-
total += len;
|
|
171
|
-
};
|
|
172
|
-
const walk = (x) => { for (const c of structural(x)) { if (structural(c).length) walk(c); else place(c); } };
|
|
173
|
-
if (structural(line).length) walk(line);
|
|
174
|
-
else place(line);
|
|
175
|
-
return Math.max(right, total);
|
|
176
|
-
};
|
|
177
|
-
// The entries that open a line; a group with no LINE clause anywhere in it is one line.
|
|
178
|
-
for (const g of rd.groups) {
|
|
179
|
-
const lines = [];
|
|
180
|
-
const collect = (x) => { if (x.rwLine) { lines.push(x); return; } for (const c of structural(x)) collect(c); };
|
|
181
|
-
collect(g);
|
|
182
|
-
if (!lines.length) lines.push(g);
|
|
183
|
-
let total = 0;
|
|
184
|
-
for (const line of lines) {
|
|
185
|
-
const w = lineWidth(line);
|
|
186
|
-
if (line !== g) { line.size = w; line.contributes = w * (line.occurs || 1); }
|
|
187
|
-
total += w;
|
|
188
178
|
}
|
|
189
|
-
|
|
190
|
-
|
|
179
|
+
const size = Math.max(right, total);
|
|
180
|
+
x.size = size;
|
|
181
|
+
x.contributes = size * (x.occurs || 1);
|
|
182
|
+
return size;
|
|
183
|
+
};
|
|
184
|
+
let width = 0;
|
|
185
|
+
for (const g of rd.groups) width = Math.max(width, sizeOf(g));
|
|
191
186
|
for (const g of rd.groups) { g.size = width; g.contributes = width; }
|
|
192
187
|
rd.width = width;
|
|
193
188
|
}
|
package/lib/parser.mjs
CHANGED
|
@@ -210,7 +210,8 @@ function evaluateCondition(text, defines) {
|
|
|
210
210
|
return null;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
// A debugging line (D in the indicator) is source under WITH DEBUGGING MODE and a comment otherwise.
|
|
214
|
+
export function normalize(src, format, defines, std, debugging = false) {
|
|
214
215
|
// Micro Focus reads a free-form line with * or / in column 1 as a comment, as cobc -std=mf does.
|
|
215
216
|
const columnOneComments = std === 'mf' || std === 'mf-strict';
|
|
216
217
|
const phys = src.split(/\r?\n/);
|
|
@@ -310,7 +311,7 @@ export function normalize(src, format, defines, std) {
|
|
|
310
311
|
text = l.slice(7, 7 + width);
|
|
311
312
|
if (!FIXED_INDICATORS.has(indicator)) { diags.push({ kind: 'invalid-indicator', line }); indicator = ' '; }
|
|
312
313
|
}
|
|
313
|
-
if (indicator === '*' || indicator === '/' || indicator === 'D' || indicator === 'd') continue;
|
|
314
|
+
if (indicator === '*' || indicator === '/' || ((indicator === 'D' || indicator === 'd') && !debugging)) continue;
|
|
314
315
|
if (fmt === 'free' && (/^\s*\*>/.test(text) || (columnOneComments && /^[*/]/.test(text)))) continue;
|
|
315
316
|
// AUTHOR, INSTALLATION, DATE-WRITTEN, DATE-COMPILED, SECURITY and REMARKS take a comment-entry:
|
|
316
317
|
// any text, a COPY or an apostrophe included. It runs to the end of the header's line, and in
|
|
@@ -409,8 +410,9 @@ export function tokenize(norm, file) {
|
|
|
409
410
|
if (c === ' ' || c === '\t' || c === '\f' || c === '\r') { i++; continue; }
|
|
410
411
|
if (exec && exec.kind === 'SQL' && c === '-' && s[i + 1] === '-') break;
|
|
411
412
|
if (picPending) {
|
|
413
|
+
// = is no PICTURE symbol, so == closes the pseudo-text a picture string ends.
|
|
412
414
|
let j = i;
|
|
413
|
-
while (j < n && s[j] !== ' ') j++;
|
|
415
|
+
while (j < n && s[j] !== ' ' && !(s[j] === '=' && s[j + 1] === '=')) j++;
|
|
414
416
|
let pic = s.slice(i, j);
|
|
415
417
|
if (pic.toUpperCase() === 'IS') { emit(tok('word', pic, line, file)); i = j; continue; }
|
|
416
418
|
let trailingPeriod = false;
|
|
@@ -853,7 +855,7 @@ function includeCopy(name, lib, pairs, at, via, ctx, stack, inheritedFormat) {
|
|
|
853
855
|
// A copybook is read in the format in force where the COPY statement sits, which a >>SOURCE
|
|
854
856
|
// directive earlier in the including file may have changed from the file's starting format.
|
|
855
857
|
const fmt = detectFormat(src) === 'terminal' && ctx.copyFormat === 'auto' ? 'terminal' : (at.fmt || (ctx.copyFormat !== 'auto' ? ctx.copyFormat : (inheritedFormat || ctx.mainFormat)));
|
|
856
|
-
const norm = normalize(src, fmt, ctx.defines, ctx.std);
|
|
858
|
+
const norm = normalize(src, fmt, ctx.defines, ctx.std, ctx.debugging);
|
|
857
859
|
// A tag written against other text - :TAG:-FIELD, FS-(), 'X'-CLE - is replaced in the text, as the
|
|
858
860
|
// compiler does, so the text around it joins the replacement into one word. A literal is taken as a
|
|
859
861
|
// tag only where it touches a word; elsewhere it is replaced token for token like any operand.
|
|
@@ -896,6 +898,53 @@ function includeCopy(name, lib, pairs, at, via, ctx, stack, inheritedFormat) {
|
|
|
896
898
|
return out;
|
|
897
899
|
}
|
|
898
900
|
|
|
901
|
+
// REPLACE ==:TAG:== BY ==text== replaces the colon-delimited operand wherever the program text holds
|
|
902
|
+
// it, inside a word or a PICTURE as much as standing alone (IBM Enterprise COBOL, REPLACE statement,
|
|
903
|
+
// replacement rules). Like a tag in COPY REPLACING it is replaced in the text, from the statement on
|
|
904
|
+
// until REPLACE OFF or a REPLACE that does not name it again; a REPLACE itself is left as written.
|
|
905
|
+
const TAG = /^:[A-Za-z0-9_-]+:$/;
|
|
906
|
+
|
|
907
|
+
function replaceStatementAt(text, at) {
|
|
908
|
+
const rest = text.slice(at);
|
|
909
|
+
const off = /^REPLACE\s+OFF\s*\./i.exec(rest);
|
|
910
|
+
if (off) return { end: at + off[0].length, off: true, tags: [] };
|
|
911
|
+
const head = /^REPLACE(\s+ALSO)?\s*/i.exec(rest);
|
|
912
|
+
let k = head[0].length;
|
|
913
|
+
const tags = [];
|
|
914
|
+
let pairs = 0;
|
|
915
|
+
for (;;) {
|
|
916
|
+
const pair = /^(?:(?:LEADING|TRAILING)\s+)?==([\s\S]*?)==\s*BY\s*==([\s\S]*?)==\s*/i.exec(rest.slice(k));
|
|
917
|
+
if (!pair) break;
|
|
918
|
+
pairs++;
|
|
919
|
+
if (TAG.test(pair[1].trim())) tags.push([pair[1].trim(), pair[2].trim().replace(/\s+/g, ' ')]);
|
|
920
|
+
k += pair[0].length;
|
|
921
|
+
}
|
|
922
|
+
if (!pairs) return null;
|
|
923
|
+
if (rest[k] === '.') k++;
|
|
924
|
+
return { end: at + k, also: !!head[1], tags };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function replaceTags(norm) {
|
|
928
|
+
const text = norm.entries.map((e) => e.text).join('\n');
|
|
929
|
+
if (!/==\s*:[A-Za-z0-9_-]+:\s*==/.test(text)) return;
|
|
930
|
+
let active = [];
|
|
931
|
+
let out = '';
|
|
932
|
+
let from = 0;
|
|
933
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- TAG admits no metacharacter
|
|
934
|
+
const swap = (chunk) => active.reduce((t, [tag, to]) => t.replace(new RegExp(tag, 'gi'), to), chunk);
|
|
935
|
+
for (const m of text.matchAll(/\bREPLACE\b/gi)) {
|
|
936
|
+
if (m.index < from) continue;
|
|
937
|
+
const st = replaceStatementAt(text, m.index);
|
|
938
|
+
if (!st) continue;
|
|
939
|
+
out += swap(text.slice(from, m.index)) + text.slice(m.index, st.end);
|
|
940
|
+
active = st.off ? [] : st.also ? [...active, ...st.tags] : st.tags;
|
|
941
|
+
from = st.end;
|
|
942
|
+
}
|
|
943
|
+
out += swap(text.slice(from));
|
|
944
|
+
const lines = out.split('\n');
|
|
945
|
+
if (lines.length === norm.entries.length) norm.entries.forEach((e, i) => { e.text = lines[i]; });
|
|
946
|
+
}
|
|
947
|
+
|
|
899
948
|
// A REPLACE governs the source up to the next one, which replaces it unless ALSO; OFF ends it; output is not rescanned.
|
|
900
949
|
function applyReplaceStatements(tokens, ctx) {
|
|
901
950
|
let active = [];
|
|
@@ -976,6 +1025,8 @@ function parseDataEntry(toks, section, file) {
|
|
|
976
1025
|
continue;
|
|
977
1026
|
}
|
|
978
1027
|
if (u === 'PIC' || u === 'PICTURE') { j++; if (toks[j] && toks[j].u === 'IS') j++; if (toks[j]) item.picture = toks[j].v; j++; continue; }
|
|
1028
|
+
// PIC U BYTE-LENGTH n: a UTF-8 item of n bytes holding as many characters as fit.
|
|
1029
|
+
if (u === 'BYTE-LENGTH') { j++; if (toks[j] && toks[j].u === 'IS') j++; if (toks[j] && /^\d+$/.test(toks[j].v)) { item.byteLength = Number(toks[j].v); j++; } continue; }
|
|
979
1030
|
if (u === 'USAGE') { j++; if (toks[j] && toks[j].u === 'IS') j++; if (toks[j]) { item.usage = toks[j].u; j++; } continue; }
|
|
980
1031
|
if (USAGE_WORDS.has(u)) { item.usage = u; j++; continue; }
|
|
981
1032
|
// OCCURS DYNAMIC [CAPACITY IN name] [FROM n] [TO m]: sized at its most, none without TO; the
|
|
@@ -1048,6 +1099,9 @@ function parseDataEntry(toks, section, file) {
|
|
|
1048
1099
|
if (u === 'CONSTANT') { item.constant = true; j++; if (toks[j] && toks[j].u === 'AS') j++; while (toks[j] && (toks[j].t === 'num' || toks[j].t === 'lit' || (toks[j].t === 'word' && /^\d+$/.test(toks[j].v)))) item.values.push(toks[j++]); continue; }
|
|
1049
1100
|
if (u === 'VALUE' || u === 'VALUES') {
|
|
1050
1101
|
j++;
|
|
1102
|
+
// A screen item's VALUE is one literal; what follows it is the item's other clauses, and a later
|
|
1103
|
+
// VALUE (two entries run together by a missing period) replaces it.
|
|
1104
|
+
if (section === 'SCREEN') { if (toks[j] && toks[j].u === 'IS') j++; if (toks[j]) { item.values = [toks[j]]; j++; } continue; }
|
|
1051
1105
|
while (toks[j] && (toks[j].t !== 'word' || !(DATA_CLAUSE_WORDS.has(toks[j].u) || (report && REPORT_CLAUSE_WORDS.has(toks[j].u))) || toks[j].u === 'IS')) { item.values.push(toks[j]); j++; }
|
|
1052
1106
|
continue;
|
|
1053
1107
|
}
|
|
@@ -1073,6 +1127,25 @@ function debugItem(at) {
|
|
|
1073
1127
|
return [root, ...root.children];
|
|
1074
1128
|
}
|
|
1075
1129
|
|
|
1130
|
+
// DEBUG-CONTENTS is X(n), n left to the compiler (IBM, DEBUG-ITEM). GnuCOBOL makes it as long as the
|
|
1131
|
+
// longest item USE FOR DEBUGGING names, a CD's records included, and never shorter than 30.
|
|
1132
|
+
function sizeDebugContents(prog, tokens, from, to, scheme, constants) {
|
|
1133
|
+
const named = [];
|
|
1134
|
+
for (let i = from; i < to - 3; i++) {
|
|
1135
|
+
if (tokens[i].u !== 'USE' || tokens[i + 1].u !== 'FOR' || tokens[i + 2].u !== 'DEBUGGING') continue;
|
|
1136
|
+
for (let k = i + 3; k < to && tokens[k].t !== 'period'; k++) if (tokens[k].t === 'word') named.push(tokens[k].u);
|
|
1137
|
+
}
|
|
1138
|
+
const sizes = named.flatMap((n) => prog.items.filter((it) => it.name === n && it.level < 50 && !it.implicit).map((it) => it.size || 0)
|
|
1139
|
+
.concat(prog.items.filter((it) => it.cd === n).map((it) => it.size || 0)));
|
|
1140
|
+
const longest = Math.max(30, ...sizes);
|
|
1141
|
+
if (longest === 30) return;
|
|
1142
|
+
const root = prog.items.find((it) => it.implicit && it.name === 'DEBUG-ITEM');
|
|
1143
|
+
const contents = root && root.children.find((c) => c.name === 'DEBUG-CONTENTS');
|
|
1144
|
+
if (!contents) return;
|
|
1145
|
+
contents.picture = `X(${longest})`;
|
|
1146
|
+
computeSizes([root], scheme, constants);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1076
1149
|
// TYPE gives an item the picture, usage and subordinate items of the TYPEDEF it names. The listing
|
|
1077
1150
|
// prints the typed item alone; what it holds is implied, reachable by qualification (RE OF WS-Z),
|
|
1078
1151
|
// so the copies are returned as items marked typeClone.
|
|
@@ -1169,6 +1242,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
|
|
|
1169
1242
|
if (s[m]) f.assign = { t: s[m].t, v: s[m].t === 'word' ? s[m].u : s[m].v };
|
|
1170
1243
|
}
|
|
1171
1244
|
for (let m = n + 1; m < s.length; m++) if (s[m].t === 'word') f.envRefs.push(s[m]);
|
|
1245
|
+
f.lineSequential = s.some((t, m) => t.u === 'LINE' && s[m + 1] && s[m + 1].u === 'SEQUENTIAL');
|
|
1172
1246
|
prog.files.push(f);
|
|
1173
1247
|
} else {
|
|
1174
1248
|
for (const t of s) if (t.t === 'word') prog.refs.push({ tok: t, zone: 'env' });
|
|
@@ -1182,6 +1256,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
|
|
|
1182
1256
|
let stack = [];
|
|
1183
1257
|
let currentFd = null;
|
|
1184
1258
|
let currentRd = null;
|
|
1259
|
+
let currentCd = null;
|
|
1185
1260
|
const dataSentences = sentences(tokens, dataStart, dataEnd);
|
|
1186
1261
|
for (let s of dataSentences) {
|
|
1187
1262
|
// What an EXEC SQL INCLUDE brings in follows it in the same sentence, up to its own first period.
|
|
@@ -1196,6 +1271,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
|
|
|
1196
1271
|
section = first.u === 'RD' ? 'REPORT' : 'COMMUNICATION';
|
|
1197
1272
|
currentFd = null;
|
|
1198
1273
|
currentRd = first.u === 'RD' ? { name: s[1].u, line: first.line, file: first.file, groups: [] } : null;
|
|
1274
|
+
currentCd = first.u === 'CD' ? s[1].u : null;
|
|
1199
1275
|
if (currentRd) (prog.reports ||= []).push(currentRd);
|
|
1200
1276
|
else (prog.cds ||= []).push(s[1].u);
|
|
1201
1277
|
stack = [];
|
|
@@ -1204,12 +1280,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
|
|
|
1204
1280
|
if (first.t === 'word' && (first.u === 'FD' || first.u === 'SD') && s[1]) {
|
|
1205
1281
|
section = 'FILE';
|
|
1206
1282
|
currentRd = null;
|
|
1207
|
-
currentFd = { kind: first.u, name: s[1].u, line: first.line, file: first.file, records: [], fdTok: s[1],
|
|
1208
|
-
const recAt = s.findIndex(x => x.t === 'word' && x.u === 'RECORD');
|
|
1209
|
-
if (recAt >= 0) for (let m = recAt + 1; m < s.length && !['LABEL', 'BLOCK', 'DATA', 'VALUE', 'RECORDING', 'CODE-SET', 'LINAGE', 'REPORT', 'REPORTS', 'DEPENDING'].includes(s[m].u); m++) {
|
|
1210
|
-
if (s[m].u === 'VARYING') currentFd.varying = true;
|
|
1211
|
-
if (/^\d+$/.test(s[m].v)) currentFd.declaredMax = Math.max(currentFd.declaredMax, Number(s[m].v));
|
|
1212
|
-
}
|
|
1283
|
+
currentFd = { kind: first.u, name: s[1].u, line: first.line, file: first.file, records: [], fdTok: s[1], ...recordClause(s) };
|
|
1213
1284
|
const repAt = s.findIndex(x => x.t === 'word' && (x.u === 'REPORT' || x.u === 'REPORTS'));
|
|
1214
1285
|
if (repAt >= 0) {
|
|
1215
1286
|
currentFd.reports = [];
|
|
@@ -1225,6 +1296,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
|
|
|
1225
1296
|
}
|
|
1226
1297
|
if ((first.t === 'word' || first.t === 'num') && /^\d{1,2}$/.test(first.v)) {
|
|
1227
1298
|
const item = parseDataEntry(s, section, first.file);
|
|
1299
|
+
if (section === 'COMMUNICATION' && item.level === 1) item.cd = currentCd;
|
|
1228
1300
|
if (item.level === 88 || item.level === 66) {
|
|
1229
1301
|
const parent = item.level === 88 ? stack[stack.length - 1] : null;
|
|
1230
1302
|
if (parent) { parent.children.push(item); item.parent = parent; }
|
|
@@ -1283,6 +1355,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
|
|
|
1283
1355
|
|| siblings.find(x => x !== it && x.name === it.redefines) || null;
|
|
1284
1356
|
}
|
|
1285
1357
|
computeSizes(roots, scheme, constants);
|
|
1358
|
+
if (prog.debuggingMode && procAt >= 0) sizeDebugContents(prog, tokens, procAt, procEnd, scheme, constants);
|
|
1286
1359
|
const subtreeCache = new Map();
|
|
1287
1360
|
const subtree = (record) => {
|
|
1288
1361
|
if (subtreeCache.has(record)) return subtreeCache.get(record);
|
|
@@ -1319,12 +1392,18 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
|
|
|
1319
1392
|
it.renamesSpan = all.filter(x => x.offset >= from && x.offset + x.contributes <= to && x !== it);
|
|
1320
1393
|
}
|
|
1321
1394
|
for (const rd of prog.reports || []) layoutReport(rd);
|
|
1322
|
-
// A file is as
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1395
|
+
// A file's record is as long as its RECORD clause allows, or its longest record description
|
|
1396
|
+
// where that is longer or there is no clause; a record longer than the clause still wins, as the
|
|
1397
|
+
// compiler warns and uses it. The FD of a line-sequential file, which GnuCOBOL has and IBM does
|
|
1398
|
+
// not, is sized by its records whatever RECORD CONTAINS says, though RECORD IS VARYING still sets
|
|
1399
|
+
// its maximum. A report file's record is as wide as the widest line of the reports written to it.
|
|
1325
1400
|
for (const fd of prog.fds || []) {
|
|
1326
1401
|
const reports = (fd.reports || []).map(n => (prog.reports || []).find(r => r.name === n)).filter(Boolean);
|
|
1327
|
-
|
|
1402
|
+
const described = Math.max(0, ...fd.records.map(recordLength), ...reports.map(r => r.width || 0));
|
|
1403
|
+
const select = prog.files.find(f => f.name === fd.name);
|
|
1404
|
+
const ignored = select && select.lineSequential && fd.kind === 'FD' && fd.recordFormat !== 3;
|
|
1405
|
+
const clause = ignored ? 0 : fd.fixedLength || fd.varyingMax || fd.rangeMax || 0;
|
|
1406
|
+
fd.size = Math.max(clause, described);
|
|
1328
1407
|
}
|
|
1329
1408
|
}
|
|
1330
1409
|
|
|
@@ -1334,6 +1413,34 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
|
|
|
1334
1413
|
return prog;
|
|
1335
1414
|
}
|
|
1336
1415
|
|
|
1416
|
+
const FD_CLAUSES = new Set(['LABEL', 'BLOCK', 'DATA', 'VALUE', 'RECORDING', 'CODE-SET', 'LINAGE', 'REPORT', 'REPORTS', 'DEPENDING', 'RECORD']);
|
|
1417
|
+
|
|
1418
|
+
// The RECORD clause of an FD in IBM's three formats, CONTAINS n, CONTAINS n TO m and IS VARYING ...
|
|
1419
|
+
// TO m, each giving the longest record the file holds. LABEL RECORD and DATA RECORD are other clauses
|
|
1420
|
+
// that share the word.
|
|
1421
|
+
function recordClause(s) {
|
|
1422
|
+
for (let at = 1; at < s.length; at++) {
|
|
1423
|
+
if (s[at].u !== 'RECORD' || ['LABEL', 'DATA'].includes(s[at - 1].u)) continue;
|
|
1424
|
+
let varying = false;
|
|
1425
|
+
let to = false;
|
|
1426
|
+
const nums = [];
|
|
1427
|
+
let toNum = null;
|
|
1428
|
+
for (let m = at + 1; m < s.length && !FD_CLAUSES.has(s[m].u); m++) {
|
|
1429
|
+
if (s[m].u === 'VARYING') varying = true;
|
|
1430
|
+
else if (s[m].u === 'TO') to = true;
|
|
1431
|
+
else if (/^\d+$/.test(s[m].v)) { nums.push(Number(s[m].v)); if (to && toNum == null) toNum = Number(s[m].v); }
|
|
1432
|
+
}
|
|
1433
|
+
if (varying) return { recordFormat: 3, varyingMax: toNum };
|
|
1434
|
+
if (nums.length === 1 && !to) return { recordFormat: 1, fixedLength: nums[0] };
|
|
1435
|
+
if (nums.length) return { recordFormat: 2, rangeMax: nums.at(-1) };
|
|
1436
|
+
}
|
|
1437
|
+
return { recordFormat: null };
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// A record's length in its file. OCCURS on a level-01 record is a GnuCOBOL extension IBM refuses;
|
|
1441
|
+
// GnuCOBOL sizes the file by one occurrence.
|
|
1442
|
+
const recordLength = (r) => (r.occurs > 1 ? r.size / r.occurs : r.size || 0);
|
|
1443
|
+
|
|
1337
1444
|
export function segmentEnd(tokens, i, to) {
|
|
1338
1445
|
let depth = 0;
|
|
1339
1446
|
for (let k = i + 1; k < to; k++) {
|
|
@@ -1485,10 +1592,15 @@ function parseProcedure(tokens, procAt, to, prog, hostVariables) {
|
|
|
1485
1592
|
const constText = target.t === 'word' && prog.constants && prog.constants.text
|
|
1486
1593
|
? prog.constants.text.get(target.u) : undefined;
|
|
1487
1594
|
const resolved = target.t === 'lit' ? target.v : constText;
|
|
1595
|
+
// A literal program name is padded to its field; the compiler drops the trailing spaces. A
|
|
1596
|
+
// literal holding a path calls the program its last component names, as the runtime loads it.
|
|
1597
|
+
const literal = resolved === undefined ? null : String(resolved).trimEnd();
|
|
1598
|
+
const name = literal === null ? target.u : literal.split(/[\\/]/).pop();
|
|
1488
1599
|
prog.calls.push({
|
|
1489
|
-
// A literal program name is padded to its field; the compiler drops the trailing spaces.
|
|
1490
1600
|
kind: resolved === undefined ? 'I' : 'L',
|
|
1491
|
-
name
|
|
1601
|
+
name,
|
|
1602
|
+
...(literal !== null && name !== literal ? { calledAs: literal } : {}),
|
|
1603
|
+
...(target.t === 'lit' && target.prefix === 'X' ? { hex: true } : {}),
|
|
1492
1604
|
...(constText !== undefined ? { viaConstant: target.u } : {}),
|
|
1493
1605
|
line: t.line, file: t.file, targetTok: target, using: args, stmtIndex: prog.statements.length - 1,
|
|
1494
1606
|
});
|
|
@@ -1953,8 +2065,15 @@ export function parseSource(src, file, opts = {}) {
|
|
|
1953
2065
|
// Where a copybook's text comes from: the disk, or the source tree the program was read from.
|
|
1954
2066
|
readText: opts.readText || ((p) => readSource(p).text) };
|
|
1955
2067
|
collectCopybookDefines(src, format, ctx, 0);
|
|
1956
|
-
|
|
2068
|
+
let norm = normalize(src, format, ctx.defines, opts.std);
|
|
2069
|
+
// WITH DEBUGGING MODE in SOURCE-COMPUTER makes the debugging lines of the program and its
|
|
2070
|
+
// copybooks source; it is read from the text with those lines still comments.
|
|
2071
|
+
if (/\bSOURCE-COMPUTER\s*\.[^.]*\bDEBUGGING\s+MODE\b/i.test(norm.entries.map(e => e.text).join(' '))) {
|
|
2072
|
+
ctx.debugging = true;
|
|
2073
|
+
norm = normalize(src, format, ctx.defines, opts.std, true);
|
|
2074
|
+
}
|
|
1957
2075
|
ctx.mainFormat = norm.finalFormat;
|
|
2076
|
+
replaceTags(norm);
|
|
1958
2077
|
const { tokens: raw, diags } = tokenize(norm, file);
|
|
1959
2078
|
ctx.diags.push(...norm.diags.map(d => ({ ...d, file })), ...diags);
|
|
1960
2079
|
const expanded = stripDirecting(applyReplaceStatements(expand(raw, ctx, [resolve(file)], norm.finalFormat), ctx));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// The PL/I statement cursor: the shared token cursor, failing with PliSyntax.
|
|
3
|
+
import { tokenCursor, split } from '../statement-cursor.mjs';
|
|
4
|
+
|
|
5
|
+
export class PliSyntax extends Error {
|
|
6
|
+
constructor(message, tok) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'PliSyntax';
|
|
9
|
+
this.line = tok ? tok.line : null;
|
|
10
|
+
this.col = tok ? tok.col : null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const cursor = (toks) => tokenCursor(toks, PliSyntax);
|
|
15
|
+
export { split };
|