@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
package/dist/src/entry-points.js
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
* value that is not a literal is not evaluated and not guessed, so a file it would
|
|
11
11
|
* have named is entered by nothing here.
|
|
12
12
|
*/
|
|
13
|
-
import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, } from "@typescript/native/ast";
|
|
13
|
+
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
|
|
14
14
|
import { SymbolFlags } from "@typescript/native/sync";
|
|
15
15
|
import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
|
|
16
16
|
import { globExpression } from "./glob.js";
|
|
17
17
|
import { htmlEntries } from "./html-entries.js";
|
|
18
|
+
import { documentEntries } from "./mdx-entries.js";
|
|
18
19
|
import { dirnamePath, relativePath, resolvePath } from "./paths.js";
|
|
19
20
|
import { UNANSWERED } from "./query.js";
|
|
20
21
|
import { workflowEntries } from "./workflow-steps.js";
|
|
@@ -37,8 +38,11 @@ const BROWSER_TEST_MATCH = "**/*.{spec,test}.{js,ts,jsx,tsx,cjs,cts,cjsx,ctsx,mj
|
|
|
37
38
|
const SETUP_MEMBERS = ["setupFiles", "globalSetup"];
|
|
38
39
|
/** A member that moves the directory every path of a test-runner configuration is read from. */
|
|
39
40
|
const ROOT_MEMBER = "root";
|
|
40
|
-
/**
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* A URL reference's scheme. A reference with none that does not start with a solidus is
|
|
43
|
+
* relative, the bare name included: https://url.spec.whatwg.org/#relative-url-string
|
|
44
|
+
*/
|
|
45
|
+
const URL_SCHEME = /^[a-z][a-z\d+.-]*:/iu;
|
|
42
46
|
/** The constructors whose first argument addresses a worker script. */
|
|
43
47
|
const WORKER_CONSTRUCTORS = new Set(["Worker", "SharedWorker"]);
|
|
44
48
|
/** The file's name, the last segment of its path. */
|
|
@@ -138,9 +142,16 @@ function literalsOf(value) {
|
|
|
138
142
|
}
|
|
139
143
|
return found;
|
|
140
144
|
}
|
|
141
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Whether a literal is a relative URL reference, which is also how the test runner reads a
|
|
147
|
+
* setup file's path against its root.
|
|
148
|
+
*/
|
|
142
149
|
function isRelative(text) {
|
|
143
|
-
return
|
|
150
|
+
return text !== "" && !text.startsWith("/") && !URL_SCHEME.test(text);
|
|
151
|
+
}
|
|
152
|
+
/** Whether a module specifier is relative, which a bare name is not: it names a package. */
|
|
153
|
+
function isModuleRelative(text) {
|
|
154
|
+
return text.startsWith("./") || text.startsWith("../");
|
|
144
155
|
}
|
|
145
156
|
/** A URL reference with its query and its fragment removed, which name no part of the file. */
|
|
146
157
|
function pathPart(text) {
|
|
@@ -348,10 +359,10 @@ function workerSitesOf(file) {
|
|
|
348
359
|
return found;
|
|
349
360
|
}
|
|
350
361
|
/**
|
|
351
|
-
* The workers one file addresses. A call's names must resolve to the platform
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
362
|
+
* The workers one file addresses. A call's names must resolve to a symbol the platform
|
|
363
|
+
* declares, which a file of the target may augment and no import brings in; a name only
|
|
364
|
+
* the target declares is the target's own constructor or object, which addresses no
|
|
365
|
+
* worker this rule knows. One batch per file that writes such a call.
|
|
355
366
|
*/
|
|
356
367
|
function workersOf(project, file, files) {
|
|
357
368
|
const sites = workerSitesOf(file);
|
|
@@ -371,8 +382,7 @@ function workersOf(project, file, files) {
|
|
|
371
382
|
}
|
|
372
383
|
if (symbol !== undefined &&
|
|
373
384
|
(symbol.flags & SymbolFlags.Alias) === 0 &&
|
|
374
|
-
symbol.declarations.
|
|
375
|
-
symbol.declarations.every((handle) => !files.byName.has(handle.path))) {
|
|
385
|
+
symbol.declarations.some((handle) => !files.byName.has(handle.path))) {
|
|
376
386
|
platform.add(name);
|
|
377
387
|
}
|
|
378
388
|
});
|
|
@@ -388,6 +398,87 @@ function workersOf(project, file, files) {
|
|
|
388
398
|
}
|
|
389
399
|
return found;
|
|
390
400
|
}
|
|
401
|
+
/** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
|
|
402
|
+
function leadingLiteral(node) {
|
|
403
|
+
const at = unwrapped(node);
|
|
404
|
+
if (isTemplateExpression(at)) {
|
|
405
|
+
return at.head.text;
|
|
406
|
+
}
|
|
407
|
+
if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
|
|
408
|
+
return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
/** Whether one call is `import.meta.glob(...)`. */
|
|
413
|
+
function isMetaGlob(node) {
|
|
414
|
+
if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const { expression, name } = node.expression;
|
|
418
|
+
return (isIdentifier(name) &&
|
|
419
|
+
name.text === "glob" &&
|
|
420
|
+
isMetaProperty(expression) &&
|
|
421
|
+
expression.keywordToken === SyntaxKind.ImportKeyword);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* The files one file loads by a computed path, each with what exports: every own file
|
|
425
|
+
* below the directory a relative literal start of an `import()` or `require()` argument
|
|
426
|
+
* names, other than the calling file's own directory or one holding it, which would name
|
|
427
|
+
* every sibling, and
|
|
428
|
+
* every own file a relative literal glob of `import.meta.glob` matches, both read against
|
|
429
|
+
* the calling file's directory, as a bundler's context module reads them.
|
|
430
|
+
*/
|
|
431
|
+
function computedImportsOf(file, files) {
|
|
432
|
+
const dir = dirnamePath(file.fileName);
|
|
433
|
+
const found = [];
|
|
434
|
+
const below = (prefix) => {
|
|
435
|
+
const target = resolvePath(dir, prefix);
|
|
436
|
+
if (!isModuleRelative(prefix) ||
|
|
437
|
+
!prefix.endsWith("/") ||
|
|
438
|
+
target === dir ||
|
|
439
|
+
relativePath(target, dir) !== undefined) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
for (const candidate of files.byName.values()) {
|
|
443
|
+
if (candidate !== file && relativePath(target, candidate.fileName) !== undefined) {
|
|
444
|
+
found.push({ file: candidate, rule: "computed-import", source: prefix, exports: true });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
const globbed = (pattern) => {
|
|
449
|
+
if (!isModuleRelative(pattern)) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const expression = globExpression(resolvePath(dir, pattern));
|
|
453
|
+
for (const candidate of files.byName.values()) {
|
|
454
|
+
if (candidate !== file && expression.test(candidate.fileName)) {
|
|
455
|
+
found.push({ file: candidate, rule: "computed-import", source: pattern, exports: true });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const visit = (node) => {
|
|
460
|
+
if (isCallExpression(node)) {
|
|
461
|
+
const [first] = node.arguments;
|
|
462
|
+
const callee = node.expression;
|
|
463
|
+
const loads = callee.kind === SyntaxKind.ImportKeyword ||
|
|
464
|
+
(isIdentifier(callee) && callee.text === "require");
|
|
465
|
+
if (loads && first !== undefined) {
|
|
466
|
+
const prefix = leadingLiteral(first);
|
|
467
|
+
if (prefix !== undefined) {
|
|
468
|
+
below(prefix);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else if (isMetaGlob(node) && first !== undefined) {
|
|
472
|
+
for (const pattern of literalsOf(first) ?? []) {
|
|
473
|
+
globbed(pattern);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
node.forEachChild(visit);
|
|
478
|
+
};
|
|
479
|
+
file.forEachChild(visit);
|
|
480
|
+
return found;
|
|
481
|
+
}
|
|
391
482
|
/**
|
|
392
483
|
* Every file of one project the rules enter, in no particular order and possibly more
|
|
393
484
|
* than once: a file two rules enter appears once per rule.
|
|
@@ -417,10 +508,20 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse,
|
|
|
417
508
|
}
|
|
418
509
|
}
|
|
419
510
|
found.push(...workersOf(project, file, files));
|
|
511
|
+
found.push(...computedImportsOf(file, files));
|
|
420
512
|
}
|
|
421
513
|
for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
|
|
422
514
|
found.push({ file, rule: "html-entry", source, exports: true });
|
|
423
515
|
}
|
|
516
|
+
for (const { file, source, names } of documentEntries(host, targetRoot, files)) {
|
|
517
|
+
found.push({
|
|
518
|
+
file,
|
|
519
|
+
rule: "document-import",
|
|
520
|
+
source,
|
|
521
|
+
exports: true,
|
|
522
|
+
...(names === undefined ? {} : { names }),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
424
525
|
for (const { file, source } of workflowEntries(host, targetRoot, files)) {
|
|
425
526
|
found.push({ file, rule: "workflow-step", source, exports: true });
|
|
426
527
|
}
|
|
@@ -4,12 +4,27 @@
|
|
|
4
4
|
* together with every finding that falls with a root they withhold.
|
|
5
5
|
*/
|
|
6
6
|
import { componentId } from "../components.js";
|
|
7
|
+
import { deadComponents } from "../matrix.js";
|
|
7
8
|
import { OUTSIDE } from "../graph.js";
|
|
8
9
|
import { KINDS } from "../kinds.js";
|
|
9
10
|
import { byPosition, positionKey } from "../position.js";
|
|
10
11
|
import { declarationKey } from "../ref.js";
|
|
11
12
|
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
12
13
|
import { isPartSubject, isRowSubject } from "../suppress.js";
|
|
14
|
+
/** The unused-file code, whose deletion removes every use its file writes. */
|
|
15
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
16
|
+
/**
|
|
17
|
+
* The codes whose subject the run that counts test references holds live, as
|
|
18
|
+
* finding.schema.json lists them, so a test file's candidate reported under one carries
|
|
19
|
+
* none of a dead declaration's sweep facts.
|
|
20
|
+
*/
|
|
21
|
+
const LIVE_SUBJECT = new Set([
|
|
22
|
+
"DS1101",
|
|
23
|
+
"DS1102",
|
|
24
|
+
"DS1104",
|
|
25
|
+
"DS1204",
|
|
26
|
+
"DS1301",
|
|
27
|
+
]);
|
|
13
28
|
/** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
|
|
14
29
|
const MANUAL = "manual";
|
|
15
30
|
/** The member kinds an object property can declare, which an `in` expression tests by name. */
|
|
@@ -72,15 +87,11 @@ function testedByIn(input) {
|
|
|
72
87
|
* published API, a public member of a published type included, is `certain` where the
|
|
73
88
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
74
89
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
75
|
-
* consumer set.
|
|
76
|
-
* rule, what it contains, and a member an `in` expression tests by name are `possible`.
|
|
90
|
+
* consumer set. A member an `in` expression tests by name is `possible`.
|
|
77
91
|
*/
|
|
78
92
|
function reachabilityClasses(input) {
|
|
79
93
|
const tested = testedByIn(input);
|
|
80
94
|
const published = publishedIds(input.swept.matrix.union);
|
|
81
|
-
const usedByTests = new Set(input.swept.sweep.candidates
|
|
82
|
-
.filter((candidate) => candidate.heldByTests !== undefined)
|
|
83
|
-
.map((candidate) => candidate.id));
|
|
84
95
|
const { declared } = input.boundary.consumers;
|
|
85
96
|
const outside = declared.length === 0
|
|
86
97
|
? "possible"
|
|
@@ -88,12 +99,25 @@ function reachabilityClasses(input) {
|
|
|
88
99
|
? "certain"
|
|
89
100
|
: "probable";
|
|
90
101
|
return (id) => {
|
|
91
|
-
if (
|
|
102
|
+
if (tested.has(id)) {
|
|
92
103
|
return "possible";
|
|
93
104
|
}
|
|
94
105
|
return published.has(id) ? outside : "certain";
|
|
95
106
|
};
|
|
96
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
|
+
}
|
|
97
121
|
/**
|
|
98
122
|
* The confidence ceiling of a test of dead code: the lowest reachability class among the
|
|
99
123
|
* target symbols it references, because its deadness is theirs, whether or not they share
|
|
@@ -102,28 +126,46 @@ function reachabilityClasses(input) {
|
|
|
102
126
|
function testCeilings(input, classOf) {
|
|
103
127
|
const union = input.swept.matrix.union;
|
|
104
128
|
const tests = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
|
|
105
|
-
return (id) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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);
|
|
116
147
|
}
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
}
|
|
149
|
+
const files = new Set(union.symbols.flatMap((symbol, at) => matrix.testUsed[at] === true ? [symbol.position.path] : []));
|
|
150
|
+
return { declarations: used, files };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The ceiling the roots the run found put on one dead subject: a subject other declarations
|
|
154
|
+
* reference, found dead only because no root reaches them, is `probable` unless
|
|
155
|
+
* the configuration declares the roots complete, because a root the analysis cannot see
|
|
156
|
+
* would make it live; every other declaration has none below `certain`.
|
|
157
|
+
*/
|
|
158
|
+
function rootsCeiling(input, finding, candidate) {
|
|
159
|
+
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
160
|
+
return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
|
|
119
161
|
}
|
|
120
162
|
/**
|
|
121
163
|
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
122
164
|
* its members listed in the canonical order where the run lists a component in full.
|
|
123
165
|
*/
|
|
124
|
-
function findingComponents(swept, cascade) {
|
|
166
|
+
function findingComponents(swept, components, cascade) {
|
|
125
167
|
const held = new Map();
|
|
126
|
-
for (const component of
|
|
168
|
+
for (const component of components) {
|
|
127
169
|
for (const member of component.members) {
|
|
128
170
|
held.set(member, component);
|
|
129
171
|
}
|
|
@@ -159,6 +201,59 @@ function findingComponents(swept, cascade) {
|
|
|
159
201
|
};
|
|
160
202
|
};
|
|
161
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* The run's dead components with each file a `DS1502` finding reports dead among them, so
|
|
206
|
+
* the file's declarations fall with it, and with the components each subject that is no
|
|
207
|
+
* declaration falls with joined into one.
|
|
208
|
+
*/
|
|
209
|
+
function runComponents(swept, findings) {
|
|
210
|
+
const union = swept.matrix.union;
|
|
211
|
+
const reported = new Set(findings.filter((one) => one.code === FILE_NEVER_IMPORTED).map((one) => one.position.path));
|
|
212
|
+
const files = new Map(union.symbols.flatMap((symbol, at) => symbol.kind === "file" && reported.has(symbol.position.path) ? [[at, symbol]] : []));
|
|
213
|
+
const computed = files.size === 0
|
|
214
|
+
? swept.sweep.components
|
|
215
|
+
: deadComponents(swept.matrix, swept.sweep, new Set(files.keys()));
|
|
216
|
+
const place = new Map();
|
|
217
|
+
computed.forEach((component, at) => {
|
|
218
|
+
for (const member of component.members) {
|
|
219
|
+
place.set(member, at);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
const leader = computed.map((_component, at) => at);
|
|
223
|
+
const find = (at) => {
|
|
224
|
+
let top = at;
|
|
225
|
+
while ((leader[top] ?? top) !== top) {
|
|
226
|
+
top = leader[top] ?? top;
|
|
227
|
+
}
|
|
228
|
+
return top;
|
|
229
|
+
};
|
|
230
|
+
for (const { fallsWith } of findings) {
|
|
231
|
+
const joined = (fallsWith ?? []).flatMap((id) => place.get(id) ?? []).map(find);
|
|
232
|
+
const first = Math.min(...joined);
|
|
233
|
+
for (const one of joined) {
|
|
234
|
+
leader[one] = first;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const merged = new Map();
|
|
238
|
+
computed.forEach((component, at) => {
|
|
239
|
+
const top = find(at);
|
|
240
|
+
const into = merged.get(top);
|
|
241
|
+
merged.set(top, into === undefined
|
|
242
|
+
? component
|
|
243
|
+
: {
|
|
244
|
+
id: into.id,
|
|
245
|
+
members: [...into.members, ...component.members],
|
|
246
|
+
roots: [...into.roots, ...component.roots],
|
|
247
|
+
deletableLines: into.deletableLines + component.deletableLines,
|
|
248
|
+
held: into.held && component.held,
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
components: [...merged.values()],
|
|
253
|
+
computed: computed.length,
|
|
254
|
+
files: new Map([...files.values()].map((symbol) => [symbol.position.path, symbol.id])),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
162
257
|
/**
|
|
163
258
|
* Every finding of the run completed, in the order given. A dead declaration carries its
|
|
164
259
|
* candidate's relation, counts, configurations and component; a live one carries no relation and
|
|
@@ -174,10 +269,13 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
174
269
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
175
270
|
const classOf = reachabilityClasses(input);
|
|
176
271
|
const ceilingOf = testCeilings(input, classOf);
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
const
|
|
272
|
+
const testUsed = testUsedSubjects(input);
|
|
273
|
+
const { components, computed, files } = runComponents(swept, findings);
|
|
274
|
+
const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
|
|
275
|
+
const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
|
|
276
|
+
// A join keeps its first component's identifier, so a minted one counts past every computed one.
|
|
277
|
+
let minted = computed;
|
|
278
|
+
const dead = new Set(components.map((component) => component.id));
|
|
181
279
|
const fallingWith = new Map();
|
|
182
280
|
for (const { heldByTests } of swept.sweep.candidates) {
|
|
183
281
|
if (heldByTests !== undefined) {
|
|
@@ -193,18 +291,30 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
193
291
|
// a row is still never the declaration. Two declarations of one file can share a
|
|
194
292
|
// reference, so the one the finding is positioned at is the subject where there is one.
|
|
195
293
|
const declared = union.at(positionKey(finding.position));
|
|
294
|
+
const ref = finding.symbol.holder ?? finding.symbol.ref;
|
|
196
295
|
const at = isRowSubject(finding.symbol.kind)
|
|
197
296
|
? OUTSIDE
|
|
198
|
-
: union.symbols[declared]?.ref ===
|
|
297
|
+
: union.symbols[declared]?.ref === ref
|
|
199
298
|
? declared
|
|
200
|
-
: (positions.get(declarationKey(
|
|
299
|
+
: (positions.get(declarationKey(ref, finding.position.path)) ?? OUTSIDE);
|
|
201
300
|
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
202
301
|
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
203
302
|
const part = isPartSubject(finding.symbol.kind);
|
|
204
303
|
const symbol = union.symbols[at];
|
|
205
304
|
const judged = !part && union.subject[at] === true ? symbol : undefined;
|
|
206
|
-
const candidate = judged === undefined
|
|
207
|
-
|
|
305
|
+
const candidate = judged === undefined || LIVE_SUBJECT.has(finding.code)
|
|
306
|
+
? undefined
|
|
307
|
+
: candidates.get(judged.id);
|
|
308
|
+
const fallsWith = finding.fallsWith?.[0];
|
|
309
|
+
const joined = fallsWith === undefined ? undefined : componentOf(fallsWith);
|
|
310
|
+
const file = finding.code === FILE_NEVER_IMPORTED ? files.get(finding.position.path) : undefined;
|
|
311
|
+
const found = joined !== undefined
|
|
312
|
+
? { ...joined, root: false }
|
|
313
|
+
: file !== undefined
|
|
314
|
+
? componentOf(file)
|
|
315
|
+
: symbol === undefined || part
|
|
316
|
+
? undefined
|
|
317
|
+
: componentOf(symbol.id);
|
|
208
318
|
// A test file's declaration is a component member only as a test of dead code, and
|
|
209
319
|
// test code keeps the test-support code it references out of every component.
|
|
210
320
|
if (candidate !== undefined &&
|
|
@@ -226,7 +336,12 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
226
336
|
const counts = candidate === undefined
|
|
227
337
|
? union.made[at]
|
|
228
338
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
229
|
-
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));
|
|
230
345
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
231
346
|
const fixability = generated
|
|
232
347
|
? "none"
|
|
@@ -236,9 +351,13 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
236
351
|
row.findingFixabilities?.includes(MANUAL) === true
|
|
237
352
|
? MANUAL
|
|
238
353
|
: row.fixability;
|
|
239
|
-
const lastUses = fixability
|
|
240
|
-
?
|
|
241
|
-
: undefined
|
|
354
|
+
const lastUses = fixability !== "deletable"
|
|
355
|
+
? undefined
|
|
356
|
+
: candidate !== undefined
|
|
357
|
+
? input.dependencies.lastUses.get(candidate.id)
|
|
358
|
+
: finding.code === FILE_NEVER_IMPORTED
|
|
359
|
+
? input.dependencies.fileLastUses.get(finding.position.path)
|
|
360
|
+
: undefined;
|
|
242
361
|
return {
|
|
243
362
|
code: finding.code,
|
|
244
363
|
kind: row.name,
|
|
@@ -246,7 +365,8 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
246
365
|
position: finding.position,
|
|
247
366
|
symbol: finding.symbol,
|
|
248
367
|
reachabilityClass,
|
|
249
|
-
confidence:
|
|
368
|
+
confidence: used ? "possible" : untested,
|
|
369
|
+
...(used ? { componentConfidence: untested } : {}),
|
|
250
370
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
251
371
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
252
372
|
generated,
|
|
@@ -270,7 +390,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
270
390
|
/**
|
|
271
391
|
* The findings with every finding of a dead component capped by the lowest confidence
|
|
272
392
|
* among the component's root members, so the minimum confidence reports or withholds a
|
|
273
|
-
* component whole.
|
|
393
|
+
* component whole but for the findings test-used code lowers on their own.
|
|
274
394
|
*/
|
|
275
395
|
function componentCapped(findings, dead) {
|
|
276
396
|
const lowest = new Map();
|
|
@@ -278,16 +398,27 @@ function componentCapped(findings, dead) {
|
|
|
278
398
|
const { id, root } = finding.component;
|
|
279
399
|
if (root && dead.has(id)) {
|
|
280
400
|
const held = lowest.get(id);
|
|
281
|
-
|
|
401
|
+
const own = componentConfidenceOf(finding);
|
|
402
|
+
lowest.set(id, held === undefined ? own : capped(held, own));
|
|
282
403
|
}
|
|
283
404
|
}
|
|
284
405
|
return findings.map((finding) => {
|
|
285
406
|
const cap = lowest.get(finding.component.id);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
+
};
|
|
289
416
|
});
|
|
290
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
|
+
}
|
|
291
422
|
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
292
423
|
function dialed(finding, minConfidence) {
|
|
293
424
|
return finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
@@ -302,7 +433,8 @@ function dialed(finding, minConfidence) {
|
|
|
302
433
|
export function dialsOf(input, findings) {
|
|
303
434
|
const minConfidence = input.config.analysis.minConfidence;
|
|
304
435
|
const components = new Set(findings
|
|
305
|
-
.filter((finding) => finding.component.root &&
|
|
436
|
+
.filter((finding) => finding.component.root &&
|
|
437
|
+
dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence))
|
|
306
438
|
.map((finding) => finding.component.id));
|
|
307
439
|
return {
|
|
308
440
|
allows: (code) => {
|
|
@@ -1,38 +1,84 @@
|
|
|
1
1
|
import { declarationKey } from "../dependencies.js";
|
|
2
2
|
import { renderRef } from "../ref.js";
|
|
3
|
+
import { nonCodeArtifacts } from "./non-code-artifacts.js";
|
|
3
4
|
const UNUSED_DEPENDENCY = "DS1601";
|
|
5
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
4
6
|
const MESSAGES = {
|
|
5
7
|
dependency: "no import in the target needs the declared dependency",
|
|
6
8
|
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
7
9
|
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
8
10
|
};
|
|
11
|
+
const DEAD_USERS_MESSAGES = {
|
|
12
|
+
dependency: "only code that is itself dead uses the declared dependency",
|
|
13
|
+
"dev-dependency": "only code that is itself dead uses the declared development dependency",
|
|
14
|
+
"peer-dependency": "only code that is itself dead uses the declared peer dependency",
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The run's dead code: test code keeps the code it references out of every dead component
|
|
18
|
+
* unless that code is dead as well, and a file's top level is dead where the file is.
|
|
19
|
+
*/
|
|
20
|
+
function deadCode(input) {
|
|
21
|
+
const { swept } = input;
|
|
22
|
+
const union = swept.matrix.union;
|
|
23
|
+
const files = new Map(union.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]));
|
|
24
|
+
const dead = new Set(swept.sweep.components.flatMap((component) => component.members));
|
|
25
|
+
const reachedOnly = new Set(swept.sweep.candidates.filter((one) => one.relation === "reachability").map((one) => one.id));
|
|
26
|
+
for (const finding of nonCodeArtifacts(input)) {
|
|
27
|
+
const id = files.get(finding.position.path);
|
|
28
|
+
if (finding.code === FILE_NEVER_IMPORTED && id !== undefined) {
|
|
29
|
+
dead.add(id);
|
|
30
|
+
if (finding.unreachableOnly === true) {
|
|
31
|
+
reachedOnly.add(id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const published = new Set(union.rooted
|
|
36
|
+
.filter((root) => root.kind === "published-api")
|
|
37
|
+
.flatMap((root) => union.symbols[root.at]?.id ?? []));
|
|
38
|
+
return { dead, reachedOnly, published };
|
|
39
|
+
}
|
|
9
40
|
/**
|
|
10
41
|
* A dependency, development dependency or peer dependency of a manifest the run reads
|
|
11
|
-
* that the target does not need, at the position of its key
|
|
42
|
+
* that the target does not need, at the position of its key, and one whose every use is
|
|
43
|
+
* held by dead code, which is dead only through that code and falls with its component:
|
|
44
|
+
* at `possible` where one of its holders is published, and found under reachability alone
|
|
45
|
+
* where one of them is.
|
|
12
46
|
*/
|
|
13
|
-
function unusedDependencies(dependencies) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
function unusedDependencies(input, dependencies) {
|
|
48
|
+
const { dead, reachedOnly, published } = deadCode(input);
|
|
49
|
+
return dependencies.declared.flatMap((dependency) => {
|
|
50
|
+
const key = declarationKey(dependency);
|
|
51
|
+
const held = dependencies.heldBy.get(key);
|
|
52
|
+
const users = held?.every((id) => dead.has(id)) === true ? held : undefined;
|
|
53
|
+
if (dependencies.ran.has(key) || (dependencies.needed.has(key) && users === undefined)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const unbuilt = dependencies.unbuilt.has(dependency.position.path);
|
|
57
|
+
const atPossible = unbuilt || users?.some((id) => published.has(id)) === true;
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
code: UNUSED_DEPENDENCY,
|
|
61
|
+
position: dependency.position,
|
|
62
|
+
symbol: {
|
|
63
|
+
ref: renderRef(dependency.manifest, {
|
|
64
|
+
of: "dependency",
|
|
65
|
+
name: dependency.name,
|
|
66
|
+
section: dependency.section,
|
|
67
|
+
}),
|
|
68
|
+
kind: "dependency",
|
|
69
|
+
name: dependency.name,
|
|
70
|
+
sizeLines: 1,
|
|
71
|
+
},
|
|
72
|
+
message: (users === undefined ? MESSAGES : DEAD_USERS_MESSAGES)[dependency.section],
|
|
73
|
+
details: { dependencyClass: dependency.section },
|
|
74
|
+
...(atPossible ? { reachabilityClass: "possible" } : {}),
|
|
75
|
+
...(users?.some((id) => reachedOnly.has(id)) === true
|
|
76
|
+
? { unreachableOnly: true }
|
|
77
|
+
: {}),
|
|
78
|
+
...(users === undefined ? {} : { fallsWith: users }),
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
});
|
|
36
82
|
}
|
|
37
83
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
38
|
-
export const dependenciesAndModuleMachinery = (
|
|
84
|
+
export const dependenciesAndModuleMachinery = (input) => unusedDependencies(input, input.dependencies);
|
|
@@ -100,7 +100,7 @@ function dialledOver(input, suppressed, found, unmarked) {
|
|
|
100
100
|
const unanswered = heldKeys(input);
|
|
101
101
|
const markup = markupKeys(input);
|
|
102
102
|
const candidates = found.filter((finding) => {
|
|
103
|
-
const key = declarationKey(finding.symbol.ref, finding.position.path);
|
|
103
|
+
const key = declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
|
|
104
104
|
return (!ledger.withheld(finding) &&
|
|
105
105
|
!unanswered.has(key) &&
|
|
106
106
|
!markup.has(key) &&
|