@scientific-method/standard-checker 0.2.0 → 0.4.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 +13 -4
- package/dist/checks/entries.js +1 -0
- package/dist/checks/fields.d.ts +4 -0
- package/dist/checks/fields.js +314 -0
- package/dist/checks/formats.d.ts +6 -0
- package/dist/checks/formats.js +31 -4
- package/dist/checks/kaitai.d.ts +2 -1
- package/dist/checks/kaitai.js +47 -34
- package/dist/checks/rules.d.ts +7 -0
- package/dist/checks/rules.js +10 -6
- package/dist/context.d.ts +3 -1
- package/dist/options.d.ts +2 -0
- package/dist/options.js +7 -1
- package/dist/problems.d.ts +22 -3
- package/dist/problems.js +14 -3
- package/dist/standard-checker.js +19 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,13 @@ pnpm exec standard-checker --check # check, and fail on a stale index or PARI
|
|
|
13
13
|
|
|
14
14
|
It exits with 0 when the repository passes, 1 when it reports problems (one line per problem,
|
|
15
15
|
starting with the file's path), and 2 when the options are invalid. `--record-validation` cannot be
|
|
16
|
-
combined with `--check`.
|
|
16
|
+
combined with `--check`, nor `--require-ksc` with `--no-ksy`.
|
|
17
|
+
|
|
18
|
+
A pass ends with `spec check passed:` and the counts of entries, parity rows and deviations. When a
|
|
19
|
+
step of the check did not run, it ends with `spec check passed with skipped steps:`, the counts, and
|
|
20
|
+
`Skipped:` followed by each step and why, such as
|
|
21
|
+
`Skipped: Kaitai compilation of 2 definitions (--no-ksy).` A failing run lists the skipped steps
|
|
22
|
+
after its problems.
|
|
17
23
|
|
|
18
24
|
A problem that breaks a numbered rule of the standard ends with the rule's label in brackets, such
|
|
19
25
|
as `[STATUS-14]`. The standard opens that rule with the heading `###### STATUS-14`, anchored at
|
|
@@ -28,7 +34,8 @@ problems under other sections carry no label yet.
|
|
|
28
34
|
| `--root <dir>` | The repository to check. | the current directory |
|
|
29
35
|
| `--check` | Fail when an index or `PARITY.md` is stale, instead of rewriting it. | rewrite |
|
|
30
36
|
| `--base <ref>` | Also fail when a spec ID, area or deviation that exists at `<ref>` is gone. | where HEAD forked from `origin/$GITHUB_BASE_REF` or `origin/main`, when that resolves |
|
|
31
|
-
| `--no-ksy` | Skip compiling the Kaitai definitions in `spec/formats/`. | compile |
|
|
37
|
+
| `--no-ksy` | Skip compiling the Kaitai definitions in `spec/formats/`. The result line names the skipped compilation. | compile |
|
|
38
|
+
| `--require-ksc` | Fail when `spec/formats/` holds Kaitai definitions and no compiler is found, instead of passing with the compilation skipped. | pass with the compilation skipped |
|
|
32
39
|
| `--glossary <path>` | Also accept the terms of a draft glossary file, or of a directory of them. | none |
|
|
33
40
|
| `--code <dirs>` | Comma-separated directories whose files may cite spec and deviation IDs and hold `PLACEHOLDER` comments. | `src,tests,tools` |
|
|
34
41
|
| `--references <dirs>` | Comma-separated directories whose files may cite IDs but whose `PLACEHOLDER` comments do not count against parity. | none |
|
|
@@ -39,12 +46,14 @@ problems under other sections carry no label yet.
|
|
|
39
46
|
| `--help` | Print the options. | |
|
|
40
47
|
|
|
41
48
|
The `KSC` environment variable names the Kaitai Struct compiler. Without it, the checker looks for
|
|
42
|
-
`kaitai-struct-compiler` or `ksc` on `PATH
|
|
49
|
+
`kaitai-struct-compiler` or `ksc` on `PATH`. When it finds neither, it skips the compilation and
|
|
50
|
+
names it in the result line, or with `--require-ksc` reports the missing compiler as a problem.
|
|
43
51
|
|
|
44
52
|
## In GitHub Actions
|
|
45
53
|
|
|
46
54
|
The toolkit's `actions/check-documentation` composite action runs this checker with `--check`,
|
|
47
|
-
installs a pinned Kaitai compiler when the repository has `.ksy` files
|
|
55
|
+
installs a pinned Kaitai compiler when the repository has `.ksy` files (and then passes
|
|
56
|
+
`--require-ksc`), and exposes every option
|
|
48
57
|
above as an input. Pin the action to the toolkit commit whose `packages/standard-checker` matches
|
|
49
58
|
the version installed here, so CI and local runs apply the same checks.
|
|
50
59
|
|
package/dist/checks/entries.js
CHANGED
|
@@ -40,6 +40,7 @@ export function checkEntries(ctx) {
|
|
|
40
40
|
const names = {
|
|
41
41
|
enumNames: new Map(), // name -> format IDs
|
|
42
42
|
fieldNames: new Map(), // format ID -> Set of names
|
|
43
|
+
layouts: new Map(), // format ID -> layout Name -> Type
|
|
43
44
|
};
|
|
44
45
|
for (const [id, e] of entries) {
|
|
45
46
|
const { file, meta, kind } = e;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Context } from "../context.ts";
|
|
2
|
+
import type { FormatNames } from "./formats.ts";
|
|
3
|
+
/** Checks the field names of every live rule's procedure. Runs after checkRules, which sets Entry.code. */
|
|
4
|
+
export declare function checkFieldNames(ctx: Context, { layouts }: FormatNames): void;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// Field names in procedures: every name after a dot, on a structure whose type is known, is a Name in
|
|
2
|
+
// the layout of that structure's format.
|
|
3
|
+
//
|
|
4
|
+
// A type is known only where it is written in the notation's type form: `FMT-DATA-005`, a pointer to
|
|
5
|
+
// one (`PTR32<FMT-DATA-005>`) or a list of either (`FMT-DATA-005[]`). The checker reads it from the
|
|
6
|
+
// places the standard's Notation lists:
|
|
7
|
+
//
|
|
8
|
+
// - a `let` with a type, `let x = new FMT-...`, or a `let` whose value has a known type: another
|
|
9
|
+
// name or field, `copy` of one, a function whose `define` gives its result type, or a rule's
|
|
10
|
+
// `call` whose Outputs open with "Returns" and a type;
|
|
11
|
+
// - the rule's Parameters section, as `name: type`, and the parameters of a `define`;
|
|
12
|
+
// - for a name that is not a local, its glossary entry, as `name: type`, or a location in the form
|
|
13
|
+
// "field `x` of FMT-..." whose layout row has a type;
|
|
14
|
+
// - a `for each` loop variable takes the element type of the list it visits.
|
|
15
|
+
//
|
|
16
|
+
// A field takes the type in its layout row, so a chain such as `world.occupancy.cell_count` is
|
|
17
|
+
// followed row by row. An index into a list gives its element, and an index on a pointer gives the
|
|
18
|
+
// structure it points at; an index on anything else, or a field of a list, ends what is known. A
|
|
19
|
+
// format's mention in prose gives no type, since prose names formats for many reasons. A name whose
|
|
20
|
+
// type is not written in one of these forms, or that two declarations give different types, stays
|
|
21
|
+
// unchecked. A declaration that writes no type, such as a parameter the Parameters section describes
|
|
22
|
+
// in prose or an untyped parameter of a define, gives none and so differs from no other.
|
|
23
|
+
import { asList } from "../ids.js";
|
|
24
|
+
import { parameterNames, withoutCommentsAndStrings } from "./rules.js";
|
|
25
|
+
const sameType = (a, b) => !!a && !!b && a.format.join(" ") === b.format.join(" ") && a.pointer === b.pointer && a.list === b.list;
|
|
26
|
+
const FMT_ID = String.raw `FMT-[A-Z][A-Z0-9]*-\d{3,}`;
|
|
27
|
+
// A type in the notation that holds a structure: a format ID, a pointer to one, or a list of either.
|
|
28
|
+
const STRUCTURE_TYPE = String.raw `(?:[A-Z][A-Z0-9]*<)?${FMT_ID}>?(?:\[[^\]\n]*\])?`;
|
|
29
|
+
const WHOLE_TYPE = new RegExp(String.raw `^([A-Z][A-Z0-9]*<)?(${FMT_ID})(>)?(\[[^\]\n]*\])?$`);
|
|
30
|
+
// `name: type`, with the backticks around the whole or around each part.
|
|
31
|
+
const TYPED_NAME = new RegExp(String.raw `\x60([a-z_][a-z0-9_]*)\x60?\s*:\s*\x60?(${STRUCTURE_TYPE})(?![\w-])`, "g");
|
|
32
|
+
// Where the glossary says the game keeps a value: a field of a structure.
|
|
33
|
+
const LOCATION = new RegExp(String.raw `\bfield\s+\x60([A-Za-z_][A-Za-z0-9_]*)\x60\s+of\s+(${FMT_ID})|\x60([A-Za-z_][A-Za-z0-9_]*)\x60\s+field\s+of\s+(${FMT_ID})`, "g");
|
|
34
|
+
const RETURNS = new RegExp(String.raw `^Returns\s+(?:an?\s+)?\x60?(${STRUCTURE_TYPE})\x60?(?=[\s,.;]|$)`);
|
|
35
|
+
const LOWER = /[a-z_][a-z0-9_]*/y;
|
|
36
|
+
const FIELD = /[A-Za-z_][A-Za-z0-9_]*/y;
|
|
37
|
+
// Whether the bracket that opens at text[open] closes at the end of text.
|
|
38
|
+
function closesAtEnd(text, open) {
|
|
39
|
+
let depth = 0;
|
|
40
|
+
for (let k = open; k < text.length; k++) {
|
|
41
|
+
if (text[k] === "(")
|
|
42
|
+
depth++;
|
|
43
|
+
else if (text[k] === ")" && --depth === 0)
|
|
44
|
+
return k === text.length - 1;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// Reads a chain that starts at i, or returns null when text[i] does not start a lower-case name.
|
|
49
|
+
function readChain(text, i) {
|
|
50
|
+
LOWER.lastIndex = i;
|
|
51
|
+
const m = LOWER.exec(text);
|
|
52
|
+
if (!m)
|
|
53
|
+
return null;
|
|
54
|
+
const steps = [];
|
|
55
|
+
const upTo = [];
|
|
56
|
+
let j = i + m[0].length;
|
|
57
|
+
for (;;) {
|
|
58
|
+
let k = j;
|
|
59
|
+
while (text[k] === " ")
|
|
60
|
+
k++;
|
|
61
|
+
if (text[k] === "[") {
|
|
62
|
+
let depth = 0;
|
|
63
|
+
for (; k < text.length; k++) {
|
|
64
|
+
if (text[k] === "[")
|
|
65
|
+
depth++;
|
|
66
|
+
else if (text[k] === "]" && --depth === 0)
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (k >= text.length)
|
|
70
|
+
break;
|
|
71
|
+
j = k + 1;
|
|
72
|
+
steps.push(null);
|
|
73
|
+
upTo.push(text.slice(i, j));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (text[j] !== ".")
|
|
77
|
+
break;
|
|
78
|
+
FIELD.lastIndex = j + 1;
|
|
79
|
+
const f = FIELD.exec(text);
|
|
80
|
+
if (!f)
|
|
81
|
+
break;
|
|
82
|
+
j += 1 + f[0].length;
|
|
83
|
+
steps.push(f[0]);
|
|
84
|
+
upTo.push(text.slice(i, j));
|
|
85
|
+
}
|
|
86
|
+
return { base: m[0], steps, upTo, end: j };
|
|
87
|
+
}
|
|
88
|
+
/** Checks the field names of every live rule's procedure. Runs after checkRules, which sets Entry.code. */
|
|
89
|
+
export function checkFieldNames(ctx, { layouts }) {
|
|
90
|
+
const { problem } = ctx;
|
|
91
|
+
const { entries, glossary } = ctx.spec;
|
|
92
|
+
// A format ID and the entries it is split with, which define the same fields.
|
|
93
|
+
const formatOf = (id) => {
|
|
94
|
+
const e = entries.get(id);
|
|
95
|
+
if (!e || e.kind !== "FMT")
|
|
96
|
+
return null;
|
|
97
|
+
return [id, ...asList(e.meta.split_with).filter((x) => entries.get(x)?.kind === "FMT")].sort();
|
|
98
|
+
};
|
|
99
|
+
// The type a type written in the notation gives, or null for a type that holds no structure.
|
|
100
|
+
const typeOf = (text) => {
|
|
101
|
+
const m = WHOLE_TYPE.exec((text ?? "").replaceAll("`", "").trim());
|
|
102
|
+
if (!m || !m[1] !== !m[3])
|
|
103
|
+
return null;
|
|
104
|
+
const format = formatOf(m[2]);
|
|
105
|
+
return format ? { format, pointer: !!m[1], list: !!m[4] } : null;
|
|
106
|
+
};
|
|
107
|
+
// The types text gives names in the form `name: type`; a name given two types has none.
|
|
108
|
+
const typedNames = (text) => {
|
|
109
|
+
const found = new Map();
|
|
110
|
+
for (const m of text.matchAll(TYPED_NAME)) {
|
|
111
|
+
const type = typeOf(m[2]);
|
|
112
|
+
found.set(m[1], found.has(m[1]) && !sameType(found.get(m[1]), type) ? null : type);
|
|
113
|
+
}
|
|
114
|
+
return found;
|
|
115
|
+
};
|
|
116
|
+
// The layout rows of a format, or null when one of its entries has a malformed layout table. A
|
|
117
|
+
// field whose entries give it different types has no type.
|
|
118
|
+
const formatRows = new Map();
|
|
119
|
+
const plain = (type) => type.replaceAll("`", "").trim();
|
|
120
|
+
const rowsOf = (format) => {
|
|
121
|
+
const key = format.join(" ");
|
|
122
|
+
if (formatRows.has(key))
|
|
123
|
+
return formatRows.get(key);
|
|
124
|
+
let rows = new Map();
|
|
125
|
+
for (const id of format) {
|
|
126
|
+
const layout = layouts.get(id);
|
|
127
|
+
if (!layout) {
|
|
128
|
+
rows = null;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
for (const [name, type] of layout)
|
|
132
|
+
rows.set(name, rows.has(name) && plain(rows.get(name)) !== plain(type) ? "" : type);
|
|
133
|
+
}
|
|
134
|
+
formatRows.set(key, rows);
|
|
135
|
+
return rows;
|
|
136
|
+
};
|
|
137
|
+
const termTypes = new Map();
|
|
138
|
+
const termType = (term) => {
|
|
139
|
+
if (termTypes.has(term))
|
|
140
|
+
return termTypes.get(term);
|
|
141
|
+
const text = glossary.get(term) ?? "";
|
|
142
|
+
const typed = typedNames(text);
|
|
143
|
+
let type = null;
|
|
144
|
+
if (typed.has(term))
|
|
145
|
+
type = typed.get(term);
|
|
146
|
+
else {
|
|
147
|
+
const locations = [...text.matchAll(LOCATION)];
|
|
148
|
+
if (locations.length === 1) {
|
|
149
|
+
const format = formatOf(locations[0][2] ?? locations[0][4]);
|
|
150
|
+
type = format ? typeOf(rowsOf(format)?.get(locations[0][1] ?? locations[0][3])) : null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
termTypes.set(term, type);
|
|
154
|
+
return type;
|
|
155
|
+
};
|
|
156
|
+
// The type of the value a rule returns, from the opening of its Outputs section.
|
|
157
|
+
const returnType = (id) => {
|
|
158
|
+
const outputs = entries
|
|
159
|
+
.get(id)
|
|
160
|
+
?.sections.find((s) => s.title === "Outputs")
|
|
161
|
+
?.text.trim() ?? "";
|
|
162
|
+
const m = RETURNS.exec(outputs);
|
|
163
|
+
return m ? typeOf(m[1]) : null;
|
|
164
|
+
};
|
|
165
|
+
const live = [...entries.values()].filter((e) => e.kind === "RULE" && e.meta.status !== "superseded" && e.code);
|
|
166
|
+
// Functions whose define gives a structure as the result: define name(...) -> FMT-...
|
|
167
|
+
const functionTypes = new Map();
|
|
168
|
+
for (const e of live)
|
|
169
|
+
for (const m of withoutCommentsAndStrings(e.code).matchAll(/\bdefine\s+([a-z_][a-z0-9_]*)\s*\([^)]*\)\s*->\s*([^:\n]+):/g)) {
|
|
170
|
+
const type = typeOf(m[2]);
|
|
171
|
+
// The entries of a split rule define the same function; if they disagree, its type is unknown.
|
|
172
|
+
const disagrees = functionTypes.has(m[1]) && !sameType(functionTypes.get(m[1]), type);
|
|
173
|
+
functionTypes.set(m[1], disagrees ? null : type);
|
|
174
|
+
}
|
|
175
|
+
for (const e of live)
|
|
176
|
+
checkRule(e);
|
|
177
|
+
function checkRule(e) {
|
|
178
|
+
const lines = withoutCommentsAndStrings(e.code).split("\n");
|
|
179
|
+
const params = e.sections.find((s) => s.title === "Parameters")?.text ?? "";
|
|
180
|
+
const declarations = [];
|
|
181
|
+
const declare = (name, type) => declarations.push({ name, type });
|
|
182
|
+
const fixed = (type, source) => () => type === undefined ? undefined : type ? { ...type, source } : null;
|
|
183
|
+
const typedParams = typedNames(params);
|
|
184
|
+
for (const name of parameterNames(params))
|
|
185
|
+
declare(name, fixed(typedParams.has(name) ? typedParams.get(name) : undefined, "the Parameters section"));
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const d = /\bdefine\s+[a-z_][a-z0-9_]*\s*\(([^)]*)\)/.exec(line);
|
|
188
|
+
if (d)
|
|
189
|
+
for (const p of d[1].split(",")) {
|
|
190
|
+
const [name, type] = p.split(":");
|
|
191
|
+
if (/^[a-z_][a-z0-9_]*$/.test(name.trim()))
|
|
192
|
+
declare(name.trim(), fixed(type === undefined ? undefined : typeOf(type), "its define"));
|
|
193
|
+
}
|
|
194
|
+
const l = /^\s*let\s+([a-z_][a-z0-9_]*)\s*(?::\s*([^=]+?))?\s*=\s*(.+?)\s*$/.exec(line);
|
|
195
|
+
if (l) {
|
|
196
|
+
const [, name, type, value] = l;
|
|
197
|
+
if (type)
|
|
198
|
+
declare(name, fixed(typeOf(type), "its let"));
|
|
199
|
+
else
|
|
200
|
+
declare(name, (known) => valueType(value, known, "its let"));
|
|
201
|
+
}
|
|
202
|
+
const f = /^\s*for\s+(?:each\s+)?([a-z_][a-z0-9_]*)\s+in\s+(.+?)\s*:\s*$/.exec(line);
|
|
203
|
+
if (f)
|
|
204
|
+
declare(f[1], (known) => {
|
|
205
|
+
const list = f[2].includes("..") ? null : valueType(f[2], known, "the list its loop visits");
|
|
206
|
+
return list?.list ? { ...list, list: false } : null;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const locals = new Set(declarations.map((d) => d.name));
|
|
210
|
+
// A let can take its type from another local, so the types are worked out again until they
|
|
211
|
+
// stop changing.
|
|
212
|
+
let types = new Map();
|
|
213
|
+
const known = (name) => {
|
|
214
|
+
if (locals.has(name))
|
|
215
|
+
return types.get(name) ?? null;
|
|
216
|
+
const type = termType(name);
|
|
217
|
+
return type ? { ...type, source: `the glossary entry for ${name}` } : null;
|
|
218
|
+
};
|
|
219
|
+
const settledAs = (a, b) => a.size === b.size && [...a].every(([name, t]) => sameType(t, b.get(name) ?? null));
|
|
220
|
+
for (let pass = 0; pass <= declarations.length; pass++) {
|
|
221
|
+
const found = new Map();
|
|
222
|
+
for (const d of declarations) {
|
|
223
|
+
const t = d.type(known);
|
|
224
|
+
if (t === undefined)
|
|
225
|
+
continue;
|
|
226
|
+
found.set(d.name, found.has(d.name) && !sameType(found.get(d.name), t) ? null : t);
|
|
227
|
+
}
|
|
228
|
+
const next = new Map();
|
|
229
|
+
for (const [name, t] of found)
|
|
230
|
+
if (t)
|
|
231
|
+
next.set(name, t);
|
|
232
|
+
const settled = settledAs(next, types);
|
|
233
|
+
types = next;
|
|
234
|
+
if (settled)
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
for (const line of lines)
|
|
238
|
+
for (let i = 0; i < line.length; i++) {
|
|
239
|
+
if (/[\w.]/.test(line[i - 1] ?? ""))
|
|
240
|
+
continue;
|
|
241
|
+
const chain = readChain(line, i);
|
|
242
|
+
if (!chain)
|
|
243
|
+
continue;
|
|
244
|
+
// Names inside the chain's indexes start chains of their own.
|
|
245
|
+
i += chain.base.length - 1;
|
|
246
|
+
if (chain.steps.some((s) => s !== null))
|
|
247
|
+
follow(chain, known, (message) => problem(e.file, message));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// The type of the value of a let, or of the list a for each loop visits, or null when it has none.
|
|
251
|
+
function valueType(value, known, source) {
|
|
252
|
+
const v = value.trim();
|
|
253
|
+
const made = new RegExp(String.raw `^new\s+(${FMT_ID})$`).exec(v);
|
|
254
|
+
if (made) {
|
|
255
|
+
const format = formatOf(made[1]);
|
|
256
|
+
return format ? { format, pointer: false, list: false, source } : null;
|
|
257
|
+
}
|
|
258
|
+
// A call is the whole value only when the bracket after its name closes at the end, so
|
|
259
|
+
// make(a) + other(b) is not a call of make.
|
|
260
|
+
const called = /^call\s+(RULE-[A-Z][A-Z0-9]*-\d{3,})\s*\(/.exec(v);
|
|
261
|
+
if (called && closesAtEnd(v, called[0].length - 1)) {
|
|
262
|
+
const type = returnType(called[1]);
|
|
263
|
+
return type ? { ...type, source: `the Outputs of ${called[1]}` } : null;
|
|
264
|
+
}
|
|
265
|
+
const copied = /^copy\s*\(/.exec(v);
|
|
266
|
+
if (copied && closesAtEnd(v, copied[0].length - 1))
|
|
267
|
+
return valueType(v.slice(copied[0].length, -1), known, source);
|
|
268
|
+
const fn = /^([a-z_][a-z0-9_]*)\s*\(/.exec(v);
|
|
269
|
+
if (fn && closesAtEnd(v, fn[0].length - 1)) {
|
|
270
|
+
const type = functionTypes.get(fn[1]);
|
|
271
|
+
return type ? { ...type, source: `the define of ${fn[1]}` } : null;
|
|
272
|
+
}
|
|
273
|
+
const chain = readChain(v, 0);
|
|
274
|
+
if (!chain || chain.end !== v.length)
|
|
275
|
+
return null;
|
|
276
|
+
const t = follow(chain, known);
|
|
277
|
+
return t ? { ...t, source } : null;
|
|
278
|
+
}
|
|
279
|
+
// Follows a chain access by access and returns the type it ends with. With report, a field missing
|
|
280
|
+
// from its structure's layout is reported.
|
|
281
|
+
function follow(chain, known, report) {
|
|
282
|
+
const base = known(chain.base);
|
|
283
|
+
if (!base)
|
|
284
|
+
return null;
|
|
285
|
+
let type = base;
|
|
286
|
+
for (const [n, field] of chain.steps.entries()) {
|
|
287
|
+
if (field === null) {
|
|
288
|
+
// list[i] is an element, and p[i] is the structure p + i points at.
|
|
289
|
+
if (type.list)
|
|
290
|
+
type = { ...type, list: false };
|
|
291
|
+
else if (type.pointer)
|
|
292
|
+
type = { ...type, pointer: false };
|
|
293
|
+
else
|
|
294
|
+
return null;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (type.list)
|
|
298
|
+
return null;
|
|
299
|
+
const rows = rowsOf(type.format);
|
|
300
|
+
if (!rows)
|
|
301
|
+
return null;
|
|
302
|
+
const cell = rows.get(field);
|
|
303
|
+
if (cell === undefined) {
|
|
304
|
+
report?.(`${chain.upTo[n]} names ${field}, which is not in the layout of ${type.format.join(" or ")} (${chain.base} has its type from ${base.source})`);
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const next = typeOf(cell);
|
|
308
|
+
if (!next)
|
|
309
|
+
return null;
|
|
310
|
+
type = next;
|
|
311
|
+
}
|
|
312
|
+
return { ...type, source: base.source };
|
|
313
|
+
}
|
|
314
|
+
}
|
package/dist/checks/formats.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export interface FormatNames {
|
|
|
6
6
|
enumNames: Map<string, string[]>;
|
|
7
7
|
/** Format ID -> the names of its layout and enumeration rows. */
|
|
8
8
|
fieldNames: Map<string, Set<string>>;
|
|
9
|
+
/**
|
|
10
|
+
* Format ID -> the Name of each layout row -> its Type cell, for a format whose layout tables all
|
|
11
|
+
* have the right columns and whose rows all have one cell per column. A format without a Layout
|
|
12
|
+
* table has an empty map.
|
|
13
|
+
*/
|
|
14
|
+
layouts: Map<string, Map<string, string>>;
|
|
9
15
|
}
|
|
10
16
|
/** The IDs cited in the last column of the tables of an entry's sections (of every section when sectionTitles is left out). */
|
|
11
17
|
export declare function tableIds(e: Entry, sectionTitles?: string[]): Set<string>;
|
package/dist/checks/formats.js
CHANGED
|
@@ -21,7 +21,7 @@ export function tableIds(e, sectionTitles) {
|
|
|
21
21
|
export function checkFormat(ctx, e, formatNames) {
|
|
22
22
|
const { problem } = ctx;
|
|
23
23
|
const { entries, buildFiles } = ctx.spec;
|
|
24
|
-
const { enumNames, fieldNames } = formatNames;
|
|
24
|
+
const { enumNames, fieldNames, layouts } = formatNames;
|
|
25
25
|
const { file, meta } = e;
|
|
26
26
|
const id = meta.id;
|
|
27
27
|
const first = asList(meta.builds)[0];
|
|
@@ -88,18 +88,45 @@ export function checkFormat(ctx, e, formatNames) {
|
|
|
88
88
|
names.add(row[nameCol].replaceAll("`", ""));
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
|
+
// A row whose status is wrong still names a field, so the field checks of rules read every row.
|
|
92
|
+
const layoutTypes = new Map();
|
|
93
|
+
let wellFormed = true;
|
|
91
94
|
if (layout) {
|
|
92
95
|
const ts = tables(layout.text);
|
|
93
96
|
const wanted = meta.text === true ? TEXT_LAYOUT : BINARY_LAYOUT;
|
|
94
97
|
if (meta.status !== "unknown" && ts.length === 0)
|
|
95
98
|
problem(file, "Layout has no table");
|
|
96
99
|
for (const t of ts) {
|
|
97
|
-
if (t.header.join("|") !== wanted.join("|"))
|
|
100
|
+
if (t.header.join("|") !== wanted.join("|")) {
|
|
98
101
|
problem(file, `a layout table has the columns ${wanted.join(" | ")}`);
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
wellFormed = false;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
visit(t, "layout");
|
|
106
|
+
const nameCol = t.header.indexOf("Name");
|
|
107
|
+
const typeCol = t.header.indexOf("Type");
|
|
108
|
+
for (const row of t.rows) {
|
|
109
|
+
// A row with the wrong number of cells may hold a field whose Name cell cannot be found.
|
|
110
|
+
if (row.length !== t.header.length) {
|
|
111
|
+
wellFormed = false;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!row[nameCol])
|
|
115
|
+
continue;
|
|
116
|
+
// A cell that names more than one field, or a path into a field (`items[i].count`), still
|
|
117
|
+
// names the field it starts with, though not its type.
|
|
118
|
+
for (const name of (row[nameCol].match(/`[^`]+`/g) ?? [row[nameCol]]).map((n) => n.replaceAll("`", "").trim())) {
|
|
119
|
+
const lead = /^[A-Za-z_][A-Za-z0-9_]*/.exec(name)?.[0];
|
|
120
|
+
if (lead === name)
|
|
121
|
+
layoutTypes.set(name, row[typeCol]);
|
|
122
|
+
else if (lead && !layoutTypes.has(lead))
|
|
123
|
+
layoutTypes.set(lead, "");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
101
126
|
}
|
|
102
127
|
}
|
|
128
|
+
if (wellFormed)
|
|
129
|
+
layouts.set(id, layoutTypes);
|
|
103
130
|
// An enumeration table kept in a value file counts as one of the entry's tables.
|
|
104
131
|
const enumTables = enums ? [...tables(enums.text), ...valueFileTables(ctx, e, enums.text)] : [];
|
|
105
132
|
e.valueTables = enumTables.filter((t) => t.file);
|
package/dist/checks/kaitai.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Checks that each .ksy file in spec/formats/ belongs to a format entry, and compiles them all with
|
|
4
|
-
* the Kaitai Struct compiler,
|
|
4
|
+
* the Kaitai Struct compiler. With --no-ksy, or with no compiler found, the compilation is recorded
|
|
5
|
+
* as a skipped step; with --require-ksc, a missing compiler is a problem instead.
|
|
5
6
|
*/
|
|
6
7
|
export declare function compileKaitai(ctx: Context): void;
|
package/dist/checks/kaitai.js
CHANGED
|
@@ -5,47 +5,60 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
6
|
/**
|
|
7
7
|
* Checks that each .ksy file in spec/formats/ belongs to a format entry, and compiles them all with
|
|
8
|
-
* the Kaitai Struct compiler,
|
|
8
|
+
* the Kaitai Struct compiler. With --no-ksy, or with no compiler found, the compilation is recorded
|
|
9
|
+
* as a skipped step; with --require-ksc, a missing compiler is a problem instead.
|
|
9
10
|
*/
|
|
10
11
|
export function compileKaitai(ctx) {
|
|
11
|
-
const { problem } = ctx;
|
|
12
|
+
const { problem, skip } = ctx;
|
|
12
13
|
const { entries } = ctx.spec;
|
|
13
|
-
const { specDir, skipKsy } = ctx.config;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
14
|
+
const { specDir, skipKsy, requireKsc } = ctx.config;
|
|
15
|
+
const ksys = [];
|
|
16
|
+
const fd = join(specDir, "formats");
|
|
17
|
+
if (existsSync(fd))
|
|
18
|
+
for (const f of readdirSync(fd))
|
|
19
|
+
if (f.endsWith(".ksy"))
|
|
20
|
+
ksys.push(join(fd, f));
|
|
21
|
+
for (const k of ksys) {
|
|
22
|
+
const id = basename(k, ".ksy")
|
|
23
|
+
.toUpperCase()
|
|
24
|
+
.replace(/^FMT_([A-Z0-9]+)_(\d+)$/, "FMT-$1-$2");
|
|
25
|
+
if (!entries.has(id))
|
|
26
|
+
problem(k, `belongs to no format entry (${id})`);
|
|
27
|
+
}
|
|
28
|
+
if (!ksys.length)
|
|
29
|
+
return;
|
|
30
|
+
const definitions = `${ksys.length} definition${ksys.length === 1 ? "" : "s"}`;
|
|
31
|
+
if (skipKsy) {
|
|
32
|
+
skip(`Kaitai compilation of ${definitions} (--no-ksy)`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const compiler = findKaitai();
|
|
36
|
+
if (!compiler) {
|
|
37
|
+
const missing = "no Kaitai Struct compiler found, set KSC or install kaitai-struct-compiler";
|
|
38
|
+
if (requireKsc)
|
|
39
|
+
problem(null, `${missing}. --require-ksc requires compiling the ${definitions} in spec/formats/`);
|
|
40
|
+
else
|
|
41
|
+
skip(`Kaitai compilation of ${definitions} (${missing})`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const out = mkdtempSync(join(tmpdir(), "ksy-check-"));
|
|
45
|
+
const fixed = [...compiler.args, "--target", "python", "--outdir", out, "--import-path", fd];
|
|
46
|
+
try {
|
|
47
|
+
for (const batch of kaitaiBatches([compiler.cmd, ...fixed], ksys)) {
|
|
32
48
|
try {
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
runTool(compiler.cmd, [...fixed, ...batch]);
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
const failure = err;
|
|
39
|
-
problem(null, `Kaitai definitions do not compile:\n${String(failure.stdout ?? "")}${String(failure.stderr ?? "")}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
49
|
+
runTool(compiler.cmd, [...fixed, ...batch]);
|
|
42
50
|
}
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
catch (err) {
|
|
52
|
+
// A compiler that cannot be started, such as a KSC naming a missing file, prints nothing,
|
|
53
|
+
// so its error message is the only account of the failure.
|
|
54
|
+
const failure = err;
|
|
55
|
+
const output = `${String(failure.stdout ?? "")}${String(failure.stderr ?? "")}`;
|
|
56
|
+
problem(null, `Kaitai definitions do not compile:\n${output || (failure.message ?? String(err))}`);
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
rmSync(out, { recursive: true, force: true });
|
|
49
62
|
}
|
|
50
63
|
}
|
|
51
64
|
// cmd.exe takes a command line of at most 8,191 characters, and the compiler's .bat launcher adds
|
package/dist/checks/rules.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import type { FormatNames } from "./formats.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A procedure with each string literal emptied and its comments dropped. Strings go first, so a `#`
|
|
5
|
+
* inside one does not start a comment, and a string ends on the line it starts on.
|
|
6
|
+
*/
|
|
7
|
+
export declare const withoutCommentsAndStrings: (code: string) => string;
|
|
8
|
+
/** The names a rule's Parameters section declares: each lower-case name that opens a code span, as `n` or `n: type`. */
|
|
9
|
+
export declare const parameterNames: (params: string) => string[];
|
|
3
10
|
/** Checks every rule's procedure. Sets Entry.code on every rule entry, superseded ones included. */
|
|
4
11
|
export declare function checkRules(ctx: Context, { enumNames }: FormatNames): void;
|
package/dist/checks/rules.js
CHANGED
|
@@ -6,6 +6,13 @@ import { mayBeInterrupted, onlyEmulatedRuns } from "../evidence.js";
|
|
|
6
6
|
import { asList, idsIn, kindOf } from "../ids.js";
|
|
7
7
|
import { readCsv } from "../markdown.js";
|
|
8
8
|
import { KINDS, LIST_LIMIT } from "../standard.js";
|
|
9
|
+
/**
|
|
10
|
+
* A procedure with each string literal emptied and its comments dropped. Strings go first, so a `#`
|
|
11
|
+
* inside one does not start a comment, and a string ends on the line it starts on.
|
|
12
|
+
*/
|
|
13
|
+
export const withoutCommentsAndStrings = (code) => code.replace(/"[^"\n]*"/g, '""').replace(/#.*$/gm, "");
|
|
14
|
+
/** The names a rule's Parameters section declares: each lower-case name that opens a code span, as `n` or `n: type`. */
|
|
15
|
+
export const parameterNames = (params) => [...params.matchAll(/`([a-z_][a-z0-9_]*)(?=`|\s*:)/g)].map((m) => m[1]);
|
|
9
16
|
const BUILTINS = new Set([
|
|
10
17
|
"min",
|
|
11
18
|
"max",
|
|
@@ -102,10 +109,7 @@ export function checkRules(ctx, { enumNames }) {
|
|
|
102
109
|
// Types in a define's signature (`define roll(n: UINT16) -> char[]:`) are not names the
|
|
103
110
|
// procedure reads, so they are dropped before the name checks.
|
|
104
111
|
const TYPE = String.raw `(?:[A-Za-z][A-Za-z0-9]*|FMT-[A-Z0-9]+-\d+)(?:\[[^\]]*\])?`;
|
|
105
|
-
const code = e
|
|
106
|
-
.code.replace(/#.*$/gm, "")
|
|
107
|
-
.replace(/"[^"]*"/g, '""')
|
|
108
|
-
.replace(new RegExp(String.raw `(\bdefine\s+[a-z_][a-z0-9_]*\s*\()([^)]*)\)(\s*->\s*${TYPE})?`, "g"), (_, head, params) => `${head}${params
|
|
112
|
+
const code = withoutCommentsAndStrings(e.code).replace(new RegExp(String.raw `(\bdefine\s+[a-z_][a-z0-9_]*\s*\()([^)]*)\)(\s*->\s*${TYPE})?`, "g"), (_, head, params) => `${head}${params
|
|
109
113
|
.split(",")
|
|
110
114
|
.map((p) => p.split(":")[0].trim())
|
|
111
115
|
.join(", ")})`);
|
|
@@ -196,8 +200,8 @@ export function checkRules(ctx, { enumNames }) {
|
|
|
196
200
|
for (const p of m[1].split(","))
|
|
197
201
|
locals.add(p.split(":")[0].trim());
|
|
198
202
|
const params = e.sections.find((s) => s.title === "Parameters")?.text ?? "";
|
|
199
|
-
for (const
|
|
200
|
-
locals.add(
|
|
203
|
+
for (const name of parameterNames(params))
|
|
204
|
+
locals.add(name);
|
|
201
205
|
for (const m of code.matchAll(/(?<![.\w])([a-z_][a-z0-9_]*)\s*\(/g)) {
|
|
202
206
|
const name = m[1];
|
|
203
207
|
if (BUILTINS.has(name) || KEYWORDS.has(name) || locals.has(name))
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Config } from "./options.ts";
|
|
2
|
-
import type { Problem } from "./problems.ts";
|
|
2
|
+
import type { Problem, Skip } from "./problems.ts";
|
|
3
3
|
import type { CodeFile, CodeRange, Entry, Meta } from "./types.ts";
|
|
4
4
|
/** The spec as the load phase read it. Later phases add to entries (Entry.code, Entry.valueTables). */
|
|
5
5
|
export interface Spec {
|
|
@@ -27,6 +27,8 @@ export interface LoadContext {
|
|
|
27
27
|
}
|
|
28
28
|
/** What every phase after the load needs. */
|
|
29
29
|
export interface Context extends LoadContext {
|
|
30
|
+
/** Records a step of the check that did not run. */
|
|
31
|
+
skip: Skip;
|
|
30
32
|
/** The spec as loaded. */
|
|
31
33
|
spec: Spec;
|
|
32
34
|
/** The files of the --code and --references directories, read on first use. */
|
package/dist/options.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface Config {
|
|
|
8
8
|
checkOnly: boolean;
|
|
9
9
|
/** --no-ksy: skip compiling the Kaitai definitions. */
|
|
10
10
|
skipKsy: boolean;
|
|
11
|
+
/** --require-ksc: fail when there are Kaitai definitions and no compiler is found. */
|
|
12
|
+
requireKsc: boolean;
|
|
11
13
|
/** --base, or null when it was not given. */
|
|
12
14
|
baseArg: string | null;
|
|
13
15
|
/** Every --glossary path, in order. */
|
package/dist/options.js
CHANGED
|
@@ -10,7 +10,7 @@ export const dirList = (value, fallback) => value === undefined
|
|
|
10
10
|
.split(",")
|
|
11
11
|
.map((x) => x.trim())
|
|
12
12
|
.filter(Boolean);
|
|
13
|
-
const FLAGS = ["--check", "--no-ksy"];
|
|
13
|
+
const FLAGS = ["--check", "--no-ksy", "--require-ksc"];
|
|
14
14
|
const VALUED = [
|
|
15
15
|
"--root",
|
|
16
16
|
"--base",
|
|
@@ -52,6 +52,11 @@ export function parseOptions(argv) {
|
|
|
52
52
|
process.exit(2);
|
|
53
53
|
}
|
|
54
54
|
const skipKsy = options["no-ksy"] === true;
|
|
55
|
+
const requireKsc = options["require-ksc"] === true;
|
|
56
|
+
if (skipKsy && requireKsc) {
|
|
57
|
+
console.error("--require-ksc requires the Kaitai compilation that --no-ksy skips, so they cannot be combined");
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
55
60
|
const baseArg = options.base ?? null;
|
|
56
61
|
const codeRoots = dirList(options.code, ["src", "tests", "tools"]);
|
|
57
62
|
const referenceRoots = dirList(options.references, []);
|
|
@@ -75,6 +80,7 @@ export function parseOptions(argv) {
|
|
|
75
80
|
specDir,
|
|
76
81
|
checkOnly,
|
|
77
82
|
skipKsy,
|
|
83
|
+
requireKsc,
|
|
78
84
|
baseArg,
|
|
79
85
|
glossaryDrafts: options.glossary,
|
|
80
86
|
codeRoots,
|
package/dist/problems.d.ts
CHANGED
|
@@ -12,15 +12,34 @@ export type Rule = `IDENTIFIERS-${UpTo<7>}` | `STATUS-${UpTo<41>}` | `ENTRY-TYPE
|
|
|
12
12
|
* a numbered rule names it.
|
|
13
13
|
*/
|
|
14
14
|
export type Problem = (file: string | null, message: string, rule?: Rule) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Records a step of the check that did not run, with the reason, such as "Kaitai compilation of 2
|
|
17
|
+
* definitions (--no-ksy)". The result line names every skipped step, so a pass never reads as a
|
|
18
|
+
* full check when part of it did not run. The line joins the steps with "; ", so a step's text
|
|
19
|
+
* must not contain "; ".
|
|
20
|
+
*/
|
|
21
|
+
export type Skip = (step: string) => void;
|
|
22
|
+
/** What a run found, for the result line. */
|
|
23
|
+
export interface Counts {
|
|
24
|
+
/** Spec entries. */
|
|
25
|
+
entries: number;
|
|
26
|
+
/** PARITY.md rows. */
|
|
27
|
+
parityRows: number;
|
|
28
|
+
/** Deviations. */
|
|
29
|
+
deviations: number;
|
|
30
|
+
}
|
|
15
31
|
/** The problem collector of one run. */
|
|
16
32
|
export interface Problems {
|
|
17
33
|
/** Records a problem. */
|
|
18
34
|
problem: Problem;
|
|
35
|
+
/** Records a skipped step. */
|
|
36
|
+
skip: Skip;
|
|
19
37
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
38
|
+
* With problems, prints every distinct one in the order found, the summary and the skipped
|
|
39
|
+
* steps, and exits with 1. With none, prints the result line: "spec check passed: " and counts,
|
|
40
|
+
* or "spec check passed with skipped steps: " and counts followed by the skipped steps.
|
|
22
41
|
*/
|
|
23
|
-
report(
|
|
42
|
+
report(counts: Counts): void;
|
|
24
43
|
}
|
|
25
44
|
/** Creates the collector for a run over repoDir, against which problem paths are printed. */
|
|
26
45
|
export declare function createProblems(repoDir: string): Problems;
|
package/dist/problems.js
CHANGED
|
@@ -3,6 +3,7 @@ import { relative } from "node:path";
|
|
|
3
3
|
/** Creates the collector for a run over repoDir, against which problem paths are printed. */
|
|
4
4
|
export function createProblems(repoDir) {
|
|
5
5
|
const problems = [];
|
|
6
|
+
const skipped = [];
|
|
6
7
|
let citedRule = false;
|
|
7
8
|
// A problem that breaks a numbered rule ends with the rule's label, so whoever fixes it can read
|
|
8
9
|
// that one rule instead of the whole section.
|
|
@@ -11,17 +12,27 @@ export function createProblems(repoDir) {
|
|
|
11
12
|
citedRule = true;
|
|
12
13
|
problems.push(`${file ? relative(repoDir, file).replaceAll("\\", "/") : "spec"}: ${message}${rule ? ` [${rule}]` : ""}`);
|
|
13
14
|
};
|
|
14
|
-
const
|
|
15
|
+
const skip = (step) => {
|
|
16
|
+
skipped.push(step);
|
|
17
|
+
};
|
|
18
|
+
const skippedLine = () => `Skipped: ${skipped.join("; ")}.`;
|
|
19
|
+
const report = ({ entries, parityRows, deviations }) => {
|
|
20
|
+
const counts = `${entries} entries, ${parityRows} parity rows, ${deviations} deviations.`;
|
|
15
21
|
// The same problem can be found twice, such as a term that cites one finding in two places.
|
|
16
22
|
const unique = [...new Set(problems)];
|
|
17
23
|
if (unique.length) {
|
|
18
24
|
for (const p of unique)
|
|
19
25
|
console.error(p);
|
|
20
|
-
console.error(`\n${unique.length} problem(s) in ${
|
|
26
|
+
console.error(`\n${unique.length} problem(s) in ${entries} spec entries.`);
|
|
21
27
|
if (citedRule)
|
|
22
28
|
console.error("A label in brackets, such as [STATUS-14], names the rule of the documentation standard that the problem breaks. The standard opens it with the heading ###### STATUS-14, anchored at https://dinorefurb.com/documentation-standard/#status-14 and at #status-14 in a vendored copy.");
|
|
29
|
+
if (skipped.length)
|
|
30
|
+
console.error(skippedLine());
|
|
23
31
|
process.exit(1);
|
|
24
32
|
}
|
|
33
|
+
console.log(skipped.length
|
|
34
|
+
? `spec check passed with skipped steps: ${counts} ${skippedLine()}`
|
|
35
|
+
: `spec check passed: ${counts}`);
|
|
25
36
|
};
|
|
26
|
-
return { problem, report };
|
|
37
|
+
return { problem, skip, report };
|
|
27
38
|
}
|
package/dist/standard-checker.js
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// --check fail when an index or PARITY.md is stale instead of rewriting it
|
|
11
11
|
// --base <ref> also fail when an ID or area that exists at <ref> is gone (default: where
|
|
12
12
|
// HEAD forked from origin/$GITHUB_BASE_REF or origin/main, when it resolves)
|
|
13
|
-
// --no-ksy skip compiling the Kaitai definitions
|
|
13
|
+
// --no-ksy skip compiling the Kaitai definitions; the result line names the skip
|
|
14
|
+
// --require-ksc fail when spec/formats/ holds Kaitai definitions and no compiler is found,
|
|
15
|
+
// instead of passing with the compilation skipped
|
|
14
16
|
// --glossary <path> also accept the terms of a draft glossary file, or of a directory of them
|
|
15
17
|
// --code <dirs> comma-separated directories whose files may cite spec and deviation IDs
|
|
16
18
|
// and hold PLACEHOLDER comments (default: src,tests,tools)
|
|
@@ -36,16 +38,22 @@
|
|
|
36
38
|
// [STATUS-4] for the rule whose heading is anchored at #status-4.
|
|
37
39
|
//
|
|
38
40
|
// The KSC environment variable names the Kaitai Struct compiler. Without it, the check looks for
|
|
39
|
-
// kaitai-struct-compiler or ksc on PATH,
|
|
41
|
+
// kaitai-struct-compiler or ksc on PATH. When it finds neither, the run skips the compilation and
|
|
42
|
+
// says so in its result line, or fails with --require-ksc.
|
|
43
|
+
//
|
|
44
|
+
// It exits with 0 when the spec passes, 1 when it reports problems, and 2 when the options are
|
|
45
|
+
// invalid. A pass prints "spec check passed: " and the counts, or, when a step did not run,
|
|
46
|
+
// "spec check passed with skipped steps: " and the counts followed by "Skipped: " and each step
|
|
47
|
+
// with its reason.
|
|
40
48
|
//
|
|
41
49
|
// No dependencies. The YAML reader understands the subset the standard's front matter uses:
|
|
42
50
|
// scalars, flow lists, and block lists of flat maps.
|
|
43
51
|
//
|
|
44
52
|
// This file reads the options and runs the phases in order: load the spec, check the entries, the
|
|
45
|
-
// rules and what crosses entries, compile the Kaitai
|
|
46
|
-
// VALIDATION.md, the code's references and comments and
|
|
47
|
-
// generated files. Every phase reports into one collector,
|
|
48
|
-
// the order they were found.
|
|
53
|
+
// rules, the field names in their procedures and what crosses entries, compile the Kaitai
|
|
54
|
+
// definitions, check the deviations, parity, VALIDATION.md, the code's references and comments and
|
|
55
|
+
// the base ref, then write or check the generated files. Every phase reports into one collector,
|
|
56
|
+
// which prints the problems at the end in the order they were found.
|
|
49
57
|
import { readFileSync } from "node:fs";
|
|
50
58
|
import { dirname } from "node:path";
|
|
51
59
|
import { fileURLToPath } from "node:url";
|
|
@@ -54,6 +62,7 @@ import { checkCommentAddresses } from "./checks/comment-addresses.js";
|
|
|
54
62
|
import { checkAcrossEntries } from "./checks/cross-entry.js";
|
|
55
63
|
import { checkDeviations } from "./checks/deviations.js";
|
|
56
64
|
import { checkEntries } from "./checks/entries.js";
|
|
65
|
+
import { checkFieldNames } from "./checks/fields.js";
|
|
57
66
|
import { compileKaitai } from "./checks/kaitai.js";
|
|
58
67
|
import { checkParity } from "./checks/parity.js";
|
|
59
68
|
import { checkReferences } from "./checks/references.js";
|
|
@@ -77,13 +86,14 @@ if (argv.includes("--help") || argv.includes("-h")) {
|
|
|
77
86
|
process.exit(0);
|
|
78
87
|
}
|
|
79
88
|
const config = parseOptions(argv);
|
|
80
|
-
const { problem, report } = createProblems(config.repoDir);
|
|
89
|
+
const { problem, skip, report } = createProblems(config.repoDir);
|
|
81
90
|
const spec = loadSpec({ config, problem });
|
|
82
91
|
// The checker's modules all sit in this file's directory, which holds nothing else, so the code
|
|
83
92
|
// checks leave the whole directory out.
|
|
84
|
-
const ctx = { config, problem, spec, codeFiles: createCodeFiles(config, dirname(selfPath)) };
|
|
93
|
+
const ctx = { config, problem, skip, spec, codeFiles: createCodeFiles(config, dirname(selfPath)) };
|
|
85
94
|
const formatNames = checkEntries(ctx);
|
|
86
95
|
checkRules(ctx, formatNames);
|
|
96
|
+
checkFieldNames(ctx, formatNames);
|
|
87
97
|
checkAcrossEntries(ctx, formatNames);
|
|
88
98
|
compileKaitai(ctx);
|
|
89
99
|
const deviations = checkDeviations(ctx);
|
|
@@ -96,5 +106,4 @@ const generated = generateIndexes(ctx);
|
|
|
96
106
|
generateParity(ctx, parity, generated);
|
|
97
107
|
writeGenerated(ctx, generated);
|
|
98
108
|
checkLineLimits(ctx);
|
|
99
|
-
report(spec.entries.size);
|
|
100
|
-
console.log(`spec check passed: ${spec.entries.size} entries, ${parity.rows.size} parity rows, ${deviations.size} deviations.`);
|
|
109
|
+
report({ entries: spec.entries.size, parityRows: parity.rows.size, deviations: deviations.size });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scientific-method/standard-checker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Checks a restoration's spec/, parity/ and deviations/ against version 1 of the dinorefurb documentation standard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|