@cplieger/deadset-ts 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/analysis.js +92 -23
- package/dist/src/edges.js +190 -0
- package/dist/src/finding.js +0 -11
- package/dist/src/findings/completion.js +92 -35
- package/dist/src/findings/dependencies-and-module-machinery.js +3 -11
- package/dist/src/findings/emitters.js +42 -4
- package/dist/src/findings/reads-and-writes.js +16 -69
- package/dist/src/findings/self-check.js +127 -2
- package/dist/src/findings/unused-declarations.js +8 -40
- package/dist/src/findings/visibility-narrowing.js +9 -40
- package/dist/src/findings-pass.js +97 -0
- package/dist/src/json-document.js +113 -0
- package/dist/src/kinds.js +47 -45
- package/dist/src/roots.js +0 -34
- package/dist/src/suppress-file.js +196 -0
- package/dist/src/suppress-inline.js +259 -0
- package/dist/src/suppress.js +148 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +176 -32
- package/src/edges.ts +241 -0
- package/src/finding.ts +76 -10
- package/src/findings/boundary.ts +0 -11
- package/src/findings/completion.ts +117 -71
- package/src/findings/dependencies-and-module-machinery.ts +4 -19
- package/src/findings/emitter.ts +6 -2
- package/src/findings/emitters.ts +81 -7
- package/src/findings/interfaces.ts +5 -17
- package/src/findings/reads-and-writes.ts +37 -151
- package/src/findings/self-check.ts +205 -2
- package/src/findings/unused-declarations.ts +11 -58
- package/src/findings/visibility-narrowing.ts +11 -58
- package/src/findings-pass.ts +145 -0
- package/src/json-document.ts +130 -0
- package/src/kinds.ts +79 -45
- package/src/roots.ts +0 -39
- package/src/suppress-file.ts +283 -0
- package/src/suppress-inline.ts +318 -0
- package/src/suppress.ts +262 -0
package/dist/src/analysis.js
CHANGED
|
@@ -11,9 +11,12 @@ import { decorator } from "./decorator.js";
|
|
|
11
11
|
import { dependenciesOf, projectNeeds } from "./dependencies.js";
|
|
12
12
|
import { deprecatedDeclarations } from "./deprecation.js";
|
|
13
13
|
import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.js";
|
|
14
|
+
import { readEdgeSides } from "./edges.js";
|
|
14
15
|
import { enumGroup } from "./enum-group.js";
|
|
15
16
|
import { computeExemptions, disabledClasses, exemptionsOf, retainedIn, } from "./exempt.js";
|
|
16
17
|
import { heldByInclusion, sourceTree } from "./file-facts.js";
|
|
18
|
+
import { findingsPass } from "./findings-pass.js";
|
|
19
|
+
import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
|
|
17
20
|
import { frameworkLifecycle } from "./framework-lifecycle.js";
|
|
18
21
|
import { implementations, mergeImplementations } from "./implementations.js";
|
|
19
22
|
import { injectionContainer } from "./injection-container.js";
|
|
@@ -25,10 +28,12 @@ import { relativePath, resolvePath } from "./paths.js";
|
|
|
25
28
|
import { byPosition } from "./position.js";
|
|
26
29
|
import { reflectiveLookup } from "./reflective-lookup.js";
|
|
27
30
|
import { references } from "./references.js";
|
|
28
|
-
import { roots, unmatchedEverywhere
|
|
31
|
+
import { roots, unmatchedEverywhere } from "./roots.js";
|
|
29
32
|
import { serializationContract } from "./serialization-contract.js";
|
|
30
33
|
import { diagnosticsOf, runSession } from "./session.js";
|
|
31
34
|
import { accessorsOf, storesOf } from "./stores.js";
|
|
35
|
+
import { readBaseline, readIgnoreFile } from "./suppress-file.js";
|
|
36
|
+
import { inlineDirectives, inlineSuppressions } from "./suppress-inline.js";
|
|
32
37
|
import { readTemplates, templateField } from "./template-field.js";
|
|
33
38
|
/** The setting whose source decides where a finding about a configured root sits. */
|
|
34
39
|
const ROOTS_SETTING = "roots.patterns";
|
|
@@ -128,7 +133,7 @@ export function runRoots(engine, host, scope, config, provenance) {
|
|
|
128
133
|
return {
|
|
129
134
|
configurations,
|
|
130
135
|
roots: ordered,
|
|
131
|
-
findings:
|
|
136
|
+
findings: unmatchedRootFindings(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
|
|
132
137
|
};
|
|
133
138
|
}
|
|
134
139
|
/** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
|
|
@@ -143,10 +148,11 @@ const DETECTORS = new Map([
|
|
|
143
148
|
["serialization-contract", serializationContract],
|
|
144
149
|
]);
|
|
145
150
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
151
|
+
* Reads every project of the run, its references and exemptions, and `extra`'s answer,
|
|
152
|
+
* while the project's view is open, and merges the projects into one matrix in the run's
|
|
153
|
+
* order.
|
|
148
154
|
*/
|
|
149
|
-
function
|
|
155
|
+
function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
150
156
|
const targetRoot = scope.target.path;
|
|
151
157
|
const disabled = disabledClasses(config);
|
|
152
158
|
const templates = disabled.has("template-field")
|
|
@@ -166,20 +172,26 @@ function sweepRun(engine, host, scope, config, input, detectors, extra) {
|
|
|
166
172
|
};
|
|
167
173
|
const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers }, detectors, {
|
|
168
174
|
disabled,
|
|
169
|
-
mode
|
|
175
|
+
mode,
|
|
170
176
|
testFiles: new Set(resolved.testFilePaths),
|
|
171
177
|
});
|
|
172
178
|
return { configured, exempt, extra: extra(project, projectRead) };
|
|
173
179
|
});
|
|
174
|
-
const matrix = matrixOf(projects.map((one) => one.configured));
|
|
175
|
-
const exempt = exemptionsOf(projects.flatMap((one) => one.exempt));
|
|
176
|
-
const swept = { ...input, exempt };
|
|
177
180
|
return {
|
|
178
|
-
|
|
179
|
-
exempt,
|
|
181
|
+
matrix: matrixOf(projects.map((one) => one.configured)),
|
|
182
|
+
exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
|
|
180
183
|
projects,
|
|
181
184
|
};
|
|
182
185
|
}
|
|
186
|
+
/** The sweep of a run read once, under one set of marks. */
|
|
187
|
+
function sweptOf(read, input) {
|
|
188
|
+
const swept = { ...input, exempt: read.exempt };
|
|
189
|
+
return {
|
|
190
|
+
matrix: read.matrix,
|
|
191
|
+
sweep: sweepMatrix(read.matrix, swept),
|
|
192
|
+
retained: retainedIn(read.matrix, swept),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
183
195
|
/**
|
|
184
196
|
* Sweeps the run the scope and the configuration describe. Each project's references
|
|
185
197
|
* and exemptions are read against its own inventory while its view is open; the
|
|
@@ -191,24 +203,29 @@ function sweepRun(engine, host, scope, config, input, detectors, extra) {
|
|
|
191
203
|
* project is swept.
|
|
192
204
|
*/
|
|
193
205
|
export function runSweep(engine, host, scope, config, input, detectors = DETECTORS) {
|
|
194
|
-
return
|
|
206
|
+
return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
|
|
195
207
|
}
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
* run's sweep, and the facts beside it each kind family reads, every per-project one
|
|
199
|
-
* read in the same session as the sweep. The run loads no consumer and reads no
|
|
200
|
-
* declared cross-language edge, so its boundary names neither.
|
|
201
|
-
*/
|
|
202
|
-
export function runEmitterInput(engine, host, scope, config, input, detectors = DETECTORS) {
|
|
208
|
+
/** Reads a run's projects with every per-project fact an emitter reads. */
|
|
209
|
+
function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
203
210
|
const targetRoot = scope.target.path;
|
|
204
|
-
|
|
211
|
+
return readRun(engine, host, scope, config, mode, detectors, (project, read) => ({
|
|
205
212
|
deprecated: deprecatedDeclarations(project, read.held),
|
|
206
213
|
accessors: accessorsOf(project, read.held),
|
|
207
214
|
needs: projectNeeds(project, read.held, host),
|
|
208
215
|
implementations: implementations(project, read.held),
|
|
209
216
|
heldByInclusion: heldByInclusion(project, targetRoot),
|
|
217
|
+
rooted: read.rooted,
|
|
218
|
+
directives: inlineDirectives(project, read.held, targetRoot),
|
|
210
219
|
}));
|
|
211
|
-
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The input every emitter reads over one sweep of a read run: the facts beside the sweep
|
|
223
|
+
* each kind family reads, every per-project one read in the same session as the sweep.
|
|
224
|
+
*/
|
|
225
|
+
function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
226
|
+
const targetRoot = scope.target.path;
|
|
227
|
+
const swept = sweptOf(read, input);
|
|
228
|
+
const { projects } = read;
|
|
212
229
|
return {
|
|
213
230
|
config,
|
|
214
231
|
swept,
|
|
@@ -216,7 +233,7 @@ export function runEmitterInput(engine, host, scope, config, input, detectors =
|
|
|
216
233
|
stores: storesOf(projects.map((one) => ({
|
|
217
234
|
references: one.configured.references,
|
|
218
235
|
accessors: one.extra.accessors,
|
|
219
|
-
})),
|
|
236
|
+
})), read.exempt, input.mode),
|
|
220
237
|
dependencies: dependenciesOf(host, targetRoot, projects.map((one) => one.extra.needs), swept.sweep.candidates.map((candidate) => candidate.id)),
|
|
221
238
|
implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
|
|
222
239
|
files: {
|
|
@@ -226,7 +243,59 @@ export function runEmitterInput(engine, host, scope, config, input, detectors =
|
|
|
226
243
|
boundary: {
|
|
227
244
|
consumers: { declared: scope.consumers.map((consumer) => consumer.id), loaded: [] },
|
|
228
245
|
encapsulated: readManifest(host, targetRoot).declaresExports,
|
|
229
|
-
edges:
|
|
246
|
+
edges: readEdgeSides(host, targetRoot),
|
|
230
247
|
},
|
|
248
|
+
selfCheck,
|
|
231
249
|
};
|
|
232
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* What every emitter reads of the run the scope and the configuration describe, swept
|
|
253
|
+
* under the caller's marks: the run's sweep, the facts beside it each kind family reads,
|
|
254
|
+
* and the sides of the target's declared cross-language edges that name a symbol of this
|
|
255
|
+
* language. The run loads no consumer, and it reads no suppression document, so the
|
|
256
|
+
* self-check family has nothing to report.
|
|
257
|
+
*/
|
|
258
|
+
export function runEmitterInput(engine, host, scope, config, input, detectors = DETECTORS) {
|
|
259
|
+
const read = readEmitterRun(engine, host, scope, config, input.mode, detectors);
|
|
260
|
+
return emitterInputOver(host, scope, config, read, input, NO_SELF_CHECK);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Reads the run the scope and the configuration describe for its findings. The inline
|
|
264
|
+
* directives, the ignore file's entries and the baseline's rows are read and bound before
|
|
265
|
+
* any sweep, and the run is swept once under every bound record's mark and once under
|
|
266
|
+
* none, so a record is decided against the finding its code would have produced. The
|
|
267
|
+
* self-check family reports the refused suppressions and the roots that named nothing.
|
|
268
|
+
*/
|
|
269
|
+
function runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors = DETECTORS) {
|
|
270
|
+
const targetRoot = scope.target.path;
|
|
271
|
+
const read = readEmitterRun(engine, host, scope, config, mode, detectors);
|
|
272
|
+
const symbols = read.matrix.union.symbols;
|
|
273
|
+
const documents = [
|
|
274
|
+
inlineSuppressions(read.projects.map((one) => one.extra.directives)),
|
|
275
|
+
readIgnoreFile(host, targetRoot, symbols),
|
|
276
|
+
readBaseline(host, targetRoot, symbols),
|
|
277
|
+
];
|
|
278
|
+
const suppressions = {
|
|
279
|
+
records: documents.flatMap((one) => one.records),
|
|
280
|
+
refusals: documents.flatMap((one) => one.refusals),
|
|
281
|
+
};
|
|
282
|
+
const facts = {
|
|
283
|
+
refusals: suppressions.refusals,
|
|
284
|
+
unmatchedRoots: unmatchedEverywhere(config.rootPatterns, read.projects.map((one) => one.extra.rooted)),
|
|
285
|
+
rootsDocument: documentOf(host, provenance, ROOTS_SETTING, targetRoot),
|
|
286
|
+
};
|
|
287
|
+
const marked = [...new Set(suppressions.records.map((record) => record.bound))].filter((id) => id !== "");
|
|
288
|
+
return {
|
|
289
|
+
marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
|
|
290
|
+
unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
|
|
291
|
+
suppressions,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* The findings of the run the scope and the configuration describe: the run read with its
|
|
296
|
+
* suppressions bound, then every family's findings with the suppressions applied and the
|
|
297
|
+
* declared edges evaluated.
|
|
298
|
+
*/
|
|
299
|
+
export function runFindings(engine, host, scope, config, provenance, mode, detectors = DETECTORS) {
|
|
300
|
+
return findingsPass(runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors));
|
|
301
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared cross-language edges: the sides of the target's edges document that name a
|
|
3
|
+
* symbol of this language, and the evaluation the report publishes for each. This analyzer
|
|
4
|
+
* never resolves the paired symbol: the other language's analyzer decides whether the
|
|
5
|
+
* pairing is a use, and the merge reads the two evaluations together.
|
|
6
|
+
*/
|
|
7
|
+
import { ConfigError } from "./config.js";
|
|
8
|
+
import { positionAt, walkDocument } from "./json-document.js";
|
|
9
|
+
import { joinPath } from "./paths.js";
|
|
10
|
+
import { positionKey } from "./position.js";
|
|
11
|
+
/** The name and the location the Contract fixes for the edges document. */
|
|
12
|
+
export const EDGES_FILE = "deadset-edges.json";
|
|
13
|
+
/** The language prefix of every reference this analyzer evaluates. */
|
|
14
|
+
const OWN_PREFIX = "ts://";
|
|
15
|
+
/** The closed key list of the document and of one edge. */
|
|
16
|
+
const DOCUMENT_KEYS = new Set(["description", "edges"]);
|
|
17
|
+
const EDGE_KEYS = new Set(["id", "because", "provides", "used_by"]);
|
|
18
|
+
/** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
|
|
19
|
+
const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
|
|
20
|
+
/** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
|
|
21
|
+
const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
|
|
22
|
+
const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
|
|
23
|
+
const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
|
|
24
|
+
const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
|
|
25
|
+
const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
|
|
26
|
+
/** An edges document the Contract refuses: a malformed input, answered with the usage code. */
|
|
27
|
+
export class EdgesError extends ConfigError {
|
|
28
|
+
constructor(at, text, want) {
|
|
29
|
+
super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
|
|
30
|
+
this.name = "EdgesError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether a reference carries a wildcard outside a quoted or a computed component, which
|
|
35
|
+
* would name a set of symbols rather than one.
|
|
36
|
+
*/
|
|
37
|
+
function carriesWildcard(ref) {
|
|
38
|
+
let quoted = false;
|
|
39
|
+
let computed = false;
|
|
40
|
+
let escaped = false;
|
|
41
|
+
for (const char of ref) {
|
|
42
|
+
if (escaped) {
|
|
43
|
+
escaped = false;
|
|
44
|
+
}
|
|
45
|
+
else if (quoted && char === "\\") {
|
|
46
|
+
escaped = true;
|
|
47
|
+
}
|
|
48
|
+
else if (quoted) {
|
|
49
|
+
quoted = char !== "'";
|
|
50
|
+
}
|
|
51
|
+
else if (computed) {
|
|
52
|
+
computed = char !== "]";
|
|
53
|
+
}
|
|
54
|
+
else if (char === "'") {
|
|
55
|
+
quoted = true;
|
|
56
|
+
}
|
|
57
|
+
else if (char === "[") {
|
|
58
|
+
computed = true;
|
|
59
|
+
}
|
|
60
|
+
else if (char === "*") {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function isObject(value) {
|
|
67
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
68
|
+
}
|
|
69
|
+
/** One edge, checked: the closed key list, the value types and the value forms. */
|
|
70
|
+
function readEdge(value, at) {
|
|
71
|
+
const refuse = (text, want) => {
|
|
72
|
+
throw new EdgesError(at, text, want);
|
|
73
|
+
};
|
|
74
|
+
if (!isObject(value)) {
|
|
75
|
+
return refuse(JSON.stringify(value), EDGE_WANT);
|
|
76
|
+
}
|
|
77
|
+
for (const [key, held] of Object.entries(value)) {
|
|
78
|
+
if (!EDGE_KEYS.has(key) || typeof held !== "string") {
|
|
79
|
+
refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const edge = value;
|
|
83
|
+
const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
|
|
84
|
+
const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
|
|
85
|
+
const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
|
|
86
|
+
if (!ID_FORM.test(id)) {
|
|
87
|
+
refuse(JSON.stringify(id), ID_WANT);
|
|
88
|
+
}
|
|
89
|
+
for (const ref of [provides, usedBy]) {
|
|
90
|
+
if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
|
|
91
|
+
refuse(JSON.stringify(ref), REFERENCE_WANT);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { id, provides, usedBy };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Every side of a declared edge that names a symbol of this language, in document order,
|
|
98
|
+
* `provides` before `used_by` within an edge. An absent document declares no edge. The
|
|
99
|
+
* document is strict JSON with a closed key list at both levels, and a defect in it is an
|
|
100
|
+
* {@link EdgesError}.
|
|
101
|
+
*/
|
|
102
|
+
export function readEdgeSides(host, targetRoot) {
|
|
103
|
+
const file = joinPath(targetRoot, EDGES_FILE);
|
|
104
|
+
if (host.kindOf(file) === "absent") {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
const text = host.readFile(file);
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(text);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw new EdgesError(EDGES_FILE, error instanceof Error ? error.message : String(error), DOCUMENT_WANT);
|
|
114
|
+
}
|
|
115
|
+
if (!isObject(parsed)) {
|
|
116
|
+
throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
|
|
117
|
+
}
|
|
118
|
+
const walk = walkDocument(text, "edges");
|
|
119
|
+
if (walk.twice !== undefined) {
|
|
120
|
+
throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)), `the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`, DOCUMENT_WANT);
|
|
121
|
+
}
|
|
122
|
+
const unknown = Object.keys(parsed).find((key) => !DOCUMENT_KEYS.has(key));
|
|
123
|
+
const description = parsed["description"];
|
|
124
|
+
const edges = parsed["edges"];
|
|
125
|
+
if (unknown !== undefined ||
|
|
126
|
+
(description !== undefined && typeof description !== "string") ||
|
|
127
|
+
!Array.isArray(edges)) {
|
|
128
|
+
throw new EdgesError(EDGES_FILE, unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`, DOCUMENT_WANT);
|
|
129
|
+
}
|
|
130
|
+
const sides = [];
|
|
131
|
+
edges.forEach((value, index) => {
|
|
132
|
+
const offset = walk.opened[index];
|
|
133
|
+
const at = offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
|
|
134
|
+
const edge = readEdge(value, at);
|
|
135
|
+
const named = [
|
|
136
|
+
["provides", edge.provides],
|
|
137
|
+
["used_by", edge.usedBy],
|
|
138
|
+
];
|
|
139
|
+
for (const [side, symbol] of named) {
|
|
140
|
+
if (symbol.startsWith(OWN_PREFIX)) {
|
|
141
|
+
sides.push({ edge: edge.id, side, symbol });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return sides;
|
|
146
|
+
}
|
|
147
|
+
/** Two strings ordered bytewise. */
|
|
148
|
+
function compare(a, b) {
|
|
149
|
+
if (a === b) {
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
return a < b ? -1 : 1;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Evaluates every declared side of this language over the run's findings. A side naming a
|
|
156
|
+
* symbol the run enumerates nothing under is `absent`; one naming a symbol the run holds a
|
|
157
|
+
* finding about is `dead` and carries that finding, pending until a merge reads the other
|
|
158
|
+
* side and reported nowhere else, a narrowing finding included; any other is `live`.
|
|
159
|
+
*/
|
|
160
|
+
export function evaluateEdges(findings, sides, symbols) {
|
|
161
|
+
if (sides.length === 0) {
|
|
162
|
+
return { findings, evaluations: [] };
|
|
163
|
+
}
|
|
164
|
+
const byRef = new Map();
|
|
165
|
+
for (const symbol of symbols) {
|
|
166
|
+
if (symbol.kind !== "file" && !byRef.has(symbol.ref)) {
|
|
167
|
+
byRef.set(symbol.ref, symbol.id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const held = new Map();
|
|
171
|
+
findings.forEach((finding, at) => {
|
|
172
|
+
held.set(positionKey(finding.position), at);
|
|
173
|
+
});
|
|
174
|
+
const pending = new Set();
|
|
175
|
+
const evaluations = sides.map((side) => {
|
|
176
|
+
const id = byRef.get(side.symbol);
|
|
177
|
+
if (id === undefined) {
|
|
178
|
+
return { ...side, state: "absent" };
|
|
179
|
+
}
|
|
180
|
+
const at = held.get(id);
|
|
181
|
+
const finding = at === undefined ? undefined : findings[at];
|
|
182
|
+
if (at === undefined || finding === undefined) {
|
|
183
|
+
return { ...side, state: "live" };
|
|
184
|
+
}
|
|
185
|
+
pending.add(at);
|
|
186
|
+
return { ...side, state: "dead", finding };
|
|
187
|
+
});
|
|
188
|
+
evaluations.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
|
|
189
|
+
return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
|
|
190
|
+
}
|
package/dist/src/finding.js
CHANGED
|
@@ -1,12 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The members of a finding its emitter decides: the code, where the subject is,
|
|
3
|
-
* what the subject is, and the sentence the finding reports. They are the
|
|
4
|
-
* Contract's finding members of the same names, spelled in this language's case.
|
|
5
|
-
*
|
|
6
|
-
* Every other member the finding schema requires follows from these and from the
|
|
7
|
-
* run: the kind's name, its fixability and its ceiling come from the code's row of
|
|
8
|
-
* the issue-kind vocabulary, the severity and the confidence from the resolved
|
|
9
|
-
* configuration, the language from the analyzer. A report completes them, so an
|
|
10
|
-
* emitter cannot state one that disagrees with the vocabulary.
|
|
11
|
-
*/
|
|
12
1
|
export {};
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What
|
|
3
|
-
* the
|
|
4
|
-
*
|
|
5
|
-
* severity the configuration gives its code. The dials withhold what they name and
|
|
6
|
-
* every finding that falls with a root they withhold.
|
|
2
|
+
* What every finding carries beyond what its emitter decides, read from the run and from
|
|
3
|
+
* the code's row of the issue-kind vocabulary, and the dials that withhold a finding
|
|
4
|
+
* together with every finding that falls with a root they withhold.
|
|
7
5
|
*/
|
|
8
6
|
import { componentId } from "../components.js";
|
|
7
|
+
import { OUTSIDE } from "../graph.js";
|
|
8
|
+
import { KINDS } from "../kinds.js";
|
|
9
9
|
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
10
10
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
11
|
-
|
|
11
|
+
const RANK = { certain: 3, probable: 2, possible: 1 };
|
|
12
12
|
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
13
|
-
|
|
13
|
+
function capped(found, ceiling) {
|
|
14
14
|
return RANK[found] <= RANK[ceiling] ? found : ceiling;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* The severity the configuration gives one code: the key naming the code, else the key
|
|
18
18
|
* naming its family, else the code's default.
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
function severityOf(config, code, fallback) {
|
|
21
21
|
return (config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback);
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
@@ -27,7 +27,7 @@ export function severityOf(config, code, fallback) {
|
|
|
27
27
|
* in the loaded program, a private member and a `#private` name among them, and is
|
|
28
28
|
* `certain`.
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
function reachabilityClasses(swept) {
|
|
31
31
|
const union = swept.matrix.union;
|
|
32
32
|
const published = new Set(union.rooted
|
|
33
33
|
.filter((root) => root.kind === "published-api")
|
|
@@ -35,7 +35,7 @@ export function reachabilityClasses(swept) {
|
|
|
35
35
|
return (id) => (published.has(id) ? "possible" : "certain");
|
|
36
36
|
}
|
|
37
37
|
/** The component each dead declaration of the run falls in, and none for a live one. */
|
|
38
|
-
|
|
38
|
+
function findingComponents(swept) {
|
|
39
39
|
const held = new Map();
|
|
40
40
|
for (const component of swept.sweep.components) {
|
|
41
41
|
for (const member of component.members) {
|
|
@@ -55,45 +55,102 @@ export function findingComponents(swept) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Every finding of the run
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* no two families mint one
|
|
58
|
+
* Every finding of the run completed, in the order given. A dead declaration carries its
|
|
59
|
+
* candidate's relation, counts, configurations and component; a live one carries no
|
|
60
|
+
* relation and holds where it is declared; a file or a manifest row is `certain` and
|
|
61
|
+
* holds in every configuration. A subject in no dead component gets one of its own,
|
|
62
|
+
* numbered past the computed ones in finding order, so no two families mint one.
|
|
63
63
|
*/
|
|
64
|
-
export function
|
|
64
|
+
export function completed(input, findings) {
|
|
65
|
+
const { swept } = input;
|
|
66
|
+
const union = swept.matrix.union;
|
|
67
|
+
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
68
|
+
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
69
|
+
const classOf = reachabilityClasses(swept);
|
|
65
70
|
const componentOf = findingComponents(swept);
|
|
66
|
-
const ids = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.ref, symbol.id]));
|
|
67
71
|
let minted = swept.sweep.components.length;
|
|
68
72
|
return findings.map((finding) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return { ...finding, component };
|
|
73
|
+
const row = KINDS.get(finding.code);
|
|
74
|
+
if (row === undefined) {
|
|
75
|
+
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
73
76
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
const at = positions.get(finding.symbol.ref) ?? OUTSIDE;
|
|
78
|
+
const symbol = union.symbols[at];
|
|
79
|
+
const judged = union.subject[at] === true ? symbol : undefined;
|
|
80
|
+
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
81
|
+
const found = symbol === undefined ? undefined : componentOf(symbol.id);
|
|
82
|
+
if (candidate !== undefined && found === undefined) {
|
|
83
|
+
throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
|
|
84
|
+
}
|
|
85
|
+
let component = found;
|
|
86
|
+
if (component === undefined) {
|
|
87
|
+
minted += 1;
|
|
88
|
+
component = {
|
|
78
89
|
id: componentId(minted),
|
|
79
90
|
root: true,
|
|
80
91
|
symbolCount: 1,
|
|
81
92
|
deletableLines: finding.symbol.sizeLines,
|
|
82
|
-
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const counts = candidate === undefined
|
|
96
|
+
? union.made[at]
|
|
97
|
+
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
98
|
+
const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
|
|
99
|
+
return {
|
|
100
|
+
code: finding.code,
|
|
101
|
+
kind: row.name,
|
|
102
|
+
language: "ts",
|
|
103
|
+
position: finding.position,
|
|
104
|
+
symbol: finding.symbol,
|
|
105
|
+
reachabilityClass,
|
|
106
|
+
confidence: capped(reachabilityClass, row.maxClass),
|
|
107
|
+
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
108
|
+
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
109
|
+
generated: false,
|
|
110
|
+
component,
|
|
111
|
+
retainedBy: [],
|
|
112
|
+
configurations: candidate?.configurations ??
|
|
113
|
+
(judged === undefined ? swept.matrix.configurations : (swept.matrix.heldIn[at] ?? [])),
|
|
114
|
+
consumersLoaded: input.boundary.consumers.loaded,
|
|
115
|
+
fixability: row.fixability,
|
|
116
|
+
severity: severityOf(input.config, finding.code, row.defaultSeverity),
|
|
117
|
+
message: finding.message,
|
|
118
|
+
details: finding.details ?? {},
|
|
83
119
|
};
|
|
84
120
|
});
|
|
85
121
|
}
|
|
122
|
+
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
123
|
+
function dialed(finding, minConfidence) {
|
|
124
|
+
return finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
125
|
+
}
|
|
86
126
|
/**
|
|
87
|
-
* The
|
|
88
|
-
* is withheld, and so is every finding of the component of a
|
|
89
|
-
* because a symbol that falls with a root is dead only through it
|
|
90
|
-
* does not compile while the root stays. A withheld finding that is no
|
|
91
|
-
* itself alone.
|
|
127
|
+
* The configuration's dials over one run's completed findings. A finding at `allow` or
|
|
128
|
+
* below the minimum confidence is withheld, and so is every finding of the component of a
|
|
129
|
+
* root the dials withhold, because a symbol that falls with a root is dead only through it
|
|
130
|
+
* and its deletion does not compile while the root stays. A withheld finding that is no
|
|
131
|
+
* root withholds itself alone. A code is withheld from a declaration when the finding it
|
|
132
|
+
* would report there, completed, is withheld by its own severity or confidence.
|
|
92
133
|
*/
|
|
93
|
-
export function
|
|
94
|
-
const
|
|
134
|
+
export function dialsOf(input, findings) {
|
|
135
|
+
const minConfidence = input.config.analysis.minConfidence;
|
|
136
|
+
const classOf = reachabilityClasses(input.swept);
|
|
95
137
|
const components = new Set(findings
|
|
96
|
-
.filter((finding) => finding.component.root && dialed(finding))
|
|
138
|
+
.filter((finding) => finding.component.root && dialed(finding, minConfidence))
|
|
97
139
|
.map((finding) => finding.component.id));
|
|
98
|
-
return
|
|
140
|
+
return {
|
|
141
|
+
withholdsCode: (code, id) => {
|
|
142
|
+
const row = KINDS.get(code);
|
|
143
|
+
return (row !== undefined &&
|
|
144
|
+
dialed({
|
|
145
|
+
severity: severityOf(input.config, code, row.defaultSeverity),
|
|
146
|
+
confidence: capped(classOf(id), row.maxClass),
|
|
147
|
+
}, minConfidence));
|
|
148
|
+
},
|
|
149
|
+
withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** The findings of one run's completed findings that no dial withholds, in the order given. */
|
|
153
|
+
export function reportable(input, findings) {
|
|
154
|
+
const dials = dialsOf(input, findings);
|
|
155
|
+
return findings.filter((finding) => !dials.withholds(finding));
|
|
99
156
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { renderRef } from "../ref.js";
|
|
2
|
-
import { severityOf } from "./completion.js";
|
|
3
2
|
const UNUSED_DEPENDENCY = "DS1601";
|
|
4
|
-
/** The severity the kind ships at, which the configuration's keys override. */
|
|
5
|
-
const UNUSED_DEPENDENCY_SEVERITY = "deny";
|
|
6
3
|
const MESSAGES = {
|
|
7
4
|
dependency: "no import in the target needs the declared dependency",
|
|
8
5
|
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
@@ -34,14 +31,9 @@ function neededBy(dependencies) {
|
|
|
34
31
|
}
|
|
35
32
|
/**
|
|
36
33
|
* A dependency, development dependency or peer dependency of the target's manifest
|
|
37
|
-
* that the target does not need, at the position of its key.
|
|
38
|
-
* `allow`. Every subject is a manifest entry, which is `certain` whatever the
|
|
39
|
-
* consumers, so no minimum confidence withholds one.
|
|
34
|
+
* that the target does not need, at the position of its key.
|
|
40
35
|
*/
|
|
41
|
-
function unusedDependencies(
|
|
42
|
-
if (severityOf(config, UNUSED_DEPENDENCY, UNUSED_DEPENDENCY_SEVERITY) === "allow") {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
36
|
+
function unusedDependencies(dependencies) {
|
|
45
37
|
const needed = neededBy(dependencies);
|
|
46
38
|
return dependencies.declared
|
|
47
39
|
.filter((dependency) => !needed.has(dependency.name))
|
|
@@ -63,4 +55,4 @@ function unusedDependencies(config, dependencies) {
|
|
|
63
55
|
}));
|
|
64
56
|
}
|
|
65
57
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
66
|
-
export const dependenciesAndModuleMachinery = ({
|
|
58
|
+
export const dependenciesAndModuleMachinery = ({ dependencies }) => unusedDependencies(dependencies);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { evaluateEdges } from "../edges.js";
|
|
2
|
+
import { ledgerOf } from "../suppress.js";
|
|
3
|
+
import { completed, dialsOf, reportable } from "./completion.js";
|
|
1
4
|
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
|
|
2
|
-
import { placed } from "./completion.js";
|
|
3
5
|
import { interfaces } from "./interfaces.js";
|
|
4
6
|
import { intraFunction } from "./intra-function.js";
|
|
5
7
|
import { nonCodeArtifacts } from "./non-code-artifacts.js";
|
|
@@ -18,10 +20,46 @@ export const EMITTERS = new Map([
|
|
|
18
20
|
["self-check", selfCheck],
|
|
19
21
|
["intra-function", intraFunction],
|
|
20
22
|
]);
|
|
23
|
+
/** Every family's findings over one sweep, completed by one step. */
|
|
24
|
+
function completedOver(input) {
|
|
25
|
+
return completed(input, [...EMITTERS.values()].flatMap((emit) => emit(input)));
|
|
26
|
+
}
|
|
21
27
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
28
|
+
* The dials over the findings a record could name, each read against the sweep that
|
|
29
|
+
* produced it, so a component one sweep numbers withholds nothing of the other's.
|
|
24
30
|
*/
|
|
31
|
+
function dialsOver(input, found, suppressed, unmarked) {
|
|
32
|
+
const marked = dialsOf(input, found);
|
|
33
|
+
const before = dialsOf(suppressed.unmarked, unmarked);
|
|
34
|
+
const fromUnmarked = new Set(unmarked);
|
|
35
|
+
return {
|
|
36
|
+
withholdsCode: marked.withholdsCode,
|
|
37
|
+
withholds: (finding) => fromUnmarked.has(finding) ? before.withholds(finding) : marked.withholds(finding),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Every finding of one run swept under its suppressions' marks, decided in one pass: every
|
|
42
|
+
* family's findings completed by one step, so every component number is minted once, a
|
|
43
|
+
* pending finding's among them; the finding each record claims withheld, whatever its
|
|
44
|
+
* kind; the rest withheld by the dials, so a root one family reports withholds what
|
|
45
|
+
* another family reports in its component; and every declared edge evaluated over what
|
|
46
|
+
* remains, so a finding it holds pending is in its evaluation and nowhere else.
|
|
47
|
+
*/
|
|
48
|
+
export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
|
|
49
|
+
const found = completedOver(input);
|
|
50
|
+
const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked);
|
|
51
|
+
const would = [...unmarked, ...found];
|
|
52
|
+
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
53
|
+
const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding)));
|
|
54
|
+
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
55
|
+
return {
|
|
56
|
+
findings: evaluated.findings,
|
|
57
|
+
evaluations: evaluated.evaluations,
|
|
58
|
+
ledger,
|
|
59
|
+
would,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Every finding the run reports, each family's in table order. */
|
|
25
63
|
export function findingsOf(input) {
|
|
26
|
-
return
|
|
64
|
+
return decidedFindings(input).findings;
|
|
27
65
|
}
|