@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
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* call sites that use or discard it, each statement control flow cannot reach, each
|
|
5
5
|
* write to a local that no read reaches, and each case an earlier case covers.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { SignatureKind } from "@typescript/native/sync";
|
|
7
|
+
import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBindingElement, isBlock, isBreakStatement, isCallExpression, isCaseClause, isCatchClause, isClassDeclaration, isClassStaticBlockDeclaration, isClassExpression, isConditionalExpression, isConstructorDeclaration, isContinueStatement, isDecorator, isDefaultClause, isDoStatement, isEnumDeclaration, isExportDeclaration, isExportAssignment, isExportSpecifier, isExpressionStatement, isExpressionWithTypeArguments, isForInStatement, isForOfStatement, isForStatement, isFunctionDeclaration, isFunctionExpression, isFunctionLikeDeclaration, isGetAccessorDeclaration, isHeritageClause, isIdentifier, isIfStatement, isInterfaceDeclaration, isImportClause, isImportDeclaration, isImportEqualsDeclaration, isImportSpecifier, isLabeledStatement, isMethodDeclaration, isModuleBlock, isModuleDeclaration, isNamedExports, isNamespaceImport, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isParameterDeclaration, isPrefixUnaryExpression, isPrivateIdentifier, isPropertyAccessExpression, isPropertyAssignment, isPropertyDeclaration, isPropertySignatureDeclaration, isQualifiedName, isReturnStatement, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSourceFile, isStringLiteral, isSwitchStatement, isThrowStatement, isTryStatement, isTypeAliasDeclaration, isTypePredicateNode, isTypeReferenceNode, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, isVoidExpression, isWhileStatement, isWithStatement, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
|
|
8
|
+
import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/sync";
|
|
10
9
|
import { exportsLocally, implicitReads, importBindings, } from "./import-bindings.js";
|
|
11
10
|
import { isParameterProperty, nodeKey } from "./inventory.js";
|
|
12
11
|
import { positionKey, renderPosition } from "./position.js";
|
|
12
|
+
import { publishedAPI } from "./roots/published-reach.js";
|
|
13
|
+
import { sourceFilesOf } from "./source-files.js";
|
|
13
14
|
import { importRef } from "./ref.js";
|
|
14
15
|
import { isAnswered, UNANSWERED } from "./query.js";
|
|
15
16
|
import { signatureOf } from "./signatures.js";
|
|
@@ -52,42 +53,595 @@ function callUse(call) {
|
|
|
52
53
|
? "awaited"
|
|
53
54
|
: "used";
|
|
54
55
|
}
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
/** What a namespace declaration holds at run time: nothing, only const enums, or values. */
|
|
57
|
+
const HOLDS_NOTHING = 0;
|
|
58
|
+
const HOLDS_CONST_ENUMS = 1;
|
|
59
|
+
const HOLDS_VALUES = 2;
|
|
60
|
+
/**
|
|
61
|
+
* The instance state of one declaration, as the compiler's binder computes it
|
|
62
|
+
* (`getModuleInstanceState`) from the declaration's own body. A name an export clause
|
|
63
|
+
* re-exports takes the state of the local declarations it names. A node met again while it
|
|
64
|
+
* is still being decided holds nothing, as in the binder.
|
|
65
|
+
*/
|
|
66
|
+
function instanceState(node, visited = new Map()) {
|
|
67
|
+
const known = visited.get(node);
|
|
68
|
+
if (known !== undefined || visited.has(node)) {
|
|
69
|
+
return known ?? HOLDS_NOTHING;
|
|
62
70
|
}
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
visited.set(node, HOLDS_NOTHING);
|
|
72
|
+
const highest = (nodes, of) => {
|
|
73
|
+
let state = HOLDS_NOTHING;
|
|
74
|
+
for (const one of nodes) {
|
|
75
|
+
state = Math.max(state, of(one));
|
|
76
|
+
if (state === HOLDS_VALUES) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return state;
|
|
81
|
+
};
|
|
82
|
+
let state = HOLDS_VALUES;
|
|
83
|
+
if (isModuleDeclaration(node)) {
|
|
84
|
+
state = node.body === undefined ? HOLDS_VALUES : instanceState(node.body, visited);
|
|
65
85
|
}
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
abrupt(statement.thenStatement) &&
|
|
69
|
-
abrupt(statement.elseStatement));
|
|
86
|
+
else if (isModuleBlock(node)) {
|
|
87
|
+
state = highest(node.statements, (one) => instanceState(one, visited));
|
|
70
88
|
}
|
|
71
|
-
|
|
89
|
+
else if (isInterfaceDeclaration(node) || isTypeAliasDeclaration(node)) {
|
|
90
|
+
state = HOLDS_NOTHING;
|
|
91
|
+
}
|
|
92
|
+
else if (isEnumDeclaration(node) && hasModifier(node, SyntaxKind.ConstKeyword)) {
|
|
93
|
+
state = HOLDS_CONST_ENUMS;
|
|
94
|
+
}
|
|
95
|
+
else if ((isImportDeclaration(node) || isImportEqualsDeclaration(node)) &&
|
|
96
|
+
!hasModifier(node, SyntaxKind.ExportKeyword)) {
|
|
97
|
+
state = HOLDS_NOTHING;
|
|
98
|
+
}
|
|
99
|
+
else if (isExportDeclaration(node) &&
|
|
100
|
+
node.moduleSpecifier === undefined &&
|
|
101
|
+
node.exportClause !== undefined &&
|
|
102
|
+
isNamedExports(node.exportClause)) {
|
|
103
|
+
state = highest(node.exportClause.elements, (one) => reexportedState(one, visited));
|
|
104
|
+
}
|
|
105
|
+
visited.set(node, state);
|
|
106
|
+
return state;
|
|
107
|
+
}
|
|
108
|
+
/** The instance state of the local declarations one export specifier names, the nearest scope's. */
|
|
109
|
+
function reexportedState(specifier, visited) {
|
|
110
|
+
const name = specifier.propertyName ?? specifier.name;
|
|
111
|
+
if (!isIdentifier(name)) {
|
|
112
|
+
return HOLDS_VALUES;
|
|
113
|
+
}
|
|
114
|
+
const names = (statement) => {
|
|
115
|
+
if (isVariableStatement(statement)) {
|
|
116
|
+
return statement.declarationList.declarations.some(names);
|
|
117
|
+
}
|
|
118
|
+
const declared = statement.name;
|
|
119
|
+
return declared !== undefined && isIdentifier(declared) && declared.text === name.text;
|
|
120
|
+
};
|
|
121
|
+
for (let at = specifier.parent;; at = at.parent) {
|
|
122
|
+
if (isBlock(at) || isModuleBlock(at) || isSourceFile(at)) {
|
|
123
|
+
let found;
|
|
124
|
+
for (const statement of at.statements.filter(names)) {
|
|
125
|
+
if (isImportEqualsDeclaration(statement)) {
|
|
126
|
+
return HOLDS_VALUES;
|
|
127
|
+
}
|
|
128
|
+
found = Math.max(found ?? HOLDS_NOTHING, instanceState(statement, visited));
|
|
129
|
+
if (found === HOLDS_VALUES) {
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (found !== undefined) {
|
|
134
|
+
return found;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (isSourceFile(at)) {
|
|
138
|
+
return HOLDS_VALUES;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Whether the compiler counts one statement as code that runs (`isPotentiallyExecutableNode`):
|
|
144
|
+
* any statement but a block or an empty one, a `var` only where it initializes a name, and a
|
|
145
|
+
* class, an enum or a namespace.
|
|
146
|
+
*/
|
|
147
|
+
function executable(node) {
|
|
148
|
+
if (isVariableStatement(node)) {
|
|
149
|
+
const list = node.declarationList;
|
|
150
|
+
return ((list.flags & NodeFlags.BlockScoped) !== 0 ||
|
|
151
|
+
list.declarations.some((one) => one.initializer !== undefined));
|
|
152
|
+
}
|
|
153
|
+
return ((node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement) ||
|
|
154
|
+
isClassDeclaration(node) ||
|
|
155
|
+
isEnumDeclaration(node) ||
|
|
156
|
+
isModuleDeclaration(node));
|
|
72
157
|
}
|
|
73
158
|
/**
|
|
74
|
-
* Whether an
|
|
75
|
-
*
|
|
159
|
+
* Whether an unreached statement is a finding, by the checker's exclusions
|
|
160
|
+
* (`isSourceElementUnreachable`): a const enum only while const enums are preserved, and a
|
|
161
|
+
* namespace only where its own declaration holds a value, or preserved const enums.
|
|
76
162
|
*/
|
|
77
|
-
function
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
isEmptyStatement(statement)) {
|
|
163
|
+
function reportable(statement, constEnumsPreserved) {
|
|
164
|
+
if (isEnumDeclaration(statement)) {
|
|
165
|
+
return constEnumsPreserved || !hasModifier(statement, SyntaxKind.ConstKeyword);
|
|
166
|
+
}
|
|
167
|
+
if (!isModuleDeclaration(statement)) {
|
|
83
168
|
return true;
|
|
84
169
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
170
|
+
const state = instanceState(statement);
|
|
171
|
+
return state === HOLDS_VALUES || (constEnumsPreserved && state === HOLDS_CONST_ENUMS);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The bodies one statement runs as part of its own flow, as the binder does
|
|
175
|
+
* (`isImmediatelyInvoked`): a non-async, non-generator function expression or arrow that the
|
|
176
|
+
* statement's whole expression calls, or each static block of a class declaration, in order.
|
|
177
|
+
* Every other function-like body starts a flow of its own.
|
|
178
|
+
*/
|
|
179
|
+
function runsInline(statement) {
|
|
180
|
+
if (isClassDeclaration(statement)) {
|
|
181
|
+
return statement.members.filter(isClassStaticBlockDeclaration).map((one) => one.body);
|
|
89
182
|
}
|
|
90
|
-
|
|
183
|
+
const call = isExpressionStatement(statement)
|
|
184
|
+
? parenthesesRemoved(statement.expression)
|
|
185
|
+
: undefined;
|
|
186
|
+
const callee = call !== undefined && isCallExpression(call) ? parenthesesRemoved(call.expression) : undefined;
|
|
187
|
+
return callee !== undefined &&
|
|
188
|
+
(isFunctionExpression(callee) || isArrowFunction(callee)) &&
|
|
189
|
+
isBlock(callee.body) &&
|
|
190
|
+
!hasModifier(callee, SyntaxKind.AsyncKeyword) &&
|
|
191
|
+
callee.asteriskToken === undefined
|
|
192
|
+
? [callee.body]
|
|
193
|
+
: [];
|
|
194
|
+
}
|
|
195
|
+
/** Whether a callee is a dotted name (`isDottedName`), whose call statement the binder's flow records. */
|
|
196
|
+
function dottedName(node) {
|
|
197
|
+
if (isPropertyAccessExpression(node) || isParenthesizedExpression(node)) {
|
|
198
|
+
return dottedName(node.expression);
|
|
199
|
+
}
|
|
200
|
+
return (isIdentifier(node) ||
|
|
201
|
+
node.kind === SyntaxKind.ThisKeyword ||
|
|
202
|
+
node.kind === SyntaxKind.SuperKeyword ||
|
|
203
|
+
node.kind === SyntaxKind.MetaProperty);
|
|
204
|
+
}
|
|
205
|
+
/** Whether an argument is the checker's constant `false` (`isFalseExpression`). */
|
|
206
|
+
function falseExpression(node) {
|
|
207
|
+
const bare = parenthesesRemoved(node);
|
|
208
|
+
if (bare.kind === SyntaxKind.FalseKeyword) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (!isBinaryExpression(bare)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const operator = bare.operatorToken.kind;
|
|
215
|
+
return operator === SyntaxKind.AmpersandAmpersandToken
|
|
216
|
+
? falseExpression(bare.left) || falseExpression(bare.right)
|
|
217
|
+
: operator === SyntaxKind.BarBarToken &&
|
|
218
|
+
falseExpression(bare.left) &&
|
|
219
|
+
falseExpression(bare.right);
|
|
220
|
+
}
|
|
221
|
+
/** Whether a node is a member of a class body (`IsClassElement`). */
|
|
222
|
+
function classElement(node) {
|
|
223
|
+
return CLASS_ELEMENTS.has(node.kind);
|
|
224
|
+
}
|
|
225
|
+
const CLASS_ELEMENTS = new Set([
|
|
226
|
+
SyntaxKind.Constructor,
|
|
227
|
+
SyntaxKind.PropertyDeclaration,
|
|
228
|
+
SyntaxKind.MethodDeclaration,
|
|
229
|
+
SyntaxKind.GetAccessor,
|
|
230
|
+
SyntaxKind.SetAccessor,
|
|
231
|
+
SyntaxKind.IndexSignature,
|
|
232
|
+
SyntaxKind.ClassStaticBlockDeclaration,
|
|
233
|
+
SyntaxKind.SemicolonClassElement,
|
|
234
|
+
]);
|
|
235
|
+
const THIS_CONTAINERS = new Set([
|
|
236
|
+
SyntaxKind.FunctionDeclaration,
|
|
237
|
+
SyntaxKind.FunctionExpression,
|
|
238
|
+
SyntaxKind.ModuleDeclaration,
|
|
239
|
+
SyntaxKind.ClassStaticBlockDeclaration,
|
|
240
|
+
SyntaxKind.PropertyDeclaration,
|
|
241
|
+
SyntaxKind.PropertySignature,
|
|
242
|
+
SyntaxKind.MethodDeclaration,
|
|
243
|
+
SyntaxKind.MethodSignature,
|
|
244
|
+
SyntaxKind.Constructor,
|
|
245
|
+
SyntaxKind.GetAccessor,
|
|
246
|
+
SyntaxKind.SetAccessor,
|
|
247
|
+
SyntaxKind.CallSignature,
|
|
248
|
+
SyntaxKind.ConstructSignature,
|
|
249
|
+
SyntaxKind.IndexSignature,
|
|
250
|
+
SyntaxKind.EnumDeclaration,
|
|
251
|
+
SyntaxKind.SourceFile,
|
|
252
|
+
]);
|
|
253
|
+
/** The node that binds a `this`, arrows passed through (`GetThisContainer`). */
|
|
254
|
+
function thisContainer(node) {
|
|
255
|
+
let at = node.parent;
|
|
256
|
+
while (at !== undefined) {
|
|
257
|
+
if (THIS_CONTAINERS.has(at.kind)) {
|
|
258
|
+
return at;
|
|
259
|
+
}
|
|
260
|
+
if (at.kind === SyntaxKind.ComputedPropertyName) {
|
|
261
|
+
at = at.parent.parent;
|
|
262
|
+
}
|
|
263
|
+
else if (at.kind === SyntaxKind.Decorator) {
|
|
264
|
+
if (at.parent.kind === SyntaxKind.Parameter && classElement(at.parent.parent)) {
|
|
265
|
+
at = at.parent.parent;
|
|
266
|
+
}
|
|
267
|
+
else if (classElement(at.parent)) {
|
|
268
|
+
at = at.parent;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
at = at.parent;
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
/** A predicate this port cannot read, whose signature then ends no call. */
|
|
276
|
+
const UNKNOWN = Symbol("unknown");
|
|
277
|
+
/**
|
|
278
|
+
* The call statements of a project's files that never return, by the checker's rule ported from
|
|
279
|
+
* the pinned typescript-go (microsoft/TypeScript 042e0fb, tsc/internal/checker/flow.go): the call
|
|
280
|
+
* arm of isReachableFlowNodeWorker (2554), getEffectsSignature (2059), getTypeOfDottedName (2134),
|
|
281
|
+
* getExplicitTypeOfSymbol (2167), hasTypePredicateOrNeverReturnType (2223), getExplicitThisType
|
|
282
|
+
* (2227); relater.go getTypePredicateOfSignature (2052). What the API hides or leaves unanswered
|
|
283
|
+
* (a mapped member's origin, a union signature's predicate) ends nothing.
|
|
284
|
+
*/
|
|
285
|
+
function neverReturning(project, files) {
|
|
286
|
+
const { queries } = project;
|
|
287
|
+
const statements = [];
|
|
288
|
+
const visit = (node) => {
|
|
289
|
+
if (isExpressionStatement(node) &&
|
|
290
|
+
isCallExpression(node.expression) &&
|
|
291
|
+
node.expression.expression.kind !== SyntaxKind.SuperKeyword &&
|
|
292
|
+
dottedName(node.expression.expression)) {
|
|
293
|
+
statements.push(node);
|
|
294
|
+
}
|
|
295
|
+
node.forEachChild(visit);
|
|
296
|
+
};
|
|
297
|
+
files.forEach(visit);
|
|
298
|
+
const known = (answer) => answer === undefined || !isAnswered(answer) ? undefined : answer;
|
|
299
|
+
const nodeOf = (handle) => handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
300
|
+
const symbolAt = (node) => known(project.symbolsAt([project.handle(node)])[0]);
|
|
301
|
+
const never = (type) => type !== undefined && (type.flags & TypeFlags.Never) !== 0;
|
|
302
|
+
// resolvingExplicitTypeOfSymbol (flow.go:2169): the for-of arm recurses through the iterated
|
|
303
|
+
// name, so loops whose iterables name each other's variables would recurse forever. A call
|
|
304
|
+
// through such a variable still reaches this walk where narrowing types it `never`. A symbol
|
|
305
|
+
// met again on the walk has no explicit type, so its call ends nothing.
|
|
306
|
+
const resolving = new Set();
|
|
307
|
+
const explicitTypeOf = (given) => {
|
|
308
|
+
// IsNonLocalAlias: an alias with no meaning of its own, or one an assignment declares.
|
|
309
|
+
const aliased = (given.flags & SymbolFlags.Alias) !== 0 &&
|
|
310
|
+
((given.flags & (SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace)) === 0 ||
|
|
311
|
+
(given.flags & SymbolFlags.Assignment) !== 0);
|
|
312
|
+
const symbol = aliased ? known(queries.aliased(given)) : given;
|
|
313
|
+
if (symbol === undefined || resolving.has(symbol.id)) {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
resolving.add(symbol.id);
|
|
317
|
+
try {
|
|
318
|
+
if ((symbol.flags &
|
|
319
|
+
(SymbolFlags.Function |
|
|
320
|
+
SymbolFlags.Method |
|
|
321
|
+
SymbolFlags.Class |
|
|
322
|
+
SymbolFlags.ValueModule)) !==
|
|
323
|
+
0) {
|
|
324
|
+
return known(queries.typeOfSymbol(symbol));
|
|
325
|
+
}
|
|
326
|
+
if ((symbol.flags & (SymbolFlags.Variable | SymbolFlags.Property)) === 0) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
// A mapped member declares no value of its own, and the API does not expose its origin.
|
|
330
|
+
const declaration = nodeOf(symbol.valueDeclaration);
|
|
331
|
+
if (declaration === undefined) {
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
if (explicitlyTyped(declaration)) {
|
|
335
|
+
return known(queries.typeOfSymbol(symbol));
|
|
336
|
+
}
|
|
337
|
+
if (isVariableDeclaration(declaration) && isForOfStatement(declaration.parent.parent)) {
|
|
338
|
+
const { expression } = declaration.parent.parent;
|
|
339
|
+
const iterated = dottedTypeOf(expression);
|
|
340
|
+
const held = iterated === undefined ? undefined : known(queries.typeAt(expression));
|
|
341
|
+
// The variable's own type iterates the expression's type there, which is the checker's
|
|
342
|
+
// iteration of the dotted name's type where the two are one type.
|
|
343
|
+
return held !== undefined && held.id === iterated?.id
|
|
344
|
+
? known(queries.typeOfSymbol(symbol))
|
|
345
|
+
: undefined;
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
resolving.delete(symbol.id);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const propertyOf = (type, name) => {
|
|
354
|
+
const properties = known(queries.propertiesOf(type));
|
|
355
|
+
if (properties === undefined) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
if (isIdentifier(name)) {
|
|
359
|
+
return properties.find((one) => one.name === name.text);
|
|
360
|
+
}
|
|
361
|
+
if (!isPrivateIdentifier(name)) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
// getTypeOfDottedName (flow.go:2149) keys a private name by the receiver type's own symbol,
|
|
365
|
+
// so only that class's member answers, and a type with no symbol has none. The lexical
|
|
366
|
+
// member's key is that key exactly where its class is the receiver type's symbol.
|
|
367
|
+
const owner = known(queries.symbolOfType(type));
|
|
368
|
+
const declared = owner === undefined ? undefined : symbolAt(name);
|
|
369
|
+
if (owner === undefined || declared === undefined) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
return known(queries.parentOf(declared))?.id === owner.id
|
|
373
|
+
? properties.find((one) => one.name === declared.name)
|
|
374
|
+
: undefined;
|
|
375
|
+
};
|
|
376
|
+
// getSignatureFromDeclaration's `this` parameter (checker.go:20510, 20529): the container's
|
|
377
|
+
// own or, for an accessor without one, its pair's, which getAccessorThisParameter takes only
|
|
378
|
+
// from a getter with one parameter or a setter with two.
|
|
379
|
+
const thisParameterOf = (container) => {
|
|
380
|
+
const [own] = container.parameters;
|
|
381
|
+
if (own !== undefined && symbolName(own) === "this") {
|
|
382
|
+
return own;
|
|
383
|
+
}
|
|
384
|
+
const getter = isGetAccessorDeclaration(container);
|
|
385
|
+
if (!getter && !isSetAccessorDeclaration(container)) {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
const other = getter ? isSetAccessorDeclaration : isGetAccessorDeclaration;
|
|
389
|
+
const pair = symbolAt(container.name)
|
|
390
|
+
?.declarations.map(nodeOf)
|
|
391
|
+
.find((one) => one !== undefined && other(one));
|
|
392
|
+
const first = pair?.parameters[0];
|
|
393
|
+
return first !== undefined &&
|
|
394
|
+
pair?.parameters.length === (getter ? 2 : 1) &&
|
|
395
|
+
symbolName(first) === "this"
|
|
396
|
+
? first
|
|
397
|
+
: undefined;
|
|
398
|
+
};
|
|
399
|
+
const explicitThisTypeOf = (node) => {
|
|
400
|
+
const container = thisContainer(node);
|
|
401
|
+
if (container === undefined) {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
if (isFunctionLikeDeclaration(container)) {
|
|
405
|
+
const signature = known(queries.signatureOf(container));
|
|
406
|
+
if (signature === undefined) {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
if (signature.thisParameter !== undefined) {
|
|
410
|
+
const declaration = thisParameterOf(container);
|
|
411
|
+
const parameter = declaration === undefined ? undefined : symbolAt(declaration.name);
|
|
412
|
+
return parameter === undefined ? undefined : explicitTypeOf(parameter);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const owner = container.parent;
|
|
416
|
+
const name = owner !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
|
|
417
|
+
? owner.name
|
|
418
|
+
: undefined;
|
|
419
|
+
const symbol = name === undefined ? undefined : symbolAt(name);
|
|
420
|
+
if (symbol === undefined) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
return isClassStaticBlockDeclaration(container) ||
|
|
424
|
+
(classElement(container) && hasModifier(container, SyntaxKind.StaticKeyword))
|
|
425
|
+
? known(queries.typeOfSymbol(symbol))
|
|
426
|
+
: known(queries.declaredTypeOf(symbol));
|
|
427
|
+
};
|
|
428
|
+
const dottedTypeOf = (node) => {
|
|
429
|
+
if ((node.flags & NodeFlags.InWithStatement) !== 0) {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
if (isIdentifier(node)) {
|
|
433
|
+
// A name's value meaning finds a module member's export symbol, which is
|
|
434
|
+
// getExportSymbolOfValueSymbolIfExported over its resolved symbol.
|
|
435
|
+
const symbol = symbolAt(node);
|
|
436
|
+
return symbol === undefined ? undefined : explicitTypeOf(symbol);
|
|
437
|
+
}
|
|
438
|
+
if (node.kind === SyntaxKind.ThisKeyword) {
|
|
439
|
+
return explicitThisTypeOf(node);
|
|
440
|
+
}
|
|
441
|
+
if (node.kind === SyntaxKind.SuperKeyword) {
|
|
442
|
+
return known(queries.typeAt(node));
|
|
443
|
+
}
|
|
444
|
+
if (isPropertyAccessExpression(node)) {
|
|
445
|
+
const type = dottedTypeOf(node.expression);
|
|
446
|
+
const property = type === undefined ? undefined : propertyOf(type, node.name);
|
|
447
|
+
return property === undefined ? undefined : explicitTypeOf(property);
|
|
448
|
+
}
|
|
449
|
+
return isParenthesizedExpression(node) ? dottedTypeOf(node.expression) : undefined;
|
|
450
|
+
};
|
|
451
|
+
const predicateOf = (signature, declaration) => {
|
|
452
|
+
const declared = known(queries.signatureOf(declaration));
|
|
453
|
+
let at = signature;
|
|
454
|
+
for (let step = 0; at !== undefined && at.id !== declared?.id; step++) {
|
|
455
|
+
at = step < SIGNATURE_TARGET_DEPTH ? known(queries.genericOf(at)) : undefined;
|
|
456
|
+
}
|
|
457
|
+
// A union's signature carries its first member's declaration and its own predicate.
|
|
458
|
+
if (at === undefined) {
|
|
459
|
+
return UNKNOWN;
|
|
460
|
+
}
|
|
461
|
+
const type = declaration.type;
|
|
462
|
+
if (type === undefined || !isTypePredicateNode(type)) {
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
const name = type.parameterName;
|
|
466
|
+
const parameters = declaration.parameters ?? [];
|
|
467
|
+
const named = parameters.filter((one, index) => !(index === 0 && symbolName(one) === "this"));
|
|
468
|
+
return {
|
|
469
|
+
asserts: type.assertsModifier !== undefined,
|
|
470
|
+
typed: type.type !== undefined,
|
|
471
|
+
index: name.kind === SyntaxKind.ThisType
|
|
472
|
+
? -1
|
|
473
|
+
: named.findIndex((one) => isIdentifier(name) && symbolName(one) === name.text),
|
|
474
|
+
};
|
|
475
|
+
};
|
|
476
|
+
// getReturnTypeFromAnnotation (checker.go:20705). A constructor's result is its instance type,
|
|
477
|
+
// which is not never, and a declaration with no type node writes no never result: a JavaScript
|
|
478
|
+
// type tag's signature is declared by the tag's own function type, which carries the annotation.
|
|
479
|
+
const annotatedNever = (declaration) => {
|
|
480
|
+
if (declaration.type === undefined) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
const declared = known(queries.signatureOf(declaration));
|
|
484
|
+
return declared !== undefined && never(known(queries.returnTypeOf(declared)));
|
|
485
|
+
};
|
|
486
|
+
// Whether a signature has a predicate or writes `never` as its result, where the API tells.
|
|
487
|
+
const marked = (signature) => {
|
|
488
|
+
const declaration = nodeOf(signature.declaration);
|
|
489
|
+
if (declaration === undefined) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
const predicate = predicateOf(signature, declaration);
|
|
493
|
+
return predicate !== UNKNOWN && (predicate !== undefined || annotatedNever(declaration));
|
|
494
|
+
};
|
|
495
|
+
const effectsOf = (call) => {
|
|
496
|
+
const type = dottedTypeOf(call.expression);
|
|
497
|
+
const signatures = type === undefined ? undefined : known(queries.signaturesOf(type, SignatureKind.Call));
|
|
498
|
+
if (signatures === undefined) {
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
const [only] = signatures;
|
|
502
|
+
const chosen = signatures.length === 1 && only !== undefined && (only.typeParameters?.length ?? 0) === 0
|
|
503
|
+
? only
|
|
504
|
+
: signatures.some(marked)
|
|
505
|
+
? known(queries.resolvedSignature(call))
|
|
506
|
+
: undefined;
|
|
507
|
+
return chosen !== undefined && marked(chosen) ? chosen : undefined;
|
|
508
|
+
};
|
|
509
|
+
// The effects signature's result is the call's own type, so a call typed neither `never` nor
|
|
510
|
+
// `void` with a false argument cannot end, and is never asked about.
|
|
511
|
+
const types = statements.length === 0 ? [] : queries.typesAt(statements.map((one) => one.expression));
|
|
512
|
+
const ends = new Set();
|
|
513
|
+
statements.forEach((statement, at) => {
|
|
514
|
+
const type = known(types[at]);
|
|
515
|
+
const call = statement.expression;
|
|
516
|
+
if (type === undefined ||
|
|
517
|
+
(!never(type) &&
|
|
518
|
+
((type.flags & TypeFlags.Void) === 0 || !call.arguments.some(falseExpression)))) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const effects = effectsOf(call);
|
|
522
|
+
const declaration = effects === undefined ? undefined : nodeOf(effects.declaration);
|
|
523
|
+
if (effects === undefined || declaration === undefined) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
// The checker ends a bare `asserts value` call whose argument there is false.
|
|
527
|
+
const predicate = predicateOf(effects, declaration);
|
|
528
|
+
const asserted = predicate !== UNKNOWN && predicate?.asserts === true && !predicate.typed
|
|
529
|
+
? call.arguments[predicate.index]
|
|
530
|
+
: undefined;
|
|
531
|
+
if ((asserted !== undefined && falseExpression(asserted)) ||
|
|
532
|
+
never(known(queries.returnTypeOf(effects)))) {
|
|
533
|
+
ends.add(statement);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
return ends;
|
|
537
|
+
}
|
|
538
|
+
/** Whether a declaration writes its own type (`isDeclarationWithExplicitTypeAnnotation`, flow.go:2209). */
|
|
539
|
+
function explicitlyTyped(declaration) {
|
|
540
|
+
if (isVariableDeclaration(declaration) ||
|
|
541
|
+
isPropertyDeclaration(declaration) ||
|
|
542
|
+
isPropertySignatureDeclaration(declaration) ||
|
|
543
|
+
isParameterDeclaration(declaration)) {
|
|
544
|
+
return declaration.type !== undefined;
|
|
545
|
+
}
|
|
546
|
+
// An expando property is typed by the function it is assigned, where that writes its result.
|
|
547
|
+
return (isBinaryExpression(declaration) &&
|
|
548
|
+
(isFunctionExpression(declaration.right) || isArrowFunction(declaration.right)) &&
|
|
549
|
+
declaration.right.type !== undefined);
|
|
550
|
+
}
|
|
551
|
+
/** The name a declaration's symbol carries, for an identifier-named one. */
|
|
552
|
+
function symbolName(declaration) {
|
|
553
|
+
const name = declaration.name;
|
|
554
|
+
return name !== undefined && isIdentifier(name) ? name.text : undefined;
|
|
555
|
+
}
|
|
556
|
+
/** How many instantiations deep a signature's generic is followed to its declaration's own. */
|
|
557
|
+
const SIGNATURE_TARGET_DEPTH = 8;
|
|
558
|
+
/**
|
|
559
|
+
* The statements of one file the compiler counts as code that runs and that no path from the
|
|
560
|
+
* start of their own flow reaches: the file's, a namespace body's, or a function-like body's
|
|
561
|
+
* that does not run inline. The walk is `flowWalk`'s with each statement a target of its own,
|
|
562
|
+
* and a construct it does not follow leaves every statement inside it reached.
|
|
563
|
+
*/
|
|
564
|
+
function unreachedOf(file, ends) {
|
|
565
|
+
const flows = [];
|
|
566
|
+
const inline = new Set();
|
|
567
|
+
const collect = (node, members) => {
|
|
568
|
+
for (const body of runsInline(node)) {
|
|
569
|
+
inline.add(body);
|
|
570
|
+
}
|
|
571
|
+
node.forEachChild((child) => {
|
|
572
|
+
const own = isModuleBlock(child)
|
|
573
|
+
? child.statements
|
|
574
|
+
: isBlock(child) &&
|
|
575
|
+
!inline.has(child) &&
|
|
576
|
+
(isFunctionLikeDeclaration(node) || isClassStaticBlockDeclaration(node))
|
|
577
|
+
? child.statements
|
|
578
|
+
: undefined;
|
|
579
|
+
if (own !== undefined) {
|
|
580
|
+
const inner = [];
|
|
581
|
+
flows.push({ statements: own, members: inner });
|
|
582
|
+
collect(child, inner);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (executable(child)) {
|
|
586
|
+
members.push(child);
|
|
587
|
+
}
|
|
588
|
+
collect(child, members);
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
const top = [];
|
|
592
|
+
flows.push({ statements: file.statements, members: top });
|
|
593
|
+
collect(file, top);
|
|
594
|
+
const unreached = new Set();
|
|
595
|
+
for (const { statements, members } of flows) {
|
|
596
|
+
// Members are in source order, so those inside one node are one run of bits.
|
|
597
|
+
const first = (pos) => {
|
|
598
|
+
let low = 0;
|
|
599
|
+
for (let high = members.length; low < high;) {
|
|
600
|
+
const middle = (low + high) >> 1;
|
|
601
|
+
if ((members[middle]?.pos ?? pos) < pos) {
|
|
602
|
+
low = middle + 1;
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
high = middle;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return low;
|
|
609
|
+
};
|
|
610
|
+
const inside = (node) => {
|
|
611
|
+
if (node === undefined) {
|
|
612
|
+
return NOTHING;
|
|
613
|
+
}
|
|
614
|
+
let low = first(node.pos);
|
|
615
|
+
const high = first(node.end);
|
|
616
|
+
while (low < high && (members[low]?.end ?? 0) > node.end) {
|
|
617
|
+
low += 1;
|
|
618
|
+
}
|
|
619
|
+
return low >= high ? NOTHING : ((1n << BigInt(high - low)) - 1n) << BigInt(low);
|
|
620
|
+
};
|
|
621
|
+
const indexes = new Map(members.map((one, at) => [one, at]));
|
|
622
|
+
const walk = flowWalk({
|
|
623
|
+
inside,
|
|
624
|
+
at: (statement) => {
|
|
625
|
+
const index = indexes.get(statement);
|
|
626
|
+
return index === undefined ? NOTHING : 1n << BigInt(index);
|
|
627
|
+
},
|
|
628
|
+
writes: [],
|
|
629
|
+
ends,
|
|
630
|
+
});
|
|
631
|
+
const reached = walk.sequence(statements, NOTHING, {
|
|
632
|
+
exit: NOTHING,
|
|
633
|
+
raise: NOTHING,
|
|
634
|
+
escape: NOTHING,
|
|
635
|
+
labels: new Map(),
|
|
636
|
+
});
|
|
637
|
+
const digits = reached.toString(2);
|
|
638
|
+
members.forEach((member, at) => {
|
|
639
|
+
if (digits[digits.length - 1 - at] !== "1") {
|
|
640
|
+
unreached.add(member);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return unreached;
|
|
91
645
|
}
|
|
92
646
|
/** The value a case expression is a literal of, as a key two equal literals share, or none. */
|
|
93
647
|
function caseKey(expression) {
|
|
@@ -190,38 +744,61 @@ function returnsAValue(signature) {
|
|
|
190
744
|
function hasModifier(node, kind) {
|
|
191
745
|
return (node.modifiers ?? []).some((modifier) => modifier.kind === kind);
|
|
192
746
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
747
|
+
/** How many alias links a heritage name is followed through, one per re-export. */
|
|
748
|
+
const ALIAS_DEPTH = 16;
|
|
749
|
+
/** The last identifier of one heritage type: a class's clause holds expressions, an interface's may hold type references. */
|
|
750
|
+
function heritageName(type) {
|
|
751
|
+
if (isTypeReferenceNode(type)) {
|
|
752
|
+
const named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
|
|
753
|
+
return isIdentifier(named) ? named : undefined;
|
|
754
|
+
}
|
|
755
|
+
return isExpressionWithTypeArguments(type) ? spelledName(type.expression) : undefined;
|
|
756
|
+
}
|
|
757
|
+
/** What every heritage clause of the project's files names, asked of the checker in one batch. */
|
|
758
|
+
function heritageOf(project, files) {
|
|
759
|
+
const named = [];
|
|
196
760
|
const visit = (node) => {
|
|
197
|
-
if (isHeritageClause(node)
|
|
198
|
-
|
|
199
|
-
// A class's clause holds expressions; an interface's holds type references.
|
|
200
|
-
let named;
|
|
201
|
-
if (isTypeReferenceNode(type)) {
|
|
202
|
-
named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
const expression = parenthesesRemoved(type.expression);
|
|
206
|
-
named = isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
207
|
-
}
|
|
208
|
-
if (isIdentifier(named)) {
|
|
209
|
-
names.add(named.text);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
761
|
+
if (isHeritageClause(node)) {
|
|
762
|
+
named.push(...node.types.flatMap((type) => heritageName(type) ?? []));
|
|
212
763
|
}
|
|
213
764
|
node.forEachChild(visit);
|
|
214
765
|
};
|
|
215
766
|
for (const file of files) {
|
|
216
767
|
file.forEachChild(visit);
|
|
217
768
|
}
|
|
218
|
-
|
|
769
|
+
const ids = new Set();
|
|
770
|
+
const unanswered = new Set();
|
|
771
|
+
project.symbolsAt(named.map((name) => project.handle(name))).forEach((answer, at) => {
|
|
772
|
+
let symbol = answer;
|
|
773
|
+
for (let step = 0; step < ALIAS_DEPTH && symbol !== undefined && symbol !== UNANSWERED; step++) {
|
|
774
|
+
const next = project.aliasStepOf(symbol);
|
|
775
|
+
if (next === undefined) {
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
symbol = next;
|
|
779
|
+
}
|
|
780
|
+
if (symbol === UNANSWERED) {
|
|
781
|
+
unanswered.add(named[at]?.text ?? "");
|
|
782
|
+
}
|
|
783
|
+
else if (symbol !== undefined) {
|
|
784
|
+
ids.add(symbol.id);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
return { ids, names: new Set(named.map((name) => name.text)), unanswered };
|
|
788
|
+
}
|
|
789
|
+
/** The names a class is known by: its own, and the variable a class expression initializes. */
|
|
790
|
+
function classNames(owner) {
|
|
791
|
+
const own = owner.name;
|
|
792
|
+
const holder = isClassExpression(owner) ? outerParent(owner) : undefined;
|
|
793
|
+
const held = holder !== undefined && isVariableDeclaration(holder) ? holder.name : undefined;
|
|
794
|
+
return [own, held].filter((name) => name !== undefined && isIdentifier(name));
|
|
219
795
|
}
|
|
220
796
|
/**
|
|
221
797
|
* Whether a class hierarchy fixes one method's signature: its class extends or implements
|
|
222
|
-
* another type, is abstract or is
|
|
798
|
+
* another type, is abstract, or is a type a clause of the project extends or implements,
|
|
799
|
+
* or the method is marked `override`.
|
|
223
800
|
*/
|
|
224
|
-
function hierarchyFixes(declared,
|
|
801
|
+
function hierarchyFixes(declared, heritage, ids) {
|
|
225
802
|
if (!isMethodDeclaration(declared)) {
|
|
226
803
|
return false;
|
|
227
804
|
}
|
|
@@ -230,13 +807,16 @@ function hierarchyFixes(declared, extended) {
|
|
|
230
807
|
return false;
|
|
231
808
|
}
|
|
232
809
|
if (hasModifier(declared, SyntaxKind.OverrideKeyword) ||
|
|
233
|
-
hasModifier(owner, SyntaxKind.AbstractKeyword)
|
|
810
|
+
hasModifier(owner, SyntaxKind.AbstractKeyword) ||
|
|
811
|
+
(owner.heritageClauses ?? []).length > 0) {
|
|
234
812
|
return true;
|
|
235
813
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
814
|
+
return classNames(owner).some((name) => {
|
|
815
|
+
const id = ids.get(name);
|
|
816
|
+
return id === undefined || id === UNANSWERED
|
|
817
|
+
? heritage.names.has(name.text)
|
|
818
|
+
: heritage.ids.has(id) || heritage.unanswered.has(name.text);
|
|
819
|
+
});
|
|
240
820
|
}
|
|
241
821
|
/** Whether a decorator on a constructor's class or on one of its parameters fixes its signature. */
|
|
242
822
|
function decoratorFixes(constructor) {
|
|
@@ -326,6 +906,15 @@ function checkedByTheCompiler(file, checkJs) {
|
|
|
326
906
|
}
|
|
327
907
|
return (!/\.[cm]?jsx?$/u.test(file.fileName) || checkJs || hasPragma(leading, /^\s*@ts-check(?:\s|$)/iu));
|
|
328
908
|
}
|
|
909
|
+
/** Whether one child of a node is a statement it runs in no statement list: a branch or a body. */
|
|
910
|
+
function inStatementPosition(node, child) {
|
|
911
|
+
if (isIfStatement(node)) {
|
|
912
|
+
return child === node.thenStatement || child === node.elseStatement;
|
|
913
|
+
}
|
|
914
|
+
// The body of a `do`, a label, a `for…of`, a `for…in` or a `with` runs whenever its
|
|
915
|
+
// statement does, so only these can start an unreachable run of their own.
|
|
916
|
+
return (isWhileStatement(node) || isForStatement(node)) && child === node.statement;
|
|
917
|
+
}
|
|
329
918
|
/**
|
|
330
919
|
* The parts of one project's own declarations. `references` are the project's own
|
|
331
920
|
* references, which say which declarations a discarded call reaches, so the checker is
|
|
@@ -333,7 +922,7 @@ function checkedByTheCompiler(file, checkJs) {
|
|
|
333
922
|
*/
|
|
334
923
|
export function projectParts(project, held, references, targetRoot) {
|
|
335
924
|
const files = project.ownSourceFiles();
|
|
336
|
-
const
|
|
925
|
+
const heritage = heritageOf(project, files);
|
|
337
926
|
const parts = [];
|
|
338
927
|
const signatures = [];
|
|
339
928
|
const calls = new Map();
|
|
@@ -345,7 +934,14 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
345
934
|
const refs = new Map(held.symbols.map((symbol) => [symbol.id, symbol.ref]));
|
|
346
935
|
const options = project.program.getCompilerOptions();
|
|
347
936
|
const jsx = options;
|
|
937
|
+
// The compiler's `shouldPreserveConstEnums`, whose isolated modules include `verbatimModuleSyntax`.
|
|
938
|
+
const constEnumsPreserved = options.preserveConstEnums === true ||
|
|
939
|
+
options.isolatedModules === true ||
|
|
940
|
+
options.verbatimModuleSyntax === true;
|
|
941
|
+
const ends = neverReturning(project, files);
|
|
942
|
+
const judged = new Set();
|
|
348
943
|
for (const file of files) {
|
|
944
|
+
judged.add(renderPosition(file, targetRoot, 0).path);
|
|
349
945
|
const checked = checkedByTheCompiler(file, options.checkJs === true);
|
|
350
946
|
// A parameter property's own parts stay with the parameter when its member goes.
|
|
351
947
|
const declarationAt = (node) => isParameterProperty(node) ? undefined : held.declarations.get(nodeKey(file, node));
|
|
@@ -371,26 +967,24 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
371
967
|
};
|
|
372
968
|
const covered = new Set();
|
|
373
969
|
const position = (node) => positionKey(renderPosition(file, targetRoot, node.getStart()));
|
|
374
|
-
// Code
|
|
375
|
-
// and reports no part of its own: the
|
|
970
|
+
// Code inside a run of unreachable statements is still walked for its calls and
|
|
971
|
+
// specifiers, and reports no part of its own: the run's first statement is the one
|
|
972
|
+
// finding, as the compiler reports one diagnostic per run.
|
|
973
|
+
const unreached = unreachedOf(file, ends);
|
|
974
|
+
const unreachable = (statement) => unreached.has(statement) && reportable(statement, constEnumsPreserved);
|
|
376
975
|
let silenced = 0;
|
|
377
976
|
const statementsOf = (statements) => {
|
|
378
|
-
let
|
|
379
|
-
let reported = false;
|
|
977
|
+
let inRun = false;
|
|
380
978
|
for (const statement of statements) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
const holder = holderOf(statement);
|
|
387
|
-
if (silenced === 0 && !reported && !unreachableExempt(statement) && holder !== undefined) {
|
|
979
|
+
const unreached = unreachable(statement);
|
|
980
|
+
const holder = unreached && !inRun && silenced === 0 ? holderOf(statement) : undefined;
|
|
981
|
+
if (holder !== undefined) {
|
|
388
982
|
walk.unreachable.push({ statement, holder });
|
|
389
|
-
reported = true;
|
|
390
983
|
}
|
|
391
|
-
|
|
984
|
+
inRun = unreached;
|
|
985
|
+
silenced += unreached ? 1 : 0;
|
|
392
986
|
visit(statement);
|
|
393
|
-
silenced -= 1;
|
|
987
|
+
silenced -= unreached ? 1 : 0;
|
|
394
988
|
}
|
|
395
989
|
};
|
|
396
990
|
const visit = (node) => {
|
|
@@ -458,7 +1052,12 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
458
1052
|
}
|
|
459
1053
|
}
|
|
460
1054
|
}
|
|
461
|
-
|
|
1055
|
+
// A block whose first statement no flow reaches is never entered: the compiler
|
|
1056
|
+
// reports the run from inside it, and its locals hold no store of their own.
|
|
1057
|
+
if (isBlock(node) &&
|
|
1058
|
+
silenced === 0 &&
|
|
1059
|
+
(node.statements[0] === undefined || !unreachable(node.statements[0])) &&
|
|
1060
|
+
holderOf(node) !== undefined) {
|
|
462
1061
|
walk.blocks.push(node);
|
|
463
1062
|
}
|
|
464
1063
|
const loopHolder = (isForOfStatement(node) || isForInStatement(node)) && silenced === 0
|
|
@@ -470,13 +1069,21 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
470
1069
|
(node.initializer.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0) {
|
|
471
1070
|
walk.loops.push({ loop: node, holder: loopHolder });
|
|
472
1071
|
}
|
|
473
|
-
if (isBlock(node) || isCaseClause(node) || isDefaultClause(node)) {
|
|
1072
|
+
if (isBlock(node) || isCaseClause(node) || isDefaultClause(node) || isModuleBlock(node)) {
|
|
474
1073
|
statementsOf(node.statements);
|
|
475
1074
|
return;
|
|
476
1075
|
}
|
|
477
|
-
|
|
1076
|
+
// A statement that is a branch or a body of another, in no list, is a run of its own.
|
|
1077
|
+
node.forEachChild((child) => {
|
|
1078
|
+
if (inStatementPosition(node, child)) {
|
|
1079
|
+
statementsOf([child]);
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
visit(child);
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
478
1085
|
};
|
|
479
|
-
file.
|
|
1086
|
+
statementsOf(file.statements);
|
|
480
1087
|
// One batch per file resolves every name a parameter or a local is read through.
|
|
481
1088
|
const asked = [];
|
|
482
1089
|
const readers = new Map();
|
|
@@ -509,6 +1116,15 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
509
1116
|
asked.push(...names, ...uses);
|
|
510
1117
|
return { names, uses, holder };
|
|
511
1118
|
});
|
|
1119
|
+
const measured = lengthReads(file);
|
|
1120
|
+
const measuredNames = new Set(measured.map((one) => one.text));
|
|
1121
|
+
const named = walk.declared.flatMap(({ node }) => {
|
|
1122
|
+
const name = node.name;
|
|
1123
|
+
return name !== undefined && isIdentifier(name) && measuredNames.has(name.text) ? [name] : [];
|
|
1124
|
+
});
|
|
1125
|
+
asked.push(...measured, ...named);
|
|
1126
|
+
const owners = walk.declared.flatMap(({ node }) => isMethodDeclaration(node) ? classNames(node.parent) : []);
|
|
1127
|
+
asked.push(...owners);
|
|
512
1128
|
const bindings = fileId === undefined ? [] : importBindings(file);
|
|
513
1129
|
const bound = new Set(bindings);
|
|
514
1130
|
const bindingUses = bindings.length === 0
|
|
@@ -538,11 +1154,11 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
538
1154
|
signatures.push({
|
|
539
1155
|
declaration: one.id,
|
|
540
1156
|
stub: isStub(one.signature),
|
|
541
|
-
fixed: constructor ? decoratorFixes(one.node) : hierarchyFixes(one.node,
|
|
1157
|
+
fixed: constructor ? decoratorFixes(one.node) : hierarchyFixes(one.node, heritage, ids),
|
|
542
1158
|
constructor,
|
|
543
1159
|
});
|
|
544
1160
|
const reader = readers.get(one.signature);
|
|
545
|
-
if (reader !== undefined) {
|
|
1161
|
+
if (reader !== undefined && !lengthRead(one.node, measured, ids)) {
|
|
546
1162
|
const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, {
|
|
547
1163
|
inValue: false,
|
|
548
1164
|
checked,
|
|
@@ -562,68 +1178,105 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
562
1178
|
contextual.push(...found.contextual);
|
|
563
1179
|
}
|
|
564
1180
|
}
|
|
1181
|
+
const part = (declaration, kind, name, node) => ({
|
|
1182
|
+
declaration,
|
|
1183
|
+
kind,
|
|
1184
|
+
name,
|
|
1185
|
+
position: spanOf(file, targetRoot, node),
|
|
1186
|
+
});
|
|
565
1187
|
walk.catches.forEach(({ name, holder }, at) => {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
kind: "store",
|
|
570
|
-
name: name.text,
|
|
571
|
-
position: spanOf(file, targetRoot, name),
|
|
572
|
-
message: `value written to ${name.text} is never read`,
|
|
573
|
-
used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
|
|
574
|
-
});
|
|
1188
|
+
if (!(caught[at] ?? []).some((use) => readsBinding(use, name, ids))) {
|
|
1189
|
+
parts.push(part(holder, "store", name.text, name));
|
|
1190
|
+
}
|
|
575
1191
|
});
|
|
576
1192
|
for (const { names, uses, holder } of loopVariables) {
|
|
577
1193
|
for (const name of names) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
kind: "store",
|
|
582
|
-
name: name.text,
|
|
583
|
-
position: spanOf(file, targetRoot, name),
|
|
584
|
-
message: `value written to ${name.text} is never read`,
|
|
585
|
-
used: uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
|
|
586
|
-
});
|
|
1194
|
+
if (!uses.some((use) => use.text === name.text && readsBinding(use, name, ids))) {
|
|
1195
|
+
parts.push(part(holder, "store", name.text, name));
|
|
1196
|
+
}
|
|
587
1197
|
}
|
|
588
1198
|
}
|
|
589
1199
|
walk.blocks.forEach((block, at) => {
|
|
590
1200
|
const holder = holderOf(block);
|
|
591
1201
|
if (holder !== undefined) {
|
|
592
|
-
|
|
1202
|
+
for (const { local, target } of deadStores(block, stores[at] ?? [], ids, ends)) {
|
|
1203
|
+
parts.push(part(holder, "store", local.name.text, target));
|
|
1204
|
+
}
|
|
593
1205
|
}
|
|
594
1206
|
});
|
|
595
1207
|
for (const { statement, holder } of walk.unreachable) {
|
|
596
|
-
parts.push(
|
|
597
|
-
code: CODE.unreachableStatement,
|
|
598
|
-
declaration: holder,
|
|
599
|
-
kind: "statement",
|
|
600
|
-
name: "statement",
|
|
601
|
-
position: spanOf(file, targetRoot, statement),
|
|
602
|
-
message: "statement is unreachable",
|
|
603
|
-
used: false,
|
|
604
|
-
});
|
|
1208
|
+
parts.push(part(holder, "statement", "statement", statement));
|
|
605
1209
|
}
|
|
606
1210
|
for (const { clause, text, holder } of walk.cases) {
|
|
607
|
-
parts.push(
|
|
608
|
-
code: CODE.unreachableCase,
|
|
609
|
-
declaration: holder,
|
|
610
|
-
kind: "case",
|
|
611
|
-
name: text,
|
|
612
|
-
position: spanOf(file, targetRoot, clause),
|
|
613
|
-
message: `case ${text} is covered by an earlier case of the same switch`,
|
|
614
|
-
used: false,
|
|
615
|
-
});
|
|
1211
|
+
parts.push(part(holder, "case", text, clause));
|
|
616
1212
|
}
|
|
617
1213
|
}
|
|
618
|
-
|
|
1214
|
+
const results = resultParts(project, targetRoot, sites, references, calls);
|
|
619
1215
|
return {
|
|
620
|
-
|
|
1216
|
+
// Only a result reads whether outside callers can reach its declaration.
|
|
1217
|
+
published: results.parts.length === 0 ? new Set() : importable(project, held, targetRoot),
|
|
1218
|
+
parts: [...withContextualRead(project, parts, contextual), ...results.parts],
|
|
621
1219
|
imports,
|
|
622
1220
|
signatures,
|
|
623
1221
|
calls,
|
|
624
1222
|
specifiers,
|
|
625
1223
|
constructions,
|
|
1224
|
+
files: judged,
|
|
1225
|
+
results: results.judged,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
/** Every identifier of one file whose `length` a property access reads. */
|
|
1229
|
+
function lengthReads(file) {
|
|
1230
|
+
const found = [];
|
|
1231
|
+
const visit = (node) => {
|
|
1232
|
+
if (isPropertyAccessExpression(node) && node.name.text === "length") {
|
|
1233
|
+
const read = parenthesesRemoved(node.expression);
|
|
1234
|
+
if (isIdentifier(read)) {
|
|
1235
|
+
found.push(read);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
node.forEachChild(visit);
|
|
626
1239
|
};
|
|
1240
|
+
file.forEachChild(visit);
|
|
1241
|
+
return found;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Whether the program reads the `length` of one declared function through its own binding,
|
|
1245
|
+
* which counts its parameters, so none can go. A read the checker leaves unanswered counts.
|
|
1246
|
+
*/
|
|
1247
|
+
function lengthRead(declared, measured, ids) {
|
|
1248
|
+
const name = declared.name;
|
|
1249
|
+
if (name === undefined || !isIdentifier(name)) {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
const own = ids.get(name);
|
|
1253
|
+
return measured.some((read) => {
|
|
1254
|
+
if (read.text !== name.text) {
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
const at = ids.get(read);
|
|
1258
|
+
return (at === undefined || at === UNANSWERED || own === undefined || own === UNANSWERED || at === own);
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Every declaration an outside importer of any of the project's own files can name, as the
|
|
1263
|
+
* published-API reach states it with each file published, and each one it guessed.
|
|
1264
|
+
*/
|
|
1265
|
+
function importable(project, held, targetRoot) {
|
|
1266
|
+
const files = sourceFilesOf(project, targetRoot);
|
|
1267
|
+
const exported = new Map();
|
|
1268
|
+
for (const symbol of held.symbols) {
|
|
1269
|
+
if (symbol.exported) {
|
|
1270
|
+
exported.set(symbol.parent, [...(exported.get(symbol.parent) ?? []), symbol.id]);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
const own = [...files.byName.values()];
|
|
1274
|
+
const exportsOf = new Map(own.map((file) => [
|
|
1275
|
+
file.fileName,
|
|
1276
|
+
exported.get(held.declarations.get(nodeKey(file, file)) ?? "") ?? [],
|
|
1277
|
+
]));
|
|
1278
|
+
const api = publishedAPI(project, own, exportsOf, held, files.byPath, []);
|
|
1279
|
+
return new Set([...api.ids, ...api.guessed]);
|
|
627
1280
|
}
|
|
628
1281
|
/**
|
|
629
1282
|
* A function written in place that no declaration declares: an arrow, a function
|
|
@@ -766,7 +1419,7 @@ function calleeSteers(project, signature, at) {
|
|
|
766
1419
|
* The parts with each name of `contextual` judged where the compiler gives its function a
|
|
767
1420
|
* contextual type: a name an object pattern binds is marked as one, and an annotated
|
|
768
1421
|
* parameter with whether the call's inference may read its annotation. A question the
|
|
769
|
-
* checker leaves unanswered keeps the name read.
|
|
1422
|
+
* checker leaves unanswered keeps the name read, so its part is dropped.
|
|
770
1423
|
*/
|
|
771
1424
|
function withContextualRead(project, parts, contextual) {
|
|
772
1425
|
if (contextual.length === 0) {
|
|
@@ -783,7 +1436,7 @@ function withContextualRead(project, parts, contextual) {
|
|
|
783
1436
|
}
|
|
784
1437
|
const place = one.part.parameter;
|
|
785
1438
|
judged.set(one.part, answer === UNANSWERED || place === undefined
|
|
786
|
-
?
|
|
1439
|
+
? undefined
|
|
787
1440
|
: one.pattern
|
|
788
1441
|
? { ...one.part, parameter: { ...place, contextualPattern: true } }
|
|
789
1442
|
: {
|
|
@@ -794,13 +1447,13 @@ function withContextualRead(project, parts, contextual) {
|
|
|
794
1447
|
},
|
|
795
1448
|
});
|
|
796
1449
|
}
|
|
797
|
-
return parts.
|
|
1450
|
+
return parts.flatMap((part) => (judged.has(part) ? (judged.get(part) ?? []) : [part]));
|
|
798
1451
|
}
|
|
799
1452
|
/**
|
|
800
|
-
* Each name one function's parameters bind
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
1453
|
+
* Each name one function's parameters bind that the function never reads, and those of
|
|
1454
|
+
* them a contextual type of a function expression may keep: the names of a parameter that
|
|
1455
|
+
* carries a type annotation or is an object binding pattern. A binding pattern counts as
|
|
1456
|
+
* read where its function reads any name it binds.
|
|
804
1457
|
*/
|
|
805
1458
|
function parameterParts(file, root, declaration, signature, reader, ids, place) {
|
|
806
1459
|
// A function that reads its `arguments` reads every parameter by position. A nested
|
|
@@ -813,24 +1466,25 @@ function parameterParts(file, root, declaration, signature, reader, ids, place)
|
|
|
813
1466
|
const found = [];
|
|
814
1467
|
const contextual = [];
|
|
815
1468
|
const expression = isArrowFunction(signature) || isFunctionExpression(signature) ? signature : undefined;
|
|
1469
|
+
const length = signature.parameters.findIndex((one) => one.initializer !== undefined || one.dotDotDotToken !== undefined);
|
|
816
1470
|
reader.names.forEach(({ name, at, pattern, exempt }, index) => {
|
|
817
|
-
if (exempt) {
|
|
1471
|
+
if (exempt || read[index] === true) {
|
|
818
1472
|
return;
|
|
819
1473
|
}
|
|
820
1474
|
const part = {
|
|
821
|
-
code: CODE.unusedParameter,
|
|
822
1475
|
declaration,
|
|
823
1476
|
kind: "parameter",
|
|
824
1477
|
name: name.text,
|
|
825
1478
|
position: spanOf(file, root, name),
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1479
|
+
parameter: {
|
|
1480
|
+
beforeARead: !pattern && at < lastRead,
|
|
1481
|
+
counted: length < 0 || at < length,
|
|
1482
|
+
...place,
|
|
1483
|
+
},
|
|
829
1484
|
};
|
|
830
1485
|
found.push(part);
|
|
831
1486
|
const parameter = signature.parameters[at];
|
|
832
1487
|
if (expression !== undefined &&
|
|
833
|
-
!part.used &&
|
|
834
1488
|
parameter !== undefined &&
|
|
835
1489
|
(parameter.type !== undefined || isObjectBindingPattern(parameter.name))) {
|
|
836
1490
|
contextual.push({
|
|
@@ -945,25 +1599,344 @@ function isPlainWrite(use) {
|
|
|
945
1599
|
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
946
1600
|
parenthesesRemoved(parent.left) === use);
|
|
947
1601
|
}
|
|
1602
|
+
const NOTHING = 0n;
|
|
1603
|
+
const READ = 1n;
|
|
1604
|
+
/** Whether a block sits inside a `try` whose `catch` or `finally` runs after the block is left. */
|
|
1605
|
+
function handledOutside(block, scope) {
|
|
1606
|
+
for (let at = block.parent; at !== scope && !isSourceFile(at); at = at.parent) {
|
|
1607
|
+
if (isTryStatement(at) &&
|
|
1608
|
+
(at.finallyBlock !== undefined ||
|
|
1609
|
+
(at.catchClause !== undefined && within(block, at.tryBlock)))) {
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
const UNEVALUATED = { yes: false, no: false, nullish: false, throws: false };
|
|
1616
|
+
/** The literals whose evaluation runs no code. */
|
|
1617
|
+
const LITERALS = new Set([
|
|
1618
|
+
SyntaxKind.TrueKeyword,
|
|
1619
|
+
SyntaxKind.FalseKeyword,
|
|
1620
|
+
SyntaxKind.NullKeyword,
|
|
1621
|
+
SyntaxKind.NumericLiteral,
|
|
1622
|
+
SyntaxKind.BigIntLiteral,
|
|
1623
|
+
SyntaxKind.StringLiteral,
|
|
1624
|
+
SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
1625
|
+
SyntaxKind.RegularExpressionLiteral,
|
|
1626
|
+
]);
|
|
948
1627
|
/**
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
* any time, and every write to a local nothing reads is dead, a nested function's included.
|
|
1628
|
+
* One walk over an expression with JavaScript's short-circuit. It folds only the values the
|
|
1629
|
+
* binder folds (`bindCondition`): an absent part, and a bare `true` or `false` that is the
|
|
1630
|
+
* whole condition, an operand of `&&` or `||` or a `?:` condition, seen through `(` and `!`
|
|
1631
|
+
* only to a logical operator. The binder sends a falsy left operand of `??` to the right;
|
|
1632
|
+
* JavaScript and this walk end the `??` there.
|
|
955
1633
|
*/
|
|
956
|
-
function
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1634
|
+
function evaluationOf(node) {
|
|
1635
|
+
if (node === undefined) {
|
|
1636
|
+
return { yes: true, no: false, nullish: false, throws: false };
|
|
1637
|
+
}
|
|
1638
|
+
const core = logicalCore(node);
|
|
1639
|
+
if (core === undefined) {
|
|
1640
|
+
const leaf = negationsRemoved(node);
|
|
1641
|
+
const literal = LITERALS.has(leaf.kind);
|
|
1642
|
+
const folded = (leaf.kind === SyntaxKind.TrueKeyword || leaf.kind === SyntaxKind.FalseKeyword) &&
|
|
1643
|
+
leaf === node &&
|
|
1644
|
+
!(isBinaryExpression(node.parent) &&
|
|
1645
|
+
node.parent.operatorToken.kind === SyntaxKind.QuestionQuestionToken);
|
|
1646
|
+
// The binder judges a `?:` whole, so it goes either way; a branch runs where its condition may go its way.
|
|
1647
|
+
const test = isConditionalExpression(leaf) ? evaluationOf(leaf.condition) : undefined;
|
|
1648
|
+
const branches = test !== undefined && isConditionalExpression(leaf)
|
|
1649
|
+
? [...(test.yes ? [leaf.whenTrue] : []), ...(test.no ? [leaf.whenFalse] : [])].map((branch) => evaluationOf(branch))
|
|
1650
|
+
: undefined;
|
|
1651
|
+
return {
|
|
1652
|
+
yes: !folded || leaf.kind === SyntaxKind.TrueKeyword,
|
|
1653
|
+
no: !folded || leaf.kind === SyntaxKind.FalseKeyword,
|
|
1654
|
+
nullish: leaf === parenthesesRemoved(node) &&
|
|
1655
|
+
(branches?.some((one) => one.nullish) ??
|
|
1656
|
+
(!literal || leaf.kind === SyntaxKind.NullKeyword)),
|
|
1657
|
+
throws: test === undefined ? !literal : test.throws || (branches ?? []).some((one) => one.throws),
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
if (isParenthesizedExpression(node)) {
|
|
1661
|
+
return evaluationOf(node.expression);
|
|
1662
|
+
}
|
|
1663
|
+
if (isPrefixUnaryExpression(node)) {
|
|
1664
|
+
const inner = evaluationOf(node.operand);
|
|
1665
|
+
return { yes: inner.no, no: inner.yes, nullish: false, throws: inner.throws };
|
|
1666
|
+
}
|
|
1667
|
+
const left = evaluationOf(core.left);
|
|
1668
|
+
const operator = core.operatorToken.kind;
|
|
1669
|
+
const runs = operator === SyntaxKind.AmpersandAmpersandToken
|
|
1670
|
+
? left.yes
|
|
1671
|
+
: operator === SyntaxKind.BarBarToken
|
|
1672
|
+
? left.no
|
|
1673
|
+
: left.nullish;
|
|
1674
|
+
const right = runs ? evaluationOf(core.right) : UNEVALUATED;
|
|
1675
|
+
return {
|
|
1676
|
+
yes: (operator !== SyntaxKind.AmpersandAmpersandToken && left.yes) || right.yes,
|
|
1677
|
+
no: (operator !== SyntaxKind.BarBarToken && left.no) || right.no,
|
|
1678
|
+
nullish: (operator === SyntaxKind.AmpersandAmpersandToken && left.nullish) || right.nullish,
|
|
1679
|
+
throws: left.throws || right.throws,
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
/** One expression below any parentheses and `!` around it. */
|
|
1683
|
+
function negationsRemoved(node) {
|
|
1684
|
+
let at = node;
|
|
1685
|
+
while (isParenthesizedExpression(at) ||
|
|
1686
|
+
(isPrefixUnaryExpression(at) && at.operator === SyntaxKind.ExclamationToken)) {
|
|
1687
|
+
at = isParenthesizedExpression(at) ? at.expression : at.operand;
|
|
1688
|
+
}
|
|
1689
|
+
return at;
|
|
1690
|
+
}
|
|
1691
|
+
/** The `&&`, `||` or `??` an expression is below any parentheses and `!`, if it is one. */
|
|
1692
|
+
function logicalCore(node) {
|
|
1693
|
+
const core = negationsRemoved(node);
|
|
1694
|
+
return isBinaryExpression(core) &&
|
|
1695
|
+
[
|
|
1696
|
+
SyntaxKind.AmpersandAmpersandToken,
|
|
1697
|
+
SyntaxKind.BarBarToken,
|
|
1698
|
+
SyntaxKind.QuestionQuestionToken,
|
|
1699
|
+
].includes(core.operatorToken.kind)
|
|
1700
|
+
? core
|
|
1701
|
+
: undefined;
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* One backward transfer over statements, which dead stores and unreachable statements share:
|
|
1705
|
+
* what a path from each statement's start reaches, given what one from its end reaches and
|
|
1706
|
+
* where its jumps land. A write is a plain assignment of the judged local that an expression
|
|
1707
|
+
* statement or a `for` head evaluates, or the bare name a `for…of` or `for…in` assigns; `live`
|
|
1708
|
+
* holds what a path from each write reaches. A node the walk does not follow reaches every
|
|
1709
|
+
* target inside it.
|
|
1710
|
+
*/
|
|
1711
|
+
function flowWalk(judged) {
|
|
1712
|
+
const { inside: reads, writes, ends } = judged;
|
|
1713
|
+
const live = new Map();
|
|
1714
|
+
const record = (target, after) => {
|
|
1715
|
+
live.set(target, (live.get(target) ?? NOTHING) | after);
|
|
1716
|
+
};
|
|
1717
|
+
const fixpoint = (step) => {
|
|
1718
|
+
let head = NOTHING;
|
|
1719
|
+
for (let next = step(head); next !== head; next = step(head)) {
|
|
1720
|
+
head = next;
|
|
1721
|
+
}
|
|
1722
|
+
return head;
|
|
1723
|
+
};
|
|
1724
|
+
const sequence = (statements, after, to) => statements.reduceRight((out, statement) => flow(statement, out, to), after);
|
|
1725
|
+
// A body run inline is a function's or a static block's: only `return` and a throw leave it,
|
|
1726
|
+
// so it starts with no break, continue or label target.
|
|
1727
|
+
const ran = (body, after, exit, raise) => sequence(body.statements, after, { exit, raise, escape: NOTHING, labels: new Map() });
|
|
1728
|
+
// Every label of a chain such as `a: b: while` names the loop, so each one continues it.
|
|
1729
|
+
const looped = (to, after, head, chain) => ({
|
|
1730
|
+
...to,
|
|
1731
|
+
breaks: after,
|
|
1732
|
+
continues: head,
|
|
1733
|
+
labels: new Map([
|
|
1734
|
+
...to.labels,
|
|
1735
|
+
...chain.map((label) => [label, { breaks: after, continues: head }]),
|
|
1736
|
+
]),
|
|
966
1737
|
});
|
|
1738
|
+
const when = (condition, reached) => (condition ? reached : NOTHING);
|
|
1739
|
+
// Code that may throw reaches `raise`, where a throw lands, before it writes anything.
|
|
1740
|
+
const thrown = (node, raise) => when(evaluationOf(node).throws, raise);
|
|
1741
|
+
// A comma's operands run in turn, so a throw in the right one follows the left one's write.
|
|
1742
|
+
// A store to the local, whose declaration has run, throws only where its value does.
|
|
1743
|
+
const evaluated = (node, after, raise) => {
|
|
1744
|
+
const expression = node === undefined ? undefined : parenthesesRemoved(node);
|
|
1745
|
+
if (expression !== undefined &&
|
|
1746
|
+
isBinaryExpression(expression) &&
|
|
1747
|
+
expression.operatorToken.kind === SyntaxKind.CommaToken) {
|
|
1748
|
+
return evaluated(expression.left, evaluated(expression.right, after, raise), raise);
|
|
1749
|
+
}
|
|
1750
|
+
const target = expression !== undefined &&
|
|
1751
|
+
isBinaryExpression(expression) &&
|
|
1752
|
+
expression.operatorToken.kind === SyntaxKind.EqualsToken
|
|
1753
|
+
? parenthesesRemoved(expression.left)
|
|
1754
|
+
: undefined;
|
|
1755
|
+
if (expression === undefined ||
|
|
1756
|
+
!isBinaryExpression(expression) ||
|
|
1757
|
+
target === undefined ||
|
|
1758
|
+
!isIdentifier(target) ||
|
|
1759
|
+
!writes.includes(target)) {
|
|
1760
|
+
return reads(expression) | after | thrown(expression, raise);
|
|
1761
|
+
}
|
|
1762
|
+
record(target, after);
|
|
1763
|
+
return reads(expression.right) | thrown(expression.right, raise);
|
|
1764
|
+
};
|
|
1765
|
+
// A statement may throw where it starts, before it writes anything, but entering a block
|
|
1766
|
+
// runs nothing. A part a statement runs again, or after a write of its own, gets its own
|
|
1767
|
+
// throw edge.
|
|
1768
|
+
const flow = (node, after, to, chain = []) => judged.at(node) | transfer(node, after, to, chain) | when(!isBlock(node), to.raise);
|
|
1769
|
+
const transfer = (node, after, to, chain) => {
|
|
1770
|
+
if (isBlock(node)) {
|
|
1771
|
+
return sequence(node.statements, after, to);
|
|
1772
|
+
}
|
|
1773
|
+
if (isExpressionStatement(node)) {
|
|
1774
|
+
const [body] = runsInline(node);
|
|
1775
|
+
const call = parenthesesRemoved(node.expression);
|
|
1776
|
+
if (body !== undefined && isCallExpression(call)) {
|
|
1777
|
+
// The arguments run first, then the body, whose `return` ends the call.
|
|
1778
|
+
return call.arguments.reduce((out, argument) => out | reads(argument), ran(body, after, after, to.raise));
|
|
1779
|
+
}
|
|
1780
|
+
return evaluated(node.expression, ends.has(node) ? NOTHING : after, to.raise);
|
|
1781
|
+
}
|
|
1782
|
+
if (isClassDeclaration(node)) {
|
|
1783
|
+
return runsInline(node).reduceRight((out, body) => ran(body, out, NOTHING, to.raise), after);
|
|
1784
|
+
}
|
|
1785
|
+
if (isIfStatement(node)) {
|
|
1786
|
+
const then = flow(node.thenStatement, after, to);
|
|
1787
|
+
const otherwise = node.elseStatement === undefined ? after : flow(node.elseStatement, after, to);
|
|
1788
|
+
const way = evaluationOf(node.expression);
|
|
1789
|
+
return reads(node.expression) | when(way.yes, then) | when(way.no, otherwise);
|
|
1790
|
+
}
|
|
1791
|
+
if (isLabeledStatement(node)) {
|
|
1792
|
+
const labels = new Map([...to.labels, [node.label.text, { breaks: after }]]);
|
|
1793
|
+
return flow(node.statement, after, { ...to, labels }, [...chain, node.label.text]);
|
|
1794
|
+
}
|
|
1795
|
+
if (isWithStatement(node)) {
|
|
1796
|
+
return reads(node.expression) | flow(node.statement, after, to);
|
|
1797
|
+
}
|
|
1798
|
+
if (isWhileStatement(node) || isDoStatement(node)) {
|
|
1799
|
+
const way = evaluationOf(node.expression);
|
|
1800
|
+
const test = fixpoint((head) => {
|
|
1801
|
+
const body = flow(node.statement, head, looped(to, after, head, chain));
|
|
1802
|
+
return (reads(node.expression) |
|
|
1803
|
+
when(way.no, after) |
|
|
1804
|
+
when(way.yes, body) |
|
|
1805
|
+
thrown(node.expression, to.raise));
|
|
1806
|
+
});
|
|
1807
|
+
return isDoStatement(node)
|
|
1808
|
+
? flow(node.statement, test, looped(to, after, test, chain))
|
|
1809
|
+
: test;
|
|
1810
|
+
}
|
|
1811
|
+
if (isForStatement(node)) {
|
|
1812
|
+
const way = evaluationOf(node.condition);
|
|
1813
|
+
const head = fixpoint((at) => {
|
|
1814
|
+
const next = evaluated(node.incrementor, at, to.raise);
|
|
1815
|
+
const body = flow(node.statement, next, looped(to, after, next, chain));
|
|
1816
|
+
return (reads(node.condition) |
|
|
1817
|
+
when(way.no, after) |
|
|
1818
|
+
when(way.yes, body) |
|
|
1819
|
+
thrown(node.condition, to.raise));
|
|
1820
|
+
});
|
|
1821
|
+
return node.initializer !== undefined && isVariableDeclarationList(node.initializer)
|
|
1822
|
+
? reads(node.initializer) | head
|
|
1823
|
+
: evaluated(node.initializer, head, to.raise);
|
|
1824
|
+
}
|
|
1825
|
+
if (isForOfStatement(node) || isForInStatement(node)) {
|
|
1826
|
+
const target = parenthesesRemoved(node.initializer);
|
|
1827
|
+
const assigned = isIdentifier(target) && writes.includes(target) ? target : undefined;
|
|
1828
|
+
// The iterator runs on each pass through the head and may throw.
|
|
1829
|
+
const head = fixpoint((at) => {
|
|
1830
|
+
const body = flow(node.statement, at, looped(to, after, at, chain));
|
|
1831
|
+
if (assigned === undefined) {
|
|
1832
|
+
return reads(node.initializer) | after | body | to.raise;
|
|
1833
|
+
}
|
|
1834
|
+
record(assigned, body);
|
|
1835
|
+
return after | to.raise;
|
|
1836
|
+
});
|
|
1837
|
+
return reads(node.expression) | head;
|
|
1838
|
+
}
|
|
1839
|
+
if (isSwitchStatement(node)) {
|
|
1840
|
+
const inside = { ...to, breaks: after };
|
|
1841
|
+
let next = after;
|
|
1842
|
+
let entered = NOTHING;
|
|
1843
|
+
for (const clause of node.caseBlock.clauses.toReversed()) {
|
|
1844
|
+
next = sequence(clause.statements, next, inside);
|
|
1845
|
+
entered |= next | (isCaseClause(clause) ? reads(clause.expression) : NOTHING);
|
|
1846
|
+
}
|
|
1847
|
+
const total = node.caseBlock.clauses.some(isDefaultClause);
|
|
1848
|
+
return reads(node.expression) | entered | when(!total, after);
|
|
1849
|
+
}
|
|
1850
|
+
if (isBreakStatement(node) || isContinueStatement(node)) {
|
|
1851
|
+
const named = node.label === undefined ? undefined : to.labels.get(node.label.text);
|
|
1852
|
+
const target = isBreakStatement(node)
|
|
1853
|
+
? node.label === undefined
|
|
1854
|
+
? to.breaks
|
|
1855
|
+
: named?.breaks
|
|
1856
|
+
: node.label === undefined
|
|
1857
|
+
? to.continues
|
|
1858
|
+
: named?.continues;
|
|
1859
|
+
return target ?? to.escape;
|
|
1860
|
+
}
|
|
1861
|
+
if (isReturnStatement(node)) {
|
|
1862
|
+
return reads(node.expression) | to.exit;
|
|
1863
|
+
}
|
|
1864
|
+
if (isThrowStatement(node)) {
|
|
1865
|
+
return reads(node.expression) | to.raise;
|
|
1866
|
+
}
|
|
1867
|
+
if (isTryStatement(node)) {
|
|
1868
|
+
const final = node.finallyBlock;
|
|
1869
|
+
// The finally block's entry depends only on what a path from where it continues
|
|
1870
|
+
// reaches, so it is computed at most once for each answer.
|
|
1871
|
+
const entries = new Map();
|
|
1872
|
+
const through = (reached) => {
|
|
1873
|
+
if (final === undefined) {
|
|
1874
|
+
return reached;
|
|
1875
|
+
}
|
|
1876
|
+
const known = entries.get(reached) ?? sequence(final.statements, reached, to);
|
|
1877
|
+
entries.set(reached, known);
|
|
1878
|
+
return known;
|
|
1879
|
+
};
|
|
1880
|
+
const map = (one) => ({
|
|
1881
|
+
breaks: through(one.breaks),
|
|
1882
|
+
...(one.continues === undefined ? {} : { continues: through(one.continues) }),
|
|
1883
|
+
});
|
|
1884
|
+
const outer = {
|
|
1885
|
+
exit: through(to.exit),
|
|
1886
|
+
raise: through(to.raise),
|
|
1887
|
+
escape: through(to.escape),
|
|
1888
|
+
...(to.breaks === undefined ? {} : { breaks: through(to.breaks) }),
|
|
1889
|
+
...(to.continues === undefined ? {} : { continues: through(to.continues) }),
|
|
1890
|
+
labels: new Map([...to.labels].map(([name, one]) => [name, map(one)])),
|
|
1891
|
+
};
|
|
1892
|
+
const normal = through(after);
|
|
1893
|
+
const handler = node.catchClause === undefined
|
|
1894
|
+
? undefined
|
|
1895
|
+
: sequence(node.catchClause.block.statements, normal, outer);
|
|
1896
|
+
// The binder's catch or finally sees the state before the try (`bindTryStatement`),
|
|
1897
|
+
// so it is reached from the entry even where the block runs nothing.
|
|
1898
|
+
const raise = handler ?? outer.raise;
|
|
1899
|
+
return sequence(node.tryBlock.statements, normal, { ...outer, raise }) | raise;
|
|
1900
|
+
}
|
|
1901
|
+
return reads(node) | after;
|
|
1902
|
+
};
|
|
1903
|
+
return { sequence, record, live };
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* The writes to one local of its block that no read reaches, by `flowWalk` from the
|
|
1907
|
+
* statement that declares it: a write is dead where no path from it reads the local before
|
|
1908
|
+
* the next write or the end of its scope, and any node the walk does not follow that names
|
|
1909
|
+
* the local reads it.
|
|
1910
|
+
*/
|
|
1911
|
+
function deadWrites(block, local, uses, escape, ends) {
|
|
1912
|
+
const reads = (node) => node !== undefined && uses.some((use) => within(use, node)) ? READ : NOTHING;
|
|
1913
|
+
const walk = flowWalk({ inside: reads, at: () => NOTHING, writes: uses, ends });
|
|
1914
|
+
const statements = block.statements;
|
|
1915
|
+
const declaration = statements[local.statement];
|
|
1916
|
+
const outlives = local.outlives ? READ : NOTHING;
|
|
1917
|
+
const after = walk.sequence(statements.slice(local.statement + 1), outlives, {
|
|
1918
|
+
exit: escape ? READ : NOTHING,
|
|
1919
|
+
raise: escape ? READ : NOTHING,
|
|
1920
|
+
escape: outlives,
|
|
1921
|
+
labels: new Map(),
|
|
1922
|
+
});
|
|
1923
|
+
// The rest of the declaring statement runs after the write: a later declarator may read the local.
|
|
1924
|
+
if (local.initialized && declaration !== undefined && reads(declaration) === NOTHING) {
|
|
1925
|
+
walk.record(local.name, after);
|
|
1926
|
+
}
|
|
1927
|
+
return [...walk.live]
|
|
1928
|
+
.filter(([, read]) => read === NOTHING)
|
|
1929
|
+
.map(([target]) => target)
|
|
1930
|
+
.toSorted((a, b) => a.pos - b.pos);
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* The writes to the locals of one block that no read reaches. A local a nested function
|
|
1934
|
+
* reads is no subject, since the function may run at any time, and every write to a local
|
|
1935
|
+
* nothing reads is dead, a nested function's included.
|
|
1936
|
+
*/
|
|
1937
|
+
function deadStores(block, locals, ids, ends) {
|
|
1938
|
+
const found = [];
|
|
1939
|
+
const scope = functionOf(block);
|
|
967
1940
|
for (const local of locals) {
|
|
968
1941
|
const own = local.hoisted ? block.statements[local.statement] : undefined;
|
|
969
1942
|
// A function's reads of itself keep it no more than its absence would.
|
|
@@ -972,89 +1945,36 @@ function deadStores(file, root, block, holder, locals, ids) {
|
|
|
972
1945
|
// A declaration with no initializer writes `undefined`, the one store left to name.
|
|
973
1946
|
const writes = [...(local.initialized ? [local.name] : []), ...uses];
|
|
974
1947
|
const targets = writes.length === 0 ? [local.name] : writes;
|
|
975
|
-
found.push(...targets.map((target) =>
|
|
1948
|
+
found.push(...targets.map((target) => ({ local, target })));
|
|
976
1949
|
continue;
|
|
977
1950
|
}
|
|
978
1951
|
// A read anywhere in the block reads a function declaration, which is written first.
|
|
979
|
-
if (local.hoisted) {
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
982
|
-
if (uses.some((use) => inNestedFunction(use, block))) {
|
|
1952
|
+
if (local.hoisted || uses.some((use) => inNestedFunction(use, block))) {
|
|
983
1953
|
continue;
|
|
984
1954
|
}
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
/** The plain assignment to the local a statement of the block is, where it is one. */
|
|
988
|
-
const writeOf = (statement) => {
|
|
989
|
-
if (!isExpressionStatement(statement)) {
|
|
990
|
-
return undefined;
|
|
991
|
-
}
|
|
992
|
-
const expression = parenthesesRemoved(statement.expression);
|
|
993
|
-
if (!isBinaryExpression(expression) ||
|
|
994
|
-
expression.operatorToken.kind !== SyntaxKind.EqualsToken) {
|
|
995
|
-
return undefined;
|
|
996
|
-
}
|
|
997
|
-
const target = parenthesesRemoved(expression.left);
|
|
998
|
-
return isIdentifier(target) && uses.includes(target)
|
|
999
|
-
? { target, value: expression.right }
|
|
1000
|
-
: undefined;
|
|
1001
|
-
};
|
|
1002
|
-
const writes = [];
|
|
1003
|
-
if (local.initialized) {
|
|
1004
|
-
writes.push({ at: local.statement, target: local.name });
|
|
1005
|
-
}
|
|
1006
|
-
statements.forEach((statement, at) => {
|
|
1007
|
-
const write = writeOf(statement);
|
|
1008
|
-
if (write !== undefined && at > local.statement) {
|
|
1009
|
-
writes.push({ at, target: write.target });
|
|
1010
|
-
}
|
|
1011
|
-
});
|
|
1012
|
-
for (const write of writes) {
|
|
1013
|
-
// The rest of the write's own statement runs after it: a later declarator may read the local.
|
|
1014
|
-
const own = statements[write.at];
|
|
1015
|
-
const sameStatement = own !== undefined && write.target === local.name ? usesIn(own) : [];
|
|
1016
|
-
if (sameStatement.length > 0) {
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
let dead = !local.outlives;
|
|
1020
|
-
for (let at = write.at + 1; at < statements.length; at += 1) {
|
|
1021
|
-
const statement = statements[at];
|
|
1022
|
-
if (statement === undefined) {
|
|
1023
|
-
break;
|
|
1024
|
-
}
|
|
1025
|
-
const next = writeOf(statement);
|
|
1026
|
-
if (next !== undefined) {
|
|
1027
|
-
dead = usesIn(next.value).length === 0;
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
1030
|
-
const named = usesIn(statement);
|
|
1031
|
-
if (named.length > 0) {
|
|
1032
|
-
dead = false;
|
|
1033
|
-
break;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
if (dead) {
|
|
1037
|
-
found.push(deadAt(local, write.target));
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1955
|
+
const escape = local.outlives && handledOutside(block, scope);
|
|
1956
|
+
found.push(...deadWrites(block, local, uses, escape, ends).map((target) => ({ local, target })));
|
|
1040
1957
|
}
|
|
1041
1958
|
return found;
|
|
1042
1959
|
}
|
|
1043
1960
|
/**
|
|
1044
1961
|
* The result of each function a discarded call reaches, where its type carries a value:
|
|
1045
1962
|
* whether it is used is the call sites' to say, which the run decides over every project.
|
|
1963
|
+
* `judged` holds every function whose result type the project was asked about.
|
|
1046
1964
|
*/
|
|
1047
1965
|
function resultParts(project, root, sites, references, calls) {
|
|
1048
1966
|
const discardedAt = new Set(references
|
|
1049
1967
|
.filter((one) => (calls.get(positionKey(one.position)) ?? "used") !== "used")
|
|
1050
1968
|
.map((one) => one.to));
|
|
1051
1969
|
const found = [];
|
|
1970
|
+
const judged = new Set();
|
|
1052
1971
|
for (const { file, declared } of sites) {
|
|
1053
1972
|
const { signature, node, id } = declared;
|
|
1054
1973
|
const generator = signature.asteriskToken !== undefined;
|
|
1055
1974
|
if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
|
|
1056
1975
|
continue;
|
|
1057
1976
|
}
|
|
1977
|
+
judged.add(id);
|
|
1058
1978
|
// A question the checker leaves unanswered withholds the finding.
|
|
1059
1979
|
const typed = project.queries.signatureOf(signature);
|
|
1060
1980
|
if (typed === undefined || !isAnswered(typed)) {
|
|
@@ -1072,33 +1992,39 @@ function resultParts(project, root, sites, references, calls) {
|
|
|
1072
1992
|
const valued = project.queries.nonNullable(result);
|
|
1073
1993
|
const members = isAnswered(valued) ? project.queries.propertiesOf(valued) : UNANSWERED;
|
|
1074
1994
|
found.push({
|
|
1075
|
-
code: CODE.unusedResult,
|
|
1076
1995
|
declaration: id,
|
|
1077
1996
|
kind: "result",
|
|
1078
1997
|
name: "result",
|
|
1079
1998
|
position: spanOf(file, root, at),
|
|
1080
|
-
message: "result is discarded at every call site",
|
|
1081
|
-
used: false,
|
|
1082
1999
|
thenable: !isAnswered(members) || members.some((member) => member.name === "then"),
|
|
1083
2000
|
});
|
|
1084
2001
|
}
|
|
1085
|
-
return found;
|
|
2002
|
+
return { parts: found, judged };
|
|
1086
2003
|
}
|
|
1087
2004
|
/**
|
|
1088
|
-
* The parts of a run's projects merged: a part
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
2005
|
+
* The parts of a run's projects merged (matrix.6): a part stands only where every project
|
|
2006
|
+
* that judges it finds it, a project judging the parts of each file it compiles and the
|
|
2007
|
+
* result of each function it asked the checker about. A signature is free when no
|
|
2008
|
+
* exemption record names it, its body is no stub, no class hierarchy fixes it, and every
|
|
2009
|
+
* reference is a call or a specifier: any other uses the function as a value, and a
|
|
2010
|
+
* consumer's reference has a form this run does not read.
|
|
1094
2011
|
*/
|
|
1095
2012
|
export function intraFunctionFacts(projects, references, exempted) {
|
|
1096
|
-
const
|
|
1097
|
-
for (const
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
2013
|
+
const found = new Map();
|
|
2014
|
+
for (const project of projects) {
|
|
2015
|
+
for (const part of project.parts) {
|
|
2016
|
+
const key = `${part.kind}\u0000${positionKey(part.position)}`;
|
|
2017
|
+
const known = found.get(key) ?? { part, by: new Set() };
|
|
2018
|
+
known.by.add(project);
|
|
2019
|
+
found.set(key, known);
|
|
2020
|
+
}
|
|
1101
2021
|
}
|
|
2022
|
+
const judges = (project, part) => part.kind === "result"
|
|
2023
|
+
? project.results.has(part.declaration)
|
|
2024
|
+
: project.files.has(part.position.path);
|
|
2025
|
+
const parts = [...found.values()]
|
|
2026
|
+
.filter(({ part, by }) => projects.every((one) => by.has(one) || !judges(one, part)))
|
|
2027
|
+
.map(({ part }) => part);
|
|
1102
2028
|
const imports = new Map();
|
|
1103
2029
|
for (const binding of projects.flatMap((one) => one.imports)) {
|
|
1104
2030
|
const key = positionKey(binding.position);
|
|
@@ -1145,15 +2071,16 @@ export function intraFunctionFacts(projects, references, exempted) {
|
|
|
1145
2071
|
.map((one) => one.declaration)
|
|
1146
2072
|
.filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id) && !constructors.has(id)));
|
|
1147
2073
|
// Awaiting a thenable result sequences on it, so a call that awaits one uses it.
|
|
1148
|
-
const sequenced = new Set(
|
|
2074
|
+
const sequenced = new Set(parts
|
|
1149
2075
|
.filter((part) => part.thenable === true && awaited.has(part.declaration))
|
|
1150
2076
|
.map((part) => part.declaration));
|
|
1151
2077
|
const discardedEverywhere = new Set([...called]
|
|
1152
2078
|
.filter(([id, all]) => all && !valued.has(id) && !sequenced.has(id))
|
|
1153
2079
|
.map(([id]) => id));
|
|
1154
2080
|
return {
|
|
1155
|
-
parts
|
|
2081
|
+
parts,
|
|
1156
2082
|
imports: [...imports.values()],
|
|
2083
|
+
published: new Set(projects.flatMap((one) => [...one.published])),
|
|
1157
2084
|
free,
|
|
1158
2085
|
freeButValued,
|
|
1159
2086
|
discardedEverywhere,
|