@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/src/resolve.ts
CHANGED
|
@@ -6,19 +6,22 @@ import {
|
|
|
6
6
|
type Config,
|
|
7
7
|
type Confidence,
|
|
8
8
|
type ConsumerTests,
|
|
9
|
+
type DeclarationEntry,
|
|
9
10
|
type Format,
|
|
10
11
|
type GeneratedFiles,
|
|
11
12
|
type Inputs,
|
|
12
13
|
type Language,
|
|
14
|
+
type LifecycleContract,
|
|
13
15
|
type Origin,
|
|
14
16
|
type Provenance,
|
|
17
|
+
type Provider,
|
|
15
18
|
type Severity,
|
|
16
19
|
type Sort,
|
|
17
20
|
type Source,
|
|
18
21
|
type TargetKind,
|
|
19
22
|
type TemplateDelimiters,
|
|
20
23
|
} from "./config.ts";
|
|
21
|
-
import { FIXED_SEVERITY_CODES } from "./kinds.ts";
|
|
24
|
+
import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.ts";
|
|
22
25
|
import {
|
|
23
26
|
checkDocument,
|
|
24
27
|
malformed,
|
|
@@ -37,6 +40,13 @@ const FAMILY_KEY_LENGTH = 4;
|
|
|
37
40
|
const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
|
|
38
41
|
const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
|
|
39
42
|
const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
|
|
43
|
+
const ANALYZER_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/u;
|
|
44
|
+
const PROVIDER_SOURCE = /^(go|npm):[^ \t\r\n]+$/u;
|
|
45
|
+
const PROVIDER_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/u;
|
|
46
|
+
const PROVIDER_DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
47
|
+
|
|
48
|
+
/** The members only an acquirable provider entry carries. */
|
|
49
|
+
const ACQUIRABLE_MEMBERS = ["source", "version", "digest"] as const;
|
|
40
50
|
|
|
41
51
|
/** The members only a platform build configuration carries. */
|
|
42
52
|
const PLATFORM_MEMBERS = ["os", "arch", "tags"] as const;
|
|
@@ -92,8 +102,12 @@ interface Doc {
|
|
|
92
102
|
readonly cascade: Cascade | undefined;
|
|
93
103
|
readonly maxFindings: number | undefined;
|
|
94
104
|
readonly failOn: Severity | undefined;
|
|
105
|
+
readonly providers: readonly Provider[] | undefined;
|
|
95
106
|
readonly testFiles: readonly string[] | undefined;
|
|
96
107
|
readonly entryFiles: readonly string[] | undefined;
|
|
108
|
+
readonly injectionRegistrations: readonly DeclarationEntry[] | undefined;
|
|
109
|
+
readonly lifecycleContracts: readonly LifecycleContract[] | undefined;
|
|
110
|
+
readonly serializers: readonly DeclarationEntry[] | undefined;
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
/** One document the resolution reads, with the origin its settings carry. */
|
|
@@ -377,8 +391,8 @@ function readDelimiters(
|
|
|
377
391
|
}
|
|
378
392
|
|
|
379
393
|
/**
|
|
380
|
-
* A severity key that names no setting
|
|
381
|
-
* the kind or kinds it names.
|
|
394
|
+
* A severity key that names no setting: it names no live kind, or the Contract
|
|
395
|
+
* fixes the severity of the kind or kinds it names.
|
|
382
396
|
*/
|
|
383
397
|
function unimplementedSeverityKey(label: string, path: string, reason: string): ConfigError {
|
|
384
398
|
return new ConfigError(
|
|
@@ -409,16 +423,23 @@ function spellCodes(codes: readonly string[]): string {
|
|
|
409
423
|
return `${codes.slice(0, -1).join(", ")} and ${String(codes[codes.length - 1])}`;
|
|
410
424
|
}
|
|
411
425
|
|
|
426
|
+
/** Whether one severity key names a live kind: the code itself, or a family holding one. */
|
|
427
|
+
function namesLiveKind(code: string): boolean {
|
|
428
|
+
return LIVE_CODES.some(
|
|
429
|
+
(candidate) =>
|
|
430
|
+
code === candidate || (code.length === FAMILY_KEY_LENGTH && candidate.startsWith(code)),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
412
434
|
/**
|
|
413
435
|
* The severity object, one code at a time.
|
|
414
436
|
*
|
|
415
|
-
* A key is one issue-kind code or one two-digit family prefix
|
|
416
|
-
* kind whose severity the Contract fixes
|
|
417
|
-
* one, is an unimplemented key rather than a setting.
|
|
437
|
+
* A key is one issue-kind code or one two-digit family prefix. A key naming no
|
|
438
|
+
* live kind, and a key naming a kind whose severity the Contract fixes or a family
|
|
439
|
+
* prefix whose range holds one, is an unimplemented key rather than a setting.
|
|
418
440
|
*
|
|
419
|
-
* Whether a key names a kind this analyzer reports is not decided here
|
|
420
|
-
*
|
|
421
|
-
* and asking it would refuse the whole object.
|
|
441
|
+
* Whether a key names a kind this analyzer reports is not decided here: a live
|
|
442
|
+
* kind of another language is a setting the analyzer of that language reads.
|
|
422
443
|
*/
|
|
423
444
|
function readSeverity(
|
|
424
445
|
document: Record<string, unknown> | undefined,
|
|
@@ -438,6 +459,15 @@ function readSeverity(
|
|
|
438
459
|
"a severity key is one issue-kind code or one two-digit family prefix",
|
|
439
460
|
);
|
|
440
461
|
}
|
|
462
|
+
if (!namesLiveKind(code)) {
|
|
463
|
+
throw unimplementedSeverityKey(
|
|
464
|
+
label,
|
|
465
|
+
path,
|
|
466
|
+
code.length === FAMILY_KEY_LENGTH
|
|
467
|
+
? "the family this prefix names holds no live issue kind"
|
|
468
|
+
: "the code names no live issue kind",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
441
471
|
const fixed = fixedByContract(code);
|
|
442
472
|
if (fixed.length > 0) {
|
|
443
473
|
throw unimplementedSeverityKey(
|
|
@@ -476,6 +506,221 @@ function readExemptions(
|
|
|
476
506
|
return disabled;
|
|
477
507
|
}
|
|
478
508
|
|
|
509
|
+
/** The members that name a declaration, each belonging to exactly one shape. */
|
|
510
|
+
const SHAPE_MEMBERS = ["symbol", "module", "name", "global"] as const;
|
|
511
|
+
|
|
512
|
+
/** The prefix of a stable symbol reference in the TypeScript form. */
|
|
513
|
+
const SYMBOL_PREFIX = "ts://";
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* A bare specifier: neither relative nor absolute, and not one the package's own
|
|
517
|
+
* imports field maps, each of which names a file of the analyzed program.
|
|
518
|
+
*/
|
|
519
|
+
const BARE_SPECIFIER = /^[^./#]/u;
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* One declaration entry, read as the one shape its members name. An entry carrying
|
|
523
|
+
* members of two shapes, or of none, is refused at the entry, as is a module named
|
|
524
|
+
* without the declaration's path or a path without its module.
|
|
525
|
+
*/
|
|
526
|
+
function readDeclaration(entry: unknown, at: string, label: string): DeclarationEntry {
|
|
527
|
+
if (!isRecord(entry)) {
|
|
528
|
+
throw malformed(label, at, "is not an object");
|
|
529
|
+
}
|
|
530
|
+
const named = SHAPE_MEMBERS.filter((name) => Object.hasOwn(entry, name));
|
|
531
|
+
if (named.length === 1 && named[0] === "symbol") {
|
|
532
|
+
const symbol = requireMember(
|
|
533
|
+
readString(entry, "symbol", `${at}.symbol`, label),
|
|
534
|
+
`${at}.symbol`,
|
|
535
|
+
label,
|
|
536
|
+
);
|
|
537
|
+
if (!symbol.startsWith(SYMBOL_PREFIX)) {
|
|
538
|
+
throw malformed(label, `${at}.symbol`, `${JSON.stringify(symbol)} is not a ts:// reference`);
|
|
539
|
+
}
|
|
540
|
+
return { shape: "symbol", symbol };
|
|
541
|
+
}
|
|
542
|
+
if (named.length === 1 && named[0] === "global") {
|
|
543
|
+
const path = `${at}.global`;
|
|
544
|
+
return {
|
|
545
|
+
shape: "global",
|
|
546
|
+
global: requireMember(readString(entry, "global", path, label), path, label),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (named.length === 2 && named.includes("module") && named.includes("name")) {
|
|
550
|
+
const module = requireMember(
|
|
551
|
+
readString(entry, "module", `${at}.module`, label),
|
|
552
|
+
`${at}.module`,
|
|
553
|
+
label,
|
|
554
|
+
);
|
|
555
|
+
if (!BARE_SPECIFIER.test(module)) {
|
|
556
|
+
throw malformed(
|
|
557
|
+
label,
|
|
558
|
+
`${at}.module`,
|
|
559
|
+
`${JSON.stringify(module)} is not a bare specifier: a file of the analyzed program is named by its symbol`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const path = `${at}.name`;
|
|
563
|
+
return {
|
|
564
|
+
shape: "module",
|
|
565
|
+
module,
|
|
566
|
+
name: requireMember(readString(entry, "name", path, label), path, label),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
throw malformed(
|
|
570
|
+
label,
|
|
571
|
+
at,
|
|
572
|
+
named.length === 0
|
|
573
|
+
? 'names no declaration: an entry carries "symbol", "module" with "name", or "global"'
|
|
574
|
+
: `carries ${spell(named)}, which is not one of "symbol", "module" with "name", or "global"`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** One array of declaration entries, absent where the document does not write it. */
|
|
579
|
+
function readDeclarations(
|
|
580
|
+
parent: Record<string, unknown> | undefined,
|
|
581
|
+
name: string,
|
|
582
|
+
path: string,
|
|
583
|
+
label: string,
|
|
584
|
+
): readonly DeclarationEntry[] | undefined {
|
|
585
|
+
const value = member(parent, name);
|
|
586
|
+
if (value === undefined) {
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
if (!Array.isArray(value)) {
|
|
590
|
+
throw malformed(label, path, "is not an array");
|
|
591
|
+
}
|
|
592
|
+
return (value as unknown[]).map((entry, index) =>
|
|
593
|
+
readDeclaration(entry, `${path}[${String(index)}]`, label),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* One lifecycle contract. The member names are required, and the entry names at least
|
|
599
|
+
* one component declaration or one base class, because a contract naming neither
|
|
600
|
+
* makes no class a component and so retains nothing.
|
|
601
|
+
*/
|
|
602
|
+
function readLifecycleContract(entry: unknown, at: string, label: string): LifecycleContract {
|
|
603
|
+
if (!isRecord(entry)) {
|
|
604
|
+
throw malformed(label, at, "is not an object");
|
|
605
|
+
}
|
|
606
|
+
const components = readDeclarations(entry, "components", `${at}.components`, label) ?? [];
|
|
607
|
+
const bases = readDeclarations(entry, "bases", `${at}.bases`, label) ?? [];
|
|
608
|
+
const members = readStrings(entry, "members", `${at}.members`, label, 1);
|
|
609
|
+
if (members === undefined) {
|
|
610
|
+
throw malformed(label, `${at}.members`, "is required and names nothing");
|
|
611
|
+
}
|
|
612
|
+
if (components.length === 0 && bases.length === 0) {
|
|
613
|
+
throw malformed(
|
|
614
|
+
label,
|
|
615
|
+
at,
|
|
616
|
+
'names no declaration in "components" and no class in "bases", so it makes no class a component',
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return { components, bases, members };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** The lifecycle contracts, one per framework the project declares. */
|
|
623
|
+
function readLifecycleContracts(
|
|
624
|
+
ts: Record<string, unknown> | undefined,
|
|
625
|
+
label: string,
|
|
626
|
+
): readonly LifecycleContract[] | undefined {
|
|
627
|
+
const path = "ts.lifecycle_contracts";
|
|
628
|
+
const value = member(ts, "lifecycle_contracts");
|
|
629
|
+
if (value === undefined) {
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
if (!Array.isArray(value)) {
|
|
633
|
+
throw malformed(label, path, "is not an array");
|
|
634
|
+
}
|
|
635
|
+
return (value as unknown[]).map((entry, index) =>
|
|
636
|
+
readLifecycleContract(entry, `${path}[${String(index)}]`, label),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** One string member a provider entry requires, refused where it fails `pattern`. */
|
|
641
|
+
function readMatching(
|
|
642
|
+
entry: Record<string, unknown>,
|
|
643
|
+
name: string,
|
|
644
|
+
at: string,
|
|
645
|
+
label: string,
|
|
646
|
+
pattern: RegExp,
|
|
647
|
+
): string {
|
|
648
|
+
const path = `${at}.${name}`;
|
|
649
|
+
const value = requireMember(readString(entry, name, path, label), path, label);
|
|
650
|
+
if (!pattern.test(value)) {
|
|
651
|
+
throw malformed(label, path, `${JSON.stringify(value)} does not match ${pattern.source}`);
|
|
652
|
+
}
|
|
653
|
+
return value;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* One provider entry, read as the one shape its members name: a member only an
|
|
658
|
+
* acquirable analyzer carries makes it acquirable, and then it carries all three.
|
|
659
|
+
*/
|
|
660
|
+
function readProvider(entry: unknown, at: string, label: string): Provider {
|
|
661
|
+
if (!isRecord(entry)) {
|
|
662
|
+
throw malformed(label, at, "is not an object");
|
|
663
|
+
}
|
|
664
|
+
const name = readMatching(entry, "name", at, label, ANALYZER_NAME);
|
|
665
|
+
const languages = readStrings(entry, "languages", `${at}.languages`, label, 1, LANGUAGES);
|
|
666
|
+
if (languages === undefined) {
|
|
667
|
+
throw malformed(label, `${at}.languages`, "is required and names nothing");
|
|
668
|
+
}
|
|
669
|
+
const commandPath = `${at}.command`;
|
|
670
|
+
const command = requireMember(
|
|
671
|
+
readString(entry, "command", commandPath, label),
|
|
672
|
+
commandPath,
|
|
673
|
+
label,
|
|
674
|
+
);
|
|
675
|
+
if (!ACQUIRABLE_MEMBERS.some((member) => Object.hasOwn(entry, member))) {
|
|
676
|
+
return { shape: "installed", name, languages, command };
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
shape: "acquirable",
|
|
680
|
+
name,
|
|
681
|
+
languages,
|
|
682
|
+
command,
|
|
683
|
+
source: readMatching(entry, "source", at, label, PROVIDER_SOURCE),
|
|
684
|
+
version: readMatching(entry, "version", at, label, PROVIDER_VERSION),
|
|
685
|
+
digest: readMatching(entry, "digest", at, label, PROVIDER_DIGEST),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* The provider list. Every file a run writes for an analyzer is keyed by the
|
|
691
|
+
* entry's name, so a list naming one name twice is refused at the later entry.
|
|
692
|
+
*/
|
|
693
|
+
function readProviders(
|
|
694
|
+
providers: Record<string, unknown> | undefined,
|
|
695
|
+
label: string,
|
|
696
|
+
): readonly Provider[] | undefined {
|
|
697
|
+
const path = "providers.analyzers";
|
|
698
|
+
const value = member(providers, "analyzers");
|
|
699
|
+
if (value === undefined) {
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
if (!Array.isArray(value)) {
|
|
703
|
+
throw malformed(label, path, "is not an array");
|
|
704
|
+
}
|
|
705
|
+
const read = (value as unknown[]).map((entry, index) =>
|
|
706
|
+
readProvider(entry, `${path}[${String(index)}]`, label),
|
|
707
|
+
);
|
|
708
|
+
const first = new Map<string, number>();
|
|
709
|
+
read.forEach((entry, index) => {
|
|
710
|
+
const earlier = first.get(entry.name);
|
|
711
|
+
if (earlier !== undefined) {
|
|
712
|
+
throw malformed(
|
|
713
|
+
label,
|
|
714
|
+
`${path}[${String(index)}].name`,
|
|
715
|
+
`${JSON.stringify(entry.name)} is already the name of ${path}[${String(earlier)}], ` +
|
|
716
|
+
"and every entry's name keys the files a run writes for it",
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
first.set(entry.name, index);
|
|
720
|
+
});
|
|
721
|
+
return read;
|
|
722
|
+
}
|
|
723
|
+
|
|
479
724
|
/**
|
|
480
725
|
* Reads one already-parsed configuration document as the settings it supplies,
|
|
481
726
|
* refusing every constraint the closed key list declares that a parse cannot
|
|
@@ -492,6 +737,7 @@ function readDoc(value: unknown, label: string): Doc {
|
|
|
492
737
|
const consumers = section(value, "consumers", "consumers", label);
|
|
493
738
|
const roots = section(value, "roots", "roots", label);
|
|
494
739
|
const reporters = section(value, "reporters", "reporters", label);
|
|
740
|
+
const providers = section(value, "providers", "providers", label);
|
|
495
741
|
const ts = section(value, "ts", "ts", label);
|
|
496
742
|
section(value, "go", "go", label);
|
|
497
743
|
section(value, "provenance", "provenance", label);
|
|
@@ -543,8 +789,17 @@ function readDoc(value: unknown, label: string): Doc {
|
|
|
543
789
|
cascade: readEnum(reporters, "cascade", "reporters.cascade", label, CASCADES),
|
|
544
790
|
maxFindings: readCount(reporters, "max_findings", "reporters.max_findings", label),
|
|
545
791
|
failOn: readEnum(reporters, "fail_on", "reporters.fail_on", label, SEVERITIES),
|
|
792
|
+
providers: readProviders(providers, label),
|
|
546
793
|
testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
|
|
547
794
|
entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
|
|
795
|
+
injectionRegistrations: readDeclarations(
|
|
796
|
+
ts,
|
|
797
|
+
"injection_registrations",
|
|
798
|
+
"ts.injection_registrations",
|
|
799
|
+
label,
|
|
800
|
+
),
|
|
801
|
+
lifecycleContracts: readLifecycleContracts(ts, label),
|
|
802
|
+
serializers: readDeclarations(ts, "serializers", "ts.serializers", label),
|
|
548
803
|
};
|
|
549
804
|
}
|
|
550
805
|
|
|
@@ -815,9 +1070,23 @@ function settleAll(inputs: Inputs): { config: Config; provenance: Map<string, Or
|
|
|
815
1070
|
),
|
|
816
1071
|
failOn: at("reporters.fail_on", defaults.reporters.failOn, (doc) => doc.failOn),
|
|
817
1072
|
},
|
|
1073
|
+
providers: {
|
|
1074
|
+
analyzers: at("providers.analyzers", defaults.providers.analyzers, (doc) => doc.providers),
|
|
1075
|
+
},
|
|
818
1076
|
ts: {
|
|
819
1077
|
testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
|
|
820
1078
|
entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
|
|
1079
|
+
injectionRegistrations: at(
|
|
1080
|
+
"ts.injection_registrations",
|
|
1081
|
+
defaults.ts.injectionRegistrations,
|
|
1082
|
+
(doc) => doc.injectionRegistrations,
|
|
1083
|
+
),
|
|
1084
|
+
lifecycleContracts: at(
|
|
1085
|
+
"ts.lifecycle_contracts",
|
|
1086
|
+
defaults.ts.lifecycleContracts,
|
|
1087
|
+
(doc) => doc.lifecycleContracts,
|
|
1088
|
+
),
|
|
1089
|
+
serializers: at("ts.serializers", defaults.ts.serializers, (doc) => doc.serializers),
|
|
821
1090
|
},
|
|
822
1091
|
};
|
|
823
1092
|
|
package/src/run.ts
CHANGED
|
@@ -59,8 +59,15 @@ export const SETTING_OPTIONS: ReadonlyMap<string, string> = new Map([
|
|
|
59
59
|
/** The options that take a value and are not settings. */
|
|
60
60
|
const PLAIN_OPTIONS: ReadonlySet<string> = new Set(["target", "config", "central", "scope"]);
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The setting options whose value is an integer rather than a string. A value
|
|
64
|
+
* written other than as digits is passed on as the string it is, so resolution
|
|
65
|
+
* refuses it by its type rather than reading `1e2` as 100.
|
|
66
|
+
*/
|
|
67
|
+
const INTEGER_OPTIONS: ReadonlySet<string> = new Set(["max-findings"]);
|
|
68
|
+
|
|
69
|
+
/** An integer as an option writes it: an optional minus sign and digits alone. */
|
|
70
|
+
const INTEGER_TEXT = /^-?[0-9]+$/u;
|
|
64
71
|
|
|
65
72
|
/** One invocation's options, by name, with the verb's remaining arguments. */
|
|
66
73
|
interface Options {
|
|
@@ -145,7 +152,7 @@ function inputsOf(host: Host, options: Options): Inputs {
|
|
|
145
152
|
if (value === undefined) {
|
|
146
153
|
continue;
|
|
147
154
|
}
|
|
148
|
-
flat[path] =
|
|
155
|
+
flat[path] = INTEGER_OPTIONS.has(name) && INTEGER_TEXT.test(value) ? Number(value) : value;
|
|
149
156
|
flagLabels.set(path, `--${name}`);
|
|
150
157
|
}
|
|
151
158
|
const flags = Object.keys(flat).length === 0 ? undefined : JSON.stringify(flat);
|
package/src/schema.ts
CHANGED
|
@@ -19,14 +19,23 @@ export type KeyKind = "leaf" | "section" | "map" | "list" | "object";
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* One node of the closed key list. `members` holds a section's declared members,
|
|
22
|
-
* or a list entry's, and is absent otherwise.
|
|
22
|
+
* or a list entry's, and is absent otherwise. `integer` marks a leaf of the
|
|
23
|
+
* integer type, whose number the document writes as digits alone.
|
|
23
24
|
*/
|
|
24
25
|
export interface KeyNode {
|
|
25
26
|
readonly kind: KeyKind;
|
|
26
27
|
readonly members?: Readonly<Record<string, KeyNode>>;
|
|
28
|
+
readonly integer?: true;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const leaf: KeyNode = { kind: "leaf" };
|
|
32
|
+
const integer: KeyNode = { kind: "leaf", integer: true };
|
|
33
|
+
|
|
34
|
+
/** A list of declarations, each entry in one of the three shapes the `ts` section states. */
|
|
35
|
+
const declarations: KeyNode = {
|
|
36
|
+
kind: "list",
|
|
37
|
+
members: { symbol: leaf, module: leaf, name: leaf, global: leaf },
|
|
38
|
+
};
|
|
30
39
|
|
|
31
40
|
/**
|
|
32
41
|
* The closed key list the Contract's configuration schema declares, derived from
|
|
@@ -67,12 +76,40 @@ export const SCHEMA_ROOT: KeyNode = {
|
|
|
67
76
|
formats: leaf,
|
|
68
77
|
sort: leaf,
|
|
69
78
|
cascade: leaf,
|
|
70
|
-
max_findings:
|
|
79
|
+
max_findings: integer,
|
|
71
80
|
fail_on: leaf,
|
|
72
81
|
},
|
|
73
82
|
},
|
|
83
|
+
providers: {
|
|
84
|
+
kind: "section",
|
|
85
|
+
members: {
|
|
86
|
+
analyzers: {
|
|
87
|
+
kind: "list",
|
|
88
|
+
members: {
|
|
89
|
+
name: leaf,
|
|
90
|
+
languages: leaf,
|
|
91
|
+
command: leaf,
|
|
92
|
+
source: leaf,
|
|
93
|
+
version: leaf,
|
|
94
|
+
digest: leaf,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
74
99
|
go: { kind: "section", members: {} },
|
|
75
|
-
ts: {
|
|
100
|
+
ts: {
|
|
101
|
+
kind: "section",
|
|
102
|
+
members: {
|
|
103
|
+
test_files: leaf,
|
|
104
|
+
entry_files: leaf,
|
|
105
|
+
injection_registrations: declarations,
|
|
106
|
+
lifecycle_contracts: {
|
|
107
|
+
kind: "list",
|
|
108
|
+
members: { components: declarations, bases: declarations, members: leaf },
|
|
109
|
+
},
|
|
110
|
+
serializers: declarations,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
76
113
|
provenance: { kind: "map" },
|
|
77
114
|
},
|
|
78
115
|
};
|
|
@@ -287,6 +324,28 @@ function skipScalar(cursor: Cursor): void {
|
|
|
287
324
|
}
|
|
288
325
|
}
|
|
289
326
|
|
|
327
|
+
/** A number the document writes as an optional minus sign and digits alone. */
|
|
328
|
+
const INTEGER_TEXT = /^-?[0-9]+$/u;
|
|
329
|
+
|
|
330
|
+
/** The start of a JSON number: a minus sign or a digit. */
|
|
331
|
+
const NUMBER_START = /^[-0-9]/u;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Refuses a number written with a fraction or an exponent where the key list
|
|
335
|
+
* declares an integer. A parse reads `100.0` and `1e2` as the integer 100, so only
|
|
336
|
+
* the text can tell them apart. A scalar that is no number is the decoder's to
|
|
337
|
+
* refuse by its type.
|
|
338
|
+
*/
|
|
339
|
+
function checkInteger(text: string, at: string, label: string): void {
|
|
340
|
+
if (NUMBER_START.test(text) && !INTEGER_TEXT.test(text)) {
|
|
341
|
+
throw malformed(
|
|
342
|
+
label,
|
|
343
|
+
at,
|
|
344
|
+
`${text} is written with a fraction or an exponent, and an integer is digits alone`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
290
349
|
function walkValue(cursor: Cursor, at: string, node: KeyNode, label: string): void {
|
|
291
350
|
skipSpace(cursor);
|
|
292
351
|
const ch = cursor.text.charCodeAt(cursor.at);
|
|
@@ -304,7 +363,11 @@ function walkValue(cursor: Cursor, at: string, node: KeyNode, label: string): vo
|
|
|
304
363
|
readName(cursor, at, label);
|
|
305
364
|
return;
|
|
306
365
|
}
|
|
366
|
+
const start = cursor.at;
|
|
307
367
|
skipScalar(cursor);
|
|
368
|
+
if (node.integer === true) {
|
|
369
|
+
checkInteger(cursor.text.slice(start, cursor.at), at, label);
|
|
370
|
+
}
|
|
308
371
|
}
|
|
309
372
|
|
|
310
373
|
function walkObject(cursor: Cursor, at: string, node: KeyNode, label: string): void {
|
package/src/scope.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { ConfigError } from "./config.ts";
|
|
1
2
|
import type { Host } from "./host.ts";
|
|
2
3
|
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
4
|
+
import { checkDocument, type KeyNode } from "./schema.ts";
|
|
3
5
|
|
|
4
6
|
/** The roles a scope document declares for a module. */
|
|
5
7
|
export const ROLE_TARGET = "target";
|
|
@@ -43,20 +45,44 @@ export interface Scope {
|
|
|
43
45
|
readonly consumers: readonly Module[];
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
+
const MODULE: KeyNode = {
|
|
49
|
+
kind: "section",
|
|
50
|
+
members: { id: { kind: "leaf" }, role: { kind: "leaf" }, path: { kind: "leaf" } },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** The closed key list of the scope document, compared as bytes. */
|
|
54
|
+
const SCOPE_ROOT: KeyNode = {
|
|
55
|
+
kind: "section",
|
|
56
|
+
members: {
|
|
57
|
+
target: MODULE,
|
|
58
|
+
workspace: { kind: "leaf" },
|
|
59
|
+
consumers: { kind: "list", members: MODULE.members ?? {} },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
48
62
|
|
|
49
63
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
64
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
65
|
}
|
|
52
66
|
|
|
53
|
-
|
|
67
|
+
/**
|
|
68
|
+
* One string member, or undefined where the document omits it. A member the
|
|
69
|
+
* document writes is a string holding at least one character, so `null` and `""`
|
|
70
|
+
* are refused rather than read as absent.
|
|
71
|
+
*/
|
|
72
|
+
function stringMember(
|
|
73
|
+
parent: Record<string, unknown>,
|
|
74
|
+
name: string,
|
|
75
|
+
at: string,
|
|
76
|
+
): string | undefined {
|
|
54
77
|
if (!Object.hasOwn(parent, name)) {
|
|
55
|
-
return
|
|
78
|
+
return undefined;
|
|
56
79
|
}
|
|
57
80
|
const value = parent[name];
|
|
58
81
|
if (typeof value !== "string") {
|
|
59
|
-
throw new ScopeError(`scope: ${at}.${name} is not a string`);
|
|
82
|
+
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} is not a string`);
|
|
83
|
+
}
|
|
84
|
+
if (value === "") {
|
|
85
|
+
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} names nothing`);
|
|
60
86
|
}
|
|
61
87
|
return value;
|
|
62
88
|
}
|
|
@@ -65,33 +91,51 @@ function readModule(value: unknown, at: string, role: string, documentDir: strin
|
|
|
65
91
|
if (!isRecord(value)) {
|
|
66
92
|
throw new ScopeError(`scope: ${at} is not an object`);
|
|
67
93
|
}
|
|
68
|
-
for (const name of Object.keys(value)) {
|
|
69
|
-
if (!MODULE_KEYS.has(name)) {
|
|
70
|
-
throw new ScopeError(`scope: ${at}.${name} is not a declared key`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
94
|
const declared = stringMember(value, "role", at);
|
|
74
|
-
if (declared !==
|
|
95
|
+
if (declared !== undefined && declared !== role) {
|
|
75
96
|
throw new ScopeError(
|
|
76
97
|
`scope: ${at}.role is ${JSON.stringify(declared)}, want ${JSON.stringify(role)}`,
|
|
77
98
|
);
|
|
78
99
|
}
|
|
79
100
|
const path = stringMember(value, "path", at);
|
|
80
|
-
if (path ===
|
|
101
|
+
if (path === undefined) {
|
|
81
102
|
throw new ScopeError(`scope: ${at}.path names nothing`);
|
|
82
103
|
}
|
|
83
104
|
return {
|
|
84
|
-
id: stringMember(value, "id", at),
|
|
105
|
+
id: stringMember(value, "id", at) ?? "",
|
|
85
106
|
path: isAbsolutePath(path) ? path : joinPath(documentDir, path),
|
|
86
107
|
};
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
111
|
+
* Walks the document's text against the closed key list, refusing a key the list
|
|
112
|
+
* does not declare, compared as bytes, and a key one object writes twice, which a
|
|
113
|
+
* parse cannot report because it keeps the last value.
|
|
114
|
+
*/
|
|
115
|
+
function checkKeys(text: string, file: string): void {
|
|
116
|
+
try {
|
|
117
|
+
checkDocument(text, SCOPE_ROOT, `scope: ${file}`);
|
|
118
|
+
} catch (error: unknown) {
|
|
119
|
+
if (error instanceof ConfigError) {
|
|
120
|
+
// The configuration's refusal names the nearest configuration key, which a
|
|
121
|
+
// scope document does not have.
|
|
122
|
+
throw new ScopeError(
|
|
123
|
+
error.kind === "unimplemented-key"
|
|
124
|
+
? `scope: ${file}: ${error.key} is not a declared key`
|
|
125
|
+
: error.message,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Decodes the scope document at `file`. Decoding is strict: an undeclared key, a
|
|
134
|
+
* key written twice in one object, and a value of a type the document does not
|
|
135
|
+
* admit, `null` included, are errors, and a relative path inside the document
|
|
136
|
+
* resolves against the document's own directory.
|
|
93
137
|
*
|
|
94
|
-
* The `workspace` key the format declares is
|
|
138
|
+
* The `workspace` key the format declares is checked and carries nothing here: it
|
|
95
139
|
* names the file a Go consumer resolves the target through, and a TypeScript
|
|
96
140
|
* project resolves one through its own compiler configuration.
|
|
97
141
|
*/
|
|
@@ -119,11 +163,8 @@ export function readScope(host: Host, file: string): Scope {
|
|
|
119
163
|
if (!isRecord(value)) {
|
|
120
164
|
throw new ScopeError(`scope: ${file} is not one JSON object`);
|
|
121
165
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
throw new ScopeError(`scope: ${file}: ${name} is not a declared key`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
166
|
+
checkKeys(text, file);
|
|
167
|
+
stringMember(value, "workspace", "");
|
|
127
168
|
const documentDir = dirnamePath(absolute);
|
|
128
169
|
if (!Object.hasOwn(value, "target")) {
|
|
129
170
|
throw new ScopeError(`scope: ${file} names no target`);
|