@cplieger/deadset-ts 8.0.0-dev.1 → 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 +3 -1
- package/dist/src/augmentations.js +44 -13
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/convention-rows.js +23 -0
- package/dist/src/findings/completion.js +61 -24
- package/dist/src/findings/unused-declarations.js +9 -4
- package/dist/src/findings/visibility-narrowing.js +8 -3
- package/dist/src/kinds.js +2 -2
- package/dist/src/matrix.js +39 -1
- package/dist/src/references.js +34 -3
- package/dist/src/suppress.js +1 -0
- package/dist/src/test-support.js +8 -11
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +3 -1
- package/src/augmentations.ts +59 -16
- package/src/conformance-record.ts +1 -1
- package/src/convention-rows.ts +25 -0
- package/src/finding.ts +5 -0
- package/src/findings/completion.ts +82 -29
- package/src/findings/unused-declarations.ts +11 -4
- package/src/findings/visibility-narrowing.ts +9 -3
- package/src/kinds.ts +2 -2
- package/src/matrix.ts +72 -1
- package/src/references.ts +38 -2
- package/src/suppress.ts +1 -0
- package/src/test-support.ts +17 -13
package/src/analysis.ts
CHANGED
|
@@ -977,7 +977,7 @@ function readRun<Extra>(
|
|
|
977
977
|
}));
|
|
978
978
|
// Test-support code is code only test code imports in any configuration of the run: a
|
|
979
979
|
// configuration that holds no test file says nothing about what the tests import.
|
|
980
|
-
const support = testSupportFiles(
|
|
980
|
+
const { support, testUsed } = testSupportFiles(
|
|
981
981
|
withOutside.flatMap((one) => one.projectRead.held.symbols),
|
|
982
982
|
withOutside.flatMap((one) => one.references),
|
|
983
983
|
withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability),
|
|
@@ -999,6 +999,7 @@ function readRun<Extra>(
|
|
|
999
999
|
const made = supportReferences([...one.references, ...(satisfied?.references ?? [])], support);
|
|
1000
1000
|
const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
|
|
1001
1001
|
const supportFiles = [...support].filter((path) => held.has(path)).sort();
|
|
1002
|
+
const testUsedFiles = [...testUsed].filter((path) => held.has(path)).sort();
|
|
1002
1003
|
const configured: Configured = {
|
|
1003
1004
|
configuration: projectRead.configuration,
|
|
1004
1005
|
symbols: projectRead.held.symbols,
|
|
@@ -1006,6 +1007,7 @@ function readRun<Extra>(
|
|
|
1006
1007
|
roots: projectRead.rooted.liveUnderReachability,
|
|
1007
1008
|
testFiles: [...one.testFiles, ...supportFiles],
|
|
1008
1009
|
...(supportFiles.length === 0 ? {} : { supportFiles }),
|
|
1010
|
+
...(testUsedFiles.length === 0 ? {} : { testUsedFiles }),
|
|
1009
1011
|
...(projectRead.held.ambient.size === 0 ? {} : { ambient: projectRead.held.ambient }),
|
|
1010
1012
|
...(projectRead.referenceOnly.length === 0
|
|
1011
1013
|
? {}
|
package/src/augmentations.ts
CHANGED
|
@@ -26,25 +26,24 @@ function isGlobalBlock(
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
* file in `scripts
|
|
31
|
-
*
|
|
32
|
-
* files that are no module, by identifier.
|
|
29
|
+
* Per declaration, the interfaces and namespaces between it and the augmentation block or
|
|
30
|
+
* the file in `scripts` that holds it, with the block itself, or none where no such block
|
|
31
|
+
* or file holds it; and whether that holder is the global scope rather than a module
|
|
32
|
+
* declaration. `scripts` holds the files that are no module, by identifier.
|
|
33
33
|
*/
|
|
34
|
-
|
|
34
|
+
function holdersOf(
|
|
35
35
|
held: Inventory,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
): readonly Reference[] {
|
|
36
|
+
scripts: ReadonlySet<string>,
|
|
37
|
+
): ((id: string) => { readonly ids: readonly string[]; readonly global: boolean }) | undefined {
|
|
39
38
|
const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
|
|
40
39
|
const globals = new Set(
|
|
41
40
|
held.symbols.filter((one) => isGlobalBlock(one, held, files)).map((one) => one.id),
|
|
42
41
|
);
|
|
43
42
|
if (globals.size === 0 && held.modules.size === 0 && scripts.size === 0) {
|
|
44
|
-
return
|
|
43
|
+
return undefined;
|
|
45
44
|
}
|
|
46
45
|
const byId = new Map(held.symbols.map((one) => [one.id, one]));
|
|
47
|
-
|
|
46
|
+
return (id) => {
|
|
48
47
|
const ids: string[] = [];
|
|
49
48
|
for (
|
|
50
49
|
let at = byId.get(byId.get(id)?.parent ?? "");
|
|
@@ -52,29 +51,73 @@ export function augmentationReferences(
|
|
|
52
51
|
at = byId.get(at.parent)
|
|
53
52
|
) {
|
|
54
53
|
if (globals.has(at.id) || held.modules.has(at.id)) {
|
|
55
|
-
return [...ids, at.id];
|
|
54
|
+
return { ids: [...ids, at.id], global: globals.has(at.id) };
|
|
56
55
|
}
|
|
57
56
|
if (scripts.has(at.id)) {
|
|
58
|
-
return ids;
|
|
57
|
+
return { ids, global: true };
|
|
59
58
|
}
|
|
60
59
|
if (at.kind === "interface" || at.kind === "namespace") {
|
|
61
60
|
ids.push(at.id);
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
|
-
return [];
|
|
63
|
+
return { ids: [], global: false };
|
|
65
64
|
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The ambient interfaces of the global scope among `interfaces`: each one a declaration
|
|
69
|
+
* file or a `declare` holds that a `declare global` block, or the top level of a file in
|
|
70
|
+
* `scripts`, declares, directly or inside a namespace there.
|
|
71
|
+
*/
|
|
72
|
+
export function ambientGlobalInterfaces(
|
|
73
|
+
held: Inventory,
|
|
74
|
+
interfaces: readonly string[],
|
|
75
|
+
scripts: ReadonlySet<string>,
|
|
76
|
+
): readonly string[] {
|
|
77
|
+
const holders = holdersOf(held, scripts);
|
|
78
|
+
return holders === undefined
|
|
79
|
+
? []
|
|
80
|
+
: interfaces.filter((id) => held.ambient.has(id) && holders(id).global);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* For each reference to a declaration an augmentation block, or the global scope of a
|
|
85
|
+
* file in `scripts` (the files that are no module), holds, a read at the same place of
|
|
86
|
+
* every interface and namespace between it and the block or the file, and of the block
|
|
87
|
+
* itself. A read through an interface of `merged`, an ambient global interface merging
|
|
88
|
+
* into a language or library one, is a production read whoever writes it: it types a
|
|
89
|
+
* value the runtime provides, so a test reading a member needs it as code does.
|
|
90
|
+
*/
|
|
91
|
+
export function augmentationReferences(
|
|
92
|
+
held: Inventory,
|
|
93
|
+
found: readonly Reference[],
|
|
94
|
+
scripts: ReadonlySet<string> = new Set(),
|
|
95
|
+
merged: ReadonlySet<string> = new Set(),
|
|
96
|
+
): readonly Reference[] {
|
|
97
|
+
const holders = holdersOf(held, scripts);
|
|
98
|
+
if (holders === undefined) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const fileOf = new Map(
|
|
102
|
+
held.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]),
|
|
103
|
+
);
|
|
66
104
|
// Two uses at one place of declarations one block holds read the block once there.
|
|
67
105
|
const made = new Set(found.map(referenceKey));
|
|
68
106
|
const reads = new Map<string, Reference>();
|
|
69
107
|
for (const reference of found) {
|
|
70
|
-
|
|
108
|
+
const { ids } = holders(reference.to);
|
|
109
|
+
// A test's read through such an interface is held by the test file, which the test
|
|
110
|
+
// runner loads whichever of its declarations runs.
|
|
111
|
+
const typed = reference.test && ids.some((id) => merged.has(id));
|
|
112
|
+
const from = typed ? (fileOf.get(reference.position.path) ?? reference.from) : reference.from;
|
|
113
|
+
for (const to of ids) {
|
|
71
114
|
const read: Reference = {
|
|
72
|
-
from
|
|
115
|
+
from,
|
|
73
116
|
to,
|
|
74
117
|
position: reference.position,
|
|
75
118
|
use: "read",
|
|
76
119
|
resolution: reference.resolution,
|
|
77
|
-
test: reference.test,
|
|
120
|
+
test: reference.test && !typed,
|
|
78
121
|
};
|
|
79
122
|
const key = referenceKey(read);
|
|
80
123
|
if (!made.has(key) && !reads.has(key)) {
|
|
@@ -4,7 +4,7 @@ export const RECORD = {
|
|
|
4
4
|
conformance: {
|
|
5
5
|
corpusVersion: "6.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:d65daae4b7c95050c07896b166d0250d233cca71f97de75a6086c1d80fe76232",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|
package/src/convention-rows.ts
CHANGED
|
@@ -209,6 +209,31 @@ const ANGULAR_PACKAGE_KEYS: readonly PackageKey[] = [
|
|
|
209
209
|
"scripts",
|
|
210
210
|
].map((key) => ({ key, files: ["angular.json"] }));
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The test runners and the assertion, mock and runner libraries whose import is test
|
|
214
|
+
* evidence beside each runner a row reads test files for. Each applies whether or not a
|
|
215
|
+
* manifest declares it, so none is a row.
|
|
216
|
+
*/
|
|
217
|
+
export const TEST_LIBRARIES: readonly string[] = [
|
|
218
|
+
"@jest/globals",
|
|
219
|
+
"@playwright/test",
|
|
220
|
+
"ava",
|
|
221
|
+
"chai",
|
|
222
|
+
"expect",
|
|
223
|
+
"jasmine",
|
|
224
|
+
"jest",
|
|
225
|
+
"mocha",
|
|
226
|
+
"msw",
|
|
227
|
+
"nock",
|
|
228
|
+
"node:test",
|
|
229
|
+
"sinon",
|
|
230
|
+
"tap",
|
|
231
|
+
"uvu",
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
/** The scopes every package of which is a test library. */
|
|
235
|
+
export const TEST_LIBRARY_SCOPES: readonly string[] = ["@testing-library/"];
|
|
236
|
+
|
|
212
237
|
/** Every row, ordered by name and then by range. */
|
|
213
238
|
export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
214
239
|
{
|
package/src/finding.ts
CHANGED
|
@@ -119,6 +119,11 @@ export interface CompletedFinding extends Finding {
|
|
|
119
119
|
readonly reachabilityClass: Confidence;
|
|
120
120
|
/** The reachability class capped by the code's ceiling. */
|
|
121
121
|
readonly confidence: Confidence;
|
|
122
|
+
/**
|
|
123
|
+
* Where test-used code alone lowers the confidence, the one the finding would carry
|
|
124
|
+
* otherwise, which its component's cap and the dials read.
|
|
125
|
+
*/
|
|
126
|
+
readonly componentConfidence?: Confidence;
|
|
122
127
|
/**
|
|
123
128
|
* The relation that decided the subject: present on a declaration the sweep judged
|
|
124
129
|
* dead, absent on a live one and on a subject that is no declaration.
|
|
@@ -132,6 +132,20 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
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
|
+
|
|
135
149
|
/**
|
|
136
150
|
* The confidence ceiling of a test of dead code: the lowest reachability class among the
|
|
137
151
|
* target symbols it references, because its deadness is theirs, whether or not they share
|
|
@@ -145,25 +159,40 @@ function testCeilings(
|
|
|
145
159
|
const tests = new Set(
|
|
146
160
|
input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
|
|
147
161
|
);
|
|
148
|
-
return (id) =>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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);
|
|
164
188
|
}
|
|
165
|
-
|
|
166
|
-
|
|
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 };
|
|
167
196
|
}
|
|
168
197
|
|
|
169
198
|
/**
|
|
@@ -321,6 +350,7 @@ export function completed(
|
|
|
321
350
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
322
351
|
const classOf = reachabilityClasses(input);
|
|
323
352
|
const ceilingOf = testCeilings(input, classOf);
|
|
353
|
+
const testUsed = testUsedSubjects(input);
|
|
324
354
|
const { components, computed, files } = runComponents(swept, findings);
|
|
325
355
|
const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
|
|
326
356
|
const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
|
|
@@ -394,8 +424,17 @@ export function completed(
|
|
|
394
424
|
candidate === undefined
|
|
395
425
|
? union.made[at]
|
|
396
426
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
397
|
-
const
|
|
427
|
+
const ownClass =
|
|
398
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
|
+
);
|
|
399
438
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
400
439
|
const fixability = generated
|
|
401
440
|
? "none"
|
|
@@ -420,10 +459,8 @@ export function completed(
|
|
|
420
459
|
position: finding.position,
|
|
421
460
|
symbol: finding.symbol,
|
|
422
461
|
reachabilityClass,
|
|
423
|
-
confidence:
|
|
424
|
-
|
|
425
|
-
judged === undefined ? "certain" : ceilingOf(judged.id),
|
|
426
|
-
),
|
|
462
|
+
confidence: used ? "possible" : untested,
|
|
463
|
+
...(used ? { componentConfidence: untested } : {}),
|
|
427
464
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
428
465
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
429
466
|
generated,
|
|
@@ -450,7 +487,7 @@ export function completed(
|
|
|
450
487
|
/**
|
|
451
488
|
* The findings with every finding of a dead component capped by the lowest confidence
|
|
452
489
|
* among the component's root members, so the minimum confidence reports or withholds a
|
|
453
|
-
* component whole.
|
|
490
|
+
* component whole but for the findings test-used code lowers on their own.
|
|
454
491
|
*/
|
|
455
492
|
function componentCapped(
|
|
456
493
|
findings: readonly CompletedFinding[],
|
|
@@ -461,17 +498,29 @@ function componentCapped(
|
|
|
461
498
|
const { id, root } = finding.component;
|
|
462
499
|
if (root && dead.has(id)) {
|
|
463
500
|
const held = lowest.get(id);
|
|
464
|
-
|
|
501
|
+
const own = componentConfidenceOf(finding);
|
|
502
|
+
lowest.set(id, held === undefined ? own : capped(held, own));
|
|
465
503
|
}
|
|
466
504
|
}
|
|
467
505
|
return findings.map((finding) => {
|
|
468
506
|
const cap = lowest.get(finding.component.id);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
+
};
|
|
472
516
|
});
|
|
473
517
|
}
|
|
474
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
|
+
|
|
475
524
|
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
476
525
|
function dialed(
|
|
477
526
|
finding: Pick<CompletedFinding, "severity" | "confidence">,
|
|
@@ -494,7 +543,11 @@ export function dialsOf(
|
|
|
494
543
|
const minConfidence = input.config.analysis.minConfidence;
|
|
495
544
|
const components = new Set(
|
|
496
545
|
findings
|
|
497
|
-
.filter(
|
|
546
|
+
.filter(
|
|
547
|
+
(finding) =>
|
|
548
|
+
finding.component.root &&
|
|
549
|
+
dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence),
|
|
550
|
+
)
|
|
498
551
|
.map((finding) => finding.component.id),
|
|
499
552
|
);
|
|
500
553
|
return {
|
|
@@ -61,8 +61,9 @@ interface Judged {
|
|
|
61
61
|
* The code one candidate is reported under, or none, each symbol once under the most
|
|
62
62
|
* specific code: a test of dead code first; no other test-file declaration or enum
|
|
63
63
|
* member nothing names; a test-support declaration test code references as test-only
|
|
64
|
-
* use; then deprecation, a test reference,
|
|
65
|
-
* unused export being the
|
|
64
|
+
* use; then deprecation, a test reference, or only dead production references beside a
|
|
65
|
+
* test that stays, and the unreferenced codes, an unimportable unused export being the
|
|
66
|
+
* unreachable kind's.
|
|
66
67
|
*/
|
|
67
68
|
function codeOf(judged: Judged): string | undefined {
|
|
68
69
|
const { candidate, symbol } = judged;
|
|
@@ -85,7 +86,11 @@ function codeOf(judged: Judged): string | undefined {
|
|
|
85
86
|
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
86
87
|
return "DS1006";
|
|
87
88
|
}
|
|
88
|
-
if (
|
|
89
|
+
if (
|
|
90
|
+
candidate.testRefs > 0 &&
|
|
91
|
+
(candidate.productionRefs === 0 ||
|
|
92
|
+
(candidate.relation === "reachability" && candidate.keptByTests))
|
|
93
|
+
) {
|
|
89
94
|
return "DS1004";
|
|
90
95
|
}
|
|
91
96
|
if (MEMBERS.has(symbol.kind)) {
|
|
@@ -132,7 +137,9 @@ function messageOf(code: string, judged: Judged): string {
|
|
|
132
137
|
case "DS1006":
|
|
133
138
|
return `deprecated ${word} has no production reference`;
|
|
134
139
|
case "DS1004":
|
|
135
|
-
return
|
|
140
|
+
return candidate.productionRefs === 0
|
|
141
|
+
? `${word} is referenced only from test files and never from production code`
|
|
142
|
+
: `${word} is referenced only from test files and from declarations that are themselves dead`;
|
|
136
143
|
}
|
|
137
144
|
const subject =
|
|
138
145
|
code === "DS1003"
|
|
@@ -63,6 +63,12 @@ function entersByName(kinds: ReadonlySet<RootKind> | undefined): boolean {
|
|
|
63
63
|
return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The root kinds whose root of a file enters no export by name: a document's import roots
|
|
68
|
+
* each export it imports, so an export it does not import is judged as in any file.
|
|
69
|
+
*/
|
|
70
|
+
const PER_NAME: ReadonlySet<RootKind> = new Set<RootKind>(["document-import"]);
|
|
71
|
+
|
|
66
72
|
/**
|
|
67
73
|
* Whether the roots of a declaration's file enter it by name: a test runner's root
|
|
68
74
|
* enters only an export the runner calls, so another export of a test file is judged.
|
|
@@ -72,7 +78,7 @@ function fileEntersByName(
|
|
|
72
78
|
symbol: InventorySymbol,
|
|
73
79
|
): boolean {
|
|
74
80
|
const entering = [...(kinds ?? [])].filter(
|
|
75
|
-
(kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind),
|
|
81
|
+
(kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind) && !PER_NAME.has(kind),
|
|
76
82
|
);
|
|
77
83
|
return (
|
|
78
84
|
entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol))
|
|
@@ -222,7 +228,7 @@ function narrowing(
|
|
|
222
228
|
* export no code outside the target can import, because the manifest declares
|
|
223
229
|
* `exports` and no root reaches it. Each arm yields to a more specific kind, so one
|
|
224
230
|
* declaration is reported once: a test whose every subject is dead, an interface, a
|
|
225
|
-
* declaration only a test file references, and a deprecated one.
|
|
231
|
+
* declaration only a test file references or a test that stays keeps, and a deprecated one.
|
|
226
232
|
*/
|
|
227
233
|
function unreachable(
|
|
228
234
|
facts: Facts,
|
|
@@ -237,7 +243,7 @@ function unreachable(
|
|
|
237
243
|
(facts.rootedBy[at]?.size ?? 0) > 0 ||
|
|
238
244
|
candidate.testOfDeadCode ||
|
|
239
245
|
symbol.kind === "interface" ||
|
|
240
|
-
(candidate.
|
|
246
|
+
(candidate.testRefs > 0 && (candidate.productionRefs === 0 || candidate.keptByTests)) ||
|
|
241
247
|
(candidate.productionRefs === 0 && input.deprecated.has(symbol.id))
|
|
242
248
|
) {
|
|
243
249
|
return undefined;
|
package/src/kinds.ts
CHANGED
|
@@ -76,7 +76,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
76
76
|
code: "DS1004",
|
|
77
77
|
name: "test-only-use",
|
|
78
78
|
languages: ["go", "ts"],
|
|
79
|
-
rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production. A reference from test-support code, a package or file that only tests reach and that carries test evidence of its own language, is a test reference. A declaration of test-support code is test code and is no subject of this code, as grammar/analysis.md states. An interface method whose only calls and selections through the interface are in test code, and that DS1203's precondition does not exempt, is reported under this code. The methods implementing it are not reported with it, because the test reaches them through it.",
|
|
79
|
+
rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production. A reference from test-support code, a package or file that only tests reach and that carries test evidence of its own language, is a test reference. A declaration of test-support code is test code and is no subject of this code, as grammar/analysis.md states. An interface method whose only calls and selections through the interface are in test code, and that DS1203's precondition does not exempt, is reported under this code. The methods implementing it are not reported with it, because the test reaches them through it. A candidate found dead under reachability, every production reference to which is made by a declaration itself dead, is reported under this code as well where test code that is no test of dead code, and that the run counting test references reaches, references it, because that test keeps it, and the finding's message names both kinds of reference.",
|
|
80
80
|
defaultSeverity: "deny",
|
|
81
81
|
maxClass: "certain",
|
|
82
82
|
fixability: "deletable",
|
|
@@ -146,7 +146,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
146
146
|
languages: ["ts"],
|
|
147
147
|
rule: "An exported declaration used only inside its own file, reported as a candidate for removing the export keyword. The finding names the narrower visibility the references support.",
|
|
148
148
|
precondition:
|
|
149
|
-
'Closed world only. Reported in a file that is not an entry file, a file only a configuration string or a script token names being none, because each runs the file and reads no name it exports, and a test file being one only for the exports its test runner calls, as grammar/analysis.md "Test code" states, and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file. Where the edge\'s other side is unknown to the analyzer, the finding is emitted pending. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.',
|
|
149
|
+
'Closed world only. Reported in a file that is not an entry file, a file only a configuration string or a script token names being none, because each runs the file and reads no name it exports, and a test file being one only for the exports its test runner calls, as grammar/analysis.md "Test code" states, and a file a Markdown or MDX document imports one only for the exports the import reads, as grammar/analysis.md "Markdown and MDX documents" states, and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file. Where the edge\'s other side is unknown to the analyzer, the finding is emitted pending. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.',
|
|
150
150
|
defaultSeverity: "warn",
|
|
151
151
|
maxClass: "certain",
|
|
152
152
|
fixability: "narrowable",
|
package/src/matrix.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface Configured {
|
|
|
30
30
|
readonly testFiles: readonly string[];
|
|
31
31
|
/** The paths of the test-support files, which only test code imports. */
|
|
32
32
|
readonly supportFiles?: readonly string[] | undefined;
|
|
33
|
+
/** The paths of the files only tests reach that are no test-support file. */
|
|
34
|
+
readonly testUsedFiles?: readonly string[] | undefined;
|
|
33
35
|
/** The ids of the ambient declarations, which describe code the target does not implement. */
|
|
34
36
|
readonly ambient?: ReadonlySet<string> | undefined;
|
|
35
37
|
/**
|
|
@@ -60,6 +62,11 @@ export interface Matrix {
|
|
|
60
62
|
* whose project holds it, in the run's order.
|
|
61
63
|
*/
|
|
62
64
|
readonly heldIn: readonly (readonly string[])[];
|
|
65
|
+
/**
|
|
66
|
+
* Per declaration of {@link Matrix.union}, whether a file only tests reach that is no
|
|
67
|
+
* test-support file holds it in some configuration: production code only tests use.
|
|
68
|
+
*/
|
|
69
|
+
readonly testUsed: readonly boolean[];
|
|
63
70
|
/** Each declaration's dependents, by id, as the first configuration holding it states them. */
|
|
64
71
|
readonly dependents: ReadonlyMap<string, readonly string[]>;
|
|
65
72
|
}
|
|
@@ -81,6 +88,17 @@ export interface MatrixCandidate extends Candidate {
|
|
|
81
88
|
* shape nothing references would be.
|
|
82
89
|
*/
|
|
83
90
|
readonly unreferencedTest: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Whether it is dead only because code only tests use is: production code only tests
|
|
93
|
+
* use holds it and the run that counts test references reaches it, or such a
|
|
94
|
+
* declaration reaches it through dead declarations.
|
|
95
|
+
*/
|
|
96
|
+
readonly testUsed: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Whether test code the run that counts test references reaches, and that is no test of
|
|
99
|
+
* dead code, references it: a test that stays keeps it.
|
|
100
|
+
*/
|
|
101
|
+
readonly keptByTests: boolean;
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
/** One run's sweep. */
|
|
@@ -158,6 +176,7 @@ export function matrixOf(per: readonly Configured[]): Matrix {
|
|
|
158
176
|
}
|
|
159
177
|
});
|
|
160
178
|
symbols.sort((a, b) => byPosition(a.position, b.position));
|
|
179
|
+
const testUsed = new Set(per.flatMap((one) => one.testUsedFiles ?? []));
|
|
161
180
|
|
|
162
181
|
return {
|
|
163
182
|
configurations: per.map((one) => one.configuration),
|
|
@@ -173,6 +192,7 @@ export function matrixOf(per: readonly Configured[]): Matrix {
|
|
|
173
192
|
new Set(per.flatMap((one) => [...(one.ambient ?? [])])),
|
|
174
193
|
),
|
|
175
194
|
heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
|
|
195
|
+
testUsed: symbols.map((symbol) => testUsed.has(symbol.position.path)),
|
|
176
196
|
dependents,
|
|
177
197
|
};
|
|
178
198
|
}
|
|
@@ -227,6 +247,14 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
227
247
|
union.subject[edge.to] !== true ||
|
|
228
248
|
judged[edge.to] !== undefined,
|
|
229
249
|
);
|
|
250
|
+
const admittedAt = (at: number): boolean =>
|
|
251
|
+
judged[at]?.every((candidate) => candidate.testOfDeadCode) === true && subjectsDead(at);
|
|
252
|
+
const kept = keptByTests(union, testReached, admittedAt);
|
|
253
|
+
const testUsed = testUsedReach(
|
|
254
|
+
union,
|
|
255
|
+
union.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testReached[at] === true),
|
|
256
|
+
judged.map((one) => one !== undefined),
|
|
257
|
+
);
|
|
230
258
|
const candidates: MatrixCandidate[] = [];
|
|
231
259
|
union.symbols.forEach((symbol, at) => {
|
|
232
260
|
const all = judged[at];
|
|
@@ -234,7 +262,7 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
234
262
|
return;
|
|
235
263
|
}
|
|
236
264
|
const held = matrix.heldIn[at] ?? [];
|
|
237
|
-
const admitted =
|
|
265
|
+
const admitted = admittedAt(at);
|
|
238
266
|
const testRefs = union.made[at]?.test ?? 0;
|
|
239
267
|
const heldByTests =
|
|
240
268
|
heldBy[union.parent[at] ?? OUTSIDE] ??
|
|
@@ -255,6 +283,8 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
255
283
|
configurations: held,
|
|
256
284
|
heldByTests,
|
|
257
285
|
unreferencedTest: unreferenced,
|
|
286
|
+
testUsed: testUsed[at] === true,
|
|
287
|
+
keptByTests: kept[at] === true,
|
|
258
288
|
});
|
|
259
289
|
});
|
|
260
290
|
|
|
@@ -266,6 +296,47 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
266
296
|
};
|
|
267
297
|
}
|
|
268
298
|
|
|
299
|
+
/** Per declaration of the union, whether one of `seeds` is it or reaches it through `dead` declarations. */
|
|
300
|
+
function testUsedReach(
|
|
301
|
+
union: Graph,
|
|
302
|
+
seeds: readonly boolean[],
|
|
303
|
+
dead: readonly boolean[],
|
|
304
|
+
): readonly boolean[] {
|
|
305
|
+
// A file is no candidate, and its top level is what reaches what it alone calls.
|
|
306
|
+
const reached = seeds.map((seed, at) => seed && dead[at] === true);
|
|
307
|
+
const queue = seeds.flatMap((one, at) => (one ? [at] : []));
|
|
308
|
+
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
309
|
+
for (const edge of union.out[at] ?? []) {
|
|
310
|
+
if (dead[edge.to] === true && reached[edge.to] !== true) {
|
|
311
|
+
reached[edge.to] = true;
|
|
312
|
+
queue.push(edge.to);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return reached;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Per declaration of the union, whether test code `reached` holds and `admitted` rejects references it. */
|
|
320
|
+
function keptByTests(
|
|
321
|
+
union: Graph,
|
|
322
|
+
reached: readonly boolean[],
|
|
323
|
+
admitted: (at: number) => boolean,
|
|
324
|
+
): readonly boolean[] {
|
|
325
|
+
const kept = union.symbols.map(() => false);
|
|
326
|
+
union.out.forEach((edges, from) => {
|
|
327
|
+
const testCode = union.test[from] === true || union.support[from] === true;
|
|
328
|
+
if (!testCode || reached[from] !== true || admitted(from)) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
for (const edge of edges) {
|
|
332
|
+
if (edge.test) {
|
|
333
|
+
kept[edge.to] = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
return kept;
|
|
338
|
+
}
|
|
339
|
+
|
|
269
340
|
/**
|
|
270
341
|
* The dead components of one sweep over the union, with the file declarations at `files`
|
|
271
342
|
* dead as well: a file that is deleted whole takes the declarations it holds and every
|