@cplieger/deadset-ts 3.4.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/README.md +1 -1
- package/dist/src/analysis.js +70 -14
- package/dist/src/components.js +66 -107
- package/dist/src/config.js +6 -0
- 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/kinds.js +40 -0
- package/dist/src/print.js +10 -1
- package/dist/src/resolve.js +91 -10
- package/dist/src/run.js +9 -3
- package/dist/src/schema.js +37 -1
- package/dist/src/scope.js +55 -21
- package/dist/src/serialization-contract.js +109 -19
- package/dist/src/stores.js +32 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +113 -16
- package/src/components.ts +73 -131
- package/src/config.ts +37 -2
- 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/kinds.ts +41 -0
- package/src/print.ts +17 -1
- package/src/resolve.ts +124 -10
- package/src/run.ts +10 -3
- package/src/schema.ts +47 -2
- package/src/scope.ts +63 -22
- package/src/serialization-contract.ts +138 -20
- package/src/stores.ts +77 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The enum-group class: every member of an `enum` declaration is held back when a value
|
|
3
|
+
* of the enum arrives by conversion rather than by a member's name. The conversions are
|
|
4
|
+
* a type assertion from a number or a string, an element access on the enum object
|
|
5
|
+
* keyed by anything but literal text, and a decoded value typed as the enum: an `any`
|
|
6
|
+
* or `unknown` expression asserted to it, or bound to a variable or returned from a
|
|
7
|
+
* function it annotates, where a number counts too. An enum's own value converts nothing.
|
|
8
|
+
*/
|
|
9
|
+
import { isArrowFunction, isAsExpression, isElementAccessExpression, isEnumDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isParenthesizedExpression, isParenthesizedTypeNode, isPropertyAccessExpression, isReturnStatement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
10
|
+
import { TypeFlags } from "@typescript/native/unstable/sync";
|
|
11
|
+
import { aliasChains } from "./alias-chain.js";
|
|
12
|
+
import { nodeKey } from "./inventory.js";
|
|
13
|
+
import { renderPosition } from "./position.js";
|
|
14
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
15
|
+
/** The operand types a conversion produces an enum value from. */
|
|
16
|
+
const NUMBER = TypeFlags.NumberLike;
|
|
17
|
+
const STRING = TypeFlags.StringLike;
|
|
18
|
+
const DECODED = TypeFlags.Any | TypeFlags.Unknown;
|
|
19
|
+
/** The expression one node is, through however many parentheses are written around it. */
|
|
20
|
+
function unparenthesized(node) {
|
|
21
|
+
let held = node;
|
|
22
|
+
while (isParenthesizedExpression(held)) {
|
|
23
|
+
held = held.expression;
|
|
24
|
+
}
|
|
25
|
+
return held;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The name node a type node names its declaration by: the identifier of a type
|
|
29
|
+
* reference, the right side of a qualified one, through parentheses. A type that is no
|
|
30
|
+
* reference names no declaration.
|
|
31
|
+
*/
|
|
32
|
+
function typeName(type) {
|
|
33
|
+
let held = type;
|
|
34
|
+
while (held !== undefined && isParenthesizedTypeNode(held)) {
|
|
35
|
+
held = held.type;
|
|
36
|
+
}
|
|
37
|
+
if (held?.kind !== SyntaxKind.TypeReference) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const name = held.typeName;
|
|
41
|
+
if (name.kind === SyntaxKind.QualifiedName) {
|
|
42
|
+
return name.right;
|
|
43
|
+
}
|
|
44
|
+
return name;
|
|
45
|
+
}
|
|
46
|
+
/** The name node an expression names an object by: an identifier, or a property access's name. */
|
|
47
|
+
function objectName(expression) {
|
|
48
|
+
const held = unparenthesized(expression);
|
|
49
|
+
if (isIdentifier(held)) {
|
|
50
|
+
return held;
|
|
51
|
+
}
|
|
52
|
+
return isPropertyAccessExpression(held) ? held.name : undefined;
|
|
53
|
+
}
|
|
54
|
+
/** The return type annotation of the function a return statement leaves, where it has one. */
|
|
55
|
+
function returnTypeOf(statement) {
|
|
56
|
+
for (let at = statement.parent; at.kind !== SyntaxKind.SourceFile; at = at.parent) {
|
|
57
|
+
if (isFunctionDeclaration(at) ||
|
|
58
|
+
isMethodDeclaration(at) ||
|
|
59
|
+
isFunctionExpression(at) ||
|
|
60
|
+
isArrowFunction(at) ||
|
|
61
|
+
isGetAccessorDeclaration(at)) {
|
|
62
|
+
return at.type;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/** Every site of one file that may produce a value of a type it names, in source order. */
|
|
68
|
+
function sitesOf(file) {
|
|
69
|
+
const found = [];
|
|
70
|
+
const annotated = (type, value, at) => {
|
|
71
|
+
const name = typeName(type);
|
|
72
|
+
if (name !== undefined && value !== undefined) {
|
|
73
|
+
found.push({ form: "annotation", name, operand: value, at });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const visit = (node) => {
|
|
77
|
+
if (isAsExpression(node) || node.kind === SyntaxKind.TypeAssertionExpression) {
|
|
78
|
+
const assertion = node;
|
|
79
|
+
const name = typeName(assertion.type);
|
|
80
|
+
if (name !== undefined) {
|
|
81
|
+
found.push({ form: "assertion", name, operand: assertion.expression, at: node });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (isElementAccessExpression(node)) {
|
|
85
|
+
const key = node.argumentExpression;
|
|
86
|
+
const name = objectName(node.expression);
|
|
87
|
+
if (name !== undefined && !isStringLiteral(key) && !isNoSubstitutionTemplateLiteral(key)) {
|
|
88
|
+
found.push({ form: "lookup", name, operand: undefined, at: node });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (isVariableDeclaration(node)) {
|
|
92
|
+
annotated(node.type, node.initializer, node.initializer ?? node);
|
|
93
|
+
}
|
|
94
|
+
else if (isReturnStatement(node) && node.expression !== undefined) {
|
|
95
|
+
annotated(returnTypeOf(node), node.expression, node.expression);
|
|
96
|
+
}
|
|
97
|
+
node.forEachChild(visit);
|
|
98
|
+
};
|
|
99
|
+
file.forEachChild(visit);
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The parts of one type that are not values of an enum: its constituents where it is a
|
|
104
|
+
* union, and itself otherwise. An enum's own value, or one of its members, is a value the
|
|
105
|
+
* source names, whichever number or string it also is.
|
|
106
|
+
*/
|
|
107
|
+
function partsOf(type) {
|
|
108
|
+
const parts = type.isUnionType() ? type.getTypes() : [type];
|
|
109
|
+
return parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The detail one site records about the enum it produces, or undefined where the
|
|
113
|
+
* operand's type converts nothing: a number, or a string an assertion converts, is
|
|
114
|
+
* converted, a value of type `any` or `unknown` is decoded, and a value of an enum is one
|
|
115
|
+
* the source names.
|
|
116
|
+
*/
|
|
117
|
+
function detailOf(site, enumName, operand) {
|
|
118
|
+
if (site.form === "lookup") {
|
|
119
|
+
return `looked up by an element access on ${enumName}`;
|
|
120
|
+
}
|
|
121
|
+
if (operand === undefined) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const parts = partsOf(operand);
|
|
125
|
+
if (parts.some((part) => (part.flags & NUMBER) !== 0)) {
|
|
126
|
+
return "converted from number";
|
|
127
|
+
}
|
|
128
|
+
if (parts.some((part) => (part.flags & STRING) !== 0)) {
|
|
129
|
+
return site.form === "assertion" ? "converted from string" : undefined;
|
|
130
|
+
}
|
|
131
|
+
if (parts.some((part) => (part.flags & DECODED) !== 0)) {
|
|
132
|
+
return `decoded as ${enumName}`;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The enum-group detector: every member of each enum declaration of the target that a
|
|
138
|
+
* conversion produces a value of, recorded at each conversion.
|
|
139
|
+
*/
|
|
140
|
+
export function enumGroup(input) {
|
|
141
|
+
const { project, held, targetRoot } = input;
|
|
142
|
+
const files = project.ownSourceFiles();
|
|
143
|
+
// The enums of the target, by the identifier of their declaration, with their names.
|
|
144
|
+
const enums = new Map();
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const visit = (node) => {
|
|
147
|
+
if (isEnumDeclaration(node)) {
|
|
148
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
149
|
+
if (id !== undefined) {
|
|
150
|
+
enums.set(id, node.name.getText());
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
node.forEachChild(visit);
|
|
154
|
+
};
|
|
155
|
+
file.forEachChild(visit);
|
|
156
|
+
}
|
|
157
|
+
if (enums.size === 0) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const sites = files.flatMap(sitesOf);
|
|
161
|
+
const named = new Map();
|
|
162
|
+
const chains = aliasChains(project, held);
|
|
163
|
+
for (let from = 0; from < sites.length; from += DEFAULT_BATCH_CAP) {
|
|
164
|
+
const run = sites.slice(from, from + DEFAULT_BATCH_CAP);
|
|
165
|
+
project.symbolsAt(run.map((site) => project.handle(site.name))).forEach((symbol, index) => {
|
|
166
|
+
const site = run[index];
|
|
167
|
+
if (symbol === undefined || site === undefined) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
|
|
171
|
+
if (target !== undefined) {
|
|
172
|
+
named.set(site, target.id);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const typed = sites.filter((site) => named.has(site) && site.operand !== undefined);
|
|
177
|
+
const operandType = new Map();
|
|
178
|
+
for (let from = 0; from < typed.length; from += DEFAULT_BATCH_CAP) {
|
|
179
|
+
const run = typed.slice(from, from + DEFAULT_BATCH_CAP);
|
|
180
|
+
project.checker
|
|
181
|
+
.getTypeAtLocation(run.map((site) => site.operand ?? site.at))
|
|
182
|
+
.forEach((type, index) => {
|
|
183
|
+
const site = run[index];
|
|
184
|
+
if (type !== undefined && site !== undefined) {
|
|
185
|
+
operandType.set(site, type);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const members = new Map();
|
|
190
|
+
for (const symbol of held.symbols) {
|
|
191
|
+
if (symbol.kind === "enum-member" && enums.has(symbol.parent)) {
|
|
192
|
+
const listed = members.get(symbol.parent);
|
|
193
|
+
if (listed === undefined) {
|
|
194
|
+
members.set(symbol.parent, [symbol.id]);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
listed.push(symbol.id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const found = [];
|
|
202
|
+
for (const site of sites) {
|
|
203
|
+
const enumId = named.get(site);
|
|
204
|
+
if (enumId === undefined) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const detail = detailOf(site, enums.get(enumId) ?? "", operandType.get(site));
|
|
208
|
+
if (detail === undefined) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
|
|
212
|
+
for (const id of members.get(enumId) ?? []) {
|
|
213
|
+
found.push({ id, detail, site: position });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return found;
|
|
217
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the file kinds read about the target's files beside the sweep: every source
|
|
3
|
+
* file below the target root, whether or not a project includes it, and the files a
|
|
4
|
+
* program holds by inclusion rather than by an import.
|
|
5
|
+
*/
|
|
6
|
+
import { IGNORED_DIRECTORIES } from "./discover.js";
|
|
7
|
+
import { packageScope } from "./inventory.js";
|
|
8
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
9
|
+
import { renderRef } from "./ref.js";
|
|
10
|
+
/** A file name the compiler reads as a source file. */
|
|
11
|
+
const SOURCE_FILE = /\.(?:ts|tsx|mts|cts)$/u;
|
|
12
|
+
/** A file name the compiler reads as a declaration file, which no build emits code from. */
|
|
13
|
+
const DECLARATION_FILE = /\.d\.(?:[^./]+\.)?[mc]?ts$/u;
|
|
14
|
+
/** A line break as the compiler counts lines. */
|
|
15
|
+
const LINE_BREAK = /\r\n|[\n\r\u2028\u2029]/gu;
|
|
16
|
+
/** The line of the last character of one text, counted from one; an empty text is one line. */
|
|
17
|
+
function lastLine(text) {
|
|
18
|
+
const last = Math.max(text.length - 1, 0);
|
|
19
|
+
let line = 1;
|
|
20
|
+
for (const found of text.matchAll(LINE_BREAK)) {
|
|
21
|
+
if (found.index + found[0].length > last) {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
line += 1;
|
|
25
|
+
}
|
|
26
|
+
return line;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Every source file below the target root, in path order. A directory that cannot be
|
|
30
|
+
* read is skipped, as discovery skips it, and a file that cannot be read is skipped
|
|
31
|
+
* with it: a tree the analysis may not enter holds no file it can report.
|
|
32
|
+
*/
|
|
33
|
+
export function sourceTree(host, targetRoot) {
|
|
34
|
+
const scopeOf = packageScope(host, targetRoot);
|
|
35
|
+
const found = [];
|
|
36
|
+
const pending = [""];
|
|
37
|
+
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = host.readDirectory(joinPath(targetRoot, dir));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
47
|
+
if (entry.directory) {
|
|
48
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
49
|
+
pending.push(path);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!SOURCE_FILE.test(entry.name) || DECLARATION_FILE.test(entry.name)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
let text;
|
|
57
|
+
try {
|
|
58
|
+
text = host.readFile(joinPath(targetRoot, path));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
found.push({
|
|
64
|
+
path,
|
|
65
|
+
ref: renderRef(scopeOf(path), { of: "module" }),
|
|
66
|
+
endLine: lastLine(text),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return found.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
71
|
+
}
|
|
72
|
+
/** Whether one file changes what the program sees by being included in it. */
|
|
73
|
+
function actsByInclusion(file) {
|
|
74
|
+
return (file.referencedFiles.length > 0 ||
|
|
75
|
+
file.typeReferenceDirectives.length > 0 ||
|
|
76
|
+
file.libReferenceDirectives.length > 0 ||
|
|
77
|
+
file.moduleAugmentations.length > 0 ||
|
|
78
|
+
file.ambientModuleNames.length > 0);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The paths below the target root of one project's own files the program holds by
|
|
82
|
+
* inclusion, in the order the program holds them, each once.
|
|
83
|
+
*/
|
|
84
|
+
export function heldByInclusion(project, targetRoot) {
|
|
85
|
+
const held = new Set();
|
|
86
|
+
const add = (fileName) => {
|
|
87
|
+
const path = relativePath(targetRoot, fileName);
|
|
88
|
+
if (path !== undefined) {
|
|
89
|
+
held.add(path);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
for (const file of project.ownSourceFiles()) {
|
|
93
|
+
if (actsByInclusion(file)) {
|
|
94
|
+
add(file.fileName);
|
|
95
|
+
}
|
|
96
|
+
for (const reference of file.referencedFiles) {
|
|
97
|
+
add(resolvePath(dirnamePath(file.fileName), reference.fileName));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [...held];
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
import { componentId } from "../components.js";
|
|
9
|
+
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
10
|
+
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
11
|
+
export const RANK = { certain: 3, probable: 2, possible: 1 };
|
|
12
|
+
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
13
|
+
export function capped(found, ceiling) {
|
|
14
|
+
return RANK[found] <= RANK[ceiling] ? found : ceiling;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The severity the configuration gives one code: the key naming the code, else the key
|
|
18
|
+
* naming its family, else the code's default.
|
|
19
|
+
*/
|
|
20
|
+
export function severityOf(config, code, fallback) {
|
|
21
|
+
return (config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The reachability class of one declaration of the run. A declaration of a library's
|
|
25
|
+
* published API has callers outside the target, and the run loads no consumer that
|
|
26
|
+
* would name them, so it is `possible`; every other declaration has every reference
|
|
27
|
+
* in the loaded program, a private member and a `#private` name among them, and is
|
|
28
|
+
* `certain`.
|
|
29
|
+
*/
|
|
30
|
+
export function reachabilityClasses(swept) {
|
|
31
|
+
const union = swept.matrix.union;
|
|
32
|
+
const published = new Set(union.rooted
|
|
33
|
+
.filter((root) => root.kind === "published-api")
|
|
34
|
+
.map((root) => union.symbols[root.at]?.id));
|
|
35
|
+
return (id) => (published.has(id) ? "possible" : "certain");
|
|
36
|
+
}
|
|
37
|
+
/** The component each dead declaration of the run falls in, and none for a live one. */
|
|
38
|
+
export function findingComponents(swept) {
|
|
39
|
+
const held = new Map();
|
|
40
|
+
for (const component of swept.sweep.components) {
|
|
41
|
+
for (const member of component.members) {
|
|
42
|
+
held.set(member, component);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return (id) => {
|
|
46
|
+
const component = held.get(id);
|
|
47
|
+
return component === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: {
|
|
50
|
+
id: component.id,
|
|
51
|
+
root: component.roots.includes(id),
|
|
52
|
+
symbolCount: component.members.length,
|
|
53
|
+
deletableLines: component.deletableLines,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Every finding of the run, each with its component, in the order given: the dead
|
|
59
|
+
* component its subject falls in. A subject in none, a live declaration, a file no
|
|
60
|
+
* program builds or a manifest row, falls with nothing and is given a component of its
|
|
61
|
+
* own, numbered past the run's computed components in the order the findings come, so
|
|
62
|
+
* no two families mint one identifier.
|
|
63
|
+
*/
|
|
64
|
+
export function placed(swept, findings) {
|
|
65
|
+
const componentOf = findingComponents(swept);
|
|
66
|
+
const ids = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.ref, symbol.id]));
|
|
67
|
+
let minted = swept.sweep.components.length;
|
|
68
|
+
return findings.map((finding) => {
|
|
69
|
+
const id = ids.get(finding.symbol.ref);
|
|
70
|
+
const component = id === undefined ? undefined : componentOf(id);
|
|
71
|
+
if (component !== undefined) {
|
|
72
|
+
return { ...finding, component };
|
|
73
|
+
}
|
|
74
|
+
minted += 1;
|
|
75
|
+
return {
|
|
76
|
+
...finding,
|
|
77
|
+
component: {
|
|
78
|
+
id: componentId(minted),
|
|
79
|
+
root: true,
|
|
80
|
+
symbolCount: 1,
|
|
81
|
+
deletableLines: finding.symbol.sizeLines,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The findings no dial withholds. A finding at `allow` or below the minimum confidence
|
|
88
|
+
* is withheld, and so is every finding of the component of a root the dials withhold,
|
|
89
|
+
* because a symbol that falls with a root is dead only through it and its deletion
|
|
90
|
+
* does not compile while the root stays. A withheld finding that is no root withholds
|
|
91
|
+
* itself alone.
|
|
92
|
+
*/
|
|
93
|
+
export function reportable(findings, minConfidence) {
|
|
94
|
+
const dialed = (finding) => finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
95
|
+
const components = new Set(findings
|
|
96
|
+
.filter((finding) => finding.component.root && dialed(finding))
|
|
97
|
+
.map((finding) => finding.component.id));
|
|
98
|
+
return findings.filter((finding) => !dialed(finding) && !components.has(finding.component.id));
|
|
99
|
+
}
|
|
@@ -1,2 +1,66 @@
|
|
|
1
|
+
import { renderRef } from "../ref.js";
|
|
2
|
+
import { severityOf } from "./completion.js";
|
|
3
|
+
const UNUSED_DEPENDENCY = "DS1601";
|
|
4
|
+
/** The severity the kind ships at, which the configuration's keys override. */
|
|
5
|
+
const UNUSED_DEPENDENCY_SEVERITY = "deny";
|
|
6
|
+
const MESSAGES = {
|
|
7
|
+
dependency: "no import in the target needs the declared dependency",
|
|
8
|
+
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
9
|
+
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The dependencies the target needs: every package the projects' uses name, every
|
|
13
|
+
* declared dependency that ships a command, which a script, a workflow or a shell
|
|
14
|
+
* runs outside anything the analysis reads, and every peer a needed package
|
|
15
|
+
* requires, which the target satisfies by declaring it.
|
|
16
|
+
*/
|
|
17
|
+
function neededBy(dependencies) {
|
|
18
|
+
const needed = new Set(dependencies.needed);
|
|
19
|
+
for (const [name, installed] of dependencies.installed) {
|
|
20
|
+
if (installed.command) {
|
|
21
|
+
needed.add(name);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const pending = [...needed];
|
|
25
|
+
for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
|
|
26
|
+
for (const peer of dependencies.installed.get(name)?.peers ?? []) {
|
|
27
|
+
if (!needed.has(peer)) {
|
|
28
|
+
needed.add(peer);
|
|
29
|
+
pending.push(peer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return needed;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A dependency, development dependency or peer dependency of the target's manifest
|
|
37
|
+
* that the target does not need, at the position of its key. Nothing is reported at
|
|
38
|
+
* `allow`. Every subject is a manifest entry, which is `certain` whatever the
|
|
39
|
+
* consumers, so no minimum confidence withholds one.
|
|
40
|
+
*/
|
|
41
|
+
function unusedDependencies(config, dependencies) {
|
|
42
|
+
if (severityOf(config, UNUSED_DEPENDENCY, UNUSED_DEPENDENCY_SEVERITY) === "allow") {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const needed = neededBy(dependencies);
|
|
46
|
+
return dependencies.declared
|
|
47
|
+
.filter((dependency) => !needed.has(dependency.name))
|
|
48
|
+
.map((dependency) => ({
|
|
49
|
+
code: UNUSED_DEPENDENCY,
|
|
50
|
+
position: dependency.position,
|
|
51
|
+
symbol: {
|
|
52
|
+
ref: renderRef(dependencies.manifest, {
|
|
53
|
+
of: "dependency",
|
|
54
|
+
name: dependency.name,
|
|
55
|
+
section: dependency.section,
|
|
56
|
+
}),
|
|
57
|
+
kind: "dependency",
|
|
58
|
+
name: dependency.name,
|
|
59
|
+
sizeLines: 1,
|
|
60
|
+
},
|
|
61
|
+
message: MESSAGES[dependency.section],
|
|
62
|
+
details: { dependencyClass: dependency.section },
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
1
65
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
2
|
-
export const dependenciesAndModuleMachinery = () =>
|
|
66
|
+
export const dependenciesAndModuleMachinery = ({ config, dependencies }) => unusedDependencies(config, dependencies);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
|
|
2
|
+
import { placed } from "./completion.js";
|
|
2
3
|
import { interfaces } from "./interfaces.js";
|
|
3
4
|
import { intraFunction } from "./intra-function.js";
|
|
4
5
|
import { nonCodeArtifacts } from "./non-code-artifacts.js";
|
|
@@ -17,3 +18,10 @@ export const EMITTERS = new Map([
|
|
|
17
18
|
["self-check", selfCheck],
|
|
18
19
|
["intra-function", intraFunction],
|
|
19
20
|
]);
|
|
21
|
+
/**
|
|
22
|
+
* Every finding of the run, each family's in table order, each placed in its component
|
|
23
|
+
* by the one step that numbers the components minted for subjects that fall with none.
|
|
24
|
+
*/
|
|
25
|
+
export function findingsOf(input) {
|
|
26
|
+
return placed(input.swept, [...EMITTERS.values()].flatMap((emit) => emit(input)));
|
|
27
|
+
}
|
|
@@ -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
|
+
}
|