@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.2
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 +108 -10
- package/dist/src/augmentations.js +62 -18
- package/dist/src/automatic-types.js +189 -0
- package/dist/src/base-url.js +18 -0
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +50 -12
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +98 -0
- package/dist/src/conventions.js +101 -5
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +183 -28
- package/dist/src/discover.js +132 -11
- package/dist/src/entry-point-gaps.js +7 -10
- package/dist/src/entry-points.js +112 -11
- package/dist/src/findings/completion.js +172 -40
- package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +44 -13
- package/dist/src/findings/non-code-artifacts.js +66 -33
- package/dist/src/findings/unused-declarations.js +30 -5
- package/dist/src/findings/visibility-narrowing.js +29 -5
- package/dist/src/generated-file.js +11 -0
- package/dist/src/git-ignore.js +86 -0
- package/dist/src/graph.js +4 -1
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +297 -45
- package/dist/src/inventory.js +52 -8
- package/dist/src/kinds.js +13 -13
- package/dist/src/matrix.js +57 -10
- package/dist/src/mdx-entries.js +197 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/overrides.js +41 -10
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +38 -4
- package/dist/src/report.js +4 -2
- package/dist/src/resolve.js +2 -0
- package/dist/src/roots.js +110 -38
- package/dist/src/schema.js +1 -0
- package/dist/src/serialization-contract.js +54 -8
- package/dist/src/session.js +7 -0
- package/dist/src/source-files.js +39 -14
- package/dist/src/suppress-file.js +10 -2
- package/dist/src/suppress-inline.js +18 -2
- package/dist/src/suppress.js +8 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +119 -18
- package/dist/src/type-errors.js +34 -7
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/verbs/analyze.js +16 -0
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +119 -22
- package/dist/src/workspace-programs.js +7 -3
- package/dist/src/workspace-resolution.js +53 -10
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +146 -11
- package/src/augmentations.ts +76 -22
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +66 -12
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +128 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +247 -41
- package/src/discover.ts +156 -13
- package/src/entry-point-gaps.ts +8 -11
- package/src/entry-points.ts +133 -11
- package/src/finding.ts +15 -0
- package/src/findings/completion.ts +222 -46
- package/src/findings/dependencies-and-module-machinery.ts +90 -28
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +56 -14
- package/src/findings/non-code-artifacts.ts +81 -41
- package/src/findings/unused-declarations.ts +36 -5
- package/src/findings/visibility-narrowing.ts +40 -5
- package/src/generated-file.ts +16 -0
- package/src/git-ignore.ts +98 -0
- package/src/graph.ts +6 -0
- package/src/host.ts +5 -9
- package/src/import-bindings.ts +128 -0
- package/src/indexed-access.ts +152 -30
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +394 -59
- package/src/inventory.ts +71 -7
- package/src/kinds.ts +13 -13
- package/src/matrix.ts +104 -15
- package/src/mdx-entries.ts +241 -0
- package/src/module-declarations.ts +11 -13
- package/src/overrides.ts +51 -12
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +48 -3
- package/src/report.ts +16 -3
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +55 -4
- package/src/session.ts +7 -0
- package/src/source-files.ts +66 -16
- package/src/suppress-file.ts +12 -1
- package/src/suppress-inline.ts +18 -1
- package/src/suppress.ts +10 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +155 -18
- package/src/type-errors.ts +38 -6
- package/src/type-parameter-reads.ts +227 -0
- package/src/verbs/analyze.ts +22 -0
- package/src/verbs/explain.ts +5 -2
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +127 -22
- package/src/workspace-programs.ts +7 -3
- package/src/workspace-resolution.ts +62 -10
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { RunSweep } from "../analysis.ts";
|
|
8
8
|
import { componentId, type Component } from "../components.ts";
|
|
9
|
+
import { deadComponents } from "../matrix.ts";
|
|
9
10
|
import type { Cascade, Confidence, Severity } from "../config.ts";
|
|
10
11
|
import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
|
|
11
12
|
import { OUTSIDE, type Graph } from "../graph.ts";
|
|
@@ -15,9 +16,26 @@ import { byPosition, positionKey } from "../position.ts";
|
|
|
15
16
|
import { declarationKey } from "../ref.ts";
|
|
16
17
|
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
17
18
|
import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
|
|
19
|
+
import type { Candidate } from "../sweep.ts";
|
|
18
20
|
import type { Boundary } from "./boundary.ts";
|
|
19
21
|
import type { EmitterInput } from "./emitter.ts";
|
|
20
22
|
|
|
23
|
+
/** The unused-file code, whose deletion removes every use its file writes. */
|
|
24
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The codes whose subject the run that counts test references holds live, as
|
|
28
|
+
* finding.schema.json lists them, so a test file's candidate reported under one carries
|
|
29
|
+
* none of a dead declaration's sweep facts.
|
|
30
|
+
*/
|
|
31
|
+
const LIVE_SUBJECT: ReadonlySet<string> = new Set([
|
|
32
|
+
"DS1101",
|
|
33
|
+
"DS1102",
|
|
34
|
+
"DS1104",
|
|
35
|
+
"DS1204",
|
|
36
|
+
"DS1301",
|
|
37
|
+
]);
|
|
38
|
+
|
|
21
39
|
/** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
|
|
22
40
|
const MANUAL = "manual";
|
|
23
41
|
|
|
@@ -94,17 +112,11 @@ function testedByIn(input: EmitterInput): ReadonlySet<string> {
|
|
|
94
112
|
* published API, a public member of a published type included, is `certain` where the
|
|
95
113
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
96
114
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
97
|
-
* consumer set.
|
|
98
|
-
* rule, what it contains, and a member an `in` expression tests by name are `possible`.
|
|
115
|
+
* consumer set. A member an `in` expression tests by name is `possible`.
|
|
99
116
|
*/
|
|
100
117
|
function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
101
118
|
const tested = testedByIn(input);
|
|
102
119
|
const published = publishedIds(input.swept.matrix.union);
|
|
103
|
-
const usedByTests = new Set(
|
|
104
|
-
input.swept.sweep.candidates
|
|
105
|
-
.filter((candidate) => candidate.heldByTests !== undefined)
|
|
106
|
-
.map((candidate) => candidate.id),
|
|
107
|
-
);
|
|
108
120
|
const { declared } = input.boundary.consumers;
|
|
109
121
|
const outside: Confidence =
|
|
110
122
|
declared.length === 0
|
|
@@ -113,13 +125,27 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
113
125
|
? "certain"
|
|
114
126
|
: "probable";
|
|
115
127
|
return (id) => {
|
|
116
|
-
if (
|
|
128
|
+
if (tested.has(id)) {
|
|
117
129
|
return "possible";
|
|
118
130
|
}
|
|
119
131
|
return published.has(id) ? outside : "certain";
|
|
120
132
|
};
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
/** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
|
|
136
|
+
function testTargets(union: Graph, id: string): readonly string[] {
|
|
137
|
+
return (union.out[union.at(id)] ?? []).flatMap((edge) => {
|
|
138
|
+
const target = union.symbols[edge.to];
|
|
139
|
+
const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
|
|
140
|
+
return target !== undefined &&
|
|
141
|
+
!testCode &&
|
|
142
|
+
union.ambient[edge.to] !== true &&
|
|
143
|
+
union.subject[edge.to] === true
|
|
144
|
+
? [target.id]
|
|
145
|
+
: [];
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
123
149
|
/**
|
|
124
150
|
* The confidence ceiling of a test of dead code: the lowest reachability class among the
|
|
125
151
|
* target symbols it references, because its deadness is theirs, whether or not they share
|
|
@@ -133,20 +159,55 @@ function testCeilings(
|
|
|
133
159
|
const tests = new Set(
|
|
134
160
|
input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
|
|
135
161
|
);
|
|
136
|
-
return (id) =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
return (id) =>
|
|
163
|
+
tests.has(id)
|
|
164
|
+
? testTargets(union, id).reduce<Confidence>(
|
|
165
|
+
(lowest, target) => capped(lowest, classOf(target)),
|
|
166
|
+
"certain",
|
|
167
|
+
)
|
|
168
|
+
: "certain";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The subjects dead only because production code only tests use is: each declaration the
|
|
173
|
+
* sweep marks test-used, each `DS1502` file of such code, and each test of dead code every
|
|
174
|
+
* target of which is one. Nothing they hold references a dead declaration outside them, so
|
|
175
|
+
* the rest of a component they share is reported without them.
|
|
176
|
+
*/
|
|
177
|
+
function testUsedSubjects(input: EmitterInput): {
|
|
178
|
+
readonly declarations: ReadonlySet<string>;
|
|
179
|
+
readonly files: ReadonlySet<string>;
|
|
180
|
+
} {
|
|
181
|
+
const { matrix, sweep } = input.swept;
|
|
182
|
+
const union = matrix.union;
|
|
183
|
+
const used = new Set(sweep.candidates.filter((one) => one.testUsed).map((one) => one.id));
|
|
184
|
+
for (const test of sweep.candidates.filter((one) => one.testOfDeadCode)) {
|
|
185
|
+
const targets = testTargets(union, test.id);
|
|
186
|
+
if (targets.length > 0 && targets.every((target) => used.has(target))) {
|
|
187
|
+
used.add(test.id);
|
|
147
188
|
}
|
|
148
|
-
|
|
149
|
-
|
|
189
|
+
}
|
|
190
|
+
const files = new Set(
|
|
191
|
+
union.symbols.flatMap((symbol, at) =>
|
|
192
|
+
matrix.testUsed[at] === true ? [symbol.position.path] : [],
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
return { declarations: used, files };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The ceiling the roots the run found put on one dead subject: a subject other declarations
|
|
200
|
+
* reference, found dead only because no root reaches them, is `probable` unless
|
|
201
|
+
* the configuration declares the roots complete, because a root the analysis cannot see
|
|
202
|
+
* would make it live; every other declaration has none below `certain`.
|
|
203
|
+
*/
|
|
204
|
+
function rootsCeiling(
|
|
205
|
+
input: EmitterInput,
|
|
206
|
+
finding: Finding,
|
|
207
|
+
candidate: Candidate | undefined,
|
|
208
|
+
): Confidence {
|
|
209
|
+
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
210
|
+
return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
|
|
150
211
|
}
|
|
151
212
|
|
|
152
213
|
/**
|
|
@@ -155,10 +216,11 @@ function testCeilings(
|
|
|
155
216
|
*/
|
|
156
217
|
function findingComponents(
|
|
157
218
|
swept: RunSweep,
|
|
219
|
+
components: readonly Component[],
|
|
158
220
|
cascade: Cascade,
|
|
159
221
|
): (id: string) => FindingComponent | undefined {
|
|
160
222
|
const held = new Map<string, Component>();
|
|
161
|
-
for (const component of
|
|
223
|
+
for (const component of components) {
|
|
162
224
|
for (const member of component.members) {
|
|
163
225
|
held.set(member, component);
|
|
164
226
|
}
|
|
@@ -196,6 +258,77 @@ function findingComponents(
|
|
|
196
258
|
};
|
|
197
259
|
}
|
|
198
260
|
|
|
261
|
+
/**
|
|
262
|
+
* The run's dead components with each file a `DS1502` finding reports dead among them, so
|
|
263
|
+
* the file's declarations fall with it, and with the components each subject that is no
|
|
264
|
+
* declaration falls with joined into one.
|
|
265
|
+
*/
|
|
266
|
+
function runComponents(
|
|
267
|
+
swept: RunSweep,
|
|
268
|
+
findings: readonly Finding[],
|
|
269
|
+
): {
|
|
270
|
+
readonly components: readonly Component[];
|
|
271
|
+
readonly computed: number;
|
|
272
|
+
readonly files: ReadonlyMap<string, string>;
|
|
273
|
+
} {
|
|
274
|
+
const union = swept.matrix.union;
|
|
275
|
+
const reported = new Set(
|
|
276
|
+
findings.filter((one) => one.code === FILE_NEVER_IMPORTED).map((one) => one.position.path),
|
|
277
|
+
);
|
|
278
|
+
const files = new Map(
|
|
279
|
+
union.symbols.flatMap((symbol, at) =>
|
|
280
|
+
symbol.kind === "file" && reported.has(symbol.position.path) ? [[at, symbol] as const] : [],
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
const computed =
|
|
284
|
+
files.size === 0
|
|
285
|
+
? swept.sweep.components
|
|
286
|
+
: deadComponents(swept.matrix, swept.sweep, new Set(files.keys()));
|
|
287
|
+
const place = new Map<string, number>();
|
|
288
|
+
computed.forEach((component, at) => {
|
|
289
|
+
for (const member of component.members) {
|
|
290
|
+
place.set(member, at);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
const leader = computed.map((_component, at) => at);
|
|
294
|
+
const find = (at: number): number => {
|
|
295
|
+
let top = at;
|
|
296
|
+
while ((leader[top] ?? top) !== top) {
|
|
297
|
+
top = leader[top] ?? top;
|
|
298
|
+
}
|
|
299
|
+
return top;
|
|
300
|
+
};
|
|
301
|
+
for (const { fallsWith } of findings) {
|
|
302
|
+
const joined = (fallsWith ?? []).flatMap((id) => place.get(id) ?? []).map(find);
|
|
303
|
+
const first = Math.min(...joined);
|
|
304
|
+
for (const one of joined) {
|
|
305
|
+
leader[one] = first;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const merged = new Map<number, Component>();
|
|
309
|
+
computed.forEach((component, at) => {
|
|
310
|
+
const top = find(at);
|
|
311
|
+
const into = merged.get(top);
|
|
312
|
+
merged.set(
|
|
313
|
+
top,
|
|
314
|
+
into === undefined
|
|
315
|
+
? component
|
|
316
|
+
: {
|
|
317
|
+
id: into.id,
|
|
318
|
+
members: [...into.members, ...component.members],
|
|
319
|
+
roots: [...into.roots, ...component.roots],
|
|
320
|
+
deletableLines: into.deletableLines + component.deletableLines,
|
|
321
|
+
held: into.held && component.held,
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
components: [...merged.values()],
|
|
327
|
+
computed: computed.length,
|
|
328
|
+
files: new Map([...files.values()].map((symbol) => [symbol.position.path, symbol.id])),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
199
332
|
/**
|
|
200
333
|
* Every finding of the run completed, in the order given. A dead declaration carries its
|
|
201
334
|
* candidate's relation, counts, configurations and component; a live one carries no relation and
|
|
@@ -217,12 +350,13 @@ export function completed(
|
|
|
217
350
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
218
351
|
const classOf = reachabilityClasses(input);
|
|
219
352
|
const ceilingOf = testCeilings(input, classOf);
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
|
|
353
|
+
const testUsed = testUsedSubjects(input);
|
|
354
|
+
const { components, computed, files } = runComponents(swept, findings);
|
|
355
|
+
const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
|
|
356
|
+
const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
|
|
357
|
+
// A join keeps its first component's identifier, so a minted one counts past every computed one.
|
|
358
|
+
let minted = computed;
|
|
359
|
+
const dead = new Set(components.map((component) => component.id));
|
|
226
360
|
const fallingWith = new Map<string, number>();
|
|
227
361
|
for (const { heldByTests } of swept.sweep.candidates) {
|
|
228
362
|
if (heldByTests !== undefined) {
|
|
@@ -239,18 +373,33 @@ export function completed(
|
|
|
239
373
|
// a row is still never the declaration. Two declarations of one file can share a
|
|
240
374
|
// reference, so the one the finding is positioned at is the subject where there is one.
|
|
241
375
|
const declared = union.at(positionKey(finding.position));
|
|
376
|
+
const ref = finding.symbol.holder ?? finding.symbol.ref;
|
|
242
377
|
const at = isRowSubject(finding.symbol.kind)
|
|
243
378
|
? OUTSIDE
|
|
244
|
-
: union.symbols[declared]?.ref ===
|
|
379
|
+
: union.symbols[declared]?.ref === ref
|
|
245
380
|
? declared
|
|
246
|
-
: (positions.get(declarationKey(
|
|
381
|
+
: (positions.get(declarationKey(ref, finding.position.path)) ?? OUTSIDE);
|
|
247
382
|
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
248
383
|
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
249
384
|
const part = isPartSubject(finding.symbol.kind);
|
|
250
385
|
const symbol = union.symbols[at];
|
|
251
386
|
const judged = !part && union.subject[at] === true ? symbol : undefined;
|
|
252
|
-
const candidate =
|
|
253
|
-
|
|
387
|
+
const candidate =
|
|
388
|
+
judged === undefined || LIVE_SUBJECT.has(finding.code)
|
|
389
|
+
? undefined
|
|
390
|
+
: candidates.get(judged.id);
|
|
391
|
+
const fallsWith = finding.fallsWith?.[0];
|
|
392
|
+
const joined = fallsWith === undefined ? undefined : componentOf(fallsWith);
|
|
393
|
+
const file =
|
|
394
|
+
finding.code === FILE_NEVER_IMPORTED ? files.get(finding.position.path) : undefined;
|
|
395
|
+
const found =
|
|
396
|
+
joined !== undefined
|
|
397
|
+
? { ...joined, root: false }
|
|
398
|
+
: file !== undefined
|
|
399
|
+
? componentOf(file)
|
|
400
|
+
: symbol === undefined || part
|
|
401
|
+
? undefined
|
|
402
|
+
: componentOf(symbol.id);
|
|
254
403
|
// A test file's declaration is a component member only as a test of dead code, and
|
|
255
404
|
// test code keeps the test-support code it references out of every component.
|
|
256
405
|
if (
|
|
@@ -275,8 +424,17 @@ export function completed(
|
|
|
275
424
|
candidate === undefined
|
|
276
425
|
? union.made[at]
|
|
277
426
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
278
|
-
const
|
|
427
|
+
const ownClass =
|
|
279
428
|
judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
|
|
429
|
+
const used =
|
|
430
|
+
judged !== undefined
|
|
431
|
+
? candidate !== undefined && testUsed.declarations.has(judged.id)
|
|
432
|
+
: finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
|
|
433
|
+
const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
|
|
434
|
+
const untested = capped(
|
|
435
|
+
capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate)),
|
|
436
|
+
judged === undefined ? "certain" : ceilingOf(judged.id),
|
|
437
|
+
);
|
|
280
438
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
281
439
|
const fixability = generated
|
|
282
440
|
? "none"
|
|
@@ -287,9 +445,13 @@ export function completed(
|
|
|
287
445
|
? MANUAL
|
|
288
446
|
: row.fixability;
|
|
289
447
|
const lastUses =
|
|
290
|
-
fixability
|
|
291
|
-
?
|
|
292
|
-
: undefined
|
|
448
|
+
fixability !== "deletable"
|
|
449
|
+
? undefined
|
|
450
|
+
: candidate !== undefined
|
|
451
|
+
? input.dependencies.lastUses.get(candidate.id)
|
|
452
|
+
: finding.code === FILE_NEVER_IMPORTED
|
|
453
|
+
? input.dependencies.fileLastUses.get(finding.position.path)
|
|
454
|
+
: undefined;
|
|
293
455
|
return {
|
|
294
456
|
code: finding.code,
|
|
295
457
|
kind: row.name,
|
|
@@ -297,10 +459,8 @@ export function completed(
|
|
|
297
459
|
position: finding.position,
|
|
298
460
|
symbol: finding.symbol,
|
|
299
461
|
reachabilityClass,
|
|
300
|
-
confidence:
|
|
301
|
-
|
|
302
|
-
judged === undefined ? "certain" : ceilingOf(judged.id),
|
|
303
|
-
),
|
|
462
|
+
confidence: used ? "possible" : untested,
|
|
463
|
+
...(used ? { componentConfidence: untested } : {}),
|
|
304
464
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
305
465
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
306
466
|
generated,
|
|
@@ -327,7 +487,7 @@ export function completed(
|
|
|
327
487
|
/**
|
|
328
488
|
* The findings with every finding of a dead component capped by the lowest confidence
|
|
329
489
|
* among the component's root members, so the minimum confidence reports or withholds a
|
|
330
|
-
* component whole.
|
|
490
|
+
* component whole but for the findings test-used code lowers on their own.
|
|
331
491
|
*/
|
|
332
492
|
function componentCapped(
|
|
333
493
|
findings: readonly CompletedFinding[],
|
|
@@ -338,17 +498,29 @@ function componentCapped(
|
|
|
338
498
|
const { id, root } = finding.component;
|
|
339
499
|
if (root && dead.has(id)) {
|
|
340
500
|
const held = lowest.get(id);
|
|
341
|
-
|
|
501
|
+
const own = componentConfidenceOf(finding);
|
|
502
|
+
lowest.set(id, held === undefined ? own : capped(held, own));
|
|
342
503
|
}
|
|
343
504
|
}
|
|
344
505
|
return findings.map((finding) => {
|
|
345
506
|
const cap = lowest.get(finding.component.id);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
507
|
+
if (cap === undefined) {
|
|
508
|
+
return finding;
|
|
509
|
+
}
|
|
510
|
+
const own = finding.componentConfidence;
|
|
511
|
+
return {
|
|
512
|
+
...finding,
|
|
513
|
+
confidence: capped(finding.confidence, cap),
|
|
514
|
+
...(own === undefined ? {} : { componentConfidence: capped(own, cap) }),
|
|
515
|
+
};
|
|
349
516
|
});
|
|
350
517
|
}
|
|
351
518
|
|
|
519
|
+
/** The confidence a finding gives its component's cap and the dials. */
|
|
520
|
+
function componentConfidenceOf(finding: CompletedFinding): Confidence {
|
|
521
|
+
return finding.componentConfidence ?? finding.confidence;
|
|
522
|
+
}
|
|
523
|
+
|
|
352
524
|
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
353
525
|
function dialed(
|
|
354
526
|
finding: Pick<CompletedFinding, "severity" | "confidence">,
|
|
@@ -371,7 +543,11 @@ export function dialsOf(
|
|
|
371
543
|
const minConfidence = input.config.analysis.minConfidence;
|
|
372
544
|
const components = new Set(
|
|
373
545
|
findings
|
|
374
|
-
.filter(
|
|
546
|
+
.filter(
|
|
547
|
+
(finding) =>
|
|
548
|
+
finding.component.root &&
|
|
549
|
+
dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence),
|
|
550
|
+
)
|
|
375
551
|
.map((finding) => finding.component.id),
|
|
376
552
|
);
|
|
377
553
|
return {
|
|
@@ -1,48 +1,110 @@
|
|
|
1
1
|
import { declarationKey, type Dependencies } from "../dependencies.ts";
|
|
2
2
|
import type { Finding } from "../finding.ts";
|
|
3
3
|
import { renderRef, type DependencySection } from "../ref.ts";
|
|
4
|
-
import type { Emitter } from "./emitter.ts";
|
|
4
|
+
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
5
|
+
import { nonCodeArtifacts } from "./non-code-artifacts.ts";
|
|
5
6
|
|
|
6
7
|
const UNUSED_DEPENDENCY = "DS1601";
|
|
7
8
|
|
|
9
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
10
|
+
|
|
8
11
|
const MESSAGES: Readonly<Record<DependencySection, string>> = {
|
|
9
12
|
dependency: "no import in the target needs the declared dependency",
|
|
10
13
|
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
11
14
|
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
12
15
|
};
|
|
13
16
|
|
|
17
|
+
const DEAD_USERS_MESSAGES: Readonly<Record<DependencySection, string>> = {
|
|
18
|
+
dependency: "only code that is itself dead uses the declared dependency",
|
|
19
|
+
"dev-dependency": "only code that is itself dead uses the declared development dependency",
|
|
20
|
+
"peer-dependency": "only code that is itself dead uses the declared peer dependency",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** How the run judged the declarations and files a dependency's uses may sit in. */
|
|
24
|
+
interface DeadCode {
|
|
25
|
+
/** The members of the dead components, and the top level of each file reported unimported. */
|
|
26
|
+
readonly dead: ReadonlySet<string>;
|
|
27
|
+
/** Those found dead under reachability alone. */
|
|
28
|
+
readonly reachedOnly: ReadonlySet<string>;
|
|
29
|
+
/** The declarations of a library's published API, whose findings sit at the consumers' class. */
|
|
30
|
+
readonly published: ReadonlySet<string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The run's dead code: test code keeps the code it references out of every dead component
|
|
35
|
+
* unless that code is dead as well, and a file's top level is dead where the file is.
|
|
36
|
+
*/
|
|
37
|
+
function deadCode(input: EmitterInput): DeadCode {
|
|
38
|
+
const { swept } = input;
|
|
39
|
+
const union = swept.matrix.union;
|
|
40
|
+
const files = new Map(
|
|
41
|
+
union.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]),
|
|
42
|
+
);
|
|
43
|
+
const dead = new Set(swept.sweep.components.flatMap((component) => component.members));
|
|
44
|
+
const reachedOnly = new Set(
|
|
45
|
+
swept.sweep.candidates.filter((one) => one.relation === "reachability").map((one) => one.id),
|
|
46
|
+
);
|
|
47
|
+
for (const finding of nonCodeArtifacts(input)) {
|
|
48
|
+
const id = files.get(finding.position.path);
|
|
49
|
+
if (finding.code === FILE_NEVER_IMPORTED && id !== undefined) {
|
|
50
|
+
dead.add(id);
|
|
51
|
+
if (finding.unreachableOnly === true) {
|
|
52
|
+
reachedOnly.add(id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const published = new Set(
|
|
57
|
+
union.rooted
|
|
58
|
+
.filter((root) => root.kind === "published-api")
|
|
59
|
+
.flatMap((root) => union.symbols[root.at]?.id ?? []),
|
|
60
|
+
);
|
|
61
|
+
return { dead, reachedOnly, published };
|
|
62
|
+
}
|
|
63
|
+
|
|
14
64
|
/**
|
|
15
65
|
* A dependency, development dependency or peer dependency of a manifest the run reads
|
|
16
|
-
* that the target does not need, at the position of its key
|
|
66
|
+
* that the target does not need, at the position of its key, and one whose every use is
|
|
67
|
+
* held by dead code, which is dead only through that code and falls with its component:
|
|
68
|
+
* at `possible` where one of its holders is published, and found under reachability alone
|
|
69
|
+
* where one of them is.
|
|
17
70
|
*/
|
|
18
|
-
function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
function unusedDependencies(input: EmitterInput, dependencies: Dependencies): Finding[] {
|
|
72
|
+
const { dead, reachedOnly, published } = deadCode(input);
|
|
73
|
+
return dependencies.declared.flatMap((dependency) => {
|
|
74
|
+
const key = declarationKey(dependency);
|
|
75
|
+
const held = dependencies.heldBy.get(key);
|
|
76
|
+
const users = held?.every((id) => dead.has(id)) === true ? held : undefined;
|
|
77
|
+
if (dependencies.ran.has(key) || (dependencies.needed.has(key) && users === undefined)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const unbuilt = dependencies.unbuilt.has(dependency.position.path);
|
|
81
|
+
const atPossible = unbuilt || users?.some((id) => published.has(id)) === true;
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
code: UNUSED_DEPENDENCY,
|
|
85
|
+
position: dependency.position,
|
|
86
|
+
symbol: {
|
|
87
|
+
ref: renderRef(dependency.manifest, {
|
|
88
|
+
of: "dependency",
|
|
89
|
+
name: dependency.name,
|
|
90
|
+
section: dependency.section,
|
|
91
|
+
}),
|
|
92
|
+
kind: "dependency",
|
|
31
93
|
name: dependency.name,
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
94
|
+
sizeLines: 1,
|
|
95
|
+
},
|
|
96
|
+
message: (users === undefined ? MESSAGES : DEAD_USERS_MESSAGES)[dependency.section],
|
|
97
|
+
details: { dependencyClass: dependency.section },
|
|
98
|
+
...(atPossible ? { reachabilityClass: "possible" as const } : {}),
|
|
99
|
+
...(users?.some((id) => reachedOnly.has(id)) === true
|
|
100
|
+
? { unreachableOnly: true as const }
|
|
101
|
+
: {}),
|
|
102
|
+
...(users === undefined ? {} : { fallsWith: users }),
|
|
37
103
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
...(dependencies.unbuilt.has(dependency.position.path)
|
|
41
|
-
? { reachabilityClass: "possible" as const }
|
|
42
|
-
: {}),
|
|
43
|
-
}));
|
|
104
|
+
];
|
|
105
|
+
});
|
|
44
106
|
}
|
|
45
107
|
|
|
46
108
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
47
|
-
export const dependenciesAndModuleMachinery: Emitter = (
|
|
48
|
-
unusedDependencies(dependencies);
|
|
109
|
+
export const dependenciesAndModuleMachinery: Emitter = (input) =>
|
|
110
|
+
unusedDependencies(input, input.dependencies);
|
package/src/findings/emitters.ts
CHANGED
|
@@ -165,7 +165,7 @@ function dialledOver(
|
|
|
165
165
|
const unanswered = heldKeys(input);
|
|
166
166
|
const markup = markupKeys(input);
|
|
167
167
|
const candidates = found.filter((finding) => {
|
|
168
|
-
const key = declarationKey(finding.symbol.ref, finding.position.path);
|
|
168
|
+
const key = declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
|
|
169
169
|
return (
|
|
170
170
|
!ledger.withheld(finding) &&
|
|
171
171
|
!unanswered.has(key) &&
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { Confidence } from "../config.ts";
|
|
1
2
|
import type { Finding } from "../finding.ts";
|
|
2
3
|
import { KINDS } from "../kinds.ts";
|
|
4
|
+
import { parameterRef } from "../ref.ts";
|
|
3
5
|
import { UNUSED_PARAMETER, UNUSED_RESULT, type PartFact } from "../intra-function-parts.ts";
|
|
4
6
|
import { reachedWithTheTests } from "../sweep.ts";
|
|
5
7
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
@@ -12,29 +14,56 @@ function spelled(rules: readonly string[]): string {
|
|
|
12
14
|
return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
/** The
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/** The compiler's rule of the parameter kind's overlap list, which reports only in a file it checks. */
|
|
18
|
+
const COMPILER_PARAMETER_RULE = "tsc --noUnusedParameters";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The clause a message ends with: the other rules that report the part, or that none is
|
|
22
|
+
* known. A parameter of a file the compiler does not check is no report of the compiler's.
|
|
23
|
+
*/
|
|
24
|
+
function overlapClause(overlap: readonly string[], part: PartFact): string {
|
|
25
|
+
const rules =
|
|
26
|
+
part.parameter?.checked === false
|
|
27
|
+
? overlap.filter((rule) => rule !== COMPILER_PARAMETER_RULE)
|
|
28
|
+
: overlap;
|
|
29
|
+
return rules.length === 1 && rules[0] === "none known"
|
|
18
30
|
? "no other rule is known to report it"
|
|
19
|
-
: `also reported by ${spelled(
|
|
31
|
+
: `also reported by ${spelled(rules)}`;
|
|
20
32
|
}
|
|
21
33
|
|
|
34
|
+
/** What a parameter's finding asks: its deletion, or, where a read parameter follows it, its rename. */
|
|
35
|
+
type ParameterVerdict =
|
|
36
|
+
| { readonly fix: "delete"; readonly confidence: Confidence }
|
|
37
|
+
| { readonly fix: "rename" }
|
|
38
|
+
| undefined;
|
|
39
|
+
|
|
22
40
|
/**
|
|
23
|
-
* Whether one parameter
|
|
24
|
-
*
|
|
25
|
-
*
|
|
41
|
+
* Whether one unread parameter is reported, and how. Of a free signature it is deleted. A
|
|
42
|
+
* caller may pass a function used as a value more arguments than it declares, so there one
|
|
43
|
+
* a read parameter follows is renamed to an underscore name, which the compilers read as
|
|
44
|
+
* declared unread, and one no read parameter follows is deleted: kept where the call's
|
|
45
|
+
* inference may read its annotation, at `probable` where nothing reads the annotation.
|
|
26
46
|
*/
|
|
27
|
-
function
|
|
47
|
+
function parameterVerdict(
|
|
28
48
|
part: PartFact,
|
|
29
49
|
free: ReadonlySet<string>,
|
|
30
50
|
freeButValued: ReadonlySet<string>,
|
|
31
|
-
):
|
|
51
|
+
): ParameterVerdict {
|
|
32
52
|
const place = part.parameter;
|
|
33
53
|
if (place?.inValue !== true && free.has(part.declaration)) {
|
|
34
|
-
return
|
|
54
|
+
return place?.annotated === undefined ? { fix: "delete", confidence: "certain" } : undefined;
|
|
35
55
|
}
|
|
36
56
|
const valued = place?.inValue === true || freeButValued.has(part.declaration);
|
|
37
|
-
|
|
57
|
+
if (!valued) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (place?.beforeARead === true) {
|
|
61
|
+
return { fix: "rename" };
|
|
62
|
+
}
|
|
63
|
+
if (place?.annotated === "steers") {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return { fix: "delete", confidence: place?.annotated === "free" ? "probable" : "certain" };
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
/**
|
|
@@ -55,7 +84,7 @@ function reported(
|
|
|
55
84
|
const { free, freeButValued, discardedEverywhere } = input.intraFunction;
|
|
56
85
|
switch (part.code) {
|
|
57
86
|
case UNUSED_PARAMETER:
|
|
58
|
-
return
|
|
87
|
+
return parameterVerdict(part, free, freeButValued) !== undefined;
|
|
59
88
|
case UNUSED_RESULT:
|
|
60
89
|
return (
|
|
61
90
|
free.has(part.declaration) &&
|
|
@@ -119,20 +148,33 @@ export const intraFunction: Emitter = (input) => {
|
|
|
119
148
|
if (declaration === undefined || !judged(declaration.id) || !reported(input, part, unknown)) {
|
|
120
149
|
continue;
|
|
121
150
|
}
|
|
151
|
+
const verdict =
|
|
152
|
+
part.code === UNUSED_PARAMETER
|
|
153
|
+
? parameterVerdict(part, input.intraFunction.free, input.intraFunction.freeButValued)
|
|
154
|
+
: undefined;
|
|
155
|
+
const message =
|
|
156
|
+
verdict?.fix === "rename"
|
|
157
|
+
? `${part.message}, and a read parameter follows it, so rename it to _${part.name}`
|
|
158
|
+
: part.message;
|
|
122
159
|
found.push({
|
|
123
160
|
code: part.code,
|
|
124
161
|
position: part.position,
|
|
125
162
|
symbol: {
|
|
126
|
-
|
|
163
|
+
...(part.code === UNUSED_PARAMETER
|
|
164
|
+
? { ref: parameterRef(declaration.ref, part.name), holder: declaration.ref }
|
|
165
|
+
: { ref: declaration.ref }),
|
|
127
166
|
kind: part.kind,
|
|
128
167
|
name: part.name,
|
|
129
168
|
sizeLines: part.position.endLine - part.position.line + 1,
|
|
130
169
|
},
|
|
131
|
-
message: `${
|
|
170
|
+
message: `${message}; ${overlapClause(overlap, part)}`,
|
|
132
171
|
details: {
|
|
133
172
|
...(part.kind === "store" ? { writePositions: [part.position] } : {}),
|
|
134
173
|
overlap,
|
|
135
174
|
},
|
|
175
|
+
...(verdict?.fix === "delete" && verdict.confidence !== "certain"
|
|
176
|
+
? { reachabilityClass: verdict.confidence }
|
|
177
|
+
: {}),
|
|
136
178
|
});
|
|
137
179
|
}
|
|
138
180
|
return found;
|