@cplieger/deadset-ts 7.2.0-dev.3 → 8.0.0-dev.1

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 (153) hide show
  1. package/dist/bin/mapper-connection.js +7 -4
  2. package/dist/src/analysis.js +116 -16
  3. package/dist/src/augmentations.js +21 -8
  4. package/dist/src/automatic-types.js +189 -0
  5. package/dist/src/base-url.js +18 -0
  6. package/dist/src/component-files.js +96 -31
  7. package/dist/src/components.js +7 -6
  8. package/dist/src/config.js +1 -0
  9. package/dist/src/configuration-files.js +54 -15
  10. package/dist/src/configured-declarations.js +24 -2
  11. package/dist/src/conformance-record.js +2 -2
  12. package/dist/src/contextual-uses.js +26 -26
  13. package/dist/src/convention-rows.js +75 -0
  14. package/dist/src/conventions.js +101 -5
  15. package/dist/src/decorator.js +21 -18
  16. package/dist/src/dependencies.js +185 -30
  17. package/dist/src/destructuring.js +77 -2
  18. package/dist/src/discover.js +188 -85
  19. package/dist/src/edges.js +1 -1
  20. package/dist/src/entry-point-gaps.js +7 -10
  21. package/dist/src/entry-points.js +112 -11
  22. package/dist/src/findings/completion.js +116 -21
  23. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  24. package/dist/src/findings/emitters.js +1 -1
  25. package/dist/src/findings/intra-function.js +44 -13
  26. package/dist/src/findings/non-code-artifacts.js +66 -33
  27. package/dist/src/findings/unused-declarations.js +21 -1
  28. package/dist/src/findings/visibility-narrowing.js +22 -3
  29. package/dist/src/generated-file.js +11 -0
  30. package/dist/src/git-ignore.js +86 -0
  31. package/dist/src/graph.js +4 -1
  32. package/dist/src/import-bindings.js +97 -0
  33. package/dist/src/indexed-access.js +130 -28
  34. package/dist/src/interface-satisfaction.js +30 -12
  35. package/dist/src/intra-function-parts.js +313 -50
  36. package/dist/src/inventory.js +63 -8
  37. package/dist/src/json-document.js +5 -5
  38. package/dist/src/kinds.js +13 -13
  39. package/dist/src/matrix.js +18 -9
  40. package/dist/src/mdx-entries.js +197 -0
  41. package/dist/src/module-declarations.js +9 -13
  42. package/dist/src/namespace-objects.js +28 -18
  43. package/dist/src/overrides.js +45 -9
  44. package/dist/src/paths.js +9 -0
  45. package/dist/src/print.js +1 -0
  46. package/dist/src/ref.js +10 -0
  47. package/dist/src/references.js +29 -13
  48. package/dist/src/reflective-lookup.js +8 -6
  49. package/dist/src/report.js +4 -2
  50. package/dist/src/reporters/template.js +22 -13
  51. package/dist/src/resolve.js +2 -0
  52. package/dist/src/roots.js +110 -38
  53. package/dist/src/schema.js +1 -0
  54. package/dist/src/serialization-contract.js +97 -20
  55. package/dist/src/session.js +26 -1
  56. package/dist/src/source-files.js +39 -14
  57. package/dist/src/spelled-names.js +58 -0
  58. package/dist/src/suppress-file.js +12 -4
  59. package/dist/src/suppress-inline.js +21 -3
  60. package/dist/src/suppress.js +7 -5
  61. package/dist/src/sweep.js +27 -10
  62. package/dist/src/symbol-keys.js +103 -2
  63. package/dist/src/template-field.js +9 -6
  64. package/dist/src/test-support.js +121 -17
  65. package/dist/src/type-errors.js +116 -35
  66. package/dist/src/type-parameter-reads.js +172 -0
  67. package/dist/src/value-reach.js +17 -5
  68. package/dist/src/verbs/analyze.js +19 -1
  69. package/dist/src/verbs/explain.js +4 -2
  70. package/dist/src/version-range.js +10 -5
  71. package/dist/src/version.js +1 -1
  72. package/dist/src/workflow-steps.js +119 -22
  73. package/dist/src/workspace-programs.js +7 -3
  74. package/dist/src/workspace-resolution.js +72 -13
  75. package/dist/src/workspace.js +1 -1
  76. package/jsr.json +1 -1
  77. package/package.json +2 -2
  78. package/src/analysis.ts +175 -16
  79. package/src/augmentations.ts +21 -10
  80. package/src/automatic-types.ts +246 -0
  81. package/src/base-url.ts +24 -0
  82. package/src/component-files.ts +116 -31
  83. package/src/components.ts +7 -6
  84. package/src/config.ts +3 -0
  85. package/src/configuration-files.ts +80 -18
  86. package/src/configured-declarations.ts +33 -4
  87. package/src/conformance-record.ts +2 -2
  88. package/src/contextual-uses.ts +27 -27
  89. package/src/convention-rows.ts +103 -0
  90. package/src/conventions.ts +121 -7
  91. package/src/decorator.ts +28 -18
  92. package/src/dependencies.ts +249 -43
  93. package/src/destructuring.ts +93 -2
  94. package/src/discover.ts +234 -86
  95. package/src/edges.ts +1 -1
  96. package/src/entry-point-gaps.ts +8 -11
  97. package/src/entry-points.ts +133 -11
  98. package/src/finding.ts +10 -0
  99. package/src/findings/completion.ts +147 -24
  100. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  101. package/src/findings/emitters.ts +1 -1
  102. package/src/findings/intra-function.ts +56 -14
  103. package/src/findings/non-code-artifacts.ts +81 -41
  104. package/src/findings/unused-declarations.ts +25 -1
  105. package/src/findings/visibility-narrowing.ts +32 -3
  106. package/src/generated-file.ts +16 -0
  107. package/src/git-ignore.ts +98 -0
  108. package/src/graph.ts +6 -0
  109. package/src/host.ts +5 -9
  110. package/src/import-bindings.ts +128 -0
  111. package/src/indexed-access.ts +152 -30
  112. package/src/interface-satisfaction.ts +33 -13
  113. package/src/intra-function-parts.ts +415 -64
  114. package/src/inventory.ts +83 -7
  115. package/src/json-document.ts +7 -7
  116. package/src/kinds.ts +13 -13
  117. package/src/matrix.ts +32 -14
  118. package/src/mdx-entries.ts +241 -0
  119. package/src/module-declarations.ts +11 -13
  120. package/src/namespace-objects.ts +34 -21
  121. package/src/overrides.ts +56 -9
  122. package/src/paths.ts +10 -0
  123. package/src/print.ts +1 -0
  124. package/src/ref.ts +12 -0
  125. package/src/references.ts +38 -17
  126. package/src/reflective-lookup.ts +9 -7
  127. package/src/report.ts +16 -3
  128. package/src/reporters/template.ts +21 -13
  129. package/src/resolve.ts +3 -0
  130. package/src/roots.ts +163 -45
  131. package/src/schema.ts +1 -0
  132. package/src/serialization-contract.ts +108 -17
  133. package/src/session.ts +29 -1
  134. package/src/source-files.ts +66 -16
  135. package/src/spelled-names.ts +102 -0
  136. package/src/suppress-file.ts +14 -3
  137. package/src/suppress-inline.ts +21 -2
  138. package/src/suppress.ts +9 -5
  139. package/src/sweep.ts +32 -11
  140. package/src/symbol-keys.ts +129 -2
  141. package/src/template-field.ts +9 -6
  142. package/src/test-support.ts +149 -16
  143. package/src/type-errors.ts +132 -41
  144. package/src/type-parameter-reads.ts +227 -0
  145. package/src/value-reach.ts +19 -6
  146. package/src/verbs/analyze.ts +28 -1
  147. package/src/verbs/explain.ts +5 -2
  148. package/src/version-range.ts +10 -5
  149. package/src/version.ts +1 -1
  150. package/src/workflow-steps.ts +127 -22
  151. package/src/workspace-programs.ts +7 -3
  152. package/src/workspace-resolution.ts +86 -14
  153. package/src/workspace.ts +1 -1
