@portll/cobolwork 0.2.76 → 0.2.117
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 +66 -14
- package/bin/cobolwork.mjs +126 -9
- package/lib/arith.mjs +235 -0
- package/lib/baseline.mjs +5 -0
- package/lib/build.mjs +112 -30
- package/lib/capabilities.mjs +12 -7
- package/lib/consequence.mjs +29 -0
- package/lib/control.mjs +1052 -91
- package/lib/csd.mjs +17 -2
- package/lib/dataflow.mjs +245 -78
- package/lib/enterprise-options.mjs +255 -0
- package/lib/equivalence.mjs +124 -0
- package/lib/evidence/cli.mjs +65 -0
- package/lib/evidence/journal.mjs +85 -0
- package/lib/evidence/record.mjs +110 -0
- package/lib/evidence/run.mjs +111 -0
- package/lib/evidence/seal.mjs +177 -0
- package/lib/evidence/slsa.mjs +64 -0
- package/lib/evidence/sshsig.mjs +172 -0
- package/lib/evidence/store.mjs +170 -0
- package/lib/evidence/verify.mjs +167 -0
- package/lib/explain.mjs +4 -1
- package/lib/exploitability.mjs +263 -0
- package/lib/ironwork.mjs +111 -0
- package/lib/kernel/findings.mjs +11 -0
- package/lib/kernel/registry.mjs +4 -0
- package/lib/option-diff.mjs +49 -0
- package/lib/options.mjs +109 -46
- package/lib/parser.mjs +29 -7
- package/lib/policy.mjs +8 -3
- package/lib/precompile.mjs +4 -0
- package/lib/reach.mjs +29 -12
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +1 -0
- package/lib/sbom.mjs +147 -0
- package/lib/scan.mjs +93 -27
- package/lib/sets/cics.mjs +114 -0
- package/lib/sets/flow.mjs +82 -2
- package/lib/sets/jcl.mjs +67 -0
- package/lib/sets/semantics.mjs +377 -0
- package/lib/sets/web.mjs +34 -10
- package/lib/sets/zowe.mjs +220 -0
- package/lib/sources.mjs +10 -0
- package/lib/tui/app.mjs +31 -13
- package/lib/tui/model.mjs +17 -2
- package/lib/utilities.mjs +85 -13
- package/lib/verify.mjs +128 -0
- package/package.json +1 -1
- package/rules/compliance-dora.json +507 -0
- package/rules/compliance-ffiec.json +507 -0
- package/rules/compliance-nist80053.json +507 -0
- package/schema/cobolwork.policy.schema.json +2 -1
package/lib/ironwork.mjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// `ironwork check` over every program in a tree: IBM Enterprise COBOL's front end as ironwork models
|
|
3
|
+
// it, for an estate whose compile step runs on z/OS and so never meets the build gate otherwise.
|
|
4
|
+
// ironwork is run as a separate program, as cobc is; nothing here links it.
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { directoryTree } from './kernel/source-tree.mjs';
|
|
9
|
+
import { printable } from './kernel/printable.mjs';
|
|
10
|
+
import { buildFileIndex, VERBS } from './parser.mjs';
|
|
11
|
+
import { isProgram, relPath } from './sources.mjs';
|
|
12
|
+
import { EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS } from './words.mjs';
|
|
13
|
+
|
|
14
|
+
// ironwork check exits with IBM's highest return code: 0 clean, 4 warnings only (compiled), 8 E,
|
|
15
|
+
// 12 S, 16 U. Only 0 and 4 produce a program.
|
|
16
|
+
const COMPILED = new Set([0, 4]);
|
|
17
|
+
const REJECTED = new Set([8, 12, 16]);
|
|
18
|
+
// Warnings and informational lines follow the errors, and say nothing about why a program failed.
|
|
19
|
+
const NOT_AN_ERROR = /^(?:informational:|.*?: warning: )/;
|
|
20
|
+
const PER_PROGRAM_MS = 60000;
|
|
21
|
+
const MAX_LISTED = 10;
|
|
22
|
+
|
|
23
|
+
// ironwork refuses by name what it does not model yet; that says nothing about the program. Where its
|
|
24
|
+
// parser stops at a statement it does not read (ENTRY, ALTER), or at a name the CICS, DL/I or SQL
|
|
25
|
+
// translator declares, it says so without the name, so both are read as its gap, not the program's.
|
|
26
|
+
const NOT_SUPPORTED = /\bnot supported\b/i;
|
|
27
|
+
const TRANSLATOR_NAMES = [EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS];
|
|
28
|
+
function notModelled(message) {
|
|
29
|
+
if (NOT_SUPPORTED.test(message)) return true;
|
|
30
|
+
const verb = /^a statement, found ([A-Z][A-Z-]*)$/.exec(message);
|
|
31
|
+
if (verb) return VERBS.has(verb[1]);
|
|
32
|
+
const name = /^([A-Z][A-Z0-9-]*) is not defined$/.exec(message);
|
|
33
|
+
return !!name && TRANSLATOR_NAMES.some((s) => s.has(name[1]));
|
|
34
|
+
}
|
|
35
|
+
// A copy library the tree does not hold leaves the program unread, as it does for the scan.
|
|
36
|
+
const UNRESOLVED = /\bno such member in the copy libraries\b/i;
|
|
37
|
+
// A diagnostic can quote a literal from the program, and no output of the gate carries source text.
|
|
38
|
+
const redact = (s) => String(s).replace(/'[^']*'|"[^"]*"/g, "'…'");
|
|
39
|
+
|
|
40
|
+
// `file:line:col: message` or `file: message`, as ironwork's Error::place writes them. `file` is the
|
|
41
|
+
// program as it was passed, or the COPY member the position is in.
|
|
42
|
+
function diagnostic(line, programPath, rel) {
|
|
43
|
+
const m = /^(.*?):(\d+):(\d+): (.*)$/.exec(line) || /^(.*?): (.*)$/.exec(line);
|
|
44
|
+
if (!m) return { path: rel, message: printable(redact(line), 200) };
|
|
45
|
+
const [file, ln, col, text] = m.length === 5 ? [m[1], Number(m[2]), Number(m[3]), m[4]] : [m[1], null, null, m[2]];
|
|
46
|
+
const inProgram = resolve(file) === programPath;
|
|
47
|
+
return {
|
|
48
|
+
path: rel, ...(ln ? { line: ln, col } : {}), ...(inProgram ? {} : { member: printable(file, 80) }),
|
|
49
|
+
message: printable(redact(text), 200),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ironworkVersion(path, { env = process.env } = {}) {
|
|
54
|
+
const r = spawnSync(path, ['--version'], { cwd: tmpdir(), env, encoding: 'utf8', timeout: 10000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
55
|
+
return r.status === 0 ? printable(String(r.stdout).trim(), 80) || null : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Runs `ironwork check` on each program under `root`, with the tree's copy directories and the
|
|
59
|
+
// estate's copy libraries as -I, and sorts the programs into those it accepts, those it rejects,
|
|
60
|
+
// and those it could not decide: a construct it does not model, a copybook the tree lacks, or a
|
|
61
|
+
// run that ended some other way.
|
|
62
|
+
export function checkWithIronwork(path, root, { allow = null, copylibs = [], env = process.env } = {}) {
|
|
63
|
+
const programs = [...new Set(directoryTree(root).list())].filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
|
|
64
|
+
const libraries = [...buildFileIndex(root).copyDirs, ...copylibs].flatMap((d) => ['-I', d]);
|
|
65
|
+
const out = { programs: programs.length, accepted: 0, warned: 0, failed: [], notModelled: [], unresolved: [], unrun: [] };
|
|
66
|
+
for (const file of programs) {
|
|
67
|
+
const rel = relPath(root, file);
|
|
68
|
+
const r = spawnSync(path, ['check', file, ...libraries], {
|
|
69
|
+
cwd: tmpdir(), env, encoding: 'utf8', timeout: PER_PROGRAM_MS, maxBuffer: 1 << 20, windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'],
|
|
70
|
+
});
|
|
71
|
+
if (COMPILED.has(r.status)) { out.accepted++; if (r.status === 4) out.warned++; continue; }
|
|
72
|
+
if (!REJECTED.has(r.status)) {
|
|
73
|
+
const why = r.error ? (r.error.code === 'ETIMEDOUT' ? `no answer in ${PER_PROGRAM_MS / 1000}s` : r.error.message) : `ironwork exited ${r.status ?? r.signal}`;
|
|
74
|
+
out.unrun.push({ path: rel, why: printable(why, 200) });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const found = String(r.stderr || '').split(/\r?\n/).filter((l) => l && !NOT_AN_ERROR.test(l)).map((l) => diagnostic(l, resolve(file), rel));
|
|
78
|
+
const errors = found.filter((d) => !notModelled(d.message) && !UNRESOLVED.test(d.message));
|
|
79
|
+
if (errors.length) out.failed.push({ ...errors[0], errors: errors.length });
|
|
80
|
+
else if (found.some((d) => UNRESOLVED.test(d.message))) out.unresolved.push(found.find((d) => UNRESOLVED.test(d.message)));
|
|
81
|
+
else out.notModelled.push(found[0] || { path: rel, message: `ironwork exited ${r.status} and gave no error` });
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The compile check from a checkWithIronwork result: false where a program does not compile, null
|
|
87
|
+
// where every program that did not pass is one ironwork could not decide, true otherwise.
|
|
88
|
+
export function ironworkVerdict(result) {
|
|
89
|
+
if (result.failed.length) return false;
|
|
90
|
+
return result.notModelled.length || result.unresolved.length || result.unrun.length ? null : true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function ironworkReasons(result) {
|
|
94
|
+
const reasons = [];
|
|
95
|
+
const at = (d) => `${d.path}${d.line ? `:${d.line}:${d.col}` : ''}${d.member ? ` (in ${d.member})` : ''}`;
|
|
96
|
+
if (result.failed.length) {
|
|
97
|
+
reasons.push(`ironwork check: ${result.failed.length} of ${result.programs} program(s) do not compile as Enterprise COBOL`);
|
|
98
|
+
for (const d of result.failed.slice(0, MAX_LISTED)) reasons.push(`${at(d)}: ${d.message}`);
|
|
99
|
+
}
|
|
100
|
+
if (result.notModelled.length) reasons.push(`ironwork check: ${result.notModelled.length} program(s) use what ironwork does not model yet, so whether they compile is not decided; the first is ${at(result.notModelled[0])}: ${result.notModelled[0].message}`);
|
|
101
|
+
if (result.unresolved.length) reasons.push(`ironwork check: ${result.unresolved.length} program(s) copy a member the copy libraries do not hold; name the estate's with --copylib`);
|
|
102
|
+
if (result.unrun.length) reasons.push(`ironwork check: ${result.unrun.length} program(s) were not checked; the first, ${result.unrun[0].path}: ${result.unrun[0].why}`);
|
|
103
|
+
return reasons;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const listed = (result) => ({
|
|
107
|
+
programs: result.programs, accepted: result.accepted, warned: result.warned,
|
|
108
|
+
failed: result.failed.slice(0, MAX_LISTED), notModelled: result.notModelled.slice(0, MAX_LISTED),
|
|
109
|
+
unresolved: result.unresolved.slice(0, MAX_LISTED), unrun: result.unrun.slice(0, MAX_LISTED),
|
|
110
|
+
counts: { failed: result.failed.length, notModelled: result.notModelled.length, unresolved: result.unresolved.length, unrun: result.unrun.length },
|
|
111
|
+
});
|
package/lib/kernel/findings.mjs
CHANGED
|
@@ -42,6 +42,17 @@ export const EVIDENCE = Object.freeze({
|
|
|
42
42
|
context: 'describes the estate - an entry point, a product in use - and asserts no defect',
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
// Whether an attacker can use a path finding, most urgent first. lib/exploitability.mjs decides it.
|
|
46
|
+
export const EXPLOITABILITY = Object.freeze({
|
|
47
|
+
confirmed: 'the estate reproduced it on a system it owns and brought the result as a witness feed',
|
|
48
|
+
exploitable: 'input an attacker supplies reaches the operation with no check on every route, and the estate declares an entry that carries it open to any user',
|
|
49
|
+
'attacker-driven': 'input whoever runs an entry supplies reaches the operation with no check on every route; the estate has not declared who may run that entry',
|
|
50
|
+
restricted: 'input reaches the operation with no check on every route, and every entry that carries it is behind a control the estate named',
|
|
51
|
+
mitigated: 'input reaches the operation, but a check runs first on every route, or SSRANGE turns an overrun into an abend',
|
|
52
|
+
upstream: 'the value comes from a file record or a database row, so whoever can write that data drives it',
|
|
53
|
+
refuted: 'no harmful value arrives: a check leaves only values the operation can take, or no route reaches it',
|
|
54
|
+
});
|
|
55
|
+
|
|
45
56
|
export const WHO_ACTS = Object.freeze({
|
|
46
57
|
path: "the program's owner",
|
|
47
58
|
construct: 'the owner, or whoever can rotate a credential',
|
package/lib/kernel/registry.mjs
CHANGED
|
@@ -25,6 +25,8 @@ import { scanWeb, WEB_RULES } from '../sets/web.mjs';
|
|
|
25
25
|
import { scanPriv, PRIV_RULES } from '../sets/priv.mjs';
|
|
26
26
|
import { scanLog, LOG_RULES } from '../sets/log.mjs';
|
|
27
27
|
import { scanCompile, COMPILE_RULES } from '../sets/compile.mjs';
|
|
28
|
+
import { scanSemantics, SEMANTICS_RULES } from '../sets/semantics.mjs';
|
|
29
|
+
import { scanZowe, ZOWE_RULES } from '../sets/zowe.mjs';
|
|
28
30
|
import { toolName } from './ruleset.mjs';
|
|
29
31
|
|
|
30
32
|
// The order is the order a scan runs them and the order --only lists them. It is not significant
|
|
@@ -43,6 +45,8 @@ export const REGISTRY = [
|
|
|
43
45
|
{ name: 'compile', scan: scanCompile, rules: COMPILE_RULES },
|
|
44
46
|
{ name: 'priv', scan: scanPriv, rules: PRIV_RULES },
|
|
45
47
|
{ name: 'log', scan: scanLog, rules: LOG_RULES },
|
|
48
|
+
{ name: 'semantics', scan: scanSemantics, rules: SEMANTICS_RULES },
|
|
49
|
+
{ name: 'zowe', scan: scanZowe, rules: ZOWE_RULES },
|
|
46
50
|
];
|
|
47
51
|
|
|
48
52
|
// Re-exported from the kernel's ruleset module, which is where it has to live: this file imports
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Which compiler options a change to a program's CBL and PROCESS cards changes, for the families
|
|
3
|
+
// that change arithmetic, validation or collation without touching a statement
|
|
4
|
+
// (docs/spec/evidence.md §13.4). Spellings come from IBM's option table (lib/enterprise-options.mjs).
|
|
5
|
+
import { OPTION_SPELLINGS } from './enterprise-options.mjs';
|
|
6
|
+
import { optionCards, optionTokens } from './options.mjs';
|
|
7
|
+
|
|
8
|
+
export const WATCHED = ['TRUNC', 'ARITH', 'NUMPROC', 'NUMCHECK', 'SSRANGE', 'CODEPAGE', 'INTDATE'];
|
|
9
|
+
const ARITH_SUBS = { C: 'COMPAT', COMPAT: 'COMPAT', E: 'EXTEND', EXTEND: 'EXTEND' };
|
|
10
|
+
|
|
11
|
+
// One option token as `FAMILY(SUB,...)` or `NOFAMILY`, or null for a family not watched.
|
|
12
|
+
function settingOf(token) {
|
|
13
|
+
const m = /^([A-Z0-9-]+)(?:\((.*)\))?$/.exec(token);
|
|
14
|
+
if (!m) return null;
|
|
15
|
+
const spelled = OPTION_SPELLINGS[m[1]];
|
|
16
|
+
if (!spelled || !WATCHED.includes(spelled[0])) return null;
|
|
17
|
+
const [family, off] = spelled;
|
|
18
|
+
if (off) return { family, value: `NO${family}` };
|
|
19
|
+
let subs = m[2] === undefined ? [] : optionTokens(m[2]);
|
|
20
|
+
if (family === 'ARITH') subs = subs.map((s) => ARITH_SUBS[s] || s);
|
|
21
|
+
if (family === 'NUMCHECK' || family === 'SSRANGE') subs = [...subs].sort();
|
|
22
|
+
return { family, value: subs.length ? `${family}(${subs.join(',')})` : family };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The watched options a program's cards set, the later card winning, each with its line.
|
|
26
|
+
export function optionsInForce(text) {
|
|
27
|
+
const state = new Map();
|
|
28
|
+
for (const card of optionCards(text)) {
|
|
29
|
+
for (const token of card.options) {
|
|
30
|
+
const s = settingOf(token);
|
|
31
|
+
if (s) state.set(s.family, { value: s.value, line: card.line });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function optionChanges(baseText, headText) {
|
|
38
|
+
const before = optionsInForce(baseText);
|
|
39
|
+
const after = optionsInForce(headText);
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const family of WATCHED) {
|
|
42
|
+
const b = before.get(family);
|
|
43
|
+
const a = after.get(family);
|
|
44
|
+
const base = b ? b.value : 'default';
|
|
45
|
+
const head = a ? a.value : 'default';
|
|
46
|
+
if (base !== head) out.push({ option: family, base, head, line: (a || b).line });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
package/lib/options.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Whether the options a program is compiled with generate the run-time checks a policy requires:
|
|
3
3
|
// docs/spec/build-gate.md §7. Enterprise COBOL's options and what they mean are from IBM's
|
|
4
|
-
// documentation; the GnuCOBOL flags are recorded in provenance/compiler-options.json with
|
|
5
|
-
// was confirmed by compiling. Nothing here is taken from
|
|
4
|
+
// documentation; the GnuCOBOL and gcobol flags are recorded in provenance/compiler-options.json with
|
|
5
|
+
// how each was confirmed by compiling. Nothing here is taken from either compiler's source (spec §16).
|
|
6
6
|
import { basename, posix } from 'node:path';
|
|
7
7
|
import { PROGRAM_EXT } from './sources.mjs';
|
|
8
|
+
import { OPTION_SPELLINGS } from './enterprise-options.mjs';
|
|
8
9
|
|
|
9
10
|
// Split on blanks and commas outside parentheses: SSRANGE(NOZLEN,ABD) is one option.
|
|
10
11
|
export function optionTokens(text) {
|
|
@@ -43,13 +44,10 @@ const parse = (token) => {
|
|
|
43
44
|
return m ? { name: m[1], subs: m[2] === undefined ? [] : optionTokens(m[2]) } : { name: token, subs: [] };
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
// Each family's spellings
|
|
47
|
-
// gate cannot read counts as absent, which fails closed.
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
NUMCHECK: { on: ['NUMCHECK'], off: ['NONUMCHECK'] },
|
|
51
|
-
PARMCHECK: { on: ['PARMCHECK', 'PC'], off: ['NOPARMCHECK', 'NOPC'] },
|
|
52
|
-
};
|
|
47
|
+
// Each family's spellings, as IBM's option table gives them. A spelling the table does not hold is
|
|
48
|
+
// not accepted, so an option the gate cannot read counts as absent, which fails closed.
|
|
49
|
+
const spellingsOf = (option, off) => Object.keys(OPTION_SPELLINGS).filter((s) => OPTION_SPELLINGS[s][0] === option && OPTION_SPELLINGS[s][1] === off);
|
|
50
|
+
const FAMILIES = Object.fromEntries(['SSRANGE', 'NUMCHECK', 'PARMCHECK'].map((f) => [f, { on: spellingsOf(f, false), off: spellingsOf(f, true) }]));
|
|
53
51
|
const FAMILY_OF_CHECK = { subscript: 'SSRANGE', 'reference-modification': 'SSRANGE', 'numeric-data': 'NUMCHECK', 'argument-length': 'PARMCHECK' };
|
|
54
52
|
|
|
55
53
|
// Whether one family's setting abends on a failed check. A bare SSRANGE is SSRANGE(NOZLEN,ABD); a
|
|
@@ -121,7 +119,17 @@ const EXCEPTION_OF_CHECK = {
|
|
|
121
119
|
'argument-length': 'EC-PROGRAM-ARG-MISMATCH',
|
|
122
120
|
};
|
|
123
121
|
|
|
124
|
-
|
|
122
|
+
// gcobol 16 refuses to compile with these enabled ("sorry, unimplemented"), and EC-ALL does not
|
|
123
|
+
// generate them.
|
|
124
|
+
const GCOBOL_UNIMPLEMENTED = new Set(['EC-DATA-INCOMPATIBLE', 'EC-PROGRAM-ARG-MISMATCH']);
|
|
125
|
+
|
|
126
|
+
// GnuCOBOL's cobc, or GCC's gcobol under its versioned or cross-compiler names.
|
|
127
|
+
export function compilerOf(command) {
|
|
128
|
+
const name = basename(String(command || '')).replace(PREFIX, '');
|
|
129
|
+
if (/^cobc(\.exe)?$/i.test(name)) return 'gnucobol';
|
|
130
|
+
if (/^(?:[a-z0-9_]+-)*gcobol(?:-\d+(?:\.\d+)*)?(\.exe)?$/i.test(name)) return 'gcobol';
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
125
133
|
|
|
126
134
|
const exceptionName = (v) => {
|
|
127
135
|
const u = String(v).toUpperCase();
|
|
@@ -251,17 +259,21 @@ const argsOf = (words) => {
|
|
|
251
259
|
return out;
|
|
252
260
|
};
|
|
253
261
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
262
|
+
// Text that may run cobc or gcobol.
|
|
263
|
+
const NAMES_A_COMPILER = /cobc|gcobol/i;
|
|
264
|
+
|
|
265
|
+
// The compiler and argument vector of each cobc or gcobol command in one simple command. A command
|
|
266
|
+
// held in one word - sh -c "…", an alias, a CMD's string - is read as a command line of its own.
|
|
267
|
+
function compilerCommands(words, dialect, depth = 0) {
|
|
257
268
|
const at = leadOf(words);
|
|
258
269
|
if (at < 0 || MESSAGE.test(words[at].replace(PREFIX, ''))) return [];
|
|
259
270
|
for (let i = at; i < words.length; i++) {
|
|
260
271
|
const w = words[i];
|
|
261
|
-
|
|
272
|
+
const compiler = compilerOf(w);
|
|
273
|
+
if (compiler) return [{ compiler, args: argsOf(words.slice(i + 1)) }];
|
|
262
274
|
const inner = w.replace(/^[A-Za-z_]\w*=/, '');
|
|
263
|
-
if (depth < 2 && /\s/.test(inner) &&
|
|
264
|
-
const found = commandsOf(shellTokens(inner, dialect)).flatMap((ws) =>
|
|
275
|
+
if (depth < 2 && /\s/.test(inner) && NAMES_A_COMPILER.test(inner)) {
|
|
276
|
+
const found = commandsOf(shellTokens(inner, dialect)).flatMap((ws) => compilerCommands(ws, dialect, depth + 1));
|
|
265
277
|
if (found.length) return found;
|
|
266
278
|
}
|
|
267
279
|
}
|
|
@@ -272,15 +284,15 @@ function cobcCommands(words, dialect, depth = 0) {
|
|
|
272
284
|
const changeDir = (dir, to) => (!to || /[$%~`]/.test(to) || /^(?:\/|[A-Za-z]:[\\/]|-$)/.test(to) ? null
|
|
273
285
|
: posix.normalize(posix.join(dir || '.', to.replace(/\\/g, '/'))));
|
|
274
286
|
|
|
275
|
-
// The
|
|
276
|
-
// `state.dir` carries a cd forward to later lines, for a script whose lines one shell runs.
|
|
287
|
+
// The compiler commands in a command line, each with the directory a cd before it on the line moved
|
|
288
|
+
// to. `state.dir` carries a cd forward to later lines, for a script whose lines one shell runs.
|
|
277
289
|
function commandLine(text, dialect, state, line, out) {
|
|
278
290
|
for (const words of commandsOf(Array.isArray(text) ? text.map((word) => ({ word })) : shellTokens(text, dialect))) {
|
|
279
291
|
const at = leadOf(words);
|
|
280
292
|
const lead = at >= 0 ? words[at].replace(PREFIX, '').toLowerCase() : '';
|
|
281
293
|
if (lead === 'cd' || lead === 'pushd' || lead === 'set-location') { state.dir = changeDir(state.dir, words.slice(at + 1).find((w) => !/^-./.test(w))); continue; }
|
|
282
|
-
for (const args of
|
|
283
|
-
if (
|
|
294
|
+
for (const { compiler, args } of compilerCommands(words, dialect)) {
|
|
295
|
+
if (sourceOperands(args, compiler).length) out.push({ line, compiler, args, ...(state.dir && state.dir !== '.' ? { dir: state.dir } : {}) });
|
|
284
296
|
}
|
|
285
297
|
}
|
|
286
298
|
}
|
|
@@ -328,11 +340,12 @@ function expanded(text, vars, kind) {
|
|
|
328
340
|
return t;
|
|
329
341
|
}
|
|
330
342
|
|
|
331
|
-
// Every cobc command a build script runs that names a source file, with the variables the
|
|
332
|
-
// assigns substituted: [{ line, args, dir? }]. `kind` is make, sh, yaml, docker,
|
|
333
|
-
// A command whose options come from a variable set elsewhere keeps the
|
|
334
|
-
// option; one that names no source - cobc --version, which cobc -
|
|
335
|
-
|
|
343
|
+
// Every cobc or gcobol command a build script runs that names a source file, with the variables the
|
|
344
|
+
// same file assigns substituted: [{ line, compiler, args, dir? }]. `kind` is make, sh, yaml, docker,
|
|
345
|
+
// batch or powershell. A command whose options come from a variable set elsewhere keeps the
|
|
346
|
+
// reference, which reads as no option; one that names no source - cobc --version, which cobc -
|
|
347
|
+
// compiles nothing and is left out.
|
|
348
|
+
export function compilerInvocations(text, { kind = 'sh' } = {}) {
|
|
336
349
|
const dialect = kind === 'batch' || kind === 'powershell' ? kind : 'sh';
|
|
337
350
|
const vars = new Map();
|
|
338
351
|
const continuation = { batch: /\^$/, powershell: /`$/ }[kind] || /\\$/;
|
|
@@ -403,11 +416,11 @@ function looseJson(text) {
|
|
|
403
416
|
return JSON.parse(out.replace(/,(\s*[}\]])/g, '$1'));
|
|
404
417
|
}
|
|
405
418
|
|
|
406
|
-
// The cobc commands an editor's task file runs: VS Code's tasks.json. A task's command
|
|
407
|
-
// command and its arguments, read as its shell reads them, with the variants it gives for
|
|
408
|
-
// Linux and macOS; it runs in the workspace folder, or in its options.cwd. ${workspaceFolder}
|
|
409
|
-
// folder, and a variable that names the open file stands for any file.
|
|
410
|
-
export function
|
|
419
|
+
// The cobc and gcobol commands an editor's task file runs: VS Code's tasks.json. A task's command
|
|
420
|
+
// line is its command and its arguments, read as its shell reads them, with the variants it gives for
|
|
421
|
+
// Windows, Linux and macOS; it runs in the workspace folder, or in its options.cwd. ${workspaceFolder}
|
|
422
|
+
// is that folder, and a variable that names the open file stands for any file.
|
|
423
|
+
export function compilerTasks(text) {
|
|
411
424
|
let doc;
|
|
412
425
|
try { doc = looseJson(text); } catch { return []; }
|
|
413
426
|
const lines = String(text).split(/\r?\n/);
|
|
@@ -428,7 +441,7 @@ export function cobcTasks(text) {
|
|
|
428
441
|
for (const [variant, dialect] of [[task, 'sh'], [task.linux, 'sh'], [task.osx, 'sh'], [task.windows, 'powershell']]) {
|
|
429
442
|
if (!variant || typeof variant !== 'object') continue;
|
|
430
443
|
const command = valueOf(variant.command);
|
|
431
|
-
if (typeof command !== 'string' ||
|
|
444
|
+
if (typeof command !== 'string' || !NAMES_A_COMPILER.test(`${command} ${JSON.stringify(variant.args || task.args || [])}`)) continue;
|
|
432
445
|
const args = Array.isArray(variant.args) ? variant.args : Array.isArray(task.args) ? task.args : [];
|
|
433
446
|
const cwd = (variant.options && variant.options.cwd) || (task.options && task.options.cwd);
|
|
434
447
|
const dir = typeof cwd === 'string' ? changeDir(null, folder(cwd)) : null;
|
|
@@ -443,19 +456,25 @@ export function cobcTasks(text) {
|
|
|
443
456
|
}
|
|
444
457
|
|
|
445
458
|
// Options whose value is the next argument, which is not a source file: the output file, include and
|
|
446
|
-
// library directories, a library,
|
|
447
|
-
//
|
|
448
|
-
|
|
459
|
+
// library directories, a library, options for the C compiler and the linker, and for gcobol a CDF
|
|
460
|
+
// name, a language, a dialect, a copybook extension and an included file. Each was observed to take
|
|
461
|
+
// the next argument (provenance/compiler-options.json). gcobol's -main takes none: it marks the source
|
|
462
|
+
// that follows it.
|
|
463
|
+
const TAKES_VALUE = {
|
|
464
|
+
gnucobol: new Set(['-o', '-I', '-L', '-l', '-A', '-Q', '-fec', '-fno-ec']),
|
|
465
|
+
gcobol: new Set(['-o', '-I', '-L', '-l', '-D', '-x', '-dialect', '-copyext', '-include', '-fcobol-exceptions', '-fno-cobol-exceptions']),
|
|
466
|
+
};
|
|
449
467
|
const SOURCE_FILE = new RegExp(`(?:${PROGRAM_EXT.map((e) => e.replace('.', '\\.')).join('|')})$`, 'i');
|
|
450
468
|
// A variable, a batch argument, a glob or find's {}: a file the command names only when it runs.
|
|
451
469
|
const UNNAMED = /[$%*?!]|\{\}/;
|
|
452
470
|
|
|
453
|
-
// The operands of a cobc command that name source files, as written.
|
|
454
|
-
export function
|
|
471
|
+
// The operands of a cobc or gcobol command that name source files, as written.
|
|
472
|
+
export function sourceOperands(args, compiler = 'gnucobol') {
|
|
473
|
+
const takesValue = TAKES_VALUE[compiler];
|
|
455
474
|
const out = [];
|
|
456
475
|
for (let i = 0; i < args.length; i++) {
|
|
457
476
|
const a = String(args[i]);
|
|
458
|
-
if (
|
|
477
|
+
if (takesValue.has(a)) { i++; continue; }
|
|
459
478
|
if (a.startsWith('-')) continue;
|
|
460
479
|
if (SOURCE_FILE.test(a) || UNNAMED.test(a)) out.push(a);
|
|
461
480
|
}
|
|
@@ -486,11 +505,6 @@ export function cobcChecks(args, { required, forbid = [] }) {
|
|
|
486
505
|
if (value === undefined) continue;
|
|
487
506
|
set(exceptionName(value), !m[1], shown);
|
|
488
507
|
}
|
|
489
|
-
const forbidden = [];
|
|
490
|
-
for (const entry of forbid) {
|
|
491
|
-
const hit = args.find((a) => a === entry || String(a).startsWith(`${entry}=`));
|
|
492
|
-
if (hit) forbidden.push(`${hit} is on the command line, and the policy forbids it`);
|
|
493
|
-
}
|
|
494
508
|
const add = [];
|
|
495
509
|
const checks = required.map((check) => {
|
|
496
510
|
if (state.get(check) === true) return { check, ok: true, why: 'turned on by the command line' };
|
|
@@ -499,14 +513,63 @@ export function cobcChecks(args, { required, forbid = [] }) {
|
|
|
499
513
|
if (!add.includes(flag)) add.push(flag);
|
|
500
514
|
return { check, ok: true, added: true, why: `added as ${flag}` };
|
|
501
515
|
});
|
|
502
|
-
return { checks, forbidden, add };
|
|
516
|
+
return { checks, forbidden: forbiddenOn(args, forbid), add };
|
|
503
517
|
}
|
|
504
518
|
|
|
519
|
+
const forbiddenOn = (args, forbid) => forbid
|
|
520
|
+
.map((entry) => args.find((a) => a === entry || String(a).startsWith(`${entry}=`)))
|
|
521
|
+
.filter(Boolean).map((hit) => `${hit} is on the command line, and the policy forbids it`);
|
|
522
|
+
|
|
523
|
+
// A gcobol argument vector, as cobcChecks reads cobc's. -fcobol-exceptions enables each condition it
|
|
524
|
+
// lists; -fno-cobol-exceptions N withdraws the enabled conditions N covers, so after EC-BOUND it does
|
|
525
|
+
// not withdraw EC-BOUND-SUBSCRIPT, and it never stops a later enable. A check whose condition gcobol
|
|
526
|
+
// does not implement cannot be generated at all.
|
|
527
|
+
export function gcobolChecks(args, { required, forbid = [] }) {
|
|
528
|
+
let enabled = [];
|
|
529
|
+
const turnedOff = new Map();
|
|
530
|
+
const listed = (v) => String(v).split(',').map((n) => n.trim().toUpperCase()).filter(Boolean);
|
|
531
|
+
for (let i = 0; i < args.length; i++) {
|
|
532
|
+
const a = String(args[i]);
|
|
533
|
+
const m = /^-f(no-)?cobol-exceptions(?:=(.*))?$/.exec(a);
|
|
534
|
+
if (!m) continue;
|
|
535
|
+
const value = m[2] !== undefined ? m[2] : args[++i];
|
|
536
|
+
if (value === undefined) continue;
|
|
537
|
+
const shown = m[2] !== undefined ? a : `${a} ${value}`;
|
|
538
|
+
for (const name of listed(value)) {
|
|
539
|
+
for (const check of required) {
|
|
540
|
+
if (!covers(name, EXCEPTION_OF_CHECK[check])) continue;
|
|
541
|
+
if (m[1]) turnedOff.set(check, shown); else turnedOff.delete(check);
|
|
542
|
+
}
|
|
543
|
+
enabled = m[1] ? enabled.filter((e) => !covers(name, e)) : [...enabled, name];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const add = [];
|
|
547
|
+
const checks = required.map((check) => {
|
|
548
|
+
const condition = EXCEPTION_OF_CHECK[check];
|
|
549
|
+
if (GCOBOL_UNIMPLEMENTED.has(condition)) return { check, ok: false, why: `${check} needs ${condition}, which gcobol does not implement` };
|
|
550
|
+
if (enabled.some((e) => covers(e, condition))) return { check, ok: true, why: 'turned on by the command line' };
|
|
551
|
+
if (turnedOff.has(check)) return { check, ok: false, why: `${turnedOff.get(check)} turns off the ${check} check` };
|
|
552
|
+
const flag = `-fcobol-exceptions=${condition}`;
|
|
553
|
+
add.push(flag);
|
|
554
|
+
return { check, ok: true, added: true, why: `added as ${flag}` };
|
|
555
|
+
});
|
|
556
|
+
return { checks, forbidden: forbiddenOn(args, forbid), add };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const CHECKS_OF = { gnucobol: cobcChecks, gcobol: gcobolChecks };
|
|
560
|
+
const NOT_GIVEN = {
|
|
561
|
+
gnucobol: (check, c) => `${check} needs -fec=${c} or -debug, and the command gives neither`,
|
|
562
|
+
gcobol: (check, c) => `${check} needs -fcobol-exceptions=${c}, and the command does not give it`,
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// The checks one compiler's argument vector generates: cobcChecks or gcobolChecks.
|
|
566
|
+
export const compilerChecks = (compiler, args, opts) => CHECKS_OF[compiler](args, opts);
|
|
567
|
+
|
|
505
568
|
// The same, for a command the gate reads rather than runs: a check nothing turned on is missing.
|
|
506
|
-
export function
|
|
507
|
-
const r =
|
|
569
|
+
export function scriptedChecks(compiler, args, opts) {
|
|
570
|
+
const r = compilerChecks(compiler, args, opts);
|
|
508
571
|
return {
|
|
509
|
-
checks: r.checks.map((c) => (c.added ? { check: c.check, ok: false, why:
|
|
572
|
+
checks: r.checks.map((c) => (c.added ? { check: c.check, ok: false, why: NOT_GIVEN[compiler](c.check, EXCEPTION_OF_CHECK[c.check]) } : c)),
|
|
510
573
|
forbidden: r.forbidden,
|
|
511
574
|
};
|
|
512
575
|
}
|
package/lib/parser.mjs
CHANGED
|
@@ -1032,6 +1032,7 @@ function parseDataEntry(toks, section, file) {
|
|
|
1032
1032
|
j++;
|
|
1033
1033
|
continue;
|
|
1034
1034
|
}
|
|
1035
|
+
if (u === 'EXTERNAL') { item.external = true; j++; continue; }
|
|
1035
1036
|
if (u === 'SEPARATE') { item.signSeparate = true; j++; continue; }
|
|
1036
1037
|
if (u === 'SIGN' || u === 'LEADING' || u === 'TRAILING') { item.signExplicit = true; j++; continue; }
|
|
1037
1038
|
if (u === 'SYNC' || u === 'SYNCHRONIZED') { item.sync = true; j++; continue; }
|
|
@@ -1638,6 +1639,20 @@ function identifierTokens(seg, from, to) {
|
|
|
1638
1639
|
return out;
|
|
1639
1640
|
}
|
|
1640
1641
|
|
|
1642
|
+
// The constant an index name is shifted by where it stands alone as `NAME + n` or `NAME - n`; 0 for a
|
|
1643
|
+
// bare name and for any longer expression, which is judged as the bare name.
|
|
1644
|
+
function offsetOf(seg, j, open, close, colonAt) {
|
|
1645
|
+
const isNum = (t) => t && (t.t === 'word' || t.t === 'num') && /^\d+$/.test(t.v);
|
|
1646
|
+
const op = seg[j + 1];
|
|
1647
|
+
if (!op || op.t !== 'op' || (op.v !== '+' && op.v !== '-') || !isNum(seg[j + 2]) || j + 2 >= close) return 0;
|
|
1648
|
+
const prev = seg[j - 1];
|
|
1649
|
+
const starts = j - 1 === open || (j - 1 === colonAt) || (prev.t === 'word' && !['OF', 'IN', 'FUNCTION'].includes(prev.u));
|
|
1650
|
+
const after = seg[j + 3];
|
|
1651
|
+
const ends = j + 3 === close || j + 3 === colonAt || (after && after.t === 'word' && !['OF', 'IN'].includes(after.u));
|
|
1652
|
+
if (!starts || !ends) return 0;
|
|
1653
|
+
return op.v === '-' ? -Number(seg[j + 2].v) : Number(seg[j + 2].v);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1641
1656
|
// What a statement indexes with. identifierTokens leaves subscripts out, because a subscript is not
|
|
1642
1657
|
// the data a statement moves; a rule about where a statement reads or writes needs exactly them.
|
|
1643
1658
|
// For each parenthesised group after a data name - reached back over OF and IN, so ELEM OF TAB (I)
|
|
@@ -1671,7 +1686,7 @@ function indexTokens(seg) {
|
|
|
1671
1686
|
if (t.t !== 'word' || /^\d+$/.test(t.v)) continue;
|
|
1672
1687
|
const prev = seg[j - 1];
|
|
1673
1688
|
if (prev && prev.t === 'word' && (prev.u === 'OF' || prev.u === 'IN' || prev.u === 'FUNCTION')) continue;
|
|
1674
|
-
out.push({ host, tok: t, kind: colonAt < 0 ? 'subscript' : j < colonAt ? 'refmod-offset' : 'refmod-length' });
|
|
1689
|
+
out.push({ host, tok: t, kind: colonAt < 0 ? 'subscript' : j < colonAt ? 'refmod-offset' : 'refmod-length', offset: offsetOf(seg, j, k, close, colonAt) });
|
|
1675
1690
|
}
|
|
1676
1691
|
}
|
|
1677
1692
|
lastHost = host;
|
|
@@ -1700,22 +1715,29 @@ function untilTokens(seg) {
|
|
|
1700
1715
|
// AFTER J ... UNTIL c after it. A bare PERFORM UNTIL has a condition and no counter.
|
|
1701
1716
|
function loopsOf(seg) {
|
|
1702
1717
|
const out = [];
|
|
1703
|
-
let depth = 0, cur = null, inUntil = false;
|
|
1718
|
+
let depth = 0, cur = null, inUntil = false, into = null;
|
|
1704
1719
|
for (let k = 0; k < seg.length; k++) {
|
|
1705
1720
|
const x = seg[k];
|
|
1706
|
-
if (x.t === 'sep') { depth += x.v === '(' ? 1 : -1; if (inUntil && cur) cur.until.push(x); continue; }
|
|
1707
|
-
if (depth === 0 && x.t === 'word' && (x.u === 'VARYING' || x.u === 'AFTER')) {
|
|
1708
|
-
cur = { counter: seg[k + 1] && seg[k + 1].t === 'word' ? seg[k + 1] : null, until: [] };
|
|
1721
|
+
if (x.t === 'sep') { depth += x.v === '(' ? 1 : -1; if (inUntil && cur) cur.until.push(x); else if (into) into.push(x); continue; }
|
|
1722
|
+
if (depth === 0 && x.t === 'word' && (x.u === 'VARYING' || (x.u === 'AFTER' && !(k && seg[k - 1].u === 'TEST')))) {
|
|
1723
|
+
cur = { counter: seg[k + 1] && seg[k + 1].t === 'word' ? seg[k + 1] : null, from: [], by: null, until: [] };
|
|
1709
1724
|
out.push(cur);
|
|
1710
1725
|
inUntil = false;
|
|
1726
|
+
into = null;
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
if (depth === 0 && !inUntil && cur && x.t === 'word' && (x.u === 'FROM' || x.u === 'BY')) {
|
|
1730
|
+
into = x.u === 'FROM' ? cur.from : (cur.by = []);
|
|
1711
1731
|
continue;
|
|
1712
1732
|
}
|
|
1713
1733
|
if (depth === 0 && x.t === 'word' && x.u === 'UNTIL') {
|
|
1714
|
-
if (!cur) { cur = { counter: null, until: [] }; out.push(cur); }
|
|
1734
|
+
if (!cur) { cur = { counter: null, from: [], by: null, until: [] }; out.push(cur); }
|
|
1715
1735
|
inUntil = true;
|
|
1736
|
+
into = null;
|
|
1716
1737
|
continue;
|
|
1717
1738
|
}
|
|
1718
1739
|
if (inUntil && cur) cur.until.push(x);
|
|
1740
|
+
else if (into && k > 0) into.push(x);
|
|
1719
1741
|
}
|
|
1720
1742
|
return out.filter((l) => l.until.length);
|
|
1721
1743
|
}
|
|
@@ -1741,7 +1763,7 @@ function markReceiving(verb, seg, set) {
|
|
|
1741
1763
|
case 'READ': case 'RETURN': { const k = at(['INTO']); if (k >= 0) mark(k + 1, k + 2); break; }
|
|
1742
1764
|
case 'WRITE': case 'REWRITE': case 'RELEASE': { if (at(['FROM']) >= 0) mark(0, 1); break; }
|
|
1743
1765
|
case 'CALL': { const k = at(['RETURNING', 'GIVING']); if (k >= 0) mark(k + 1, k + 2); break; }
|
|
1744
|
-
case 'PERFORM': {
|
|
1766
|
+
case 'PERFORM': { for (let k = 0; k < seg.length; k++) if (seg[k].t === 'word' && (seg[k].u === 'VARYING' || (seg[k].u === 'AFTER' && !(k && seg[k - 1].u === 'TEST')))) mark(k + 1, k + 2); break; }
|
|
1745
1767
|
case 'SEARCH': { const v = at(['VARYING']); if (v >= 0) mark(v + 1, v + 2); break; }
|
|
1746
1768
|
default: break;
|
|
1747
1769
|
}
|
package/lib/policy.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { CLASSES, TIERS, TIER_RANK } from './consequence.mjs';
|
|
|
12
12
|
|
|
13
13
|
export const POLICY_FILE = 'cobolwork.policy.json';
|
|
14
14
|
export const CHECKS = ['subscript', 'reference-modification', 'numeric-data', 'argument-length'];
|
|
15
|
-
export const COMPILERS = ['enterprise', 'gnucobol'];
|
|
15
|
+
export const COMPILERS = ['enterprise', 'gnucobol', 'gcobol'];
|
|
16
16
|
|
|
17
17
|
export const DEFAULT_POLICY = Object.freeze({
|
|
18
18
|
policyVersion: 1,
|
|
@@ -24,9 +24,12 @@ export const DEFAULT_POLICY = Object.freeze({
|
|
|
24
24
|
options: 'warn',
|
|
25
25
|
waivers: { maxDays: 180 },
|
|
26
26
|
checks: ['subscript', 'reference-modification'],
|
|
27
|
-
forbid: { enterprise: [], gnucobol: [] },
|
|
27
|
+
forbid: { enterprise: [], gnucobol: [], gcobol: [] },
|
|
28
|
+
requireEquivalence: 'never',
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
export const EQUIVALENCE_MODES = ['never', 'machineAuthored', 'always'];
|
|
32
|
+
|
|
30
33
|
const KEYS = Object.keys(DEFAULT_POLICY);
|
|
31
34
|
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
32
35
|
const isStrings = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string' && x.trim() !== '');
|
|
@@ -60,8 +63,9 @@ export function validatePolicy(raw) {
|
|
|
60
63
|
if (!isStrings(raw.checks)) errs.push(`checks must be a list drawn from ${CHECKS.join(', ')}`);
|
|
61
64
|
else for (const c of raw.checks) if (!CHECKS.includes(c)) errs.push(`checks: ${printable(c, 40)} is not one of ${CHECKS.join(', ')}`);
|
|
62
65
|
}
|
|
66
|
+
if ('requireEquivalence' in raw && !EQUIVALENCE_MODES.includes(raw.requireEquivalence)) errs.push(`requireEquivalence takes ${EQUIVALENCE_MODES.join(', ')}`);
|
|
63
67
|
if ('forbid' in raw) {
|
|
64
|
-
if (!isObject(raw.forbid)) errs.push(`forbid must be an object keyed by ${COMPILERS.join('
|
|
68
|
+
if (!isObject(raw.forbid)) errs.push(`forbid must be an object keyed by ${COMPILERS.join(', ')}`);
|
|
65
69
|
else for (const [c, list] of Object.entries(raw.forbid)) {
|
|
66
70
|
if (!COMPILERS.includes(c)) errs.push(`forbid: ${printable(c, 40)} is not one of ${COMPILERS.join(', ')}`);
|
|
67
71
|
else if (!isStrings(list)) errs.push(`forbid.${c} must be a list of options`);
|
|
@@ -137,6 +141,7 @@ export function combinePolicies({ floor = null, repository = null } = {}) {
|
|
|
137
141
|
waivers: pick('waivers', (a, b) => ({ maxDays: Math.min(a.maxDays, b.maxDays) })),
|
|
138
142
|
checks: checks.value,
|
|
139
143
|
forbid: Object.fromEntries(COMPILERS.map((c) => [c, union((f.forbid || {})[c] || [], (r.forbid || {})[c] || [])])),
|
|
144
|
+
requireEquivalence: pick('requireEquivalence', (a, b) => (EQUIVALENCE_MODES.indexOf(a) >= EQUIVALENCE_MODES.indexOf(b) ? a : b)),
|
|
140
145
|
rules: {},
|
|
141
146
|
};
|
|
142
147
|
setBy.checks = checks.by;
|
package/lib/precompile.mjs
CHANGED
|
@@ -182,6 +182,10 @@ function callRoles(toks, read, write) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
export function hostVariableRoles(sql) {
|
|
186
|
+
return roles(tokenize(sql));
|
|
187
|
+
}
|
|
188
|
+
|
|
185
189
|
// The WHENEVER tests after each executable statement, from Db2's definitions of the conditions. IBM
|
|
186
190
|
// does not document which wins when two hold at once, so the order is fixed, not the source's.
|
|
187
191
|
const WHENEVER_TEST = {
|