@cplieger/deadset-ts 3.5.0 → 3.7.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/finding.js +0 -11
- package/dist/src/findings/boundary.js +1 -0
- package/dist/src/findings/completion.js +135 -0
- package/dist/src/findings/dependencies-and-module-machinery.js +57 -1
- package/dist/src/findings/emitters.js +10 -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 +146 -2
- package/dist/src/findings/unused-declarations.js +150 -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/kinds.js +47 -45
- 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/finding.ts +76 -10
- package/src/findings/boundary.ts +39 -0
- package/src/findings/completion.ts +163 -0
- package/src/findings/dependencies-and-module-machinery.ts +64 -1
- package/src/findings/emitter.ts +24 -3
- package/src/findings/emitters.ts +13 -1
- package/src/findings/interfaces.ts +137 -3
- package/src/findings/non-code-artifacts.ts +124 -2
- package/src/findings/reads-and-writes.ts +196 -3
- package/src/findings/unused-declarations.ts +175 -1
- package/src/findings/visibility-narrowing.ts +319 -3
- package/src/implementations.ts +286 -0
- package/src/inventory.ts +2 -2
- package/src/kinds.ts +79 -45
- package/src/resolve.ts +1 -1
- package/src/stores.ts +77 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every finding carries beyond what its emitter decides, read from the run and from
|
|
3
|
+
* the code's row of the issue-kind vocabulary, and the dials that withhold a finding
|
|
4
|
+
* together with every finding that falls with a root they withhold.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { RunSweep } from "../analysis.ts";
|
|
8
|
+
import { componentId, type Component } from "../components.ts";
|
|
9
|
+
import type { Confidence, Config, Severity } from "../config.ts";
|
|
10
|
+
import type { CompletedFinding, Finding, FindingComponent } from "../finding.ts";
|
|
11
|
+
import { OUTSIDE } from "../graph.ts";
|
|
12
|
+
import { KINDS } from "../kinds.ts";
|
|
13
|
+
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
14
|
+
import type { EmitterInput } from "./emitter.ts";
|
|
15
|
+
|
|
16
|
+
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
17
|
+
const RANK: Readonly<Record<Confidence, number>> = { certain: 3, probable: 2, possible: 1 };
|
|
18
|
+
|
|
19
|
+
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
20
|
+
function capped(found: Confidence, ceiling: Confidence): Confidence {
|
|
21
|
+
return RANK[found] <= RANK[ceiling] ? found : ceiling;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The severity the configuration gives one code: the key naming the code, else the key
|
|
26
|
+
* naming its family, else the code's default.
|
|
27
|
+
*/
|
|
28
|
+
function severityOf(config: Config, code: string, fallback: Severity): Severity {
|
|
29
|
+
return (
|
|
30
|
+
config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The reachability class of one declaration of the run. A declaration of a library's
|
|
36
|
+
* published API has callers outside the target, and the run loads no consumer that
|
|
37
|
+
* would name them, so it is `possible`; every other declaration has every reference
|
|
38
|
+
* in the loaded program, a private member and a `#private` name among them, and is
|
|
39
|
+
* `certain`.
|
|
40
|
+
*/
|
|
41
|
+
function reachabilityClasses(swept: RunSweep): (id: string) => Confidence {
|
|
42
|
+
const union = swept.matrix.union;
|
|
43
|
+
const published = new Set(
|
|
44
|
+
union.rooted
|
|
45
|
+
.filter((root) => root.kind === "published-api")
|
|
46
|
+
.map((root) => union.symbols[root.at]?.id),
|
|
47
|
+
);
|
|
48
|
+
return (id) => (published.has(id) ? "possible" : "certain");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The component each dead declaration of the run falls in, and none for a live one. */
|
|
52
|
+
function findingComponents(swept: RunSweep): (id: string) => FindingComponent | undefined {
|
|
53
|
+
const held = new Map<string, Component>();
|
|
54
|
+
for (const component of swept.sweep.components) {
|
|
55
|
+
for (const member of component.members) {
|
|
56
|
+
held.set(member, component);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return (id) => {
|
|
60
|
+
const component = held.get(id);
|
|
61
|
+
return component === undefined
|
|
62
|
+
? undefined
|
|
63
|
+
: {
|
|
64
|
+
id: component.id,
|
|
65
|
+
root: component.roots.includes(id),
|
|
66
|
+
symbolCount: component.members.length,
|
|
67
|
+
deletableLines: component.deletableLines,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Every finding of the run completed, in the order given. A dead declaration carries its
|
|
74
|
+
* candidate's relation, counts, configurations and component; a live one carries no
|
|
75
|
+
* relation and holds where it is declared; a file or a manifest row is `certain` and
|
|
76
|
+
* holds in every configuration. A subject in no dead component gets one of its own,
|
|
77
|
+
* numbered past the computed ones in finding order, so no two families mint one.
|
|
78
|
+
*/
|
|
79
|
+
export function completed(
|
|
80
|
+
input: EmitterInput,
|
|
81
|
+
findings: readonly Finding[],
|
|
82
|
+
): readonly CompletedFinding[] {
|
|
83
|
+
const { swept } = input;
|
|
84
|
+
const union = swept.matrix.union;
|
|
85
|
+
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
86
|
+
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
87
|
+
const classOf = reachabilityClasses(swept);
|
|
88
|
+
const componentOf = findingComponents(swept);
|
|
89
|
+
let minted = swept.sweep.components.length;
|
|
90
|
+
|
|
91
|
+
return findings.map((finding): CompletedFinding => {
|
|
92
|
+
const row = KINDS.get(finding.code);
|
|
93
|
+
if (row === undefined) {
|
|
94
|
+
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
95
|
+
}
|
|
96
|
+
const at = positions.get(finding.symbol.ref) ?? OUTSIDE;
|
|
97
|
+
const symbol = union.symbols[at];
|
|
98
|
+
const judged = union.subject[at] === true ? symbol : undefined;
|
|
99
|
+
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
100
|
+
const found = symbol === undefined ? undefined : componentOf(symbol.id);
|
|
101
|
+
if (candidate !== undefined && found === undefined) {
|
|
102
|
+
throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
|
|
103
|
+
}
|
|
104
|
+
let component = found;
|
|
105
|
+
if (component === undefined) {
|
|
106
|
+
minted += 1;
|
|
107
|
+
component = {
|
|
108
|
+
id: componentId(minted),
|
|
109
|
+
root: true,
|
|
110
|
+
symbolCount: 1,
|
|
111
|
+
deletableLines: finding.symbol.sizeLines,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const counts =
|
|
115
|
+
candidate === undefined
|
|
116
|
+
? union.made[at]
|
|
117
|
+
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
118
|
+
const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
|
|
119
|
+
return {
|
|
120
|
+
code: finding.code,
|
|
121
|
+
kind: row.name,
|
|
122
|
+
language: "ts",
|
|
123
|
+
position: finding.position,
|
|
124
|
+
symbol: finding.symbol,
|
|
125
|
+
reachabilityClass,
|
|
126
|
+
confidence: capped(reachabilityClass, row.maxClass),
|
|
127
|
+
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
128
|
+
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
129
|
+
generated: false,
|
|
130
|
+
component,
|
|
131
|
+
retainedBy: [],
|
|
132
|
+
configurations:
|
|
133
|
+
candidate?.configurations ??
|
|
134
|
+
(judged === undefined ? swept.matrix.configurations : (swept.matrix.heldIn[at] ?? [])),
|
|
135
|
+
consumersLoaded: input.boundary.consumers.loaded,
|
|
136
|
+
fixability: row.fixability,
|
|
137
|
+
severity: severityOf(input.config, finding.code, row.defaultSeverity),
|
|
138
|
+
message: finding.message,
|
|
139
|
+
details: finding.details ?? {},
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The findings no dial withholds. A finding at `allow` or below the minimum confidence
|
|
146
|
+
* is withheld, and so is every finding of the component of a root the dials withhold,
|
|
147
|
+
* because a symbol that falls with a root is dead only through it and its deletion
|
|
148
|
+
* does not compile while the root stays. A withheld finding that is no root withholds
|
|
149
|
+
* itself alone.
|
|
150
|
+
*/
|
|
151
|
+
export function reportable(
|
|
152
|
+
findings: readonly CompletedFinding[],
|
|
153
|
+
minConfidence: Confidence,
|
|
154
|
+
): readonly CompletedFinding[] {
|
|
155
|
+
const dialed = (finding: CompletedFinding): boolean =>
|
|
156
|
+
finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
157
|
+
const components = new Set(
|
|
158
|
+
findings
|
|
159
|
+
.filter((finding) => finding.component.root && dialed(finding))
|
|
160
|
+
.map((finding) => finding.component.id),
|
|
161
|
+
);
|
|
162
|
+
return findings.filter((finding) => !dialed(finding) && !components.has(finding.component.id));
|
|
163
|
+
}
|
|
@@ -1,4 +1,67 @@
|
|
|
1
|
+
import type { Dependencies } from "../dependencies.ts";
|
|
2
|
+
import type { Finding } from "../finding.ts";
|
|
3
|
+
import { renderRef, type DependencySection } from "../ref.ts";
|
|
1
4
|
import type { Emitter } from "./emitter.ts";
|
|
2
5
|
|
|
6
|
+
const UNUSED_DEPENDENCY = "DS1601";
|
|
7
|
+
|
|
8
|
+
const MESSAGES: Readonly<Record<DependencySection, string>> = {
|
|
9
|
+
dependency: "no import in the target needs the declared dependency",
|
|
10
|
+
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
11
|
+
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The dependencies the target needs: every package the projects' uses name, every
|
|
16
|
+
* declared dependency that ships a command, which a script, a workflow or a shell
|
|
17
|
+
* runs outside anything the analysis reads, and every peer a needed package
|
|
18
|
+
* requires, which the target satisfies by declaring it.
|
|
19
|
+
*/
|
|
20
|
+
function neededBy(dependencies: Dependencies): ReadonlySet<string> {
|
|
21
|
+
const needed = new Set(dependencies.needed);
|
|
22
|
+
for (const [name, installed] of dependencies.installed) {
|
|
23
|
+
if (installed.command) {
|
|
24
|
+
needed.add(name);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const pending = [...needed];
|
|
28
|
+
for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
|
|
29
|
+
for (const peer of dependencies.installed.get(name)?.peers ?? []) {
|
|
30
|
+
if (!needed.has(peer)) {
|
|
31
|
+
needed.add(peer);
|
|
32
|
+
pending.push(peer);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return needed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A dependency, development dependency or peer dependency of the target's manifest
|
|
41
|
+
* that the target does not need, at the position of its key.
|
|
42
|
+
*/
|
|
43
|
+
function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
44
|
+
const needed = neededBy(dependencies);
|
|
45
|
+
return dependencies.declared
|
|
46
|
+
.filter((dependency) => !needed.has(dependency.name))
|
|
47
|
+
.map((dependency) => ({
|
|
48
|
+
code: UNUSED_DEPENDENCY,
|
|
49
|
+
position: dependency.position,
|
|
50
|
+
symbol: {
|
|
51
|
+
ref: renderRef(dependencies.manifest, {
|
|
52
|
+
of: "dependency",
|
|
53
|
+
name: dependency.name,
|
|
54
|
+
section: dependency.section,
|
|
55
|
+
}),
|
|
56
|
+
kind: "dependency",
|
|
57
|
+
name: dependency.name,
|
|
58
|
+
sizeLines: 1,
|
|
59
|
+
},
|
|
60
|
+
message: MESSAGES[dependency.section],
|
|
61
|
+
details: { dependencyClass: dependency.section },
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
3
65
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
4
|
-
export const dependenciesAndModuleMachinery: Emitter = () =>
|
|
66
|
+
export const dependenciesAndModuleMachinery: Emitter = ({ dependencies }) =>
|
|
67
|
+
unusedDependencies(dependencies);
|
package/src/findings/emitter.ts
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
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
|
/**
|
|
13
|
-
* One kind family's rule over one run:
|
|
14
|
-
* of the Contract's code space,
|
|
33
|
+
* One kind family's rule over one run: every finding of the codes in its family's range
|
|
34
|
+
* of the Contract's code space, whatever severity and minimum confidence the
|
|
35
|
+
* configuration sets, in no particular order.
|
|
15
36
|
*/
|
|
16
37
|
export type Emitter = (input: EmitterInput) => readonly Finding[];
|
|
17
38
|
|
package/src/findings/emitters.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CompletedFinding } from "../finding.ts";
|
|
3
|
+
import { completed, reportable } from "./completion.ts";
|
|
4
|
+
import type { Emitter, EmitterInput, Emitters } from "./emitter.ts";
|
|
3
5
|
import { interfaces } from "./interfaces.ts";
|
|
4
6
|
import { intraFunction } from "./intra-function.ts";
|
|
5
7
|
import { nonCodeArtifacts } from "./non-code-artifacts.ts";
|
|
@@ -19,3 +21,13 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
|
|
|
19
21
|
["self-check", selfCheck],
|
|
20
22
|
["intra-function", intraFunction],
|
|
21
23
|
]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every finding of the run the dials do not withhold, each family's in table order: every
|
|
27
|
+
* family's findings completed by one step, then withheld by one step, so a root one
|
|
28
|
+
* family reports withholds what another family reports in its component.
|
|
29
|
+
*/
|
|
30
|
+
export function findingsOf(input: EmitterInput): readonly CompletedFinding[] {
|
|
31
|
+
const found = [...EMITTERS.values()].flatMap((emit) => emit(input));
|
|
32
|
+
return reportable(completed(input, found), input.config.analysis.minConfidence);
|
|
33
|
+
}
|
|
@@ -1,4 +1,138 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Finding, FindingPosition, PositionedSymbol } 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
|
+
/** Where one declaration is written, as a finding carries it. */
|
|
18
|
+
function positionOf(symbol: InventorySymbol): FindingPosition {
|
|
19
|
+
return { ...symbol.position, endLine: Math.max(symbol.position.line, symbol.endLine) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Two strings ordered bytewise. */
|
|
23
|
+
function compare(a: string, b: string): number {
|
|
24
|
+
if (a === b) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
return a < b ? -1 : 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One run's declarations and candidates, indexed for the two kinds. */
|
|
31
|
+
function indexed(input: EmitterInput): {
|
|
32
|
+
readonly symbolOf: (id: string) => InventorySymbol | undefined;
|
|
33
|
+
readonly inTest: (id: string) => boolean;
|
|
34
|
+
readonly candidateOf: (id: string) => Candidate | undefined;
|
|
35
|
+
readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
|
|
36
|
+
} {
|
|
37
|
+
const union = input.swept.matrix.union;
|
|
38
|
+
const symbols = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
39
|
+
const candidates = new Map(
|
|
40
|
+
input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]),
|
|
41
|
+
);
|
|
42
|
+
const symbolOf = (id: string): InventorySymbol | undefined => symbols.get(id);
|
|
43
|
+
return {
|
|
44
|
+
symbolOf,
|
|
45
|
+
inTest: (id) => union.test[union.at(id)] === true,
|
|
46
|
+
candidateOf: (id) => candidates.get(id),
|
|
47
|
+
implementationsOf: (id) =>
|
|
48
|
+
[...(input.implementations.classes.get(id) ?? [])]
|
|
49
|
+
.map(symbolOf)
|
|
50
|
+
.filter((symbol): symbol is InventorySymbol => symbol !== undefined)
|
|
51
|
+
.map((symbol) => ({ ref: symbol.ref, name: symbol.name, position: positionOf(symbol) }))
|
|
52
|
+
.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref)),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The finding one code makes about one declaration. */
|
|
57
|
+
function findingOf(
|
|
58
|
+
code: string,
|
|
59
|
+
symbol: InventorySymbol,
|
|
60
|
+
message: string,
|
|
61
|
+
implementations: readonly PositionedSymbol[],
|
|
62
|
+
): Finding {
|
|
63
|
+
const position = positionOf(symbol);
|
|
64
|
+
return {
|
|
65
|
+
code,
|
|
66
|
+
position,
|
|
67
|
+
symbol: {
|
|
68
|
+
ref: symbol.ref,
|
|
69
|
+
kind: symbol.kind,
|
|
70
|
+
name: symbol.name,
|
|
71
|
+
sizeLines: position.endLine - position.line + 1,
|
|
72
|
+
},
|
|
73
|
+
message,
|
|
74
|
+
details: { implementations },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
|
|
80
|
+
* interface, whose members fall with it unreported; and `DS1203`, a method of a live
|
|
81
|
+
* interface no reference names, unless it is a marker, a method whose every
|
|
82
|
+
* implementation has an empty body. A test file's declaration is neither kind's
|
|
83
|
+
* subject, because a production sweep counts none of the references it can have.
|
|
84
|
+
*/
|
|
85
|
+
export const interfaces: Emitter = (input) => {
|
|
86
|
+
const run = indexed(input);
|
|
87
|
+
const found: Finding[] = [];
|
|
88
|
+
for (const candidate of input.swept.sweep.candidates) {
|
|
89
|
+
const symbol = run.symbolOf(candidate.id);
|
|
90
|
+
if (symbol === undefined || run.inTest(candidate.id)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (symbol.kind === "interface") {
|
|
94
|
+
found.push(
|
|
95
|
+
findingOf(
|
|
96
|
+
UNUSED_INTERFACE,
|
|
97
|
+
symbol,
|
|
98
|
+
unusedMessage(candidate),
|
|
99
|
+
run.implementationsOf(candidate.id),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
symbol.kind !== "interface-method" ||
|
|
106
|
+
candidate.productionRefs + candidate.testRefs > 0 ||
|
|
107
|
+
run.candidateOf(symbol.parent) !== undefined ||
|
|
108
|
+
marker(input, candidate.id)
|
|
109
|
+
) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
found.push(
|
|
113
|
+
findingOf(
|
|
114
|
+
UNCALLED_INTERFACE_METHOD,
|
|
115
|
+
symbol,
|
|
116
|
+
UNCALLED_MESSAGE,
|
|
117
|
+
run.implementationsOf(symbol.parent),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return found;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** What one unused interface's finding says, by the references the sweep counted. */
|
|
125
|
+
function unusedMessage(candidate: Candidate): string {
|
|
126
|
+
if (candidate.relation === "reachability") {
|
|
127
|
+
return UNUSED_DEAD_MESSAGE;
|
|
128
|
+
}
|
|
129
|
+
return candidate.testRefs > 0 && candidate.productionRefs === 0
|
|
130
|
+
? UNUSED_TEST_MESSAGE
|
|
131
|
+
: UNUSED_MESSAGE;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Whether one interface method has implementations and every one has an empty body. */
|
|
135
|
+
function marker(input: EmitterInput, method: string): boolean {
|
|
136
|
+
const bodies = [...(input.implementations.bodies.get(method)?.values() ?? [])];
|
|
137
|
+
return bodies.length > 0 && bodies.every((empty) => empty);
|
|
138
|
+
}
|
|
@@ -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
|
+
];
|