@@ -6,7 +6,6 @@
6
6
  */
7
7
 
8
8
  import {
9
- isArrayBindingPattern,
10
9
  isArrowFunction,
11
10
  isAwaitExpression,
12
11
  isBigIntLiteral,
@@ -29,6 +28,9 @@ import {
29
28
  isExpressionStatement,
30
29
  isFunctionDeclaration,
31
30
  isFunctionExpression,
31
+ isForInStatement,
32
+ isForOfStatement,
33
+ isFunctionLikeDeclaration,
32
34
  isHeritageClause,
33
35
  isIdentifier,
34
36
  isIfStatement,
@@ -45,15 +47,18 @@ import {
45
47
  isObjectLiteralExpression,
46
48
  isParenthesizedExpression,
47
49
  isPrefixUnaryExpression,
50
+ isPropertyAssignment,
48
51
  isPropertyAccessExpression,
49
52
  isQualifiedName,
50
53
  isReturnStatement,
51
54
  isShorthandPropertyAssignment,
55
+ isSourceFile,
52
56
  isStringLiteral,
53
57
  isSwitchStatement,
54
58
  isThrowStatement,
55
59
  isTypeAliasDeclaration,
56
60
  isTypeReferenceNode,
61
+ isVariableDeclarationList,
57
62
  isVariableStatement,
58
63
  isVoidExpression,
59
64
  NodeFlags,
@@ -64,19 +69,32 @@ import {
64
69
  type CallExpression,
65
70
  type CatchClause,
66
71
  type ConstructorDeclaration,
72
+ type ForInStatement,
73
+ type ForOfStatement,
74
+ type FunctionLikeDeclaration,
67
75
  type FunctionExpression,
68
76
  type Identifier,
69
77
  type Node,
70
78
  type SourceFile,
71
79
  type Statement,
80
+ type VariableDeclarationList,
72
81
  } from "@typescript/native/ast";
82
+ import { SignatureKind, type Type } from "@typescript/native/sync";
73
83
  import type { FindingPosition } from "./finding.ts";
84
+ import {
85
+ exportsLocally,
86
+ implicitReads,
87
+ importBindings,
88
+ type JsxOptions,
89
+ } from "./import-bindings.ts";
74
90
  import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
75
91
  import { positionKey, renderPosition } from "./position.ts";
92
+ import { importRef } from "./ref.ts";
76
93
  import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
77
94
  import type { Reference } from "./references.ts";
78
95
  import type { ProjectView } from "./session.ts";
79
96
  import { signatureOf, type SignatureNode } from "./signatures.ts";
97
+ import { spellsNoBinding } from "./spelled-names.ts";
80
98
 
81
99
  /** The subject kind of each part this module reads. */
82
100
  type PartKind = "parameter" | "result" | "statement" | "store" | "case";
@@ -113,6 +131,14 @@ interface ParameterPlace {
113
131
  readonly beforeARead: boolean;
114
132
  /** Whether no declaration of the inventory declares the function, which is then a value. */
115
133
  readonly inValue: boolean;
134
+ /**
135
+ * Where the parameter carries an annotation and the compiler gives its function a
136
+ * contextual type: whether a type parameter of the callee reads the parameter's type,
137
+ * so the annotation may steer the call's inference.
138
+ */
139
+ readonly annotated?: "steers" | "free";
140
+ /** Whether the compiler checks the parameter's file, so its unused-parameter rule may report it. */
141
+ readonly checked: boolean;
116
142
  }
117
143
 
118
144
  /** What one declaration's signature answers to beside its body. */
@@ -126,9 +152,18 @@ interface SignatureFact {
126
152
  readonly constructor: boolean;
127
153
  }
128
154
 
155
+ /** One name an import declaration binds, and whether its file reads it. */
156
+ interface ImportFact {
157
+ readonly ref: string;
158
+ readonly name: string;
159
+ readonly position: FindingPosition;
160
+ readonly used: boolean;
161
+ }
162
+
129
163
  /** What one project answers about the parts of its declarations. */
130
164
  export interface ProjectParts {
131
165
  readonly parts: readonly PartFact[];
166
+ readonly imports: readonly ImportFact[];
132
167
  readonly signatures: readonly SignatureFact[];
133
168
  /** Each name written as the callee of a call, by position, and what the call does with its value. */
134
169
  readonly calls: ReadonlyMap<string, CallUse>;
@@ -268,13 +303,12 @@ function caseKey(expression: Node): string | undefined {
268
303
  return undefined;
269
304
  }
270
305
 
271
- /** Every identifier below one node whose text one of the names spells, a member's name aside. */
306
+ /** Every identifier below one node whose text one of the names spells as a reference. */
272
307
  function identifiersNaming(node: Node, names: ReadonlySet<string>): Identifier[] {
273
308
  const found: Identifier[] = [];
274
309
  const visit = (at: Node): void => {
275
310
  if (isIdentifier(at)) {
276
- const parent = at.parent;
277
- if (names.has(at.text) && !(isPropertyAccessExpression(parent) && parent.name === at)) {
311
+ if (names.has(at.text) && !spellsNoBinding(at)) {
278
312
  found.push(at);
279
313
  }
280
314
  return;
@@ -310,6 +344,16 @@ function inNestedFunction(node: Node, outer: Node): boolean {
310
344
  return false;
311
345
  }
312
346
 
347
+ /** The nearest function around one node whose `arguments` object it names: an arrow has none. */
348
+ function ownArgumentsOf(node: Node): Node | undefined {
349
+ for (let at = node.parent; !isSourceFile(at); at = at.parent) {
350
+ if (isFunctionLikeDeclaration(at) && !isArrowFunction(at)) {
351
+ return at;
352
+ }
353
+ }
354
+ return undefined;
355
+ }
356
+
313
357
  /** Whether a body is a stub: it holds no statement, or nothing but `throw` statements. */
314
358
  function isStub(signature: SignatureNode): boolean {
315
359
  const body = signature.body;
@@ -498,10 +542,49 @@ interface ParameterName {
498
542
  /** The parameter's index in its list. */
499
543
  readonly at: number;
500
544
  readonly pattern: boolean;
501
- /** A name beside a rest element of its object pattern: removing it changes what the rest holds. */
545
+ /**
546
+ * Whether the name is never reported: it is spelled declared unread, or it stands beside a
547
+ * rest element of its object pattern, whose removal changes what the rest holds.
548
+ */
502
549
  readonly exempt: boolean;
503
550
  }
504
551
 
552
+ /** Whether a name is spelled with a leading underscore, which declares it unread. */
553
+ function declaredUnread(name: Identifier): boolean {
554
+ return name.text.startsWith("_");
555
+ }
556
+
557
+ /** A file's leading comments, where the compiler reads its `@ts-check` and `@ts-nocheck` pragmas. */
558
+ const LEADING_COMMENTS = /^(?:#![^\n]*)?(?:\s|\/\/[^\n]*|\/\*[\s\S]*?\*\/)*/u;
559
+
560
+ /** A comment of the leading comments, a line comment's text after its slashes captured. */
561
+ const LEADING_COMMENT = /\/\*[\s\S]*?\*\/|\/\/\/?([^\n]*)/gu;
562
+
563
+ /**
564
+ * Whether a file's leading comments hold one pragma: as the compiler reads it, a line
565
+ * comment that starts with the pragma's name, in any case, followed by nothing or a space.
566
+ */
567
+ function hasPragma(leading: string, pragma: RegExp): boolean {
568
+ return [...leading.matchAll(LEADING_COMMENT)].some(
569
+ ([, text]) => text !== undefined && pragma.test(text),
570
+ );
571
+ }
572
+
573
+ /**
574
+ * Whether the compiler checks a file, so its rules report what they find there: a
575
+ * JavaScript file only under `checkJs` or a `@ts-check` pragma, and no file with a
576
+ * `@ts-nocheck` pragma.
577
+ */
578
+ function checkedByTheCompiler(file: SourceFile, checkJs: boolean): boolean {
579
+ const leading = LEADING_COMMENTS.exec(file.text)?.[0] ?? "";
580
+ if (hasPragma(leading, /^\s*@ts-nocheck(?:\s|$)/iu)) {
581
+ return false;
582
+ }
583
+ return (
584
+ !/\.[cm]?jsx?$/u.test(file.fileName) || checkJs || hasPragma(leading, /^\s*@ts-check(?:\s|$)/iu)
585
+ );
586
+ }
587
+
505
588
  /** What one file's walk collects before the batch that resolves its names. */
506
589
  interface FileWalk {
507
590
  readonly declared: Declared[];
@@ -512,6 +595,8 @@ interface FileWalk {
512
595
  readonly holder: string;
513
596
  }[];
514
597
  readonly blocks: Block[];
598
+ /** The loops whose `let` or `const` variables each iteration writes. */
599
+ readonly loops: { readonly loop: ForOfStatement | ForInStatement; readonly holder: string }[];
515
600
  readonly unreachable: { readonly statement: Statement; readonly holder: string }[];
516
601
  readonly cases: { readonly clause: Node; readonly text: string; readonly holder: string }[];
517
602
  }
@@ -536,8 +621,13 @@ export function projectParts<Brand>(
536
621
  const constructions = new Set<string>();
537
622
  const sites: { readonly file: SourceFile; readonly declared: Declared }[] = [];
538
623
  const contextual: ContextualParameter[] = [];
624
+ const imports: ImportFact[] = [];
625
+ const refs = new Map(held.symbols.map((symbol) => [symbol.id, symbol.ref]));
626
+ const options = project.program.getCompilerOptions();
627
+ const jsx = options as JsxOptions;
539
628
 
540
629
  for (const file of files) {
630
+ const checked = checkedByTheCompiler(file, options.checkJs === true);
541
631
  // A parameter property's own parts stay with the parameter when its member goes.
542
632
  const declarationAt = (node: Node): string | undefined =>
543
633
  isParameterProperty(node) ? undefined : held.declarations.get(nodeKey(file, node));
@@ -557,6 +647,7 @@ export function projectParts<Brand>(
557
647
  values: [],
558
648
  catches: [],
559
649
  blocks: [],
650
+ loops: [],
560
651
  unreachable: [],
561
652
  cases: [],
562
653
  };
@@ -655,6 +746,18 @@ export function projectParts<Brand>(
655
746
  if (isBlock(node) && silenced === 0 && holderOf(node) !== undefined) {
656
747
  walk.blocks.push(node);
657
748
  }
749
+ const loopHolder =
750
+ (isForOfStatement(node) || isForInStatement(node)) && silenced === 0
751
+ ? holderOf(node)
752
+ : undefined;
753
+ if (
754
+ loopHolder !== undefined &&
755
+ (isForOfStatement(node) || isForInStatement(node)) &&
756
+ isVariableDeclarationList(node.initializer) &&
757
+ (node.initializer.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0
758
+ ) {
759
+ walk.loops.push({ loop: node, holder: loopHolder });
760
+ }
658
761
  if (isBlock(node) || isCaseClause(node) || isDefaultClause(node)) {
659
762
  statementsOf(node.statements);
660
763
  return;
@@ -687,7 +790,44 @@ export function projectParts<Brand>(
687
790
  for (const store of stores) {
688
791
  asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
689
792
  }
793
+ const loopVariables = walk.loops.map(({ loop, holder }) => {
794
+ const list = loop.initializer as VariableDeclarationList;
795
+ // A loop must bind a name, so one spelled declared unread is no subject.
796
+ const names = list.declarations
797
+ .flatMap((declaration) => boundNames(declaration.name))
798
+ .filter((name) => !declaredUnread(name));
799
+ const uses = identifiersNaming(loop.statement, new Set(names.map((one) => one.text)));
800
+ asked.push(...names, ...uses);
801
+ return { names, uses, holder };
802
+ });
803
+ const bindings = fileId === undefined ? [] : importBindings(file);
804
+ const bound = new Set<Node>(bindings);
805
+ const bindingUses =
806
+ bindings.length === 0
807
+ ? []
808
+ : identifiersNaming(file, new Set(bindings.map((one) => one.text))).filter(
809
+ (use) => !bound.has(use),
810
+ );
811
+ asked.push(...bindings, ...bindingUses);
690
812
  const ids = resolvedIds(project, [...new Set(asked)]);
813
+ const implicit = bindings.length === 0 ? new Set<string>() : implicitReads(file, jsx);
814
+ for (const binding of bindings) {
815
+ const fileRef = fileId === undefined ? undefined : refs.get(fileId);
816
+ if (fileId === undefined || fileRef === undefined) {
817
+ continue;
818
+ }
819
+ imports.push({
820
+ ref: importRef(fileRef, binding.text),
821
+ name: binding.text,
822
+ position: spanOf(file, targetRoot, binding),
823
+ used:
824
+ implicit.has(binding.text) ||
825
+ bindingUses.some(
826
+ (use) =>
827
+ use.text === binding.text && (exportsLocally(use) || readsBinding(use, binding, ids)),
828
+ ),
829
+ });
830
+ }
691
831
 
692
832
  for (const one of walk.declared) {
693
833
  const constructor = isConstructorDeclaration(one.node);
@@ -702,7 +842,10 @@ export function projectParts<Brand>(
702
842
  });
703
843
  const reader = readers.get(one.signature);
704
844
  if (reader !== undefined) {
705
- const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false);
845
+ const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, {
846
+ inValue: false,
847
+ checked,
848
+ });
706
849
  parts.push(...found.parts);
707
850
  contextual.push(...found.contextual);
708
851
  }
@@ -710,15 +853,10 @@ export function projectParts<Brand>(
710
853
  for (const one of walk.values) {
711
854
  const reader = readers.get(one.signature);
712
855
  if (reader !== undefined) {
713
- const found = parameterParts(
714
- file,
715
- targetRoot,
716
- one.holder,
717
- one.signature,
718
- reader,
719
- ids,
720
- true,
721
- );
856
+ const found = parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, {
857
+ inValue: true,
858
+ checked,
859
+ });
722
860
  parts.push(...found.parts);
723
861
  contextual.push(...found.contextual);
724
862
  }
@@ -734,6 +872,19 @@ export function projectParts<Brand>(
734
872
  used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
735
873
  });
736
874
  });
875
+ for (const { names, uses, holder } of loopVariables) {
876
+ for (const name of names) {
877
+ parts.push({
878
+ code: DEAD_STORE,
879
+ declaration: holder,
880
+ kind: "store",
881
+ name: name.text,
882
+ position: spanOf(file, targetRoot, name),
883
+ message: `value written to ${name.text} is never read`,
884
+ used: uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
885
+ });
886
+ }
887
+ }
737
888
  walk.blocks.forEach((block, at) => {
738
889
  const holder = holderOf(block);
739
890
  if (holder !== undefined) {
@@ -767,6 +918,7 @@ export function projectParts<Brand>(
767
918
  parts.push(...resultParts(project, targetRoot, sites, references, calls));
768
919
  return {
769
920
  parts: withContextualRead(project, parts, contextual),
921
+ imports,
770
922
  signatures,
771
923
  calls,
772
924
  specifiers,
@@ -795,7 +947,9 @@ function valueSignature(node: Node): SignatureNode | undefined {
795
947
  /**
796
948
  * Every name one function's parameters bind, a binding pattern's names one by one. A
797
949
  * `this` parameter receives no argument, a decorated one is read by its decorator, and a
798
- * parameter property is read by the write to its member, so none binds a subject.
950
+ * parameter property is read by the write to its member, so none binds a subject. A name
951
+ * spelled with a leading underscore is declared unread, so it is listed for its reads and
952
+ * reported never.
799
953
  */
800
954
  function parameterNames(signature: SignatureNode): ParameterName[] {
801
955
  const found: ParameterName[] = [];
@@ -808,7 +962,8 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
808
962
  continue;
809
963
  }
810
964
  if (isIdentifier(element.name)) {
811
- const exempt = rest && element.dotDotDotToken === undefined;
965
+ const exempt =
966
+ (rest && element.dotDotDotToken === undefined) || declaredUnread(element.name);
812
967
  found.push({ name: element.name, at, pattern: true, exempt });
813
968
  } else {
814
969
  patternNames(element.name, at);
@@ -825,7 +980,7 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
825
980
  }
826
981
  if (isIdentifier(name)) {
827
982
  if (name.text !== "this") {
828
- found.push({ name, at, pattern: false, exempt: false });
983
+ found.push({ name, at, pattern: false, exempt: declaredUnread(name) });
829
984
  }
830
985
  return;
831
986
  }
@@ -838,13 +993,110 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
838
993
  interface ContextualParameter {
839
994
  readonly part: PartFact;
840
995
  readonly signature: ArrowFunction | FunctionExpression;
996
+ /** The parameter's index in its list. */
997
+ readonly at: number;
998
+ /** Whether the parameter is an object binding pattern, rather than an annotated name. */
999
+ readonly pattern: boolean;
1000
+ }
1001
+
1002
+ /** How deep a type is searched for a type parameter: a callback of a callback is two levels. */
1003
+ const TYPE_PARAMETER_DEPTH = 3;
1004
+
1005
+ /** Whether one type is or holds a type parameter, through constituents, type arguments and signatures. */
1006
+ function namesTypeParameter<Brand>(project: ProjectView<Brand>, type: Type, depth = 0): boolean {
1007
+ if (type.isTypeParameter()) {
1008
+ return true;
1009
+ }
1010
+ if (depth >= TYPE_PARAMETER_DEPTH) {
1011
+ return false;
1012
+ }
1013
+ const { queries } = project;
1014
+ const parts = type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [];
1015
+ const args = queries.typeArguments(type);
1016
+ const signatures = queries.signaturesOf(type, SignatureKind.Call);
1017
+ if (!isAnswered(parts) || !isAnswered(args) || !isAnswered(signatures)) {
1018
+ return true;
1019
+ }
1020
+ const inner = [
1021
+ ...parts,
1022
+ ...args,
1023
+ ...signatures.flatMap((signature) =>
1024
+ signature.parameters.map((_parameter, at) => queries.typeAtPosition(signature, at)),
1025
+ ),
1026
+ ];
1027
+ return inner.some((one) => !isAnswered(one) || namesTypeParameter(project, one, depth + 1));
841
1028
  }
842
1029
 
843
1030
  /**
844
- * The parts with each name of `contextual` read where the compiler gives its function a
845
- * contextual type: the inference of the call holding the function may read a parameter's
846
- * annotation, and the code handing the argument may act on what an object pattern reads.
847
- * A question the checker leaves unanswered keeps the name read.
1031
+ * Whether the call a function expression is written in may read the annotation of its
1032
+ * parameter at `at`: the callee is generic, and its parameter type at the function's place
1033
+ * reads a type parameter there. A function written inside an object literal a generic
1034
+ * call is passed is read so too, and a question the checker leaves unanswered reads it.
1035
+ */
1036
+ function calleeSteers<Brand>(
1037
+ project: ProjectView<Brand>,
1038
+ signature: ArrowFunction | FunctionExpression,
1039
+ at: number,
1040
+ ): boolean {
1041
+ const { queries } = project;
1042
+ let value: Node = signature;
1043
+ while (isParenthesizedExpression(value.parent)) {
1044
+ value = value.parent;
1045
+ }
1046
+ let nested = false;
1047
+ while (
1048
+ (isPropertyAssignment(value.parent) && isObjectLiteralExpression(value.parent.parent)) ||
1049
+ isObjectLiteralExpression(value.parent)
1050
+ ) {
1051
+ value = isObjectLiteralExpression(value.parent) ? value.parent : value.parent.parent;
1052
+ nested = true;
1053
+ }
1054
+ const call = value.parent;
1055
+ if (!isCallExpression(call) && !isNewExpression(call)) {
1056
+ return false;
1057
+ }
1058
+ const argument = (call.arguments ?? []).findIndex((one) => one === value);
1059
+ const resolved = argument < 0 ? undefined : queries.resolvedSignature(call);
1060
+ const generic =
1061
+ resolved === undefined || !isAnswered(resolved) ? resolved : queries.genericOf(resolved);
1062
+ if (argument < 0 || generic === undefined) {
1063
+ return false;
1064
+ }
1065
+ if (!isAnswered(generic) || nested) {
1066
+ return true;
1067
+ }
1068
+ const position = queries.typeAtPosition(generic, argument);
1069
+ if (!isAnswered(position) || position.isTypeParameter()) {
1070
+ return true;
1071
+ }
1072
+ const parts =
1073
+ position.isUnionType() || position.isIntersectionType()
1074
+ ? queries.constituents(position)
1075
+ : [position];
1076
+ if (!isAnswered(parts)) {
1077
+ return true;
1078
+ }
1079
+ return parts.some((part) => {
1080
+ if (part.isTypeParameter()) {
1081
+ return true;
1082
+ }
1083
+ const calls = queries.signaturesOf(part, SignatureKind.Call);
1084
+ if (!isAnswered(calls)) {
1085
+ return true;
1086
+ }
1087
+ return calls.some((one) => {
1088
+ const type = at < one.parameters.length ? queries.typeAtPosition(one, at) : undefined;
1089
+ return type !== undefined && (!isAnswered(type) || namesTypeParameter(project, type));
1090
+ });
1091
+ });
1092
+ }
1093
+
1094
+ /**
1095
+ * The parts with each name of `contextual` judged where the compiler gives its function a
1096
+ * contextual type: the code handing the argument may act on what an object pattern reads,
1097
+ * so each such name is read, and an annotated parameter is marked with whether the call's
1098
+ * inference may read its annotation. A question the checker leaves unanswered keeps the
1099
+ * name read.
848
1100
  */
849
1101
  function withContextualRead<Brand>(
850
1102
  project: ProjectView<Brand>,
@@ -856,9 +1108,28 @@ function withContextualRead<Brand>(
856
1108
  }
857
1109
  const signatures = [...new Set(contextual.map((one) => one.signature))];
858
1110
  const answers = project.queries.contextualTypes(signatures);
859
- const typed = new Set(signatures.filter((_signature, at) => answers[at] !== undefined));
860
- const read = new Set(contextual.filter((one) => typed.has(one.signature)).map((one) => one.part));
861
- return parts.map((part) => (read.has(part) ? { ...part, used: true } : part));
1111
+ const typedBy = new Map(signatures.map((signature, at) => [signature, answers[at]]));
1112
+ const judged = new Map<PartFact, PartFact>();
1113
+ for (const one of contextual) {
1114
+ const answer = typedBy.get(one.signature);
1115
+ if (answer === undefined) {
1116
+ continue;
1117
+ }
1118
+ const place = one.part.parameter;
1119
+ judged.set(
1120
+ one.part,
1121
+ one.pattern || answer === UNANSWERED || place === undefined
1122
+ ? { ...one.part, used: true }
1123
+ : {
1124
+ ...one.part,
1125
+ parameter: {
1126
+ ...place,
1127
+ annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
1128
+ },
1129
+ },
1130
+ );
1131
+ }
1132
+ return parts.map((part) => judged.get(part) ?? part);
862
1133
  }
863
1134
 
864
1135
  /**
@@ -874,11 +1145,15 @@ function parameterParts(
874
1145
  signature: SignatureNode,
875
1146
  reader: { readonly names: readonly ParameterName[]; readonly uses: readonly Identifier[] },
876
1147
  ids: ReadonlyMap<Node, Answer<number>>,
877
- inValue: boolean,
1148
+ place: Pick<ParameterPlace, "inValue" | "checked">,
878
1149
  ): { readonly parts: PartFact[]; readonly contextual: ContextualParameter[] } {
879
- // A function that reads `arguments` reads every parameter by position.
1150
+ // A function that reads its `arguments` reads every parameter by position. A nested
1151
+ // function other than an arrow has an `arguments` object of its own.
880
1152
  const readsArguments =
881
- !isArrowFunction(signature) && identifiersNaming(signature, new Set(["arguments"])).length > 0;
1153
+ !isArrowFunction(signature) &&
1154
+ identifiersNaming(signature, new Set(["arguments"])).some(
1155
+ (use) => ownArgumentsOf(use) === signature,
1156
+ );
882
1157
  const read = reader.names.map(
883
1158
  ({ name }) =>
884
1159
  readsArguments ||
@@ -901,7 +1176,7 @@ function parameterParts(
901
1176
  position: spanOf(file, root, name),
902
1177
  message: `parameter ${name.text} is never read in the body`,
903
1178
  used: read[index] === true,
904
- parameter: { beforeARead: !pattern && at < lastRead, inValue },
1179
+ parameter: { beforeARead: !pattern && at < lastRead, ...place },
905
1180
  };
906
1181
  found.push(part);
907
1182
  const parameter = signature.parameters[at];
@@ -911,7 +1186,12 @@ function parameterParts(
911
1186
  parameter !== undefined &&
912
1187
  (parameter.type !== undefined || isObjectBindingPattern(parameter.name))
913
1188
  ) {
914
- contextual.push({ part, signature: expression });
1189
+ contextual.push({
1190
+ part,
1191
+ signature: expression,
1192
+ at,
1193
+ pattern: isObjectBindingPattern(parameter.name),
1194
+ });
915
1195
  }
916
1196
  });
917
1197
  return { parts: found, contextual };
@@ -923,44 +1203,85 @@ interface Local {
923
1203
  readonly statement: number;
924
1204
  readonly initialized: boolean;
925
1205
  readonly uses: readonly Identifier[];
1206
+ /** A `var` of a nested block, whose function scope outlives the block's end. */
1207
+ readonly outlives: boolean;
926
1208
  }
927
1209
 
928
1210
  /**
929
1211
  * The names one declaration binds that a write of its initializer stores each on its own:
930
- * the declared name, or each element name of an array binding pattern at any depth.
1212
+ * the declared name, or each element name of a binding pattern at any depth. A name with a
1213
+ * default is left out, because removing it skips the default's evaluation, and so is a name
1214
+ * beside a rest element of its object pattern, whose removal changes what the rest holds.
931
1215
  */
932
- function arrayPatternNames(name: BindingName): readonly Identifier[] {
1216
+ function boundNames(name: BindingName): readonly Identifier[] {
933
1217
  if (isIdentifier(name)) {
934
1218
  return [name];
935
1219
  }
936
- if (!isArrayBindingPattern(name)) {
937
- return [];
1220
+ const rest =
1221
+ isObjectBindingPattern(name) &&
1222
+ name.elements.some((element) => element.dotDotDotToken !== undefined);
1223
+ return name.elements.flatMap((element) => {
1224
+ if (!isBindingElement(element) || element.name === undefined) {
1225
+ return [];
1226
+ }
1227
+ if (element.initializer !== undefined || (rest && element.dotDotDotToken === undefined)) {
1228
+ return [];
1229
+ }
1230
+ return boundNames(element.name);
1231
+ });
1232
+ }
1233
+
1234
+ /** The function whose scope a `var` of one block belongs to, or none at a module's top level. */
1235
+ function functionOf(block: Block): FunctionLikeDeclaration | undefined {
1236
+ for (let at: Node = block.parent; ; at = at.parent) {
1237
+ if (isFunctionLikeDeclaration(at)) {
1238
+ return at;
1239
+ }
1240
+ if (isSourceFile(at)) {
1241
+ return undefined;
1242
+ }
938
1243
  }
939
- return name.elements.flatMap((element) =>
940
- isBindingElement(element) && element.name !== undefined ? arrayPatternNames(element.name) : [],
941
- );
942
1244
  }
943
1245
 
944
- /** The locals one block declares with `let` or `const`, each with the names that spell it. */
1246
+ /**
1247
+ * The locals one block declares with `let`, `const` or `var`, each with the names of the
1248
+ * block that spell it. A `var` of a nested block is one where no nested function of its
1249
+ * function names it, since the function scope holds it past the block's end.
1250
+ */
945
1251
  function localsOf(block: Block): readonly Local[] {
946
- const declared: { name: Identifier; statement: number; initialized: boolean }[] = [];
1252
+ const declared: {
1253
+ name: Identifier;
1254
+ statement: number;
1255
+ initialized: boolean;
1256
+ outlives: boolean;
1257
+ }[] = [];
1258
+ const scope = functionOf(block);
1259
+ const outlives = scope?.body !== block;
947
1260
  block.statements.forEach((statement, at) => {
948
1261
  if (!isVariableStatement(statement)) {
949
1262
  return;
950
1263
  }
951
1264
  const list = statement.declarationList;
952
- if (
953
- (list.flags & (NodeFlags.Let | NodeFlags.Const)) === 0 ||
954
- (list.flags & NodeFlags.Using) !== 0
955
- ) {
1265
+ const blockScoped = (list.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0;
1266
+ if ((!blockScoped && scope === undefined) || (list.flags & NodeFlags.Using) !== 0) {
956
1267
  return;
957
1268
  }
958
1269
  for (const declaration of list.declarations) {
959
- for (const name of arrayPatternNames(declaration.name)) {
1270
+ for (const name of boundNames(declaration.name)) {
1271
+ if (
1272
+ !blockScoped &&
1273
+ outlives &&
1274
+ identifiersNaming(scope?.body ?? block, new Set([name.text])).some((use) =>
1275
+ inNestedFunction(use, scope ?? block),
1276
+ )
1277
+ ) {
1278
+ continue;
1279
+ }
960
1280
  declared.push({
961
1281
  name,
962
1282
  statement: at,
963
1283
  initialized: declaration.initializer !== undefined,
1284
+ outlives: !blockScoped && outlives,
964
1285
  });
965
1286
  }
966
1287
  }
@@ -975,14 +1296,23 @@ function localsOf(block: Block): readonly Local[] {
975
1296
  }));
976
1297
  }
977
1298
 
1299
+ /** Whether one name is the whole target of a plain assignment, which writes it and reads nothing. */
1300
+ function isPlainWrite(use: Identifier): boolean {
1301
+ const parent = outerParent(use);
1302
+ return (
1303
+ isBinaryExpression(parent) &&
1304
+ parent.operatorToken.kind === SyntaxKind.EqualsToken &&
1305
+ unwrapped(parent.left) === use
1306
+ );
1307
+ }
1308
+
978
1309
  /**
979
1310
  * The writes to the locals of one block that no read reaches. A write is a declaration's
980
- * initializer or a plain assignment written as a statement of the block itself; it is
981
- * dead when the statements after it in the block read the local nowhere before the next
982
- * such write to it, or before the block ends, which ends the local's scope. A statement
983
- * that writes the local inside a nested construct ends the search with the write kept,
984
- * and a local a nested function names is no subject, because the function may read it
985
- * at any time.
1311
+ * initializer or a plain assignment written as a statement of the block; it is dead when
1312
+ * the block's later statements read the local nowhere before the next such write or the
1313
+ * block's end. A statement writing it inside a nested construct ends the search with the
1314
+ * write kept. A local a nested function reads is no subject, since the function may run at
1315
+ * any time, and every write to a local nothing reads is dead, a nested function's included.
986
1316
  */
987
1317
  function deadStores(
988
1318
  file: SourceFile,
@@ -993,8 +1323,22 @@ function deadStores(
993
1323
  ids: ReadonlyMap<Node, Answer<number>>,
994
1324
  ): PartFact[] {
995
1325
  const found: PartFact[] = [];
1326
+ const deadAt = (local: Local, target: Identifier): PartFact => ({
1327
+ code: DEAD_STORE,
1328
+ declaration: holder,
1329
+ kind: "store",
1330
+ name: local.name.text,
1331
+ position: spanOf(file, root, target),
1332
+ message: `value written to ${local.name.text} is never read`,
1333
+ used: false,
1334
+ });
996
1335
  for (const local of locals) {
997
1336
  const uses = local.uses.filter((use) => readsBinding(use, local.name, ids));
1337
+ if (!local.outlives && uses.every(isPlainWrite)) {
1338
+ const targets = [...(local.initialized ? [local.name] : []), ...uses];
1339
+ found.push(...targets.map((target) => deadAt(local, target)));
1340
+ continue;
1341
+ }
998
1342
  if (uses.some((use) => inNestedFunction(use, block))) {
999
1343
  continue;
1000
1344
  }
@@ -1034,7 +1378,7 @@ function deadStores(
1034
1378
  if (sameStatement.length > 0) {
1035
1379
  continue;
1036
1380
  }
1037
- let dead = true;
1381
+ let dead = !local.outlives;
1038
1382
  for (let at = write.at + 1; at < statements.length; at += 1) {
1039
1383
  const statement = statements[at];
1040
1384
  if (statement === undefined) {
@@ -1051,19 +1395,9 @@ function deadStores(
1051
1395
  break;
1052
1396
  }
1053
1397
  }
1054
- if (!dead) {
1055
- continue;
1398
+ if (dead) {
1399
+ found.push(deadAt(local, write.target));
1056
1400
  }
1057
- const position = spanOf(file, root, write.target);
1058
- found.push({
1059
- code: DEAD_STORE,
1060
- declaration: holder,
1061
- kind: "store",
1062
- name: local.name.text,
1063
- position,
1064
- message: `value written to ${local.name.text} is never read`,
1065
- used: false,
1066
- });
1067
1401
  }
1068
1402
  }
1069
1403
  return found;
@@ -1126,6 +1460,8 @@ function resultParts<Brand>(
1126
1460
  export interface IntraFunctionFacts {
1127
1461
  /** Every part of the run, each once, in use where any project found it in use. */
1128
1462
  readonly parts: readonly PartFact[];
1463
+ /** Every import binding of the run, each once, read where any project found it read. */
1464
+ readonly imports: readonly ImportFact[];
1129
1465
  /** The declarations whose signature is free to change. */
1130
1466
  readonly free: ReadonlySet<string>;
1131
1467
  /** The declarations whose signature is free but for a use as a value. */
@@ -1156,6 +1492,15 @@ export function intraFunctionFacts(
1156
1492
  const known = parts.get(key);
1157
1493
  parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
1158
1494
  }
1495
+ const imports = new Map<string, ImportFact>();
1496
+ for (const binding of projects.flatMap((one) => one.imports)) {
1497
+ const key = positionKey(binding.position);
1498
+ const known = imports.get(key);
1499
+ imports.set(
1500
+ key,
1501
+ known === undefined ? binding : { ...known, used: known.used || binding.used },
1502
+ );
1503
+ }
1159
1504
  const calls = new Map<string, CallUse>();
1160
1505
  const specifiers = new Set<string>();
1161
1506
  for (const one of projects) {
@@ -1215,5 +1560,11 @@ export function intraFunctionFacts(
1215
1560
  .filter(([id, all]) => all && !valued.has(id) && !sequenced.has(id))
1216
1561
  .map(([id]) => id),
1217
1562
  );
1218
- return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
1563
+ return {
1564
+ parts: [...parts.values()],
1565
+ imports: [...imports.values()],
1566
+ free,
1567
+ freeButValued,
1568
+ discardedEverywhere,
1569
+ };
1219
1570
  }