@cplieger/deadset-ts 3.4.0 → 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/components.js +61 -106
- package/dist/src/config.js +6 -0
- package/dist/src/kinds.js +40 -0
- package/dist/src/print.js +10 -1
- package/dist/src/resolve.js +90 -9
- package/dist/src/run.js +9 -3
- package/dist/src/schema.js +37 -1
- package/dist/src/scope.js +55 -21
- package/dist/src/serialization-contract.js +109 -19
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/components.ts +67 -130
- package/src/config.ts +37 -2
- package/src/kinds.ts +41 -0
- package/src/print.ts +17 -1
- package/src/resolve.ts +123 -9
- package/src/run.ts +10 -3
- package/src/schema.ts +47 -2
- package/src/scope.ts +63 -22
- package/src/serialization-contract.ts +138 -20
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
|
|
|
8
8
|
|
|
9
9
|
deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
|
|
10
10
|
|
|
11
|
-
**Status: pre-release.** This version implements contract 3.
|
|
11
|
+
**Status: pre-release.** This version implements contract 3.2.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; no other verb reports findings yet, so `analyze`, `explain` and `describe` exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
package/dist/src/components.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The dead components of one graph
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The dead components of one graph. A component is a set of dead declarations one
|
|
3
|
+
* deletion removes together: its root members, which are where the deletion starts,
|
|
4
|
+
* and every dead declaration that falls with them, being dead only through them.
|
|
5
5
|
*/
|
|
6
6
|
import { OUTSIDE } from "./graph.js";
|
|
7
7
|
/** The name every component identifier this analyzer mints starts with. */
|
|
@@ -10,21 +10,21 @@ const ANALYZER = "deadset-ts";
|
|
|
10
10
|
const ID_DIGITS = 4;
|
|
11
11
|
/** The index of a declaration the component walk has not reached. */
|
|
12
12
|
const UNVISITED = -1;
|
|
13
|
-
/** What a report carries for one component: its roots and
|
|
13
|
+
/** What a report carries for one component: its roots and counts, and its members in full. */
|
|
14
14
|
export function listing(component, cascade) {
|
|
15
15
|
return {
|
|
16
16
|
roots: component.roots,
|
|
17
17
|
members: cascade === "full" ? component.members : [],
|
|
18
|
-
symbolCount: component.
|
|
18
|
+
symbolCount: component.members.length,
|
|
19
19
|
deletableLines: component.deletableLines,
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Groups the declarations `dead` marks into components
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* Groups the declarations `dead` marks into components, ordered by their first member's
|
|
24
|
+
* site; `dead` and `testOfDeadCode` hold one flag per declaration of the graph. Root
|
|
25
|
+
* members sit on the cycles no other cycle references. A cycle two roots both reach is
|
|
26
|
+
* dead only through both, so a component is a weakly connected part of the dead
|
|
27
|
+
* subgraph: no dead declaration of one references a declaration of another.
|
|
28
28
|
*/
|
|
29
29
|
export function componentsOf(graph, dead, testOfDeadCode) {
|
|
30
30
|
const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode);
|
|
@@ -32,25 +32,21 @@ export function componentsOf(graph, dead, testOfDeadCode) {
|
|
|
32
32
|
return [];
|
|
33
33
|
}
|
|
34
34
|
const { componentOf, members } = connected(adjacent);
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const outsideReference = externallyReferenced(adjacent, componentOf);
|
|
35
|
+
const referenced = referencedCycles(adjacent, componentOf, members.length);
|
|
36
|
+
const groups = joined(adjacent);
|
|
38
37
|
const idOf = (position) => graph.symbols[at[position] ?? 0]?.id ?? "";
|
|
39
|
-
return
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
// deletion starts and the member falls with it.
|
|
38
|
+
return groups.map((group, place) => {
|
|
39
|
+
const roots = group.filter((position) => {
|
|
40
|
+
// A dead member's dead container is on the member's own cycle, and the container
|
|
41
|
+
// is where the deletion starts.
|
|
44
42
|
const container = graph.parent[at[position] ?? 0] ?? OUTSIDE;
|
|
45
|
-
return
|
|
43
|
+
return referenced[componentOf[position] ?? 0] !== true && dead[container] !== true;
|
|
46
44
|
});
|
|
47
|
-
const fallen = falls[group] ?? [];
|
|
48
45
|
return {
|
|
49
46
|
id: `${ANALYZER}/c-${String(place + 1).padStart(ID_DIGITS, "0")}`,
|
|
50
|
-
members:
|
|
47
|
+
members: group.map(idOf),
|
|
51
48
|
roots: roots.map(idOf),
|
|
52
|
-
|
|
53
|
-
deletableLines: linesSpanned(fallen.map((position) => graph.symbols[at[position] ?? 0])),
|
|
49
|
+
deletableLines: linesSpanned(group.map((position) => graph.symbols[at[position] ?? 0])),
|
|
54
50
|
};
|
|
55
51
|
});
|
|
56
52
|
}
|
|
@@ -127,10 +123,9 @@ function deadSubgraph(graph, dead, testOfDeadCode) {
|
|
|
127
123
|
return { at, adjacent };
|
|
128
124
|
}
|
|
129
125
|
/**
|
|
130
|
-
* Each declaration's
|
|
126
|
+
* Each declaration's cycle and each cycle's members by site, by Tarjan's
|
|
131
127
|
* algorithm in one pass. The walk keeps its own stack of frames, so a chain of any length
|
|
132
|
-
* is walked without growing the call stack.
|
|
133
|
-
* each follows every component it reaches.
|
|
128
|
+
* is walked without growing the call stack.
|
|
134
129
|
*/
|
|
135
130
|
function connected(adjacent) {
|
|
136
131
|
const index = adjacent.map(() => UNVISITED);
|
|
@@ -193,97 +188,57 @@ function connected(adjacent) {
|
|
|
193
188
|
});
|
|
194
189
|
return { componentOf, members };
|
|
195
190
|
}
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
* the result is acyclic.
|
|
200
|
-
*/
|
|
201
|
-
function condense(adjacent, componentOf, count) {
|
|
202
|
-
const edges = Array.from({ length: count }, () => []);
|
|
203
|
-
const into = Array.from({ length: count }, () => 0);
|
|
204
|
-
const seen = new Set();
|
|
191
|
+
/** Per cycle, whether a dead declaration outside it references one of its members. */
|
|
192
|
+
function referencedCycles(adjacent, componentOf, count) {
|
|
193
|
+
const referenced = Array.from({ length: count }, () => false);
|
|
205
194
|
adjacent.forEach((targets, from) => {
|
|
206
195
|
for (const to of targets) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (a === b || seen.has(pair)) {
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
seen.add(pair);
|
|
214
|
-
edges[a]?.push(b);
|
|
215
|
-
into[b] = (into[b] ?? 0) + 1;
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
return { edges, into };
|
|
219
|
-
}
|
|
220
|
-
/** Per dead declaration, whether a dead component other than its own references it. */
|
|
221
|
-
function externallyReferenced(adjacent, componentOf) {
|
|
222
|
-
const referenced = adjacent.map(() => false);
|
|
223
|
-
adjacent.forEach((targets, from) => {
|
|
224
|
-
for (const to of targets) {
|
|
225
|
-
if (componentOf[from] !== componentOf[to]) {
|
|
226
|
-
referenced[to] = true;
|
|
196
|
+
const cycle = componentOf[to] ?? 0;
|
|
197
|
+
if (componentOf[from] !== cycle) {
|
|
198
|
+
referenced[cycle] = true;
|
|
227
199
|
}
|
|
228
200
|
}
|
|
229
201
|
});
|
|
230
202
|
return referenced;
|
|
231
203
|
}
|
|
232
204
|
/**
|
|
233
|
-
* The
|
|
234
|
-
*
|
|
235
|
-
*
|
|
205
|
+
* The weakly connected parts of the dead subgraph, each its positions in ascending
|
|
206
|
+
* order, ordered by their first position. Positions follow the graph's declaration
|
|
207
|
+
* order, so a part's first position is its first member's site.
|
|
236
208
|
*/
|
|
237
|
-
function
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const owners = edges.map(() => 0);
|
|
259
|
-
edges.forEach((_targets, root) => {
|
|
260
|
-
if (into[root] !== 0) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const reached = [root];
|
|
264
|
-
stamp[root] = root;
|
|
265
|
-
// An array's iterator reads its length at every step, so what a step pushes is
|
|
266
|
-
// walked in the same loop.
|
|
267
|
-
for (const at of reached) {
|
|
268
|
-
for (const to of edges[at] ?? []) {
|
|
269
|
-
if (stamp[to] !== root) {
|
|
270
|
-
stamp[to] = root;
|
|
271
|
-
reached.push(to);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
reach.set(root, reached);
|
|
276
|
-
for (const component of reached) {
|
|
277
|
-
owners[component] = (owners[component] ?? 0) + 1;
|
|
209
|
+
function joined(adjacent) {
|
|
210
|
+
const leader = adjacent.map((_edges, position) => position);
|
|
211
|
+
const find = (position) => {
|
|
212
|
+
let top = position;
|
|
213
|
+
while ((leader[top] ?? top) !== top) {
|
|
214
|
+
top = leader[top] ?? top;
|
|
215
|
+
}
|
|
216
|
+
// Every position on the walk points at the top afterwards, so a later find is short.
|
|
217
|
+
for (let step = position; step !== top;) {
|
|
218
|
+
const next = leader[step] ?? top;
|
|
219
|
+
leader[step] = top;
|
|
220
|
+
step = next;
|
|
221
|
+
}
|
|
222
|
+
return top;
|
|
223
|
+
};
|
|
224
|
+
adjacent.forEach((targets, from) => {
|
|
225
|
+
for (const to of targets) {
|
|
226
|
+
const a = find(from);
|
|
227
|
+
const b = find(to);
|
|
228
|
+
// The smaller position leads, so a part's leader is its first position.
|
|
229
|
+
leader[Math.max(a, b)] = Math.min(a, b);
|
|
278
230
|
}
|
|
279
231
|
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
232
|
+
const parts = new Map();
|
|
233
|
+
adjacent.forEach((_edges, position) => {
|
|
234
|
+
const top = find(position);
|
|
235
|
+
const part = parts.get(top);
|
|
236
|
+
if (part === undefined) {
|
|
237
|
+
parts.set(top, [position]);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
part.push(position);
|
|
286
241
|
}
|
|
287
|
-
return set.sort((a, b) => a - b);
|
|
288
242
|
});
|
|
243
|
+
return [...parts.values()];
|
|
289
244
|
}
|
package/dist/src/config.js
CHANGED
|
@@ -34,6 +34,12 @@ export function defaultConfig() {
|
|
|
34
34
|
maxFindings: 0,
|
|
35
35
|
failOn: "deny",
|
|
36
36
|
},
|
|
37
|
+
providers: {
|
|
38
|
+
analyzers: [
|
|
39
|
+
{ shape: "installed", name: "deadset-go", languages: ["go"], command: "deadset-go" },
|
|
40
|
+
{ shape: "installed", name: "deadset-ts", languages: ["ts"], command: "deadset-ts" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
37
43
|
ts: {
|
|
38
44
|
testFiles: ["**/*.test.{ts,tsx,mts,cts}"],
|
|
39
45
|
entryFiles: [],
|
package/dist/src/kinds.js
CHANGED
|
@@ -10,3 +10,43 @@
|
|
|
10
10
|
* code the Contract fixes arrives here as a failing test.
|
|
11
11
|
*/
|
|
12
12
|
export const FIXED_SEVERITY_CODES = ["DS1703", "DS1704"];
|
|
13
|
+
/**
|
|
14
|
+
* The code of every live issue kind, in ascending order. A severity key names one of
|
|
15
|
+
* them or a family prefix whose range holds one; a key naming a retired code, an
|
|
16
|
+
* unassigned code or a range holding no live kind is an unimplemented key, so every
|
|
17
|
+
* severity a configuration sets is one some analyzer reports under. A test compares
|
|
18
|
+
* this list against the Contract release the analyzer is written against.
|
|
19
|
+
*/
|
|
20
|
+
export const LIVE_CODES = [
|
|
21
|
+
"DS1001",
|
|
22
|
+
"DS1002",
|
|
23
|
+
"DS1003",
|
|
24
|
+
"DS1004",
|
|
25
|
+
"DS1005",
|
|
26
|
+
"DS1006",
|
|
27
|
+
"DS1101",
|
|
28
|
+
"DS1102",
|
|
29
|
+
"DS1103",
|
|
30
|
+
"DS1104",
|
|
31
|
+
"DS1201",
|
|
32
|
+
"DS1203",
|
|
33
|
+
"DS1204",
|
|
34
|
+
"DS1301",
|
|
35
|
+
"DS1302",
|
|
36
|
+
"DS1303",
|
|
37
|
+
"DS1501",
|
|
38
|
+
"DS1502",
|
|
39
|
+
"DS1601",
|
|
40
|
+
"DS1605",
|
|
41
|
+
"DS1701",
|
|
42
|
+
"DS1702",
|
|
43
|
+
"DS1703",
|
|
44
|
+
"DS1704",
|
|
45
|
+
"DS1705",
|
|
46
|
+
"DS1801",
|
|
47
|
+
"DS1802",
|
|
48
|
+
"DS1803",
|
|
49
|
+
"DS1805",
|
|
50
|
+
"DS1807",
|
|
51
|
+
"DS1809",
|
|
52
|
+
];
|
package/dist/src/print.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderOrigin } from "./config.js";
|
|
1
|
+
import { renderOrigin, } from "./config.js";
|
|
2
2
|
/** One declaration entry as a document writes it: the members of its shape alone. */
|
|
3
3
|
function declarationOf(entry) {
|
|
4
4
|
switch (entry.shape) {
|
|
@@ -10,6 +10,14 @@ function declarationOf(entry) {
|
|
|
10
10
|
return { global: entry.global };
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
/** One provider entry as a document writes it: the members of its shape alone. */
|
|
14
|
+
function providerOf(entry) {
|
|
15
|
+
const installed = { name: entry.name, languages: entry.languages, command: entry.command };
|
|
16
|
+
if (entry.shape === "installed") {
|
|
17
|
+
return installed;
|
|
18
|
+
}
|
|
19
|
+
return { ...installed, source: entry.source, version: entry.version, digest: entry.digest };
|
|
20
|
+
}
|
|
13
21
|
/**
|
|
14
22
|
* The resolved configuration and its provenance as one JSON object, indented,
|
|
15
23
|
* ending in a newline.
|
|
@@ -51,6 +59,7 @@ export function printConfig(config, provenance) {
|
|
|
51
59
|
max_findings: config.reporters.maxFindings,
|
|
52
60
|
fail_on: config.reporters.failOn,
|
|
53
61
|
},
|
|
62
|
+
providers: { analyzers: config.providers.analyzers.map(providerOf) },
|
|
54
63
|
go: {},
|
|
55
64
|
ts: {
|
|
56
65
|
test_files: config.ts.testFiles,
|
package/dist/src/resolve.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigError, defaultConfig, } from "./config.js";
|
|
2
|
-
import { FIXED_SEVERITY_CODES } from "./kinds.js";
|
|
2
|
+
import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.js";
|
|
3
3
|
import { checkDocument, malformed, nestFlags, SCHEMA_ROOT, SEVERITY_SECTION, } from "./schema.js";
|
|
4
4
|
/** Names the command-line flags in a refusal, where a document is named by its path. */
|
|
5
5
|
const FLAGS_LABEL = "the command-line flags";
|
|
@@ -8,6 +8,12 @@ const FAMILY_KEY_LENGTH = 4;
|
|
|
8
8
|
const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
|
|
9
9
|
const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
|
|
10
10
|
const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
|
|
11
|
+
const ANALYZER_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/u;
|
|
12
|
+
const PROVIDER_SOURCE = /^(go|npm):[^ \t\r\n]+$/u;
|
|
13
|
+
const PROVIDER_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/u;
|
|
14
|
+
const PROVIDER_DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
15
|
+
/** The members only an acquirable provider entry carries. */
|
|
16
|
+
const ACQUIRABLE_MEMBERS = ["source", "version", "digest"];
|
|
11
17
|
/** The members only a platform build configuration carries. */
|
|
12
18
|
const PLATFORM_MEMBERS = ["os", "arch", "tags"];
|
|
13
19
|
/** The member only a project build configuration carries. */
|
|
@@ -223,8 +229,8 @@ function readDelimiters(analysis, label) {
|
|
|
223
229
|
};
|
|
224
230
|
}
|
|
225
231
|
/**
|
|
226
|
-
* A severity key that names no setting
|
|
227
|
-
* the kind or kinds it names.
|
|
232
|
+
* A severity key that names no setting: it names no live kind, or the Contract
|
|
233
|
+
* fixes the severity of the kind or kinds it names.
|
|
228
234
|
*/
|
|
229
235
|
function unimplementedSeverityKey(label, path, reason) {
|
|
230
236
|
return new ConfigError("unimplemented-key", path, `${label}: key ${JSON.stringify(path)} is not implemented: ${reason}`);
|
|
@@ -245,16 +251,19 @@ function spellCodes(codes) {
|
|
|
245
251
|
}
|
|
246
252
|
return `${codes.slice(0, -1).join(", ")} and ${String(codes[codes.length - 1])}`;
|
|
247
253
|
}
|
|
254
|
+
/** Whether one severity key names a live kind: the code itself, or a family holding one. */
|
|
255
|
+
function namesLiveKind(code) {
|
|
256
|
+
return LIVE_CODES.some((candidate) => code === candidate || (code.length === FAMILY_KEY_LENGTH && candidate.startsWith(code)));
|
|
257
|
+
}
|
|
248
258
|
/**
|
|
249
259
|
* The severity object, one code at a time.
|
|
250
260
|
*
|
|
251
|
-
* A key is one issue-kind code or one two-digit family prefix
|
|
252
|
-
* kind whose severity the Contract fixes
|
|
253
|
-
* one, is an unimplemented key rather than a setting.
|
|
261
|
+
* A key is one issue-kind code or one two-digit family prefix. A key naming no
|
|
262
|
+
* live kind, and a key naming a kind whose severity the Contract fixes or a family
|
|
263
|
+
* prefix whose range holds one, is an unimplemented key rather than a setting.
|
|
254
264
|
*
|
|
255
|
-
* Whether a key names a kind this analyzer reports is not decided here
|
|
256
|
-
*
|
|
257
|
-
* and asking it would refuse the whole object.
|
|
265
|
+
* Whether a key names a kind this analyzer reports is not decided here: a live
|
|
266
|
+
* kind of another language is a setting the analyzer of that language reads.
|
|
258
267
|
*/
|
|
259
268
|
function readSeverity(document, label) {
|
|
260
269
|
const object = section(document, SEVERITY_SECTION, SEVERITY_SECTION, label);
|
|
@@ -267,6 +276,11 @@ function readSeverity(document, label) {
|
|
|
267
276
|
if (!SEVERITY_KEY.test(code)) {
|
|
268
277
|
throw unimplementedSeverityKey(label, path, "a severity key is one issue-kind code or one two-digit family prefix");
|
|
269
278
|
}
|
|
279
|
+
if (!namesLiveKind(code)) {
|
|
280
|
+
throw unimplementedSeverityKey(label, path, code.length === FAMILY_KEY_LENGTH
|
|
281
|
+
? "the family this prefix names holds no live issue kind"
|
|
282
|
+
: "the code names no live issue kind");
|
|
283
|
+
}
|
|
270
284
|
const fixed = fixedByContract(code);
|
|
271
285
|
if (fixed.length > 0) {
|
|
272
286
|
throw unimplementedSeverityKey(label, path, `the Contract fixes the severity of ${spellCodes(fixed)}, which this key names`);
|
|
@@ -384,6 +398,68 @@ function readLifecycleContracts(ts, label) {
|
|
|
384
398
|
}
|
|
385
399
|
return value.map((entry, index) => readLifecycleContract(entry, `${path}[${String(index)}]`, label));
|
|
386
400
|
}
|
|
401
|
+
/** One string member a provider entry requires, refused where it fails `pattern`. */
|
|
402
|
+
function readMatching(entry, name, at, label, pattern) {
|
|
403
|
+
const path = `${at}.${name}`;
|
|
404
|
+
const value = requireMember(readString(entry, name, path, label), path, label);
|
|
405
|
+
if (!pattern.test(value)) {
|
|
406
|
+
throw malformed(label, path, `${JSON.stringify(value)} does not match ${pattern.source}`);
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* One provider entry, read as the one shape its members name: a member only an
|
|
412
|
+
* acquirable analyzer carries makes it acquirable, and then it carries all three.
|
|
413
|
+
*/
|
|
414
|
+
function readProvider(entry, at, label) {
|
|
415
|
+
if (!isRecord(entry)) {
|
|
416
|
+
throw malformed(label, at, "is not an object");
|
|
417
|
+
}
|
|
418
|
+
const name = readMatching(entry, "name", at, label, ANALYZER_NAME);
|
|
419
|
+
const languages = readStrings(entry, "languages", `${at}.languages`, label, 1, LANGUAGES);
|
|
420
|
+
if (languages === undefined) {
|
|
421
|
+
throw malformed(label, `${at}.languages`, "is required and names nothing");
|
|
422
|
+
}
|
|
423
|
+
const commandPath = `${at}.command`;
|
|
424
|
+
const command = requireMember(readString(entry, "command", commandPath, label), commandPath, label);
|
|
425
|
+
if (!ACQUIRABLE_MEMBERS.some((member) => Object.hasOwn(entry, member))) {
|
|
426
|
+
return { shape: "installed", name, languages, command };
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
shape: "acquirable",
|
|
430
|
+
name,
|
|
431
|
+
languages,
|
|
432
|
+
command,
|
|
433
|
+
source: readMatching(entry, "source", at, label, PROVIDER_SOURCE),
|
|
434
|
+
version: readMatching(entry, "version", at, label, PROVIDER_VERSION),
|
|
435
|
+
digest: readMatching(entry, "digest", at, label, PROVIDER_DIGEST),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* The provider list. Every file a run writes for an analyzer is keyed by the
|
|
440
|
+
* entry's name, so a list naming one name twice is refused at the later entry.
|
|
441
|
+
*/
|
|
442
|
+
function readProviders(providers, label) {
|
|
443
|
+
const path = "providers.analyzers";
|
|
444
|
+
const value = member(providers, "analyzers");
|
|
445
|
+
if (value === undefined) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
if (!Array.isArray(value)) {
|
|
449
|
+
throw malformed(label, path, "is not an array");
|
|
450
|
+
}
|
|
451
|
+
const read = value.map((entry, index) => readProvider(entry, `${path}[${String(index)}]`, label));
|
|
452
|
+
const first = new Map();
|
|
453
|
+
read.forEach((entry, index) => {
|
|
454
|
+
const earlier = first.get(entry.name);
|
|
455
|
+
if (earlier !== undefined) {
|
|
456
|
+
throw malformed(label, `${path}[${String(index)}].name`, `${JSON.stringify(entry.name)} is already the name of ${path}[${String(earlier)}], ` +
|
|
457
|
+
"and every entry's name keys the files a run writes for it");
|
|
458
|
+
}
|
|
459
|
+
first.set(entry.name, index);
|
|
460
|
+
});
|
|
461
|
+
return read;
|
|
462
|
+
}
|
|
387
463
|
/**
|
|
388
464
|
* Reads one already-parsed configuration document as the settings it supplies,
|
|
389
465
|
* refusing every constraint the closed key list declares that a parse cannot
|
|
@@ -400,6 +476,7 @@ function readDoc(value, label) {
|
|
|
400
476
|
const consumers = section(value, "consumers", "consumers", label);
|
|
401
477
|
const roots = section(value, "roots", "roots", label);
|
|
402
478
|
const reporters = section(value, "reporters", "reporters", label);
|
|
479
|
+
const providers = section(value, "providers", "providers", label);
|
|
403
480
|
const ts = section(value, "ts", "ts", label);
|
|
404
481
|
section(value, "go", "go", label);
|
|
405
482
|
section(value, "provenance", "provenance", label);
|
|
@@ -427,6 +504,7 @@ function readDoc(value, label) {
|
|
|
427
504
|
cascade: readEnum(reporters, "cascade", "reporters.cascade", label, CASCADES),
|
|
428
505
|
maxFindings: readCount(reporters, "max_findings", "reporters.max_findings", label),
|
|
429
506
|
failOn: readEnum(reporters, "fail_on", "reporters.fail_on", label, SEVERITIES),
|
|
507
|
+
providers: readProviders(providers, label),
|
|
430
508
|
testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
|
|
431
509
|
entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
|
|
432
510
|
injectionRegistrations: readDeclarations(ts, "injection_registrations", "ts.injection_registrations", label),
|
|
@@ -630,6 +708,9 @@ function settleAll(inputs) {
|
|
|
630
708
|
maxFindings: at("reporters.max_findings", defaults.reporters.maxFindings, (doc) => doc.maxFindings),
|
|
631
709
|
failOn: at("reporters.fail_on", defaults.reporters.failOn, (doc) => doc.failOn),
|
|
632
710
|
},
|
|
711
|
+
providers: {
|
|
712
|
+
analyzers: at("providers.analyzers", defaults.providers.analyzers, (doc) => doc.providers),
|
|
713
|
+
},
|
|
633
714
|
ts: {
|
|
634
715
|
testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
|
|
635
716
|
entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
|
package/dist/src/run.js
CHANGED
|
@@ -47,8 +47,14 @@ export const SETTING_OPTIONS = new Map([
|
|
|
47
47
|
]);
|
|
48
48
|
/** The options that take a value and are not settings. */
|
|
49
49
|
const PLAIN_OPTIONS = new Set(["target", "config", "central", "scope"]);
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The setting options whose value is an integer rather than a string. A value
|
|
52
|
+
* written other than as digits is passed on as the string it is, so resolution
|
|
53
|
+
* refuses it by its type rather than reading `1e2` as 100.
|
|
54
|
+
*/
|
|
55
|
+
const INTEGER_OPTIONS = new Set(["max-findings"]);
|
|
56
|
+
/** An integer as an option writes it: an optional minus sign and digits alone. */
|
|
57
|
+
const INTEGER_TEXT = /^-?[0-9]+$/u;
|
|
52
58
|
/**
|
|
53
59
|
* The first option whose name carries a source-edit token, because every verb
|
|
54
60
|
* reports and never edits. The match is on the name alone, so an option whose
|
|
@@ -116,7 +122,7 @@ function inputsOf(host, options) {
|
|
|
116
122
|
if (value === undefined) {
|
|
117
123
|
continue;
|
|
118
124
|
}
|
|
119
|
-
flat[path] =
|
|
125
|
+
flat[path] = INTEGER_OPTIONS.has(name) && INTEGER_TEXT.test(value) ? Number(value) : value;
|
|
120
126
|
flagLabels.set(path, `--${name}`);
|
|
121
127
|
}
|
|
122
128
|
const flags = Object.keys(flat).length === 0 ? undefined : JSON.stringify(flat);
|
package/dist/src/schema.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConfigError } from "./config.js";
|
|
2
2
|
const leaf = { kind: "leaf" };
|
|
3
|
+
const integer = { kind: "leaf", integer: true };
|
|
3
4
|
/** A list of declarations, each entry in one of the three shapes the `ts` section states. */
|
|
4
5
|
const declarations = {
|
|
5
6
|
kind: "list",
|
|
@@ -44,10 +45,26 @@ export const SCHEMA_ROOT = {
|
|
|
44
45
|
formats: leaf,
|
|
45
46
|
sort: leaf,
|
|
46
47
|
cascade: leaf,
|
|
47
|
-
max_findings:
|
|
48
|
+
max_findings: integer,
|
|
48
49
|
fail_on: leaf,
|
|
49
50
|
},
|
|
50
51
|
},
|
|
52
|
+
providers: {
|
|
53
|
+
kind: "section",
|
|
54
|
+
members: {
|
|
55
|
+
analyzers: {
|
|
56
|
+
kind: "list",
|
|
57
|
+
members: {
|
|
58
|
+
name: leaf,
|
|
59
|
+
languages: leaf,
|
|
60
|
+
command: leaf,
|
|
61
|
+
source: leaf,
|
|
62
|
+
version: leaf,
|
|
63
|
+
digest: leaf,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
51
68
|
go: { kind: "section", members: {} },
|
|
52
69
|
ts: {
|
|
53
70
|
kind: "section",
|
|
@@ -244,6 +261,21 @@ function skipScalar(cursor) {
|
|
|
244
261
|
cursor.at += 1;
|
|
245
262
|
}
|
|
246
263
|
}
|
|
264
|
+
/** A number the document writes as an optional minus sign and digits alone. */
|
|
265
|
+
const INTEGER_TEXT = /^-?[0-9]+$/u;
|
|
266
|
+
/** The start of a JSON number: a minus sign or a digit. */
|
|
267
|
+
const NUMBER_START = /^[-0-9]/u;
|
|
268
|
+
/**
|
|
269
|
+
* Refuses a number written with a fraction or an exponent where the key list
|
|
270
|
+
* declares an integer. A parse reads `100.0` and `1e2` as the integer 100, so only
|
|
271
|
+
* the text can tell them apart. A scalar that is no number is the decoder's to
|
|
272
|
+
* refuse by its type.
|
|
273
|
+
*/
|
|
274
|
+
function checkInteger(text, at, label) {
|
|
275
|
+
if (NUMBER_START.test(text) && !INTEGER_TEXT.test(text)) {
|
|
276
|
+
throw malformed(label, at, `${text} is written with a fraction or an exponent, and an integer is digits alone`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
247
279
|
function walkValue(cursor, at, node, label) {
|
|
248
280
|
skipSpace(cursor);
|
|
249
281
|
const ch = cursor.text.charCodeAt(cursor.at);
|
|
@@ -261,7 +293,11 @@ function walkValue(cursor, at, node, label) {
|
|
|
261
293
|
readName(cursor, at, label);
|
|
262
294
|
return;
|
|
263
295
|
}
|
|
296
|
+
const start = cursor.at;
|
|
264
297
|
skipScalar(cursor);
|
|
298
|
+
if (node.integer === true) {
|
|
299
|
+
checkInteger(cursor.text.slice(start, cursor.at), at, label);
|
|
300
|
+
}
|
|
265
301
|
}
|
|
266
302
|
function walkObject(cursor, at, node, label) {
|
|
267
303
|
const seen = new Set();
|