@cplieger/deadset-ts 3.5.0 → 3.6.0
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 +70 -14
- package/dist/src/components.js +5 -1
- package/dist/src/dependencies.js +493 -0
- package/dist/src/deprecation.js +83 -0
- package/dist/src/discover.js +1 -1
- package/dist/src/enum-group.js +217 -0
- package/dist/src/file-facts.js +101 -0
- package/dist/src/findings/boundary.js +1 -0
- package/dist/src/findings/completion.js +99 -0
- package/dist/src/findings/dependencies-and-module-machinery.js +65 -1
- package/dist/src/findings/emitters.js +8 -0
- package/dist/src/findings/interfaces.js +95 -2
- package/dist/src/findings/non-code-artifacts.js +97 -1
- package/dist/src/findings/reads-and-writes.js +199 -2
- package/dist/src/findings/unused-declarations.js +182 -1
- package/dist/src/findings/visibility-narrowing.js +239 -2
- package/dist/src/implementations.js +210 -0
- package/dist/src/inventory.js +2 -2
- package/dist/src/resolve.js +1 -1
- package/dist/src/stores.js +32 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +113 -16
- package/src/components.ts +6 -1
- package/src/dependencies.ts +617 -0
- package/src/deprecation.ts +101 -0
- package/src/discover.ts +1 -1
- package/src/enum-group.ts +265 -0
- package/src/file-facts.ts +143 -0
- package/src/findings/boundary.ts +39 -0
- package/src/findings/completion.ts +151 -0
- package/src/findings/dependencies-and-module-machinery.ts +79 -1
- package/src/findings/emitter.ts +21 -1
- package/src/findings/emitters.ts +13 -1
- package/src/findings/interfaces.ts +149 -3
- package/src/findings/non-code-artifacts.ts +124 -2
- package/src/findings/reads-and-writes.ts +310 -3
- package/src/findings/unused-declarations.ts +223 -2
- package/src/findings/visibility-narrowing.ts +324 -3
- package/src/implementations.ts +286 -0
- package/src/inventory.ts +2 -2
- package/src/resolve.ts +1 -1
- package/src/stores.ts +77 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a finding about a declaration carries beyond what its emitter decides, read from
|
|
3
|
+
* the run: the reachability class and the confidence, the relation that decided the
|
|
4
|
+
* subject, the dead component it falls in, the configurations it holds in, and the
|
|
5
|
+
* severity the configuration gives its code. The dials withhold what they name and
|
|
6
|
+
* every finding that falls with a root they withhold.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RunSweep } from "../analysis.ts";
|
|
10
|
+
import { componentId, type Component } from "../components.ts";
|
|
11
|
+
import type { Confidence, Config, Severity } from "../config.ts";
|
|
12
|
+
import type { Finding } from "../finding.ts";
|
|
13
|
+
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
14
|
+
import type { Relation } from "../sweep.ts";
|
|
15
|
+
|
|
16
|
+
/** The dead component a finding names, as the finding schema spells its members. */
|
|
17
|
+
export interface FindingComponent {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** Whether the subject is a root member of the component. */
|
|
20
|
+
readonly root: boolean;
|
|
21
|
+
/** The number of the component's members. */
|
|
22
|
+
readonly symbolCount: number;
|
|
23
|
+
readonly deletableLines: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One finding with the members the run decides, in this language's case. */
|
|
27
|
+
export interface CompletedFinding extends Finding {
|
|
28
|
+
readonly reachabilityClass: Confidence;
|
|
29
|
+
/** The reachability class capped by the code's ceiling. */
|
|
30
|
+
readonly confidence: Confidence;
|
|
31
|
+
/** The relation that decided the subject, absent where none did. */
|
|
32
|
+
readonly livenessRelation?: Relation;
|
|
33
|
+
/** Whether every reference to the subject comes from a test file. */
|
|
34
|
+
readonly testOnly: boolean;
|
|
35
|
+
readonly component: FindingComponent;
|
|
36
|
+
/** The configurations the finding holds in, in the run's order. */
|
|
37
|
+
readonly configurations: readonly string[];
|
|
38
|
+
readonly severity: Severity;
|
|
39
|
+
/** The per-code members of the finding schema's `details`, empty for a code with none. */
|
|
40
|
+
readonly details: Readonly<Record<string, unknown>>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
44
|
+
export const RANK: Readonly<Record<Confidence, number>> = { certain: 3, probable: 2, possible: 1 };
|
|
45
|
+
|
|
46
|
+
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
47
|
+
export function capped(found: Confidence, ceiling: Confidence): Confidence {
|
|
48
|
+
return RANK[found] <= RANK[ceiling] ? found : ceiling;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The severity the configuration gives one code: the key naming the code, else the key
|
|
53
|
+
* naming its family, else the code's default.
|
|
54
|
+
*/
|
|
55
|
+
export function severityOf(config: Config, code: string, fallback: Severity): Severity {
|
|
56
|
+
return (
|
|
57
|
+
config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The reachability class of one declaration of the run. A declaration of a library's
|
|
63
|
+
* published API has callers outside the target, and the run loads no consumer that
|
|
64
|
+
* would name them, so it is `possible`; every other declaration has every reference
|
|
65
|
+
* in the loaded program, a private member and a `#private` name among them, and is
|
|
66
|
+
* `certain`.
|
|
67
|
+
*/
|
|
68
|
+
export function reachabilityClasses(swept: RunSweep): (id: string) => Confidence {
|
|
69
|
+
const union = swept.matrix.union;
|
|
70
|
+
const published = new Set(
|
|
71
|
+
union.rooted
|
|
72
|
+
.filter((root) => root.kind === "published-api")
|
|
73
|
+
.map((root) => union.symbols[root.at]?.id),
|
|
74
|
+
);
|
|
75
|
+
return (id) => (published.has(id) ? "possible" : "certain");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The component each dead declaration of the run falls in, and none for a live one. */
|
|
79
|
+
export function findingComponents(swept: RunSweep): (id: string) => FindingComponent | undefined {
|
|
80
|
+
const held = new Map<string, Component>();
|
|
81
|
+
for (const component of swept.sweep.components) {
|
|
82
|
+
for (const member of component.members) {
|
|
83
|
+
held.set(member, component);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return (id) => {
|
|
87
|
+
const component = held.get(id);
|
|
88
|
+
return component === undefined
|
|
89
|
+
? undefined
|
|
90
|
+
: {
|
|
91
|
+
id: component.id,
|
|
92
|
+
root: component.roots.includes(id),
|
|
93
|
+
symbolCount: component.members.length,
|
|
94
|
+
deletableLines: component.deletableLines,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** One finding with the component it names. */
|
|
100
|
+
export type PlacedFinding = Finding & { readonly component: FindingComponent };
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Every finding of the run, each with its component, in the order given: the dead
|
|
104
|
+
* component its subject falls in. A subject in none, a live declaration, a file no
|
|
105
|
+
* program builds or a manifest row, falls with nothing and is given a component of its
|
|
106
|
+
* own, numbered past the run's computed components in the order the findings come, so
|
|
107
|
+
* no two families mint one identifier.
|
|
108
|
+
*/
|
|
109
|
+
export function placed(swept: RunSweep, findings: readonly Finding[]): readonly PlacedFinding[] {
|
|
110
|
+
const componentOf = findingComponents(swept);
|
|
111
|
+
const ids = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.ref, symbol.id]));
|
|
112
|
+
let minted = swept.sweep.components.length;
|
|
113
|
+
return findings.map((finding) => {
|
|
114
|
+
const id = ids.get(finding.symbol.ref);
|
|
115
|
+
const component = id === undefined ? undefined : componentOf(id);
|
|
116
|
+
if (component !== undefined) {
|
|
117
|
+
return { ...finding, component };
|
|
118
|
+
}
|
|
119
|
+
minted += 1;
|
|
120
|
+
return {
|
|
121
|
+
...finding,
|
|
122
|
+
component: {
|
|
123
|
+
id: componentId(minted),
|
|
124
|
+
root: true,
|
|
125
|
+
symbolCount: 1,
|
|
126
|
+
deletableLines: finding.symbol.sizeLines,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The findings no dial withholds. A finding at `allow` or below the minimum confidence
|
|
134
|
+
* is withheld, and so is every finding of the component of a root the dials withhold,
|
|
135
|
+
* because a symbol that falls with a root is dead only through it and its deletion
|
|
136
|
+
* does not compile while the root stays. A withheld finding that is no root withholds
|
|
137
|
+
* itself alone.
|
|
138
|
+
*/
|
|
139
|
+
export function reportable(
|
|
140
|
+
findings: readonly CompletedFinding[],
|
|
141
|
+
minConfidence: Confidence,
|
|
142
|
+
): readonly CompletedFinding[] {
|
|
143
|
+
const dialed = (finding: CompletedFinding): boolean =>
|
|
144
|
+
finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
145
|
+
const components = new Set(
|
|
146
|
+
findings
|
|
147
|
+
.filter((finding) => finding.component.root && dialed(finding))
|
|
148
|
+
.map((finding) => finding.component.id),
|
|
149
|
+
);
|
|
150
|
+
return findings.filter((finding) => !dialed(finding) && !components.has(finding.component.id));
|
|
151
|
+
}
|
|
@@ -1,4 +1,82 @@
|
|
|
1
|
+
import type { Config, Severity } from "../config.ts";
|
|
2
|
+
import type { Dependencies } from "../dependencies.ts";
|
|
3
|
+
import type { Finding } from "../finding.ts";
|
|
4
|
+
import { renderRef, type DependencySection } from "../ref.ts";
|
|
5
|
+
import { severityOf } from "./completion.ts";
|
|
1
6
|
import type { Emitter } from "./emitter.ts";
|
|
2
7
|
|
|
8
|
+
const UNUSED_DEPENDENCY = "DS1601";
|
|
9
|
+
|
|
10
|
+
/** The severity the kind ships at, which the configuration's keys override. */
|
|
11
|
+
const UNUSED_DEPENDENCY_SEVERITY: Severity = "deny";
|
|
12
|
+
|
|
13
|
+
const MESSAGES: Readonly<Record<DependencySection, string>> = {
|
|
14
|
+
dependency: "no import in the target needs the declared dependency",
|
|
15
|
+
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
16
|
+
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** An unused-dependency finding, with the section that declares the dependency. */
|
|
20
|
+
export interface UnusedDependencyFinding extends Finding {
|
|
21
|
+
readonly details: { readonly dependencyClass: DependencySection };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The dependencies the target needs: every package the projects' uses name, every
|
|
26
|
+
* declared dependency that ships a command, which a script, a workflow or a shell
|
|
27
|
+
* runs outside anything the analysis reads, and every peer a needed package
|
|
28
|
+
* requires, which the target satisfies by declaring it.
|
|
29
|
+
*/
|
|
30
|
+
function neededBy(dependencies: Dependencies): ReadonlySet<string> {
|
|
31
|
+
const needed = new Set(dependencies.needed);
|
|
32
|
+
for (const [name, installed] of dependencies.installed) {
|
|
33
|
+
if (installed.command) {
|
|
34
|
+
needed.add(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const pending = [...needed];
|
|
38
|
+
for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
|
|
39
|
+
for (const peer of dependencies.installed.get(name)?.peers ?? []) {
|
|
40
|
+
if (!needed.has(peer)) {
|
|
41
|
+
needed.add(peer);
|
|
42
|
+
pending.push(peer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return needed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A dependency, development dependency or peer dependency of the target's manifest
|
|
51
|
+
* that the target does not need, at the position of its key. Nothing is reported at
|
|
52
|
+
* `allow`. Every subject is a manifest entry, which is `certain` whatever the
|
|
53
|
+
* consumers, so no minimum confidence withholds one.
|
|
54
|
+
*/
|
|
55
|
+
function unusedDependencies(config: Config, dependencies: Dependencies): UnusedDependencyFinding[] {
|
|
56
|
+
if (severityOf(config, UNUSED_DEPENDENCY, UNUSED_DEPENDENCY_SEVERITY) === "allow") {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const needed = neededBy(dependencies);
|
|
60
|
+
return dependencies.declared
|
|
61
|
+
.filter((dependency) => !needed.has(dependency.name))
|
|
62
|
+
.map((dependency) => ({
|
|
63
|
+
code: UNUSED_DEPENDENCY,
|
|
64
|
+
position: dependency.position,
|
|
65
|
+
symbol: {
|
|
66
|
+
ref: renderRef(dependencies.manifest, {
|
|
67
|
+
of: "dependency",
|
|
68
|
+
name: dependency.name,
|
|
69
|
+
section: dependency.section,
|
|
70
|
+
}),
|
|
71
|
+
kind: "dependency",
|
|
72
|
+
name: dependency.name,
|
|
73
|
+
sizeLines: 1,
|
|
74
|
+
},
|
|
75
|
+
message: MESSAGES[dependency.section],
|
|
76
|
+
details: { dependencyClass: dependency.section },
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
3
80
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
4
|
-
export const dependenciesAndModuleMachinery: Emitter = () =>
|
|
81
|
+
export const dependenciesAndModuleMachinery: Emitter = ({ config, dependencies }) =>
|
|
82
|
+
unusedDependencies(config, dependencies);
|
package/src/findings/emitter.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import type { RunSweep } from "../analysis.ts";
|
|
2
2
|
import type { Config } from "../config.ts";
|
|
3
|
+
import type { Dependencies } from "../dependencies.ts";
|
|
4
|
+
import type { FileFacts } from "../file-facts.ts";
|
|
3
5
|
import type { Finding } from "../finding.ts";
|
|
6
|
+
import type { Implementations } from "../implementations.ts";
|
|
7
|
+
import type { Stores } from "../stores.ts";
|
|
8
|
+
import type { Boundary } from "./boundary.ts";
|
|
4
9
|
|
|
5
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* What every emitter reads: the configuration the run resolved, the run's sweep, and
|
|
12
|
+
* the facts about the run each kind family reads beside the sweep.
|
|
13
|
+
*/
|
|
6
14
|
export interface EmitterInput {
|
|
7
15
|
readonly config: Config;
|
|
8
16
|
/** The production sweep a report is built from, beside the matrix it judged. */
|
|
9
17
|
readonly swept: RunSweep;
|
|
18
|
+
/** The declarations carrying the deprecation marker in any configuration of the run. */
|
|
19
|
+
readonly deprecated: ReadonlySet<string>;
|
|
20
|
+
/** The stores into the run's declarations and the reads of them, which the sweep does not keep. */
|
|
21
|
+
readonly stores: Stores;
|
|
22
|
+
/** What the target's manifest declares, and what its projects need of it. */
|
|
23
|
+
readonly dependencies: Dependencies;
|
|
24
|
+
/** How the run's interfaces are implemented by its classes. */
|
|
25
|
+
readonly implementations: Implementations;
|
|
26
|
+
/** The target's files beside the sweep: the source tree, and what a program holds by inclusion. */
|
|
27
|
+
readonly files: FileFacts;
|
|
28
|
+
/** What can reach the target from outside it: consumers, declared edges, and `exports`. */
|
|
29
|
+
readonly boundary: Boundary;
|
|
10
30
|
}
|
|
11
31
|
|
|
12
32
|
/**
|
package/src/findings/emitters.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.ts";
|
|
2
|
-
import
|
|
2
|
+
import { placed, type PlacedFinding } from "./completion.ts";
|
|
3
|
+
import type { Emitter, EmitterInput, Emitters } from "./emitter.ts";
|
|
3
4
|
import { interfaces } from "./interfaces.ts";
|
|
4
5
|
import { intraFunction } from "./intra-function.ts";
|
|
5
6
|
import { nonCodeArtifacts } from "./non-code-artifacts.ts";
|
|
@@ -19,3 +20,14 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
|
|
|
19
20
|
["self-check", selfCheck],
|
|
20
21
|
["intra-function", intraFunction],
|
|
21
22
|
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every finding of the run, each family's in table order, each placed in its component
|
|
26
|
+
* by the one step that numbers the components minted for subjects that fall with none.
|
|
27
|
+
*/
|
|
28
|
+
export function findingsOf(input: EmitterInput): readonly PlacedFinding[] {
|
|
29
|
+
return placed(
|
|
30
|
+
input.swept,
|
|
31
|
+
[...EMITTERS.values()].flatMap((emit) => emit(input)),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -1,4 +1,150 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Finding, FindingPosition } from "../finding.ts";
|
|
2
|
+
import type { InventorySymbol } from "../inventory.ts";
|
|
3
|
+
import { byPosition } from "../position.ts";
|
|
4
|
+
import type { Candidate } from "../sweep.ts";
|
|
5
|
+
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
const UNUSED_INTERFACE = "DS1201";
|
|
8
|
+
const UNCALLED_INTERFACE_METHOD = "DS1203";
|
|
9
|
+
|
|
10
|
+
/** What each finding says, by what the sweep found about the subject's references. */
|
|
11
|
+
const UNUSED_MESSAGE = "nothing in the target names the interface as a type";
|
|
12
|
+
const UNUSED_DEAD_MESSAGE =
|
|
13
|
+
"the interface is named as a type only from declarations that are themselves dead";
|
|
14
|
+
const UNUSED_TEST_MESSAGE = "the interface is named as a type only from test files";
|
|
15
|
+
const UNCALLED_MESSAGE = "no call site invokes or selects the method through the interface";
|
|
16
|
+
|
|
17
|
+
/** One class a finding names as an implementation of its interface. */
|
|
18
|
+
interface Implementation {
|
|
19
|
+
readonly ref: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly position: FindingPosition;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A finding of this family, with the implementations its details carry. */
|
|
25
|
+
interface InterfaceFinding extends Finding {
|
|
26
|
+
readonly details: { readonly implementations: readonly Implementation[] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Where one declaration is written, as a finding carries it. */
|
|
30
|
+
function positionOf(symbol: InventorySymbol): FindingPosition {
|
|
31
|
+
return { ...symbol.position, endLine: Math.max(symbol.position.line, symbol.endLine) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Two strings ordered bytewise. */
|
|
35
|
+
function compare(a: string, b: string): number {
|
|
36
|
+
if (a === b) {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
return a < b ? -1 : 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One run's declarations and candidates, indexed for the two kinds. */
|
|
43
|
+
function indexed(input: EmitterInput): {
|
|
44
|
+
readonly symbolOf: (id: string) => InventorySymbol | undefined;
|
|
45
|
+
readonly inTest: (id: string) => boolean;
|
|
46
|
+
readonly candidateOf: (id: string) => Candidate | undefined;
|
|
47
|
+
readonly implementationsOf: (id: string) => readonly Implementation[];
|
|
48
|
+
} {
|
|
49
|
+
const union = input.swept.matrix.union;
|
|
50
|
+
const symbols = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
51
|
+
const candidates = new Map(
|
|
52
|
+
input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]),
|
|
53
|
+
);
|
|
54
|
+
const symbolOf = (id: string): InventorySymbol | undefined => symbols.get(id);
|
|
55
|
+
return {
|
|
56
|
+
symbolOf,
|
|
57
|
+
inTest: (id) => union.test[union.at(id)] === true,
|
|
58
|
+
candidateOf: (id) => candidates.get(id),
|
|
59
|
+
implementationsOf: (id) =>
|
|
60
|
+
[...(input.implementations.classes.get(id) ?? [])]
|
|
61
|
+
.map(symbolOf)
|
|
62
|
+
.filter((symbol): symbol is InventorySymbol => symbol !== undefined)
|
|
63
|
+
.map((symbol) => ({ ref: symbol.ref, name: symbol.name, position: positionOf(symbol) }))
|
|
64
|
+
.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref)),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The finding one code makes about one declaration. */
|
|
69
|
+
function findingOf(
|
|
70
|
+
code: string,
|
|
71
|
+
symbol: InventorySymbol,
|
|
72
|
+
message: string,
|
|
73
|
+
implementations: readonly Implementation[],
|
|
74
|
+
): InterfaceFinding {
|
|
75
|
+
const position = positionOf(symbol);
|
|
76
|
+
return {
|
|
77
|
+
code,
|
|
78
|
+
position,
|
|
79
|
+
symbol: {
|
|
80
|
+
ref: symbol.ref,
|
|
81
|
+
kind: symbol.kind,
|
|
82
|
+
name: symbol.name,
|
|
83
|
+
sizeLines: position.endLine - position.line + 1,
|
|
84
|
+
},
|
|
85
|
+
message,
|
|
86
|
+
details: { implementations },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
|
|
92
|
+
* interface, whose members fall with it unreported; and `DS1203`, a method of a live
|
|
93
|
+
* interface no reference names, unless it is a marker, a method whose every
|
|
94
|
+
* implementation has an empty body. A test file's declaration is neither kind's
|
|
95
|
+
* subject, because a production sweep counts none of the references it can have.
|
|
96
|
+
*/
|
|
97
|
+
export const interfaces: Emitter = (input) => {
|
|
98
|
+
const run = indexed(input);
|
|
99
|
+
const found: InterfaceFinding[] = [];
|
|
100
|
+
for (const candidate of input.swept.sweep.candidates) {
|
|
101
|
+
const symbol = run.symbolOf(candidate.id);
|
|
102
|
+
if (symbol === undefined || run.inTest(candidate.id)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (symbol.kind === "interface") {
|
|
106
|
+
found.push(
|
|
107
|
+
findingOf(
|
|
108
|
+
UNUSED_INTERFACE,
|
|
109
|
+
symbol,
|
|
110
|
+
unusedMessage(candidate),
|
|
111
|
+
run.implementationsOf(candidate.id),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
symbol.kind !== "interface-method" ||
|
|
118
|
+
candidate.productionRefs + candidate.testRefs > 0 ||
|
|
119
|
+
run.candidateOf(symbol.parent) !== undefined ||
|
|
120
|
+
marker(input, candidate.id)
|
|
121
|
+
) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
found.push(
|
|
125
|
+
findingOf(
|
|
126
|
+
UNCALLED_INTERFACE_METHOD,
|
|
127
|
+
symbol,
|
|
128
|
+
UNCALLED_MESSAGE,
|
|
129
|
+
run.implementationsOf(symbol.parent),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return found;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** What one unused interface's finding says, by the references the sweep counted. */
|
|
137
|
+
function unusedMessage(candidate: Candidate): string {
|
|
138
|
+
if (candidate.relation === "reachability") {
|
|
139
|
+
return UNUSED_DEAD_MESSAGE;
|
|
140
|
+
}
|
|
141
|
+
return candidate.testRefs > 0 && candidate.productionRefs === 0
|
|
142
|
+
? UNUSED_TEST_MESSAGE
|
|
143
|
+
: UNUSED_MESSAGE;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Whether one interface method has implementations and every one has an empty body. */
|
|
147
|
+
function marker(input: EmitterInput, method: string): boolean {
|
|
148
|
+
const bodies = [...(input.implementations.bodies.get(method)?.values() ?? [])];
|
|
149
|
+
return bodies.length > 0 && bodies.every((empty) => empty);
|
|
150
|
+
}
|
|
@@ -1,4 +1,126 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Config } from "../config.ts";
|
|
2
|
+
import type { Finding } from "../finding.ts";
|
|
3
|
+
import type { Graph } from "../graph.ts";
|
|
4
|
+
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
5
|
+
|
|
6
|
+
const FILE_NEVER_BUILT = "DS1501";
|
|
7
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
8
|
+
|
|
9
|
+
/** The subject kind of a finding about a source file, from the finding schema's vocabulary. */
|
|
10
|
+
const FILE_SUBJECT = "file";
|
|
11
|
+
|
|
12
|
+
/** A file's subject starts at its first character. */
|
|
13
|
+
const FILE_START = 1;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether the configuration declares the run's matrix complete. The declaration is
|
|
17
|
+
* about the projects the configuration lists, so a configuration declaring
|
|
18
|
+
* completeness and listing no project declares it of a matrix the run derived, and a
|
|
19
|
+
* derived matrix is never complete.
|
|
20
|
+
*/
|
|
21
|
+
function completeMatrix(config: Config): boolean {
|
|
22
|
+
return (
|
|
23
|
+
config.analysis.matrixComplete &&
|
|
24
|
+
config.analysis.configurations.some((entry) => entry.shape === "project")
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One finding about a whole file. */
|
|
29
|
+
function fileFinding(
|
|
30
|
+
code: string,
|
|
31
|
+
file: { readonly path: string; readonly ref: string; readonly endLine: number },
|
|
32
|
+
message: string,
|
|
33
|
+
): Finding {
|
|
34
|
+
return {
|
|
35
|
+
code,
|
|
36
|
+
position: { path: file.path, line: FILE_START, column: FILE_START, endLine: file.endLine },
|
|
37
|
+
symbol: { ref: file.ref, kind: FILE_SUBJECT, name: file.path, sizeLines: file.endLine },
|
|
38
|
+
message,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every source file below the target root that no project of the matrix includes,
|
|
44
|
+
* under a matrix the configuration declares complete: that declaration says the listed
|
|
45
|
+
* projects are every one the target builds, so a file none of them includes is built
|
|
46
|
+
* by nothing. Under a derived matrix nothing is reported, because a project the
|
|
47
|
+
* derivation did not find is no evidence that nothing builds the file.
|
|
48
|
+
*/
|
|
49
|
+
function fileNeverBuilt({ config, swept, files }: EmitterInput): readonly Finding[] {
|
|
50
|
+
if (!completeMatrix(config)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const built = new Set(
|
|
54
|
+
swept.matrix.union.symbols
|
|
55
|
+
.filter((symbol) => symbol.kind === FILE_SUBJECT)
|
|
56
|
+
.map((symbol) => symbol.position.path),
|
|
57
|
+
);
|
|
58
|
+
const message = `no project of the build matrix includes this file (${swept.matrix.configurations.join(", ")})`;
|
|
59
|
+
return files.tree
|
|
60
|
+
.filter((file) => !built.has(file.path))
|
|
61
|
+
.map((file) => fileFinding(FILE_NEVER_BUILT, file, message));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The paths of every file a reference written in another file reaches, naming the
|
|
66
|
+
* file or a declaration in it. An import is such a reference, so an import from any
|
|
67
|
+
* file reaches what it names, whatever reaches the importing file.
|
|
68
|
+
*/
|
|
69
|
+
function reachedFromOutside(union: Graph): ReadonlySet<string> {
|
|
70
|
+
const reached = new Set<string>();
|
|
71
|
+
union.out.forEach((edges, from) => {
|
|
72
|
+
const fromPath = union.symbols[from]?.position.path;
|
|
73
|
+
for (const edge of edges) {
|
|
74
|
+
const to = union.symbols[edge.to];
|
|
75
|
+
if (to !== undefined && to.position.path !== fromPath) {
|
|
76
|
+
reached.add(to.position.path);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return reached;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The paths of the files an import reaches, a root names, or a declared edge names a
|
|
85
|
+
* declaration in. A file holding a declaration the sweep judged live is kept whatever
|
|
86
|
+
* held it live, and so is a file a program holds by inclusion, which acts on every
|
|
87
|
+
* file of the program without an import naming it.
|
|
88
|
+
*/
|
|
89
|
+
function keptFiles({ swept, files, boundary }: EmitterInput): ReadonlySet<string> {
|
|
90
|
+
const union = swept.matrix.union;
|
|
91
|
+
const edgeNamed = new Set(boundary.edges.map((side) => side.symbol));
|
|
92
|
+
const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
|
|
93
|
+
const kept = new Set([...reachedFromOutside(union), ...files.heldByInclusion]);
|
|
94
|
+
for (const root of union.rooted) {
|
|
95
|
+
const named = union.symbols[root.at];
|
|
96
|
+
if (named !== undefined) {
|
|
97
|
+
kept.add(named.position.path);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const symbol of union.symbols) {
|
|
101
|
+
if ((symbol.kind !== FILE_SUBJECT && !dead.has(symbol.id)) || edgeNamed.has(symbol.ref)) {
|
|
102
|
+
kept.add(symbol.position.path);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return kept;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Every file of the run that no import reaches and no root or edge names. */
|
|
109
|
+
function fileNeverImported(input: EmitterInput): readonly Finding[] {
|
|
110
|
+
const kept = keptFiles(input);
|
|
111
|
+
return input.swept.matrix.union.symbols
|
|
112
|
+
.filter((symbol) => symbol.kind === FILE_SUBJECT && !kept.has(symbol.position.path))
|
|
113
|
+
.map((symbol) =>
|
|
114
|
+
fileFinding(
|
|
115
|
+
FILE_NEVER_IMPORTED,
|
|
116
|
+
{ path: symbol.position.path, ref: symbol.ref, endLine: symbol.endLine },
|
|
117
|
+
"no import reaches this file and no root names it or a declaration in it",
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
2
121
|
|
|
3
122
|
/** The findings of the non-code-artifacts family, `DS1500` to `DS1599`. */
|
|
4
|
-
export const nonCodeArtifacts: Emitter = () => [
|
|
123
|
+
export const nonCodeArtifacts: Emitter = (input) => [
|
|
124
|
+
...fileNeverBuilt(input),
|
|
125
|
+
...fileNeverImported(input),
|
|
126
|
+
];
|