@cplieger/deadset-ts 3.12.1 → 3.13.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 +2 -2
- package/dist/src/analysis.js +100 -27
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +36 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/edges.js +4 -1
- package/dist/src/findings/completion.js +37 -8
- package/dist/src/findings/emitters.js +5 -5
- package/dist/src/findings/intra-function.js +91 -2
- package/dist/src/findings/self-check.js +30 -7
- package/dist/src/findings/unused-declarations.js +15 -3
- package/dist/src/findings-pass.js +18 -11
- package/dist/src/intra-function-parts.js +643 -0
- package/dist/src/kinds.js +337 -35
- package/dist/src/reflective-lookup.js +35 -2
- package/dist/src/report.js +11 -2
- package/dist/src/reporters/sarif.js +20 -15
- package/dist/src/reporters/template.js +64 -5
- package/dist/src/serialization-contract.js +41 -22
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +27 -6
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/value-reach.js +33 -9
- package/dist/src/verbs/analyze.js +19 -11
- package/dist/src/verbs/describe.js +12 -8
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +167 -48
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +57 -1
- package/src/conformance-record.ts +2 -2
- package/src/edges.ts +4 -1
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +46 -10
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +9 -5
- package/src/findings/intra-function.ts +106 -3
- package/src/findings/self-check.ts +48 -7
- package/src/findings/unused-declarations.ts +18 -4
- package/src/findings-pass.ts +26 -11
- package/src/intra-function-parts.ts +871 -0
- package/src/kinds.ts +355 -41
- package/src/reflective-lookup.ts +42 -2
- package/src/report.ts +18 -2
- package/src/reporters/sarif.ts +24 -16
- package/src/reporters/template.ts +69 -6
- package/src/serialization-contract.ts +47 -30
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +34 -6
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/value-reach.ts +53 -12
- package/src/verbs/analyze.ts +26 -11
- package/src/verbs/describe.ts +13 -9
- package/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of the declarations one project's own files hold, read while the project's
|
|
3
|
+
* view is open: each parameter and whether its function reads it, each result and the
|
|
4
|
+
* call sites that use or discard it, each statement control flow cannot reach, each
|
|
5
|
+
* write to a local that no read reaches, and each case an earlier case covers.
|
|
6
|
+
*/
|
|
7
|
+
import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
8
|
+
import { nodeKey } from "./inventory.js";
|
|
9
|
+
import { positionKey, renderPosition } from "./position.js";
|
|
10
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
11
|
+
import { signatureOf } from "./signatures.js";
|
|
12
|
+
/** The codes of the parts this module reads. */
|
|
13
|
+
export const UNUSED_PARAMETER = "DS1801";
|
|
14
|
+
export const UNUSED_RESULT = "DS1803";
|
|
15
|
+
const UNREACHABLE_STATEMENT = "DS1805";
|
|
16
|
+
const DEAD_STORE = "DS1807";
|
|
17
|
+
const UNREACHABLE_CASE = "DS1809";
|
|
18
|
+
/** The result types that carry no value, as the checker spells them. */
|
|
19
|
+
const NO_VALUE = new Set([
|
|
20
|
+
"void",
|
|
21
|
+
"undefined",
|
|
22
|
+
"never",
|
|
23
|
+
"Promise<void>",
|
|
24
|
+
"Promise<undefined>",
|
|
25
|
+
"Promise<never>",
|
|
26
|
+
]);
|
|
27
|
+
/** The prefix that marks a parameter as unused on purpose, as the compiler's own rule reads it. */
|
|
28
|
+
const UNUSED_PREFIX = "_";
|
|
29
|
+
/** The rendered position of one node, from its first token to its last line. */
|
|
30
|
+
function spanOf(file, root, node) {
|
|
31
|
+
const start = renderPosition(file, root, node.getStart());
|
|
32
|
+
const endLine = file.getLineAndCharacterOfPosition(node.end).line + 1;
|
|
33
|
+
return { ...start, endLine };
|
|
34
|
+
}
|
|
35
|
+
/** The parenthesized expression's content, through every pair of parentheses. */
|
|
36
|
+
function unwrapped(node) {
|
|
37
|
+
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
38
|
+
}
|
|
39
|
+
/** The node the parentheses around one expression sit in. */
|
|
40
|
+
function outerParent(node) {
|
|
41
|
+
return isParenthesizedExpression(node.parent) ? outerParent(node.parent) : node.parent;
|
|
42
|
+
}
|
|
43
|
+
/** The name a call is made through: the identifier, or the member a property access names. */
|
|
44
|
+
function calleeName(call) {
|
|
45
|
+
const callee = unwrapped(call.expression);
|
|
46
|
+
if (isIdentifier(callee)) {
|
|
47
|
+
return callee;
|
|
48
|
+
}
|
|
49
|
+
return isPropertyAccessExpression(callee) && isIdentifier(callee.name) ? callee.name : undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether a call's value is discarded: the call is a statement of its own, awaited as a
|
|
53
|
+
* statement, or the operand of `void`.
|
|
54
|
+
*/
|
|
55
|
+
function discarded(call) {
|
|
56
|
+
const parent = outerParent(call);
|
|
57
|
+
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return isAwaitExpression(parent) && isExpressionStatement(outerParent(parent));
|
|
61
|
+
}
|
|
62
|
+
/** Whether one statement leaves its statement list on every path: the rest of the list never runs. */
|
|
63
|
+
function abrupt(statement) {
|
|
64
|
+
if (isReturnStatement(statement) ||
|
|
65
|
+
isThrowStatement(statement) ||
|
|
66
|
+
isBreakStatement(statement) ||
|
|
67
|
+
isContinueStatement(statement)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (isBlock(statement)) {
|
|
71
|
+
return statement.statements.some(abrupt);
|
|
72
|
+
}
|
|
73
|
+
if (isIfStatement(statement)) {
|
|
74
|
+
return (statement.elseStatement !== undefined &&
|
|
75
|
+
abrupt(statement.thenStatement) &&
|
|
76
|
+
abrupt(statement.elseStatement));
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether an unreachable statement is no subject: a declaration the language hoists or
|
|
82
|
+
* that emits nothing, an empty statement, and a `var` that initializes nothing.
|
|
83
|
+
*/
|
|
84
|
+
function unreachableExempt(statement) {
|
|
85
|
+
if (isFunctionDeclaration(statement) ||
|
|
86
|
+
isInterfaceDeclaration(statement) ||
|
|
87
|
+
isTypeAliasDeclaration(statement) ||
|
|
88
|
+
isModuleDeclaration(statement) ||
|
|
89
|
+
isEmptyStatement(statement)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (isVariableStatement(statement)) {
|
|
93
|
+
const list = statement.declarationList;
|
|
94
|
+
return ((list.flags & (NodeFlags.Let | NodeFlags.Const | NodeFlags.Using)) === 0 &&
|
|
95
|
+
list.declarations.every((one) => one.initializer === undefined));
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
/** The value a case expression is a literal of, as a key two equal literals share, or none. */
|
|
100
|
+
function caseKey(expression) {
|
|
101
|
+
const node = unwrapped(expression);
|
|
102
|
+
if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
|
|
103
|
+
return `s:${node.text}`;
|
|
104
|
+
}
|
|
105
|
+
if (isNumericLiteral(node)) {
|
|
106
|
+
return `n:${String(Number(node.text))}`;
|
|
107
|
+
}
|
|
108
|
+
if (isBigIntLiteral(node)) {
|
|
109
|
+
return `b:${BigInt(node.text.slice(0, -1)).toString()}`;
|
|
110
|
+
}
|
|
111
|
+
if (isPrefixUnaryExpression(node) &&
|
|
112
|
+
node.operator === SyntaxKind.MinusToken &&
|
|
113
|
+
isNumericLiteral(node.operand)) {
|
|
114
|
+
return `n:${String(-Number(node.operand.text))}`;
|
|
115
|
+
}
|
|
116
|
+
if (node.kind === SyntaxKind.TrueKeyword ||
|
|
117
|
+
node.kind === SyntaxKind.FalseKeyword ||
|
|
118
|
+
node.kind === SyntaxKind.NullKeyword) {
|
|
119
|
+
return `k:${String(node.kind)}`;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
/** Every identifier below one node whose text one of the names spells, a member's name aside. */
|
|
124
|
+
function identifiersNaming(node, names) {
|
|
125
|
+
const found = [];
|
|
126
|
+
const visit = (at) => {
|
|
127
|
+
if (isIdentifier(at)) {
|
|
128
|
+
const parent = at.parent;
|
|
129
|
+
if (names.has(at.text) && !(isPropertyAccessExpression(parent) && parent.name === at)) {
|
|
130
|
+
found.push(at);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
at.forEachChild(visit);
|
|
135
|
+
};
|
|
136
|
+
visit(node);
|
|
137
|
+
return found;
|
|
138
|
+
}
|
|
139
|
+
/** Whether one node is below another, or is it. */
|
|
140
|
+
function within(node, container) {
|
|
141
|
+
return node.pos >= container.pos && node.end <= container.end;
|
|
142
|
+
}
|
|
143
|
+
/** Whether a node is written inside a function nested in `outer`, which may run at any time. */
|
|
144
|
+
function inNestedFunction(node, outer) {
|
|
145
|
+
for (let at = node.parent; at !== outer; at = at.parent) {
|
|
146
|
+
if (isArrowFunction(at) ||
|
|
147
|
+
at.kind === SyntaxKind.FunctionExpression ||
|
|
148
|
+
isFunctionDeclaration(at) ||
|
|
149
|
+
isMethodDeclaration(at) ||
|
|
150
|
+
isClassDeclaration(at) ||
|
|
151
|
+
isClassExpression(at) ||
|
|
152
|
+
at.kind === SyntaxKind.GetAccessor ||
|
|
153
|
+
at.kind === SyntaxKind.SetAccessor ||
|
|
154
|
+
at.kind === SyntaxKind.Constructor) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
/** Whether a body is a stub: it holds no statement, or nothing but `throw` statements. */
|
|
161
|
+
function isStub(signature) {
|
|
162
|
+
const body = signature.body;
|
|
163
|
+
return body !== undefined && isBlock(body) && body.statements.every(isThrowStatement);
|
|
164
|
+
}
|
|
165
|
+
/** Whether a function's body or signature returns a value: an expression body, or `return` with one. */
|
|
166
|
+
function returnsAValue(signature) {
|
|
167
|
+
const body = signature.body;
|
|
168
|
+
if (body === undefined) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (!isBlock(body)) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
let found = false;
|
|
175
|
+
const visit = (node) => {
|
|
176
|
+
if (found || inNestedFunction(node, signature)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (isReturnStatement(node) && node.expression !== undefined) {
|
|
180
|
+
found = true;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
node.forEachChild(visit);
|
|
184
|
+
};
|
|
185
|
+
body.forEachChild(visit);
|
|
186
|
+
return found;
|
|
187
|
+
}
|
|
188
|
+
/** Whether one node is written with a modifier of one kind. */
|
|
189
|
+
function hasModifier(node, kind) {
|
|
190
|
+
return (node.modifiers ?? []).some((modifier) => modifier.kind === kind);
|
|
191
|
+
}
|
|
192
|
+
/** Every class name an `extends` clause of the project names, by its last identifier. */
|
|
193
|
+
function extendedNames(files) {
|
|
194
|
+
const names = new Set();
|
|
195
|
+
const visit = (node) => {
|
|
196
|
+
if (isHeritageClause(node) && node.token === SyntaxKind.ExtendsKeyword) {
|
|
197
|
+
for (const type of node.types) {
|
|
198
|
+
const expression = unwrapped(type.expression);
|
|
199
|
+
const named = isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
200
|
+
if (isIdentifier(named)) {
|
|
201
|
+
names.add(named.text);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
node.forEachChild(visit);
|
|
206
|
+
};
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
file.forEachChild(visit);
|
|
209
|
+
}
|
|
210
|
+
return names;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Whether a class hierarchy fixes one method's signature: its class extends or implements
|
|
214
|
+
* another type, is abstract or is extended itself, or the method is marked `override`.
|
|
215
|
+
*/
|
|
216
|
+
function hierarchyFixes(declared, extended) {
|
|
217
|
+
if (!isMethodDeclaration(declared)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const owner = declared.parent;
|
|
221
|
+
if (!isClassDeclaration(owner) && !isClassExpression(owner)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
if (hasModifier(declared, SyntaxKind.OverrideKeyword) ||
|
|
225
|
+
hasModifier(owner, SyntaxKind.AbstractKeyword)) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if ((owner.heritageClauses ?? []).length > 0) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
return owner.name !== undefined && extended.has(owner.name.text);
|
|
232
|
+
}
|
|
233
|
+
/** The symbol identifier each of a batch of identifiers resolves to, where the batch answers. */
|
|
234
|
+
function resolvedIds(project, nodes) {
|
|
235
|
+
const ids = new Map();
|
|
236
|
+
for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
|
|
237
|
+
const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
|
|
238
|
+
project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
239
|
+
const node = run[index];
|
|
240
|
+
if (symbol !== undefined && node !== undefined) {
|
|
241
|
+
ids.set(node, symbol.id);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return ids;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Whether one identifier reads the binding a name node declares: it resolves to the
|
|
249
|
+
* same symbol, or the checker resolves it to nothing, which is read as a use rather than
|
|
250
|
+
* risk a finding; a shorthand property reads the binding of its own name.
|
|
251
|
+
*/
|
|
252
|
+
function readsBinding(use, declared, ids) {
|
|
253
|
+
if (isShorthandPropertyAssignment(use.parent)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
const at = ids.get(use);
|
|
257
|
+
return at === undefined || at === ids.get(declared);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The parts of one project's own declarations. `references` are the project's own
|
|
261
|
+
* references, which say which declarations a discarded call reaches, so the checker is
|
|
262
|
+
* asked for a result's type only where a call discards it.
|
|
263
|
+
*/
|
|
264
|
+
export function projectParts(project, held, references, targetRoot) {
|
|
265
|
+
const files = project.ownSourceFiles();
|
|
266
|
+
const extended = extendedNames(files);
|
|
267
|
+
const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
|
|
268
|
+
const parts = [];
|
|
269
|
+
const signatures = [];
|
|
270
|
+
const calls = new Map();
|
|
271
|
+
const specifiers = new Set();
|
|
272
|
+
const sites = [];
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
const declarationAt = (node) => {
|
|
275
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
276
|
+
return id === undefined || kinds.get(id) === "file" ? undefined : id;
|
|
277
|
+
};
|
|
278
|
+
const holderOf = (node) => {
|
|
279
|
+
for (let at = node.parent; at.kind !== SyntaxKind.SourceFile; at = at.parent) {
|
|
280
|
+
const id = declarationAt(at);
|
|
281
|
+
if (id !== undefined) {
|
|
282
|
+
return id;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
};
|
|
287
|
+
const walk = { declared: [], blocks: [], unreachable: [], cases: [] };
|
|
288
|
+
const position = (node) => positionKey(renderPosition(file, targetRoot, node.getStart()));
|
|
289
|
+
// Code below an unreachable statement is still walked for its calls and specifiers,
|
|
290
|
+
// and reports no part of its own: the unreachable statement is the one finding.
|
|
291
|
+
let silenced = 0;
|
|
292
|
+
const statementsOf = (statements) => {
|
|
293
|
+
let reachable = true;
|
|
294
|
+
let reported = false;
|
|
295
|
+
for (const statement of statements) {
|
|
296
|
+
if (reachable) {
|
|
297
|
+
visit(statement);
|
|
298
|
+
reachable = !abrupt(statement);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const holder = holderOf(statement);
|
|
302
|
+
if (silenced === 0 && !reported && !unreachableExempt(statement) && holder !== undefined) {
|
|
303
|
+
walk.unreachable.push({ statement, holder });
|
|
304
|
+
reported = true;
|
|
305
|
+
}
|
|
306
|
+
silenced += 1;
|
|
307
|
+
visit(statement);
|
|
308
|
+
silenced -= 1;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const visit = (node) => {
|
|
312
|
+
const id = declarationAt(node);
|
|
313
|
+
const signature = id === undefined ? undefined : signatureOf(node);
|
|
314
|
+
if (id !== undefined && signature !== undefined) {
|
|
315
|
+
walk.declared.push({ id, node, signature });
|
|
316
|
+
}
|
|
317
|
+
if (isCallExpression(node)) {
|
|
318
|
+
const name = calleeName(node);
|
|
319
|
+
if (name !== undefined) {
|
|
320
|
+
calls.set(position(name), discarded(node));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (isImportSpecifier(node) || isExportSpecifier(node)) {
|
|
324
|
+
for (const name of [node.propertyName, node.name]) {
|
|
325
|
+
if (name !== undefined) {
|
|
326
|
+
specifiers.add(position(name));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if ((isImportClause(node) || isNamespaceImport(node)) && node.name !== undefined) {
|
|
331
|
+
specifiers.add(position(node.name));
|
|
332
|
+
}
|
|
333
|
+
if (isExportAssignment(node) && isIdentifier(unwrapped(node.expression))) {
|
|
334
|
+
specifiers.add(position(unwrapped(node.expression)));
|
|
335
|
+
}
|
|
336
|
+
if (isSwitchStatement(node)) {
|
|
337
|
+
const holder = holderOf(node);
|
|
338
|
+
const seen = new Set();
|
|
339
|
+
for (const clause of node.caseBlock.clauses) {
|
|
340
|
+
if (!isCaseClause(clause)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const key = caseKey(clause.expression);
|
|
344
|
+
if (key !== undefined && seen.has(key) && holder !== undefined && silenced === 0) {
|
|
345
|
+
walk.cases.push({ clause, text: clause.expression.getText(), holder });
|
|
346
|
+
}
|
|
347
|
+
if (key !== undefined) {
|
|
348
|
+
seen.add(key);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (isBlock(node) && silenced === 0 && holderOf(node) !== undefined) {
|
|
353
|
+
walk.blocks.push(node);
|
|
354
|
+
}
|
|
355
|
+
if (isBlock(node) || isCaseClause(node) || isDefaultClause(node)) {
|
|
356
|
+
statementsOf(node.statements);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
node.forEachChild(visit);
|
|
360
|
+
};
|
|
361
|
+
file.forEachChild(visit);
|
|
362
|
+
// One batch per file resolves every name a parameter or a local is read through.
|
|
363
|
+
const asked = [];
|
|
364
|
+
const readers = new Map();
|
|
365
|
+
for (const one of walk.declared) {
|
|
366
|
+
const names = one.signature.parameters
|
|
367
|
+
.map((parameter) => parameter.name)
|
|
368
|
+
.filter(isIdentifier);
|
|
369
|
+
const spelled = new Set(names.map((name) => name.text));
|
|
370
|
+
const uses = identifiersNaming(one.signature, spelled).filter((use) => !names.includes(use));
|
|
371
|
+
readers.set(one, { names, uses });
|
|
372
|
+
asked.push(...names, ...uses);
|
|
373
|
+
}
|
|
374
|
+
const stores = walk.blocks.map((block) => localsOf(block));
|
|
375
|
+
for (const store of stores) {
|
|
376
|
+
asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
|
|
377
|
+
}
|
|
378
|
+
const ids = resolvedIds(project, [...new Set(asked)]);
|
|
379
|
+
for (const one of walk.declared) {
|
|
380
|
+
sites.push({ file, declared: one });
|
|
381
|
+
signatures.push({
|
|
382
|
+
declaration: one.id,
|
|
383
|
+
stub: isStub(one.signature),
|
|
384
|
+
fixed: hierarchyFixes(one.node, extended),
|
|
385
|
+
});
|
|
386
|
+
const { uses } = readers.get(one) ?? { uses: [] };
|
|
387
|
+
parts.push(...parameterParts(file, targetRoot, one, uses, ids));
|
|
388
|
+
}
|
|
389
|
+
walk.blocks.forEach((block, at) => {
|
|
390
|
+
const holder = holderOf(block);
|
|
391
|
+
if (holder !== undefined) {
|
|
392
|
+
parts.push(...deadStores(file, targetRoot, block, holder, stores[at] ?? [], ids));
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
for (const { statement, holder } of walk.unreachable) {
|
|
396
|
+
parts.push({
|
|
397
|
+
code: UNREACHABLE_STATEMENT,
|
|
398
|
+
declaration: holder,
|
|
399
|
+
kind: "statement",
|
|
400
|
+
name: "statement",
|
|
401
|
+
position: spanOf(file, targetRoot, statement),
|
|
402
|
+
message: "statement is unreachable",
|
|
403
|
+
used: false,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
for (const { clause, text, holder } of walk.cases) {
|
|
407
|
+
parts.push({
|
|
408
|
+
code: UNREACHABLE_CASE,
|
|
409
|
+
declaration: holder,
|
|
410
|
+
kind: "case",
|
|
411
|
+
name: text,
|
|
412
|
+
position: spanOf(file, targetRoot, clause),
|
|
413
|
+
message: `case ${text} is covered by an earlier case of the same switch`,
|
|
414
|
+
used: false,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
parts.push(...resultParts(project, targetRoot, sites, references, calls));
|
|
419
|
+
return { parts, signatures, calls, specifiers };
|
|
420
|
+
}
|
|
421
|
+
/** Each named parameter of one function and whether the function reads it. */
|
|
422
|
+
function parameterParts(file, root, one, uses, ids) {
|
|
423
|
+
const signature = one.signature;
|
|
424
|
+
// A function that reads `arguments` reads every parameter by position.
|
|
425
|
+
const readsArguments = !isArrowFunction(signature) && identifiersNaming(signature, new Set(["arguments"])).length > 0;
|
|
426
|
+
const found = [];
|
|
427
|
+
for (const parameter of signature.parameters) {
|
|
428
|
+
const name = parameter.name;
|
|
429
|
+
if (!isIdentifier(name) ||
|
|
430
|
+
name.text === "this" ||
|
|
431
|
+
name.text.startsWith(UNUSED_PREFIX) ||
|
|
432
|
+
(parameter.modifiers ?? []).some(isDecorator)) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
found.push({
|
|
436
|
+
code: UNUSED_PARAMETER,
|
|
437
|
+
declaration: one.id,
|
|
438
|
+
kind: "parameter",
|
|
439
|
+
name: name.text,
|
|
440
|
+
position: spanOf(file, root, name),
|
|
441
|
+
message: `parameter ${name.text} is never read in the body`,
|
|
442
|
+
used: readsArguments ||
|
|
443
|
+
uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return found;
|
|
447
|
+
}
|
|
448
|
+
/** The locals one block declares with `let` or `const`, each with the names that spell it. */
|
|
449
|
+
function localsOf(block) {
|
|
450
|
+
const declared = [];
|
|
451
|
+
block.statements.forEach((statement, at) => {
|
|
452
|
+
if (!isVariableStatement(statement)) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const list = statement.declarationList;
|
|
456
|
+
if ((list.flags & (NodeFlags.Let | NodeFlags.Const)) === 0 ||
|
|
457
|
+
(list.flags & NodeFlags.Using) !== 0) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
for (const declaration of list.declarations) {
|
|
461
|
+
if (isIdentifier(declaration.name)) {
|
|
462
|
+
declared.push({
|
|
463
|
+
name: declaration.name,
|
|
464
|
+
statement: at,
|
|
465
|
+
initialized: declaration.initializer !== undefined,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
if (declared.length === 0) {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
const spelled = identifiersNaming(block, new Set(declared.map((one) => one.name.text)));
|
|
474
|
+
return declared.map((one) => ({
|
|
475
|
+
...one,
|
|
476
|
+
uses: spelled.filter((use) => use !== one.name && use.text === one.name.text),
|
|
477
|
+
}));
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* The writes to the locals of one block that no read reaches. A write is a declaration's
|
|
481
|
+
* initializer or a plain assignment written as a statement of the block itself; it is
|
|
482
|
+
* dead when the statements after it in the block read the local nowhere before the next
|
|
483
|
+
* such write to it, or before the block ends, which ends the local's scope. A statement
|
|
484
|
+
* that writes the local inside a nested construct ends the search with the write kept,
|
|
485
|
+
* and a local a nested function names is no subject, because the function may read it
|
|
486
|
+
* at any time.
|
|
487
|
+
*/
|
|
488
|
+
function deadStores(file, root, block, holder, locals, ids) {
|
|
489
|
+
const found = [];
|
|
490
|
+
for (const local of locals) {
|
|
491
|
+
const uses = local.uses.filter((use) => readsBinding(use, local.name, ids));
|
|
492
|
+
if (uses.some((use) => inNestedFunction(use, block))) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const statements = block.statements;
|
|
496
|
+
const usesIn = (statement) => uses.filter((use) => within(use, statement));
|
|
497
|
+
/** The plain assignment to the local a statement of the block is, where it is one. */
|
|
498
|
+
const writeOf = (statement) => {
|
|
499
|
+
if (!isExpressionStatement(statement)) {
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
const expression = unwrapped(statement.expression);
|
|
503
|
+
if (!isBinaryExpression(expression) ||
|
|
504
|
+
expression.operatorToken.kind !== SyntaxKind.EqualsToken) {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
const target = unwrapped(expression.left);
|
|
508
|
+
return isIdentifier(target) && uses.includes(target)
|
|
509
|
+
? { target, value: expression.right }
|
|
510
|
+
: undefined;
|
|
511
|
+
};
|
|
512
|
+
const writes = [];
|
|
513
|
+
if (local.initialized) {
|
|
514
|
+
writes.push({ at: local.statement, target: local.name });
|
|
515
|
+
}
|
|
516
|
+
statements.forEach((statement, at) => {
|
|
517
|
+
const write = writeOf(statement);
|
|
518
|
+
if (write !== undefined && at > local.statement) {
|
|
519
|
+
writes.push({ at, target: write.target });
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
for (const write of writes) {
|
|
523
|
+
// The rest of the write's own statement runs after it: a later declarator may read the local.
|
|
524
|
+
const own = statements[write.at];
|
|
525
|
+
const sameStatement = own !== undefined && write.target === local.name
|
|
526
|
+
? usesIn(own).filter((use) => use !== local.name)
|
|
527
|
+
: [];
|
|
528
|
+
if (sameStatement.length > 0) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
let dead = true;
|
|
532
|
+
for (let at = write.at + 1; at < statements.length; at += 1) {
|
|
533
|
+
const statement = statements[at];
|
|
534
|
+
if (statement === undefined) {
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
const next = writeOf(statement);
|
|
538
|
+
if (next !== undefined) {
|
|
539
|
+
dead = usesIn(next.value).length === 0;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
const named = usesIn(statement);
|
|
543
|
+
if (named.length > 0) {
|
|
544
|
+
dead = false;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (!dead) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const position = spanOf(file, root, write.target);
|
|
552
|
+
found.push({
|
|
553
|
+
code: DEAD_STORE,
|
|
554
|
+
declaration: holder,
|
|
555
|
+
kind: "store",
|
|
556
|
+
name: local.name.text,
|
|
557
|
+
position,
|
|
558
|
+
message: `value written to ${local.name.text} is never read`,
|
|
559
|
+
used: false,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return found;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* The result of each function a discarded call reaches, where its type carries a value:
|
|
567
|
+
* whether it is used is the call sites' to say, which the run decides over every project.
|
|
568
|
+
*/
|
|
569
|
+
function resultParts(project, root, sites, references, calls) {
|
|
570
|
+
const discardedAt = new Set(references.filter((one) => calls.get(positionKey(one.position)) === true).map((one) => one.to));
|
|
571
|
+
const found = [];
|
|
572
|
+
for (const { file, declared } of sites) {
|
|
573
|
+
const { signature, node, id } = declared;
|
|
574
|
+
const generator = signature.asteriskToken !== undefined;
|
|
575
|
+
if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const typed = project.checker.getSignatureFromDeclaration(signature);
|
|
579
|
+
const result = typed === undefined ? undefined : project.checker.getReturnTypeOfSignature(typed);
|
|
580
|
+
if (result === undefined || NO_VALUE.has(project.checker.typeToString(result))) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const at = signature.type ?? node.name ?? node;
|
|
584
|
+
found.push({
|
|
585
|
+
code: UNUSED_RESULT,
|
|
586
|
+
declaration: id,
|
|
587
|
+
kind: "result",
|
|
588
|
+
name: "result",
|
|
589
|
+
position: spanOf(file, root, at),
|
|
590
|
+
message: "result is discarded at every call site",
|
|
591
|
+
used: false,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return found;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* The parts of a run's projects merged: a part two projects hold is one part, in use
|
|
598
|
+
* where either found it in use. A signature is free when no exemption record names its
|
|
599
|
+
* declaration, its body is no stub, no class hierarchy fixes it, and every reference to
|
|
600
|
+
* the declaration is a call or a specifier: any other reference uses the function as a
|
|
601
|
+
* value, whose type then fixes the signature, and a reference a consumer makes is one
|
|
602
|
+
* whose form this run does not read.
|
|
603
|
+
*/
|
|
604
|
+
export function intraFunctionFacts(projects, references, exempted) {
|
|
605
|
+
const parts = new Map();
|
|
606
|
+
for (const part of projects.flatMap((one) => one.parts)) {
|
|
607
|
+
const key = `${part.code}\u0000${positionKey(part.position)}`;
|
|
608
|
+
const known = parts.get(key);
|
|
609
|
+
parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
|
|
610
|
+
}
|
|
611
|
+
const calls = new Map();
|
|
612
|
+
const specifiers = new Set();
|
|
613
|
+
for (const one of projects) {
|
|
614
|
+
for (const [at, value] of one.calls) {
|
|
615
|
+
calls.set(at, value);
|
|
616
|
+
}
|
|
617
|
+
for (const at of one.specifiers) {
|
|
618
|
+
specifiers.add(at);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const valued = new Set();
|
|
622
|
+
const called = new Map();
|
|
623
|
+
for (const reference of references) {
|
|
624
|
+
if (reference.use === "decorator" || reference.use === "evaluation") {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const at = positionKey(reference.position);
|
|
628
|
+
const call = reference.consumer === undefined ? calls.get(at) : undefined;
|
|
629
|
+
if (call !== undefined) {
|
|
630
|
+
called.set(reference.to, (called.get(reference.to) ?? true) && call);
|
|
631
|
+
}
|
|
632
|
+
else if (reference.consumer !== undefined || !specifiers.has(at)) {
|
|
633
|
+
valued.add(reference.to);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const signatures = projects.flatMap((one) => one.signatures);
|
|
637
|
+
const fixed = new Set(signatures.filter((one) => one.stub || one.fixed).map((one) => one.declaration));
|
|
638
|
+
const free = new Set(signatures
|
|
639
|
+
.map((one) => one.declaration)
|
|
640
|
+
.filter((id) => !fixed.has(id) && !exempted.has(id) && !valued.has(id)));
|
|
641
|
+
const discardedEverywhere = new Set([...called].filter(([id, all]) => all && !valued.has(id)).map(([id]) => id));
|
|
642
|
+
return { parts: [...parts.values()], free, discardedEverywhere };
|
|
643
|
+
}
|