@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/src/edges.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
|
|
8
|
+
import { ConfigError } from "./config.ts";
|
|
9
|
+
import type { Finding } from "./finding.ts";
|
|
10
|
+
import type { EdgeSide, EdgeSideName } from "./findings/boundary.ts";
|
|
11
|
+
import type { Host } from "./host.ts";
|
|
12
|
+
import type { InventorySymbol } from "./inventory.ts";
|
|
13
|
+
import { positionAt, walkDocument } from "./json-document.ts";
|
|
14
|
+
import { joinPath } from "./paths.ts";
|
|
15
|
+
import { positionKey } from "./position.ts";
|
|
16
|
+
|
|
17
|
+
/** The name and the location the Contract fixes for the edges document. */
|
|
18
|
+
export const EDGES_FILE = "deadset-edges.json";
|
|
19
|
+
|
|
20
|
+
/** The language prefix of every reference this analyzer evaluates. */
|
|
21
|
+
const OWN_PREFIX = "ts://";
|
|
22
|
+
|
|
23
|
+
/** The closed key list of the document and of one edge. */
|
|
24
|
+
const DOCUMENT_KEYS: ReadonlySet<string> = new Set(["description", "edges"]);
|
|
25
|
+
const EDGE_KEYS: ReadonlySet<string> = new Set(["id", "because", "provides", "used_by"]);
|
|
26
|
+
|
|
27
|
+
/** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
|
|
28
|
+
const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
|
|
29
|
+
|
|
30
|
+
/** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
|
|
31
|
+
const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
|
|
32
|
+
|
|
33
|
+
const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
|
|
34
|
+
const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
|
|
35
|
+
const ID_WANT =
|
|
36
|
+
"one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
|
|
37
|
+
const REFERENCE_WANT =
|
|
38
|
+
"a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
|
|
39
|
+
|
|
40
|
+
/** An edges document the Contract refuses: a malformed input, answered with the usage code. */
|
|
41
|
+
export class EdgesError extends ConfigError {
|
|
42
|
+
constructor(at: string, text: string, want: string) {
|
|
43
|
+
super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
|
|
44
|
+
this.name = "EdgesError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a reference carries a wildcard outside a quoted or a computed component, which
|
|
50
|
+
* would name a set of symbols rather than one.
|
|
51
|
+
*/
|
|
52
|
+
function carriesWildcard(ref: string): boolean {
|
|
53
|
+
let quoted = false;
|
|
54
|
+
let computed = false;
|
|
55
|
+
let escaped = false;
|
|
56
|
+
for (const char of ref) {
|
|
57
|
+
if (escaped) {
|
|
58
|
+
escaped = false;
|
|
59
|
+
} else if (quoted && char === "\\") {
|
|
60
|
+
escaped = true;
|
|
61
|
+
} else if (quoted) {
|
|
62
|
+
quoted = char !== "'";
|
|
63
|
+
} else if (computed) {
|
|
64
|
+
computed = char !== "]";
|
|
65
|
+
} else if (char === "'") {
|
|
66
|
+
quoted = true;
|
|
67
|
+
} else if (char === "[") {
|
|
68
|
+
computed = true;
|
|
69
|
+
} else if (char === "*") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One edge, checked: the closed key list, the value types and the value forms. */
|
|
81
|
+
function readEdge(value: unknown, at: string): { id: string; provides: string; usedBy: string } {
|
|
82
|
+
const refuse = (text: string, want: string): never => {
|
|
83
|
+
throw new EdgesError(at, text, want);
|
|
84
|
+
};
|
|
85
|
+
if (!isObject(value)) {
|
|
86
|
+
return refuse(JSON.stringify(value), EDGE_WANT);
|
|
87
|
+
}
|
|
88
|
+
for (const [key, held] of Object.entries(value)) {
|
|
89
|
+
if (!EDGE_KEYS.has(key) || typeof held !== "string") {
|
|
90
|
+
refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const edge = value as Readonly<Record<string, string | undefined>>;
|
|
94
|
+
const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
|
|
95
|
+
const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
|
|
96
|
+
const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
|
|
97
|
+
if (!ID_FORM.test(id)) {
|
|
98
|
+
refuse(JSON.stringify(id), ID_WANT);
|
|
99
|
+
}
|
|
100
|
+
for (const ref of [provides, usedBy]) {
|
|
101
|
+
if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
|
|
102
|
+
refuse(JSON.stringify(ref), REFERENCE_WANT);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { id, provides, usedBy };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Every side of a declared edge that names a symbol of this language, in document order,
|
|
110
|
+
* `provides` before `used_by` within an edge. An absent document declares no edge. The
|
|
111
|
+
* document is strict JSON with a closed key list at both levels, and a defect in it is an
|
|
112
|
+
* {@link EdgesError}.
|
|
113
|
+
*/
|
|
114
|
+
export function readEdgeSides(host: Host, targetRoot: string): readonly EdgeSide[] {
|
|
115
|
+
const file = joinPath(targetRoot, EDGES_FILE);
|
|
116
|
+
if (host.kindOf(file) === "absent") {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const text = host.readFile(file);
|
|
120
|
+
let parsed: unknown;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(text);
|
|
123
|
+
} catch (error: unknown) {
|
|
124
|
+
throw new EdgesError(
|
|
125
|
+
EDGES_FILE,
|
|
126
|
+
error instanceof Error ? error.message : String(error),
|
|
127
|
+
DOCUMENT_WANT,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (!isObject(parsed)) {
|
|
131
|
+
throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
|
|
132
|
+
}
|
|
133
|
+
const walk = walkDocument(text, "edges");
|
|
134
|
+
if (walk.twice !== undefined) {
|
|
135
|
+
throw new EdgesError(
|
|
136
|
+
positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)),
|
|
137
|
+
`the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`,
|
|
138
|
+
DOCUMENT_WANT,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const unknown = Object.keys(parsed).find((key) => !DOCUMENT_KEYS.has(key));
|
|
142
|
+
const description = parsed["description"];
|
|
143
|
+
const edges = parsed["edges"];
|
|
144
|
+
if (
|
|
145
|
+
unknown !== undefined ||
|
|
146
|
+
(description !== undefined && typeof description !== "string") ||
|
|
147
|
+
!Array.isArray(edges)
|
|
148
|
+
) {
|
|
149
|
+
throw new EdgesError(
|
|
150
|
+
EDGES_FILE,
|
|
151
|
+
unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`,
|
|
152
|
+
DOCUMENT_WANT,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const sides: EdgeSide[] = [];
|
|
157
|
+
(edges as readonly unknown[]).forEach((value, index) => {
|
|
158
|
+
const offset = walk.opened[index];
|
|
159
|
+
const at =
|
|
160
|
+
offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
|
|
161
|
+
const edge = readEdge(value, at);
|
|
162
|
+
const named: readonly [EdgeSideName, string][] = [
|
|
163
|
+
["provides", edge.provides],
|
|
164
|
+
["used_by", edge.usedBy],
|
|
165
|
+
];
|
|
166
|
+
for (const [side, symbol] of named) {
|
|
167
|
+
if (symbol.startsWith(OWN_PREFIX)) {
|
|
168
|
+
sides.push({ edge: edge.id, side, symbol });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return sides;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** One side's verdict, as an evaluation names it. */
|
|
176
|
+
export type EdgeState = "live" | "dead" | "absent";
|
|
177
|
+
|
|
178
|
+
/** One edge evaluation: one side of this language, its state, and the finding it holds. */
|
|
179
|
+
export interface EdgeEvaluation<F extends Finding = Finding> extends EdgeSide {
|
|
180
|
+
readonly state: EdgeState;
|
|
181
|
+
/** Present exactly when the state is `dead`: the finding a live paired symbol cancels. */
|
|
182
|
+
readonly finding?: F;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The findings a report publishes, and one evaluation per declared side of this language. */
|
|
186
|
+
export interface Evaluated<F extends Finding> {
|
|
187
|
+
readonly findings: readonly F[];
|
|
188
|
+
/** Ordered by edge, then by side, each compared bytewise. */
|
|
189
|
+
readonly evaluations: readonly EdgeEvaluation<F>[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Two strings ordered bytewise. */
|
|
193
|
+
function compare(a: string, b: string): number {
|
|
194
|
+
if (a === b) {
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
return a < b ? -1 : 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Evaluates every declared side of this language over the run's findings. A side naming a
|
|
202
|
+
* symbol the run enumerates nothing under is `absent`; one naming a symbol the run holds a
|
|
203
|
+
* finding about is `dead` and carries that finding, pending until a merge reads the other
|
|
204
|
+
* side and reported nowhere else, a narrowing finding included; any other is `live`.
|
|
205
|
+
*/
|
|
206
|
+
export function evaluateEdges<F extends Finding>(
|
|
207
|
+
findings: readonly F[],
|
|
208
|
+
sides: readonly EdgeSide[],
|
|
209
|
+
symbols: readonly InventorySymbol[],
|
|
210
|
+
): Evaluated<F> {
|
|
211
|
+
if (sides.length === 0) {
|
|
212
|
+
return { findings, evaluations: [] };
|
|
213
|
+
}
|
|
214
|
+
const byRef = new Map<string, string>();
|
|
215
|
+
for (const symbol of symbols) {
|
|
216
|
+
if (symbol.kind !== "file" && !byRef.has(symbol.ref)) {
|
|
217
|
+
byRef.set(symbol.ref, symbol.id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const held = new Map<string, number>();
|
|
221
|
+
findings.forEach((finding, at) => {
|
|
222
|
+
held.set(positionKey(finding.position), at);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const pending = new Set<number>();
|
|
226
|
+
const evaluations = sides.map((side): EdgeEvaluation<F> => {
|
|
227
|
+
const id = byRef.get(side.symbol);
|
|
228
|
+
if (id === undefined) {
|
|
229
|
+
return { ...side, state: "absent" };
|
|
230
|
+
}
|
|
231
|
+
const at = held.get(id);
|
|
232
|
+
const finding = at === undefined ? undefined : findings[at];
|
|
233
|
+
if (at === undefined || finding === undefined) {
|
|
234
|
+
return { ...side, state: "live" };
|
|
235
|
+
}
|
|
236
|
+
pending.add(at);
|
|
237
|
+
return { ...side, state: "dead", finding };
|
|
238
|
+
});
|
|
239
|
+
evaluations.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
|
|
240
|
+
return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
|
|
241
|
+
}
|
package/src/finding.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import type { Confidence, Severity } from "./config.ts";
|
|
2
|
+
import type { Fixability } from "./kinds.ts";
|
|
3
|
+
import type { DependencySection } from "./ref.ts";
|
|
4
|
+
import type { Relation } from "./sweep.ts";
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
|
+
* The finding: what an emitter decides about one subject, and every member the
|
|
8
|
+
* Contract's finding schema requires, spelled in this language's case. An emitter states
|
|
9
|
+
* the code, where the subject is, what it is, the sentence, and the details its code
|
|
10
|
+
* carries; the completion step reads every other member from the run and from the code's
|
|
11
|
+
* row of the issue-kind vocabulary, so no emitter states one that disagrees with either.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
/** Where the subject of a finding is. */
|
|
@@ -34,10 +35,75 @@ export interface FindingSubject {
|
|
|
34
35
|
readonly sizeLines: number;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/**
|
|
38
|
+
/** A declaration a finding names beside the subject, with where it is written. */
|
|
39
|
+
export interface PositionedSymbol {
|
|
40
|
+
readonly ref: string;
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly position: FindingPosition;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What only some codes carry. The finding schema's branches decide which code carries
|
|
47
|
+
* which member, and every member is absent under every other code.
|
|
48
|
+
*/
|
|
49
|
+
export interface FindingDetails {
|
|
50
|
+
/**
|
|
51
|
+
* On `DS1101` and `DS1104`: the widest scope that holds every reference to the
|
|
52
|
+
* subject, which is the visibility its references support.
|
|
53
|
+
*/
|
|
54
|
+
readonly narrowerVisibility?: "file" | "package";
|
|
55
|
+
/** On `DS1201` and `DS1203`: the classes implementing the interface, in site order. */
|
|
56
|
+
readonly implementations?: readonly PositionedSymbol[];
|
|
57
|
+
/** On `DS1301`: every position the subject is written at, in site order. */
|
|
58
|
+
readonly writePositions?: readonly FindingPosition[];
|
|
59
|
+
/** On `DS1601`: the manifest section that declares the dependency. */
|
|
60
|
+
readonly dependencyClass?: DependencySection;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One finding as its emitter states it. */
|
|
38
64
|
export interface Finding {
|
|
39
65
|
readonly code: string;
|
|
40
66
|
readonly position: FindingPosition;
|
|
41
67
|
readonly symbol: FindingSubject;
|
|
42
68
|
readonly message: string;
|
|
69
|
+
/** Absent where the code carries nothing. */
|
|
70
|
+
readonly details?: FindingDetails;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The dead component a finding names, as the finding schema spells its members. */
|
|
74
|
+
export interface FindingComponent {
|
|
75
|
+
readonly id: string;
|
|
76
|
+
/** Whether the subject is a root member of the component. */
|
|
77
|
+
readonly root: boolean;
|
|
78
|
+
/** The number of the component's members. */
|
|
79
|
+
readonly symbolCount: number;
|
|
80
|
+
readonly deletableLines: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One finding with every member the finding schema requires. */
|
|
84
|
+
export interface CompletedFinding extends Finding {
|
|
85
|
+
/** The name of the code's row of the issue-kind vocabulary. */
|
|
86
|
+
readonly kind: string;
|
|
87
|
+
readonly language: "ts";
|
|
88
|
+
readonly reachabilityClass: Confidence;
|
|
89
|
+
/** The reachability class capped by the code's ceiling. */
|
|
90
|
+
readonly confidence: Confidence;
|
|
91
|
+
/**
|
|
92
|
+
* The relation that decided the subject: present on a declaration the sweep judged
|
|
93
|
+
* dead, absent on a live one and on a subject that is no declaration.
|
|
94
|
+
*/
|
|
95
|
+
readonly livenessRelation?: Relation;
|
|
96
|
+
/** Whether every reference to the subject comes from a test file. */
|
|
97
|
+
readonly testOnly: boolean;
|
|
98
|
+
readonly generated: boolean;
|
|
99
|
+
readonly component: FindingComponent;
|
|
100
|
+
/** Empty on every finding: a declaration an exemption holds back is not reported. */
|
|
101
|
+
readonly retainedBy: readonly string[];
|
|
102
|
+
/** The configurations the finding holds in, in the run's order. */
|
|
103
|
+
readonly configurations: readonly string[];
|
|
104
|
+
/** The consumers the run loaded beside the target. */
|
|
105
|
+
readonly consumersLoaded: readonly string[];
|
|
106
|
+
readonly fixability: Fixability;
|
|
107
|
+
readonly severity: Severity;
|
|
108
|
+
readonly details: FindingDetails;
|
|
43
109
|
}
|
package/src/findings/boundary.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { Finding } from "../finding.ts";
|
|
2
|
-
|
|
3
1
|
/** Which side of a declared cross-language edge, as the edges document spells it. */
|
|
4
2
|
export type EdgeSideName = "provides" | "used_by";
|
|
5
3
|
|
|
@@ -12,15 +10,6 @@ export interface EdgeSide {
|
|
|
12
10
|
readonly symbol: string;
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
/**
|
|
16
|
-
* One finding a declared edge holds back: the finding the analyzer would report about
|
|
17
|
-
* the symbol one side of the edge names, which a report carries only inside that side's
|
|
18
|
-
* evaluation, because the code that may use the symbol is in the other language.
|
|
19
|
-
*/
|
|
20
|
-
export interface PendingFinding extends EdgeSide {
|
|
21
|
-
readonly finding: Finding;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
13
|
/** What the run knows about the code outside the target that can reach its declarations. */
|
|
25
14
|
export interface Boundary {
|
|
26
15
|
readonly consumers: {
|
|
@@ -1,50 +1,24 @@
|
|
|
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
|
|
|
9
7
|
import type { RunSweep } from "../analysis.ts";
|
|
10
8
|
import { componentId, type Component } from "../components.ts";
|
|
11
9
|
import type { Confidence, Config, Severity } from "../config.ts";
|
|
12
|
-
import type { Finding } from "../finding.ts";
|
|
10
|
+
import type { CompletedFinding, Finding, FindingComponent } from "../finding.ts";
|
|
11
|
+
import { OUTSIDE } from "../graph.ts";
|
|
12
|
+
import { KINDS } from "../kinds.ts";
|
|
13
13
|
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
14
|
-
import type {
|
|
15
|
-
|
|
16
|
-
/** The dead component a finding names, as the finding schema spells its members. */
|
|
17
|
-
export interface FindingComponent {
|
|
18
|
-
readonly id: string;
|
|
19
|
-
/** Whether the subject is a root member of the component. */
|
|
20
|
-
readonly root: boolean;
|
|
21
|
-
/** The number of the component's members. */
|
|
22
|
-
readonly symbolCount: number;
|
|
23
|
-
readonly deletableLines: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** One finding with the members the run decides, in this language's case. */
|
|
27
|
-
export interface CompletedFinding extends Finding {
|
|
28
|
-
readonly reachabilityClass: Confidence;
|
|
29
|
-
/** The reachability class capped by the code's ceiling. */
|
|
30
|
-
readonly confidence: Confidence;
|
|
31
|
-
/** The relation that decided the subject, absent where none did. */
|
|
32
|
-
readonly livenessRelation?: Relation;
|
|
33
|
-
/** Whether every reference to the subject comes from a test file. */
|
|
34
|
-
readonly testOnly: boolean;
|
|
35
|
-
readonly component: FindingComponent;
|
|
36
|
-
/** The configurations the finding holds in, in the run's order. */
|
|
37
|
-
readonly configurations: readonly string[];
|
|
38
|
-
readonly severity: Severity;
|
|
39
|
-
/** The per-code members of the finding schema's `details`, empty for a code with none. */
|
|
40
|
-
readonly details: Readonly<Record<string, unknown>>;
|
|
41
|
-
}
|
|
14
|
+
import type { Dials } from "../suppress.ts";
|
|
15
|
+
import type { EmitterInput } from "./emitter.ts";
|
|
42
16
|
|
|
43
17
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
44
|
-
|
|
18
|
+
const RANK: Readonly<Record<Confidence, number>> = { certain: 3, probable: 2, possible: 1 };
|
|
45
19
|
|
|
46
20
|
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
47
|
-
|
|
21
|
+
function capped(found: Confidence, ceiling: Confidence): Confidence {
|
|
48
22
|
return RANK[found] <= RANK[ceiling] ? found : ceiling;
|
|
49
23
|
}
|
|
50
24
|
|
|
@@ -52,7 +26,7 @@ export function capped(found: Confidence, ceiling: Confidence): Confidence {
|
|
|
52
26
|
* The severity the configuration gives one code: the key naming the code, else the key
|
|
53
27
|
* naming its family, else the code's default.
|
|
54
28
|
*/
|
|
55
|
-
|
|
29
|
+
function severityOf(config: Config, code: string, fallback: Severity): Severity {
|
|
56
30
|
return (
|
|
57
31
|
config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
|
|
58
32
|
);
|
|
@@ -65,7 +39,7 @@ export function severityOf(config: Config, code: string, fallback: Severity): Se
|
|
|
65
39
|
* in the loaded program, a private member and a `#private` name among them, and is
|
|
66
40
|
* `certain`.
|
|
67
41
|
*/
|
|
68
|
-
|
|
42
|
+
function reachabilityClasses(swept: RunSweep): (id: string) => Confidence {
|
|
69
43
|
const union = swept.matrix.union;
|
|
70
44
|
const published = new Set(
|
|
71
45
|
union.rooted
|
|
@@ -76,7 +50,7 @@ export function reachabilityClasses(swept: RunSweep): (id: string) => Confidence
|
|
|
76
50
|
}
|
|
77
51
|
|
|
78
52
|
/** The component each dead declaration of the run falls in, and none for a live one. */
|
|
79
|
-
|
|
53
|
+
function findingComponents(swept: RunSweep): (id: string) => FindingComponent | undefined {
|
|
80
54
|
const held = new Map<string, Component>();
|
|
81
55
|
for (const component of swept.sweep.components) {
|
|
82
56
|
for (const member of component.members) {
|
|
@@ -96,56 +70,128 @@ export function findingComponents(swept: RunSweep): (id: string) => FindingCompo
|
|
|
96
70
|
};
|
|
97
71
|
}
|
|
98
72
|
|
|
99
|
-
/** One finding with the component it names. */
|
|
100
|
-
export type PlacedFinding = Finding & { readonly component: FindingComponent };
|
|
101
|
-
|
|
102
73
|
/**
|
|
103
|
-
* Every finding of the run
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* no two families mint one
|
|
74
|
+
* Every finding of the run completed, in the order given. A dead declaration carries its
|
|
75
|
+
* candidate's relation, counts, configurations and component; a live one carries no
|
|
76
|
+
* relation and holds where it is declared; a file or a manifest row is `certain` and
|
|
77
|
+
* holds in every configuration. A subject in no dead component gets one of its own,
|
|
78
|
+
* numbered past the computed ones in finding order, so no two families mint one.
|
|
108
79
|
*/
|
|
109
|
-
export function
|
|
80
|
+
export function completed(
|
|
81
|
+
input: EmitterInput,
|
|
82
|
+
findings: readonly Finding[],
|
|
83
|
+
): readonly CompletedFinding[] {
|
|
84
|
+
const { swept } = input;
|
|
85
|
+
const union = swept.matrix.union;
|
|
86
|
+
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
87
|
+
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
88
|
+
const classOf = reachabilityClasses(swept);
|
|
110
89
|
const componentOf = findingComponents(swept);
|
|
111
|
-
const ids = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.ref, symbol.id]));
|
|
112
90
|
let minted = swept.sweep.components.length;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
91
|
+
|
|
92
|
+
return findings.map((finding): CompletedFinding => {
|
|
93
|
+
const row = KINDS.get(finding.code);
|
|
94
|
+
if (row === undefined) {
|
|
95
|
+
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
118
96
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
97
|
+
const at = positions.get(finding.symbol.ref) ?? OUTSIDE;
|
|
98
|
+
const symbol = union.symbols[at];
|
|
99
|
+
const judged = union.subject[at] === true ? symbol : undefined;
|
|
100
|
+
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
101
|
+
const found = symbol === undefined ? undefined : componentOf(symbol.id);
|
|
102
|
+
if (candidate !== undefined && found === undefined) {
|
|
103
|
+
throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
|
|
104
|
+
}
|
|
105
|
+
let component = found;
|
|
106
|
+
if (component === undefined) {
|
|
107
|
+
minted += 1;
|
|
108
|
+
component = {
|
|
123
109
|
id: componentId(minted),
|
|
124
110
|
root: true,
|
|
125
111
|
symbolCount: 1,
|
|
126
112
|
deletableLines: finding.symbol.sizeLines,
|
|
127
|
-
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const counts =
|
|
116
|
+
candidate === undefined
|
|
117
|
+
? union.made[at]
|
|
118
|
+
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
119
|
+
const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
|
|
120
|
+
return {
|
|
121
|
+
code: finding.code,
|
|
122
|
+
kind: row.name,
|
|
123
|
+
language: "ts",
|
|
124
|
+
position: finding.position,
|
|
125
|
+
symbol: finding.symbol,
|
|
126
|
+
reachabilityClass,
|
|
127
|
+
confidence: capped(reachabilityClass, row.maxClass),
|
|
128
|
+
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
129
|
+
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
130
|
+
generated: false,
|
|
131
|
+
component,
|
|
132
|
+
retainedBy: [],
|
|
133
|
+
configurations:
|
|
134
|
+
candidate?.configurations ??
|
|
135
|
+
(judged === undefined ? swept.matrix.configurations : (swept.matrix.heldIn[at] ?? [])),
|
|
136
|
+
consumersLoaded: input.boundary.consumers.loaded,
|
|
137
|
+
fixability: row.fixability,
|
|
138
|
+
severity: severityOf(input.config, finding.code, row.defaultSeverity),
|
|
139
|
+
message: finding.message,
|
|
140
|
+
details: finding.details ?? {},
|
|
128
141
|
};
|
|
129
142
|
});
|
|
130
143
|
}
|
|
131
144
|
|
|
145
|
+
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
146
|
+
function dialed(
|
|
147
|
+
finding: Pick<CompletedFinding, "severity" | "confidence">,
|
|
148
|
+
minConfidence: Confidence,
|
|
149
|
+
): boolean {
|
|
150
|
+
return finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
151
|
+
}
|
|
152
|
+
|
|
132
153
|
/**
|
|
133
|
-
* The
|
|
134
|
-
* is withheld, and so is every finding of the component of a
|
|
135
|
-
* because a symbol that falls with a root is dead only through it
|
|
136
|
-
* does not compile while the root stays. A withheld finding that is no
|
|
137
|
-
* itself alone.
|
|
154
|
+
* The configuration's dials over one run's completed findings. A finding at `allow` or
|
|
155
|
+
* below the minimum confidence is withheld, and so is every finding of the component of a
|
|
156
|
+
* root the dials withhold, because a symbol that falls with a root is dead only through it
|
|
157
|
+
* and its deletion does not compile while the root stays. A withheld finding that is no
|
|
158
|
+
* root withholds itself alone. A code is withheld from a declaration when the finding it
|
|
159
|
+
* would report there, completed, is withheld by its own severity or confidence.
|
|
138
160
|
*/
|
|
139
|
-
export function
|
|
161
|
+
export function dialsOf(
|
|
162
|
+
input: EmitterInput,
|
|
140
163
|
findings: readonly CompletedFinding[],
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
164
|
+
): Dials<CompletedFinding> {
|
|
165
|
+
const minConfidence = input.config.analysis.minConfidence;
|
|
166
|
+
const classOf = reachabilityClasses(input.swept);
|
|
145
167
|
const components = new Set(
|
|
146
168
|
findings
|
|
147
|
-
.filter((finding) => finding.component.root && dialed(finding))
|
|
169
|
+
.filter((finding) => finding.component.root && dialed(finding, minConfidence))
|
|
148
170
|
.map((finding) => finding.component.id),
|
|
149
171
|
);
|
|
150
|
-
return
|
|
172
|
+
return {
|
|
173
|
+
withholdsCode: (code, id) => {
|
|
174
|
+
const row = KINDS.get(code);
|
|
175
|
+
return (
|
|
176
|
+
row !== undefined &&
|
|
177
|
+
dialed(
|
|
178
|
+
{
|
|
179
|
+
severity: severityOf(input.config, code, row.defaultSeverity),
|
|
180
|
+
confidence: capped(classOf(id), row.maxClass),
|
|
181
|
+
},
|
|
182
|
+
minConfidence,
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The findings of one run's completed findings that no dial withholds, in the order given. */
|
|
191
|
+
export function reportable(
|
|
192
|
+
input: EmitterInput,
|
|
193
|
+
findings: readonly CompletedFinding[],
|
|
194
|
+
): readonly CompletedFinding[] {
|
|
195
|
+
const dials = dialsOf(input, findings);
|
|
196
|
+
return findings.filter((finding) => !dials.withholds(finding));
|
|
151
197
|
}
|
|
@@ -1,26 +1,16 @@
|
|
|
1
|
-
import type { Config, Severity } from "../config.ts";
|
|
2
1
|
import type { Dependencies } from "../dependencies.ts";
|
|
3
2
|
import type { Finding } from "../finding.ts";
|
|
4
3
|
import { renderRef, type DependencySection } from "../ref.ts";
|
|
5
|
-
import { severityOf } from "./completion.ts";
|
|
6
4
|
import type { Emitter } from "./emitter.ts";
|
|
7
5
|
|
|
8
6
|
const UNUSED_DEPENDENCY = "DS1601";
|
|
9
7
|
|
|
10
|
-
/** The severity the kind ships at, which the configuration's keys override. */
|
|
11
|
-
const UNUSED_DEPENDENCY_SEVERITY: Severity = "deny";
|
|
12
|
-
|
|
13
8
|
const MESSAGES: Readonly<Record<DependencySection, string>> = {
|
|
14
9
|
dependency: "no import in the target needs the declared dependency",
|
|
15
10
|
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
16
11
|
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
17
12
|
};
|
|
18
13
|
|
|
19
|
-
/** An unused-dependency finding, with the section that declares the dependency. */
|
|
20
|
-
export interface UnusedDependencyFinding extends Finding {
|
|
21
|
-
readonly details: { readonly dependencyClass: DependencySection };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
/**
|
|
25
15
|
* The dependencies the target needs: every package the projects' uses name, every
|
|
26
16
|
* declared dependency that ships a command, which a script, a workflow or a shell
|
|
@@ -48,14 +38,9 @@ function neededBy(dependencies: Dependencies): ReadonlySet<string> {
|
|
|
48
38
|
|
|
49
39
|
/**
|
|
50
40
|
* A dependency, development dependency or peer dependency of the target's manifest
|
|
51
|
-
* that the target does not need, at the position of its key.
|
|
52
|
-
* `allow`. Every subject is a manifest entry, which is `certain` whatever the
|
|
53
|
-
* consumers, so no minimum confidence withholds one.
|
|
41
|
+
* that the target does not need, at the position of its key.
|
|
54
42
|
*/
|
|
55
|
-
function unusedDependencies(
|
|
56
|
-
if (severityOf(config, UNUSED_DEPENDENCY, UNUSED_DEPENDENCY_SEVERITY) === "allow") {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
43
|
+
function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
59
44
|
const needed = neededBy(dependencies);
|
|
60
45
|
return dependencies.declared
|
|
61
46
|
.filter((dependency) => !needed.has(dependency.name))
|
|
@@ -78,5 +63,5 @@ function unusedDependencies(config: Config, dependencies: Dependencies): UnusedD
|
|
|
78
63
|
}
|
|
79
64
|
|
|
80
65
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
81
|
-
export const dependenciesAndModuleMachinery: Emitter = ({
|
|
82
|
-
unusedDependencies(
|
|
66
|
+
export const dependenciesAndModuleMachinery: Emitter = ({ dependencies }) =>
|
|
67
|
+
unusedDependencies(dependencies);
|