@cplieger/deadset-ts 3.3.1 → 3.5.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 +1 -1
- package/dist/src/analysis.js +8 -1
- package/dist/src/calls.js +154 -0
- package/dist/src/class-members.js +92 -0
- package/dist/src/components.js +61 -106
- package/dist/src/config.js +9 -0
- package/dist/src/configured-declarations.js +232 -0
- package/dist/src/decorator.js +3 -10
- package/dist/src/framework-lifecycle.js +192 -0
- package/dist/src/injection-container.js +101 -0
- package/dist/src/kinds.js +40 -0
- package/dist/src/print.js +28 -1
- package/dist/src/resolve.js +188 -9
- package/dist/src/run.js +9 -3
- package/dist/src/schema.js +55 -2
- package/dist/src/scope.js +55 -21
- package/dist/src/serialization-contract.js +405 -0
- package/dist/src/value-reach.js +120 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +8 -1
- package/src/calls.ts +224 -0
- package/src/class-members.ts +136 -0
- package/src/components.ts +67 -130
- package/src/config.ts +68 -2
- package/src/configured-declarations.ts +308 -0
- package/src/decorator.ts +4 -16
- package/src/exempt.ts +5 -1
- package/src/framework-lifecycle.ts +280 -0
- package/src/injection-container.ts +141 -0
- package/src/kinds.ts +41 -0
- package/src/print.ts +36 -1
- package/src/resolve.ts +278 -9
- package/src/run.ts +10 -3
- package/src/schema.ts +66 -3
- package/src/scope.ts +63 -22
- package/src/serialization-contract.ts +610 -0
- package/src/value-reach.ts +152 -0
- package/src/version.ts +1 -1
package/dist/src/resolve.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigError, defaultConfig, } from "./config.js";
|
|
2
|
-
import { FIXED_SEVERITY_CODES } from "./kinds.js";
|
|
2
|
+
import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.js";
|
|
3
3
|
import { checkDocument, malformed, nestFlags, SCHEMA_ROOT, SEVERITY_SECTION, } from "./schema.js";
|
|
4
4
|
/** Names the command-line flags in a refusal, where a document is named by its path. */
|
|
5
5
|
const FLAGS_LABEL = "the command-line flags";
|
|
@@ -8,6 +8,12 @@ const FAMILY_KEY_LENGTH = 4;
|
|
|
8
8
|
const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
|
|
9
9
|
const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
|
|
10
10
|
const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
|
|
11
|
+
const ANALYZER_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/u;
|
|
12
|
+
const PROVIDER_SOURCE = /^(go|npm):[^ \t\r\n]+$/u;
|
|
13
|
+
const PROVIDER_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/u;
|
|
14
|
+
const PROVIDER_DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
15
|
+
/** The members only an acquirable provider entry carries. */
|
|
16
|
+
const ACQUIRABLE_MEMBERS = ["source", "version", "digest"];
|
|
11
17
|
/** The members only a platform build configuration carries. */
|
|
12
18
|
const PLATFORM_MEMBERS = ["os", "arch", "tags"];
|
|
13
19
|
/** The member only a project build configuration carries. */
|
|
@@ -223,8 +229,8 @@ function readDelimiters(analysis, label) {
|
|
|
223
229
|
};
|
|
224
230
|
}
|
|
225
231
|
/**
|
|
226
|
-
* A severity key that names no setting
|
|
227
|
-
* the kind or kinds it names.
|
|
232
|
+
* A severity key that names no setting: it names no live kind, or the Contract
|
|
233
|
+
* fixes the severity of the kind or kinds it names.
|
|
228
234
|
*/
|
|
229
235
|
function unimplementedSeverityKey(label, path, reason) {
|
|
230
236
|
return new ConfigError("unimplemented-key", path, `${label}: key ${JSON.stringify(path)} is not implemented: ${reason}`);
|
|
@@ -245,16 +251,19 @@ function spellCodes(codes) {
|
|
|
245
251
|
}
|
|
246
252
|
return `${codes.slice(0, -1).join(", ")} and ${String(codes[codes.length - 1])}`;
|
|
247
253
|
}
|
|
254
|
+
/** Whether one severity key names a live kind: the code itself, or a family holding one. */
|
|
255
|
+
function namesLiveKind(code) {
|
|
256
|
+
return LIVE_CODES.some((candidate) => code === candidate || (code.length === FAMILY_KEY_LENGTH && candidate.startsWith(code)));
|
|
257
|
+
}
|
|
248
258
|
/**
|
|
249
259
|
* The severity object, one code at a time.
|
|
250
260
|
*
|
|
251
|
-
* A key is one issue-kind code or one two-digit family prefix
|
|
252
|
-
* kind whose severity the Contract fixes
|
|
253
|
-
* one, is an unimplemented key rather than a setting.
|
|
261
|
+
* A key is one issue-kind code or one two-digit family prefix. A key naming no
|
|
262
|
+
* live kind, and a key naming a kind whose severity the Contract fixes or a family
|
|
263
|
+
* prefix whose range holds one, is an unimplemented key rather than a setting.
|
|
254
264
|
*
|
|
255
|
-
* Whether a key names a kind this analyzer reports is not decided here
|
|
256
|
-
*
|
|
257
|
-
* and asking it would refuse the whole object.
|
|
265
|
+
* Whether a key names a kind this analyzer reports is not decided here: a live
|
|
266
|
+
* kind of another language is a setting the analyzer of that language reads.
|
|
258
267
|
*/
|
|
259
268
|
function readSeverity(document, label) {
|
|
260
269
|
const object = section(document, SEVERITY_SECTION, SEVERITY_SECTION, label);
|
|
@@ -267,6 +276,11 @@ function readSeverity(document, label) {
|
|
|
267
276
|
if (!SEVERITY_KEY.test(code)) {
|
|
268
277
|
throw unimplementedSeverityKey(label, path, "a severity key is one issue-kind code or one two-digit family prefix");
|
|
269
278
|
}
|
|
279
|
+
if (!namesLiveKind(code)) {
|
|
280
|
+
throw unimplementedSeverityKey(label, path, code.length === FAMILY_KEY_LENGTH
|
|
281
|
+
? "the family this prefix names holds no live issue kind"
|
|
282
|
+
: "the code names no live issue kind");
|
|
283
|
+
}
|
|
270
284
|
const fixed = fixedByContract(code);
|
|
271
285
|
if (fixed.length > 0) {
|
|
272
286
|
throw unimplementedSeverityKey(label, path, `the Contract fixes the severity of ${spellCodes(fixed)}, which this key names`);
|
|
@@ -292,6 +306,160 @@ function readExemptions(document, label) {
|
|
|
292
306
|
}
|
|
293
307
|
return disabled;
|
|
294
308
|
}
|
|
309
|
+
/** The members that name a declaration, each belonging to exactly one shape. */
|
|
310
|
+
const SHAPE_MEMBERS = ["symbol", "module", "name", "global"];
|
|
311
|
+
/** The prefix of a stable symbol reference in the TypeScript form. */
|
|
312
|
+
const SYMBOL_PREFIX = "ts://";
|
|
313
|
+
/**
|
|
314
|
+
* A bare specifier: neither relative nor absolute, and not one the package's own
|
|
315
|
+
* imports field maps, each of which names a file of the analyzed program.
|
|
316
|
+
*/
|
|
317
|
+
const BARE_SPECIFIER = /^[^./#]/u;
|
|
318
|
+
/**
|
|
319
|
+
* One declaration entry, read as the one shape its members name. An entry carrying
|
|
320
|
+
* members of two shapes, or of none, is refused at the entry, as is a module named
|
|
321
|
+
* without the declaration's path or a path without its module.
|
|
322
|
+
*/
|
|
323
|
+
function readDeclaration(entry, at, label) {
|
|
324
|
+
if (!isRecord(entry)) {
|
|
325
|
+
throw malformed(label, at, "is not an object");
|
|
326
|
+
}
|
|
327
|
+
const named = SHAPE_MEMBERS.filter((name) => Object.hasOwn(entry, name));
|
|
328
|
+
if (named.length === 1 && named[0] === "symbol") {
|
|
329
|
+
const symbol = requireMember(readString(entry, "symbol", `${at}.symbol`, label), `${at}.symbol`, label);
|
|
330
|
+
if (!symbol.startsWith(SYMBOL_PREFIX)) {
|
|
331
|
+
throw malformed(label, `${at}.symbol`, `${JSON.stringify(symbol)} is not a ts:// reference`);
|
|
332
|
+
}
|
|
333
|
+
return { shape: "symbol", symbol };
|
|
334
|
+
}
|
|
335
|
+
if (named.length === 1 && named[0] === "global") {
|
|
336
|
+
const path = `${at}.global`;
|
|
337
|
+
return {
|
|
338
|
+
shape: "global",
|
|
339
|
+
global: requireMember(readString(entry, "global", path, label), path, label),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (named.length === 2 && named.includes("module") && named.includes("name")) {
|
|
343
|
+
const module = requireMember(readString(entry, "module", `${at}.module`, label), `${at}.module`, label);
|
|
344
|
+
if (!BARE_SPECIFIER.test(module)) {
|
|
345
|
+
throw malformed(label, `${at}.module`, `${JSON.stringify(module)} is not a bare specifier: a file of the analyzed program is named by its symbol`);
|
|
346
|
+
}
|
|
347
|
+
const path = `${at}.name`;
|
|
348
|
+
return {
|
|
349
|
+
shape: "module",
|
|
350
|
+
module,
|
|
351
|
+
name: requireMember(readString(entry, "name", path, label), path, label),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
throw malformed(label, at, named.length === 0
|
|
355
|
+
? 'names no declaration: an entry carries "symbol", "module" with "name", or "global"'
|
|
356
|
+
: `carries ${spell(named)}, which is not one of "symbol", "module" with "name", or "global"`);
|
|
357
|
+
}
|
|
358
|
+
/** One array of declaration entries, absent where the document does not write it. */
|
|
359
|
+
function readDeclarations(parent, name, path, label) {
|
|
360
|
+
const value = member(parent, name);
|
|
361
|
+
if (value === undefined) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
if (!Array.isArray(value)) {
|
|
365
|
+
throw malformed(label, path, "is not an array");
|
|
366
|
+
}
|
|
367
|
+
return value.map((entry, index) => readDeclaration(entry, `${path}[${String(index)}]`, label));
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* One lifecycle contract. The member names are required, and the entry names at least
|
|
371
|
+
* one component declaration or one base class, because a contract naming neither
|
|
372
|
+
* makes no class a component and so retains nothing.
|
|
373
|
+
*/
|
|
374
|
+
function readLifecycleContract(entry, at, label) {
|
|
375
|
+
if (!isRecord(entry)) {
|
|
376
|
+
throw malformed(label, at, "is not an object");
|
|
377
|
+
}
|
|
378
|
+
const components = readDeclarations(entry, "components", `${at}.components`, label) ?? [];
|
|
379
|
+
const bases = readDeclarations(entry, "bases", `${at}.bases`, label) ?? [];
|
|
380
|
+
const members = readStrings(entry, "members", `${at}.members`, label, 1);
|
|
381
|
+
if (members === undefined) {
|
|
382
|
+
throw malformed(label, `${at}.members`, "is required and names nothing");
|
|
383
|
+
}
|
|
384
|
+
if (components.length === 0 && bases.length === 0) {
|
|
385
|
+
throw malformed(label, at, 'names no declaration in "components" and no class in "bases", so it makes no class a component');
|
|
386
|
+
}
|
|
387
|
+
return { components, bases, members };
|
|
388
|
+
}
|
|
389
|
+
/** The lifecycle contracts, one per framework the project declares. */
|
|
390
|
+
function readLifecycleContracts(ts, label) {
|
|
391
|
+
const path = "ts.lifecycle_contracts";
|
|
392
|
+
const value = member(ts, "lifecycle_contracts");
|
|
393
|
+
if (value === undefined) {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
if (!Array.isArray(value)) {
|
|
397
|
+
throw malformed(label, path, "is not an array");
|
|
398
|
+
}
|
|
399
|
+
return value.map((entry, index) => readLifecycleContract(entry, `${path}[${String(index)}]`, label));
|
|
400
|
+
}
|
|
401
|
+
/** One string member a provider entry requires, refused where it fails `pattern`. */
|
|
402
|
+
function readMatching(entry, name, at, label, pattern) {
|
|
403
|
+
const path = `${at}.${name}`;
|
|
404
|
+
const value = requireMember(readString(entry, name, path, label), path, label);
|
|
405
|
+
if (!pattern.test(value)) {
|
|
406
|
+
throw malformed(label, path, `${JSON.stringify(value)} does not match ${pattern.source}`);
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* One provider entry, read as the one shape its members name: a member only an
|
|
412
|
+
* acquirable analyzer carries makes it acquirable, and then it carries all three.
|
|
413
|
+
*/
|
|
414
|
+
function readProvider(entry, at, label) {
|
|
415
|
+
if (!isRecord(entry)) {
|
|
416
|
+
throw malformed(label, at, "is not an object");
|
|
417
|
+
}
|
|
418
|
+
const name = readMatching(entry, "name", at, label, ANALYZER_NAME);
|
|
419
|
+
const languages = readStrings(entry, "languages", `${at}.languages`, label, 1, LANGUAGES);
|
|
420
|
+
if (languages === undefined) {
|
|
421
|
+
throw malformed(label, `${at}.languages`, "is required and names nothing");
|
|
422
|
+
}
|
|
423
|
+
const commandPath = `${at}.command`;
|
|
424
|
+
const command = requireMember(readString(entry, "command", commandPath, label), commandPath, label);
|
|
425
|
+
if (!ACQUIRABLE_MEMBERS.some((member) => Object.hasOwn(entry, member))) {
|
|
426
|
+
return { shape: "installed", name, languages, command };
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
shape: "acquirable",
|
|
430
|
+
name,
|
|
431
|
+
languages,
|
|
432
|
+
command,
|
|
433
|
+
source: readMatching(entry, "source", at, label, PROVIDER_SOURCE),
|
|
434
|
+
version: readMatching(entry, "version", at, label, PROVIDER_VERSION),
|
|
435
|
+
digest: readMatching(entry, "digest", at, label, PROVIDER_DIGEST),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* The provider list. Every file a run writes for an analyzer is keyed by the
|
|
440
|
+
* entry's name, so a list naming one name twice is refused at the later entry.
|
|
441
|
+
*/
|
|
442
|
+
function readProviders(providers, label) {
|
|
443
|
+
const path = "providers.analyzers";
|
|
444
|
+
const value = member(providers, "analyzers");
|
|
445
|
+
if (value === undefined) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
if (!Array.isArray(value)) {
|
|
449
|
+
throw malformed(label, path, "is not an array");
|
|
450
|
+
}
|
|
451
|
+
const read = value.map((entry, index) => readProvider(entry, `${path}[${String(index)}]`, label));
|
|
452
|
+
const first = new Map();
|
|
453
|
+
read.forEach((entry, index) => {
|
|
454
|
+
const earlier = first.get(entry.name);
|
|
455
|
+
if (earlier !== undefined) {
|
|
456
|
+
throw malformed(label, `${path}[${String(index)}].name`, `${JSON.stringify(entry.name)} is already the name of ${path}[${String(earlier)}], ` +
|
|
457
|
+
"and every entry's name keys the files a run writes for it");
|
|
458
|
+
}
|
|
459
|
+
first.set(entry.name, index);
|
|
460
|
+
});
|
|
461
|
+
return read;
|
|
462
|
+
}
|
|
295
463
|
/**
|
|
296
464
|
* Reads one already-parsed configuration document as the settings it supplies,
|
|
297
465
|
* refusing every constraint the closed key list declares that a parse cannot
|
|
@@ -308,6 +476,7 @@ function readDoc(value, label) {
|
|
|
308
476
|
const consumers = section(value, "consumers", "consumers", label);
|
|
309
477
|
const roots = section(value, "roots", "roots", label);
|
|
310
478
|
const reporters = section(value, "reporters", "reporters", label);
|
|
479
|
+
const providers = section(value, "providers", "providers", label);
|
|
311
480
|
const ts = section(value, "ts", "ts", label);
|
|
312
481
|
section(value, "go", "go", label);
|
|
313
482
|
section(value, "provenance", "provenance", label);
|
|
@@ -335,8 +504,12 @@ function readDoc(value, label) {
|
|
|
335
504
|
cascade: readEnum(reporters, "cascade", "reporters.cascade", label, CASCADES),
|
|
336
505
|
maxFindings: readCount(reporters, "max_findings", "reporters.max_findings", label),
|
|
337
506
|
failOn: readEnum(reporters, "fail_on", "reporters.fail_on", label, SEVERITIES),
|
|
507
|
+
providers: readProviders(providers, label),
|
|
338
508
|
testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
|
|
339
509
|
entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
|
|
510
|
+
injectionRegistrations: readDeclarations(ts, "injection_registrations", "ts.injection_registrations", label),
|
|
511
|
+
lifecycleContracts: readLifecycleContracts(ts, label),
|
|
512
|
+
serializers: readDeclarations(ts, "serializers", "ts.serializers", label),
|
|
340
513
|
};
|
|
341
514
|
}
|
|
342
515
|
/** Parses one document's text and walks it against the closed key list. */
|
|
@@ -535,9 +708,15 @@ function settleAll(inputs) {
|
|
|
535
708
|
maxFindings: at("reporters.max_findings", defaults.reporters.maxFindings, (doc) => doc.maxFindings),
|
|
536
709
|
failOn: at("reporters.fail_on", defaults.reporters.failOn, (doc) => doc.failOn),
|
|
537
710
|
},
|
|
711
|
+
providers: {
|
|
712
|
+
analyzers: at("providers.analyzers", defaults.providers.analyzers, (doc) => doc.providers),
|
|
713
|
+
},
|
|
538
714
|
ts: {
|
|
539
715
|
testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
|
|
540
716
|
entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
|
|
717
|
+
injectionRegistrations: at("ts.injection_registrations", defaults.ts.injectionRegistrations, (doc) => doc.injectionRegistrations),
|
|
718
|
+
lifecycleContracts: at("ts.lifecycle_contracts", defaults.ts.lifecycleContracts, (doc) => doc.lifecycleContracts),
|
|
719
|
+
serializers: at("ts.serializers", defaults.ts.serializers, (doc) => doc.serializers),
|
|
541
720
|
},
|
|
542
721
|
};
|
|
543
722
|
return { config, provenance };
|
package/dist/src/run.js
CHANGED
|
@@ -47,8 +47,14 @@ export const SETTING_OPTIONS = new Map([
|
|
|
47
47
|
]);
|
|
48
48
|
/** The options that take a value and are not settings. */
|
|
49
49
|
const PLAIN_OPTIONS = new Set(["target", "config", "central", "scope"]);
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The setting options whose value is an integer rather than a string. A value
|
|
52
|
+
* written other than as digits is passed on as the string it is, so resolution
|
|
53
|
+
* refuses it by its type rather than reading `1e2` as 100.
|
|
54
|
+
*/
|
|
55
|
+
const INTEGER_OPTIONS = new Set(["max-findings"]);
|
|
56
|
+
/** An integer as an option writes it: an optional minus sign and digits alone. */
|
|
57
|
+
const INTEGER_TEXT = /^-?[0-9]+$/u;
|
|
52
58
|
/**
|
|
53
59
|
* The first option whose name carries a source-edit token, because every verb
|
|
54
60
|
* reports and never edits. The match is on the name alone, so an option whose
|
|
@@ -116,7 +122,7 @@ function inputsOf(host, options) {
|
|
|
116
122
|
if (value === undefined) {
|
|
117
123
|
continue;
|
|
118
124
|
}
|
|
119
|
-
flat[path] =
|
|
125
|
+
flat[path] = INTEGER_OPTIONS.has(name) && INTEGER_TEXT.test(value) ? Number(value) : value;
|
|
120
126
|
flagLabels.set(path, `--${name}`);
|
|
121
127
|
}
|
|
122
128
|
const flags = Object.keys(flat).length === 0 ? undefined : JSON.stringify(flat);
|
package/dist/src/schema.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ConfigError } from "./config.js";
|
|
2
2
|
const leaf = { kind: "leaf" };
|
|
3
|
+
const integer = { kind: "leaf", integer: true };
|
|
4
|
+
/** A list of declarations, each entry in one of the three shapes the `ts` section states. */
|
|
5
|
+
const declarations = {
|
|
6
|
+
kind: "list",
|
|
7
|
+
members: { symbol: leaf, module: leaf, name: leaf, global: leaf },
|
|
8
|
+
};
|
|
3
9
|
/**
|
|
4
10
|
* The closed key list the Contract's configuration schema declares, derived from
|
|
5
11
|
* that schema key by key. A test pins the two equal, so a Contract change that
|
|
@@ -39,12 +45,40 @@ export const SCHEMA_ROOT = {
|
|
|
39
45
|
formats: leaf,
|
|
40
46
|
sort: leaf,
|
|
41
47
|
cascade: leaf,
|
|
42
|
-
max_findings:
|
|
48
|
+
max_findings: integer,
|
|
43
49
|
fail_on: leaf,
|
|
44
50
|
},
|
|
45
51
|
},
|
|
52
|
+
providers: {
|
|
53
|
+
kind: "section",
|
|
54
|
+
members: {
|
|
55
|
+
analyzers: {
|
|
56
|
+
kind: "list",
|
|
57
|
+
members: {
|
|
58
|
+
name: leaf,
|
|
59
|
+
languages: leaf,
|
|
60
|
+
command: leaf,
|
|
61
|
+
source: leaf,
|
|
62
|
+
version: leaf,
|
|
63
|
+
digest: leaf,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
46
68
|
go: { kind: "section", members: {} },
|
|
47
|
-
ts: {
|
|
69
|
+
ts: {
|
|
70
|
+
kind: "section",
|
|
71
|
+
members: {
|
|
72
|
+
test_files: leaf,
|
|
73
|
+
entry_files: leaf,
|
|
74
|
+
injection_registrations: declarations,
|
|
75
|
+
lifecycle_contracts: {
|
|
76
|
+
kind: "list",
|
|
77
|
+
members: { components: declarations, bases: declarations, members: leaf },
|
|
78
|
+
},
|
|
79
|
+
serializers: declarations,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
48
82
|
provenance: { kind: "map" },
|
|
49
83
|
},
|
|
50
84
|
};
|
|
@@ -227,6 +261,21 @@ function skipScalar(cursor) {
|
|
|
227
261
|
cursor.at += 1;
|
|
228
262
|
}
|
|
229
263
|
}
|
|
264
|
+
/** A number the document writes as an optional minus sign and digits alone. */
|
|
265
|
+
const INTEGER_TEXT = /^-?[0-9]+$/u;
|
|
266
|
+
/** The start of a JSON number: a minus sign or a digit. */
|
|
267
|
+
const NUMBER_START = /^[-0-9]/u;
|
|
268
|
+
/**
|
|
269
|
+
* Refuses a number written with a fraction or an exponent where the key list
|
|
270
|
+
* declares an integer. A parse reads `100.0` and `1e2` as the integer 100, so only
|
|
271
|
+
* the text can tell them apart. A scalar that is no number is the decoder's to
|
|
272
|
+
* refuse by its type.
|
|
273
|
+
*/
|
|
274
|
+
function checkInteger(text, at, label) {
|
|
275
|
+
if (NUMBER_START.test(text) && !INTEGER_TEXT.test(text)) {
|
|
276
|
+
throw malformed(label, at, `${text} is written with a fraction or an exponent, and an integer is digits alone`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
230
279
|
function walkValue(cursor, at, node, label) {
|
|
231
280
|
skipSpace(cursor);
|
|
232
281
|
const ch = cursor.text.charCodeAt(cursor.at);
|
|
@@ -244,7 +293,11 @@ function walkValue(cursor, at, node, label) {
|
|
|
244
293
|
readName(cursor, at, label);
|
|
245
294
|
return;
|
|
246
295
|
}
|
|
296
|
+
const start = cursor.at;
|
|
247
297
|
skipScalar(cursor);
|
|
298
|
+
if (node.integer === true) {
|
|
299
|
+
checkInteger(cursor.text.slice(start, cursor.at), at, label);
|
|
300
|
+
}
|
|
248
301
|
}
|
|
249
302
|
function walkObject(cursor, at, node, label) {
|
|
250
303
|
const seen = new Set();
|
package/dist/src/scope.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ConfigError } from "./config.js";
|
|
1
2
|
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.js";
|
|
3
|
+
import { checkDocument } from "./schema.js";
|
|
2
4
|
/** The roles a scope document declares for a module. */
|
|
3
5
|
export const ROLE_TARGET = "target";
|
|
4
6
|
export const ROLE_CONSUMER = "consumer";
|
|
@@ -17,18 +19,37 @@ export class ScopeError extends Error {
|
|
|
17
19
|
this.name = "ScopeError";
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
+
const MODULE = {
|
|
23
|
+
kind: "section",
|
|
24
|
+
members: { id: { kind: "leaf" }, role: { kind: "leaf" }, path: { kind: "leaf" } },
|
|
25
|
+
};
|
|
26
|
+
/** The closed key list of the scope document, compared as bytes. */
|
|
27
|
+
const SCOPE_ROOT = {
|
|
28
|
+
kind: "section",
|
|
29
|
+
members: {
|
|
30
|
+
target: MODULE,
|
|
31
|
+
workspace: { kind: "leaf" },
|
|
32
|
+
consumers: { kind: "list", members: MODULE.members ?? {} },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
22
35
|
function isRecord(value) {
|
|
23
36
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* One string member, or undefined where the document omits it. A member the
|
|
40
|
+
* document writes is a string holding at least one character, so `null` and `""`
|
|
41
|
+
* are refused rather than read as absent.
|
|
42
|
+
*/
|
|
25
43
|
function stringMember(parent, name, at) {
|
|
26
44
|
if (!Object.hasOwn(parent, name)) {
|
|
27
|
-
return
|
|
45
|
+
return undefined;
|
|
28
46
|
}
|
|
29
47
|
const value = parent[name];
|
|
30
48
|
if (typeof value !== "string") {
|
|
31
|
-
throw new ScopeError(`scope: ${at}.${name} is not a string`);
|
|
49
|
+
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} is not a string`);
|
|
50
|
+
}
|
|
51
|
+
if (value === "") {
|
|
52
|
+
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} names nothing`);
|
|
32
53
|
}
|
|
33
54
|
return value;
|
|
34
55
|
}
|
|
@@ -36,30 +57,46 @@ function readModule(value, at, role, documentDir) {
|
|
|
36
57
|
if (!isRecord(value)) {
|
|
37
58
|
throw new ScopeError(`scope: ${at} is not an object`);
|
|
38
59
|
}
|
|
39
|
-
for (const name of Object.keys(value)) {
|
|
40
|
-
if (!MODULE_KEYS.has(name)) {
|
|
41
|
-
throw new ScopeError(`scope: ${at}.${name} is not a declared key`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
60
|
const declared = stringMember(value, "role", at);
|
|
45
|
-
if (declared !==
|
|
61
|
+
if (declared !== undefined && declared !== role) {
|
|
46
62
|
throw new ScopeError(`scope: ${at}.role is ${JSON.stringify(declared)}, want ${JSON.stringify(role)}`);
|
|
47
63
|
}
|
|
48
64
|
const path = stringMember(value, "path", at);
|
|
49
|
-
if (path ===
|
|
65
|
+
if (path === undefined) {
|
|
50
66
|
throw new ScopeError(`scope: ${at}.path names nothing`);
|
|
51
67
|
}
|
|
52
68
|
return {
|
|
53
|
-
id: stringMember(value, "id", at),
|
|
69
|
+
id: stringMember(value, "id", at) ?? "",
|
|
54
70
|
path: isAbsolutePath(path) ? path : joinPath(documentDir, path),
|
|
55
71
|
};
|
|
56
72
|
}
|
|
57
73
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
74
|
+
* Walks the document's text against the closed key list, refusing a key the list
|
|
75
|
+
* does not declare, compared as bytes, and a key one object writes twice, which a
|
|
76
|
+
* parse cannot report because it keeps the last value.
|
|
77
|
+
*/
|
|
78
|
+
function checkKeys(text, file) {
|
|
79
|
+
try {
|
|
80
|
+
checkDocument(text, SCOPE_ROOT, `scope: ${file}`);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof ConfigError) {
|
|
84
|
+
// The configuration's refusal names the nearest configuration key, which a
|
|
85
|
+
// scope document does not have.
|
|
86
|
+
throw new ScopeError(error.kind === "unimplemented-key"
|
|
87
|
+
? `scope: ${file}: ${error.key} is not a declared key`
|
|
88
|
+
: error.message);
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Decodes the scope document at `file`. Decoding is strict: an undeclared key, a
|
|
95
|
+
* key written twice in one object, and a value of a type the document does not
|
|
96
|
+
* admit, `null` included, are errors, and a relative path inside the document
|
|
97
|
+
* resolves against the document's own directory.
|
|
61
98
|
*
|
|
62
|
-
* The `workspace` key the format declares is
|
|
99
|
+
* The `workspace` key the format declares is checked and carries nothing here: it
|
|
63
100
|
* names the file a Go consumer resolves the target through, and a TypeScript
|
|
64
101
|
* project resolves one through its own compiler configuration.
|
|
65
102
|
*/
|
|
@@ -85,11 +122,8 @@ export function readScope(host, file) {
|
|
|
85
122
|
if (!isRecord(value)) {
|
|
86
123
|
throw new ScopeError(`scope: ${file} is not one JSON object`);
|
|
87
124
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
throw new ScopeError(`scope: ${file}: ${name} is not a declared key`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
125
|
+
checkKeys(text, file);
|
|
126
|
+
stringMember(value, "workspace", "");
|
|
93
127
|
const documentDir = dirnamePath(absolute);
|
|
94
128
|
if (!Object.hasOwn(value, "target")) {
|
|
95
129
|
throw new ScopeError(`scope: ${file} names no target`);
|