@cplieger/deadset-ts 7.2.0-dev.3 → 8.0.0-dev.1
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/bin/mapper-connection.js +7 -4
- package/dist/src/analysis.js +116 -16
- package/dist/src/augmentations.js +21 -8
- package/dist/src/automatic-types.js +189 -0
- package/dist/src/base-url.js +18 -0
- package/dist/src/component-files.js +96 -31
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +54 -15
- package/dist/src/configured-declarations.js +24 -2
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +26 -26
- package/dist/src/convention-rows.js +75 -0
- package/dist/src/conventions.js +101 -5
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +185 -30
- package/dist/src/destructuring.js +77 -2
- package/dist/src/discover.js +188 -85
- package/dist/src/edges.js +1 -1
- package/dist/src/entry-point-gaps.js +7 -10
- package/dist/src/entry-points.js +112 -11
- package/dist/src/findings/completion.js +116 -21
- 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 +21 -1
- package/dist/src/findings/visibility-narrowing.js +22 -3
- 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 +313 -50
- package/dist/src/inventory.js +63 -8
- package/dist/src/json-document.js +5 -5
- package/dist/src/kinds.js +13 -13
- package/dist/src/matrix.js +18 -9
- package/dist/src/mdx-entries.js +197 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/namespace-objects.js +28 -18
- package/dist/src/overrides.js +45 -9
- package/dist/src/paths.js +9 -0
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +29 -13
- package/dist/src/reflective-lookup.js +8 -6
- package/dist/src/report.js +4 -2
- package/dist/src/reporters/template.js +22 -13
- 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 +97 -20
- package/dist/src/session.js +26 -1
- package/dist/src/source-files.js +39 -14
- package/dist/src/spelled-names.js +58 -0
- package/dist/src/suppress-file.js +12 -4
- package/dist/src/suppress-inline.js +21 -3
- package/dist/src/suppress.js +7 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/template-field.js +9 -6
- package/dist/src/test-support.js +121 -17
- package/dist/src/type-errors.js +116 -35
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/value-reach.js +17 -5
- package/dist/src/verbs/analyze.js +19 -1
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version-range.js +10 -5
- 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 +72 -13
- package/dist/src/workspace.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +175 -16
- package/src/augmentations.ts +21 -10
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/component-files.ts +116 -31
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +80 -18
- package/src/configured-declarations.ts +33 -4
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +27 -27
- package/src/convention-rows.ts +103 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +249 -43
- package/src/destructuring.ts +93 -2
- package/src/discover.ts +234 -86
- package/src/edges.ts +1 -1
- package/src/entry-point-gaps.ts +8 -11
- package/src/entry-points.ts +133 -11
- package/src/finding.ts +10 -0
- package/src/findings/completion.ts +147 -24
- 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 +25 -1
- package/src/findings/visibility-narrowing.ts +32 -3
- 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 +415 -64
- package/src/inventory.ts +83 -7
- package/src/json-document.ts +7 -7
- package/src/kinds.ts +13 -13
- package/src/matrix.ts +32 -14
- package/src/mdx-entries.ts +241 -0
- package/src/module-declarations.ts +11 -13
- package/src/namespace-objects.ts +34 -21
- package/src/overrides.ts +56 -9
- package/src/paths.ts +10 -0
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +38 -17
- package/src/reflective-lookup.ts +9 -7
- package/src/report.ts +16 -3
- package/src/reporters/template.ts +21 -13
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +108 -17
- package/src/session.ts +29 -1
- package/src/source-files.ts +66 -16
- package/src/spelled-names.ts +102 -0
- package/src/suppress-file.ts +14 -3
- package/src/suppress-inline.ts +21 -2
- package/src/suppress.ts +9 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/template-field.ts +9 -6
- package/src/test-support.ts +149 -16
- package/src/type-errors.ts +132 -41
- package/src/type-parameter-reads.ts +227 -0
- package/src/value-reach.ts +19 -6
- package/src/verbs/analyze.ts +28 -1
- package/src/verbs/explain.ts +5 -2
- package/src/version-range.ts +10 -5
- 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 +86 -14
- package/src/workspace.ts +1 -1
package/src/entry-points.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import {
|
|
15
15
|
isArrayLiteralExpression,
|
|
16
16
|
isAsExpression,
|
|
17
|
+
isBinaryExpression,
|
|
17
18
|
isCallExpression,
|
|
18
19
|
isExportAssignment,
|
|
19
20
|
isIdentifier,
|
|
@@ -26,6 +27,8 @@ import {
|
|
|
26
27
|
isPropertyAssignment,
|
|
27
28
|
isSatisfiesExpression,
|
|
28
29
|
isStringLiteral,
|
|
30
|
+
isTemplateExpression,
|
|
31
|
+
SyntaxKind,
|
|
29
32
|
type Node,
|
|
30
33
|
type SourceFile,
|
|
31
34
|
} from "@typescript/native/ast";
|
|
@@ -40,6 +43,7 @@ import type { Selected } from "./conventions.ts";
|
|
|
40
43
|
import { globExpression } from "./glob.ts";
|
|
41
44
|
import type { Host } from "./host.ts";
|
|
42
45
|
import { htmlEntries } from "./html-entries.ts";
|
|
46
|
+
import { documentEntries } from "./mdx-entries.ts";
|
|
43
47
|
import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
44
48
|
import { UNANSWERED } from "./query.ts";
|
|
45
49
|
import type { ProjectView } from "./session.ts";
|
|
@@ -61,7 +65,15 @@ export type EntryRule =
|
|
|
61
65
|
/** A file a module script of an HTML file below the target root loads. */
|
|
62
66
|
| "html-entry"
|
|
63
67
|
/** A file a `run:` step of a workflow in the target root's `.github/workflows` names. */
|
|
64
|
-
| "workflow-step"
|
|
68
|
+
| "workflow-step"
|
|
69
|
+
/**
|
|
70
|
+
* A file a load whose path is computed may name: below the directory the literal
|
|
71
|
+
* start of an `import()` or `require()` argument names, or matched by the literal
|
|
72
|
+
* glob of `import.meta.glob`.
|
|
73
|
+
*/
|
|
74
|
+
| "computed-import"
|
|
75
|
+
/** A file an import line of a Markdown or MDX document below the target root names. */
|
|
76
|
+
| "document-import";
|
|
65
77
|
|
|
66
78
|
/** One file a rule enters. */
|
|
67
79
|
interface EntryPoint {
|
|
@@ -78,6 +90,8 @@ interface EntryPoint {
|
|
|
78
90
|
* level and names none of its exports.
|
|
79
91
|
*/
|
|
80
92
|
readonly exports: boolean;
|
|
93
|
+
/** Where the file is entered for some of its exports only, their export names. */
|
|
94
|
+
readonly names?: readonly string[];
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
/** One file-name convention: the stems a tool looks for, under each extension it loads. */
|
|
@@ -114,8 +128,11 @@ const SETUP_MEMBERS = ["setupFiles", "globalSetup"] as const;
|
|
|
114
128
|
/** A member that moves the directory every path of a test-runner configuration is read from. */
|
|
115
129
|
const ROOT_MEMBER = "root";
|
|
116
130
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
131
|
+
/**
|
|
132
|
+
* A URL reference's scheme. A reference with none that does not start with a solidus is
|
|
133
|
+
* relative, the bare name included: https://url.spec.whatwg.org/#relative-url-string
|
|
134
|
+
*/
|
|
135
|
+
const URL_SCHEME = /^[a-z][a-z\d+.-]*:/iu;
|
|
119
136
|
|
|
120
137
|
/** The constructors whose first argument addresses a worker script. */
|
|
121
138
|
const WORKER_CONSTRUCTORS: ReadonlySet<string> = new Set(["Worker", "SharedWorker"]);
|
|
@@ -228,9 +245,17 @@ function literalsOf(value: Node): readonly string[] | undefined {
|
|
|
228
245
|
return found;
|
|
229
246
|
}
|
|
230
247
|
|
|
231
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* Whether a literal is a relative URL reference, which is also how the test runner reads a
|
|
250
|
+
* setup file's path against its root.
|
|
251
|
+
*/
|
|
232
252
|
function isRelative(text: string): boolean {
|
|
233
|
-
return
|
|
253
|
+
return text !== "" && !text.startsWith("/") && !URL_SCHEME.test(text);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Whether a module specifier is relative, which a bare name is not: it names a package. */
|
|
257
|
+
function isModuleRelative(text: string): boolean {
|
|
258
|
+
return text.startsWith("./") || text.startsWith("../");
|
|
234
259
|
}
|
|
235
260
|
|
|
236
261
|
/** A URL reference with its query and its fragment removed, which name no part of the file. */
|
|
@@ -470,10 +495,10 @@ function workerSitesOf(file: SourceFile): readonly WorkerSite[] {
|
|
|
470
495
|
}
|
|
471
496
|
|
|
472
497
|
/**
|
|
473
|
-
* The workers one file addresses. A call's names must resolve to the platform
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
498
|
+
* The workers one file addresses. A call's names must resolve to a symbol the platform
|
|
499
|
+
* declares, which a file of the target may augment and no import brings in; a name only
|
|
500
|
+
* the target declares is the target's own constructor or object, which addresses no
|
|
501
|
+
* worker this rule knows. One batch per file that writes such a call.
|
|
477
502
|
*/
|
|
478
503
|
function workersOf<Brand>(
|
|
479
504
|
project: ProjectView<Brand>,
|
|
@@ -498,8 +523,7 @@ function workersOf<Brand>(
|
|
|
498
523
|
if (
|
|
499
524
|
symbol !== undefined &&
|
|
500
525
|
(symbol.flags & SymbolFlags.Alias) === 0 &&
|
|
501
|
-
symbol.declarations.
|
|
502
|
-
symbol.declarations.every((handle) => !files.byName.has(handle.path))
|
|
526
|
+
symbol.declarations.some((handle) => !files.byName.has(handle.path))
|
|
503
527
|
) {
|
|
504
528
|
platform.add(name);
|
|
505
529
|
}
|
|
@@ -517,6 +541,94 @@ function workersOf<Brand>(
|
|
|
517
541
|
return found;
|
|
518
542
|
}
|
|
519
543
|
|
|
544
|
+
/** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
|
|
545
|
+
function leadingLiteral(node: Node): string | undefined {
|
|
546
|
+
const at = unwrapped(node);
|
|
547
|
+
if (isTemplateExpression(at)) {
|
|
548
|
+
return at.head.text;
|
|
549
|
+
}
|
|
550
|
+
if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
|
|
551
|
+
return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
|
|
552
|
+
}
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Whether one call is `import.meta.glob(...)`. */
|
|
557
|
+
function isMetaGlob(node: Node): boolean {
|
|
558
|
+
if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
const { expression, name } = node.expression;
|
|
562
|
+
return (
|
|
563
|
+
isIdentifier(name) &&
|
|
564
|
+
name.text === "glob" &&
|
|
565
|
+
isMetaProperty(expression) &&
|
|
566
|
+
expression.keywordToken === SyntaxKind.ImportKeyword
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* The files one file loads by a computed path, each with what exports: every own file
|
|
572
|
+
* below the directory a relative literal start of an `import()` or `require()` argument
|
|
573
|
+
* names, other than the calling file's own directory or one holding it, which would name
|
|
574
|
+
* every sibling, and
|
|
575
|
+
* every own file a relative literal glob of `import.meta.glob` matches, both read against
|
|
576
|
+
* the calling file's directory, as a bundler's context module reads them.
|
|
577
|
+
*/
|
|
578
|
+
function computedImportsOf(file: SourceFile, files: SourceFiles): readonly EntryPoint[] {
|
|
579
|
+
const dir = dirnamePath(file.fileName);
|
|
580
|
+
const found: EntryPoint[] = [];
|
|
581
|
+
const below = (prefix: string): void => {
|
|
582
|
+
const target = resolvePath(dir, prefix);
|
|
583
|
+
if (
|
|
584
|
+
!isModuleRelative(prefix) ||
|
|
585
|
+
!prefix.endsWith("/") ||
|
|
586
|
+
target === dir ||
|
|
587
|
+
relativePath(target, dir) !== undefined
|
|
588
|
+
) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
for (const candidate of files.byName.values()) {
|
|
592
|
+
if (candidate !== file && relativePath(target, candidate.fileName) !== undefined) {
|
|
593
|
+
found.push({ file: candidate, rule: "computed-import", source: prefix, exports: true });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
const globbed = (pattern: string): void => {
|
|
598
|
+
if (!isModuleRelative(pattern)) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const expression = globExpression(resolvePath(dir, pattern));
|
|
602
|
+
for (const candidate of files.byName.values()) {
|
|
603
|
+
if (candidate !== file && expression.test(candidate.fileName)) {
|
|
604
|
+
found.push({ file: candidate, rule: "computed-import", source: pattern, exports: true });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
const visit = (node: Node): void => {
|
|
609
|
+
if (isCallExpression(node)) {
|
|
610
|
+
const [first] = node.arguments;
|
|
611
|
+
const callee = node.expression;
|
|
612
|
+
const loads =
|
|
613
|
+
callee.kind === SyntaxKind.ImportKeyword ||
|
|
614
|
+
(isIdentifier(callee) && callee.text === "require");
|
|
615
|
+
if (loads && first !== undefined) {
|
|
616
|
+
const prefix = leadingLiteral(first);
|
|
617
|
+
if (prefix !== undefined) {
|
|
618
|
+
below(prefix);
|
|
619
|
+
}
|
|
620
|
+
} else if (isMetaGlob(node) && first !== undefined) {
|
|
621
|
+
for (const pattern of literalsOf(first) ?? []) {
|
|
622
|
+
globbed(pattern);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
node.forEachChild(visit);
|
|
627
|
+
};
|
|
628
|
+
file.forEachChild(visit);
|
|
629
|
+
return found;
|
|
630
|
+
}
|
|
631
|
+
|
|
520
632
|
/**
|
|
521
633
|
* Every file of one project the rules enter, in no particular order and possibly more
|
|
522
634
|
* than once: a file two rules enter appears once per rule.
|
|
@@ -554,10 +666,20 @@ export function entryPoints<Brand>(
|
|
|
554
666
|
}
|
|
555
667
|
}
|
|
556
668
|
found.push(...workersOf(project, file, files));
|
|
669
|
+
found.push(...computedImportsOf(file, files));
|
|
557
670
|
}
|
|
558
671
|
for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
|
|
559
672
|
found.push({ file, rule: "html-entry", source, exports: true });
|
|
560
673
|
}
|
|
674
|
+
for (const { file, source, names } of documentEntries(host, targetRoot, files)) {
|
|
675
|
+
found.push({
|
|
676
|
+
file,
|
|
677
|
+
rule: "document-import",
|
|
678
|
+
source,
|
|
679
|
+
exports: true,
|
|
680
|
+
...(names === undefined ? {} : { names }),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
561
683
|
for (const { file, source } of workflowEntries(host, targetRoot, files)) {
|
|
562
684
|
found.push({ file, rule: "workflow-step", source, exports: true });
|
|
563
685
|
}
|
package/src/finding.ts
CHANGED
|
@@ -34,6 +34,12 @@ interface FindingSubject {
|
|
|
34
34
|
readonly name: string;
|
|
35
35
|
/** The number of source lines the subject spans. */
|
|
36
36
|
readonly sizeLines: number;
|
|
37
|
+
/**
|
|
38
|
+
* Where the subject is a part whose reference is its own, a parameter's, the reference
|
|
39
|
+
* of the declaration that holds it, which a record binding that declaration matches.
|
|
40
|
+
* No report carries it.
|
|
41
|
+
*/
|
|
42
|
+
readonly holder?: string;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
/** A declaration a finding names beside the subject, with where it is written. */
|
|
@@ -87,6 +93,10 @@ export interface Finding {
|
|
|
87
93
|
readonly details?: FindingDetails;
|
|
88
94
|
/** The reachability class of a subject that is no declaration, where it is not `certain`. */
|
|
89
95
|
readonly reachabilityClass?: Confidence;
|
|
96
|
+
/** Whether references to a subject that is no declaration exist, all from dead code. */
|
|
97
|
+
readonly unreachableOnly?: true;
|
|
98
|
+
/** The dead declarations and files whose component a subject that is no declaration falls with. */
|
|
99
|
+
readonly fallsWith?: readonly string[];
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
/** The dead component a finding names, as the finding schema spells its members. */
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { RunSweep } from "../analysis.ts";
|
|
8
8
|
import { componentId, type Component } from "../components.ts";
|
|
9
|
+
import { deadComponents } from "../matrix.ts";
|
|
9
10
|
import type { Cascade, Confidence, Severity } from "../config.ts";
|
|
10
11
|
import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
|
|
11
12
|
import { OUTSIDE, type Graph } from "../graph.ts";
|
|
@@ -15,9 +16,26 @@ import { byPosition, positionKey } from "../position.ts";
|
|
|
15
16
|
import { declarationKey } from "../ref.ts";
|
|
16
17
|
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
17
18
|
import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
|
|
19
|
+
import type { Candidate } from "../sweep.ts";
|
|
18
20
|
import type { Boundary } from "./boundary.ts";
|
|
19
21
|
import type { EmitterInput } from "./emitter.ts";
|
|
20
22
|
|
|
23
|
+
/** The unused-file code, whose deletion removes every use its file writes. */
|
|
24
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The codes whose subject the run that counts test references holds live, as
|
|
28
|
+
* finding.schema.json lists them, so a test file's candidate reported under one carries
|
|
29
|
+
* none of a dead declaration's sweep facts.
|
|
30
|
+
*/
|
|
31
|
+
const LIVE_SUBJECT: ReadonlySet<string> = new Set([
|
|
32
|
+
"DS1101",
|
|
33
|
+
"DS1102",
|
|
34
|
+
"DS1104",
|
|
35
|
+
"DS1204",
|
|
36
|
+
"DS1301",
|
|
37
|
+
]);
|
|
38
|
+
|
|
21
39
|
/** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
|
|
22
40
|
const MANUAL = "manual";
|
|
23
41
|
|
|
@@ -94,17 +112,11 @@ function testedByIn(input: EmitterInput): ReadonlySet<string> {
|
|
|
94
112
|
* published API, a public member of a published type included, is `certain` where the
|
|
95
113
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
96
114
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
97
|
-
* consumer set.
|
|
98
|
-
* rule, what it contains, and a member an `in` expression tests by name are `possible`.
|
|
115
|
+
* consumer set. A member an `in` expression tests by name is `possible`.
|
|
99
116
|
*/
|
|
100
117
|
function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
101
118
|
const tested = testedByIn(input);
|
|
102
119
|
const published = publishedIds(input.swept.matrix.union);
|
|
103
|
-
const usedByTests = new Set(
|
|
104
|
-
input.swept.sweep.candidates
|
|
105
|
-
.filter((candidate) => candidate.heldByTests !== undefined)
|
|
106
|
-
.map((candidate) => candidate.id),
|
|
107
|
-
);
|
|
108
120
|
const { declared } = input.boundary.consumers;
|
|
109
121
|
const outside: Confidence =
|
|
110
122
|
declared.length === 0
|
|
@@ -113,7 +125,7 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
113
125
|
? "certain"
|
|
114
126
|
: "probable";
|
|
115
127
|
return (id) => {
|
|
116
|
-
if (
|
|
128
|
+
if (tested.has(id)) {
|
|
117
129
|
return "possible";
|
|
118
130
|
}
|
|
119
131
|
return published.has(id) ? outside : "certain";
|
|
@@ -141,7 +153,12 @@ function testCeilings(
|
|
|
141
153
|
for (const edge of union.out[union.at(id)] ?? []) {
|
|
142
154
|
const target = union.symbols[edge.to];
|
|
143
155
|
const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
|
|
144
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
target !== undefined &&
|
|
158
|
+
!testCode &&
|
|
159
|
+
union.ambient[edge.to] !== true &&
|
|
160
|
+
union.subject[edge.to] === true
|
|
161
|
+
) {
|
|
145
162
|
lowest = capped(lowest, classOf(target.id));
|
|
146
163
|
}
|
|
147
164
|
}
|
|
@@ -149,16 +166,32 @@ function testCeilings(
|
|
|
149
166
|
};
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
/**
|
|
170
|
+
* The ceiling the roots the run found put on one dead subject: a subject other declarations
|
|
171
|
+
* reference, found dead only because no root reaches them, is `probable` unless
|
|
172
|
+
* the configuration declares the roots complete, because a root the analysis cannot see
|
|
173
|
+
* would make it live; every other declaration has none below `certain`.
|
|
174
|
+
*/
|
|
175
|
+
function rootsCeiling(
|
|
176
|
+
input: EmitterInput,
|
|
177
|
+
finding: Finding,
|
|
178
|
+
candidate: Candidate | undefined,
|
|
179
|
+
): Confidence {
|
|
180
|
+
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
181
|
+
return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
|
|
182
|
+
}
|
|
183
|
+
|
|
152
184
|
/**
|
|
153
185
|
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
154
186
|
* its members listed in the canonical order where the run lists a component in full.
|
|
155
187
|
*/
|
|
156
188
|
function findingComponents(
|
|
157
189
|
swept: RunSweep,
|
|
190
|
+
components: readonly Component[],
|
|
158
191
|
cascade: Cascade,
|
|
159
192
|
): (id: string) => FindingComponent | undefined {
|
|
160
193
|
const held = new Map<string, Component>();
|
|
161
|
-
for (const component of
|
|
194
|
+
for (const component of components) {
|
|
162
195
|
for (const member of component.members) {
|
|
163
196
|
held.set(member, component);
|
|
164
197
|
}
|
|
@@ -196,6 +229,77 @@ function findingComponents(
|
|
|
196
229
|
};
|
|
197
230
|
}
|
|
198
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The run's dead components with each file a `DS1502` finding reports dead among them, so
|
|
234
|
+
* the file's declarations fall with it, and with the components each subject that is no
|
|
235
|
+
* declaration falls with joined into one.
|
|
236
|
+
*/
|
|
237
|
+
function runComponents(
|
|
238
|
+
swept: RunSweep,
|
|
239
|
+
findings: readonly Finding[],
|
|
240
|
+
): {
|
|
241
|
+
readonly components: readonly Component[];
|
|
242
|
+
readonly computed: number;
|
|
243
|
+
readonly files: ReadonlyMap<string, string>;
|
|
244
|
+
} {
|
|
245
|
+
const union = swept.matrix.union;
|
|
246
|
+
const reported = new Set(
|
|
247
|
+
findings.filter((one) => one.code === FILE_NEVER_IMPORTED).map((one) => one.position.path),
|
|
248
|
+
);
|
|
249
|
+
const files = new Map(
|
|
250
|
+
union.symbols.flatMap((symbol, at) =>
|
|
251
|
+
symbol.kind === "file" && reported.has(symbol.position.path) ? [[at, symbol] as const] : [],
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
const computed =
|
|
255
|
+
files.size === 0
|
|
256
|
+
? swept.sweep.components
|
|
257
|
+
: deadComponents(swept.matrix, swept.sweep, new Set(files.keys()));
|
|
258
|
+
const place = new Map<string, number>();
|
|
259
|
+
computed.forEach((component, at) => {
|
|
260
|
+
for (const member of component.members) {
|
|
261
|
+
place.set(member, at);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
const leader = computed.map((_component, at) => at);
|
|
265
|
+
const find = (at: number): number => {
|
|
266
|
+
let top = at;
|
|
267
|
+
while ((leader[top] ?? top) !== top) {
|
|
268
|
+
top = leader[top] ?? top;
|
|
269
|
+
}
|
|
270
|
+
return top;
|
|
271
|
+
};
|
|
272
|
+
for (const { fallsWith } of findings) {
|
|
273
|
+
const joined = (fallsWith ?? []).flatMap((id) => place.get(id) ?? []).map(find);
|
|
274
|
+
const first = Math.min(...joined);
|
|
275
|
+
for (const one of joined) {
|
|
276
|
+
leader[one] = first;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const merged = new Map<number, Component>();
|
|
280
|
+
computed.forEach((component, at) => {
|
|
281
|
+
const top = find(at);
|
|
282
|
+
const into = merged.get(top);
|
|
283
|
+
merged.set(
|
|
284
|
+
top,
|
|
285
|
+
into === undefined
|
|
286
|
+
? component
|
|
287
|
+
: {
|
|
288
|
+
id: into.id,
|
|
289
|
+
members: [...into.members, ...component.members],
|
|
290
|
+
roots: [...into.roots, ...component.roots],
|
|
291
|
+
deletableLines: into.deletableLines + component.deletableLines,
|
|
292
|
+
held: into.held && component.held,
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
components: [...merged.values()],
|
|
298
|
+
computed: computed.length,
|
|
299
|
+
files: new Map([...files.values()].map((symbol) => [symbol.position.path, symbol.id])),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
199
303
|
/**
|
|
200
304
|
* Every finding of the run completed, in the order given. A dead declaration carries its
|
|
201
305
|
* candidate's relation, counts, configurations and component; a live one carries no relation and
|
|
@@ -217,12 +321,12 @@ export function completed(
|
|
|
217
321
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
218
322
|
const classOf = reachabilityClasses(input);
|
|
219
323
|
const ceilingOf = testCeilings(input, classOf);
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
let minted =
|
|
225
|
-
const dead = new Set(
|
|
324
|
+
const { components, computed, files } = runComponents(swept, findings);
|
|
325
|
+
const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
|
|
326
|
+
const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
|
|
327
|
+
// A join keeps its first component's identifier, so a minted one counts past every computed one.
|
|
328
|
+
let minted = computed;
|
|
329
|
+
const dead = new Set(components.map((component) => component.id));
|
|
226
330
|
const fallingWith = new Map<string, number>();
|
|
227
331
|
for (const { heldByTests } of swept.sweep.candidates) {
|
|
228
332
|
if (heldByTests !== undefined) {
|
|
@@ -239,18 +343,33 @@ export function completed(
|
|
|
239
343
|
// a row is still never the declaration. Two declarations of one file can share a
|
|
240
344
|
// reference, so the one the finding is positioned at is the subject where there is one.
|
|
241
345
|
const declared = union.at(positionKey(finding.position));
|
|
346
|
+
const ref = finding.symbol.holder ?? finding.symbol.ref;
|
|
242
347
|
const at = isRowSubject(finding.symbol.kind)
|
|
243
348
|
? OUTSIDE
|
|
244
|
-
: union.symbols[declared]?.ref ===
|
|
349
|
+
: union.symbols[declared]?.ref === ref
|
|
245
350
|
? declared
|
|
246
|
-
: (positions.get(declarationKey(
|
|
351
|
+
: (positions.get(declarationKey(ref, finding.position.path)) ?? OUTSIDE);
|
|
247
352
|
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
248
353
|
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
249
354
|
const part = isPartSubject(finding.symbol.kind);
|
|
250
355
|
const symbol = union.symbols[at];
|
|
251
356
|
const judged = !part && union.subject[at] === true ? symbol : undefined;
|
|
252
|
-
const candidate =
|
|
253
|
-
|
|
357
|
+
const candidate =
|
|
358
|
+
judged === undefined || LIVE_SUBJECT.has(finding.code)
|
|
359
|
+
? undefined
|
|
360
|
+
: candidates.get(judged.id);
|
|
361
|
+
const fallsWith = finding.fallsWith?.[0];
|
|
362
|
+
const joined = fallsWith === undefined ? undefined : componentOf(fallsWith);
|
|
363
|
+
const file =
|
|
364
|
+
finding.code === FILE_NEVER_IMPORTED ? files.get(finding.position.path) : undefined;
|
|
365
|
+
const found =
|
|
366
|
+
joined !== undefined
|
|
367
|
+
? { ...joined, root: false }
|
|
368
|
+
: file !== undefined
|
|
369
|
+
? componentOf(file)
|
|
370
|
+
: symbol === undefined || part
|
|
371
|
+
? undefined
|
|
372
|
+
: componentOf(symbol.id);
|
|
254
373
|
// A test file's declaration is a component member only as a test of dead code, and
|
|
255
374
|
// test code keeps the test-support code it references out of every component.
|
|
256
375
|
if (
|
|
@@ -287,9 +406,13 @@ export function completed(
|
|
|
287
406
|
? MANUAL
|
|
288
407
|
: row.fixability;
|
|
289
408
|
const lastUses =
|
|
290
|
-
fixability
|
|
291
|
-
?
|
|
292
|
-
: undefined
|
|
409
|
+
fixability !== "deletable"
|
|
410
|
+
? undefined
|
|
411
|
+
: candidate !== undefined
|
|
412
|
+
? input.dependencies.lastUses.get(candidate.id)
|
|
413
|
+
: finding.code === FILE_NEVER_IMPORTED
|
|
414
|
+
? input.dependencies.fileLastUses.get(finding.position.path)
|
|
415
|
+
: undefined;
|
|
293
416
|
return {
|
|
294
417
|
code: finding.code,
|
|
295
418
|
kind: row.name,
|
|
@@ -298,7 +421,7 @@ export function completed(
|
|
|
298
421
|
symbol: finding.symbol,
|
|
299
422
|
reachabilityClass,
|
|
300
423
|
confidence: capped(
|
|
301
|
-
capped(reachabilityClass, row.maxClass),
|
|
424
|
+
capped(capped(reachabilityClass, row.maxClass), rootsCeiling(input, finding, candidate)),
|
|
302
425
|
judged === undefined ? "certain" : ceilingOf(judged.id),
|
|
303
426
|
),
|
|
304
427
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
@@ -1,48 +1,110 @@
|
|
|
1
1
|
import { declarationKey, type Dependencies } from "../dependencies.ts";
|
|
2
2
|
import type { Finding } from "../finding.ts";
|
|
3
3
|
import { renderRef, type DependencySection } from "../ref.ts";
|
|
4
|
-
import type { Emitter } from "./emitter.ts";
|
|
4
|
+
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
5
|
+
import { nonCodeArtifacts } from "./non-code-artifacts.ts";
|
|
5
6
|
|
|
6
7
|
const UNUSED_DEPENDENCY = "DS1601";
|
|
7
8
|
|
|
9
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
10
|
+
|
|
8
11
|
const MESSAGES: Readonly<Record<DependencySection, string>> = {
|
|
9
12
|
dependency: "no import in the target needs the declared dependency",
|
|
10
13
|
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
11
14
|
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
12
15
|
};
|
|
13
16
|
|
|
17
|
+
const DEAD_USERS_MESSAGES: Readonly<Record<DependencySection, string>> = {
|
|
18
|
+
dependency: "only code that is itself dead uses the declared dependency",
|
|
19
|
+
"dev-dependency": "only code that is itself dead uses the declared development dependency",
|
|
20
|
+
"peer-dependency": "only code that is itself dead uses the declared peer dependency",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** How the run judged the declarations and files a dependency's uses may sit in. */
|
|
24
|
+
interface DeadCode {
|
|
25
|
+
/** The members of the dead components, and the top level of each file reported unimported. */
|
|
26
|
+
readonly dead: ReadonlySet<string>;
|
|
27
|
+
/** Those found dead under reachability alone. */
|
|
28
|
+
readonly reachedOnly: ReadonlySet<string>;
|
|
29
|
+
/** The declarations of a library's published API, whose findings sit at the consumers' class. */
|
|
30
|
+
readonly published: ReadonlySet<string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The run's dead code: test code keeps the code it references out of every dead component
|
|
35
|
+
* unless that code is dead as well, and a file's top level is dead where the file is.
|
|
36
|
+
*/
|
|
37
|
+
function deadCode(input: EmitterInput): DeadCode {
|
|
38
|
+
const { swept } = input;
|
|
39
|
+
const union = swept.matrix.union;
|
|
40
|
+
const files = new Map(
|
|
41
|
+
union.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]),
|
|
42
|
+
);
|
|
43
|
+
const dead = new Set(swept.sweep.components.flatMap((component) => component.members));
|
|
44
|
+
const reachedOnly = new Set(
|
|
45
|
+
swept.sweep.candidates.filter((one) => one.relation === "reachability").map((one) => one.id),
|
|
46
|
+
);
|
|
47
|
+
for (const finding of nonCodeArtifacts(input)) {
|
|
48
|
+
const id = files.get(finding.position.path);
|
|
49
|
+
if (finding.code === FILE_NEVER_IMPORTED && id !== undefined) {
|
|
50
|
+
dead.add(id);
|
|
51
|
+
if (finding.unreachableOnly === true) {
|
|
52
|
+
reachedOnly.add(id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const published = new Set(
|
|
57
|
+
union.rooted
|
|
58
|
+
.filter((root) => root.kind === "published-api")
|
|
59
|
+
.flatMap((root) => union.symbols[root.at]?.id ?? []),
|
|
60
|
+
);
|
|
61
|
+
return { dead, reachedOnly, published };
|
|
62
|
+
}
|
|
63
|
+
|
|
14
64
|
/**
|
|
15
65
|
* A dependency, development dependency or peer dependency of a manifest the run reads
|
|
16
|
-
* that the target does not need, at the position of its key
|
|
66
|
+
* that the target does not need, at the position of its key, and one whose every use is
|
|
67
|
+
* held by dead code, which is dead only through that code and falls with its component:
|
|
68
|
+
* at `possible` where one of its holders is published, and found under reachability alone
|
|
69
|
+
* where one of them is.
|
|
17
70
|
*/
|
|
18
|
-
function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
function unusedDependencies(input: EmitterInput, dependencies: Dependencies): Finding[] {
|
|
72
|
+
const { dead, reachedOnly, published } = deadCode(input);
|
|
73
|
+
return dependencies.declared.flatMap((dependency) => {
|
|
74
|
+
const key = declarationKey(dependency);
|
|
75
|
+
const held = dependencies.heldBy.get(key);
|
|
76
|
+
const users = held?.every((id) => dead.has(id)) === true ? held : undefined;
|
|
77
|
+
if (dependencies.ran.has(key) || (dependencies.needed.has(key) && users === undefined)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const unbuilt = dependencies.unbuilt.has(dependency.position.path);
|
|
81
|
+
const atPossible = unbuilt || users?.some((id) => published.has(id)) === true;
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
code: UNUSED_DEPENDENCY,
|
|
85
|
+
position: dependency.position,
|
|
86
|
+
symbol: {
|
|
87
|
+
ref: renderRef(dependency.manifest, {
|
|
88
|
+
of: "dependency",
|
|
89
|
+
name: dependency.name,
|
|
90
|
+
section: dependency.section,
|
|
91
|
+
}),
|
|
92
|
+
kind: "dependency",
|
|
31
93
|
name: dependency.name,
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
94
|
+
sizeLines: 1,
|
|
95
|
+
},
|
|
96
|
+
message: (users === undefined ? MESSAGES : DEAD_USERS_MESSAGES)[dependency.section],
|
|
97
|
+
details: { dependencyClass: dependency.section },
|
|
98
|
+
...(atPossible ? { reachabilityClass: "possible" as const } : {}),
|
|
99
|
+
...(users?.some((id) => reachedOnly.has(id)) === true
|
|
100
|
+
? { unreachableOnly: true as const }
|
|
101
|
+
: {}),
|
|
102
|
+
...(users === undefined ? {} : { fallsWith: users }),
|
|
37
103
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
...(dependencies.unbuilt.has(dependency.position.path)
|
|
41
|
-
? { reachabilityClass: "possible" as const }
|
|
42
|
-
: {}),
|
|
43
|
-
}));
|
|
104
|
+
];
|
|
105
|
+
});
|
|
44
106
|
}
|
|
45
107
|
|
|
46
108
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
47
|
-
export const dependenciesAndModuleMachinery: Emitter = (
|
|
48
|
-
unusedDependencies(dependencies);
|
|
109
|
+
export const dependenciesAndModuleMachinery: Emitter = (input) =>
|
|
110
|
+
unusedDependencies(input, input.dependencies);
|
package/src/findings/emitters.ts
CHANGED
|
@@ -165,7 +165,7 @@ function dialledOver(
|
|
|
165
165
|
const unanswered = heldKeys(input);
|
|
166
166
|
const markup = markupKeys(input);
|
|
167
167
|
const candidates = found.filter((finding) => {
|
|
168
|
-
const key = declarationKey(finding.symbol.ref, finding.position.path);
|
|
168
|
+
const key = declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
|
|
169
169
|
return (
|
|
170
170
|
!ledger.withheld(finding) &&
|
|
171
171
|
!unanswered.has(key) &&
|