@cplieger/deadset-ts 8.0.0-dev.7 → 8.0.0-dev.8
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/bin/node-host.js +21 -3
- package/dist/src/analysis.js +13 -14
- package/dist/src/config.js +6 -83
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/consumers.js +2 -2
- package/dist/src/contract/config.generated.js +544 -0
- package/dist/src/contract/report.generated.js +3 -2
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +36 -112
- package/dist/src/exempt-classes.js +0 -2
- package/dist/src/exempt.js +1 -1
- package/dist/src/findings/completion.js +5 -4
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +1 -1
- package/dist/src/findings/non-code-artifacts.js +2 -2
- package/dist/src/findings/unused-declarations.js +1 -1
- package/dist/src/findings/visibility-narrowing.js +2 -2
- package/dist/src/findings-pass.js +1 -1
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +59 -0
- package/dist/src/injection-container.js +1 -1
- package/dist/src/json-document.js +194 -67
- package/dist/src/kinds.js +0 -7
- package/dist/src/print.js +10 -79
- package/dist/src/report-schema.js +3 -224
- package/dist/src/resolve.js +245 -715
- package/dist/src/run.js +48 -55
- package/dist/src/schema.js +320 -344
- package/dist/src/scope.js +33 -101
- package/dist/src/serialization-contract.js +1 -1
- package/dist/src/suppress-file.js +15 -17
- package/dist/src/suppress.js +3 -3
- package/dist/src/template-field.js +4 -4
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +12 -15
- package/dist/src/verbs/explain.js +3 -3
- package/dist/src/verbs/print-projects.js +2 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +30 -26
- package/src/config.ts +31 -298
- package/src/configured-declarations.ts +18 -23
- package/src/consumers.ts +4 -4
- package/src/contract/config.generated.ts +798 -0
- package/src/contract/report.generated.ts +3 -2
- package/src/discover.ts +1 -1
- package/src/edges.ts +39 -124
- package/src/exempt-classes.ts +0 -5
- package/src/exempt.ts +4 -4
- package/src/findings/completion.ts +5 -6
- package/src/findings/emitter.ts +2 -2
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +1 -1
- package/src/findings/non-code-artifacts.ts +4 -4
- package/src/findings/unused-declarations.ts +1 -1
- package/src/findings/visibility-narrowing.ts +2 -2
- package/src/findings-pass.ts +3 -3
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +74 -0
- package/src/host.ts +12 -5
- package/src/injection-container.ts +1 -1
- package/src/json-document.ts +236 -76
- package/src/kinds.ts +0 -8
- package/src/print.ts +11 -92
- package/src/report-schema.ts +3 -299
- package/src/resolve.ts +281 -1105
- package/src/run.ts +50 -63
- package/src/schema.ts +391 -389
- package/src/scope.ts +39 -119
- package/src/serialization-contract.ts +1 -1
- package/src/suppress-file.ts +15 -20
- package/src/suppress.ts +3 -3
- package/src/template-field.ts +10 -7
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +12 -23
- package/src/verbs/explain.ts +3 -7
- package/src/verbs/print-projects.ts +2 -2
- package/src/verbs/verb.ts +2 -2
package/src/resolve.ts
CHANGED
|
@@ -1,1194 +1,370 @@
|
|
|
1
|
+
import { SETTINGS, type SettingRow } from "./contract/config.generated.ts";
|
|
2
|
+
import { FAMILY_KEY_LENGTH } from "./contract/contract.generated.ts";
|
|
1
3
|
import {
|
|
2
|
-
|
|
3
|
-
LANGUAGES,
|
|
4
|
-
CONFIDENCES,
|
|
5
|
-
FORMATS,
|
|
6
|
-
type Confidence,
|
|
7
|
-
type Format,
|
|
8
|
-
type Language,
|
|
9
|
-
type Severity,
|
|
10
|
-
FAMILY_KEY_LENGTH,
|
|
11
|
-
} from "./contract/contract.generated.ts";
|
|
12
|
-
import {
|
|
13
|
-
ConfigError,
|
|
14
|
-
defaultConfig,
|
|
15
|
-
type BuildConfiguration,
|
|
16
|
-
type Cascade,
|
|
17
|
-
type Config,
|
|
18
|
-
type ConsumerTests,
|
|
19
|
-
type DeclarationEntry,
|
|
20
|
-
type GeneratedFiles,
|
|
4
|
+
type Document,
|
|
21
5
|
type Inputs,
|
|
22
|
-
type LifecycleContract,
|
|
23
6
|
type Origin,
|
|
7
|
+
type PartialConfig,
|
|
24
8
|
type Provenance,
|
|
25
|
-
type
|
|
26
|
-
type Sort,
|
|
9
|
+
type ResolvedConfig,
|
|
27
10
|
type Source,
|
|
28
|
-
type TargetKind,
|
|
29
|
-
type TargetKindSetting,
|
|
30
|
-
type TemplateDelimiters,
|
|
31
11
|
} from "./config.ts";
|
|
32
12
|
import { CONVENTION_ROWS } from "./convention-rows.ts";
|
|
33
|
-
import {
|
|
34
|
-
import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.ts";
|
|
13
|
+
import { refuseOverBound } from "./fs/document.ts";
|
|
35
14
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
15
|
+
dotted,
|
|
16
|
+
isRecord,
|
|
17
|
+
locatedFault,
|
|
18
|
+
type Place,
|
|
19
|
+
placeIn,
|
|
20
|
+
pointerOf,
|
|
21
|
+
readStrict,
|
|
22
|
+
StrictJSONError,
|
|
23
|
+
} from "./json-document.ts";
|
|
24
|
+
import { FIXED_SEVERITY_CODES } from "./kinds.ts";
|
|
25
|
+
import { LANGUAGE } from "./product.ts";
|
|
26
|
+
import { firstRefusal, type SchemaRefusal } from "./schema.ts";
|
|
43
27
|
import { bytewise } from "./text/compare.ts";
|
|
44
|
-
import {
|
|
45
|
-
import { message } from "./product.ts";
|
|
46
|
-
|
|
47
|
-
/** Names the command-line flags in a refusal, where a document is named by its path. */
|
|
48
|
-
const FLAGS_LABEL = "the command-line flags";
|
|
49
|
-
|
|
50
|
-
const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
|
|
51
|
-
const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
|
|
52
|
-
const ANALYZER_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/u;
|
|
53
|
-
const PROVIDER_SOURCE = /^(go|npm):[^ \t\r\n]+$/u;
|
|
54
|
-
const PROVIDER_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/u;
|
|
55
|
-
const PROVIDER_DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
56
|
-
|
|
57
|
-
/** The members only an acquirable provider entry carries. */
|
|
58
|
-
const ACQUIRABLE_MEMBERS = ["source", "version", "digest"] as const;
|
|
28
|
+
import { UsageError } from "./usage.ts";
|
|
59
29
|
|
|
60
|
-
|
|
61
|
-
const PLATFORM_MEMBERS = ["os", "arch", "tags"] as const;
|
|
30
|
+
const ROWS: ReadonlyMap<string, SettingRow> = new Map(SETTINGS.map((row) => [row.key, row]));
|
|
62
31
|
|
|
63
|
-
/** The
|
|
64
|
-
const
|
|
32
|
+
/** The schema of a section or a keyed section's value, so the validator words its refusal. */
|
|
33
|
+
const OBJECT = { type: "object" };
|
|
65
34
|
|
|
66
35
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
36
|
+
* The settings whose entries name rows of this analyzer's own data, which the
|
|
37
|
+
* Contract's schema cannot list: a disabled convention names a convention row.
|
|
69
38
|
*/
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* A project path: segments joined by a solidus, so the path is relative, below
|
|
74
|
-
* the root it is read against, and spelled one way on every machine.
|
|
75
|
-
*/
|
|
76
|
-
const PROJECT_PATH = new RegExp(`^${PROJECT_SEGMENT}(?:/${PROJECT_SEGMENT})*$`, "u");
|
|
77
|
-
|
|
78
|
-
const TARGET_KINDS: readonly TargetKind[] = ["application", "library"];
|
|
79
|
-
const GENERATED_FILES: readonly GeneratedFiles[] = ["exclude", "include"];
|
|
80
|
-
const CONSUMER_TESTS: readonly ConsumerTests[] = ["test", "production"];
|
|
81
|
-
const SORTS: readonly Sort[] = ["position", "size"];
|
|
82
|
-
const CASCADES: readonly Cascade[] = ["roots", "full"];
|
|
39
|
+
const OWN_VOCABULARIES: ReadonlyMap<string, ReadonlySet<string>> = new Map([
|
|
40
|
+
["ts.disabled_conventions", new Set(CONVENTION_ROWS.map((row) => row.name))],
|
|
41
|
+
]);
|
|
83
42
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* possibly undefined, so a key the document omits is distinguishable from one it
|
|
87
|
-
* sets to a value equal to the default, which is what makes resolution per
|
|
88
|
-
* setting rather than per document.
|
|
89
|
-
*/
|
|
90
|
-
interface Doc {
|
|
91
|
-
readonly contractVersion: string | undefined;
|
|
92
|
-
readonly targetKind: TargetKindSetting | undefined;
|
|
93
|
-
readonly languages: readonly Language[] | undefined;
|
|
94
|
-
readonly minConfidence: Confidence | undefined;
|
|
95
|
-
readonly generatedFiles: GeneratedFiles | undefined;
|
|
96
|
-
readonly consumerTests: ConsumerTests | undefined;
|
|
97
|
-
readonly configurations: readonly BuildConfiguration[] | undefined;
|
|
98
|
-
readonly matrixComplete: boolean | undefined;
|
|
99
|
-
readonly templateDirs: readonly string[] | undefined;
|
|
100
|
-
readonly templateDelimiters: TemplateDelimiters | undefined;
|
|
101
|
-
readonly consumersComplete: boolean | undefined;
|
|
102
|
-
readonly rootPatterns: readonly string[] | undefined;
|
|
103
|
-
readonly severity: ReadonlyMap<string, Severity> | undefined;
|
|
104
|
-
readonly exemptionsDisabled: readonly ExemptionClass[] | undefined;
|
|
105
|
-
readonly formats: readonly Format[] | undefined;
|
|
106
|
-
readonly sort: Sort | undefined;
|
|
107
|
-
readonly cascade: Cascade | undefined;
|
|
108
|
-
readonly maxFindings: number | undefined;
|
|
109
|
-
readonly failOn: Severity | undefined;
|
|
110
|
-
readonly providers: readonly Provider[] | undefined;
|
|
111
|
-
readonly testFiles: readonly string[] | undefined;
|
|
112
|
-
readonly entryFiles: readonly string[] | undefined;
|
|
113
|
-
readonly componentExtensions: readonly string[] | undefined;
|
|
114
|
-
readonly disabledConventions: readonly string[] | undefined;
|
|
115
|
-
readonly injectionRegistrations: readonly DeclarationEntry[] | undefined;
|
|
116
|
-
readonly lifecycleContracts: readonly LifecycleContract[] | undefined;
|
|
117
|
-
readonly serializers: readonly DeclarationEntry[] | undefined;
|
|
118
|
-
readonly rootsComplete: boolean | undefined;
|
|
43
|
+
function malformed(label: string, path: string, detail: string): UsageError {
|
|
44
|
+
return new UsageError(path === "" ? `${label}: ${detail}` : `${label}: ${path}: ${detail}`);
|
|
119
45
|
}
|
|
120
46
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
readonly label: string;
|
|
126
|
-
readonly flagLabels: ReadonlyMap<string, string>;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* The origin one setting of this source carries. A flag names itself per setting,
|
|
131
|
-
* because one invocation carries many.
|
|
132
|
-
*/
|
|
133
|
-
function originOf(source: DocSource, path: string): Origin {
|
|
134
|
-
if (source.kind === "flag") {
|
|
135
|
-
return { source: "flag", label: source.flagLabels.get(path) ?? "" };
|
|
136
|
-
}
|
|
137
|
-
return { source: source.kind, label: source.label };
|
|
47
|
+
/** A key the closed key list does not declare, with the reason where there is more to say. */
|
|
48
|
+
function unimplemented(label: string, path: string, reason = ""): UsageError {
|
|
49
|
+
const why = reason === "" ? "" : `: ${reason}`;
|
|
50
|
+
return new UsageError(`${label}: key ${JSON.stringify(path)} is not implemented${why}`);
|
|
138
51
|
}
|
|
139
52
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
53
|
+
/** Why a well-formed severity key the vocabulary does not hold names no setting. */
|
|
54
|
+
function severityKeyRefused(key: string): string {
|
|
55
|
+
const fixed = FIXED_SEVERITY_CODES.filter(
|
|
56
|
+
(code) => code === key || (key.length === FAMILY_KEY_LENGTH && code.startsWith(key)),
|
|
57
|
+
).sort(bytewise);
|
|
58
|
+
if (fixed.length > 0) {
|
|
59
|
+
const named =
|
|
60
|
+
fixed.length < 2
|
|
61
|
+
? fixed.join("")
|
|
62
|
+
: `${fixed.slice(0, -1).join(", ")} and ${String(fixed.at(-1))}`;
|
|
63
|
+
return `the Contract fixes the severity of ${named}, which this key names`;
|
|
143
64
|
}
|
|
144
|
-
return
|
|
65
|
+
return key.length === FAMILY_KEY_LENGTH
|
|
66
|
+
? `no issue kind carries a code of the family ${key}`
|
|
67
|
+
: `no issue kind carries the code ${key}`;
|
|
145
68
|
}
|
|
146
69
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
label: string,
|
|
152
|
-
): Record<string, unknown> | undefined {
|
|
153
|
-
const value = member(parent, name);
|
|
154
|
-
if (value === undefined) {
|
|
155
|
-
return undefined;
|
|
70
|
+
/** A value with every object's members in ascending order, so it prints one way. */
|
|
71
|
+
function canonical(value: unknown): unknown {
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
return value.map(canonical);
|
|
156
74
|
}
|
|
157
75
|
if (!isRecord(value)) {
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
return value;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function spell(values: readonly string[]): string {
|
|
164
|
-
return values.map((value) => JSON.stringify(value)).join(", ");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function readEnum<T extends string>(
|
|
168
|
-
parent: Record<string, unknown> | undefined,
|
|
169
|
-
name: string,
|
|
170
|
-
path: string,
|
|
171
|
-
label: string,
|
|
172
|
-
allowed: readonly T[],
|
|
173
|
-
): T | undefined {
|
|
174
|
-
const value = member(parent, name);
|
|
175
|
-
if (value === undefined) {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
if (typeof value !== "string") {
|
|
179
|
-
throw malformed(label, path, "is not a string");
|
|
76
|
+
return value;
|
|
180
77
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
function readString(
|
|
188
|
-
parent: Record<string, unknown> | undefined,
|
|
189
|
-
name: string,
|
|
190
|
-
path: string,
|
|
191
|
-
label: string,
|
|
192
|
-
): string | undefined {
|
|
193
|
-
const value = member(parent, name);
|
|
194
|
-
if (value === undefined) {
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
if (typeof value !== "string") {
|
|
198
|
-
throw malformed(label, path, "is not a string");
|
|
199
|
-
}
|
|
200
|
-
return value;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function readBoolean(
|
|
204
|
-
parent: Record<string, unknown> | undefined,
|
|
205
|
-
name: string,
|
|
206
|
-
path: string,
|
|
207
|
-
label: string,
|
|
208
|
-
): boolean | undefined {
|
|
209
|
-
const value = member(parent, name);
|
|
210
|
-
if (value === undefined) {
|
|
211
|
-
return undefined;
|
|
212
|
-
}
|
|
213
|
-
if (typeof value !== "boolean") {
|
|
214
|
-
throw malformed(label, path, "is not a boolean");
|
|
215
|
-
}
|
|
216
|
-
return value;
|
|
78
|
+
return Object.fromEntries(
|
|
79
|
+
Object.keys(value)
|
|
80
|
+
.sort(bytewise)
|
|
81
|
+
.map((name) => [name, canonical(value[name])]),
|
|
82
|
+
);
|
|
217
83
|
}
|
|
218
84
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
label: string,
|
|
224
|
-
): number | undefined {
|
|
225
|
-
const value = member(parent, name);
|
|
226
|
-
if (value === undefined) {
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
230
|
-
throw malformed(label, path, "is not an integer");
|
|
231
|
-
}
|
|
232
|
-
if (value < 0) {
|
|
233
|
-
throw malformed(label, path, `${String(value)} is below the minimum of 0`);
|
|
234
|
-
}
|
|
235
|
-
return value;
|
|
85
|
+
/** What one checked source supplies: each setting's value and each keyed section's members. */
|
|
86
|
+
interface Supplied {
|
|
87
|
+
readonly values: Map<string, unknown>;
|
|
88
|
+
readonly keyed: Map<string, Readonly<Record<string, unknown>>>;
|
|
236
89
|
}
|
|
237
90
|
|
|
238
91
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
92
|
+
* The walk of one source over the closed key list. The members of an object are checked
|
|
93
|
+
* in ascending order and a key is checked whole before the next. A key this analyzer's
|
|
94
|
+
* language does not read is accepted unread, and of a key written per language, every
|
|
95
|
+
* other language's member.
|
|
242
96
|
*/
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (typeof entry !== "string") {
|
|
269
|
-
throw malformed(label, at, "is not a string");
|
|
270
|
-
}
|
|
271
|
-
if (entry === "") {
|
|
272
|
-
throw malformed(label, at, "is empty");
|
|
273
|
-
}
|
|
274
|
-
if (seen.has(entry)) {
|
|
275
|
-
throw malformed(label, at, `names ${JSON.stringify(entry)} again`);
|
|
276
|
-
}
|
|
277
|
-
if (allowed !== undefined && !(allowed as readonly string[]).includes(entry)) {
|
|
278
|
-
throw malformed(label, at, `${JSON.stringify(entry)} is not one of ${spell(allowed)}`);
|
|
97
|
+
class Walk {
|
|
98
|
+
private readonly label: string;
|
|
99
|
+
private readonly fractions: ReadonlyMap<string, string>;
|
|
100
|
+
private readonly into: Supplied;
|
|
101
|
+
|
|
102
|
+
constructor(label: string, fractions: ReadonlyMap<string, string>, into: Supplied) {
|
|
103
|
+
this.label = label;
|
|
104
|
+
this.fractions = fractions;
|
|
105
|
+
this.into = into;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
section(at: string, members: Readonly<Record<string, unknown>>): void {
|
|
109
|
+
for (const name of Object.keys(members).sort(bytewise)) {
|
|
110
|
+
const key = at === "" ? name : `${at}.${name}`;
|
|
111
|
+
const row = ROWS.get(key);
|
|
112
|
+
if (name.includes(".")) {
|
|
113
|
+
const reason = row === undefined ? "" : "it is one setting, written as nested objects";
|
|
114
|
+
throw unimplemented(this.label, key, reason);
|
|
115
|
+
}
|
|
116
|
+
if (row === undefined) {
|
|
117
|
+
throw unimplemented(this.label, key);
|
|
118
|
+
}
|
|
119
|
+
if (row.read) {
|
|
120
|
+
this.key(row, members[name]);
|
|
121
|
+
}
|
|
279
122
|
}
|
|
280
|
-
seen.add(entry);
|
|
281
123
|
}
|
|
282
|
-
return value as readonly T[];
|
|
283
|
-
}
|
|
284
124
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return undefined;
|
|
299
|
-
}
|
|
300
|
-
if (typeof value === "string") {
|
|
301
|
-
if (!(TARGET_KINDS as readonly string[]).includes(value)) {
|
|
302
|
-
throw malformed(
|
|
303
|
-
label,
|
|
304
|
-
"target.kind",
|
|
305
|
-
`${JSON.stringify(value)} is not one of ${spell(TARGET_KINDS)}, nor ${perLanguage}`,
|
|
306
|
-
);
|
|
125
|
+
private key(row: SettingRow, value: unknown): void {
|
|
126
|
+
if (row.role === "section") {
|
|
127
|
+
this.refuse(row.key.split("."), value, firstRefusal(OBJECT, value));
|
|
128
|
+
this.section(row.key, value as Record<string, unknown>);
|
|
129
|
+
} else if (row.role === "setting") {
|
|
130
|
+
const read = row.perLanguage === undefined ? value : this.narrowed(row, value);
|
|
131
|
+
this.setting(row, read);
|
|
132
|
+
this.into.values.set(row.key, canonical(read));
|
|
133
|
+
} else {
|
|
134
|
+
const members = this.keyed(row, value);
|
|
135
|
+
if (row.role === "keyed") {
|
|
136
|
+
this.into.keyed.set(row.key, members);
|
|
137
|
+
}
|
|
307
138
|
}
|
|
308
|
-
return value as TargetKind;
|
|
309
139
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
140
|
+
|
|
141
|
+
/** A per-language value as this language reads it: of the object form, its own member alone. */
|
|
142
|
+
private narrowed(row: SettingRow, value: unknown): unknown {
|
|
143
|
+
if (!isRecord(value)) {
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
const others: readonly string[] = row.perLanguage ?? [];
|
|
147
|
+
return Object.fromEntries(
|
|
148
|
+
Object.entries(value).filter(([name]) => name === LANGUAGE || !others.includes(name)),
|
|
315
149
|
);
|
|
316
150
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const kind = value[language];
|
|
323
|
-
if (typeof kind !== "string" || !(TARGET_KINDS as readonly string[]).includes(kind)) {
|
|
151
|
+
|
|
152
|
+
private setting(row: SettingRow, value: unknown): void {
|
|
153
|
+
this.refuse(row.key.split("."), value, firstRefusal(row.value ?? {}, value));
|
|
154
|
+
const text = this.fractions.get(pointerOf(row.key.split(".")));
|
|
155
|
+
if (typeof value === "number" && text !== undefined) {
|
|
324
156
|
throw malformed(
|
|
325
|
-
label,
|
|
326
|
-
|
|
327
|
-
|
|
157
|
+
this.label,
|
|
158
|
+
row.key,
|
|
159
|
+
`holds ${text}, want an integer written as an optional minus sign and digits alone`,
|
|
328
160
|
);
|
|
329
161
|
}
|
|
162
|
+
const own = OWN_VOCABULARIES.get(row.key);
|
|
163
|
+
if (own !== undefined) {
|
|
164
|
+
const names = value as readonly string[];
|
|
165
|
+
const unknown = names.findIndex((name) => !own.has(name));
|
|
166
|
+
if (unknown !== -1) {
|
|
167
|
+
throw malformed(
|
|
168
|
+
this.label,
|
|
169
|
+
`${row.key}[${String(unknown)}]`,
|
|
170
|
+
`${JSON.stringify(names[unknown])} names no row this analyzer carries`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
330
174
|
}
|
|
331
|
-
return { go: value["go"] as TargetKind, ts: value["ts"] as TargetKind };
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/** A component file's extension: a full stop followed by letters and digits. */
|
|
335
|
-
const COMPONENT_EXTENSION = /^\.[A-Za-z0-9]+$/u;
|
|
336
|
-
|
|
337
|
-
function readComponentExtensions(
|
|
338
|
-
ts: Record<string, unknown> | undefined,
|
|
339
|
-
label: string,
|
|
340
|
-
): readonly string[] | undefined {
|
|
341
|
-
const extensions = readStrings(ts, "component_extensions", "ts.component_extensions", label, 0);
|
|
342
|
-
const wrong = extensions?.findIndex((one) => !COMPONENT_EXTENSION.test(one)) ?? -1;
|
|
343
|
-
if (extensions !== undefined && wrong !== -1) {
|
|
344
|
-
throw malformed(
|
|
345
|
-
label,
|
|
346
|
-
`ts.component_extensions[${String(wrong)}]`,
|
|
347
|
-
`${JSON.stringify(extensions[wrong])} is not a full stop followed by letters and digits`,
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
return extensions;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** The names of the convention rows this analyzer's table carries. */
|
|
354
|
-
const CONVENTION_NAMES: ReadonlySet<string> = new Set(CONVENTION_ROWS.map((row) => row.name));
|
|
355
|
-
|
|
356
|
-
function readDisabledConventions(
|
|
357
|
-
ts: Record<string, unknown> | undefined,
|
|
358
|
-
label: string,
|
|
359
|
-
): readonly string[] | undefined {
|
|
360
|
-
const names = readStrings(ts, "disabled_conventions", "ts.disabled_conventions", label, 0);
|
|
361
|
-
const unknown = names?.findIndex((one) => !CONVENTION_NAMES.has(one)) ?? -1;
|
|
362
|
-
if (names !== undefined && unknown !== -1) {
|
|
363
|
-
throw malformed(
|
|
364
|
-
label,
|
|
365
|
-
`ts.disabled_conventions[${String(unknown)}]`,
|
|
366
|
-
`${JSON.stringify(names[unknown])} names no convention row this analyzer carries`,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
return names;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function requireMember(value: string | undefined, path: string, label: string): string {
|
|
373
|
-
if (value === undefined || value === "") {
|
|
374
|
-
throw malformed(label, path, "is required and names nothing");
|
|
375
|
-
}
|
|
376
|
-
return value;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/** One build configuration entry as a platform: an identifier, an operating system and an architecture. */
|
|
380
|
-
function readPlatform(
|
|
381
|
-
entry: Record<string, unknown>,
|
|
382
|
-
at: string,
|
|
383
|
-
label: string,
|
|
384
|
-
): BuildConfiguration {
|
|
385
|
-
return {
|
|
386
|
-
shape: "platform",
|
|
387
|
-
id: requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label),
|
|
388
|
-
os: requireMember(readString(entry, "os", `${at}.os`, label), `${at}.os`, label),
|
|
389
|
-
arch: requireMember(readString(entry, "arch", `${at}.arch`, label), `${at}.arch`, label),
|
|
390
|
-
tags: readStrings(entry, "tags", `${at}.tags`, label, 0) ?? [],
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/** One build configuration entry as a project: an identifier and a configuration file below the target root. */
|
|
395
|
-
function readProject(
|
|
396
|
-
entry: Record<string, unknown>,
|
|
397
|
-
at: string,
|
|
398
|
-
label: string,
|
|
399
|
-
): BuildConfiguration {
|
|
400
|
-
const id = requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label);
|
|
401
|
-
const path = `${at}.${PROJECT_MEMBER}`;
|
|
402
|
-
const project = requireMember(readString(entry, PROJECT_MEMBER, path, label), path, label);
|
|
403
|
-
if (!PROJECT_PATH.test(project)) {
|
|
404
|
-
throw malformed(
|
|
405
|
-
label,
|
|
406
|
-
path,
|
|
407
|
-
`${JSON.stringify(project)} is not a path below the target root: segments joined by /, ` +
|
|
408
|
-
"none empty, . or .., and no control character",
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
return { shape: "project", id, project };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* One build configuration entry, read as the one shape its members name: a member
|
|
416
|
-
* only a platform carries makes it a platform, the member only a project carries
|
|
417
|
-
* makes it a project, and an entry naming members of both, or of neither, is
|
|
418
|
-
* refused at the entry, because the two shapes are exclusive.
|
|
419
|
-
*/
|
|
420
|
-
function readConfiguration(entry: unknown, at: string, label: string): BuildConfiguration {
|
|
421
|
-
if (!isRecord(entry)) {
|
|
422
|
-
throw malformed(label, at, "is not an object");
|
|
423
|
-
}
|
|
424
|
-
const platform = PLATFORM_MEMBERS.filter((name) => Object.hasOwn(entry, name));
|
|
425
|
-
const project = Object.hasOwn(entry, PROJECT_MEMBER);
|
|
426
|
-
if (platform.length > 0 && project) {
|
|
427
|
-
throw malformed(
|
|
428
|
-
label,
|
|
429
|
-
at,
|
|
430
|
-
`carries ${spell(platform)} of a platform and ${JSON.stringify(PROJECT_MEMBER)} of a ` +
|
|
431
|
-
"project, and an entry is one shape or the other",
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
if (project) {
|
|
435
|
-
return readProject(entry, at, label);
|
|
436
|
-
}
|
|
437
|
-
if (platform.length === 0) {
|
|
438
|
-
throw malformed(
|
|
439
|
-
label,
|
|
440
|
-
at,
|
|
441
|
-
`names neither a platform's "os" and "arch" nor a project's ${JSON.stringify(PROJECT_MEMBER)}`,
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
return readPlatform(entry, at, label);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/** The build matrix, one entry in one shape at a time. */
|
|
448
|
-
function readConfigurations(
|
|
449
|
-
analysis: Record<string, unknown> | undefined,
|
|
450
|
-
label: string,
|
|
451
|
-
): readonly BuildConfiguration[] | undefined {
|
|
452
|
-
const value = member(analysis, "configurations");
|
|
453
|
-
if (value === undefined) {
|
|
454
|
-
return undefined;
|
|
455
|
-
}
|
|
456
|
-
if (!Array.isArray(value)) {
|
|
457
|
-
throw malformed(label, "analysis.configurations", "is not an array");
|
|
458
|
-
}
|
|
459
|
-
return (value as unknown[]).map((entry, index) =>
|
|
460
|
-
readConfiguration(entry, `analysis.configurations[${String(index)}]`, label),
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* The action delimiter pair: once a document names the object both members are
|
|
466
|
-
* required, because a pair carrying one member names no delimiters at all, so the
|
|
467
|
-
* refusal names the member the document left out rather than pairing the one it
|
|
468
|
-
* named with a default.
|
|
469
|
-
*/
|
|
470
|
-
function readDelimiters(
|
|
471
|
-
analysis: Record<string, unknown> | undefined,
|
|
472
|
-
label: string,
|
|
473
|
-
): TemplateDelimiters | undefined {
|
|
474
|
-
const at = "analysis.template_delimiters";
|
|
475
|
-
const pair = section(analysis, "template_delimiters", at, label);
|
|
476
|
-
if (pair === undefined) {
|
|
477
|
-
return undefined;
|
|
478
|
-
}
|
|
479
|
-
return {
|
|
480
|
-
left: requireMember(readString(pair, "left", `${at}.left`, label), `${at}.left`, label),
|
|
481
|
-
right: requireMember(readString(pair, "right", `${at}.right`, label), `${at}.right`, label),
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
175
|
|
|
485
|
-
/**
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
*/
|
|
503
|
-
function fixedByContract(code: string): string[] {
|
|
504
|
-
return FIXED_SEVERITY_CODES.filter(
|
|
505
|
-
(candidate) =>
|
|
506
|
-
code === candidate || (code.length === FAMILY_KEY_LENGTH && candidate.startsWith(code)),
|
|
507
|
-
).sort(bytewise);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/** The codes a refusal names: one on its own, and several with the last joined by and. */
|
|
511
|
-
function spellCodes(codes: readonly string[]): string {
|
|
512
|
-
if (codes.length < 2) {
|
|
513
|
-
return codes.join("");
|
|
176
|
+
/** A keyed or an ignored section: an object whose member names its key pattern admits. */
|
|
177
|
+
private keyed(row: SettingRow, value: unknown): Readonly<Record<string, unknown>> {
|
|
178
|
+
this.refuse(row.key.split("."), value, firstRefusal(OBJECT, value));
|
|
179
|
+
const members = value as Readonly<Record<string, unknown>>;
|
|
180
|
+
const shape = new RegExp(row.keyPattern ?? "", "u");
|
|
181
|
+
for (const name of Object.keys(members).sort(bytewise)) {
|
|
182
|
+
const at = `${row.key}.${name}`;
|
|
183
|
+
if (!shape.test(name)) {
|
|
184
|
+
throw unimplemented(this.label, at, `the name does not match ${String(row.keyPattern)}`);
|
|
185
|
+
}
|
|
186
|
+
if (row.values !== undefined && !row.values.includes(name)) {
|
|
187
|
+
throw unimplemented(this.label, at, severityKeyRefused(name));
|
|
188
|
+
}
|
|
189
|
+
const member = members[name];
|
|
190
|
+
this.refuse([...row.key.split("."), name], member, firstRefusal(row.value ?? {}, member));
|
|
191
|
+
}
|
|
192
|
+
return members;
|
|
514
193
|
}
|
|
515
|
-
return `${codes.slice(0, -1).join(", ")} and ${String(codes[codes.length - 1])}`;
|
|
516
|
-
}
|
|
517
194
|
|
|
518
|
-
/**
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
code === candidate || (code.length === FAMILY_KEY_LENGTH && candidate.startsWith(code)),
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* The severity object, one code at a time.
|
|
528
|
-
*
|
|
529
|
-
* A key is one issue-kind code or one two-digit family prefix. A key naming no
|
|
530
|
-
* live kind, and a key naming a kind whose severity the Contract fixes or a family
|
|
531
|
-
* prefix whose range holds one, is an unimplemented key rather than a setting.
|
|
532
|
-
*
|
|
533
|
-
* Whether a key names a kind this analyzer reports is not decided here: a live
|
|
534
|
-
* kind of another language is a setting the analyzer of that language reads.
|
|
535
|
-
*/
|
|
536
|
-
function readSeverity(
|
|
537
|
-
document: Record<string, unknown> | undefined,
|
|
538
|
-
label: string,
|
|
539
|
-
): ReadonlyMap<string, Severity> | undefined {
|
|
540
|
-
const object = section(document, SEVERITY_SECTION, SEVERITY_SECTION, label);
|
|
541
|
-
if (object === undefined) {
|
|
542
|
-
return undefined;
|
|
543
|
-
}
|
|
544
|
-
const severity = new Map<string, Severity>();
|
|
545
|
-
for (const code of Object.keys(object).sort(bytewise)) {
|
|
546
|
-
const path = `${SEVERITY_SECTION}.${code}`;
|
|
547
|
-
if (!SEVERITY_KEY.test(code)) {
|
|
548
|
-
throw unimplementedSeverityKey(
|
|
549
|
-
label,
|
|
550
|
-
path,
|
|
551
|
-
"a severity key is one issue-kind code or one two-digit family prefix",
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
if (!namesLiveKind(code)) {
|
|
555
|
-
throw unimplementedSeverityKey(
|
|
556
|
-
label,
|
|
557
|
-
path,
|
|
558
|
-
code.length === FAMILY_KEY_LENGTH
|
|
559
|
-
? "the family this prefix names holds no live issue kind"
|
|
560
|
-
: "the code names no live issue kind",
|
|
561
|
-
);
|
|
195
|
+
/** Throws the refusal the schema makes of `value`, at `at`, naming the place inside it. */
|
|
196
|
+
private refuse(at: Place, value: unknown, refused: SchemaRefusal | undefined): void {
|
|
197
|
+
if (refused === undefined) {
|
|
198
|
+
return;
|
|
562
199
|
}
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
label,
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
const value = readEnum(object, code, path, label, SEVERITIES);
|
|
572
|
-
if (value !== undefined) {
|
|
573
|
-
severity.set(code, value);
|
|
200
|
+
const place = dotted([...at, ...placeIn(value, refused.pointer, refused.member)]);
|
|
201
|
+
switch (refused.keyword) {
|
|
202
|
+
case "additionalProperties":
|
|
203
|
+
throw unimplemented(this.label, place);
|
|
204
|
+
case "required":
|
|
205
|
+
throw malformed(this.label, place, "is required and absent");
|
|
574
206
|
}
|
|
207
|
+
throw malformed(this.label, place, refused.reason);
|
|
575
208
|
}
|
|
576
|
-
return severity;
|
|
577
209
|
}
|
|
578
210
|
|
|
579
|
-
/**
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
label: string
|
|
583
|
-
|
|
584
|
-
const exemptions = section(document, "exemptions", "exemptions", label);
|
|
585
|
-
return readStrings(
|
|
586
|
-
exemptions,
|
|
587
|
-
"disabled",
|
|
588
|
-
"exemptions.disabled",
|
|
589
|
-
label,
|
|
590
|
-
0,
|
|
591
|
-
EXEMPTION_CLASS_NAMES,
|
|
592
|
-
);
|
|
211
|
+
/** One source a resolution reads, with what it supplies. */
|
|
212
|
+
interface Held {
|
|
213
|
+
readonly source: Source;
|
|
214
|
+
readonly label: string;
|
|
215
|
+
readonly supplied: Supplied;
|
|
593
216
|
}
|
|
594
217
|
|
|
595
|
-
|
|
596
|
-
const SHAPE_MEMBERS = ["symbol", "module", "name", "global"] as const;
|
|
597
|
-
|
|
598
|
-
/** The prefix of a stable symbol reference in the TypeScript form. */
|
|
599
|
-
const SYMBOL_PREFIX = "ts://";
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* A bare specifier: neither relative nor absolute, and not one the package's own
|
|
603
|
-
* imports field maps, each of which names a file of the analyzed program.
|
|
604
|
-
*/
|
|
605
|
-
const BARE_SPECIFIER = /^[^./#]/u;
|
|
218
|
+
const supplies = (): Supplied => ({ values: new Map(), keyed: new Map() });
|
|
606
219
|
|
|
607
|
-
/**
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (named.length === 1 && named[0] === "symbol") {
|
|
618
|
-
const symbol = requireMember(
|
|
619
|
-
readString(entry, "symbol", `${at}.symbol`, label),
|
|
620
|
-
`${at}.symbol`,
|
|
621
|
-
label,
|
|
622
|
-
);
|
|
623
|
-
if (!symbol.startsWith(SYMBOL_PREFIX)) {
|
|
624
|
-
throw malformed(label, `${at}.symbol`, `${JSON.stringify(symbol)} is not a ts:// reference`);
|
|
625
|
-
}
|
|
626
|
-
return { shape: "symbol", symbol };
|
|
627
|
-
}
|
|
628
|
-
if (named.length === 1 && named[0] === "global") {
|
|
629
|
-
const path = `${at}.global`;
|
|
630
|
-
return {
|
|
631
|
-
shape: "global",
|
|
632
|
-
global: requireMember(readString(entry, "global", path, label), path, label),
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
if (named.length === 2 && named.includes("module") && named.includes("name")) {
|
|
636
|
-
const module = requireMember(
|
|
637
|
-
readString(entry, "module", `${at}.module`, label),
|
|
638
|
-
`${at}.module`,
|
|
639
|
-
label,
|
|
640
|
-
);
|
|
641
|
-
if (!BARE_SPECIFIER.test(module)) {
|
|
642
|
-
throw malformed(
|
|
643
|
-
label,
|
|
644
|
-
`${at}.module`,
|
|
645
|
-
`${JSON.stringify(module)} is not a bare specifier: a file of the analyzed program is named by its symbol`,
|
|
646
|
-
);
|
|
220
|
+
/** Reads and checks one configuration document. */
|
|
221
|
+
function documentSource(source: Source, document: Document): Held {
|
|
222
|
+
const { text, label } = document;
|
|
223
|
+
refuseOverBound(label, text);
|
|
224
|
+
let read;
|
|
225
|
+
try {
|
|
226
|
+
read = readStrict(text);
|
|
227
|
+
} catch (error: unknown) {
|
|
228
|
+
if (!(error instanceof StrictJSONError)) {
|
|
229
|
+
throw error;
|
|
647
230
|
}
|
|
648
|
-
|
|
649
|
-
return {
|
|
650
|
-
shape: "module",
|
|
651
|
-
module,
|
|
652
|
-
name: requireMember(readString(entry, "name", path, label), path, label),
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
throw malformed(
|
|
656
|
-
label,
|
|
657
|
-
at,
|
|
658
|
-
named.length === 0
|
|
659
|
-
? 'names no declaration: an entry carries "symbol", "module" with "name", or "global"'
|
|
660
|
-
: `carries ${spell(named)}, which is not one of "symbol", "module" with "name", or "global"`,
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/** One array of declaration entries, absent where the document does not write it. */
|
|
665
|
-
function readDeclarations(
|
|
666
|
-
parent: Record<string, unknown> | undefined,
|
|
667
|
-
name: string,
|
|
668
|
-
path: string,
|
|
669
|
-
label: string,
|
|
670
|
-
): readonly DeclarationEntry[] | undefined {
|
|
671
|
-
const value = member(parent, name);
|
|
672
|
-
if (value === undefined) {
|
|
673
|
-
return undefined;
|
|
231
|
+
throw malformed(label, dotted(error.place), locatedFault(text, error));
|
|
674
232
|
}
|
|
675
|
-
if (!
|
|
676
|
-
throw malformed(label,
|
|
233
|
+
if (!isRecord(read.value)) {
|
|
234
|
+
throw malformed(label, "", "is not one JSON object");
|
|
677
235
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
236
|
+
const supplied = supplies();
|
|
237
|
+
new Walk(label, read.fractions, supplied).section("", read.value);
|
|
238
|
+
return { source, label, supplied };
|
|
681
239
|
}
|
|
682
240
|
|
|
683
|
-
/**
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
label,
|
|
701
|
-
at,
|
|
702
|
-
'names no declaration in "components" and no class in "bases", so it makes no class a component',
|
|
241
|
+
/** The command line as a source: each flag checked as the one-setting document it supplies. */
|
|
242
|
+
function flagSource(flags: ReadonlyMap<string, string>): Held {
|
|
243
|
+
const supplied = supplies();
|
|
244
|
+
for (const key of [...flags.keys()].sort(bytewise)) {
|
|
245
|
+
const row = ROWS.get(key);
|
|
246
|
+
const text = flags.get(key) ?? "";
|
|
247
|
+
if (row?.option === undefined) {
|
|
248
|
+
throw unimplemented("the command line", key, "no flag supplies it");
|
|
249
|
+
}
|
|
250
|
+
const integer = row.value?.["type"] === "integer" && /^-?[0-9]+$/u.test(text);
|
|
251
|
+
let document: unknown = integer ? Number(text) : text;
|
|
252
|
+
for (const name of key.split(".").reverse()) {
|
|
253
|
+
document = { [name]: document };
|
|
254
|
+
}
|
|
255
|
+
new Walk(`--${row.option}`, new Map(), supplied).section(
|
|
256
|
+
"",
|
|
257
|
+
document as Record<string, unknown>,
|
|
703
258
|
);
|
|
704
259
|
}
|
|
705
|
-
return {
|
|
260
|
+
return { source: "flag", label: "", supplied };
|
|
706
261
|
}
|
|
707
262
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
label: string,
|
|
712
|
-
): readonly LifecycleContract[] | undefined {
|
|
713
|
-
const path = "ts.lifecycle_contracts";
|
|
714
|
-
const value = member(ts, "lifecycle_contracts");
|
|
715
|
-
if (value === undefined) {
|
|
716
|
-
return undefined;
|
|
717
|
-
}
|
|
718
|
-
if (!Array.isArray(value)) {
|
|
719
|
-
throw malformed(label, path, "is not an array");
|
|
720
|
-
}
|
|
721
|
-
return (value as unknown[]).map((entry, index) =>
|
|
722
|
-
readLifecycleContract(entry, `${path}[${String(index)}]`, label),
|
|
723
|
-
);
|
|
263
|
+
function originOf(held: Held, key: string): Origin {
|
|
264
|
+
const label = held.source === "flag" ? `--${String(ROWS.get(key)?.option)}` : held.label;
|
|
265
|
+
return { source: held.source, label };
|
|
724
266
|
}
|
|
725
267
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
entry: Record<string, unknown>,
|
|
729
|
-
name: string,
|
|
730
|
-
at: string,
|
|
731
|
-
label: string,
|
|
732
|
-
pattern: RegExp,
|
|
733
|
-
): string {
|
|
734
|
-
const path = `${at}.${name}`;
|
|
735
|
-
const value = requireMember(readString(entry, name, path, label), path, label);
|
|
736
|
-
if (!pattern.test(value)) {
|
|
737
|
-
throw malformed(label, path, `${JSON.stringify(value)} does not match ${pattern.source}`);
|
|
738
|
-
}
|
|
739
|
-
return value;
|
|
268
|
+
function searched(named: string, document: Document | undefined): string {
|
|
269
|
+
return `${named} (${document === undefined ? "not present" : document.label})`;
|
|
740
270
|
}
|
|
741
271
|
|
|
742
|
-
/**
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
throw malformed(label, at, "is not an object");
|
|
749
|
-
}
|
|
750
|
-
const name = readMatching(entry, "name", at, label, ANALYZER_NAME);
|
|
751
|
-
const languages = readStrings(entry, "languages", `${at}.languages`, label, 1, LANGUAGES);
|
|
752
|
-
if (languages === undefined) {
|
|
753
|
-
throw malformed(label, `${at}.languages`, "is required and names nothing");
|
|
754
|
-
}
|
|
755
|
-
const commandPath = `${at}.command`;
|
|
756
|
-
const command = requireMember(
|
|
757
|
-
readString(entry, "command", commandPath, label),
|
|
758
|
-
commandPath,
|
|
759
|
-
label,
|
|
760
|
-
);
|
|
761
|
-
if (!ACQUIRABLE_MEMBERS.some((member) => Object.hasOwn(entry, member))) {
|
|
762
|
-
return { shape: "installed", name, languages, command };
|
|
763
|
-
}
|
|
764
|
-
return {
|
|
765
|
-
shape: "acquirable",
|
|
766
|
-
name,
|
|
767
|
-
languages,
|
|
768
|
-
command,
|
|
769
|
-
source: readMatching(entry, "source", at, label, PROVIDER_SOURCE),
|
|
770
|
-
version: readMatching(entry, "version", at, label, PROVIDER_VERSION),
|
|
771
|
-
digest: readMatching(entry, "digest", at, label, PROVIDER_DIGEST),
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* The provider list. Every file a run writes for an analyzer is keyed by the
|
|
777
|
-
* entry's name, so a list naming one name twice is refused at the later entry.
|
|
778
|
-
*/
|
|
779
|
-
function readProviders(
|
|
780
|
-
providers: Record<string, unknown> | undefined,
|
|
781
|
-
label: string,
|
|
782
|
-
): readonly Provider[] | undefined {
|
|
783
|
-
const path = "providers.analyzers";
|
|
784
|
-
const value = member(providers, "analyzers");
|
|
785
|
-
if (value === undefined) {
|
|
786
|
-
return undefined;
|
|
272
|
+
/** Sets the value at one dotted key of the resolved tree, whose sections the table lists first. */
|
|
273
|
+
function setAt(tree: Record<string, unknown>, key: string, value: unknown): void {
|
|
274
|
+
const names = key.split(".");
|
|
275
|
+
let into = tree;
|
|
276
|
+
for (const name of names.slice(0, -1)) {
|
|
277
|
+
into = into[name] as Record<string, unknown>;
|
|
787
278
|
}
|
|
788
|
-
|
|
789
|
-
throw malformed(label, path, "is not an array");
|
|
790
|
-
}
|
|
791
|
-
const read = (value as unknown[]).map((entry, index) =>
|
|
792
|
-
readProvider(entry, `${path}[${String(index)}]`, label),
|
|
793
|
-
);
|
|
794
|
-
const first = new Map<string, number>();
|
|
795
|
-
read.forEach((entry, index) => {
|
|
796
|
-
const earlier = first.get(entry.name);
|
|
797
|
-
if (earlier !== undefined) {
|
|
798
|
-
throw malformed(
|
|
799
|
-
label,
|
|
800
|
-
`${path}[${String(index)}].name`,
|
|
801
|
-
`${JSON.stringify(entry.name)} is already the name of ${path}[${String(earlier)}], ` +
|
|
802
|
-
"and every entry's name keys the files a run writes for it",
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
first.set(entry.name, index);
|
|
806
|
-
});
|
|
807
|
-
return read;
|
|
279
|
+
into[names.at(-1) ?? key] = value;
|
|
808
280
|
}
|
|
809
281
|
|
|
810
282
|
/**
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
283
|
+
* Every setting this analyzer reads, each from the highest-ranked source carrying it,
|
|
284
|
+
* else its default, in the closed key list's order. A setting with no default no
|
|
285
|
+
* source supplies is refused where its row is required and `enforce` holds, and left
|
|
286
|
+
* unset otherwise. A keyed section's members overlay its default's, member by member.
|
|
815
287
|
*/
|
|
816
|
-
function
|
|
817
|
-
|
|
818
|
-
|
|
288
|
+
function settle(
|
|
289
|
+
inputs: Inputs,
|
|
290
|
+
enforce: boolean,
|
|
291
|
+
): { config: PartialConfig; provenance: Provenance } {
|
|
292
|
+
const sources: Held[] = [];
|
|
293
|
+
if (inputs.flags !== undefined && inputs.flags.size > 0) {
|
|
294
|
+
sources.push(flagSource(inputs.flags));
|
|
819
295
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const target = section(value, "target", "target", label);
|
|
823
|
-
const consumers = section(value, "consumers", "consumers", label);
|
|
824
|
-
const roots = section(value, "roots", "roots", label);
|
|
825
|
-
const reporters = section(value, "reporters", "reporters", label);
|
|
826
|
-
const providers = section(value, "providers", "providers", label);
|
|
827
|
-
const ts = section(value, "ts", "ts", label);
|
|
828
|
-
section(value, "go", "go", label);
|
|
829
|
-
section(value, "provenance", "provenance", label);
|
|
830
|
-
|
|
831
|
-
const contractVersion = readString(value, "contract_version", "contract_version", label);
|
|
832
|
-
if (contractVersion !== undefined && !CONTRACT_VERSION_PATTERN.test(contractVersion)) {
|
|
833
|
-
throw malformed(
|
|
834
|
-
label,
|
|
835
|
-
"contract_version",
|
|
836
|
-
`${JSON.stringify(contractVersion)} is not a semantic version`,
|
|
837
|
-
);
|
|
296
|
+
if (inputs.repository !== undefined) {
|
|
297
|
+
sources.push(documentSource("repository", inputs.repository));
|
|
838
298
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
contractVersion,
|
|
842
|
-
targetKind: readTargetKind(target, label),
|
|
843
|
-
languages: readStrings(analysis, "languages", "analysis.languages", label, 0, LANGUAGES),
|
|
844
|
-
minConfidence: readEnum(
|
|
845
|
-
analysis,
|
|
846
|
-
"min_confidence",
|
|
847
|
-
"analysis.min_confidence",
|
|
848
|
-
label,
|
|
849
|
-
CONFIDENCES,
|
|
850
|
-
),
|
|
851
|
-
generatedFiles: readEnum(
|
|
852
|
-
analysis,
|
|
853
|
-
"generated_files",
|
|
854
|
-
"analysis.generated_files",
|
|
855
|
-
label,
|
|
856
|
-
GENERATED_FILES,
|
|
857
|
-
),
|
|
858
|
-
consumerTests: readEnum(
|
|
859
|
-
analysis,
|
|
860
|
-
"consumer_tests",
|
|
861
|
-
"analysis.consumer_tests",
|
|
862
|
-
label,
|
|
863
|
-
CONSUMER_TESTS,
|
|
864
|
-
),
|
|
865
|
-
configurations: readConfigurations(analysis, label),
|
|
866
|
-
matrixComplete: readBoolean(matrix, "complete", "analysis.matrix.complete", label),
|
|
867
|
-
templateDirs: readStrings(analysis, "template_dirs", "analysis.template_dirs", label, 0),
|
|
868
|
-
templateDelimiters: readDelimiters(analysis, label),
|
|
869
|
-
consumersComplete: readBoolean(consumers, "complete", "consumers.complete", label),
|
|
870
|
-
rootPatterns: readStrings(roots, "patterns", "roots.patterns", label, 0),
|
|
871
|
-
severity: readSeverity(value, label),
|
|
872
|
-
exemptionsDisabled: readExemptions(value, label),
|
|
873
|
-
formats: readStrings(reporters, "formats", "reporters.formats", label, 1, FORMATS),
|
|
874
|
-
sort: readEnum(reporters, "sort", "reporters.sort", label, SORTS),
|
|
875
|
-
cascade: readEnum(reporters, "cascade", "reporters.cascade", label, CASCADES),
|
|
876
|
-
maxFindings: readCount(reporters, "max_findings", "reporters.max_findings", label),
|
|
877
|
-
failOn: readEnum(reporters, "fail_on", "reporters.fail_on", label, SEVERITIES),
|
|
878
|
-
providers: readProviders(providers, label),
|
|
879
|
-
testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
|
|
880
|
-
entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
|
|
881
|
-
componentExtensions: readComponentExtensions(ts, label),
|
|
882
|
-
disabledConventions: readDisabledConventions(ts, label),
|
|
883
|
-
injectionRegistrations: readDeclarations(
|
|
884
|
-
ts,
|
|
885
|
-
"injection_registrations",
|
|
886
|
-
"ts.injection_registrations",
|
|
887
|
-
label,
|
|
888
|
-
),
|
|
889
|
-
lifecycleContracts: readLifecycleContracts(ts, label),
|
|
890
|
-
serializers: readDeclarations(ts, "serializers", "ts.serializers", label),
|
|
891
|
-
rootsComplete: readBoolean(ts, "roots_complete", "ts.roots_complete", label),
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
/** Parses one document's text and walks it against the closed key list. */
|
|
896
|
-
function parseDocument(text: string, node: KeyNode, label: string): unknown {
|
|
897
|
-
let value: unknown;
|
|
898
|
-
try {
|
|
899
|
-
value = JSON.parse(text);
|
|
900
|
-
} catch (error: unknown) {
|
|
901
|
-
throw malformed(label, "", message(error));
|
|
299
|
+
if (inputs.central !== undefined) {
|
|
300
|
+
sources.push(documentSource("central", inputs.central));
|
|
902
301
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
/** Names a document in a refusal: the path the invocation passed, or what it is. */
|
|
908
|
-
function labelOr(label: string | undefined, named: string): string {
|
|
909
|
-
return label === undefined || label === "" ? named : label;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* Decodes the documents one invocation supplies, in the order they outrank each
|
|
914
|
-
* other, skipping every source it does not carry.
|
|
915
|
-
*/
|
|
916
|
-
function readSources(inputs: Inputs): DocSource[] {
|
|
917
|
-
const sources: DocSource[] = [];
|
|
918
|
-
const flagLabels = inputs.flagLabels ?? new Map<string, string>();
|
|
919
|
-
if (inputs.flags !== undefined) {
|
|
920
|
-
const flat = parseDocument(inputs.flags, { kind: "map" }, FLAGS_LABEL);
|
|
921
|
-
if (!isRecord(flat)) {
|
|
922
|
-
throw malformed(FLAGS_LABEL, "", "is not one JSON object");
|
|
923
|
-
}
|
|
924
|
-
sources.push({
|
|
925
|
-
doc: readDoc(nestFlags(flat, FLAGS_LABEL), FLAGS_LABEL),
|
|
926
|
-
kind: "flag",
|
|
927
|
-
label: "",
|
|
928
|
-
flagLabels,
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
const held: readonly { kind: Source; named: string; text?: string; label?: string }[] = [
|
|
932
|
-
{
|
|
933
|
-
kind: "repository",
|
|
934
|
-
named: "the repository configuration",
|
|
935
|
-
...(inputs.repository === undefined ? {} : { text: inputs.repository }),
|
|
936
|
-
...(inputs.repositoryLabel === undefined ? {} : { label: inputs.repositoryLabel }),
|
|
937
|
-
},
|
|
938
|
-
{
|
|
939
|
-
kind: "central",
|
|
940
|
-
named: "the central configuration",
|
|
941
|
-
...(inputs.central === undefined ? {} : { text: inputs.central }),
|
|
942
|
-
...(inputs.centralLabel === undefined ? {} : { label: inputs.centralLabel }),
|
|
943
|
-
},
|
|
944
|
-
];
|
|
945
|
-
for (const entry of held) {
|
|
946
|
-
if (entry.text === undefined) {
|
|
302
|
+
const tree: Record<string, unknown> = {};
|
|
303
|
+
const provenance = new Map<string, Origin>();
|
|
304
|
+
for (const row of SETTINGS) {
|
|
305
|
+
if (!row.read || row.role === "ignored") {
|
|
947
306
|
continue;
|
|
948
307
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
*/
|
|
987
|
-
function settleSeverity(
|
|
988
|
-
provenance: Map<string, Origin>,
|
|
989
|
-
sources: readonly DocSource[],
|
|
990
|
-
): ReadonlyMap<string, Severity> {
|
|
991
|
-
const codes = new Set<string>();
|
|
992
|
-
for (const source of sources) {
|
|
993
|
-
for (const code of source.doc.severity?.keys() ?? []) {
|
|
994
|
-
codes.add(code);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
const severity = new Map<string, Severity>();
|
|
998
|
-
for (const code of [...codes].sort(bytewise)) {
|
|
999
|
-
const path = `${SEVERITY_SECTION}.${code}`;
|
|
1000
|
-
for (const source of sources) {
|
|
1001
|
-
const value = source.doc.severity?.get(code);
|
|
1002
|
-
if (value === undefined) {
|
|
1003
|
-
continue;
|
|
308
|
+
if (row.role === "section") {
|
|
309
|
+
setAt(tree, row.key, {});
|
|
310
|
+
} else if (row.role === "keyed") {
|
|
311
|
+
const members: Record<string, unknown> = isRecord(row.default) ? { ...row.default } : {};
|
|
312
|
+
provenance.set(row.key, { source: "default", label: "" });
|
|
313
|
+
for (const name of Object.keys(members)) {
|
|
314
|
+
provenance.set(`${row.key}.${name}`, { source: "default", label: "" });
|
|
315
|
+
}
|
|
316
|
+
for (const held of sources.toReversed()) {
|
|
317
|
+
const named = held.supplied.keyed.get(row.key);
|
|
318
|
+
if (named === undefined) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
provenance.set(row.key, originOf(held, row.key));
|
|
322
|
+
for (const [name, value] of Object.entries(named)) {
|
|
323
|
+
members[name] = value;
|
|
324
|
+
provenance.set(`${row.key}.${name}`, originOf(held, row.key));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (Object.keys(members).length > 0) {
|
|
328
|
+
provenance.delete(row.key);
|
|
329
|
+
}
|
|
330
|
+
setAt(tree, row.key, canonical(members));
|
|
331
|
+
} else {
|
|
332
|
+
const held = sources.find((one) => one.supplied.values.has(row.key));
|
|
333
|
+
if (held !== undefined) {
|
|
334
|
+
setAt(tree, row.key, held.supplied.values.get(row.key));
|
|
335
|
+
provenance.set(row.key, originOf(held, row.key));
|
|
336
|
+
} else if (row.default !== undefined) {
|
|
337
|
+
setAt(tree, row.key, canonical(row.default));
|
|
338
|
+
provenance.set(row.key, { source: "default", label: "" });
|
|
339
|
+
} else if (enforce && row.required === true) {
|
|
340
|
+
throw new UsageError(
|
|
341
|
+
`${row.key} is not set, it has no default and is never inferred: searched ` +
|
|
342
|
+
`${searched("the repository configuration", inputs.repository)} and ` +
|
|
343
|
+
searched("the central configuration", inputs.central),
|
|
344
|
+
);
|
|
1004
345
|
}
|
|
1005
|
-
severity.set(code, value);
|
|
1006
|
-
provenance.set(path, originOf(source, path));
|
|
1007
|
-
break;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
if (codes.size > 0) {
|
|
1011
|
-
return severity;
|
|
1012
|
-
}
|
|
1013
|
-
provenance.set(SEVERITY_SECTION, { source: "default", label: "" });
|
|
1014
|
-
for (const source of sources) {
|
|
1015
|
-
if (source.doc.severity !== undefined) {
|
|
1016
|
-
provenance.set(SEVERITY_SECTION, originOf(source, SEVERITY_SECTION));
|
|
1017
|
-
break;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
return severity;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/** Names one configuration source and whether the invocation carried it. */
|
|
1024
|
-
function describeSource(
|
|
1025
|
-
named: string,
|
|
1026
|
-
text: string | undefined,
|
|
1027
|
-
label: string | undefined,
|
|
1028
|
-
): string {
|
|
1029
|
-
if (text === undefined) {
|
|
1030
|
-
return `${named} (not present)`;
|
|
1031
|
-
}
|
|
1032
|
-
return `${named} (${labelOr(label, "unnamed")})`;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
/**
|
|
1036
|
-
* Refuses a provenance entry whose source names no file or flag, which would
|
|
1037
|
-
* print a provenance value the closed key list refuses to read back.
|
|
1038
|
-
*/
|
|
1039
|
-
function checkLabels(provenance: ReadonlyMap<string, Origin>): void {
|
|
1040
|
-
for (const path of [...provenance.keys()].sort(bytewise)) {
|
|
1041
|
-
const origin = provenance.get(path);
|
|
1042
|
-
if (origin !== undefined && origin.source !== "default" && origin.label === "") {
|
|
1043
|
-
throw malformed(
|
|
1044
|
-
origin.source === "flag" ? FLAGS_LABEL : origin.source,
|
|
1045
|
-
path,
|
|
1046
|
-
`came from ${origin.source}, which names no file or flag`,
|
|
1047
|
-
);
|
|
1048
346
|
}
|
|
1049
347
|
}
|
|
348
|
+
// gen-config.ts marks a member optional exactly where this loop may leave it unset.
|
|
349
|
+
return { config: tree as unknown as PartialConfig, provenance };
|
|
1050
350
|
}
|
|
1051
351
|
|
|
1052
352
|
/**
|
|
1053
|
-
* Applies
|
|
353
|
+
* Applies the command-line flags over the repository configuration over the central
|
|
1054
354
|
* configuration over the default each key declares, and returns the resolved
|
|
1055
|
-
* configuration with the origin of every setting
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
* list does not declare, a document that names one key twice, and a resolved
|
|
1059
|
-
* configuration no source supplied a target kind for. Every one of them maps to
|
|
1060
|
-
* the usage exit code.
|
|
1061
|
-
*/
|
|
1062
|
-
export function resolve(inputs: Inputs): { config: Config; provenance: Provenance } {
|
|
1063
|
-
const { config, provenance } = settleAll(inputs);
|
|
1064
|
-
if (config.targetKind === "") {
|
|
1065
|
-
throw new ConfigError(
|
|
1066
|
-
"missing-target-kind",
|
|
1067
|
-
"target.kind",
|
|
1068
|
-
"target.kind is not set, it has no default and is never inferred: searched " +
|
|
1069
|
-
`${describeSource("the repository configuration", inputs.repository, inputs.repositoryLabel)} and ` +
|
|
1070
|
-
describeSource("the central configuration", inputs.central, inputs.centralLabel),
|
|
1071
|
-
);
|
|
1072
|
-
}
|
|
1073
|
-
checkLabels(provenance);
|
|
1074
|
-
return { config, provenance };
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
/**
|
|
1078
|
-
* The configuration the documents resolve to, every document read and refused as
|
|
1079
|
-
* {@link resolve} refuses it, with no target kind required of them: which projects a
|
|
1080
|
-
* run analyzes does not depend on the kind, so a question about the projects asks
|
|
1081
|
-
* none. The kind is empty where no document names one.
|
|
355
|
+
* configuration with the origin of every setting: its keys in the closed key list's
|
|
356
|
+
* order, every object's members inside a value ascending. Every refusal is a
|
|
357
|
+
* {@link UsageError}.
|
|
1082
358
|
*/
|
|
1083
|
-
export function
|
|
1084
|
-
|
|
359
|
+
export function resolve(inputs: Inputs): { config: ResolvedConfig; provenance: Provenance } {
|
|
360
|
+
const { config, provenance } = settle(inputs, true);
|
|
361
|
+
return { config: config as ResolvedConfig, provenance };
|
|
1085
362
|
}
|
|
1086
363
|
|
|
1087
364
|
/**
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
365
|
+
* The configuration the sources resolve to, refused as {@link resolve} refuses it but
|
|
366
|
+
* with no target kind required: which projects a run analyzes asks none.
|
|
1090
367
|
*/
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
const defaults = defaultConfig();
|
|
1094
|
-
const provenance = new Map<string, Origin>();
|
|
1095
|
-
const at = <T>(path: string, fallback: T, read: (doc: Doc) => T | undefined): T =>
|
|
1096
|
-
settle(fallback, path, provenance, sources, read);
|
|
1097
|
-
|
|
1098
|
-
const config: Config = {
|
|
1099
|
-
contractVersion: at("contract_version", defaults.contractVersion, (doc) => doc.contractVersion),
|
|
1100
|
-
targetKind: at<TargetKindSetting | "">("target.kind", "", (doc) => doc.targetKind),
|
|
1101
|
-
analysis: {
|
|
1102
|
-
languages: at("analysis.languages", defaults.analysis.languages, (doc) => doc.languages),
|
|
1103
|
-
minConfidence: at(
|
|
1104
|
-
"analysis.min_confidence",
|
|
1105
|
-
defaults.analysis.minConfidence,
|
|
1106
|
-
(doc) => doc.minConfidence,
|
|
1107
|
-
),
|
|
1108
|
-
generatedFiles: at(
|
|
1109
|
-
"analysis.generated_files",
|
|
1110
|
-
defaults.analysis.generatedFiles,
|
|
1111
|
-
(doc) => doc.generatedFiles,
|
|
1112
|
-
),
|
|
1113
|
-
consumerTests: at(
|
|
1114
|
-
"analysis.consumer_tests",
|
|
1115
|
-
defaults.analysis.consumerTests,
|
|
1116
|
-
(doc) => doc.consumerTests,
|
|
1117
|
-
),
|
|
1118
|
-
configurations: at(
|
|
1119
|
-
"analysis.configurations",
|
|
1120
|
-
defaults.analysis.configurations,
|
|
1121
|
-
(doc) => doc.configurations,
|
|
1122
|
-
),
|
|
1123
|
-
matrixComplete: at(
|
|
1124
|
-
"analysis.matrix.complete",
|
|
1125
|
-
defaults.analysis.matrixComplete,
|
|
1126
|
-
(doc) => doc.matrixComplete,
|
|
1127
|
-
),
|
|
1128
|
-
templateDirs: at(
|
|
1129
|
-
"analysis.template_dirs",
|
|
1130
|
-
defaults.analysis.templateDirs,
|
|
1131
|
-
(doc) => doc.templateDirs,
|
|
1132
|
-
),
|
|
1133
|
-
templateDelimiters: at(
|
|
1134
|
-
"analysis.template_delimiters",
|
|
1135
|
-
defaults.analysis.templateDelimiters,
|
|
1136
|
-
(doc) => doc.templateDelimiters,
|
|
1137
|
-
),
|
|
1138
|
-
},
|
|
1139
|
-
consumersComplete: at(
|
|
1140
|
-
"consumers.complete",
|
|
1141
|
-
defaults.consumersComplete,
|
|
1142
|
-
(doc) => doc.consumersComplete,
|
|
1143
|
-
),
|
|
1144
|
-
rootPatterns: at("roots.patterns", defaults.rootPatterns, (doc) => doc.rootPatterns),
|
|
1145
|
-
severity: settleSeverity(provenance, sources),
|
|
1146
|
-
exemptionsDisabled: at(
|
|
1147
|
-
"exemptions.disabled",
|
|
1148
|
-
defaults.exemptionsDisabled,
|
|
1149
|
-
(doc) => doc.exemptionsDisabled,
|
|
1150
|
-
),
|
|
1151
|
-
reporters: {
|
|
1152
|
-
formats: at("reporters.formats", defaults.reporters.formats, (doc) => doc.formats),
|
|
1153
|
-
sort: at("reporters.sort", defaults.reporters.sort, (doc) => doc.sort),
|
|
1154
|
-
cascade: at("reporters.cascade", defaults.reporters.cascade, (doc) => doc.cascade),
|
|
1155
|
-
maxFindings: at(
|
|
1156
|
-
"reporters.max_findings",
|
|
1157
|
-
defaults.reporters.maxFindings,
|
|
1158
|
-
(doc) => doc.maxFindings,
|
|
1159
|
-
),
|
|
1160
|
-
failOn: at("reporters.fail_on", defaults.reporters.failOn, (doc) => doc.failOn),
|
|
1161
|
-
},
|
|
1162
|
-
providers: {
|
|
1163
|
-
analyzers: at("providers.analyzers", defaults.providers.analyzers, (doc) => doc.providers),
|
|
1164
|
-
},
|
|
1165
|
-
ts: {
|
|
1166
|
-
testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
|
|
1167
|
-
entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
|
|
1168
|
-
componentExtensions: at(
|
|
1169
|
-
"ts.component_extensions",
|
|
1170
|
-
defaults.ts.componentExtensions,
|
|
1171
|
-
(doc) => doc.componentExtensions,
|
|
1172
|
-
),
|
|
1173
|
-
disabledConventions: at(
|
|
1174
|
-
"ts.disabled_conventions",
|
|
1175
|
-
defaults.ts.disabledConventions,
|
|
1176
|
-
(doc) => doc.disabledConventions,
|
|
1177
|
-
),
|
|
1178
|
-
injectionRegistrations: at(
|
|
1179
|
-
"ts.injection_registrations",
|
|
1180
|
-
defaults.ts.injectionRegistrations,
|
|
1181
|
-
(doc) => doc.injectionRegistrations,
|
|
1182
|
-
),
|
|
1183
|
-
lifecycleContracts: at(
|
|
1184
|
-
"ts.lifecycle_contracts",
|
|
1185
|
-
defaults.ts.lifecycleContracts,
|
|
1186
|
-
(doc) => doc.lifecycleContracts,
|
|
1187
|
-
),
|
|
1188
|
-
serializers: at("ts.serializers", defaults.ts.serializers, (doc) => doc.serializers),
|
|
1189
|
-
rootsComplete: at("ts.roots_complete", defaults.ts.rootsComplete, (doc) => doc.rootsComplete),
|
|
1190
|
-
},
|
|
1191
|
-
};
|
|
1192
|
-
|
|
1193
|
-
return { config, provenance };
|
|
368
|
+
export function resolvePartial(inputs: Inputs): PartialConfig {
|
|
369
|
+
return settle(inputs, false).config;
|
|
1194
370
|
}
|