@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.
@@ -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 { CODE } from "./contract/contract.generated.js";
8
- import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isBindingElement, isCallExpression, isCaseClause, isCatchClause, isClassDeclaration, isClassExpression, isConstructorDeclaration, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isFunctionExpression, isForInStatement, isForOfStatement, isFunctionLikeDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAssignment, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isSourceFile, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableDeclarationList, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
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
- /** Whether one statement leaves its statement list on every path: the rest of the list never runs. */
56
- function abrupt(statement) {
57
- if (isReturnStatement(statement) ||
58
- isThrowStatement(statement) ||
59
- isBreakStatement(statement) ||
60
- isContinueStatement(statement)) {
61
- return true;
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
- if (isBlock(statement)) {
64
- return statement.statements.some(abrupt);
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 (isIfStatement(statement)) {
67
- return (statement.elseStatement !== undefined &&
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
- return false;
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 unreachable statement is no subject: a declaration the language hoists or
75
- * that emits nothing, an empty statement, and a `var` that initializes nothing.
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 unreachableExempt(statement) {
78
- if (isFunctionDeclaration(statement) ||
79
- isInterfaceDeclaration(statement) ||
80
- isTypeAliasDeclaration(statement) ||
81
- isModuleDeclaration(statement) ||
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
- if (isVariableStatement(statement)) {
86
- const list = statement.declarationList;
87
- return ((list.flags & (NodeFlags.Let | NodeFlags.Const | NodeFlags.Using)) === 0 &&
88
- list.declarations.every((one) => one.initializer === undefined));
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
- return false;
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
- /** Every class name an `extends` clause of the project names, by its last identifier. */
194
- function extendedNames(files) {
195
- const names = new Set();
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) && node.token === SyntaxKind.ExtendsKeyword) {
198
- for (const type of node.types) {
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
- return names;
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 extended itself, or the method is marked `override`.
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, extended) {
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
- if ((owner.heritageClauses ?? []).length > 0) {
237
- return true;
238
- }
239
- return owner.name !== undefined && extended.has(owner.name.text);
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 extended = extendedNames(files);
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 below an unreachable statement is still walked for its calls and specifiers,
375
- // and reports no part of its own: the unreachable statement is the one finding.
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 reachable = true;
379
- let reported = false;
977
+ let inRun = false;
380
978
  for (const statement of statements) {
381
- if (reachable) {
382
- visit(statement);
383
- reachable = !abrupt(statement);
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
- silenced += 1;
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
- if (isBlock(node) && silenced === 0 && holderOf(node) !== undefined) {
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
- node.forEachChild(visit);
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.forEachChild(visit);
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, extended),
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
- parts.push({
567
- code: CODE.deadStore,
568
- declaration: holder,
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
- parts.push({
579
- code: CODE.deadStore,
580
- declaration: holder,
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
- parts.push(...deadStores(file, targetRoot, block, holder, stores[at] ?? [], ids));
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
- parts.push(...resultParts(project, targetRoot, sites, references, calls));
1214
+ const results = resultParts(project, targetRoot, sites, references, calls);
619
1215
  return {
620
- parts: withContextualRead(project, parts, contextual),
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
- ? { ...one.part, used: true }
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.map((part) => judged.get(part) ?? part);
1450
+ return parts.flatMap((part) => (judged.has(part) ? (judged.get(part) ?? []) : [part]));
798
1451
  }
799
1452
  /**
800
- * Each name one function's parameters bind and whether the function reads it, and the
801
- * unread names a contextual type of a function expression may keep: those of a parameter
802
- * that carries a type annotation or is an object binding pattern. A binding pattern counts
803
- * as read where its function reads any name it binds.
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
- message: `parameter ${name.text} is never read in the body`,
827
- used: read[index] === true,
828
- parameter: { beforeARead: !pattern && at < lastRead, ...place },
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
- * The writes to the locals of one block that no read reaches. A write is a declaration's
950
- * initializer or a plain assignment written as a statement of the block; it is dead when
951
- * the block's later statements read the local nowhere before the next such write or the
952
- * block's end. A statement writing it inside a nested construct ends the search with the
953
- * write kept. A local a nested function reads is no subject, since the function may run at
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 deadStores(file, root, block, holder, locals, ids) {
957
- const found = [];
958
- const deadAt = (local, target) => ({
959
- code: CODE.deadStore,
960
- declaration: holder,
961
- kind: "store",
962
- name: local.name.text,
963
- position: spanOf(file, root, target),
964
- message: `value written to ${local.name.text} is never read`,
965
- used: false,
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) => deadAt(local, 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 statements = block.statements;
986
- const usesIn = (statement) => uses.filter((use) => within(use, statement));
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 two projects hold is one part, in use
1089
- * where either found it in use. A signature is free when no exemption record names its
1090
- * declaration, its body is no stub, no class hierarchy fixes it, and every reference to
1091
- * the declaration is a call or a specifier: any other reference uses the function as a
1092
- * value, whose type then fixes the signature, and a reference a consumer makes is one
1093
- * whose form this run does not read.
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 parts = new Map();
1097
- for (const part of projects.flatMap((one) => one.parts)) {
1098
- const key = `${part.code}\u0000${positionKey(part.position)}`;
1099
- const known = parts.get(key);
1100
- parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
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([...parts.values()]
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: [...parts.values()],
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,