@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
|
@@ -1,2 +1,95 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { byPosition } from "../position.js";
|
|
2
|
+
const UNUSED_INTERFACE = "DS1201";
|
|
3
|
+
const UNCALLED_INTERFACE_METHOD = "DS1203";
|
|
4
|
+
/** What each finding says, by what the sweep found about the subject's references. */
|
|
5
|
+
const UNUSED_MESSAGE = "nothing in the target names the interface as a type";
|
|
6
|
+
const UNUSED_DEAD_MESSAGE = "the interface is named as a type only from declarations that are themselves dead";
|
|
7
|
+
const UNUSED_TEST_MESSAGE = "the interface is named as a type only from test files";
|
|
8
|
+
const UNCALLED_MESSAGE = "no call site invokes or selects the method through the interface";
|
|
9
|
+
/** Where one declaration is written, as a finding carries it. */
|
|
10
|
+
function positionOf(symbol) {
|
|
11
|
+
return { ...symbol.position, endLine: Math.max(symbol.position.line, symbol.endLine) };
|
|
12
|
+
}
|
|
13
|
+
/** Two strings ordered bytewise. */
|
|
14
|
+
function compare(a, b) {
|
|
15
|
+
if (a === b) {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
return a < b ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
/** One run's declarations and candidates, indexed for the two kinds. */
|
|
21
|
+
function indexed(input) {
|
|
22
|
+
const union = input.swept.matrix.union;
|
|
23
|
+
const symbols = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
24
|
+
const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
25
|
+
const symbolOf = (id) => symbols.get(id);
|
|
26
|
+
return {
|
|
27
|
+
symbolOf,
|
|
28
|
+
inTest: (id) => union.test[union.at(id)] === true,
|
|
29
|
+
candidateOf: (id) => candidates.get(id),
|
|
30
|
+
implementationsOf: (id) => [...(input.implementations.classes.get(id) ?? [])]
|
|
31
|
+
.map(symbolOf)
|
|
32
|
+
.filter((symbol) => symbol !== undefined)
|
|
33
|
+
.map((symbol) => ({ ref: symbol.ref, name: symbol.name, position: positionOf(symbol) }))
|
|
34
|
+
.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** The finding one code makes about one declaration. */
|
|
38
|
+
function findingOf(code, symbol, message, implementations) {
|
|
39
|
+
const position = positionOf(symbol);
|
|
40
|
+
return {
|
|
41
|
+
code,
|
|
42
|
+
position,
|
|
43
|
+
symbol: {
|
|
44
|
+
ref: symbol.ref,
|
|
45
|
+
kind: symbol.kind,
|
|
46
|
+
name: symbol.name,
|
|
47
|
+
sizeLines: position.endLine - position.line + 1,
|
|
48
|
+
},
|
|
49
|
+
message,
|
|
50
|
+
details: { implementations },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
|
|
55
|
+
* interface, whose members fall with it unreported; and `DS1203`, a method of a live
|
|
56
|
+
* interface no reference names, unless it is a marker, a method whose every
|
|
57
|
+
* implementation has an empty body. A test file's declaration is neither kind's
|
|
58
|
+
* subject, because a production sweep counts none of the references it can have.
|
|
59
|
+
*/
|
|
60
|
+
export const interfaces = (input) => {
|
|
61
|
+
const run = indexed(input);
|
|
62
|
+
const found = [];
|
|
63
|
+
for (const candidate of input.swept.sweep.candidates) {
|
|
64
|
+
const symbol = run.symbolOf(candidate.id);
|
|
65
|
+
if (symbol === undefined || run.inTest(candidate.id)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (symbol.kind === "interface") {
|
|
69
|
+
found.push(findingOf(UNUSED_INTERFACE, symbol, unusedMessage(candidate), run.implementationsOf(candidate.id)));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (symbol.kind !== "interface-method" ||
|
|
73
|
+
candidate.productionRefs + candidate.testRefs > 0 ||
|
|
74
|
+
run.candidateOf(symbol.parent) !== undefined ||
|
|
75
|
+
marker(input, candidate.id)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
found.push(findingOf(UNCALLED_INTERFACE_METHOD, symbol, UNCALLED_MESSAGE, run.implementationsOf(symbol.parent)));
|
|
79
|
+
}
|
|
80
|
+
return found;
|
|
81
|
+
};
|
|
82
|
+
/** What one unused interface's finding says, by the references the sweep counted. */
|
|
83
|
+
function unusedMessage(candidate) {
|
|
84
|
+
if (candidate.relation === "reachability") {
|
|
85
|
+
return UNUSED_DEAD_MESSAGE;
|
|
86
|
+
}
|
|
87
|
+
return candidate.testRefs > 0 && candidate.productionRefs === 0
|
|
88
|
+
? UNUSED_TEST_MESSAGE
|
|
89
|
+
: UNUSED_MESSAGE;
|
|
90
|
+
}
|
|
91
|
+
/** Whether one interface method has implementations and every one has an empty body. */
|
|
92
|
+
function marker(input, method) {
|
|
93
|
+
const bodies = [...(input.implementations.bodies.get(method)?.values() ?? [])];
|
|
94
|
+
return bodies.length > 0 && bodies.every((empty) => empty);
|
|
95
|
+
}
|
|
@@ -1,2 +1,98 @@
|
|
|
1
|
+
const FILE_NEVER_BUILT = "DS1501";
|
|
2
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
3
|
+
/** The subject kind of a finding about a source file, from the finding schema's vocabulary. */
|
|
4
|
+
const FILE_SUBJECT = "file";
|
|
5
|
+
/** A file's subject starts at its first character. */
|
|
6
|
+
const FILE_START = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the configuration declares the run's matrix complete. The declaration is
|
|
9
|
+
* about the projects the configuration lists, so a configuration declaring
|
|
10
|
+
* completeness and listing no project declares it of a matrix the run derived, and a
|
|
11
|
+
* derived matrix is never complete.
|
|
12
|
+
*/
|
|
13
|
+
function completeMatrix(config) {
|
|
14
|
+
return (config.analysis.matrixComplete &&
|
|
15
|
+
config.analysis.configurations.some((entry) => entry.shape === "project"));
|
|
16
|
+
}
|
|
17
|
+
/** One finding about a whole file. */
|
|
18
|
+
function fileFinding(code, file, message) {
|
|
19
|
+
return {
|
|
20
|
+
code,
|
|
21
|
+
position: { path: file.path, line: FILE_START, column: FILE_START, endLine: file.endLine },
|
|
22
|
+
symbol: { ref: file.ref, kind: FILE_SUBJECT, name: file.path, sizeLines: file.endLine },
|
|
23
|
+
message,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Every source file below the target root that no project of the matrix includes,
|
|
28
|
+
* under a matrix the configuration declares complete: that declaration says the listed
|
|
29
|
+
* projects are every one the target builds, so a file none of them includes is built
|
|
30
|
+
* by nothing. Under a derived matrix nothing is reported, because a project the
|
|
31
|
+
* derivation did not find is no evidence that nothing builds the file.
|
|
32
|
+
*/
|
|
33
|
+
function fileNeverBuilt({ config, swept, files }) {
|
|
34
|
+
if (!completeMatrix(config)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const built = new Set(swept.matrix.union.symbols
|
|
38
|
+
.filter((symbol) => symbol.kind === FILE_SUBJECT)
|
|
39
|
+
.map((symbol) => symbol.position.path));
|
|
40
|
+
const message = `no project of the build matrix includes this file (${swept.matrix.configurations.join(", ")})`;
|
|
41
|
+
return files.tree
|
|
42
|
+
.filter((file) => !built.has(file.path))
|
|
43
|
+
.map((file) => fileFinding(FILE_NEVER_BUILT, file, message));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The paths of every file a reference written in another file reaches, naming the
|
|
47
|
+
* file or a declaration in it. An import is such a reference, so an import from any
|
|
48
|
+
* file reaches what it names, whatever reaches the importing file.
|
|
49
|
+
*/
|
|
50
|
+
function reachedFromOutside(union) {
|
|
51
|
+
const reached = new Set();
|
|
52
|
+
union.out.forEach((edges, from) => {
|
|
53
|
+
const fromPath = union.symbols[from]?.position.path;
|
|
54
|
+
for (const edge of edges) {
|
|
55
|
+
const to = union.symbols[edge.to];
|
|
56
|
+
if (to !== undefined && to.position.path !== fromPath) {
|
|
57
|
+
reached.add(to.position.path);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return reached;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The paths of the files an import reaches, a root names, or a declared edge names a
|
|
65
|
+
* declaration in. A file holding a declaration the sweep judged live is kept whatever
|
|
66
|
+
* held it live, and so is a file a program holds by inclusion, which acts on every
|
|
67
|
+
* file of the program without an import naming it.
|
|
68
|
+
*/
|
|
69
|
+
function keptFiles({ swept, files, boundary }) {
|
|
70
|
+
const union = swept.matrix.union;
|
|
71
|
+
const edgeNamed = new Set(boundary.edges.map((side) => side.symbol));
|
|
72
|
+
const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
|
|
73
|
+
const kept = new Set([...reachedFromOutside(union), ...files.heldByInclusion]);
|
|
74
|
+
for (const root of union.rooted) {
|
|
75
|
+
const named = union.symbols[root.at];
|
|
76
|
+
if (named !== undefined) {
|
|
77
|
+
kept.add(named.position.path);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const symbol of union.symbols) {
|
|
81
|
+
if ((symbol.kind !== FILE_SUBJECT && !dead.has(symbol.id)) || edgeNamed.has(symbol.ref)) {
|
|
82
|
+
kept.add(symbol.position.path);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return kept;
|
|
86
|
+
}
|
|
87
|
+
/** Every file of the run that no import reaches and no root or edge names. */
|
|
88
|
+
function fileNeverImported(input) {
|
|
89
|
+
const kept = keptFiles(input);
|
|
90
|
+
return input.swept.matrix.union.symbols
|
|
91
|
+
.filter((symbol) => symbol.kind === FILE_SUBJECT && !kept.has(symbol.position.path))
|
|
92
|
+
.map((symbol) => fileFinding(FILE_NEVER_IMPORTED, { path: symbol.position.path, ref: symbol.ref, endLine: symbol.endLine }, "no import reaches this file and no root names it or a declaration in it"));
|
|
93
|
+
}
|
|
1
94
|
/** The findings of the non-code-artifacts family, `DS1500` to `DS1599`. */
|
|
2
|
-
export const nonCodeArtifacts = () => [
|
|
95
|
+
export const nonCodeArtifacts = (input) => [
|
|
96
|
+
...fileNeverBuilt(input),
|
|
97
|
+
...fileNeverImported(input),
|
|
98
|
+
];
|
|
@@ -1,2 +1,146 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { positionKey } from "../position.js";
|
|
2
|
+
/** The containers whose type parameters `DS1303` reports: a function and a class method. */
|
|
3
|
+
const SIGNATURES = new Set(["function", "method"]);
|
|
4
|
+
function indexed(input) {
|
|
5
|
+
const { matrix, sweep } = input.swept;
|
|
6
|
+
return {
|
|
7
|
+
input,
|
|
8
|
+
symbols: new Map(matrix.union.symbols.map((symbol) => [symbol.id, symbol])),
|
|
9
|
+
candidates: new Map(sweep.candidates.map((candidate) => [candidate.id, candidate])),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Where one declaration is, as a finding states it. */
|
|
13
|
+
function positionOf(symbol) {
|
|
14
|
+
return {
|
|
15
|
+
path: symbol.position.path,
|
|
16
|
+
line: symbol.position.line,
|
|
17
|
+
column: symbol.position.column,
|
|
18
|
+
endLine: Math.max(symbol.position.line, symbol.endLine),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** The finding one code makes about one declaration. */
|
|
22
|
+
function findingAbout(symbol, code, message, details) {
|
|
23
|
+
const position = positionOf(symbol);
|
|
24
|
+
return {
|
|
25
|
+
code,
|
|
26
|
+
position,
|
|
27
|
+
symbol: {
|
|
28
|
+
ref: symbol.ref,
|
|
29
|
+
kind: symbol.kind,
|
|
30
|
+
name: symbol.name,
|
|
31
|
+
sizeLines: position.endLine - position.line + 1,
|
|
32
|
+
},
|
|
33
|
+
message,
|
|
34
|
+
...(details === undefined ? {} : { details }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether one declaration is a subject of the write-only kind: a module-level variable,
|
|
39
|
+
* or a class member that is state rather than code. A getter or a setter is code, so a
|
|
40
|
+
* store into one runs it.
|
|
41
|
+
*/
|
|
42
|
+
function writable(run, symbol) {
|
|
43
|
+
if (symbol.kind === "variable") {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return symbol.kind === "class-member" && !run.input.stores.accessors.has(symbol.id);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `DS1301`: a module-level variable or a class member the counted references store into
|
|
50
|
+
* and never read, named with every position written.
|
|
51
|
+
*
|
|
52
|
+
* The subject is a live declaration: a store is a reference, so the sweep holds a
|
|
53
|
+
* written one live and its candidates are never this kind's. Every reference that is
|
|
54
|
+
* not a store reads, and so does an exemption record naming the declaration.
|
|
55
|
+
*/
|
|
56
|
+
function writeOnlySymbols(run) {
|
|
57
|
+
const { stores } = run.input;
|
|
58
|
+
const { matrix } = run.input.swept;
|
|
59
|
+
const uses = new Map();
|
|
60
|
+
for (const reference of stores.references) {
|
|
61
|
+
let held = uses.get(reference.to);
|
|
62
|
+
if (held === undefined) {
|
|
63
|
+
held = { writes: [], reads: 0 };
|
|
64
|
+
uses.set(reference.to, held);
|
|
65
|
+
}
|
|
66
|
+
if (reference.use !== "write") {
|
|
67
|
+
held.reads += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const at = reference.position;
|
|
71
|
+
const key = positionKey(at);
|
|
72
|
+
if (!held.writes.some((written) => positionKey(written) === key)) {
|
|
73
|
+
held.writes.push({ path: at.path, line: at.line, column: at.column, endLine: at.line });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const found = [];
|
|
77
|
+
for (const symbol of matrix.union.symbols) {
|
|
78
|
+
const held = uses.get(symbol.id);
|
|
79
|
+
if (held === undefined ||
|
|
80
|
+
held.writes.length === 0 ||
|
|
81
|
+
held.reads > 0 ||
|
|
82
|
+
!writable(run, symbol) ||
|
|
83
|
+
stores.exempt.has(symbol.id) ||
|
|
84
|
+
run.candidates.has(symbol.id)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
|
|
88
|
+
const word = symbol.kind === "variable" ? "variable" : "member";
|
|
89
|
+
found.push(findingAbout(symbol, "DS1301", `${word} ${symbol.name} is written ${writes} and never read`, {
|
|
90
|
+
writePositions: held.writes,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The candidates no reference of any file names whose container is not itself dead, of
|
|
97
|
+
* one kind. A reference from a test file alone leaves a candidate to the test-only kind,
|
|
98
|
+
* and a member of a dead container falls with it and is reported inside its component.
|
|
99
|
+
*/
|
|
100
|
+
function unnamed(run, kind) {
|
|
101
|
+
return run.input.swept.sweep.candidates.flatMap((candidate) => {
|
|
102
|
+
const symbol = run.symbols.get(candidate.id);
|
|
103
|
+
if (symbol?.kind !== kind ||
|
|
104
|
+
candidate.productionRefs > 0 ||
|
|
105
|
+
candidate.testRefs > 0 ||
|
|
106
|
+
run.candidates.has(symbol.parent)) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
return [symbol];
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* `DS1302`: a member of an enum declaration that nothing names. A member an enum-group
|
|
114
|
+
* record holds back is no candidate, so a member a conversion can produce never reaches
|
|
115
|
+
* this kind.
|
|
116
|
+
*/
|
|
117
|
+
function unusedEnumMembers(run) {
|
|
118
|
+
return unnamed(run, "enum-member").map((symbol) => findingAbout(symbol, "DS1302", `enum member ${symbol.name} is named nowhere`));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* `DS1303`: a type parameter of a function or a class method that neither the
|
|
122
|
+
* declaration's signature nor its body names. A type parameter of a class, an interface,
|
|
123
|
+
* a type alias or a method signature belongs to a type declaration and is never
|
|
124
|
+
* reported, because a phantom parameter makes two instantiations distinct types.
|
|
125
|
+
*/
|
|
126
|
+
function unusedTypeParameters(run) {
|
|
127
|
+
return unnamed(run, "type-parameter")
|
|
128
|
+
.filter((symbol) => SIGNATURES.has(run.symbols.get(symbol.parent)?.kind ?? ""))
|
|
129
|
+
.map((symbol) => findingAbout(symbol, "DS1303", `type parameter ${symbol.name} is named in neither its signature nor its body`));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The references of the declarations the write-only kind reports, whatever the
|
|
133
|
+
* severity: a narrowing kind yields each to it, since deleting the declaration
|
|
134
|
+
* supersedes narrowing it.
|
|
135
|
+
*/
|
|
136
|
+
export function writeOnlyDeclarations(input) {
|
|
137
|
+
return new Set(writeOnlySymbols(indexed(input)).map((finding) => finding.symbol.ref));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The findings of the reads-and-writes family, `DS1300` to `DS1399`, in code order and
|
|
141
|
+
* each code's in site order.
|
|
142
|
+
*/
|
|
143
|
+
export const readsAndWrites = (input) => {
|
|
144
|
+
const run = indexed(input);
|
|
145
|
+
return [...writeOnlySymbols(run), ...unusedEnumMembers(run), ...unusedTypeParameters(run)];
|
|
146
|
+
};
|
|
@@ -1,2 +1,151 @@
|
|
|
1
|
+
import { unreachableExports } from "./visibility-narrowing.js";
|
|
2
|
+
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
3
|
+
const MEMBERS = new Set([
|
|
4
|
+
"method",
|
|
5
|
+
"class-member",
|
|
6
|
+
"type-member",
|
|
7
|
+
"enum-member",
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* The kinds of declaration no code of this family reports: an interface and a method an
|
|
11
|
+
* interface declares are the interface kinds' subjects, and a type parameter is the
|
|
12
|
+
* subject of the unused-type-parameter kind or of none.
|
|
13
|
+
*/
|
|
14
|
+
const NOT_THIS_FAMILY = new Set([
|
|
15
|
+
"file",
|
|
16
|
+
"interface",
|
|
17
|
+
"interface-method",
|
|
18
|
+
"type-parameter",
|
|
19
|
+
]);
|
|
20
|
+
/** The reader's word for each kind of declaration a finding of the family names. */
|
|
21
|
+
const WORDS = {
|
|
22
|
+
file: "file",
|
|
23
|
+
function: "function",
|
|
24
|
+
class: "class",
|
|
25
|
+
interface: "interface",
|
|
26
|
+
type: "type alias",
|
|
27
|
+
enum: "enum",
|
|
28
|
+
namespace: "namespace",
|
|
29
|
+
variable: "variable",
|
|
30
|
+
"export-alias": "re-export",
|
|
31
|
+
method: "method",
|
|
32
|
+
"class-member": "class member",
|
|
33
|
+
"interface-method": "interface method",
|
|
34
|
+
"type-member": "type member",
|
|
35
|
+
"enum-member": "enum member",
|
|
36
|
+
"type-parameter": "type parameter",
|
|
37
|
+
};
|
|
38
|
+
/** What the test-of-dead-code kind says, which states the rule it applies. */
|
|
39
|
+
const TEST_OF_DEAD_CODE = "every target symbol this test references is reported dead";
|
|
40
|
+
/**
|
|
41
|
+
* The code one candidate is reported under, or none, each symbol once under the most
|
|
42
|
+
* specific code: a test of dead code first; no other test-file declaration, member of a
|
|
43
|
+
* dead container or enum member nothing names; then deprecation, a test reference, and
|
|
44
|
+
* the unreferenced codes, an unimportable unused export being the unreachable kind's.
|
|
45
|
+
*/
|
|
46
|
+
function codeOf(judged) {
|
|
47
|
+
const { candidate, symbol } = judged;
|
|
48
|
+
if (candidate.testOfDeadCode) {
|
|
49
|
+
return "DS1005";
|
|
50
|
+
}
|
|
51
|
+
if (judged.test || judged.containerDead || NOT_THIS_FAMILY.has(symbol.kind)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const referenced = candidate.productionRefs + candidate.testRefs > 0;
|
|
55
|
+
if (symbol.kind === "enum-member" && !referenced) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
59
|
+
return "DS1006";
|
|
60
|
+
}
|
|
61
|
+
if (candidate.productionRefs === 0 && candidate.testRefs > 0) {
|
|
62
|
+
return "DS1004";
|
|
63
|
+
}
|
|
64
|
+
if (MEMBERS.has(symbol.kind)) {
|
|
65
|
+
return "DS1003";
|
|
66
|
+
}
|
|
67
|
+
if (!symbol.exported) {
|
|
68
|
+
return "DS1002";
|
|
69
|
+
}
|
|
70
|
+
return judged.unreachable ? undefined : "DS1001";
|
|
71
|
+
}
|
|
72
|
+
/** The visibility a member's message names, where it narrows the member's reach. */
|
|
73
|
+
function visibilityWord(symbol) {
|
|
74
|
+
switch (symbol.visibility) {
|
|
75
|
+
case "private":
|
|
76
|
+
case "private-name":
|
|
77
|
+
return "private ";
|
|
78
|
+
case "protected":
|
|
79
|
+
return "protected ";
|
|
80
|
+
case "public":
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* What one finding says: what the subject is, and what the analysis found about its
|
|
86
|
+
* references. A candidate reachability found has references, each from a declaration
|
|
87
|
+
* that is itself dead, so saying it has none would be false.
|
|
88
|
+
*/
|
|
89
|
+
function messageOf(code, judged) {
|
|
90
|
+
const { candidate, symbol } = judged;
|
|
91
|
+
const word = WORDS[symbol.kind];
|
|
92
|
+
switch (code) {
|
|
93
|
+
case "DS1005":
|
|
94
|
+
return TEST_OF_DEAD_CODE;
|
|
95
|
+
case "DS1006":
|
|
96
|
+
return `deprecated ${word} has no production reference`;
|
|
97
|
+
case "DS1004":
|
|
98
|
+
return `${word} is referenced only from test files and never from production code`;
|
|
99
|
+
}
|
|
100
|
+
const subject = code === "DS1003"
|
|
101
|
+
? `${visibilityWord(symbol)}${word}`
|
|
102
|
+
: symbol.kind === "export-alias"
|
|
103
|
+
? word
|
|
104
|
+
: `${symbol.exported ? "exported" : "unexported"} ${word}`;
|
|
105
|
+
if (candidate.relation === "reachability") {
|
|
106
|
+
return `${subject} is referenced only from declarations that are themselves dead`;
|
|
107
|
+
}
|
|
108
|
+
if (code === "DS1001") {
|
|
109
|
+
return `${subject} has no reference in the target and none from any loaded consumer`;
|
|
110
|
+
}
|
|
111
|
+
return `${subject} has no reference in the target`;
|
|
112
|
+
}
|
|
1
113
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
2
|
-
export const unusedDeclarations = () =>
|
|
114
|
+
export const unusedDeclarations = (input) => {
|
|
115
|
+
const union = input.swept.matrix.union;
|
|
116
|
+
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
117
|
+
const unreachable = unreachableExports(input);
|
|
118
|
+
const found = [];
|
|
119
|
+
for (const candidate of input.swept.sweep.candidates) {
|
|
120
|
+
const at = union.at(candidate.id);
|
|
121
|
+
const symbol = union.symbols[at];
|
|
122
|
+
if (symbol === undefined) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const judged = {
|
|
126
|
+
candidate,
|
|
127
|
+
symbol,
|
|
128
|
+
test: union.test[at] === true,
|
|
129
|
+
containerDead: dead.has(symbol.parent),
|
|
130
|
+
deprecated: input.deprecated.has(symbol.id),
|
|
131
|
+
unreachable: unreachable.has(symbol.id),
|
|
132
|
+
};
|
|
133
|
+
const code = codeOf(judged);
|
|
134
|
+
if (code === undefined) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const endLine = Math.max(symbol.position.line, symbol.endLine);
|
|
138
|
+
found.push({
|
|
139
|
+
code,
|
|
140
|
+
position: { ...symbol.position, endLine },
|
|
141
|
+
symbol: {
|
|
142
|
+
ref: symbol.ref,
|
|
143
|
+
kind: symbol.kind,
|
|
144
|
+
name: symbol.name,
|
|
145
|
+
sizeLines: endLine - symbol.position.line + 1,
|
|
146
|
+
},
|
|
147
|
+
message: messageOf(code, judged),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return found;
|
|
151
|
+
};
|