@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,871 @@
|
|
|
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
|
+
|
|
8
|
+
import {
|
|
9
|
+
isArrowFunction,
|
|
10
|
+
isAwaitExpression,
|
|
11
|
+
isBigIntLiteral,
|
|
12
|
+
isBinaryExpression,
|
|
13
|
+
isBlock,
|
|
14
|
+
isBreakStatement,
|
|
15
|
+
isCallExpression,
|
|
16
|
+
isCaseClause,
|
|
17
|
+
isClassDeclaration,
|
|
18
|
+
isClassExpression,
|
|
19
|
+
isContinueStatement,
|
|
20
|
+
isDecorator,
|
|
21
|
+
isDefaultClause,
|
|
22
|
+
isEmptyStatement,
|
|
23
|
+
isExportAssignment,
|
|
24
|
+
isExportSpecifier,
|
|
25
|
+
isExpressionStatement,
|
|
26
|
+
isFunctionDeclaration,
|
|
27
|
+
isHeritageClause,
|
|
28
|
+
isIdentifier,
|
|
29
|
+
isIfStatement,
|
|
30
|
+
isImportClause,
|
|
31
|
+
isImportSpecifier,
|
|
32
|
+
isInterfaceDeclaration,
|
|
33
|
+
isMethodDeclaration,
|
|
34
|
+
isModuleDeclaration,
|
|
35
|
+
isNamespaceImport,
|
|
36
|
+
isNoSubstitutionTemplateLiteral,
|
|
37
|
+
isNumericLiteral,
|
|
38
|
+
isParenthesizedExpression,
|
|
39
|
+
isPrefixUnaryExpression,
|
|
40
|
+
isPropertyAccessExpression,
|
|
41
|
+
isReturnStatement,
|
|
42
|
+
isShorthandPropertyAssignment,
|
|
43
|
+
isStringLiteral,
|
|
44
|
+
isSwitchStatement,
|
|
45
|
+
isThrowStatement,
|
|
46
|
+
isTypeAliasDeclaration,
|
|
47
|
+
isVariableStatement,
|
|
48
|
+
isVoidExpression,
|
|
49
|
+
NodeFlags,
|
|
50
|
+
SyntaxKind,
|
|
51
|
+
type Block,
|
|
52
|
+
type CallExpression,
|
|
53
|
+
type Identifier,
|
|
54
|
+
type Node,
|
|
55
|
+
type SourceFile,
|
|
56
|
+
type Statement,
|
|
57
|
+
} from "@typescript/native/unstable/ast";
|
|
58
|
+
import type { FindingPosition } from "./finding.ts";
|
|
59
|
+
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
60
|
+
import { positionKey, renderPosition } from "./position.ts";
|
|
61
|
+
import { DEFAULT_BATCH_CAP, type Reference } from "./references.ts";
|
|
62
|
+
import type { ProjectView } from "./session.ts";
|
|
63
|
+
import { signatureOf, type SignatureNode } from "./signatures.ts";
|
|
64
|
+
|
|
65
|
+
/** The subject kind of each part this module reads. */
|
|
66
|
+
type PartKind = "parameter" | "result" | "statement" | "store" | "case";
|
|
67
|
+
|
|
68
|
+
/** One part of one declaration, and whether this project found it in use. */
|
|
69
|
+
export interface PartFact {
|
|
70
|
+
readonly code: string;
|
|
71
|
+
/** The inventory's identifier of the declaration that holds the part. */
|
|
72
|
+
readonly declaration: string;
|
|
73
|
+
readonly kind: PartKind;
|
|
74
|
+
/** The part's display name: an identifier, or the text of a case. */
|
|
75
|
+
readonly name: string;
|
|
76
|
+
readonly position: FindingPosition;
|
|
77
|
+
/** What a finding about the part says, before the rules that report it too. */
|
|
78
|
+
readonly message: string;
|
|
79
|
+
/** Whether the project found the part in use, which takes it out of the answer. */
|
|
80
|
+
readonly used: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** What one declaration's signature answers to beside its body. */
|
|
84
|
+
interface SignatureFact {
|
|
85
|
+
readonly declaration: string;
|
|
86
|
+
/** Whether the body is empty or does nothing but throw. */
|
|
87
|
+
readonly stub: boolean;
|
|
88
|
+
/** Whether a class hierarchy fixes the signature: the method overrides, or is overridden. */
|
|
89
|
+
readonly fixed: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** What one project answers about the parts of its declarations. */
|
|
93
|
+
export interface ProjectParts {
|
|
94
|
+
readonly parts: readonly PartFact[];
|
|
95
|
+
readonly signatures: readonly SignatureFact[];
|
|
96
|
+
/** Each name written as the callee of a call, by position, and whether the call's value is discarded. */
|
|
97
|
+
readonly calls: ReadonlyMap<string, boolean>;
|
|
98
|
+
/** Each name an import or an export specifier writes, by position, which uses no value. */
|
|
99
|
+
readonly specifiers: ReadonlySet<string>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The codes of the parts this module reads. */
|
|
103
|
+
export const UNUSED_PARAMETER = "DS1801";
|
|
104
|
+
export const UNUSED_RESULT = "DS1803";
|
|
105
|
+
const UNREACHABLE_STATEMENT = "DS1805";
|
|
106
|
+
const DEAD_STORE = "DS1807";
|
|
107
|
+
const UNREACHABLE_CASE = "DS1809";
|
|
108
|
+
|
|
109
|
+
/** The result types that carry no value, as the checker spells them. */
|
|
110
|
+
const NO_VALUE: ReadonlySet<string> = new Set([
|
|
111
|
+
"void",
|
|
112
|
+
"undefined",
|
|
113
|
+
"never",
|
|
114
|
+
"Promise<void>",
|
|
115
|
+
"Promise<undefined>",
|
|
116
|
+
"Promise<never>",
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
/** The prefix that marks a parameter as unused on purpose, as the compiler's own rule reads it. */
|
|
120
|
+
const UNUSED_PREFIX = "_";
|
|
121
|
+
|
|
122
|
+
/** The rendered position of one node, from its first token to its last line. */
|
|
123
|
+
function spanOf(file: SourceFile, root: string, node: Node): FindingPosition {
|
|
124
|
+
const start = renderPosition(file, root, node.getStart());
|
|
125
|
+
const endLine = file.getLineAndCharacterOfPosition(node.end).line + 1;
|
|
126
|
+
return { ...start, endLine };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The parenthesized expression's content, through every pair of parentheses. */
|
|
130
|
+
function unwrapped(node: Node): Node {
|
|
131
|
+
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The node the parentheses around one expression sit in. */
|
|
135
|
+
function outerParent(node: Node): Node {
|
|
136
|
+
return isParenthesizedExpression(node.parent) ? outerParent(node.parent) : node.parent;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The name a call is made through: the identifier, or the member a property access names. */
|
|
140
|
+
function calleeName(call: CallExpression): Identifier | undefined {
|
|
141
|
+
const callee = unwrapped(call.expression);
|
|
142
|
+
if (isIdentifier(callee)) {
|
|
143
|
+
return callee;
|
|
144
|
+
}
|
|
145
|
+
return isPropertyAccessExpression(callee) && isIdentifier(callee.name) ? callee.name : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether a call's value is discarded: the call is a statement of its own, awaited as a
|
|
150
|
+
* statement, or the operand of `void`.
|
|
151
|
+
*/
|
|
152
|
+
function discarded(call: CallExpression): boolean {
|
|
153
|
+
const parent = outerParent(call);
|
|
154
|
+
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return isAwaitExpression(parent) && isExpressionStatement(outerParent(parent));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Whether one statement leaves its statement list on every path: the rest of the list never runs. */
|
|
161
|
+
function abrupt(statement: Statement): boolean {
|
|
162
|
+
if (
|
|
163
|
+
isReturnStatement(statement) ||
|
|
164
|
+
isThrowStatement(statement) ||
|
|
165
|
+
isBreakStatement(statement) ||
|
|
166
|
+
isContinueStatement(statement)
|
|
167
|
+
) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
if (isBlock(statement)) {
|
|
171
|
+
return statement.statements.some(abrupt);
|
|
172
|
+
}
|
|
173
|
+
if (isIfStatement(statement)) {
|
|
174
|
+
return (
|
|
175
|
+
statement.elseStatement !== undefined &&
|
|
176
|
+
abrupt(statement.thenStatement) &&
|
|
177
|
+
abrupt(statement.elseStatement)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Whether an unreachable statement is no subject: a declaration the language hoists or
|
|
185
|
+
* that emits nothing, an empty statement, and a `var` that initializes nothing.
|
|
186
|
+
*/
|
|
187
|
+
function unreachableExempt(statement: Statement): boolean {
|
|
188
|
+
if (
|
|
189
|
+
isFunctionDeclaration(statement) ||
|
|
190
|
+
isInterfaceDeclaration(statement) ||
|
|
191
|
+
isTypeAliasDeclaration(statement) ||
|
|
192
|
+
isModuleDeclaration(statement) ||
|
|
193
|
+
isEmptyStatement(statement)
|
|
194
|
+
) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
if (isVariableStatement(statement)) {
|
|
198
|
+
const list = statement.declarationList;
|
|
199
|
+
return (
|
|
200
|
+
(list.flags & (NodeFlags.Let | NodeFlags.Const | NodeFlags.Using)) === 0 &&
|
|
201
|
+
list.declarations.every((one) => one.initializer === undefined)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The value a case expression is a literal of, as a key two equal literals share, or none. */
|
|
208
|
+
function caseKey(expression: Node): string | undefined {
|
|
209
|
+
const node = unwrapped(expression);
|
|
210
|
+
if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
|
|
211
|
+
return `s:${node.text}`;
|
|
212
|
+
}
|
|
213
|
+
if (isNumericLiteral(node)) {
|
|
214
|
+
return `n:${String(Number(node.text))}`;
|
|
215
|
+
}
|
|
216
|
+
if (isBigIntLiteral(node)) {
|
|
217
|
+
return `b:${BigInt(node.text.slice(0, -1)).toString()}`;
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
isPrefixUnaryExpression(node) &&
|
|
221
|
+
node.operator === SyntaxKind.MinusToken &&
|
|
222
|
+
isNumericLiteral(node.operand)
|
|
223
|
+
) {
|
|
224
|
+
return `n:${String(-Number(node.operand.text))}`;
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
node.kind === SyntaxKind.TrueKeyword ||
|
|
228
|
+
node.kind === SyntaxKind.FalseKeyword ||
|
|
229
|
+
node.kind === SyntaxKind.NullKeyword
|
|
230
|
+
) {
|
|
231
|
+
return `k:${String(node.kind)}`;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Every identifier below one node whose text one of the names spells, a member's name aside. */
|
|
237
|
+
function identifiersNaming(node: Node, names: ReadonlySet<string>): Identifier[] {
|
|
238
|
+
const found: Identifier[] = [];
|
|
239
|
+
const visit = (at: Node): void => {
|
|
240
|
+
if (isIdentifier(at)) {
|
|
241
|
+
const parent = at.parent;
|
|
242
|
+
if (names.has(at.text) && !(isPropertyAccessExpression(parent) && parent.name === at)) {
|
|
243
|
+
found.push(at);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
at.forEachChild(visit);
|
|
248
|
+
};
|
|
249
|
+
visit(node);
|
|
250
|
+
return found;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Whether one node is below another, or is it. */
|
|
254
|
+
function within(node: Node, container: Node): boolean {
|
|
255
|
+
return node.pos >= container.pos && node.end <= container.end;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Whether a node is written inside a function nested in `outer`, which may run at any time. */
|
|
259
|
+
function inNestedFunction(node: Node, outer: Node): boolean {
|
|
260
|
+
for (let at = node.parent; at !== outer; at = at.parent) {
|
|
261
|
+
if (
|
|
262
|
+
isArrowFunction(at) ||
|
|
263
|
+
at.kind === SyntaxKind.FunctionExpression ||
|
|
264
|
+
isFunctionDeclaration(at) ||
|
|
265
|
+
isMethodDeclaration(at) ||
|
|
266
|
+
isClassDeclaration(at) ||
|
|
267
|
+
isClassExpression(at) ||
|
|
268
|
+
at.kind === SyntaxKind.GetAccessor ||
|
|
269
|
+
at.kind === SyntaxKind.SetAccessor ||
|
|
270
|
+
at.kind === SyntaxKind.Constructor
|
|
271
|
+
) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Whether a body is a stub: it holds no statement, or nothing but `throw` statements. */
|
|
279
|
+
function isStub(signature: SignatureNode): boolean {
|
|
280
|
+
const body = signature.body;
|
|
281
|
+
return body !== undefined && isBlock(body) && body.statements.every(isThrowStatement);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Whether a function's body or signature returns a value: an expression body, or `return` with one. */
|
|
285
|
+
function returnsAValue(signature: SignatureNode): boolean {
|
|
286
|
+
const body = signature.body;
|
|
287
|
+
if (body === undefined) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (!isBlock(body)) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
let found = false;
|
|
294
|
+
const visit = (node: Node): void => {
|
|
295
|
+
if (found || inNestedFunction(node, signature)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (isReturnStatement(node) && node.expression !== undefined) {
|
|
299
|
+
found = true;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
node.forEachChild(visit);
|
|
303
|
+
};
|
|
304
|
+
body.forEachChild(visit);
|
|
305
|
+
return found;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Whether one node is written with a modifier of one kind. */
|
|
309
|
+
function hasModifier(node: Node, kind: SyntaxKind): boolean {
|
|
310
|
+
return ((node as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(
|
|
311
|
+
(modifier) => modifier.kind === kind,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Every class name an `extends` clause of the project names, by its last identifier. */
|
|
316
|
+
function extendedNames(files: readonly SourceFile[]): ReadonlySet<string> {
|
|
317
|
+
const names = new Set<string>();
|
|
318
|
+
const visit = (node: Node): void => {
|
|
319
|
+
if (isHeritageClause(node) && node.token === SyntaxKind.ExtendsKeyword) {
|
|
320
|
+
for (const type of node.types) {
|
|
321
|
+
const expression = unwrapped(type.expression);
|
|
322
|
+
const named = isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
323
|
+
if (isIdentifier(named)) {
|
|
324
|
+
names.add(named.text);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
node.forEachChild(visit);
|
|
329
|
+
};
|
|
330
|
+
for (const file of files) {
|
|
331
|
+
file.forEachChild(visit);
|
|
332
|
+
}
|
|
333
|
+
return names;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Whether a class hierarchy fixes one method's signature: its class extends or implements
|
|
338
|
+
* another type, is abstract or is extended itself, or the method is marked `override`.
|
|
339
|
+
*/
|
|
340
|
+
function hierarchyFixes(declared: Node, extended: ReadonlySet<string>): boolean {
|
|
341
|
+
if (!isMethodDeclaration(declared)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
const owner = declared.parent;
|
|
345
|
+
if (!isClassDeclaration(owner) && !isClassExpression(owner)) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (
|
|
349
|
+
hasModifier(declared, SyntaxKind.OverrideKeyword) ||
|
|
350
|
+
hasModifier(owner, SyntaxKind.AbstractKeyword)
|
|
351
|
+
) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if ((owner.heritageClauses ?? []).length > 0) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
return owner.name !== undefined && extended.has(owner.name.text);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** One declaration of the inventory that declares a function, and that function. */
|
|
361
|
+
interface Declared {
|
|
362
|
+
readonly id: string;
|
|
363
|
+
readonly node: Node;
|
|
364
|
+
readonly signature: SignatureNode;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The symbol identifier each of a batch of identifiers resolves to, where the batch answers. */
|
|
368
|
+
function resolvedIds<Brand>(
|
|
369
|
+
project: ProjectView<Brand>,
|
|
370
|
+
nodes: readonly Identifier[],
|
|
371
|
+
): ReadonlyMap<Node, number> {
|
|
372
|
+
const ids = new Map<Node, number>();
|
|
373
|
+
for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
|
|
374
|
+
const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
|
|
375
|
+
project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
376
|
+
const node = run[index];
|
|
377
|
+
if (symbol !== undefined && node !== undefined) {
|
|
378
|
+
ids.set(node, symbol.id);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return ids;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Whether one identifier reads the binding a name node declares: it resolves to the
|
|
387
|
+
* same symbol, or the checker resolves it to nothing, which is read as a use rather than
|
|
388
|
+
* risk a finding; a shorthand property reads the binding of its own name.
|
|
389
|
+
*/
|
|
390
|
+
function readsBinding(
|
|
391
|
+
use: Identifier,
|
|
392
|
+
declared: Identifier,
|
|
393
|
+
ids: ReadonlyMap<Node, number>,
|
|
394
|
+
): boolean {
|
|
395
|
+
if (isShorthandPropertyAssignment(use.parent)) {
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
const at = ids.get(use);
|
|
399
|
+
return at === undefined || at === ids.get(declared);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** What one file's walk collects before the batch that resolves its names. */
|
|
403
|
+
interface FileWalk {
|
|
404
|
+
readonly declared: Declared[];
|
|
405
|
+
readonly blocks: Block[];
|
|
406
|
+
readonly unreachable: { readonly statement: Statement; readonly holder: string }[];
|
|
407
|
+
readonly cases: { readonly clause: Node; readonly text: string; readonly holder: string }[];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The parts of one project's own declarations. `references` are the project's own
|
|
412
|
+
* references, which say which declarations a discarded call reaches, so the checker is
|
|
413
|
+
* asked for a result's type only where a call discards it.
|
|
414
|
+
*/
|
|
415
|
+
export function projectParts<Brand>(
|
|
416
|
+
project: ProjectView<Brand>,
|
|
417
|
+
held: Inventory,
|
|
418
|
+
references: readonly Reference[],
|
|
419
|
+
targetRoot: string,
|
|
420
|
+
): ProjectParts {
|
|
421
|
+
const files = project.ownSourceFiles();
|
|
422
|
+
const extended = extendedNames(files);
|
|
423
|
+
const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
|
|
424
|
+
const parts: PartFact[] = [];
|
|
425
|
+
const signatures: SignatureFact[] = [];
|
|
426
|
+
const calls = new Map<string, boolean>();
|
|
427
|
+
const specifiers = new Set<string>();
|
|
428
|
+
const sites: { readonly file: SourceFile; readonly declared: Declared }[] = [];
|
|
429
|
+
|
|
430
|
+
for (const file of files) {
|
|
431
|
+
const declarationAt = (node: Node): string | undefined => {
|
|
432
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
433
|
+
return id === undefined || kinds.get(id) === "file" ? undefined : id;
|
|
434
|
+
};
|
|
435
|
+
const holderOf = (node: Node): string | undefined => {
|
|
436
|
+
for (let at = node.parent; at.kind !== SyntaxKind.SourceFile; at = at.parent) {
|
|
437
|
+
const id = declarationAt(at);
|
|
438
|
+
if (id !== undefined) {
|
|
439
|
+
return id;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return undefined;
|
|
443
|
+
};
|
|
444
|
+
const walk: FileWalk = { declared: [], blocks: [], unreachable: [], cases: [] };
|
|
445
|
+
const position = (node: Node): string =>
|
|
446
|
+
positionKey(renderPosition(file, targetRoot, node.getStart()));
|
|
447
|
+
|
|
448
|
+
// Code below an unreachable statement is still walked for its calls and specifiers,
|
|
449
|
+
// and reports no part of its own: the unreachable statement is the one finding.
|
|
450
|
+
let silenced = 0;
|
|
451
|
+
const statementsOf = (statements: readonly Statement[]): void => {
|
|
452
|
+
let reachable = true;
|
|
453
|
+
let reported = false;
|
|
454
|
+
for (const statement of statements) {
|
|
455
|
+
if (reachable) {
|
|
456
|
+
visit(statement);
|
|
457
|
+
reachable = !abrupt(statement);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const holder = holderOf(statement);
|
|
461
|
+
if (silenced === 0 && !reported && !unreachableExempt(statement) && holder !== undefined) {
|
|
462
|
+
walk.unreachable.push({ statement, holder });
|
|
463
|
+
reported = true;
|
|
464
|
+
}
|
|
465
|
+
silenced += 1;
|
|
466
|
+
visit(statement);
|
|
467
|
+
silenced -= 1;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const visit = (node: Node): void => {
|
|
471
|
+
const id = declarationAt(node);
|
|
472
|
+
const signature = id === undefined ? undefined : signatureOf(node);
|
|
473
|
+
if (id !== undefined && signature !== undefined) {
|
|
474
|
+
walk.declared.push({ id, node, signature });
|
|
475
|
+
}
|
|
476
|
+
if (isCallExpression(node)) {
|
|
477
|
+
const name = calleeName(node);
|
|
478
|
+
if (name !== undefined) {
|
|
479
|
+
calls.set(position(name), discarded(node));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (isImportSpecifier(node) || isExportSpecifier(node)) {
|
|
483
|
+
for (const name of [node.propertyName, node.name]) {
|
|
484
|
+
if (name !== undefined) {
|
|
485
|
+
specifiers.add(position(name));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if ((isImportClause(node) || isNamespaceImport(node)) && node.name !== undefined) {
|
|
490
|
+
specifiers.add(position(node.name));
|
|
491
|
+
}
|
|
492
|
+
if (isExportAssignment(node) && isIdentifier(unwrapped(node.expression))) {
|
|
493
|
+
specifiers.add(position(unwrapped(node.expression)));
|
|
494
|
+
}
|
|
495
|
+
if (isSwitchStatement(node)) {
|
|
496
|
+
const holder = holderOf(node);
|
|
497
|
+
const seen = new Set<string>();
|
|
498
|
+
for (const clause of node.caseBlock.clauses) {
|
|
499
|
+
if (!isCaseClause(clause)) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const key = caseKey(clause.expression);
|
|
503
|
+
if (key !== undefined && seen.has(key) && holder !== undefined && silenced === 0) {
|
|
504
|
+
walk.cases.push({ clause, text: clause.expression.getText(), holder });
|
|
505
|
+
}
|
|
506
|
+
if (key !== undefined) {
|
|
507
|
+
seen.add(key);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (isBlock(node) && silenced === 0 && holderOf(node) !== undefined) {
|
|
512
|
+
walk.blocks.push(node);
|
|
513
|
+
}
|
|
514
|
+
if (isBlock(node) || isCaseClause(node) || isDefaultClause(node)) {
|
|
515
|
+
statementsOf(node.statements);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
node.forEachChild(visit);
|
|
519
|
+
};
|
|
520
|
+
file.forEachChild(visit);
|
|
521
|
+
|
|
522
|
+
// One batch per file resolves every name a parameter or a local is read through.
|
|
523
|
+
const asked: Identifier[] = [];
|
|
524
|
+
const readers = new Map<Declared, { names: Identifier[]; uses: Identifier[] }>();
|
|
525
|
+
for (const one of walk.declared) {
|
|
526
|
+
const names = one.signature.parameters
|
|
527
|
+
.map((parameter) => parameter.name)
|
|
528
|
+
.filter(isIdentifier);
|
|
529
|
+
const spelled = new Set(names.map((name) => name.text));
|
|
530
|
+
const uses = identifiersNaming(one.signature, spelled).filter((use) => !names.includes(use));
|
|
531
|
+
readers.set(one, { names, uses });
|
|
532
|
+
asked.push(...names, ...uses);
|
|
533
|
+
}
|
|
534
|
+
const stores = walk.blocks.map((block) => localsOf(block));
|
|
535
|
+
for (const store of stores) {
|
|
536
|
+
asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
|
|
537
|
+
}
|
|
538
|
+
const ids = resolvedIds(project, [...new Set(asked)]);
|
|
539
|
+
|
|
540
|
+
for (const one of walk.declared) {
|
|
541
|
+
sites.push({ file, declared: one });
|
|
542
|
+
signatures.push({
|
|
543
|
+
declaration: one.id,
|
|
544
|
+
stub: isStub(one.signature),
|
|
545
|
+
fixed: hierarchyFixes(one.node, extended),
|
|
546
|
+
});
|
|
547
|
+
const { uses } = readers.get(one) ?? { uses: [] };
|
|
548
|
+
parts.push(...parameterParts(file, targetRoot, one, uses, ids));
|
|
549
|
+
}
|
|
550
|
+
walk.blocks.forEach((block, at) => {
|
|
551
|
+
const holder = holderOf(block);
|
|
552
|
+
if (holder !== undefined) {
|
|
553
|
+
parts.push(...deadStores(file, targetRoot, block, holder, stores[at] ?? [], ids));
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
for (const { statement, holder } of walk.unreachable) {
|
|
557
|
+
parts.push({
|
|
558
|
+
code: UNREACHABLE_STATEMENT,
|
|
559
|
+
declaration: holder,
|
|
560
|
+
kind: "statement",
|
|
561
|
+
name: "statement",
|
|
562
|
+
position: spanOf(file, targetRoot, statement),
|
|
563
|
+
message: "statement is unreachable",
|
|
564
|
+
used: false,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
for (const { clause, text, holder } of walk.cases) {
|
|
568
|
+
parts.push({
|
|
569
|
+
code: UNREACHABLE_CASE,
|
|
570
|
+
declaration: holder,
|
|
571
|
+
kind: "case",
|
|
572
|
+
name: text,
|
|
573
|
+
position: spanOf(file, targetRoot, clause),
|
|
574
|
+
message: `case ${text} is covered by an earlier case of the same switch`,
|
|
575
|
+
used: false,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
parts.push(...resultParts(project, targetRoot, sites, references, calls));
|
|
581
|
+
return { parts, signatures, calls, specifiers };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Each named parameter of one function and whether the function reads it. */
|
|
585
|
+
function parameterParts(
|
|
586
|
+
file: SourceFile,
|
|
587
|
+
root: string,
|
|
588
|
+
one: Declared,
|
|
589
|
+
uses: readonly Identifier[],
|
|
590
|
+
ids: ReadonlyMap<Node, number>,
|
|
591
|
+
): PartFact[] {
|
|
592
|
+
const signature = one.signature;
|
|
593
|
+
// A function that reads `arguments` reads every parameter by position.
|
|
594
|
+
const readsArguments =
|
|
595
|
+
!isArrowFunction(signature) && identifiersNaming(signature, new Set(["arguments"])).length > 0;
|
|
596
|
+
const found: PartFact[] = [];
|
|
597
|
+
for (const parameter of signature.parameters) {
|
|
598
|
+
const name = parameter.name;
|
|
599
|
+
if (
|
|
600
|
+
!isIdentifier(name) ||
|
|
601
|
+
name.text === "this" ||
|
|
602
|
+
name.text.startsWith(UNUSED_PREFIX) ||
|
|
603
|
+
((parameter as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(isDecorator)
|
|
604
|
+
) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
found.push({
|
|
608
|
+
code: UNUSED_PARAMETER,
|
|
609
|
+
declaration: one.id,
|
|
610
|
+
kind: "parameter",
|
|
611
|
+
name: name.text,
|
|
612
|
+
position: spanOf(file, root, name),
|
|
613
|
+
message: `parameter ${name.text} is never read in the body`,
|
|
614
|
+
used:
|
|
615
|
+
readsArguments ||
|
|
616
|
+
uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return found;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** One local a block declares by name, and every identifier of the block that spells it. */
|
|
623
|
+
interface Local {
|
|
624
|
+
readonly name: Identifier;
|
|
625
|
+
readonly statement: number;
|
|
626
|
+
readonly initialized: boolean;
|
|
627
|
+
readonly uses: readonly Identifier[];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** The locals one block declares with `let` or `const`, each with the names that spell it. */
|
|
631
|
+
function localsOf(block: Block): readonly Local[] {
|
|
632
|
+
const declared: { name: Identifier; statement: number; initialized: boolean }[] = [];
|
|
633
|
+
block.statements.forEach((statement, at) => {
|
|
634
|
+
if (!isVariableStatement(statement)) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const list = statement.declarationList;
|
|
638
|
+
if (
|
|
639
|
+
(list.flags & (NodeFlags.Let | NodeFlags.Const)) === 0 ||
|
|
640
|
+
(list.flags & NodeFlags.Using) !== 0
|
|
641
|
+
) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
for (const declaration of list.declarations) {
|
|
645
|
+
if (isIdentifier(declaration.name)) {
|
|
646
|
+
declared.push({
|
|
647
|
+
name: declaration.name,
|
|
648
|
+
statement: at,
|
|
649
|
+
initialized: declaration.initializer !== undefined,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
if (declared.length === 0) {
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
const spelled = identifiersNaming(block, new Set(declared.map((one) => one.name.text)));
|
|
658
|
+
return declared.map((one) => ({
|
|
659
|
+
...one,
|
|
660
|
+
uses: spelled.filter((use) => use !== one.name && use.text === one.name.text),
|
|
661
|
+
}));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* The writes to the locals of one block that no read reaches. A write is a declaration's
|
|
666
|
+
* initializer or a plain assignment written as a statement of the block itself; it is
|
|
667
|
+
* dead when the statements after it in the block read the local nowhere before the next
|
|
668
|
+
* such write to it, or before the block ends, which ends the local's scope. A statement
|
|
669
|
+
* that writes the local inside a nested construct ends the search with the write kept,
|
|
670
|
+
* and a local a nested function names is no subject, because the function may read it
|
|
671
|
+
* at any time.
|
|
672
|
+
*/
|
|
673
|
+
function deadStores(
|
|
674
|
+
file: SourceFile,
|
|
675
|
+
root: string,
|
|
676
|
+
block: Block,
|
|
677
|
+
holder: string,
|
|
678
|
+
locals: readonly Local[],
|
|
679
|
+
ids: ReadonlyMap<Node, number>,
|
|
680
|
+
): PartFact[] {
|
|
681
|
+
const found: PartFact[] = [];
|
|
682
|
+
for (const local of locals) {
|
|
683
|
+
const uses = local.uses.filter((use) => readsBinding(use, local.name, ids));
|
|
684
|
+
if (uses.some((use) => inNestedFunction(use, block))) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const statements = block.statements;
|
|
688
|
+
const usesIn = (statement: Node): Identifier[] => uses.filter((use) => within(use, statement));
|
|
689
|
+
/** The plain assignment to the local a statement of the block is, where it is one. */
|
|
690
|
+
const writeOf = (statement: Statement): { target: Identifier; value: Node } | undefined => {
|
|
691
|
+
if (!isExpressionStatement(statement)) {
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
const expression = unwrapped(statement.expression);
|
|
695
|
+
if (
|
|
696
|
+
!isBinaryExpression(expression) ||
|
|
697
|
+
expression.operatorToken.kind !== SyntaxKind.EqualsToken
|
|
698
|
+
) {
|
|
699
|
+
return undefined;
|
|
700
|
+
}
|
|
701
|
+
const target = unwrapped(expression.left);
|
|
702
|
+
return isIdentifier(target) && uses.includes(target)
|
|
703
|
+
? { target, value: expression.right }
|
|
704
|
+
: undefined;
|
|
705
|
+
};
|
|
706
|
+
const writes: { at: number; target: Identifier }[] = [];
|
|
707
|
+
if (local.initialized) {
|
|
708
|
+
writes.push({ at: local.statement, target: local.name });
|
|
709
|
+
}
|
|
710
|
+
statements.forEach((statement, at) => {
|
|
711
|
+
const write = writeOf(statement);
|
|
712
|
+
if (write !== undefined && at > local.statement) {
|
|
713
|
+
writes.push({ at, target: write.target });
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
for (const write of writes) {
|
|
717
|
+
// The rest of the write's own statement runs after it: a later declarator may read the local.
|
|
718
|
+
const own = statements[write.at];
|
|
719
|
+
const sameStatement =
|
|
720
|
+
own !== undefined && write.target === local.name
|
|
721
|
+
? usesIn(own).filter((use) => use !== local.name)
|
|
722
|
+
: [];
|
|
723
|
+
if (sameStatement.length > 0) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
let dead = true;
|
|
727
|
+
for (let at = write.at + 1; at < statements.length; at += 1) {
|
|
728
|
+
const statement = statements[at];
|
|
729
|
+
if (statement === undefined) {
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
const next = writeOf(statement);
|
|
733
|
+
if (next !== undefined) {
|
|
734
|
+
dead = usesIn(next.value).length === 0;
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
const named = usesIn(statement);
|
|
738
|
+
if (named.length > 0) {
|
|
739
|
+
dead = false;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (!dead) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const position = spanOf(file, root, write.target);
|
|
747
|
+
found.push({
|
|
748
|
+
code: DEAD_STORE,
|
|
749
|
+
declaration: holder,
|
|
750
|
+
kind: "store",
|
|
751
|
+
name: local.name.text,
|
|
752
|
+
position,
|
|
753
|
+
message: `value written to ${local.name.text} is never read`,
|
|
754
|
+
used: false,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return found;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* The result of each function a discarded call reaches, where its type carries a value:
|
|
763
|
+
* whether it is used is the call sites' to say, which the run decides over every project.
|
|
764
|
+
*/
|
|
765
|
+
function resultParts<Brand>(
|
|
766
|
+
project: ProjectView<Brand>,
|
|
767
|
+
root: string,
|
|
768
|
+
sites: readonly { readonly file: SourceFile; readonly declared: Declared }[],
|
|
769
|
+
references: readonly Reference[],
|
|
770
|
+
calls: ReadonlyMap<string, boolean>,
|
|
771
|
+
): PartFact[] {
|
|
772
|
+
const discardedAt = new Set(
|
|
773
|
+
references.filter((one) => calls.get(positionKey(one.position)) === true).map((one) => one.to),
|
|
774
|
+
);
|
|
775
|
+
const found: PartFact[] = [];
|
|
776
|
+
for (const { file, declared } of sites) {
|
|
777
|
+
const { signature, node, id } = declared;
|
|
778
|
+
const generator = (signature as { readonly asteriskToken?: Node }).asteriskToken !== undefined;
|
|
779
|
+
if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const typed = project.checker.getSignatureFromDeclaration(signature);
|
|
783
|
+
const result =
|
|
784
|
+
typed === undefined ? undefined : project.checker.getReturnTypeOfSignature(typed);
|
|
785
|
+
if (result === undefined || NO_VALUE.has(project.checker.typeToString(result))) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const at = signature.type ?? (node as { readonly name?: Node }).name ?? node;
|
|
789
|
+
found.push({
|
|
790
|
+
code: UNUSED_RESULT,
|
|
791
|
+
declaration: id,
|
|
792
|
+
kind: "result",
|
|
793
|
+
name: "result",
|
|
794
|
+
position: spanOf(file, root, at),
|
|
795
|
+
message: "result is discarded at every call site",
|
|
796
|
+
used: false,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
return found;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** What the run answers about the parts of its declarations, over every project. */
|
|
803
|
+
export interface IntraFunctionFacts {
|
|
804
|
+
/** Every part of the run, each once, in use where any project found it in use. */
|
|
805
|
+
readonly parts: readonly PartFact[];
|
|
806
|
+
/** The declarations whose signature is free to change. */
|
|
807
|
+
readonly free: ReadonlySet<string>;
|
|
808
|
+
/**
|
|
809
|
+
* The declarations every reference of which is a call in the loaded program that
|
|
810
|
+
* discards the call's value, with at least one such call.
|
|
811
|
+
*/
|
|
812
|
+
readonly discardedEverywhere: ReadonlySet<string>;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* The parts of a run's projects merged: a part two projects hold is one part, in use
|
|
817
|
+
* where either found it in use. A signature is free when no exemption record names its
|
|
818
|
+
* declaration, its body is no stub, no class hierarchy fixes it, and every reference to
|
|
819
|
+
* the declaration is a call or a specifier: any other reference uses the function as a
|
|
820
|
+
* value, whose type then fixes the signature, and a reference a consumer makes is one
|
|
821
|
+
* whose form this run does not read.
|
|
822
|
+
*/
|
|
823
|
+
export function intraFunctionFacts(
|
|
824
|
+
projects: readonly ProjectParts[],
|
|
825
|
+
references: readonly Reference[],
|
|
826
|
+
exempted: ReadonlySet<string>,
|
|
827
|
+
): IntraFunctionFacts {
|
|
828
|
+
const parts = new Map<string, PartFact>();
|
|
829
|
+
for (const part of projects.flatMap((one) => one.parts)) {
|
|
830
|
+
const key = `${part.code}\u0000${positionKey(part.position)}`;
|
|
831
|
+
const known = parts.get(key);
|
|
832
|
+
parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
|
|
833
|
+
}
|
|
834
|
+
const calls = new Map<string, boolean>();
|
|
835
|
+
const specifiers = new Set<string>();
|
|
836
|
+
for (const one of projects) {
|
|
837
|
+
for (const [at, value] of one.calls) {
|
|
838
|
+
calls.set(at, value);
|
|
839
|
+
}
|
|
840
|
+
for (const at of one.specifiers) {
|
|
841
|
+
specifiers.add(at);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const valued = new Set<string>();
|
|
845
|
+
const called = new Map<string, boolean>();
|
|
846
|
+
for (const reference of references) {
|
|
847
|
+
if (reference.use === "decorator" || reference.use === "evaluation") {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const at = positionKey(reference.position);
|
|
851
|
+
const call = reference.consumer === undefined ? calls.get(at) : undefined;
|
|
852
|
+
if (call !== undefined) {
|
|
853
|
+
called.set(reference.to, (called.get(reference.to) ?? true) && call);
|
|
854
|
+
} else if (reference.consumer !== undefined || !specifiers.has(at)) {
|
|
855
|
+
valued.add(reference.to);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const signatures = projects.flatMap((one) => one.signatures);
|
|
859
|
+
const fixed = new Set(
|
|
860
|
+
signatures.filter((one) => one.stub || one.fixed).map((one) => one.declaration),
|
|
861
|
+
);
|
|
862
|
+
const free = new Set(
|
|
863
|
+
signatures
|
|
864
|
+
.map((one) => one.declaration)
|
|
865
|
+
.filter((id) => !fixed.has(id) && !exempted.has(id) && !valued.has(id)),
|
|
866
|
+
);
|
|
867
|
+
const discardedEverywhere = new Set(
|
|
868
|
+
[...called].filter(([id, all]) => all && !valued.has(id)).map(([id]) => id),
|
|
869
|
+
);
|
|
870
|
+
return { parts: [...parts.values()], free, discardedEverywhere };
|
|
871
|
+
}
|