@scientific-method/standard-checker 0.2.0 → 0.3.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/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/rules.d.ts +7 -0
- package/dist/checks/rules.js +10 -6
- package/dist/standard-checker.js +6 -4
- package/package.json +1 -1
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/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/standard-checker.js
CHANGED
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
// scalars, flow lists, and block lists of flat maps.
|
|
43
43
|
//
|
|
44
44
|
// 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.
|
|
45
|
+
// rules, the field names in their procedures and what crosses entries, compile the Kaitai
|
|
46
|
+
// definitions, check the deviations, parity, VALIDATION.md, the code's references and comments and
|
|
47
|
+
// the base ref, then write or check the generated files. Every phase reports into one collector,
|
|
48
|
+
// which prints the problems at the end in the order they were found.
|
|
49
49
|
import { readFileSync } from "node:fs";
|
|
50
50
|
import { dirname } from "node:path";
|
|
51
51
|
import { fileURLToPath } from "node:url";
|
|
@@ -54,6 +54,7 @@ import { checkCommentAddresses } from "./checks/comment-addresses.js";
|
|
|
54
54
|
import { checkAcrossEntries } from "./checks/cross-entry.js";
|
|
55
55
|
import { checkDeviations } from "./checks/deviations.js";
|
|
56
56
|
import { checkEntries } from "./checks/entries.js";
|
|
57
|
+
import { checkFieldNames } from "./checks/fields.js";
|
|
57
58
|
import { compileKaitai } from "./checks/kaitai.js";
|
|
58
59
|
import { checkParity } from "./checks/parity.js";
|
|
59
60
|
import { checkReferences } from "./checks/references.js";
|
|
@@ -84,6 +85,7 @@ const spec = loadSpec({ config, problem });
|
|
|
84
85
|
const ctx = { config, problem, spec, codeFiles: createCodeFiles(config, dirname(selfPath)) };
|
|
85
86
|
const formatNames = checkEntries(ctx);
|
|
86
87
|
checkRules(ctx, formatNames);
|
|
88
|
+
checkFieldNames(ctx, formatNames);
|
|
87
89
|
checkAcrossEntries(ctx, formatNames);
|
|
88
90
|
compileKaitai(ctx);
|
|
89
91
|
const deviations = checkDeviations(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scientific-method/standard-checker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|