@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/dist/src/analysis.js
CHANGED
|
@@ -531,7 +531,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
531
531
|
}));
|
|
532
532
|
// Test-support code is code only test code imports in any configuration of the run: a
|
|
533
533
|
// configuration that holds no test file says nothing about what the tests import.
|
|
534
|
-
const support = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles), testEvidence(host, targetRoot, withOutside));
|
|
534
|
+
const { support, testUsed } = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles), testEvidence(host, targetRoot, withOutside));
|
|
535
535
|
const settled = settledSatisfaction(withOutside.map((one) => ({ evidence: one.evidence, held: one.projectRead.held })), [...withOutside.flatMap((one) => one.references), ...read.consumers.references]);
|
|
536
536
|
const answers = withOutside.map((one, index) => {
|
|
537
537
|
const { projectRead } = one;
|
|
@@ -544,6 +544,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
544
544
|
const made = supportReferences([...one.references, ...(satisfied?.references ?? [])], support);
|
|
545
545
|
const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
|
|
546
546
|
const supportFiles = [...support].filter((path) => held.has(path)).sort();
|
|
547
|
+
const testUsedFiles = [...testUsed].filter((path) => held.has(path)).sort();
|
|
547
548
|
const configured = {
|
|
548
549
|
configuration: projectRead.configuration,
|
|
549
550
|
symbols: projectRead.held.symbols,
|
|
@@ -551,6 +552,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
551
552
|
roots: projectRead.rooted.liveUnderReachability,
|
|
552
553
|
testFiles: [...one.testFiles, ...supportFiles],
|
|
553
554
|
...(supportFiles.length === 0 ? {} : { supportFiles }),
|
|
555
|
+
...(testUsedFiles.length === 0 ? {} : { testUsedFiles }),
|
|
554
556
|
...(projectRead.held.ambient.size === 0 ? {} : { ambient: projectRead.held.ambient }),
|
|
555
557
|
...(projectRead.referenceOnly.length === 0
|
|
556
558
|
? {}
|
|
@@ -15,45 +15,76 @@ function isGlobalBlock(symbol, held, files) {
|
|
|
15
15
|
!held.modules.has(symbol.id));
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
* file in `scripts
|
|
20
|
-
*
|
|
21
|
-
* files that are no module, by identifier.
|
|
18
|
+
* Per declaration, the interfaces and namespaces between it and the augmentation block or
|
|
19
|
+
* the file in `scripts` that holds it, with the block itself, or none where no such block
|
|
20
|
+
* or file holds it; and whether that holder is the global scope rather than a module
|
|
21
|
+
* declaration. `scripts` holds the files that are no module, by identifier.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
function holdersOf(held, scripts) {
|
|
24
24
|
const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
|
|
25
25
|
const globals = new Set(held.symbols.filter((one) => isGlobalBlock(one, held, files)).map((one) => one.id));
|
|
26
26
|
if (globals.size === 0 && held.modules.size === 0 && scripts.size === 0) {
|
|
27
|
-
return
|
|
27
|
+
return undefined;
|
|
28
28
|
}
|
|
29
29
|
const byId = new Map(held.symbols.map((one) => [one.id, one]));
|
|
30
|
-
|
|
30
|
+
return (id) => {
|
|
31
31
|
const ids = [];
|
|
32
32
|
for (let at = byId.get(byId.get(id)?.parent ?? ""); at !== undefined; at = byId.get(at.parent)) {
|
|
33
33
|
if (globals.has(at.id) || held.modules.has(at.id)) {
|
|
34
|
-
return [...ids, at.id];
|
|
34
|
+
return { ids: [...ids, at.id], global: globals.has(at.id) };
|
|
35
35
|
}
|
|
36
36
|
if (scripts.has(at.id)) {
|
|
37
|
-
return ids;
|
|
37
|
+
return { ids, global: true };
|
|
38
38
|
}
|
|
39
39
|
if (at.kind === "interface" || at.kind === "namespace") {
|
|
40
40
|
ids.push(at.id);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
return [];
|
|
43
|
+
return { ids: [], global: false };
|
|
44
44
|
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The ambient interfaces of the global scope among `interfaces`: each one a declaration
|
|
48
|
+
* file or a `declare` holds that a `declare global` block, or the top level of a file in
|
|
49
|
+
* `scripts`, declares, directly or inside a namespace there.
|
|
50
|
+
*/
|
|
51
|
+
export function ambientGlobalInterfaces(held, interfaces, scripts) {
|
|
52
|
+
const holders = holdersOf(held, scripts);
|
|
53
|
+
return holders === undefined
|
|
54
|
+
? []
|
|
55
|
+
: interfaces.filter((id) => held.ambient.has(id) && holders(id).global);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* For each reference to a declaration an augmentation block, or the global scope of a
|
|
59
|
+
* file in `scripts` (the files that are no module), holds, a read at the same place of
|
|
60
|
+
* every interface and namespace between it and the block or the file, and of the block
|
|
61
|
+
* itself. A read through an interface of `merged`, an ambient global interface merging
|
|
62
|
+
* into a language or library one, is a production read whoever writes it: it types a
|
|
63
|
+
* value the runtime provides, so a test reading a member needs it as code does.
|
|
64
|
+
*/
|
|
65
|
+
export function augmentationReferences(held, found, scripts = new Set(), merged = new Set()) {
|
|
66
|
+
const holders = holdersOf(held, scripts);
|
|
67
|
+
if (holders === undefined) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const fileOf = new Map(held.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]));
|
|
45
71
|
// Two uses at one place of declarations one block holds read the block once there.
|
|
46
72
|
const made = new Set(found.map(referenceKey));
|
|
47
73
|
const reads = new Map();
|
|
48
74
|
for (const reference of found) {
|
|
49
|
-
|
|
75
|
+
const { ids } = holders(reference.to);
|
|
76
|
+
// A test's read through such an interface is held by the test file, which the test
|
|
77
|
+
// runner loads whichever of its declarations runs.
|
|
78
|
+
const typed = reference.test && ids.some((id) => merged.has(id));
|
|
79
|
+
const from = typed ? (fileOf.get(reference.position.path) ?? reference.from) : reference.from;
|
|
80
|
+
for (const to of ids) {
|
|
50
81
|
const read = {
|
|
51
|
-
from
|
|
82
|
+
from,
|
|
52
83
|
to,
|
|
53
84
|
position: reference.position,
|
|
54
85
|
use: "read",
|
|
55
86
|
resolution: reference.resolution,
|
|
56
|
-
test: reference.test,
|
|
87
|
+
test: reference.test && !typed,
|
|
57
88
|
};
|
|
58
89
|
const key = referenceKey(read);
|
|
59
90
|
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
|
};
|
|
@@ -61,6 +61,29 @@ const ANGULAR_PACKAGE_KEYS = [
|
|
|
61
61
|
"styles",
|
|
62
62
|
"scripts",
|
|
63
63
|
].map((key) => ({ key, files: ["angular.json"] }));
|
|
64
|
+
/**
|
|
65
|
+
* The test runners and the assertion, mock and runner libraries whose import is test
|
|
66
|
+
* evidence beside each runner a row reads test files for. Each applies whether or not a
|
|
67
|
+
* manifest declares it, so none is a row.
|
|
68
|
+
*/
|
|
69
|
+
export const TEST_LIBRARIES = [
|
|
70
|
+
"@jest/globals",
|
|
71
|
+
"@playwright/test",
|
|
72
|
+
"ava",
|
|
73
|
+
"chai",
|
|
74
|
+
"expect",
|
|
75
|
+
"jasmine",
|
|
76
|
+
"jest",
|
|
77
|
+
"mocha",
|
|
78
|
+
"msw",
|
|
79
|
+
"nock",
|
|
80
|
+
"node:test",
|
|
81
|
+
"sinon",
|
|
82
|
+
"tap",
|
|
83
|
+
"uvu",
|
|
84
|
+
];
|
|
85
|
+
/** The scopes every package of which is a test library. */
|
|
86
|
+
export const TEST_LIBRARY_SCOPES = ["@testing-library/"];
|
|
64
87
|
/** Every row, ordered by name and then by range. */
|
|
65
88
|
export const CONVENTION_ROWS = [
|
|
66
89
|
{
|
|
@@ -105,6 +105,19 @@ function reachabilityClasses(input) {
|
|
|
105
105
|
return published.has(id) ? outside : "certain";
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
/** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
|
|
109
|
+
function testTargets(union, id) {
|
|
110
|
+
return (union.out[union.at(id)] ?? []).flatMap((edge) => {
|
|
111
|
+
const target = union.symbols[edge.to];
|
|
112
|
+
const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
|
|
113
|
+
return target !== undefined &&
|
|
114
|
+
!testCode &&
|
|
115
|
+
union.ambient[edge.to] !== true &&
|
|
116
|
+
union.subject[edge.to] === true
|
|
117
|
+
? [target.id]
|
|
118
|
+
: [];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
108
121
|
/**
|
|
109
122
|
* The confidence ceiling of a test of dead code: the lowest reachability class among the
|
|
110
123
|
* target symbols it references, because its deadness is theirs, whether or not they share
|
|
@@ -113,23 +126,28 @@ function reachabilityClasses(input) {
|
|
|
113
126
|
function testCeilings(input, classOf) {
|
|
114
127
|
const union = input.swept.matrix.union;
|
|
115
128
|
const tests = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
|
|
116
|
-
return (id) =>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
return (id) => tests.has(id)
|
|
130
|
+
? testTargets(union, id).reduce((lowest, target) => capped(lowest, classOf(target)), "certain")
|
|
131
|
+
: "certain";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The subjects dead only because production code only tests use is: each declaration the
|
|
135
|
+
* sweep marks test-used, each `DS1502` file of such code, and each test of dead code every
|
|
136
|
+
* target of which is one. Nothing they hold references a dead declaration outside them, so
|
|
137
|
+
* the rest of a component they share is reported without them.
|
|
138
|
+
*/
|
|
139
|
+
function testUsedSubjects(input) {
|
|
140
|
+
const { matrix, sweep } = input.swept;
|
|
141
|
+
const union = matrix.union;
|
|
142
|
+
const used = new Set(sweep.candidates.filter((one) => one.testUsed).map((one) => one.id));
|
|
143
|
+
for (const test of sweep.candidates.filter((one) => one.testOfDeadCode)) {
|
|
144
|
+
const targets = testTargets(union, test.id);
|
|
145
|
+
if (targets.length > 0 && targets.every((target) => used.has(target))) {
|
|
146
|
+
used.add(test.id);
|
|
130
147
|
}
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
}
|
|
149
|
+
const files = new Set(union.symbols.flatMap((symbol, at) => matrix.testUsed[at] === true ? [symbol.position.path] : []));
|
|
150
|
+
return { declarations: used, files };
|
|
133
151
|
}
|
|
134
152
|
/**
|
|
135
153
|
* The ceiling the roots the run found put on one dead subject: a subject other declarations
|
|
@@ -251,6 +269,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
251
269
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
252
270
|
const classOf = reachabilityClasses(input);
|
|
253
271
|
const ceilingOf = testCeilings(input, classOf);
|
|
272
|
+
const testUsed = testUsedSubjects(input);
|
|
254
273
|
const { components, computed, files } = runComponents(swept, findings);
|
|
255
274
|
const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
|
|
256
275
|
const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
|
|
@@ -317,7 +336,12 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
317
336
|
const counts = candidate === undefined
|
|
318
337
|
? union.made[at]
|
|
319
338
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
320
|
-
const
|
|
339
|
+
const ownClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
|
|
340
|
+
const used = judged !== undefined
|
|
341
|
+
? candidate !== undefined && testUsed.declarations.has(judged.id)
|
|
342
|
+
: finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
|
|
343
|
+
const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
|
|
344
|
+
const untested = capped(capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate)), judged === undefined ? "certain" : ceilingOf(judged.id));
|
|
321
345
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
322
346
|
const fixability = generated
|
|
323
347
|
? "none"
|
|
@@ -341,7 +365,8 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
341
365
|
position: finding.position,
|
|
342
366
|
symbol: finding.symbol,
|
|
343
367
|
reachabilityClass,
|
|
344
|
-
confidence:
|
|
368
|
+
confidence: used ? "possible" : untested,
|
|
369
|
+
...(used ? { componentConfidence: untested } : {}),
|
|
345
370
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
346
371
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
347
372
|
generated,
|
|
@@ -365,7 +390,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
365
390
|
/**
|
|
366
391
|
* The findings with every finding of a dead component capped by the lowest confidence
|
|
367
392
|
* among the component's root members, so the minimum confidence reports or withholds a
|
|
368
|
-
* component whole.
|
|
393
|
+
* component whole but for the findings test-used code lowers on their own.
|
|
369
394
|
*/
|
|
370
395
|
function componentCapped(findings, dead) {
|
|
371
396
|
const lowest = new Map();
|
|
@@ -373,16 +398,27 @@ function componentCapped(findings, dead) {
|
|
|
373
398
|
const { id, root } = finding.component;
|
|
374
399
|
if (root && dead.has(id)) {
|
|
375
400
|
const held = lowest.get(id);
|
|
376
|
-
|
|
401
|
+
const own = componentConfidenceOf(finding);
|
|
402
|
+
lowest.set(id, held === undefined ? own : capped(held, own));
|
|
377
403
|
}
|
|
378
404
|
}
|
|
379
405
|
return findings.map((finding) => {
|
|
380
406
|
const cap = lowest.get(finding.component.id);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
407
|
+
if (cap === undefined) {
|
|
408
|
+
return finding;
|
|
409
|
+
}
|
|
410
|
+
const own = finding.componentConfidence;
|
|
411
|
+
return {
|
|
412
|
+
...finding,
|
|
413
|
+
confidence: capped(finding.confidence, cap),
|
|
414
|
+
...(own === undefined ? {} : { componentConfidence: capped(own, cap) }),
|
|
415
|
+
};
|
|
384
416
|
});
|
|
385
417
|
}
|
|
418
|
+
/** The confidence a finding gives its component's cap and the dials. */
|
|
419
|
+
function componentConfidenceOf(finding) {
|
|
420
|
+
return finding.componentConfidence ?? finding.confidence;
|
|
421
|
+
}
|
|
386
422
|
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
387
423
|
function dialed(finding, minConfidence) {
|
|
388
424
|
return finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
@@ -397,7 +433,8 @@ function dialed(finding, minConfidence) {
|
|
|
397
433
|
export function dialsOf(input, findings) {
|
|
398
434
|
const minConfidence = input.config.analysis.minConfidence;
|
|
399
435
|
const components = new Set(findings
|
|
400
|
-
.filter((finding) => finding.component.root &&
|
|
436
|
+
.filter((finding) => finding.component.root &&
|
|
437
|
+
dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence))
|
|
401
438
|
.map((finding) => finding.component.id));
|
|
402
439
|
return {
|
|
403
440
|
allows: (code) => {
|
|
@@ -41,8 +41,9 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
|
|
|
41
41
|
* The code one candidate is reported under, or none, each symbol once under the most
|
|
42
42
|
* specific code: a test of dead code first; no other test-file declaration or enum
|
|
43
43
|
* member nothing names; a test-support declaration test code references as test-only
|
|
44
|
-
* use; then deprecation, a test reference,
|
|
45
|
-
* unused export being the
|
|
44
|
+
* use; then deprecation, a test reference, or only dead production references beside a
|
|
45
|
+
* test that stays, and the unreferenced codes, an unimportable unused export being the
|
|
46
|
+
* unreachable kind's.
|
|
46
47
|
*/
|
|
47
48
|
function codeOf(judged) {
|
|
48
49
|
const { candidate, symbol } = judged;
|
|
@@ -65,7 +66,9 @@ function codeOf(judged) {
|
|
|
65
66
|
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
66
67
|
return "DS1006";
|
|
67
68
|
}
|
|
68
|
-
if (candidate.
|
|
69
|
+
if (candidate.testRefs > 0 &&
|
|
70
|
+
(candidate.productionRefs === 0 ||
|
|
71
|
+
(candidate.relation === "reachability" && candidate.keptByTests))) {
|
|
69
72
|
return "DS1004";
|
|
70
73
|
}
|
|
71
74
|
if (MEMBERS.has(symbol.kind)) {
|
|
@@ -109,7 +112,9 @@ function messageOf(code, judged) {
|
|
|
109
112
|
case "DS1006":
|
|
110
113
|
return `deprecated ${word} has no production reference`;
|
|
111
114
|
case "DS1004":
|
|
112
|
-
return
|
|
115
|
+
return candidate.productionRefs === 0
|
|
116
|
+
? `${word} is referenced only from test files and never from production code`
|
|
117
|
+
: `${word} is referenced only from test files and from declarations that are themselves dead`;
|
|
113
118
|
}
|
|
114
119
|
const subject = code === "DS1003"
|
|
115
120
|
? `${visibilityWord(symbol)}${word}`
|
|
@@ -34,12 +34,17 @@ const RUN_ONLY = new Set(["configuration-string", "script"]);
|
|
|
34
34
|
function entersByName(kinds) {
|
|
35
35
|
return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The root kinds whose root of a file enters no export by name: a document's import roots
|
|
39
|
+
* each export it imports, so an export it does not import is judged as in any file.
|
|
40
|
+
*/
|
|
41
|
+
const PER_NAME = new Set(["document-import"]);
|
|
37
42
|
/**
|
|
38
43
|
* Whether the roots of a declaration's file enter it by name: a test runner's root
|
|
39
44
|
* enters only an export the runner calls, so another export of a test file is judged.
|
|
40
45
|
*/
|
|
41
46
|
function fileEntersByName(kinds, symbol) {
|
|
42
|
-
const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
|
|
47
|
+
const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind) && !PER_NAME.has(kind));
|
|
43
48
|
return (entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol)));
|
|
44
49
|
}
|
|
45
50
|
/**
|
|
@@ -163,7 +168,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
163
168
|
* export no code outside the target can import, because the manifest declares
|
|
164
169
|
* `exports` and no root reaches it. Each arm yields to a more specific kind, so one
|
|
165
170
|
* declaration is reported once: a test whose every subject is dead, an interface, a
|
|
166
|
-
* declaration only a test file references, and a deprecated one.
|
|
171
|
+
* declaration only a test file references or a test that stays keeps, and a deprecated one.
|
|
167
172
|
*/
|
|
168
173
|
function unreachable(facts, at, symbol, candidate, input) {
|
|
169
174
|
if (!input.boundary.encapsulated ||
|
|
@@ -171,7 +176,7 @@ function unreachable(facts, at, symbol, candidate, input) {
|
|
|
171
176
|
(facts.rootedBy[at]?.size ?? 0) > 0 ||
|
|
172
177
|
candidate.testOfDeadCode ||
|
|
173
178
|
symbol.kind === "interface" ||
|
|
174
|
-
(candidate.
|
|
179
|
+
(candidate.testRefs > 0 && (candidate.productionRefs === 0 || candidate.keptByTests)) ||
|
|
175
180
|
(candidate.productionRefs === 0 && input.deprecated.has(symbol.id))) {
|
|
176
181
|
return undefined;
|
|
177
182
|
}
|
package/dist/src/kinds.js
CHANGED
|
@@ -43,7 +43,7 @@ const ROWS = [
|
|
|
43
43
|
code: "DS1004",
|
|
44
44
|
name: "test-only-use",
|
|
45
45
|
languages: ["go", "ts"],
|
|
46
|
-
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.",
|
|
46
|
+
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.",
|
|
47
47
|
defaultSeverity: "deny",
|
|
48
48
|
maxClass: "certain",
|
|
49
49
|
fixability: "deletable",
|
|
@@ -110,7 +110,7 @@ const ROWS = [
|
|
|
110
110
|
name: "redundant-export-keyword",
|
|
111
111
|
languages: ["ts"],
|
|
112
112
|
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.",
|
|
113
|
-
precondition: '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.',
|
|
113
|
+
precondition: '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.',
|
|
114
114
|
defaultSeverity: "warn",
|
|
115
115
|
maxClass: "certain",
|
|
116
116
|
fixability: "narrowable",
|
package/dist/src/matrix.js
CHANGED
|
@@ -70,11 +70,13 @@ export function matrixOf(per) {
|
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
symbols.sort((a, b) => byPosition(a.position, b.position));
|
|
73
|
+
const testUsed = new Set(per.flatMap((one) => one.testUsedFiles ?? []));
|
|
73
74
|
return {
|
|
74
75
|
configurations: per.map((one) => one.configuration),
|
|
75
76
|
graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles, one.supportFiles, one.ambient)),
|
|
76
77
|
union: graphOf(symbols, references, per.flatMap((one) => one.roots), [...new Set(per.flatMap((one) => one.testFiles))], [...new Set(per.flatMap((one) => one.supportFiles ?? []))], new Set(per.flatMap((one) => [...(one.ambient ?? [])]))),
|
|
77
78
|
heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
|
|
79
|
+
testUsed: symbols.map((symbol) => testUsed.has(symbol.position.path)),
|
|
78
80
|
dependents,
|
|
79
81
|
};
|
|
80
82
|
}
|
|
@@ -121,6 +123,9 @@ export function sweepMatrix(matrix, input) {
|
|
|
121
123
|
union.ambient[edge.to] === true ||
|
|
122
124
|
union.subject[edge.to] !== true ||
|
|
123
125
|
judged[edge.to] !== undefined);
|
|
126
|
+
const admittedAt = (at) => judged[at]?.every((candidate) => candidate.testOfDeadCode) === true && subjectsDead(at);
|
|
127
|
+
const kept = keptByTests(union, testReached, admittedAt);
|
|
128
|
+
const testUsed = testUsedReach(union, union.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testReached[at] === true), judged.map((one) => one !== undefined));
|
|
124
129
|
const candidates = [];
|
|
125
130
|
union.symbols.forEach((symbol, at) => {
|
|
126
131
|
const all = judged[at];
|
|
@@ -128,7 +133,7 @@ export function sweepMatrix(matrix, input) {
|
|
|
128
133
|
return;
|
|
129
134
|
}
|
|
130
135
|
const held = matrix.heldIn[at] ?? [];
|
|
131
|
-
const admitted =
|
|
136
|
+
const admitted = admittedAt(at);
|
|
132
137
|
const testRefs = union.made[at]?.test ?? 0;
|
|
133
138
|
const heldByTests = heldBy[union.parent[at] ?? OUTSIDE] ??
|
|
134
139
|
(union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
|
|
@@ -148,6 +153,8 @@ export function sweepMatrix(matrix, input) {
|
|
|
148
153
|
configurations: held,
|
|
149
154
|
heldByTests,
|
|
150
155
|
unreferencedTest: unreferenced,
|
|
156
|
+
testUsed: testUsed[at] === true,
|
|
157
|
+
keptByTests: kept[at] === true,
|
|
151
158
|
});
|
|
152
159
|
});
|
|
153
160
|
const liveUnder = liveAnywhere(union.symbols, per);
|
|
@@ -157,6 +164,37 @@ export function sweepMatrix(matrix, input) {
|
|
|
157
164
|
components: deadComponents(matrix, { liveUnder, candidates }, new Set()),
|
|
158
165
|
};
|
|
159
166
|
}
|
|
167
|
+
/** Per declaration of the union, whether one of `seeds` is it or reaches it through `dead` declarations. */
|
|
168
|
+
function testUsedReach(union, seeds, dead) {
|
|
169
|
+
// A file is no candidate, and its top level is what reaches what it alone calls.
|
|
170
|
+
const reached = seeds.map((seed, at) => seed && dead[at] === true);
|
|
171
|
+
const queue = seeds.flatMap((one, at) => (one ? [at] : []));
|
|
172
|
+
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
173
|
+
for (const edge of union.out[at] ?? []) {
|
|
174
|
+
if (dead[edge.to] === true && reached[edge.to] !== true) {
|
|
175
|
+
reached[edge.to] = true;
|
|
176
|
+
queue.push(edge.to);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return reached;
|
|
181
|
+
}
|
|
182
|
+
/** Per declaration of the union, whether test code `reached` holds and `admitted` rejects references it. */
|
|
183
|
+
function keptByTests(union, reached, admitted) {
|
|
184
|
+
const kept = union.symbols.map(() => false);
|
|
185
|
+
union.out.forEach((edges, from) => {
|
|
186
|
+
const testCode = union.test[from] === true || union.support[from] === true;
|
|
187
|
+
if (!testCode || reached[from] !== true || admitted(from)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (const edge of edges) {
|
|
191
|
+
if (edge.test) {
|
|
192
|
+
kept[edge.to] = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
return kept;
|
|
197
|
+
}
|
|
160
198
|
/**
|
|
161
199
|
* The dead components of one sweep over the union, with the file declarations at `files`
|
|
162
200
|
* dead as well: a file that is deleted whole takes the declarations it holds and every
|
package/dist/src/references.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isMetaProperty, isNewExpression, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/ast";
|
|
1
|
+
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isInterfaceDeclaration, isMetaProperty, isModuleBlock, isModuleDeclaration, isNewExpression, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/ast";
|
|
2
2
|
import { aliasChains } from "./alias-chain.js";
|
|
3
3
|
import { contextualUses } from "./contextual-uses.js";
|
|
4
4
|
import { destructuring, propertyReadsOf } from "./destructuring.js";
|
|
@@ -6,7 +6,7 @@ import { specifierCandidates } from "./configuration-files.js";
|
|
|
6
6
|
import { namespaceUses } from "./namespace-objects.js";
|
|
7
7
|
import { globExpression } from "./glob.js";
|
|
8
8
|
import { declarationsByName, isParameterProperty, nodeKey, } from "./inventory.js";
|
|
9
|
-
import { augmentationReferences } from "./augmentations.js";
|
|
9
|
+
import { ambientGlobalInterfaces, augmentationReferences } from "./augmentations.js";
|
|
10
10
|
import { elementAccessReferences } from "./element-access.js";
|
|
11
11
|
import { indexedAccessReferences } from "./indexed-access.js";
|
|
12
12
|
import { keyofComparisonReferences } from "./keyof-comparisons.js";
|
|
@@ -1135,6 +1135,37 @@ export function references(project, held, targetRoot, options) {
|
|
|
1135
1135
|
}
|
|
1136
1136
|
}
|
|
1137
1137
|
}
|
|
1138
|
+
/** The ambient global interfaces of the walked files that merge into a declaration outside the project's own files. */
|
|
1139
|
+
const libraryMerged = (scripts) => {
|
|
1140
|
+
const names = new Map();
|
|
1141
|
+
const visit = (node) => {
|
|
1142
|
+
if (isInterfaceDeclaration(node)) {
|
|
1143
|
+
const id = declaredAt(node);
|
|
1144
|
+
if (id !== undefined) {
|
|
1145
|
+
names.set(id, node.name);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
else if (isModuleDeclaration(node) || isModuleBlock(node)) {
|
|
1149
|
+
node.forEachChild(visit);
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
for (const { file } of walked) {
|
|
1153
|
+
file.forEachChild(visit);
|
|
1154
|
+
}
|
|
1155
|
+
const global = new Set(ambientGlobalInterfaces(held, [...names.keys()], scripts));
|
|
1156
|
+
const asked = [...names].filter(([id]) => global.has(id));
|
|
1157
|
+
if (asked.length === 0) {
|
|
1158
|
+
return new Set();
|
|
1159
|
+
}
|
|
1160
|
+
const answers = project.symbolsAt(asked.map(([, name]) => project.handle(name)));
|
|
1161
|
+
return new Set(asked.flatMap(([id], at) => {
|
|
1162
|
+
const symbol = answers[at];
|
|
1163
|
+
return typeof symbol === "object" &&
|
|
1164
|
+
symbol.declarations.some((handle) => !ownPaths.has(handle.path))
|
|
1165
|
+
? [id]
|
|
1166
|
+
: [];
|
|
1167
|
+
}));
|
|
1168
|
+
};
|
|
1138
1169
|
const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
|
|
1139
1170
|
for (const [file, writes] of valued) {
|
|
1140
1171
|
const uses = usesOf.get(file) ?? [];
|
|
@@ -1159,7 +1190,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
1159
1190
|
typedLookups = (options.requestCount?.() ?? 0) - before;
|
|
1160
1191
|
}
|
|
1161
1192
|
const scripts = new Set(walked.filter((one) => one.file.externalModuleIndicator === undefined).map((one) => one.fileId));
|
|
1162
|
-
found.push(...augmentationReferences(held, found, scripts));
|
|
1193
|
+
found.push(...augmentationReferences(held, found, scripts, libraryMerged(scripts)));
|
|
1163
1194
|
found.sort((a, b) => byPosition(a.position, b.position) ||
|
|
1164
1195
|
compare(a.to, b.to) ||
|
|
1165
1196
|
compare(a.from, b.from) ||
|
package/dist/src/suppress.js
CHANGED
|
@@ -57,6 +57,7 @@ export const GO_REF_EXPRESSIONS = [
|
|
|
57
57
|
`^go://${GO_PATH}#${GO_IDENT}$`,
|
|
58
58
|
`^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})+$`,
|
|
59
59
|
`^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})?\\[${GO_IDENT}\\]$`,
|
|
60
|
+
`^go://${GO_PATH}#(?:${GO_IDENT}(?:\\.${GO_IDENT})?)?\\(${GO_IDENT}\\)$`,
|
|
60
61
|
`^go://${GO_PATH}#${GO_FILE}:file$`,
|
|
61
62
|
`^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:require$`,
|
|
62
63
|
`^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:replace$`,
|
package/dist/src/test-support.js
CHANGED
|
@@ -5,21 +5,18 @@
|
|
|
5
5
|
* another such file, so it is judged with the tests: what it references is referenced by
|
|
6
6
|
* test code.
|
|
7
7
|
*/
|
|
8
|
-
import { CONVENTION_ROWS } from "./convention-rows.js";
|
|
8
|
+
import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./convention-rows.js";
|
|
9
9
|
import { packageOfSpecifier } from "./dependencies.js";
|
|
10
10
|
/** The root kind that never takes a file out of test-support code: the skip only withholds. */
|
|
11
11
|
const SKIP_ROOT = "type-error";
|
|
12
12
|
/**
|
|
13
13
|
* The test runners and test libraries an import of which is test evidence: each runner a
|
|
14
|
-
* convention row reads test files for, the
|
|
14
|
+
* convention row reads test files for, and the convention table's test libraries.
|
|
15
15
|
*/
|
|
16
16
|
const TEST_PACKAGES = new Set([
|
|
17
17
|
...CONVENTION_ROWS.filter((row) => row.testFiles !== undefined).map((row) => row.package),
|
|
18
|
-
|
|
19
|
-
"@playwright/test",
|
|
18
|
+
...TEST_LIBRARIES,
|
|
20
19
|
]);
|
|
21
|
-
/** The scopes every package of which is a test library. */
|
|
22
|
-
const TEST_SCOPES = ["@testing-library/"];
|
|
23
20
|
/**
|
|
24
21
|
* The Node.js built-in modules a bare specifier names, which are no package. The set is
|
|
25
22
|
* closed: Node.js adds a new built-in under the `node:` scheme alone
|
|
@@ -73,7 +70,7 @@ const NODE_BUILTINS = new Set([
|
|
|
73
70
|
function namesTestPackage(specifier) {
|
|
74
71
|
const name = specifier.startsWith("node:") ? specifier : packageOfSpecifier(specifier);
|
|
75
72
|
return (name !== undefined &&
|
|
76
|
-
(TEST_PACKAGES.has(name) ||
|
|
73
|
+
(TEST_PACKAGES.has(name) || TEST_LIBRARY_SCOPES.some((scope) => name.startsWith(scope))));
|
|
77
74
|
}
|
|
78
75
|
/**
|
|
79
76
|
* Whether a file carries test evidence: an import of a test runner or a test library, or
|
|
@@ -106,9 +103,9 @@ function evidenceReader(tests, evidence) {
|
|
|
106
103
|
* The files of one project that are test-support code, below the target root: the
|
|
107
104
|
* largest set of files a test file reaches, none of them a test file or holding a root,
|
|
108
105
|
* each carrying test evidence of its own, whose every user outside the set is a test
|
|
109
|
-
* file
|
|
110
|
-
*
|
|
111
|
-
* the test-file rules classified.
|
|
106
|
+
* file; and the files only tests reach that are not among them. A root has a caller
|
|
107
|
+
* outside the reference graph: a runtime, a tool, a configuration, or a consumer of a
|
|
108
|
+
* library's published API. `testFiles` are the files the test-file rules classified.
|
|
112
109
|
*/
|
|
113
110
|
export function testSupportFiles(symbols, references, roots, testFiles, evidence) {
|
|
114
111
|
const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
|
|
@@ -162,7 +159,7 @@ export function testSupportFiles(symbols, references, roots, testFiles, evidence
|
|
|
162
159
|
const onlyTests = withoutOutsideUsers(new Set([...reached].filter((path) => !rooted.has(path))));
|
|
163
160
|
const carries = evidenceReader(tests, evidence);
|
|
164
161
|
const support = withoutOutsideUsers(new Set([...onlyTests].filter((path) => carries(onlyTests, path))));
|
|
165
|
-
return support;
|
|
162
|
+
return { support, testUsed: new Set([...onlyTests].filter((path) => !support.has(path))) };
|
|
166
163
|
}
|
|
167
164
|
/** The references with every one a test-support file makes counted as a test reference. */
|
|
168
165
|
export function supportReferences(references, support) {
|
package/jsr.json
CHANGED