@cplieger/deadset-ts 8.0.0-dev.21 → 8.0.0-dev.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +1 -1
- package/dist/src/findings/intra-function.js +50 -37
- package/dist/src/intra-function-parts.js +1149 -222
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +1 -1
- package/src/findings/intra-function.ts +59 -37
- package/src/intra-function-parts.ts +1435 -247
|
@@ -5,41 +5,52 @@
|
|
|
5
5
|
* write to a local that no read reaches, and each case an earlier case covers.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { CODE } from "./contract/contract.generated.ts";
|
|
9
8
|
import {
|
|
10
9
|
isArrowFunction,
|
|
11
10
|
isAwaitExpression,
|
|
12
11
|
isBigIntLiteral,
|
|
13
12
|
isBinaryExpression,
|
|
13
|
+
isBindingElement,
|
|
14
14
|
isBlock,
|
|
15
15
|
isBreakStatement,
|
|
16
|
-
isBindingElement,
|
|
17
16
|
isCallExpression,
|
|
18
17
|
isCaseClause,
|
|
19
18
|
isCatchClause,
|
|
20
19
|
isClassDeclaration,
|
|
20
|
+
isClassStaticBlockDeclaration,
|
|
21
21
|
isClassExpression,
|
|
22
|
+
isConditionalExpression,
|
|
22
23
|
isConstructorDeclaration,
|
|
23
24
|
isContinueStatement,
|
|
24
25
|
isDecorator,
|
|
25
26
|
isDefaultClause,
|
|
26
|
-
|
|
27
|
+
isDoStatement,
|
|
28
|
+
isEnumDeclaration,
|
|
29
|
+
isExportDeclaration,
|
|
27
30
|
isExportAssignment,
|
|
28
31
|
isExportSpecifier,
|
|
29
32
|
isExpressionStatement,
|
|
30
|
-
|
|
31
|
-
isFunctionExpression,
|
|
33
|
+
isExpressionWithTypeArguments,
|
|
32
34
|
isForInStatement,
|
|
33
35
|
isForOfStatement,
|
|
36
|
+
isForStatement,
|
|
37
|
+
isFunctionDeclaration,
|
|
38
|
+
isFunctionExpression,
|
|
34
39
|
isFunctionLikeDeclaration,
|
|
40
|
+
isGetAccessorDeclaration,
|
|
35
41
|
isHeritageClause,
|
|
36
42
|
isIdentifier,
|
|
37
43
|
isIfStatement,
|
|
44
|
+
isInterfaceDeclaration,
|
|
38
45
|
isImportClause,
|
|
46
|
+
isImportDeclaration,
|
|
47
|
+
isImportEqualsDeclaration,
|
|
39
48
|
isImportSpecifier,
|
|
40
|
-
|
|
49
|
+
isLabeledStatement,
|
|
41
50
|
isMethodDeclaration,
|
|
51
|
+
isModuleBlock,
|
|
42
52
|
isModuleDeclaration,
|
|
53
|
+
isNamedExports,
|
|
43
54
|
isNamespaceImport,
|
|
44
55
|
isNewExpression,
|
|
45
56
|
isNoSubstitutionTemplateLiteral,
|
|
@@ -47,40 +58,63 @@ import {
|
|
|
47
58
|
isObjectBindingPattern,
|
|
48
59
|
isObjectLiteralExpression,
|
|
49
60
|
isParenthesizedExpression,
|
|
61
|
+
isParameterDeclaration,
|
|
50
62
|
isPrefixUnaryExpression,
|
|
51
|
-
|
|
63
|
+
isPrivateIdentifier,
|
|
52
64
|
isPropertyAccessExpression,
|
|
65
|
+
isPropertyAssignment,
|
|
66
|
+
isPropertyDeclaration,
|
|
67
|
+
isPropertySignatureDeclaration,
|
|
53
68
|
isQualifiedName,
|
|
54
69
|
isReturnStatement,
|
|
70
|
+
isSetAccessorDeclaration,
|
|
55
71
|
isShorthandPropertyAssignment,
|
|
56
72
|
isSourceFile,
|
|
57
73
|
isStringLiteral,
|
|
58
74
|
isSwitchStatement,
|
|
59
75
|
isThrowStatement,
|
|
76
|
+
isTryStatement,
|
|
60
77
|
isTypeAliasDeclaration,
|
|
78
|
+
isTypePredicateNode,
|
|
61
79
|
isTypeReferenceNode,
|
|
80
|
+
isVariableDeclaration,
|
|
62
81
|
isVariableDeclarationList,
|
|
63
82
|
isVariableStatement,
|
|
64
83
|
isVoidExpression,
|
|
84
|
+
isWhileStatement,
|
|
85
|
+
isWithStatement,
|
|
65
86
|
NodeFlags,
|
|
66
87
|
SyntaxKind,
|
|
88
|
+
type AccessorDeclaration,
|
|
67
89
|
type ArrowFunction,
|
|
90
|
+
type BinaryExpression,
|
|
68
91
|
type BindingName,
|
|
69
92
|
type Block,
|
|
70
93
|
type CallExpression,
|
|
71
94
|
type CatchClause,
|
|
72
95
|
type ConstructorDeclaration,
|
|
96
|
+
type ExportSpecifier,
|
|
97
|
+
type ExpressionStatement,
|
|
73
98
|
type ForInStatement,
|
|
74
99
|
type ForOfStatement,
|
|
75
|
-
type FunctionLikeDeclaration,
|
|
76
100
|
type FunctionExpression,
|
|
101
|
+
type FunctionLikeDeclaration,
|
|
77
102
|
type Identifier,
|
|
78
103
|
type Node,
|
|
104
|
+
type ParameterDeclaration,
|
|
79
105
|
type SourceFile,
|
|
80
106
|
type Statement,
|
|
81
107
|
type VariableDeclarationList,
|
|
82
108
|
} from "@typescript/native/ast";
|
|
83
|
-
import {
|
|
109
|
+
import {
|
|
110
|
+
SignatureKind,
|
|
111
|
+
SymbolFlags,
|
|
112
|
+
TypeFlags,
|
|
113
|
+
type NodeHandle,
|
|
114
|
+
type Signature,
|
|
115
|
+
type Symbol as TSSymbol,
|
|
116
|
+
type Type,
|
|
117
|
+
} from "@typescript/native/sync";
|
|
84
118
|
import type { FindingPosition } from "./finding.ts";
|
|
85
119
|
import {
|
|
86
120
|
exportsLocally,
|
|
@@ -90,6 +124,8 @@ import {
|
|
|
90
124
|
} from "./import-bindings.ts";
|
|
91
125
|
import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
|
|
92
126
|
import { positionKey, renderPosition } from "./position.ts";
|
|
127
|
+
import { publishedAPI } from "./roots/published-reach.ts";
|
|
128
|
+
import { sourceFilesOf } from "./source-files.ts";
|
|
93
129
|
import { importRef } from "./ref.ts";
|
|
94
130
|
import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
|
|
95
131
|
import type { Reference } from "./references.ts";
|
|
@@ -99,21 +135,16 @@ import { spellsNoBinding } from "./spelled-names.ts";
|
|
|
99
135
|
import { parenthesesRemoved } from "./unwrap.ts";
|
|
100
136
|
|
|
101
137
|
/** The subject kind of each part this module reads. */
|
|
102
|
-
type PartKind = "parameter" | "result" | "statement" | "store" | "case";
|
|
138
|
+
export type PartKind = "parameter" | "result" | "statement" | "store" | "case";
|
|
103
139
|
|
|
104
|
-
/** One part of one declaration
|
|
140
|
+
/** One part of one declaration that a project found doing nothing. */
|
|
105
141
|
export interface PartFact {
|
|
106
|
-
readonly code: string;
|
|
107
142
|
/** The inventory's identifier of the declaration that holds the part. */
|
|
108
143
|
readonly declaration: string;
|
|
109
144
|
readonly kind: PartKind;
|
|
110
145
|
/** The part's display name: an identifier, or the text of a case. */
|
|
111
146
|
readonly name: string;
|
|
112
147
|
readonly position: FindingPosition;
|
|
113
|
-
/** What a finding about the part says, before the rules that report it too. */
|
|
114
|
-
readonly message: string;
|
|
115
|
-
/** Whether the project found the part in use, which takes it out of the answer. */
|
|
116
|
-
readonly used: boolean;
|
|
117
148
|
/** Where the part is a parameter's name, what a function used as a value decides it by. */
|
|
118
149
|
readonly parameter?: ParameterPlace;
|
|
119
150
|
/** Where the part is a result, whether its type has a `then` member, as a promise does. */
|
|
@@ -131,6 +162,8 @@ type CallUse = "used" | "discarded" | "awaited";
|
|
|
131
162
|
interface ParameterPlace {
|
|
132
163
|
/** Whether a parameter after this one, which is no binding-pattern name, is read. */
|
|
133
164
|
readonly beforeARead: boolean;
|
|
165
|
+
/** Whether the function's `length` counts the parameter: none at or before it has a default or is a rest. */
|
|
166
|
+
readonly counted: boolean;
|
|
134
167
|
/** Whether no declaration of the inventory declares the function, which is then a value. */
|
|
135
168
|
readonly inValue: boolean;
|
|
136
169
|
/**
|
|
@@ -179,6 +212,12 @@ export interface ProjectParts {
|
|
|
179
212
|
readonly specifiers: ReadonlySet<string>;
|
|
180
213
|
/** Each name of {@link constructionNames}, by position, which uses no constructor as a value. */
|
|
181
214
|
readonly constructions: ReadonlySet<string>;
|
|
215
|
+
/** The rendered path of each file the project compiles, whose parts it judges. */
|
|
216
|
+
readonly files: ReadonlySet<string>;
|
|
217
|
+
/** The declarations whose result the project judged, holding a call that discards it. */
|
|
218
|
+
readonly results: ReadonlySet<string>;
|
|
219
|
+
/** What an outside importer of any of the project's files can name, read where a result part needs it. */
|
|
220
|
+
readonly published: ReadonlySet<string>;
|
|
182
221
|
}
|
|
183
222
|
|
|
184
223
|
/** The result types that carry no value, as the checker spells them. */
|
|
@@ -223,51 +262,673 @@ function callUse(call: CallExpression): CallUse {
|
|
|
223
262
|
: "used";
|
|
224
263
|
}
|
|
225
264
|
|
|
226
|
-
/**
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
265
|
+
/** What a namespace declaration holds at run time: nothing, only const enums, or values. */
|
|
266
|
+
const HOLDS_NOTHING = 0;
|
|
267
|
+
const HOLDS_CONST_ENUMS = 1;
|
|
268
|
+
const HOLDS_VALUES = 2;
|
|
269
|
+
type InstanceState = typeof HOLDS_NOTHING | typeof HOLDS_CONST_ENUMS | typeof HOLDS_VALUES;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The instance state of one declaration, as the compiler's binder computes it
|
|
273
|
+
* (`getModuleInstanceState`) from the declaration's own body. A name an export clause
|
|
274
|
+
* re-exports takes the state of the local declarations it names. A node met again while it
|
|
275
|
+
* is still being decided holds nothing, as in the binder.
|
|
276
|
+
*/
|
|
277
|
+
function instanceState(node: Node, visited = new Map<Node, InstanceState>()): InstanceState {
|
|
278
|
+
const known = visited.get(node);
|
|
279
|
+
if (known !== undefined || visited.has(node)) {
|
|
280
|
+
return known ?? HOLDS_NOTHING;
|
|
281
|
+
}
|
|
282
|
+
visited.set(node, HOLDS_NOTHING);
|
|
283
|
+
const highest = (nodes: readonly Node[], of: (one: Node) => InstanceState): InstanceState => {
|
|
284
|
+
let state: InstanceState = HOLDS_NOTHING;
|
|
285
|
+
for (const one of nodes) {
|
|
286
|
+
state = Math.max(state, of(one)) as InstanceState;
|
|
287
|
+
if (state === HOLDS_VALUES) {
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return state;
|
|
292
|
+
};
|
|
293
|
+
let state: InstanceState = HOLDS_VALUES;
|
|
294
|
+
if (isModuleDeclaration(node)) {
|
|
295
|
+
state = node.body === undefined ? HOLDS_VALUES : instanceState(node.body, visited);
|
|
296
|
+
} else if (isModuleBlock(node)) {
|
|
297
|
+
state = highest(node.statements, (one) => instanceState(one, visited));
|
|
298
|
+
} else if (isInterfaceDeclaration(node) || isTypeAliasDeclaration(node)) {
|
|
299
|
+
state = HOLDS_NOTHING;
|
|
300
|
+
} else if (isEnumDeclaration(node) && hasModifier(node, SyntaxKind.ConstKeyword)) {
|
|
301
|
+
state = HOLDS_CONST_ENUMS;
|
|
302
|
+
} else if (
|
|
303
|
+
(isImportDeclaration(node) || isImportEqualsDeclaration(node)) &&
|
|
304
|
+
!hasModifier(node, SyntaxKind.ExportKeyword)
|
|
233
305
|
) {
|
|
234
|
-
|
|
306
|
+
state = HOLDS_NOTHING;
|
|
307
|
+
} else if (
|
|
308
|
+
isExportDeclaration(node) &&
|
|
309
|
+
node.moduleSpecifier === undefined &&
|
|
310
|
+
node.exportClause !== undefined &&
|
|
311
|
+
isNamedExports(node.exportClause)
|
|
312
|
+
) {
|
|
313
|
+
state = highest(node.exportClause.elements, (one) =>
|
|
314
|
+
reexportedState(one as ExportSpecifier, visited),
|
|
315
|
+
);
|
|
235
316
|
}
|
|
236
|
-
|
|
237
|
-
|
|
317
|
+
visited.set(node, state);
|
|
318
|
+
return state;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The instance state of the local declarations one export specifier names, the nearest scope's. */
|
|
322
|
+
function reexportedState(
|
|
323
|
+
specifier: ExportSpecifier,
|
|
324
|
+
visited: Map<Node, InstanceState>,
|
|
325
|
+
): InstanceState {
|
|
326
|
+
const name = specifier.propertyName ?? specifier.name;
|
|
327
|
+
if (!isIdentifier(name)) {
|
|
328
|
+
return HOLDS_VALUES;
|
|
238
329
|
}
|
|
239
|
-
|
|
330
|
+
const names = (statement: Node): boolean => {
|
|
331
|
+
if (isVariableStatement(statement)) {
|
|
332
|
+
return statement.declarationList.declarations.some(names);
|
|
333
|
+
}
|
|
334
|
+
const declared = (statement as { readonly name?: Node }).name;
|
|
335
|
+
return declared !== undefined && isIdentifier(declared) && declared.text === name.text;
|
|
336
|
+
};
|
|
337
|
+
for (let at: Node = specifier.parent; ; at = at.parent) {
|
|
338
|
+
if (isBlock(at) || isModuleBlock(at) || isSourceFile(at)) {
|
|
339
|
+
let found: InstanceState | undefined;
|
|
340
|
+
for (const statement of at.statements.filter(names)) {
|
|
341
|
+
if (isImportEqualsDeclaration(statement)) {
|
|
342
|
+
return HOLDS_VALUES;
|
|
343
|
+
}
|
|
344
|
+
found = Math.max(
|
|
345
|
+
found ?? HOLDS_NOTHING,
|
|
346
|
+
instanceState(statement, visited),
|
|
347
|
+
) as InstanceState;
|
|
348
|
+
if (found === HOLDS_VALUES) {
|
|
349
|
+
return found;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (found !== undefined) {
|
|
353
|
+
return found;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (isSourceFile(at)) {
|
|
357
|
+
return HOLDS_VALUES;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Whether the compiler counts one statement as code that runs (`isPotentiallyExecutableNode`):
|
|
364
|
+
* any statement but a block or an empty one, a `var` only where it initializes a name, and a
|
|
365
|
+
* class, an enum or a namespace.
|
|
366
|
+
*/
|
|
367
|
+
function executable(node: Node): boolean {
|
|
368
|
+
if (isVariableStatement(node)) {
|
|
369
|
+
const list = node.declarationList;
|
|
240
370
|
return (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
abrupt(statement.elseStatement)
|
|
371
|
+
(list.flags & NodeFlags.BlockScoped) !== 0 ||
|
|
372
|
+
list.declarations.some((one) => one.initializer !== undefined)
|
|
244
373
|
);
|
|
245
374
|
}
|
|
246
|
-
return
|
|
375
|
+
return (
|
|
376
|
+
(node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement) ||
|
|
377
|
+
isClassDeclaration(node) ||
|
|
378
|
+
isEnumDeclaration(node) ||
|
|
379
|
+
isModuleDeclaration(node)
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Whether an unreached statement is a finding, by the checker's exclusions
|
|
385
|
+
* (`isSourceElementUnreachable`): a const enum only while const enums are preserved, and a
|
|
386
|
+
* namespace only where its own declaration holds a value, or preserved const enums.
|
|
387
|
+
*/
|
|
388
|
+
function reportable(statement: Statement, constEnumsPreserved: boolean): boolean {
|
|
389
|
+
if (isEnumDeclaration(statement)) {
|
|
390
|
+
return constEnumsPreserved || !hasModifier(statement, SyntaxKind.ConstKeyword);
|
|
391
|
+
}
|
|
392
|
+
if (!isModuleDeclaration(statement)) {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
const state = instanceState(statement);
|
|
396
|
+
return state === HOLDS_VALUES || (constEnumsPreserved && state === HOLDS_CONST_ENUMS);
|
|
247
397
|
}
|
|
248
398
|
|
|
249
399
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
400
|
+
* The bodies one statement runs as part of its own flow, as the binder does
|
|
401
|
+
* (`isImmediatelyInvoked`): a non-async, non-generator function expression or arrow that the
|
|
402
|
+
* statement's whole expression calls, or each static block of a class declaration, in order.
|
|
403
|
+
* Every other function-like body starts a flow of its own.
|
|
252
404
|
*/
|
|
253
|
-
function
|
|
405
|
+
function runsInline(statement: Node): readonly Block[] {
|
|
406
|
+
if (isClassDeclaration(statement)) {
|
|
407
|
+
return statement.members.filter(isClassStaticBlockDeclaration).map((one) => one.body);
|
|
408
|
+
}
|
|
409
|
+
const call = isExpressionStatement(statement)
|
|
410
|
+
? parenthesesRemoved(statement.expression)
|
|
411
|
+
: undefined;
|
|
412
|
+
const callee =
|
|
413
|
+
call !== undefined && isCallExpression(call) ? parenthesesRemoved(call.expression) : undefined;
|
|
414
|
+
return callee !== undefined &&
|
|
415
|
+
(isFunctionExpression(callee) || isArrowFunction(callee)) &&
|
|
416
|
+
isBlock(callee.body) &&
|
|
417
|
+
!hasModifier(callee, SyntaxKind.AsyncKeyword) &&
|
|
418
|
+
(callee as { readonly asteriskToken?: Node }).asteriskToken === undefined
|
|
419
|
+
? [callee.body]
|
|
420
|
+
: [];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Whether a callee is a dotted name (`isDottedName`), whose call statement the binder's flow records. */
|
|
424
|
+
function dottedName(node: Node): boolean {
|
|
425
|
+
if (isPropertyAccessExpression(node) || isParenthesizedExpression(node)) {
|
|
426
|
+
return dottedName(node.expression);
|
|
427
|
+
}
|
|
428
|
+
return (
|
|
429
|
+
isIdentifier(node) ||
|
|
430
|
+
node.kind === SyntaxKind.ThisKeyword ||
|
|
431
|
+
node.kind === SyntaxKind.SuperKeyword ||
|
|
432
|
+
node.kind === SyntaxKind.MetaProperty
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Whether an argument is the checker's constant `false` (`isFalseExpression`). */
|
|
437
|
+
function falseExpression(node: Node): boolean {
|
|
438
|
+
const bare = parenthesesRemoved(node);
|
|
439
|
+
if (bare.kind === SyntaxKind.FalseKeyword) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
if (!isBinaryExpression(bare)) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const operator = bare.operatorToken.kind;
|
|
446
|
+
return operator === SyntaxKind.AmpersandAmpersandToken
|
|
447
|
+
? falseExpression(bare.left) || falseExpression(bare.right)
|
|
448
|
+
: operator === SyntaxKind.BarBarToken &&
|
|
449
|
+
falseExpression(bare.left) &&
|
|
450
|
+
falseExpression(bare.right);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Whether a node is a member of a class body (`IsClassElement`). */
|
|
454
|
+
function classElement(node: Node): boolean {
|
|
455
|
+
return CLASS_ELEMENTS.has(node.kind);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const CLASS_ELEMENTS: ReadonlySet<SyntaxKind> = new Set([
|
|
459
|
+
SyntaxKind.Constructor,
|
|
460
|
+
SyntaxKind.PropertyDeclaration,
|
|
461
|
+
SyntaxKind.MethodDeclaration,
|
|
462
|
+
SyntaxKind.GetAccessor,
|
|
463
|
+
SyntaxKind.SetAccessor,
|
|
464
|
+
SyntaxKind.IndexSignature,
|
|
465
|
+
SyntaxKind.ClassStaticBlockDeclaration,
|
|
466
|
+
SyntaxKind.SemicolonClassElement,
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
const THIS_CONTAINERS: ReadonlySet<SyntaxKind> = new Set([
|
|
470
|
+
SyntaxKind.FunctionDeclaration,
|
|
471
|
+
SyntaxKind.FunctionExpression,
|
|
472
|
+
SyntaxKind.ModuleDeclaration,
|
|
473
|
+
SyntaxKind.ClassStaticBlockDeclaration,
|
|
474
|
+
SyntaxKind.PropertyDeclaration,
|
|
475
|
+
SyntaxKind.PropertySignature,
|
|
476
|
+
SyntaxKind.MethodDeclaration,
|
|
477
|
+
SyntaxKind.MethodSignature,
|
|
478
|
+
SyntaxKind.Constructor,
|
|
479
|
+
SyntaxKind.GetAccessor,
|
|
480
|
+
SyntaxKind.SetAccessor,
|
|
481
|
+
SyntaxKind.CallSignature,
|
|
482
|
+
SyntaxKind.ConstructSignature,
|
|
483
|
+
SyntaxKind.IndexSignature,
|
|
484
|
+
SyntaxKind.EnumDeclaration,
|
|
485
|
+
SyntaxKind.SourceFile,
|
|
486
|
+
]);
|
|
487
|
+
|
|
488
|
+
/** The node that binds a `this`, arrows passed through (`GetThisContainer`). */
|
|
489
|
+
function thisContainer(node: Node): Node | undefined {
|
|
490
|
+
let at = node.parent as Node | undefined;
|
|
491
|
+
while (at !== undefined) {
|
|
492
|
+
if (THIS_CONTAINERS.has(at.kind)) {
|
|
493
|
+
return at;
|
|
494
|
+
}
|
|
495
|
+
if (at.kind === SyntaxKind.ComputedPropertyName) {
|
|
496
|
+
at = at.parent.parent;
|
|
497
|
+
} else if (at.kind === SyntaxKind.Decorator) {
|
|
498
|
+
if (at.parent.kind === SyntaxKind.Parameter && classElement(at.parent.parent)) {
|
|
499
|
+
at = at.parent.parent;
|
|
500
|
+
} else if (classElement(at.parent)) {
|
|
501
|
+
at = at.parent;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
at = at.parent;
|
|
505
|
+
}
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** A signature's type predicate, as the checker reads it. */
|
|
510
|
+
interface Predicate {
|
|
511
|
+
readonly asserts: boolean;
|
|
512
|
+
/** Whether it names a type (`asserts value is T`), not the bare `asserts value`. */
|
|
513
|
+
readonly typed: boolean;
|
|
514
|
+
/** The parameter it names, counted without a `this` parameter, or -1. */
|
|
515
|
+
readonly index: number;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** A predicate this port cannot read, whose signature then ends no call. */
|
|
519
|
+
const UNKNOWN = Symbol("unknown");
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The call statements of a project's files that never return, by the checker's rule ported from
|
|
523
|
+
* the pinned typescript-go (microsoft/TypeScript 042e0fb, tsc/internal/checker/flow.go): the call
|
|
524
|
+
* arm of isReachableFlowNodeWorker (2554), getEffectsSignature (2059), getTypeOfDottedName (2134),
|
|
525
|
+
* getExplicitTypeOfSymbol (2167), hasTypePredicateOrNeverReturnType (2223), getExplicitThisType
|
|
526
|
+
* (2227); relater.go getTypePredicateOfSignature (2052). What the API hides or leaves unanswered
|
|
527
|
+
* (a mapped member's origin, a union signature's predicate) ends nothing.
|
|
528
|
+
*/
|
|
529
|
+
function neverReturning<Brand>(
|
|
530
|
+
project: ProjectView<Brand>,
|
|
531
|
+
files: readonly SourceFile[],
|
|
532
|
+
): ReadonlySet<Node> {
|
|
533
|
+
const { queries } = project;
|
|
534
|
+
const statements: (ExpressionStatement & { readonly expression: CallExpression })[] = [];
|
|
535
|
+
const visit = (node: Node): void => {
|
|
536
|
+
if (
|
|
537
|
+
isExpressionStatement(node) &&
|
|
538
|
+
isCallExpression(node.expression) &&
|
|
539
|
+
node.expression.expression.kind !== SyntaxKind.SuperKeyword &&
|
|
540
|
+
dottedName(node.expression.expression)
|
|
541
|
+
) {
|
|
542
|
+
statements.push(node as ExpressionStatement & { readonly expression: CallExpression });
|
|
543
|
+
}
|
|
544
|
+
node.forEachChild(visit);
|
|
545
|
+
};
|
|
546
|
+
files.forEach(visit);
|
|
547
|
+
const known = <T>(answer: Answer<T | undefined> | undefined): T | undefined =>
|
|
548
|
+
answer === undefined || !isAnswered(answer) ? undefined : answer;
|
|
549
|
+
const nodeOf = (handle: NodeHandle | undefined): Node | undefined =>
|
|
550
|
+
handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
551
|
+
const symbolAt = (node: Node): TSSymbol | undefined =>
|
|
552
|
+
known(project.symbolsAt([project.handle(node)])[0]);
|
|
553
|
+
const never = (type: Type | undefined): boolean =>
|
|
554
|
+
type !== undefined && (type.flags & TypeFlags.Never) !== 0;
|
|
555
|
+
|
|
556
|
+
// resolvingExplicitTypeOfSymbol (flow.go:2169): the for-of arm recurses through the iterated
|
|
557
|
+
// name, so loops whose iterables name each other's variables would recurse forever. A call
|
|
558
|
+
// through such a variable still reaches this walk where narrowing types it `never`. A symbol
|
|
559
|
+
// met again on the walk has no explicit type, so its call ends nothing.
|
|
560
|
+
const resolving = new Set<number>();
|
|
561
|
+
const explicitTypeOf = (given: TSSymbol): Type | undefined => {
|
|
562
|
+
// IsNonLocalAlias: an alias with no meaning of its own, or one an assignment declares.
|
|
563
|
+
const aliased =
|
|
564
|
+
(given.flags & SymbolFlags.Alias) !== 0 &&
|
|
565
|
+
((given.flags & (SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace)) === 0 ||
|
|
566
|
+
(given.flags & SymbolFlags.Assignment) !== 0);
|
|
567
|
+
const symbol = aliased ? known(queries.aliased(given)) : given;
|
|
568
|
+
if (symbol === undefined || resolving.has(symbol.id)) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
resolving.add(symbol.id);
|
|
572
|
+
try {
|
|
573
|
+
if (
|
|
574
|
+
(symbol.flags &
|
|
575
|
+
(SymbolFlags.Function |
|
|
576
|
+
SymbolFlags.Method |
|
|
577
|
+
SymbolFlags.Class |
|
|
578
|
+
SymbolFlags.ValueModule)) !==
|
|
579
|
+
0
|
|
580
|
+
) {
|
|
581
|
+
return known(queries.typeOfSymbol(symbol));
|
|
582
|
+
}
|
|
583
|
+
if ((symbol.flags & (SymbolFlags.Variable | SymbolFlags.Property)) === 0) {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
// A mapped member declares no value of its own, and the API does not expose its origin.
|
|
587
|
+
const declaration = nodeOf(symbol.valueDeclaration);
|
|
588
|
+
if (declaration === undefined) {
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
if (explicitlyTyped(declaration)) {
|
|
592
|
+
return known(queries.typeOfSymbol(symbol));
|
|
593
|
+
}
|
|
594
|
+
if (isVariableDeclaration(declaration) && isForOfStatement(declaration.parent.parent)) {
|
|
595
|
+
const { expression } = declaration.parent.parent;
|
|
596
|
+
const iterated = dottedTypeOf(expression);
|
|
597
|
+
const held = iterated === undefined ? undefined : known(queries.typeAt(expression));
|
|
598
|
+
// The variable's own type iterates the expression's type there, which is the checker's
|
|
599
|
+
// iteration of the dotted name's type where the two are one type.
|
|
600
|
+
return held !== undefined && held.id === iterated?.id
|
|
601
|
+
? known(queries.typeOfSymbol(symbol))
|
|
602
|
+
: undefined;
|
|
603
|
+
}
|
|
604
|
+
return undefined;
|
|
605
|
+
} finally {
|
|
606
|
+
resolving.delete(symbol.id);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const propertyOf = (type: Type, name: Node): TSSymbol | undefined => {
|
|
611
|
+
const properties = known(queries.propertiesOf(type));
|
|
612
|
+
if (properties === undefined) {
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
if (isIdentifier(name)) {
|
|
616
|
+
return properties.find((one) => one.name === name.text);
|
|
617
|
+
}
|
|
618
|
+
if (!isPrivateIdentifier(name)) {
|
|
619
|
+
return undefined;
|
|
620
|
+
}
|
|
621
|
+
// getTypeOfDottedName (flow.go:2149) keys a private name by the receiver type's own symbol,
|
|
622
|
+
// so only that class's member answers, and a type with no symbol has none. The lexical
|
|
623
|
+
// member's key is that key exactly where its class is the receiver type's symbol.
|
|
624
|
+
const owner = known(queries.symbolOfType(type));
|
|
625
|
+
const declared = owner === undefined ? undefined : symbolAt(name);
|
|
626
|
+
if (owner === undefined || declared === undefined) {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
return known(queries.parentOf(declared))?.id === owner.id
|
|
630
|
+
? properties.find((one) => one.name === declared.name)
|
|
631
|
+
: undefined;
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// getSignatureFromDeclaration's `this` parameter (checker.go:20510, 20529): the container's
|
|
635
|
+
// own or, for an accessor without one, its pair's, which getAccessorThisParameter takes only
|
|
636
|
+
// from a getter with one parameter or a setter with two.
|
|
637
|
+
const thisParameterOf = (
|
|
638
|
+
container: FunctionLikeDeclaration,
|
|
639
|
+
): ParameterDeclaration | undefined => {
|
|
640
|
+
const [own] = container.parameters;
|
|
641
|
+
if (own !== undefined && symbolName(own) === "this") {
|
|
642
|
+
return own;
|
|
643
|
+
}
|
|
644
|
+
const getter = isGetAccessorDeclaration(container);
|
|
645
|
+
if (!getter && !isSetAccessorDeclaration(container)) {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
const other = getter ? isSetAccessorDeclaration : isGetAccessorDeclaration;
|
|
649
|
+
const pair = symbolAt(container.name)
|
|
650
|
+
?.declarations.map(nodeOf)
|
|
651
|
+
.find((one): one is AccessorDeclaration => one !== undefined && other(one));
|
|
652
|
+
const first = pair?.parameters[0];
|
|
653
|
+
return first !== undefined &&
|
|
654
|
+
pair?.parameters.length === (getter ? 2 : 1) &&
|
|
655
|
+
symbolName(first) === "this"
|
|
656
|
+
? first
|
|
657
|
+
: undefined;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const explicitThisTypeOf = (node: Node): Type | undefined => {
|
|
661
|
+
const container = thisContainer(node);
|
|
662
|
+
if (container === undefined) {
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
if (isFunctionLikeDeclaration(container)) {
|
|
666
|
+
const signature = known(queries.signatureOf(container));
|
|
667
|
+
if (signature === undefined) {
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
if (signature.thisParameter !== undefined) {
|
|
671
|
+
const declaration = thisParameterOf(container);
|
|
672
|
+
const parameter = declaration === undefined ? undefined : symbolAt(declaration.name);
|
|
673
|
+
return parameter === undefined ? undefined : explicitTypeOf(parameter);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const owner = container.parent as Node | undefined;
|
|
677
|
+
const name =
|
|
678
|
+
owner !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
|
|
679
|
+
? owner.name
|
|
680
|
+
: undefined;
|
|
681
|
+
const symbol = name === undefined ? undefined : symbolAt(name);
|
|
682
|
+
if (symbol === undefined) {
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
return isClassStaticBlockDeclaration(container) ||
|
|
686
|
+
(classElement(container) && hasModifier(container, SyntaxKind.StaticKeyword))
|
|
687
|
+
? known(queries.typeOfSymbol(symbol))
|
|
688
|
+
: known(queries.declaredTypeOf(symbol));
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const dottedTypeOf = (node: Node): Type | undefined => {
|
|
692
|
+
if ((node.flags & NodeFlags.InWithStatement) !== 0) {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
if (isIdentifier(node)) {
|
|
696
|
+
// A name's value meaning finds a module member's export symbol, which is
|
|
697
|
+
// getExportSymbolOfValueSymbolIfExported over its resolved symbol.
|
|
698
|
+
const symbol = symbolAt(node);
|
|
699
|
+
return symbol === undefined ? undefined : explicitTypeOf(symbol);
|
|
700
|
+
}
|
|
701
|
+
if (node.kind === SyntaxKind.ThisKeyword) {
|
|
702
|
+
return explicitThisTypeOf(node);
|
|
703
|
+
}
|
|
704
|
+
if (node.kind === SyntaxKind.SuperKeyword) {
|
|
705
|
+
return known(queries.typeAt(node));
|
|
706
|
+
}
|
|
707
|
+
if (isPropertyAccessExpression(node)) {
|
|
708
|
+
const type = dottedTypeOf(node.expression);
|
|
709
|
+
const property = type === undefined ? undefined : propertyOf(type, node.name);
|
|
710
|
+
return property === undefined ? undefined : explicitTypeOf(property);
|
|
711
|
+
}
|
|
712
|
+
return isParenthesizedExpression(node) ? dottedTypeOf(node.expression) : undefined;
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const predicateOf = (
|
|
716
|
+
signature: Signature,
|
|
717
|
+
declaration: Node,
|
|
718
|
+
): Predicate | undefined | typeof UNKNOWN => {
|
|
719
|
+
const declared = known(queries.signatureOf(declaration));
|
|
720
|
+
let at: Signature | undefined = signature;
|
|
721
|
+
for (let step = 0; at !== undefined && at.id !== declared?.id; step++) {
|
|
722
|
+
at = step < SIGNATURE_TARGET_DEPTH ? known(queries.genericOf(at)) : undefined;
|
|
723
|
+
}
|
|
724
|
+
// A union's signature carries its first member's declaration and its own predicate.
|
|
725
|
+
if (at === undefined) {
|
|
726
|
+
return UNKNOWN;
|
|
727
|
+
}
|
|
728
|
+
const type = (declaration as { readonly type?: Node }).type;
|
|
729
|
+
if (type === undefined || !isTypePredicateNode(type)) {
|
|
730
|
+
return undefined;
|
|
731
|
+
}
|
|
732
|
+
const name = type.parameterName;
|
|
733
|
+
const parameters = (declaration as { readonly parameters?: readonly Node[] }).parameters ?? [];
|
|
734
|
+
const named = parameters.filter((one, index) => !(index === 0 && symbolName(one) === "this"));
|
|
735
|
+
return {
|
|
736
|
+
asserts: type.assertsModifier !== undefined,
|
|
737
|
+
typed: type.type !== undefined,
|
|
738
|
+
index:
|
|
739
|
+
name.kind === SyntaxKind.ThisType
|
|
740
|
+
? -1
|
|
741
|
+
: named.findIndex((one) => isIdentifier(name) && symbolName(one) === name.text),
|
|
742
|
+
};
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// getReturnTypeFromAnnotation (checker.go:20705). A constructor's result is its instance type,
|
|
746
|
+
// which is not never, and a declaration with no type node writes no never result: a JavaScript
|
|
747
|
+
// type tag's signature is declared by the tag's own function type, which carries the annotation.
|
|
748
|
+
const annotatedNever = (declaration: Node): boolean => {
|
|
749
|
+
if ((declaration as { readonly type?: Node }).type === undefined) {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
const declared = known(queries.signatureOf(declaration));
|
|
753
|
+
return declared !== undefined && never(known(queries.returnTypeOf(declared)));
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// Whether a signature has a predicate or writes `never` as its result, where the API tells.
|
|
757
|
+
const marked = (signature: Signature): boolean => {
|
|
758
|
+
const declaration = nodeOf(signature.declaration);
|
|
759
|
+
if (declaration === undefined) {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
const predicate = predicateOf(signature, declaration);
|
|
763
|
+
return predicate !== UNKNOWN && (predicate !== undefined || annotatedNever(declaration));
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const effectsOf = (call: CallExpression): Signature | undefined => {
|
|
767
|
+
const type = dottedTypeOf(call.expression);
|
|
768
|
+
const signatures =
|
|
769
|
+
type === undefined ? undefined : known(queries.signaturesOf(type, SignatureKind.Call));
|
|
770
|
+
if (signatures === undefined) {
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
const [only] = signatures;
|
|
774
|
+
const chosen =
|
|
775
|
+
signatures.length === 1 && only !== undefined && (only.typeParameters?.length ?? 0) === 0
|
|
776
|
+
? only
|
|
777
|
+
: signatures.some(marked)
|
|
778
|
+
? known(queries.resolvedSignature(call))
|
|
779
|
+
: undefined;
|
|
780
|
+
return chosen !== undefined && marked(chosen) ? chosen : undefined;
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
// The effects signature's result is the call's own type, so a call typed neither `never` nor
|
|
784
|
+
// `void` with a false argument cannot end, and is never asked about.
|
|
785
|
+
const types =
|
|
786
|
+
statements.length === 0 ? [] : queries.typesAt(statements.map((one) => one.expression));
|
|
787
|
+
const ends = new Set<Node>();
|
|
788
|
+
statements.forEach((statement, at) => {
|
|
789
|
+
const type = known(types[at]);
|
|
790
|
+
const call = statement.expression;
|
|
791
|
+
if (
|
|
792
|
+
type === undefined ||
|
|
793
|
+
(!never(type) &&
|
|
794
|
+
((type.flags & TypeFlags.Void) === 0 || !call.arguments.some(falseExpression)))
|
|
795
|
+
) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const effects = effectsOf(call);
|
|
799
|
+
const declaration = effects === undefined ? undefined : nodeOf(effects.declaration);
|
|
800
|
+
if (effects === undefined || declaration === undefined) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
// The checker ends a bare `asserts value` call whose argument there is false.
|
|
804
|
+
const predicate = predicateOf(effects, declaration);
|
|
805
|
+
const asserted =
|
|
806
|
+
predicate !== UNKNOWN && predicate?.asserts === true && !predicate.typed
|
|
807
|
+
? call.arguments[predicate.index]
|
|
808
|
+
: undefined;
|
|
809
|
+
if (
|
|
810
|
+
(asserted !== undefined && falseExpression(asserted)) ||
|
|
811
|
+
never(known(queries.returnTypeOf(effects)))
|
|
812
|
+
) {
|
|
813
|
+
ends.add(statement);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
return ends;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Whether a declaration writes its own type (`isDeclarationWithExplicitTypeAnnotation`, flow.go:2209). */
|
|
820
|
+
function explicitlyTyped(declaration: Node): boolean {
|
|
254
821
|
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
isEmptyStatement(statement)
|
|
822
|
+
isVariableDeclaration(declaration) ||
|
|
823
|
+
isPropertyDeclaration(declaration) ||
|
|
824
|
+
isPropertySignatureDeclaration(declaration) ||
|
|
825
|
+
isParameterDeclaration(declaration)
|
|
260
826
|
) {
|
|
261
|
-
return
|
|
827
|
+
return declaration.type !== undefined;
|
|
262
828
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
829
|
+
// An expando property is typed by the function it is assigned, where that writes its result.
|
|
830
|
+
return (
|
|
831
|
+
isBinaryExpression(declaration) &&
|
|
832
|
+
(isFunctionExpression(declaration.right) || isArrowFunction(declaration.right)) &&
|
|
833
|
+
declaration.right.type !== undefined
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** The name a declaration's symbol carries, for an identifier-named one. */
|
|
838
|
+
function symbolName(declaration: Node): string | undefined {
|
|
839
|
+
const name = (declaration as { readonly name?: Node }).name;
|
|
840
|
+
return name !== undefined && isIdentifier(name) ? name.text : undefined;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** How many instantiations deep a signature's generic is followed to its declaration's own. */
|
|
844
|
+
const SIGNATURE_TARGET_DEPTH = 8;
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* The statements of one file the compiler counts as code that runs and that no path from the
|
|
848
|
+
* start of their own flow reaches: the file's, a namespace body's, or a function-like body's
|
|
849
|
+
* that does not run inline. The walk is `flowWalk`'s with each statement a target of its own,
|
|
850
|
+
* and a construct it does not follow leaves every statement inside it reached.
|
|
851
|
+
*/
|
|
852
|
+
function unreachedOf(file: SourceFile, ends: ReadonlySet<Node>): ReadonlySet<Node> {
|
|
853
|
+
const flows: { readonly statements: readonly Statement[]; readonly members: Statement[] }[] = [];
|
|
854
|
+
const inline = new Set<Node>();
|
|
855
|
+
const collect = (node: Node, members: Statement[]): void => {
|
|
856
|
+
for (const body of runsInline(node)) {
|
|
857
|
+
inline.add(body);
|
|
858
|
+
}
|
|
859
|
+
node.forEachChild((child) => {
|
|
860
|
+
const own = isModuleBlock(child)
|
|
861
|
+
? child.statements
|
|
862
|
+
: isBlock(child) &&
|
|
863
|
+
!inline.has(child) &&
|
|
864
|
+
(isFunctionLikeDeclaration(node) || isClassStaticBlockDeclaration(node))
|
|
865
|
+
? child.statements
|
|
866
|
+
: undefined;
|
|
867
|
+
if (own !== undefined) {
|
|
868
|
+
const inner: Statement[] = [];
|
|
869
|
+
flows.push({ statements: own, members: inner });
|
|
870
|
+
collect(child, inner);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (executable(child)) {
|
|
874
|
+
members.push(child as Statement);
|
|
875
|
+
}
|
|
876
|
+
collect(child, members);
|
|
877
|
+
});
|
|
878
|
+
};
|
|
879
|
+
const top: Statement[] = [];
|
|
880
|
+
flows.push({ statements: file.statements, members: top });
|
|
881
|
+
collect(file, top);
|
|
882
|
+
const unreached = new Set<Node>();
|
|
883
|
+
for (const { statements, members } of flows) {
|
|
884
|
+
// Members are in source order, so those inside one node are one run of bits.
|
|
885
|
+
const first = (pos: number): number => {
|
|
886
|
+
let low = 0;
|
|
887
|
+
for (let high = members.length; low < high;) {
|
|
888
|
+
const middle = (low + high) >> 1;
|
|
889
|
+
if ((members[middle]?.pos ?? pos) < pos) {
|
|
890
|
+
low = middle + 1;
|
|
891
|
+
} else {
|
|
892
|
+
high = middle;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return low;
|
|
896
|
+
};
|
|
897
|
+
const inside = (node: Node | undefined): Reached => {
|
|
898
|
+
if (node === undefined) {
|
|
899
|
+
return NOTHING;
|
|
900
|
+
}
|
|
901
|
+
let low = first(node.pos);
|
|
902
|
+
const high = first(node.end);
|
|
903
|
+
while (low < high && (members[low]?.end ?? 0) > node.end) {
|
|
904
|
+
low += 1;
|
|
905
|
+
}
|
|
906
|
+
return low >= high ? NOTHING : ((1n << BigInt(high - low)) - 1n) << BigInt(low);
|
|
907
|
+
};
|
|
908
|
+
const indexes = new Map(members.map((one, at) => [one, at]));
|
|
909
|
+
const walk = flowWalk({
|
|
910
|
+
inside,
|
|
911
|
+
at: (statement) => {
|
|
912
|
+
const index = indexes.get(statement);
|
|
913
|
+
return index === undefined ? NOTHING : 1n << BigInt(index);
|
|
914
|
+
},
|
|
915
|
+
writes: [],
|
|
916
|
+
ends,
|
|
917
|
+
});
|
|
918
|
+
const reached = walk.sequence(statements, NOTHING, {
|
|
919
|
+
exit: NOTHING,
|
|
920
|
+
raise: NOTHING,
|
|
921
|
+
escape: NOTHING,
|
|
922
|
+
labels: new Map(),
|
|
923
|
+
});
|
|
924
|
+
const digits = reached.toString(2);
|
|
925
|
+
members.forEach((member, at) => {
|
|
926
|
+
if (digits[digits.length - 1 - at] !== "1") {
|
|
927
|
+
unreached.add(member);
|
|
928
|
+
}
|
|
929
|
+
});
|
|
269
930
|
}
|
|
270
|
-
return
|
|
931
|
+
return unreached;
|
|
271
932
|
}
|
|
272
933
|
|
|
273
934
|
/** The value a case expression is a literal of, as a key two equal literals share, or none. */
|
|
@@ -387,38 +1048,85 @@ function hasModifier(node: Node, kind: SyntaxKind): boolean {
|
|
|
387
1048
|
);
|
|
388
1049
|
}
|
|
389
1050
|
|
|
390
|
-
/**
|
|
391
|
-
|
|
392
|
-
|
|
1051
|
+
/** The types the project's `extends` and `implements` clauses name. */
|
|
1052
|
+
interface Heritage {
|
|
1053
|
+
/** The symbol each clause resolves to, an import alias followed to what it names. */
|
|
1054
|
+
readonly ids: ReadonlySet<number>;
|
|
1055
|
+
/** Every name a clause spells, read where the checker leaves a class's own symbol unanswered. */
|
|
1056
|
+
readonly names: ReadonlySet<string>;
|
|
1057
|
+
/**
|
|
1058
|
+
* The names of the clauses the checker left unanswered, which may name any class. A clause
|
|
1059
|
+
* it resolved to nothing names no class of the program.
|
|
1060
|
+
*/
|
|
1061
|
+
readonly unanswered: ReadonlySet<string>;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** How many alias links a heritage name is followed through, one per re-export. */
|
|
1065
|
+
const ALIAS_DEPTH = 16;
|
|
1066
|
+
|
|
1067
|
+
/** The last identifier of one heritage type: a class's clause holds expressions, an interface's may hold type references. */
|
|
1068
|
+
function heritageName(type: Node): Identifier | undefined {
|
|
1069
|
+
if (isTypeReferenceNode(type)) {
|
|
1070
|
+
const named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
|
|
1071
|
+
return isIdentifier(named) ? named : undefined;
|
|
1072
|
+
}
|
|
1073
|
+
return isExpressionWithTypeArguments(type) ? spelledName(type.expression) : undefined;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** What every heritage clause of the project's files names, asked of the checker in one batch. */
|
|
1077
|
+
function heritageOf<Brand>(project: ProjectView<Brand>, files: readonly SourceFile[]): Heritage {
|
|
1078
|
+
const named: Identifier[] = [];
|
|
393
1079
|
const visit = (node: Node): void => {
|
|
394
|
-
if (isHeritageClause(node)
|
|
395
|
-
|
|
396
|
-
// A class's clause holds expressions; an interface's holds type references.
|
|
397
|
-
let named: Node;
|
|
398
|
-
if (isTypeReferenceNode(type)) {
|
|
399
|
-
named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
|
|
400
|
-
} else {
|
|
401
|
-
const expression = parenthesesRemoved(type.expression);
|
|
402
|
-
named = isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
403
|
-
}
|
|
404
|
-
if (isIdentifier(named)) {
|
|
405
|
-
names.add(named.text);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
1080
|
+
if (isHeritageClause(node)) {
|
|
1081
|
+
named.push(...node.types.flatMap((type) => heritageName(type) ?? []));
|
|
408
1082
|
}
|
|
409
1083
|
node.forEachChild(visit);
|
|
410
1084
|
};
|
|
411
1085
|
for (const file of files) {
|
|
412
1086
|
file.forEachChild(visit);
|
|
413
1087
|
}
|
|
414
|
-
|
|
1088
|
+
const ids = new Set<number>();
|
|
1089
|
+
const unanswered = new Set<string>();
|
|
1090
|
+
project.symbolsAt(named.map((name) => project.handle(name))).forEach((answer, at) => {
|
|
1091
|
+
let symbol: Answer<TSSymbol | undefined> = answer;
|
|
1092
|
+
for (
|
|
1093
|
+
let step = 0;
|
|
1094
|
+
step < ALIAS_DEPTH && symbol !== undefined && symbol !== UNANSWERED;
|
|
1095
|
+
step++
|
|
1096
|
+
) {
|
|
1097
|
+
const next = project.aliasStepOf(symbol);
|
|
1098
|
+
if (next === undefined) {
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
symbol = next;
|
|
1102
|
+
}
|
|
1103
|
+
if (symbol === UNANSWERED) {
|
|
1104
|
+
unanswered.add(named[at]?.text ?? "");
|
|
1105
|
+
} else if (symbol !== undefined) {
|
|
1106
|
+
ids.add(symbol.id);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
return { ids, names: new Set(named.map((name) => name.text)), unanswered };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** The names a class is known by: its own, and the variable a class expression initializes. */
|
|
1113
|
+
function classNames(owner: Node): readonly Identifier[] {
|
|
1114
|
+
const own = (owner as { readonly name?: Node }).name;
|
|
1115
|
+
const holder = isClassExpression(owner) ? outerParent(owner) : undefined;
|
|
1116
|
+
const held = holder !== undefined && isVariableDeclaration(holder) ? holder.name : undefined;
|
|
1117
|
+
return [own, held].filter((name): name is Identifier => name !== undefined && isIdentifier(name));
|
|
415
1118
|
}
|
|
416
1119
|
|
|
417
1120
|
/**
|
|
418
1121
|
* Whether a class hierarchy fixes one method's signature: its class extends or implements
|
|
419
|
-
* another type, is abstract or is
|
|
1122
|
+
* another type, is abstract, or is a type a clause of the project extends or implements,
|
|
1123
|
+
* or the method is marked `override`.
|
|
420
1124
|
*/
|
|
421
|
-
function hierarchyFixes(
|
|
1125
|
+
function hierarchyFixes(
|
|
1126
|
+
declared: Node,
|
|
1127
|
+
heritage: Heritage,
|
|
1128
|
+
ids: ReadonlyMap<Node, Answer<number>>,
|
|
1129
|
+
): boolean {
|
|
422
1130
|
if (!isMethodDeclaration(declared)) {
|
|
423
1131
|
return false;
|
|
424
1132
|
}
|
|
@@ -428,14 +1136,17 @@ function hierarchyFixes(declared: Node, extended: ReadonlySet<string>): boolean
|
|
|
428
1136
|
}
|
|
429
1137
|
if (
|
|
430
1138
|
hasModifier(declared, SyntaxKind.OverrideKeyword) ||
|
|
431
|
-
hasModifier(owner, SyntaxKind.AbstractKeyword)
|
|
1139
|
+
hasModifier(owner, SyntaxKind.AbstractKeyword) ||
|
|
1140
|
+
(owner.heritageClauses ?? []).length > 0
|
|
432
1141
|
) {
|
|
433
1142
|
return true;
|
|
434
1143
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1144
|
+
return classNames(owner).some((name) => {
|
|
1145
|
+
const id = ids.get(name);
|
|
1146
|
+
return id === undefined || id === UNANSWERED
|
|
1147
|
+
? heritage.names.has(name.text)
|
|
1148
|
+
: heritage.ids.has(id) || heritage.unanswered.has(name.text);
|
|
1149
|
+
});
|
|
439
1150
|
}
|
|
440
1151
|
|
|
441
1152
|
/** Whether a decorator on a constructor's class or on one of its parameters fixes its signature. */
|
|
@@ -581,6 +1292,16 @@ function checkedByTheCompiler(file: SourceFile, checkJs: boolean): boolean {
|
|
|
581
1292
|
);
|
|
582
1293
|
}
|
|
583
1294
|
|
|
1295
|
+
/** Whether one child of a node is a statement it runs in no statement list: a branch or a body. */
|
|
1296
|
+
function inStatementPosition(node: Node, child: Node): boolean {
|
|
1297
|
+
if (isIfStatement(node)) {
|
|
1298
|
+
return child === node.thenStatement || child === node.elseStatement;
|
|
1299
|
+
}
|
|
1300
|
+
// The body of a `do`, a label, a `for…of`, a `for…in` or a `with` runs whenever its
|
|
1301
|
+
// statement does, so only these can start an unreachable run of their own.
|
|
1302
|
+
return (isWhileStatement(node) || isForStatement(node)) && child === node.statement;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
584
1305
|
/** What one file's walk collects before the batch that resolves its names. */
|
|
585
1306
|
interface FileWalk {
|
|
586
1307
|
readonly declared: Declared[];
|
|
@@ -609,7 +1330,7 @@ export function projectParts<Brand>(
|
|
|
609
1330
|
targetRoot: string,
|
|
610
1331
|
): ProjectParts {
|
|
611
1332
|
const files = project.ownSourceFiles();
|
|
612
|
-
const
|
|
1333
|
+
const heritage = heritageOf(project, files);
|
|
613
1334
|
const parts: PartFact[] = [];
|
|
614
1335
|
const signatures: SignatureFact[] = [];
|
|
615
1336
|
const calls = new Map<string, CallUse>();
|
|
@@ -621,8 +1342,16 @@ export function projectParts<Brand>(
|
|
|
621
1342
|
const refs = new Map(held.symbols.map((symbol) => [symbol.id, symbol.ref]));
|
|
622
1343
|
const options = project.program.getCompilerOptions();
|
|
623
1344
|
const jsx = options as JsxOptions;
|
|
1345
|
+
// The compiler's `shouldPreserveConstEnums`, whose isolated modules include `verbatimModuleSyntax`.
|
|
1346
|
+
const constEnumsPreserved =
|
|
1347
|
+
options.preserveConstEnums === true ||
|
|
1348
|
+
options.isolatedModules === true ||
|
|
1349
|
+
options.verbatimModuleSyntax === true;
|
|
1350
|
+
const ends = neverReturning(project, files);
|
|
1351
|
+
const judged = new Set<string>();
|
|
624
1352
|
|
|
625
1353
|
for (const file of files) {
|
|
1354
|
+
judged.add(renderPosition(file, targetRoot, 0).path);
|
|
626
1355
|
const checked = checkedByTheCompiler(file, options.checkJs === true);
|
|
627
1356
|
// A parameter property's own parts stay with the parameter when its member goes.
|
|
628
1357
|
const declarationAt = (node: Node): string | undefined =>
|
|
@@ -651,26 +1380,25 @@ export function projectParts<Brand>(
|
|
|
651
1380
|
const position = (node: Node): string =>
|
|
652
1381
|
positionKey(renderPosition(file, targetRoot, node.getStart()));
|
|
653
1382
|
|
|
654
|
-
// Code
|
|
655
|
-
// and reports no part of its own: the
|
|
1383
|
+
// Code inside a run of unreachable statements is still walked for its calls and
|
|
1384
|
+
// specifiers, and reports no part of its own: the run's first statement is the one
|
|
1385
|
+
// finding, as the compiler reports one diagnostic per run.
|
|
1386
|
+
const unreached = unreachedOf(file, ends);
|
|
1387
|
+
const unreachable = (statement: Statement): boolean =>
|
|
1388
|
+
unreached.has(statement) && reportable(statement, constEnumsPreserved);
|
|
656
1389
|
let silenced = 0;
|
|
657
1390
|
const statementsOf = (statements: readonly Statement[]): void => {
|
|
658
|
-
let
|
|
659
|
-
let reported = false;
|
|
1391
|
+
let inRun = false;
|
|
660
1392
|
for (const statement of statements) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
const holder = holderOf(statement);
|
|
667
|
-
if (silenced === 0 && !reported && !unreachableExempt(statement) && holder !== undefined) {
|
|
1393
|
+
const unreached = unreachable(statement);
|
|
1394
|
+
const holder = unreached && !inRun && silenced === 0 ? holderOf(statement) : undefined;
|
|
1395
|
+
if (holder !== undefined) {
|
|
668
1396
|
walk.unreachable.push({ statement, holder });
|
|
669
|
-
reported = true;
|
|
670
1397
|
}
|
|
671
|
-
|
|
1398
|
+
inRun = unreached;
|
|
1399
|
+
silenced += unreached ? 1 : 0;
|
|
672
1400
|
visit(statement);
|
|
673
|
-
silenced -= 1;
|
|
1401
|
+
silenced -= unreached ? 1 : 0;
|
|
674
1402
|
}
|
|
675
1403
|
};
|
|
676
1404
|
const visit = (node: Node): void => {
|
|
@@ -739,7 +1467,14 @@ export function projectParts<Brand>(
|
|
|
739
1467
|
}
|
|
740
1468
|
}
|
|
741
1469
|
}
|
|
742
|
-
|
|
1470
|
+
// A block whose first statement no flow reaches is never entered: the compiler
|
|
1471
|
+
// reports the run from inside it, and its locals hold no store of their own.
|
|
1472
|
+
if (
|
|
1473
|
+
isBlock(node) &&
|
|
1474
|
+
silenced === 0 &&
|
|
1475
|
+
(node.statements[0] === undefined || !unreachable(node.statements[0])) &&
|
|
1476
|
+
holderOf(node) !== undefined
|
|
1477
|
+
) {
|
|
743
1478
|
walk.blocks.push(node);
|
|
744
1479
|
}
|
|
745
1480
|
const loopHolder =
|
|
@@ -754,13 +1489,20 @@ export function projectParts<Brand>(
|
|
|
754
1489
|
) {
|
|
755
1490
|
walk.loops.push({ loop: node, holder: loopHolder });
|
|
756
1491
|
}
|
|
757
|
-
if (isBlock(node) || isCaseClause(node) || isDefaultClause(node)) {
|
|
1492
|
+
if (isBlock(node) || isCaseClause(node) || isDefaultClause(node) || isModuleBlock(node)) {
|
|
758
1493
|
statementsOf(node.statements);
|
|
759
1494
|
return;
|
|
760
1495
|
}
|
|
761
|
-
|
|
1496
|
+
// A statement that is a branch or a body of another, in no list, is a run of its own.
|
|
1497
|
+
node.forEachChild((child) => {
|
|
1498
|
+
if (inStatementPosition(node, child)) {
|
|
1499
|
+
statementsOf([child as Statement]);
|
|
1500
|
+
} else {
|
|
1501
|
+
visit(child);
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
762
1504
|
};
|
|
763
|
-
file.
|
|
1505
|
+
statementsOf(file.statements);
|
|
764
1506
|
|
|
765
1507
|
// One batch per file resolves every name a parameter or a local is read through.
|
|
766
1508
|
const asked: Identifier[] = [];
|
|
@@ -796,6 +1538,17 @@ export function projectParts<Brand>(
|
|
|
796
1538
|
asked.push(...names, ...uses);
|
|
797
1539
|
return { names, uses, holder };
|
|
798
1540
|
});
|
|
1541
|
+
const measured = lengthReads(file);
|
|
1542
|
+
const measuredNames = new Set(measured.map((one) => one.text));
|
|
1543
|
+
const named = walk.declared.flatMap(({ node }) => {
|
|
1544
|
+
const name = (node as { readonly name?: Node }).name;
|
|
1545
|
+
return name !== undefined && isIdentifier(name) && measuredNames.has(name.text) ? [name] : [];
|
|
1546
|
+
});
|
|
1547
|
+
asked.push(...measured, ...named);
|
|
1548
|
+
const owners = walk.declared.flatMap(({ node }) =>
|
|
1549
|
+
isMethodDeclaration(node) ? classNames(node.parent) : [],
|
|
1550
|
+
);
|
|
1551
|
+
asked.push(...owners);
|
|
799
1552
|
const bindings = fileId === undefined ? [] : importBindings(file);
|
|
800
1553
|
const bound = new Set<Node>(bindings);
|
|
801
1554
|
const bindingUses =
|
|
@@ -833,11 +1586,11 @@ export function projectParts<Brand>(
|
|
|
833
1586
|
signatures.push({
|
|
834
1587
|
declaration: one.id,
|
|
835
1588
|
stub: isStub(one.signature),
|
|
836
|
-
fixed: constructor ? decoratorFixes(one.node) : hierarchyFixes(one.node,
|
|
1589
|
+
fixed: constructor ? decoratorFixes(one.node) : hierarchyFixes(one.node, heritage, ids),
|
|
837
1590
|
constructor,
|
|
838
1591
|
});
|
|
839
1592
|
const reader = readers.get(one.signature);
|
|
840
|
-
if (reader !== undefined) {
|
|
1593
|
+
if (reader !== undefined && !lengthRead(one.node, measured, ids)) {
|
|
841
1594
|
const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, {
|
|
842
1595
|
inValue: false,
|
|
843
1596
|
checked,
|
|
@@ -857,71 +1610,123 @@ export function projectParts<Brand>(
|
|
|
857
1610
|
contextual.push(...found.contextual);
|
|
858
1611
|
}
|
|
859
1612
|
}
|
|
1613
|
+
const part = (declaration: string, kind: PartKind, name: string, node: Node): PartFact => ({
|
|
1614
|
+
declaration,
|
|
1615
|
+
kind,
|
|
1616
|
+
name,
|
|
1617
|
+
position: spanOf(file, targetRoot, node),
|
|
1618
|
+
});
|
|
860
1619
|
walk.catches.forEach(({ name, holder }, at) => {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
kind: "store",
|
|
865
|
-
name: name.text,
|
|
866
|
-
position: spanOf(file, targetRoot, name),
|
|
867
|
-
message: `value written to ${name.text} is never read`,
|
|
868
|
-
used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
|
|
869
|
-
});
|
|
1620
|
+
if (!(caught[at] ?? []).some((use) => readsBinding(use, name, ids))) {
|
|
1621
|
+
parts.push(part(holder, "store", name.text, name));
|
|
1622
|
+
}
|
|
870
1623
|
});
|
|
871
1624
|
for (const { names, uses, holder } of loopVariables) {
|
|
872
1625
|
for (const name of names) {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
kind: "store",
|
|
877
|
-
name: name.text,
|
|
878
|
-
position: spanOf(file, targetRoot, name),
|
|
879
|
-
message: `value written to ${name.text} is never read`,
|
|
880
|
-
used: uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
|
|
881
|
-
});
|
|
1626
|
+
if (!uses.some((use) => use.text === name.text && readsBinding(use, name, ids))) {
|
|
1627
|
+
parts.push(part(holder, "store", name.text, name));
|
|
1628
|
+
}
|
|
882
1629
|
}
|
|
883
1630
|
}
|
|
884
1631
|
walk.blocks.forEach((block, at) => {
|
|
885
1632
|
const holder = holderOf(block);
|
|
886
1633
|
if (holder !== undefined) {
|
|
887
|
-
|
|
1634
|
+
for (const { local, target } of deadStores(block, stores[at] ?? [], ids, ends)) {
|
|
1635
|
+
parts.push(part(holder, "store", local.name.text, target));
|
|
1636
|
+
}
|
|
888
1637
|
}
|
|
889
1638
|
});
|
|
890
1639
|
for (const { statement, holder } of walk.unreachable) {
|
|
891
|
-
parts.push(
|
|
892
|
-
code: CODE.unreachableStatement,
|
|
893
|
-
declaration: holder,
|
|
894
|
-
kind: "statement",
|
|
895
|
-
name: "statement",
|
|
896
|
-
position: spanOf(file, targetRoot, statement),
|
|
897
|
-
message: "statement is unreachable",
|
|
898
|
-
used: false,
|
|
899
|
-
});
|
|
1640
|
+
parts.push(part(holder, "statement", "statement", statement));
|
|
900
1641
|
}
|
|
901
1642
|
for (const { clause, text, holder } of walk.cases) {
|
|
902
|
-
parts.push(
|
|
903
|
-
code: CODE.unreachableCase,
|
|
904
|
-
declaration: holder,
|
|
905
|
-
kind: "case",
|
|
906
|
-
name: text,
|
|
907
|
-
position: spanOf(file, targetRoot, clause),
|
|
908
|
-
message: `case ${text} is covered by an earlier case of the same switch`,
|
|
909
|
-
used: false,
|
|
910
|
-
});
|
|
1643
|
+
parts.push(part(holder, "case", text, clause));
|
|
911
1644
|
}
|
|
912
1645
|
}
|
|
913
1646
|
|
|
914
|
-
|
|
1647
|
+
const results = resultParts(project, targetRoot, sites, references, calls);
|
|
915
1648
|
return {
|
|
916
|
-
|
|
1649
|
+
// Only a result reads whether outside callers can reach its declaration.
|
|
1650
|
+
published: results.parts.length === 0 ? new Set() : importable(project, held, targetRoot),
|
|
1651
|
+
parts: [...withContextualRead(project, parts, contextual), ...results.parts],
|
|
917
1652
|
imports,
|
|
918
1653
|
signatures,
|
|
919
1654
|
calls,
|
|
920
1655
|
specifiers,
|
|
921
1656
|
constructions,
|
|
1657
|
+
files: judged,
|
|
1658
|
+
results: results.judged,
|
|
922
1659
|
};
|
|
923
1660
|
}
|
|
924
1661
|
|
|
1662
|
+
/** Every identifier of one file whose `length` a property access reads. */
|
|
1663
|
+
function lengthReads(file: SourceFile): readonly Identifier[] {
|
|
1664
|
+
const found: Identifier[] = [];
|
|
1665
|
+
const visit = (node: Node): void => {
|
|
1666
|
+
if (isPropertyAccessExpression(node) && node.name.text === "length") {
|
|
1667
|
+
const read = parenthesesRemoved(node.expression);
|
|
1668
|
+
if (isIdentifier(read)) {
|
|
1669
|
+
found.push(read);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
node.forEachChild(visit);
|
|
1673
|
+
};
|
|
1674
|
+
file.forEachChild(visit);
|
|
1675
|
+
return found;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Whether the program reads the `length` of one declared function through its own binding,
|
|
1680
|
+
* which counts its parameters, so none can go. A read the checker leaves unanswered counts.
|
|
1681
|
+
*/
|
|
1682
|
+
function lengthRead(
|
|
1683
|
+
declared: Node,
|
|
1684
|
+
measured: readonly Identifier[],
|
|
1685
|
+
ids: ReadonlyMap<Node, Answer<number>>,
|
|
1686
|
+
): boolean {
|
|
1687
|
+
const name = (declared as { readonly name?: Node }).name;
|
|
1688
|
+
if (name === undefined || !isIdentifier(name)) {
|
|
1689
|
+
return false;
|
|
1690
|
+
}
|
|
1691
|
+
const own = ids.get(name);
|
|
1692
|
+
return measured.some((read) => {
|
|
1693
|
+
if (read.text !== name.text) {
|
|
1694
|
+
return false;
|
|
1695
|
+
}
|
|
1696
|
+
const at = ids.get(read);
|
|
1697
|
+
return (
|
|
1698
|
+
at === undefined || at === UNANSWERED || own === undefined || own === UNANSWERED || at === own
|
|
1699
|
+
);
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Every declaration an outside importer of any of the project's own files can name, as the
|
|
1705
|
+
* published-API reach states it with each file published, and each one it guessed.
|
|
1706
|
+
*/
|
|
1707
|
+
function importable<Brand>(
|
|
1708
|
+
project: ProjectView<Brand>,
|
|
1709
|
+
held: Inventory,
|
|
1710
|
+
targetRoot: string,
|
|
1711
|
+
): ReadonlySet<string> {
|
|
1712
|
+
const files = sourceFilesOf(project, targetRoot);
|
|
1713
|
+
const exported = new Map<string, string[]>();
|
|
1714
|
+
for (const symbol of held.symbols) {
|
|
1715
|
+
if (symbol.exported) {
|
|
1716
|
+
exported.set(symbol.parent, [...(exported.get(symbol.parent) ?? []), symbol.id]);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
const own = [...files.byName.values()];
|
|
1720
|
+
const exportsOf = new Map(
|
|
1721
|
+
own.map((file) => [
|
|
1722
|
+
file.fileName,
|
|
1723
|
+
exported.get(held.declarations.get(nodeKey(file, file)) ?? "") ?? [],
|
|
1724
|
+
]),
|
|
1725
|
+
);
|
|
1726
|
+
const api = publishedAPI(project, own, exportsOf, held, files.byPath, []);
|
|
1727
|
+
return new Set([...api.ids, ...api.guessed]);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
925
1730
|
/**
|
|
926
1731
|
* A function written in place that no declaration declares: an arrow, a function
|
|
927
1732
|
* expression, a nested function declaration, or a method of an object literal.
|
|
@@ -1091,7 +1896,7 @@ function calleeSteers<Brand>(
|
|
|
1091
1896
|
* The parts with each name of `contextual` judged where the compiler gives its function a
|
|
1092
1897
|
* contextual type: a name an object pattern binds is marked as one, and an annotated
|
|
1093
1898
|
* parameter with whether the call's inference may read its annotation. A question the
|
|
1094
|
-
* checker leaves unanswered keeps the name read.
|
|
1899
|
+
* checker leaves unanswered keeps the name read, so its part is dropped.
|
|
1095
1900
|
*/
|
|
1096
1901
|
function withContextualRead<Brand>(
|
|
1097
1902
|
project: ProjectView<Brand>,
|
|
@@ -1104,7 +1909,7 @@ function withContextualRead<Brand>(
|
|
|
1104
1909
|
const signatures = [...new Set(contextual.map((one) => one.signature))];
|
|
1105
1910
|
const answers = project.queries.contextualTypes(signatures);
|
|
1106
1911
|
const typedBy = new Map(signatures.map((signature, at) => [signature, answers[at]]));
|
|
1107
|
-
const judged = new Map<PartFact, PartFact>();
|
|
1912
|
+
const judged = new Map<PartFact, PartFact | undefined>();
|
|
1108
1913
|
for (const one of contextual) {
|
|
1109
1914
|
const answer = typedBy.get(one.signature);
|
|
1110
1915
|
if (answer === undefined) {
|
|
@@ -1114,7 +1919,7 @@ function withContextualRead<Brand>(
|
|
|
1114
1919
|
judged.set(
|
|
1115
1920
|
one.part,
|
|
1116
1921
|
answer === UNANSWERED || place === undefined
|
|
1117
|
-
?
|
|
1922
|
+
? undefined
|
|
1118
1923
|
: one.pattern
|
|
1119
1924
|
? { ...one.part, parameter: { ...place, contextualPattern: true } }
|
|
1120
1925
|
: {
|
|
@@ -1126,14 +1931,14 @@ function withContextualRead<Brand>(
|
|
|
1126
1931
|
},
|
|
1127
1932
|
);
|
|
1128
1933
|
}
|
|
1129
|
-
return parts.
|
|
1934
|
+
return parts.flatMap((part) => (judged.has(part) ? (judged.get(part) ?? []) : [part]));
|
|
1130
1935
|
}
|
|
1131
1936
|
|
|
1132
1937
|
/**
|
|
1133
|
-
* Each name one function's parameters bind
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1938
|
+
* Each name one function's parameters bind that the function never reads, and those of
|
|
1939
|
+
* them a contextual type of a function expression may keep: the names of a parameter that
|
|
1940
|
+
* carries a type annotation or is an object binding pattern. A binding pattern counts as
|
|
1941
|
+
* read where its function reads any name it binds.
|
|
1137
1942
|
*/
|
|
1138
1943
|
function parameterParts(
|
|
1139
1944
|
file: SourceFile,
|
|
@@ -1161,25 +1966,28 @@ function parameterParts(
|
|
|
1161
1966
|
const contextual: ContextualParameter[] = [];
|
|
1162
1967
|
const expression =
|
|
1163
1968
|
isArrowFunction(signature) || isFunctionExpression(signature) ? signature : undefined;
|
|
1969
|
+
const length = signature.parameters.findIndex(
|
|
1970
|
+
(one) => one.initializer !== undefined || one.dotDotDotToken !== undefined,
|
|
1971
|
+
);
|
|
1164
1972
|
reader.names.forEach(({ name, at, pattern, exempt }, index) => {
|
|
1165
|
-
if (exempt) {
|
|
1973
|
+
if (exempt || read[index] === true) {
|
|
1166
1974
|
return;
|
|
1167
1975
|
}
|
|
1168
1976
|
const part: PartFact = {
|
|
1169
|
-
code: CODE.unusedParameter,
|
|
1170
1977
|
declaration,
|
|
1171
1978
|
kind: "parameter",
|
|
1172
1979
|
name: name.text,
|
|
1173
1980
|
position: spanOf(file, root, name),
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1981
|
+
parameter: {
|
|
1982
|
+
beforeARead: !pattern && at < lastRead,
|
|
1983
|
+
counted: length < 0 || at < length,
|
|
1984
|
+
...place,
|
|
1985
|
+
},
|
|
1177
1986
|
};
|
|
1178
1987
|
found.push(part);
|
|
1179
1988
|
const parameter = signature.parameters[at];
|
|
1180
1989
|
if (
|
|
1181
1990
|
expression !== undefined &&
|
|
1182
|
-
!part.used &&
|
|
1183
1991
|
parameter !== undefined &&
|
|
1184
1992
|
(parameter.type !== undefined || isObjectBindingPattern(parameter.name))
|
|
1185
1993
|
) {
|
|
@@ -1326,31 +2134,454 @@ function isPlainWrite(use: Identifier): boolean {
|
|
|
1326
2134
|
}
|
|
1327
2135
|
|
|
1328
2136
|
/**
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
2137
|
+
* What a path from one point reaches, one bit per target: for a judged local, the one bit
|
|
2138
|
+
* is a read before the next write; for a flow's statements, each statement has its own.
|
|
2139
|
+
*/
|
|
2140
|
+
type Reached = bigint;
|
|
2141
|
+
|
|
2142
|
+
const NOTHING: Reached = 0n;
|
|
2143
|
+
const READ: Reached = 1n;
|
|
2144
|
+
|
|
2145
|
+
/** What a path reaches from where each jump out of the statement being walked lands. */
|
|
2146
|
+
interface Targets {
|
|
2147
|
+
readonly exit: Reached;
|
|
2148
|
+
/** Where a `throw` lands, which any statement inside a `try` may also reach. */
|
|
2149
|
+
readonly raise: Reached;
|
|
2150
|
+
/** Where a jump with no target inside the walked statements lands. */
|
|
2151
|
+
readonly escape: Reached;
|
|
2152
|
+
readonly breaks?: Reached;
|
|
2153
|
+
readonly continues?: Reached;
|
|
2154
|
+
readonly labels: ReadonlyMap<string, { readonly breaks: Reached; readonly continues?: Reached }>;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/** What one walk of `flowWalk` asks about the statements it walks. */
|
|
2158
|
+
interface Judged {
|
|
2159
|
+
/** The targets inside one node, which a path entering it reaches where the walk does not follow it. */
|
|
2160
|
+
readonly inside: (node: Node | undefined) => Reached;
|
|
2161
|
+
/** The target one statement is, where the walk asks which statements a path reaches. */
|
|
2162
|
+
readonly at: (statement: Statement) => Reached;
|
|
2163
|
+
/** The identifiers of the judged local, whose plain writes the walk records. */
|
|
2164
|
+
readonly writes: readonly Identifier[];
|
|
2165
|
+
/** The call statements that never return. */
|
|
2166
|
+
readonly ends: ReadonlySet<Node>;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/** Whether a block sits inside a `try` whose `catch` or `finally` runs after the block is left. */
|
|
2170
|
+
function handledOutside(block: Block, scope: Node | undefined): boolean {
|
|
2171
|
+
for (let at: Node = block.parent; at !== scope && !isSourceFile(at); at = at.parent) {
|
|
2172
|
+
if (
|
|
2173
|
+
isTryStatement(at) &&
|
|
2174
|
+
(at.finallyBlock !== undefined ||
|
|
2175
|
+
(at.catchClause !== undefined && within(block, at.tryBlock)))
|
|
2176
|
+
) {
|
|
2177
|
+
return true;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
return false;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* What evaluating one expression may do: go each way as a condition, give `null` or
|
|
2185
|
+
* `undefined` (which goes `no`), and run code that may throw.
|
|
2186
|
+
*/
|
|
2187
|
+
interface Evaluation {
|
|
2188
|
+
readonly yes: boolean;
|
|
2189
|
+
readonly no: boolean;
|
|
2190
|
+
readonly nullish: boolean;
|
|
2191
|
+
readonly throws: boolean;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
const UNEVALUATED: Evaluation = { yes: false, no: false, nullish: false, throws: false };
|
|
2195
|
+
|
|
2196
|
+
/** The literals whose evaluation runs no code. */
|
|
2197
|
+
const LITERALS: ReadonlySet<SyntaxKind> = new Set([
|
|
2198
|
+
SyntaxKind.TrueKeyword,
|
|
2199
|
+
SyntaxKind.FalseKeyword,
|
|
2200
|
+
SyntaxKind.NullKeyword,
|
|
2201
|
+
SyntaxKind.NumericLiteral,
|
|
2202
|
+
SyntaxKind.BigIntLiteral,
|
|
2203
|
+
SyntaxKind.StringLiteral,
|
|
2204
|
+
SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
2205
|
+
SyntaxKind.RegularExpressionLiteral,
|
|
2206
|
+
]);
|
|
2207
|
+
|
|
2208
|
+
/**
|
|
2209
|
+
* One walk over an expression with JavaScript's short-circuit. It folds only the values the
|
|
2210
|
+
* binder folds (`bindCondition`): an absent part, and a bare `true` or `false` that is the
|
|
2211
|
+
* whole condition, an operand of `&&` or `||` or a `?:` condition, seen through `(` and `!`
|
|
2212
|
+
* only to a logical operator. The binder sends a falsy left operand of `??` to the right;
|
|
2213
|
+
* JavaScript and this walk end the `??` there.
|
|
2214
|
+
*/
|
|
2215
|
+
function evaluationOf(node: Node | undefined): Evaluation {
|
|
2216
|
+
if (node === undefined) {
|
|
2217
|
+
return { yes: true, no: false, nullish: false, throws: false };
|
|
2218
|
+
}
|
|
2219
|
+
const core = logicalCore(node);
|
|
2220
|
+
if (core === undefined) {
|
|
2221
|
+
const leaf = negationsRemoved(node);
|
|
2222
|
+
const literal = LITERALS.has(leaf.kind);
|
|
2223
|
+
const folded =
|
|
2224
|
+
(leaf.kind === SyntaxKind.TrueKeyword || leaf.kind === SyntaxKind.FalseKeyword) &&
|
|
2225
|
+
leaf === node &&
|
|
2226
|
+
!(
|
|
2227
|
+
isBinaryExpression(node.parent) &&
|
|
2228
|
+
node.parent.operatorToken.kind === SyntaxKind.QuestionQuestionToken
|
|
2229
|
+
);
|
|
2230
|
+
// The binder judges a `?:` whole, so it goes either way; a branch runs where its condition may go its way.
|
|
2231
|
+
const test = isConditionalExpression(leaf) ? evaluationOf(leaf.condition) : undefined;
|
|
2232
|
+
const branches =
|
|
2233
|
+
test !== undefined && isConditionalExpression(leaf)
|
|
2234
|
+
? [...(test.yes ? [leaf.whenTrue] : []), ...(test.no ? [leaf.whenFalse] : [])].map(
|
|
2235
|
+
(branch) => evaluationOf(branch),
|
|
2236
|
+
)
|
|
2237
|
+
: undefined;
|
|
2238
|
+
return {
|
|
2239
|
+
yes: !folded || leaf.kind === SyntaxKind.TrueKeyword,
|
|
2240
|
+
no: !folded || leaf.kind === SyntaxKind.FalseKeyword,
|
|
2241
|
+
nullish:
|
|
2242
|
+
leaf === parenthesesRemoved(node) &&
|
|
2243
|
+
(branches?.some((one) => one.nullish) ??
|
|
2244
|
+
(!literal || leaf.kind === SyntaxKind.NullKeyword)),
|
|
2245
|
+
throws:
|
|
2246
|
+
test === undefined ? !literal : test.throws || (branches ?? []).some((one) => one.throws),
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
if (isParenthesizedExpression(node)) {
|
|
2250
|
+
return evaluationOf(node.expression);
|
|
2251
|
+
}
|
|
2252
|
+
if (isPrefixUnaryExpression(node)) {
|
|
2253
|
+
const inner = evaluationOf(node.operand);
|
|
2254
|
+
return { yes: inner.no, no: inner.yes, nullish: false, throws: inner.throws };
|
|
2255
|
+
}
|
|
2256
|
+
const left = evaluationOf(core.left);
|
|
2257
|
+
const operator = core.operatorToken.kind;
|
|
2258
|
+
const runs =
|
|
2259
|
+
operator === SyntaxKind.AmpersandAmpersandToken
|
|
2260
|
+
? left.yes
|
|
2261
|
+
: operator === SyntaxKind.BarBarToken
|
|
2262
|
+
? left.no
|
|
2263
|
+
: left.nullish;
|
|
2264
|
+
const right = runs ? evaluationOf(core.right) : UNEVALUATED;
|
|
2265
|
+
return {
|
|
2266
|
+
yes: (operator !== SyntaxKind.AmpersandAmpersandToken && left.yes) || right.yes,
|
|
2267
|
+
no: (operator !== SyntaxKind.BarBarToken && left.no) || right.no,
|
|
2268
|
+
nullish: (operator === SyntaxKind.AmpersandAmpersandToken && left.nullish) || right.nullish,
|
|
2269
|
+
throws: left.throws || right.throws,
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
/** One expression below any parentheses and `!` around it. */
|
|
2274
|
+
function negationsRemoved(node: Node): Node {
|
|
2275
|
+
let at = node;
|
|
2276
|
+
while (
|
|
2277
|
+
isParenthesizedExpression(at) ||
|
|
2278
|
+
(isPrefixUnaryExpression(at) && at.operator === SyntaxKind.ExclamationToken)
|
|
2279
|
+
) {
|
|
2280
|
+
at = isParenthesizedExpression(at) ? at.expression : at.operand;
|
|
2281
|
+
}
|
|
2282
|
+
return at;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/** The `&&`, `||` or `??` an expression is below any parentheses and `!`, if it is one. */
|
|
2286
|
+
function logicalCore(node: Node): BinaryExpression | undefined {
|
|
2287
|
+
const core = negationsRemoved(node);
|
|
2288
|
+
return isBinaryExpression(core) &&
|
|
2289
|
+
[
|
|
2290
|
+
SyntaxKind.AmpersandAmpersandToken,
|
|
2291
|
+
SyntaxKind.BarBarToken,
|
|
2292
|
+
SyntaxKind.QuestionQuestionToken,
|
|
2293
|
+
].includes(core.operatorToken.kind)
|
|
2294
|
+
? core
|
|
2295
|
+
: undefined;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* One backward transfer over statements, which dead stores and unreachable statements share:
|
|
2300
|
+
* what a path from each statement's start reaches, given what one from its end reaches and
|
|
2301
|
+
* where its jumps land. A write is a plain assignment of the judged local that an expression
|
|
2302
|
+
* statement or a `for` head evaluates, or the bare name a `for…of` or `for…in` assigns; `live`
|
|
2303
|
+
* holds what a path from each write reaches. A node the walk does not follow reaches every
|
|
2304
|
+
* target inside it.
|
|
2305
|
+
*/
|
|
2306
|
+
function flowWalk(judged: Judged): {
|
|
2307
|
+
readonly sequence: (statements: readonly Statement[], after: Reached, to: Targets) => Reached;
|
|
2308
|
+
readonly record: (target: Identifier, after: Reached) => void;
|
|
2309
|
+
readonly live: ReadonlyMap<Identifier, Reached>;
|
|
2310
|
+
} {
|
|
2311
|
+
const { inside: reads, writes, ends } = judged;
|
|
2312
|
+
const live = new Map<Identifier, Reached>();
|
|
2313
|
+
const record = (target: Identifier, after: Reached): void => {
|
|
2314
|
+
live.set(target, (live.get(target) ?? NOTHING) | after);
|
|
2315
|
+
};
|
|
2316
|
+
const fixpoint = (step: (head: Reached) => Reached): Reached => {
|
|
2317
|
+
let head = NOTHING;
|
|
2318
|
+
for (let next = step(head); next !== head; next = step(head)) {
|
|
2319
|
+
head = next;
|
|
2320
|
+
}
|
|
2321
|
+
return head;
|
|
2322
|
+
};
|
|
2323
|
+
const sequence = (statements: readonly Statement[], after: Reached, to: Targets): Reached =>
|
|
2324
|
+
statements.reduceRight((out, statement) => flow(statement, out, to), after);
|
|
2325
|
+
// A body run inline is a function's or a static block's: only `return` and a throw leave it,
|
|
2326
|
+
// so it starts with no break, continue or label target.
|
|
2327
|
+
const ran = (body: Block, after: Reached, exit: Reached, raise: Reached): Reached =>
|
|
2328
|
+
sequence(body.statements, after, { exit, raise, escape: NOTHING, labels: new Map() });
|
|
2329
|
+
// Every label of a chain such as `a: b: while` names the loop, so each one continues it.
|
|
2330
|
+
const looped = (
|
|
2331
|
+
to: Targets,
|
|
2332
|
+
after: Reached,
|
|
2333
|
+
head: Reached,
|
|
2334
|
+
chain: readonly string[],
|
|
2335
|
+
): Targets => ({
|
|
2336
|
+
...to,
|
|
2337
|
+
breaks: after,
|
|
2338
|
+
continues: head,
|
|
2339
|
+
labels: new Map([
|
|
2340
|
+
...to.labels,
|
|
2341
|
+
...chain.map((label) => [label, { breaks: after, continues: head }] as const),
|
|
2342
|
+
]),
|
|
2343
|
+
});
|
|
2344
|
+
const when = (condition: boolean, reached: Reached): Reached => (condition ? reached : NOTHING);
|
|
2345
|
+
// Code that may throw reaches `raise`, where a throw lands, before it writes anything.
|
|
2346
|
+
const thrown = (node: Node | undefined, raise: Reached): Reached =>
|
|
2347
|
+
when(evaluationOf(node).throws, raise);
|
|
2348
|
+
// A comma's operands run in turn, so a throw in the right one follows the left one's write.
|
|
2349
|
+
// A store to the local, whose declaration has run, throws only where its value does.
|
|
2350
|
+
const evaluated = (node: Node | undefined, after: Reached, raise: Reached): Reached => {
|
|
2351
|
+
const expression = node === undefined ? undefined : parenthesesRemoved(node);
|
|
2352
|
+
if (
|
|
2353
|
+
expression !== undefined &&
|
|
2354
|
+
isBinaryExpression(expression) &&
|
|
2355
|
+
expression.operatorToken.kind === SyntaxKind.CommaToken
|
|
2356
|
+
) {
|
|
2357
|
+
return evaluated(expression.left, evaluated(expression.right, after, raise), raise);
|
|
2358
|
+
}
|
|
2359
|
+
const target =
|
|
2360
|
+
expression !== undefined &&
|
|
2361
|
+
isBinaryExpression(expression) &&
|
|
2362
|
+
expression.operatorToken.kind === SyntaxKind.EqualsToken
|
|
2363
|
+
? parenthesesRemoved(expression.left)
|
|
2364
|
+
: undefined;
|
|
2365
|
+
if (
|
|
2366
|
+
expression === undefined ||
|
|
2367
|
+
!isBinaryExpression(expression) ||
|
|
2368
|
+
target === undefined ||
|
|
2369
|
+
!isIdentifier(target) ||
|
|
2370
|
+
!writes.includes(target)
|
|
2371
|
+
) {
|
|
2372
|
+
return reads(expression) | after | thrown(expression, raise);
|
|
2373
|
+
}
|
|
2374
|
+
record(target, after);
|
|
2375
|
+
return reads(expression.right) | thrown(expression.right, raise);
|
|
2376
|
+
};
|
|
2377
|
+
// A statement may throw where it starts, before it writes anything, but entering a block
|
|
2378
|
+
// runs nothing. A part a statement runs again, or after a write of its own, gets its own
|
|
2379
|
+
// throw edge.
|
|
2380
|
+
const flow = (
|
|
2381
|
+
node: Statement,
|
|
2382
|
+
after: Reached,
|
|
2383
|
+
to: Targets,
|
|
2384
|
+
chain: readonly string[] = [],
|
|
2385
|
+
): Reached => judged.at(node) | transfer(node, after, to, chain) | when(!isBlock(node), to.raise);
|
|
2386
|
+
const transfer = (
|
|
2387
|
+
node: Statement,
|
|
2388
|
+
after: Reached,
|
|
2389
|
+
to: Targets,
|
|
2390
|
+
chain: readonly string[],
|
|
2391
|
+
): Reached => {
|
|
2392
|
+
if (isBlock(node)) {
|
|
2393
|
+
return sequence(node.statements, after, to);
|
|
2394
|
+
}
|
|
2395
|
+
if (isExpressionStatement(node)) {
|
|
2396
|
+
const [body] = runsInline(node);
|
|
2397
|
+
const call = parenthesesRemoved(node.expression);
|
|
2398
|
+
if (body !== undefined && isCallExpression(call)) {
|
|
2399
|
+
// The arguments run first, then the body, whose `return` ends the call.
|
|
2400
|
+
return call.arguments.reduce(
|
|
2401
|
+
(out, argument) => out | reads(argument),
|
|
2402
|
+
ran(body, after, after, to.raise),
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
return evaluated(node.expression, ends.has(node) ? NOTHING : after, to.raise);
|
|
2406
|
+
}
|
|
2407
|
+
if (isClassDeclaration(node)) {
|
|
2408
|
+
return runsInline(node).reduceRight((out, body) => ran(body, out, NOTHING, to.raise), after);
|
|
2409
|
+
}
|
|
2410
|
+
if (isIfStatement(node)) {
|
|
2411
|
+
const then = flow(node.thenStatement, after, to);
|
|
2412
|
+
const otherwise =
|
|
2413
|
+
node.elseStatement === undefined ? after : flow(node.elseStatement, after, to);
|
|
2414
|
+
const way = evaluationOf(node.expression);
|
|
2415
|
+
return reads(node.expression) | when(way.yes, then) | when(way.no, otherwise);
|
|
2416
|
+
}
|
|
2417
|
+
if (isLabeledStatement(node)) {
|
|
2418
|
+
const labels = new Map([...to.labels, [node.label.text, { breaks: after }]]);
|
|
2419
|
+
return flow(node.statement, after, { ...to, labels }, [...chain, node.label.text]);
|
|
2420
|
+
}
|
|
2421
|
+
if (isWithStatement(node)) {
|
|
2422
|
+
return reads(node.expression) | flow(node.statement, after, to);
|
|
2423
|
+
}
|
|
2424
|
+
if (isWhileStatement(node) || isDoStatement(node)) {
|
|
2425
|
+
const way = evaluationOf(node.expression);
|
|
2426
|
+
const test = fixpoint((head) => {
|
|
2427
|
+
const body = flow(node.statement, head, looped(to, after, head, chain));
|
|
2428
|
+
return (
|
|
2429
|
+
reads(node.expression) |
|
|
2430
|
+
when(way.no, after) |
|
|
2431
|
+
when(way.yes, body) |
|
|
2432
|
+
thrown(node.expression, to.raise)
|
|
2433
|
+
);
|
|
2434
|
+
});
|
|
2435
|
+
return isDoStatement(node)
|
|
2436
|
+
? flow(node.statement, test, looped(to, after, test, chain))
|
|
2437
|
+
: test;
|
|
2438
|
+
}
|
|
2439
|
+
if (isForStatement(node)) {
|
|
2440
|
+
const way = evaluationOf(node.condition);
|
|
2441
|
+
const head = fixpoint((at) => {
|
|
2442
|
+
const next = evaluated(node.incrementor, at, to.raise);
|
|
2443
|
+
const body = flow(node.statement, next, looped(to, after, next, chain));
|
|
2444
|
+
return (
|
|
2445
|
+
reads(node.condition) |
|
|
2446
|
+
when(way.no, after) |
|
|
2447
|
+
when(way.yes, body) |
|
|
2448
|
+
thrown(node.condition, to.raise)
|
|
2449
|
+
);
|
|
2450
|
+
});
|
|
2451
|
+
return node.initializer !== undefined && isVariableDeclarationList(node.initializer)
|
|
2452
|
+
? reads(node.initializer) | head
|
|
2453
|
+
: evaluated(node.initializer, head, to.raise);
|
|
2454
|
+
}
|
|
2455
|
+
if (isForOfStatement(node) || isForInStatement(node)) {
|
|
2456
|
+
const target = parenthesesRemoved(node.initializer);
|
|
2457
|
+
const assigned = isIdentifier(target) && writes.includes(target) ? target : undefined;
|
|
2458
|
+
// The iterator runs on each pass through the head and may throw.
|
|
2459
|
+
const head = fixpoint((at) => {
|
|
2460
|
+
const body = flow(node.statement, at, looped(to, after, at, chain));
|
|
2461
|
+
if (assigned === undefined) {
|
|
2462
|
+
return reads(node.initializer) | after | body | to.raise;
|
|
2463
|
+
}
|
|
2464
|
+
record(assigned, body);
|
|
2465
|
+
return after | to.raise;
|
|
2466
|
+
});
|
|
2467
|
+
return reads(node.expression) | head;
|
|
2468
|
+
}
|
|
2469
|
+
if (isSwitchStatement(node)) {
|
|
2470
|
+
const inside: Targets = { ...to, breaks: after };
|
|
2471
|
+
let next = after;
|
|
2472
|
+
let entered = NOTHING;
|
|
2473
|
+
for (const clause of node.caseBlock.clauses.toReversed()) {
|
|
2474
|
+
next = sequence(clause.statements, next, inside);
|
|
2475
|
+
entered |= next | (isCaseClause(clause) ? reads(clause.expression) : NOTHING);
|
|
2476
|
+
}
|
|
2477
|
+
const total = node.caseBlock.clauses.some(isDefaultClause);
|
|
2478
|
+
return reads(node.expression) | entered | when(!total, after);
|
|
2479
|
+
}
|
|
2480
|
+
if (isBreakStatement(node) || isContinueStatement(node)) {
|
|
2481
|
+
const named = node.label === undefined ? undefined : to.labels.get(node.label.text);
|
|
2482
|
+
const target = isBreakStatement(node)
|
|
2483
|
+
? node.label === undefined
|
|
2484
|
+
? to.breaks
|
|
2485
|
+
: named?.breaks
|
|
2486
|
+
: node.label === undefined
|
|
2487
|
+
? to.continues
|
|
2488
|
+
: named?.continues;
|
|
2489
|
+
return target ?? to.escape;
|
|
2490
|
+
}
|
|
2491
|
+
if (isReturnStatement(node)) {
|
|
2492
|
+
return reads(node.expression) | to.exit;
|
|
2493
|
+
}
|
|
2494
|
+
if (isThrowStatement(node)) {
|
|
2495
|
+
return reads(node.expression) | to.raise;
|
|
2496
|
+
}
|
|
2497
|
+
if (isTryStatement(node)) {
|
|
2498
|
+
const final = node.finallyBlock;
|
|
2499
|
+
// The finally block's entry depends only on what a path from where it continues
|
|
2500
|
+
// reaches, so it is computed at most once for each answer.
|
|
2501
|
+
const entries = new Map<Reached, Reached>();
|
|
2502
|
+
const through = (reached: Reached): Reached => {
|
|
2503
|
+
if (final === undefined) {
|
|
2504
|
+
return reached;
|
|
2505
|
+
}
|
|
2506
|
+
const known = entries.get(reached) ?? sequence(final.statements, reached, to);
|
|
2507
|
+
entries.set(reached, known);
|
|
2508
|
+
return known;
|
|
2509
|
+
};
|
|
2510
|
+
const map = (one: { readonly breaks: Reached; readonly continues?: Reached }) => ({
|
|
2511
|
+
breaks: through(one.breaks),
|
|
2512
|
+
...(one.continues === undefined ? {} : { continues: through(one.continues) }),
|
|
2513
|
+
});
|
|
2514
|
+
const outer: Targets = {
|
|
2515
|
+
exit: through(to.exit),
|
|
2516
|
+
raise: through(to.raise),
|
|
2517
|
+
escape: through(to.escape),
|
|
2518
|
+
...(to.breaks === undefined ? {} : { breaks: through(to.breaks) }),
|
|
2519
|
+
...(to.continues === undefined ? {} : { continues: through(to.continues) }),
|
|
2520
|
+
labels: new Map([...to.labels].map(([name, one]) => [name, map(one)])),
|
|
2521
|
+
};
|
|
2522
|
+
const normal = through(after);
|
|
2523
|
+
const handler =
|
|
2524
|
+
node.catchClause === undefined
|
|
2525
|
+
? undefined
|
|
2526
|
+
: sequence(node.catchClause.block.statements, normal, outer);
|
|
2527
|
+
// The binder's catch or finally sees the state before the try (`bindTryStatement`),
|
|
2528
|
+
// so it is reached from the entry even where the block runs nothing.
|
|
2529
|
+
const raise = handler ?? outer.raise;
|
|
2530
|
+
return sequence(node.tryBlock.statements, normal, { ...outer, raise }) | raise;
|
|
2531
|
+
}
|
|
2532
|
+
return reads(node) | after;
|
|
2533
|
+
};
|
|
2534
|
+
return { sequence, record, live };
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
/**
|
|
2538
|
+
* The writes to one local of its block that no read reaches, by `flowWalk` from the
|
|
2539
|
+
* statement that declares it: a write is dead where no path from it reads the local before
|
|
2540
|
+
* the next write or the end of its scope, and any node the walk does not follow that names
|
|
2541
|
+
* the local reads it.
|
|
2542
|
+
*/
|
|
2543
|
+
function deadWrites(
|
|
2544
|
+
block: Block,
|
|
2545
|
+
local: Local,
|
|
2546
|
+
uses: readonly Identifier[],
|
|
2547
|
+
escape: boolean,
|
|
2548
|
+
ends: ReadonlySet<Node>,
|
|
2549
|
+
): readonly Identifier[] {
|
|
2550
|
+
const reads = (node: Node | undefined): Reached =>
|
|
2551
|
+
node !== undefined && uses.some((use) => within(use, node)) ? READ : NOTHING;
|
|
2552
|
+
const walk = flowWalk({ inside: reads, at: () => NOTHING, writes: uses, ends });
|
|
2553
|
+
const statements = block.statements;
|
|
2554
|
+
const declaration = statements[local.statement];
|
|
2555
|
+
const outlives = local.outlives ? READ : NOTHING;
|
|
2556
|
+
const after = walk.sequence(statements.slice(local.statement + 1), outlives, {
|
|
2557
|
+
exit: escape ? READ : NOTHING,
|
|
2558
|
+
raise: escape ? READ : NOTHING,
|
|
2559
|
+
escape: outlives,
|
|
2560
|
+
labels: new Map(),
|
|
2561
|
+
});
|
|
2562
|
+
// The rest of the declaring statement runs after the write: a later declarator may read the local.
|
|
2563
|
+
if (local.initialized && declaration !== undefined && reads(declaration) === NOTHING) {
|
|
2564
|
+
walk.record(local.name, after);
|
|
2565
|
+
}
|
|
2566
|
+
return [...walk.live]
|
|
2567
|
+
.filter(([, read]) => read === NOTHING)
|
|
2568
|
+
.map(([target]) => target)
|
|
2569
|
+
.toSorted((a, b) => a.pos - b.pos);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
/**
|
|
2573
|
+
* The writes to the locals of one block that no read reaches. A local a nested function
|
|
2574
|
+
* reads is no subject, since the function may run at any time, and every write to a local
|
|
2575
|
+
* nothing reads is dead, a nested function's included.
|
|
1335
2576
|
*/
|
|
1336
2577
|
function deadStores(
|
|
1337
|
-
file: SourceFile,
|
|
1338
|
-
root: string,
|
|
1339
2578
|
block: Block,
|
|
1340
|
-
holder: string,
|
|
1341
2579
|
locals: readonly Local[],
|
|
1342
2580
|
ids: ReadonlyMap<Node, Answer<number>>,
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
declaration: holder,
|
|
1348
|
-
kind: "store",
|
|
1349
|
-
name: local.name.text,
|
|
1350
|
-
position: spanOf(file, root, target),
|
|
1351
|
-
message: `value written to ${local.name.text} is never read`,
|
|
1352
|
-
used: false,
|
|
1353
|
-
});
|
|
2581
|
+
ends: ReadonlySet<Node>,
|
|
2582
|
+
): readonly { readonly local: Local; readonly target: Identifier }[] {
|
|
2583
|
+
const found: { local: Local; target: Identifier }[] = [];
|
|
2584
|
+
const scope = functionOf(block);
|
|
1354
2585
|
for (const local of locals) {
|
|
1355
2586
|
const own = local.hoisted ? block.statements[local.statement] : undefined;
|
|
1356
2587
|
// A function's reads of itself keep it no more than its absence would.
|
|
@@ -1361,73 +2592,17 @@ function deadStores(
|
|
|
1361
2592
|
// A declaration with no initializer writes `undefined`, the one store left to name.
|
|
1362
2593
|
const writes = [...(local.initialized ? [local.name] : []), ...uses];
|
|
1363
2594
|
const targets = writes.length === 0 ? [local.name] : writes;
|
|
1364
|
-
found.push(...targets.map((target) =>
|
|
2595
|
+
found.push(...targets.map((target) => ({ local, target })));
|
|
1365
2596
|
continue;
|
|
1366
2597
|
}
|
|
1367
2598
|
// A read anywhere in the block reads a function declaration, which is written first.
|
|
1368
|
-
if (local.hoisted) {
|
|
1369
|
-
continue;
|
|
1370
|
-
}
|
|
1371
|
-
if (uses.some((use) => inNestedFunction(use, block))) {
|
|
2599
|
+
if (local.hoisted || uses.some((use) => inNestedFunction(use, block))) {
|
|
1372
2600
|
continue;
|
|
1373
2601
|
}
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
if (!isExpressionStatement(statement)) {
|
|
1379
|
-
return undefined;
|
|
1380
|
-
}
|
|
1381
|
-
const expression = parenthesesRemoved(statement.expression);
|
|
1382
|
-
if (
|
|
1383
|
-
!isBinaryExpression(expression) ||
|
|
1384
|
-
expression.operatorToken.kind !== SyntaxKind.EqualsToken
|
|
1385
|
-
) {
|
|
1386
|
-
return undefined;
|
|
1387
|
-
}
|
|
1388
|
-
const target = parenthesesRemoved(expression.left);
|
|
1389
|
-
return isIdentifier(target) && uses.includes(target)
|
|
1390
|
-
? { target, value: expression.right }
|
|
1391
|
-
: undefined;
|
|
1392
|
-
};
|
|
1393
|
-
const writes: { at: number; target: Identifier }[] = [];
|
|
1394
|
-
if (local.initialized) {
|
|
1395
|
-
writes.push({ at: local.statement, target: local.name });
|
|
1396
|
-
}
|
|
1397
|
-
statements.forEach((statement, at) => {
|
|
1398
|
-
const write = writeOf(statement);
|
|
1399
|
-
if (write !== undefined && at > local.statement) {
|
|
1400
|
-
writes.push({ at, target: write.target });
|
|
1401
|
-
}
|
|
1402
|
-
});
|
|
1403
|
-
for (const write of writes) {
|
|
1404
|
-
// The rest of the write's own statement runs after it: a later declarator may read the local.
|
|
1405
|
-
const own = statements[write.at];
|
|
1406
|
-
const sameStatement = own !== undefined && write.target === local.name ? usesIn(own) : [];
|
|
1407
|
-
if (sameStatement.length > 0) {
|
|
1408
|
-
continue;
|
|
1409
|
-
}
|
|
1410
|
-
let dead = !local.outlives;
|
|
1411
|
-
for (let at = write.at + 1; at < statements.length; at += 1) {
|
|
1412
|
-
const statement = statements[at];
|
|
1413
|
-
if (statement === undefined) {
|
|
1414
|
-
break;
|
|
1415
|
-
}
|
|
1416
|
-
const next = writeOf(statement);
|
|
1417
|
-
if (next !== undefined) {
|
|
1418
|
-
dead = usesIn(next.value).length === 0;
|
|
1419
|
-
break;
|
|
1420
|
-
}
|
|
1421
|
-
const named = usesIn(statement);
|
|
1422
|
-
if (named.length > 0) {
|
|
1423
|
-
dead = false;
|
|
1424
|
-
break;
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
if (dead) {
|
|
1428
|
-
found.push(deadAt(local, write.target));
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
2602
|
+
const escape = local.outlives && handledOutside(block, scope);
|
|
2603
|
+
found.push(
|
|
2604
|
+
...deadWrites(block, local, uses, escape, ends).map((target) => ({ local, target })),
|
|
2605
|
+
);
|
|
1431
2606
|
}
|
|
1432
2607
|
return found;
|
|
1433
2608
|
}
|
|
@@ -1435,6 +2610,7 @@ function deadStores(
|
|
|
1435
2610
|
/**
|
|
1436
2611
|
* The result of each function a discarded call reaches, where its type carries a value:
|
|
1437
2612
|
* whether it is used is the call sites' to say, which the run decides over every project.
|
|
2613
|
+
* `judged` holds every function whose result type the project was asked about.
|
|
1438
2614
|
*/
|
|
1439
2615
|
function resultParts<Brand>(
|
|
1440
2616
|
project: ProjectView<Brand>,
|
|
@@ -1442,19 +2618,21 @@ function resultParts<Brand>(
|
|
|
1442
2618
|
sites: readonly { readonly file: SourceFile; readonly declared: Declared }[],
|
|
1443
2619
|
references: readonly Reference[],
|
|
1444
2620
|
calls: ReadonlyMap<string, CallUse>,
|
|
1445
|
-
): PartFact[] {
|
|
2621
|
+
): { readonly parts: readonly PartFact[]; readonly judged: ReadonlySet<string> } {
|
|
1446
2622
|
const discardedAt = new Set(
|
|
1447
2623
|
references
|
|
1448
2624
|
.filter((one) => (calls.get(positionKey(one.position)) ?? "used") !== "used")
|
|
1449
2625
|
.map((one) => one.to),
|
|
1450
2626
|
);
|
|
1451
2627
|
const found: PartFact[] = [];
|
|
2628
|
+
const judged = new Set<string>();
|
|
1452
2629
|
for (const { file, declared } of sites) {
|
|
1453
2630
|
const { signature, node, id } = declared;
|
|
1454
2631
|
const generator = (signature as { readonly asteriskToken?: Node }).asteriskToken !== undefined;
|
|
1455
2632
|
if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
|
|
1456
2633
|
continue;
|
|
1457
2634
|
}
|
|
2635
|
+
judged.add(id);
|
|
1458
2636
|
// A question the checker leaves unanswered withholds the finding.
|
|
1459
2637
|
const typed = project.queries.signatureOf(signature);
|
|
1460
2638
|
if (typed === undefined || !isAnswered(typed)) {
|
|
@@ -1472,25 +2650,24 @@ function resultParts<Brand>(
|
|
|
1472
2650
|
const valued = project.queries.nonNullable(result);
|
|
1473
2651
|
const members = isAnswered(valued) ? project.queries.propertiesOf(valued) : UNANSWERED;
|
|
1474
2652
|
found.push({
|
|
1475
|
-
code: CODE.unusedResult,
|
|
1476
2653
|
declaration: id,
|
|
1477
2654
|
kind: "result",
|
|
1478
2655
|
name: "result",
|
|
1479
2656
|
position: spanOf(file, root, at),
|
|
1480
|
-
message: "result is discarded at every call site",
|
|
1481
|
-
used: false,
|
|
1482
2657
|
thenable: !isAnswered(members) || members.some((member) => member.name === "then"),
|
|
1483
2658
|
});
|
|
1484
2659
|
}
|
|
1485
|
-
return found;
|
|
2660
|
+
return { parts: found, judged };
|
|
1486
2661
|
}
|
|
1487
2662
|
|
|
1488
2663
|
/** What the run answers about the parts of its declarations, over every project. */
|
|
1489
2664
|
export interface IntraFunctionFacts {
|
|
1490
|
-
/** Every part of the run, each once,
|
|
2665
|
+
/** Every part of the run, each once, that every project judging it found doing nothing. */
|
|
1491
2666
|
readonly parts: readonly PartFact[];
|
|
1492
2667
|
/** Every import binding of the run, each once, read where any project found it read. */
|
|
1493
2668
|
readonly imports: readonly ImportFact[];
|
|
2669
|
+
/** The declarations an outside importer of the run's files can name. */
|
|
2670
|
+
readonly published: ReadonlySet<string>;
|
|
1494
2671
|
/** The declarations whose signature is free to change. */
|
|
1495
2672
|
readonly free: ReadonlySet<string>;
|
|
1496
2673
|
/** The declarations whose signature is free but for a use as a value. */
|
|
@@ -1503,24 +2680,34 @@ export interface IntraFunctionFacts {
|
|
|
1503
2680
|
}
|
|
1504
2681
|
|
|
1505
2682
|
/**
|
|
1506
|
-
* The parts of a run's projects merged: a part
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
2683
|
+
* The parts of a run's projects merged (matrix.6): a part stands only where every project
|
|
2684
|
+
* that judges it finds it, a project judging the parts of each file it compiles and the
|
|
2685
|
+
* result of each function it asked the checker about. A signature is free when no
|
|
2686
|
+
* exemption record names it, its body is no stub, no class hierarchy fixes it, and every
|
|
2687
|
+
* reference is a call or a specifier: any other uses the function as a value, and a
|
|
2688
|
+
* consumer's reference has a form this run does not read.
|
|
1512
2689
|
*/
|
|
1513
2690
|
export function intraFunctionFacts(
|
|
1514
2691
|
projects: readonly ProjectParts[],
|
|
1515
2692
|
references: readonly Reference[],
|
|
1516
2693
|
exempted: ReadonlySet<string>,
|
|
1517
2694
|
): IntraFunctionFacts {
|
|
1518
|
-
const
|
|
1519
|
-
for (const
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
2695
|
+
const found = new Map<string, { readonly part: PartFact; readonly by: Set<ProjectParts> }>();
|
|
2696
|
+
for (const project of projects) {
|
|
2697
|
+
for (const part of project.parts) {
|
|
2698
|
+
const key = `${part.kind}\u0000${positionKey(part.position)}`;
|
|
2699
|
+
const known = found.get(key) ?? { part, by: new Set<ProjectParts>() };
|
|
2700
|
+
known.by.add(project);
|
|
2701
|
+
found.set(key, known);
|
|
2702
|
+
}
|
|
1523
2703
|
}
|
|
2704
|
+
const judges = (project: ProjectParts, part: PartFact): boolean =>
|
|
2705
|
+
part.kind === "result"
|
|
2706
|
+
? project.results.has(part.declaration)
|
|
2707
|
+
: project.files.has(part.position.path);
|
|
2708
|
+
const parts = [...found.values()]
|
|
2709
|
+
.filter(({ part, by }) => projects.every((one) => by.has(one) || !judges(one, part)))
|
|
2710
|
+
.map(({ part }) => part);
|
|
1524
2711
|
const imports = new Map<string, ImportFact>();
|
|
1525
2712
|
for (const binding of projects.flatMap((one) => one.imports)) {
|
|
1526
2713
|
const key = positionKey(binding.position);
|
|
@@ -1580,7 +2767,7 @@ export function intraFunctionFacts(
|
|
|
1580
2767
|
);
|
|
1581
2768
|
// Awaiting a thenable result sequences on it, so a call that awaits one uses it.
|
|
1582
2769
|
const sequenced = new Set(
|
|
1583
|
-
|
|
2770
|
+
parts
|
|
1584
2771
|
.filter((part) => part.thenable === true && awaited.has(part.declaration))
|
|
1585
2772
|
.map((part) => part.declaration),
|
|
1586
2773
|
);
|
|
@@ -1590,8 +2777,9 @@ export function intraFunctionFacts(
|
|
|
1590
2777
|
.map(([id]) => id),
|
|
1591
2778
|
);
|
|
1592
2779
|
return {
|
|
1593
|
-
parts
|
|
2780
|
+
parts,
|
|
1594
2781
|
imports: [...imports.values()],
|
|
2782
|
+
published: new Set(projects.flatMap((one) => [...one.published])),
|
|
1595
2783
|
free,
|
|
1596
2784
|
freeButValued,
|
|
1597
2785
|
discardedEverywhere,
|