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