@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.2

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 (122) hide show
  1. package/dist/src/analysis.js +108 -10
  2. package/dist/src/augmentations.js +62 -18
  3. package/dist/src/automatic-types.js +189 -0
  4. package/dist/src/base-url.js +18 -0
  5. package/dist/src/components.js +7 -6
  6. package/dist/src/config.js +1 -0
  7. package/dist/src/configuration-files.js +50 -12
  8. package/dist/src/conformance-record.js +2 -2
  9. package/dist/src/convention-rows.js +98 -0
  10. package/dist/src/conventions.js +101 -5
  11. package/dist/src/decorator.js +21 -18
  12. package/dist/src/dependencies.js +183 -28
  13. package/dist/src/discover.js +132 -11
  14. package/dist/src/entry-point-gaps.js +7 -10
  15. package/dist/src/entry-points.js +112 -11
  16. package/dist/src/findings/completion.js +172 -40
  17. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  18. package/dist/src/findings/emitters.js +1 -1
  19. package/dist/src/findings/intra-function.js +44 -13
  20. package/dist/src/findings/non-code-artifacts.js +66 -33
  21. package/dist/src/findings/unused-declarations.js +30 -5
  22. package/dist/src/findings/visibility-narrowing.js +29 -5
  23. package/dist/src/generated-file.js +11 -0
  24. package/dist/src/git-ignore.js +86 -0
  25. package/dist/src/graph.js +4 -1
  26. package/dist/src/import-bindings.js +97 -0
  27. package/dist/src/indexed-access.js +130 -28
  28. package/dist/src/interface-satisfaction.js +30 -12
  29. package/dist/src/intra-function-parts.js +297 -45
  30. package/dist/src/inventory.js +52 -8
  31. package/dist/src/kinds.js +13 -13
  32. package/dist/src/matrix.js +57 -10
  33. package/dist/src/mdx-entries.js +197 -0
  34. package/dist/src/module-declarations.js +9 -13
  35. package/dist/src/overrides.js +41 -10
  36. package/dist/src/print.js +1 -0
  37. package/dist/src/ref.js +10 -0
  38. package/dist/src/references.js +38 -4
  39. package/dist/src/report.js +4 -2
  40. package/dist/src/resolve.js +2 -0
  41. package/dist/src/roots.js +110 -38
  42. package/dist/src/schema.js +1 -0
  43. package/dist/src/serialization-contract.js +54 -8
  44. package/dist/src/session.js +7 -0
  45. package/dist/src/source-files.js +39 -14
  46. package/dist/src/suppress-file.js +10 -2
  47. package/dist/src/suppress-inline.js +18 -2
  48. package/dist/src/suppress.js +8 -5
  49. package/dist/src/sweep.js +27 -10
  50. package/dist/src/symbol-keys.js +103 -2
  51. package/dist/src/test-support.js +119 -18
  52. package/dist/src/type-errors.js +34 -7
  53. package/dist/src/type-parameter-reads.js +172 -0
  54. package/dist/src/verbs/analyze.js +16 -0
  55. package/dist/src/verbs/explain.js +4 -2
  56. package/dist/src/version.js +1 -1
  57. package/dist/src/workflow-steps.js +119 -22
  58. package/dist/src/workspace-programs.js +7 -3
  59. package/dist/src/workspace-resolution.js +53 -10
  60. package/jsr.json +1 -1
  61. package/package.json +2 -2
  62. package/src/analysis.ts +146 -11
  63. package/src/augmentations.ts +76 -22
  64. package/src/automatic-types.ts +246 -0
  65. package/src/base-url.ts +24 -0
  66. package/src/components.ts +7 -6
  67. package/src/config.ts +3 -0
  68. package/src/configuration-files.ts +66 -12
  69. package/src/conformance-record.ts +2 -2
  70. package/src/convention-rows.ts +128 -0
  71. package/src/conventions.ts +121 -7
  72. package/src/decorator.ts +28 -18
  73. package/src/dependencies.ts +247 -41
  74. package/src/discover.ts +156 -13
  75. package/src/entry-point-gaps.ts +8 -11
  76. package/src/entry-points.ts +133 -11
  77. package/src/finding.ts +15 -0
  78. package/src/findings/completion.ts +222 -46
  79. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +56 -14
  82. package/src/findings/non-code-artifacts.ts +81 -41
  83. package/src/findings/unused-declarations.ts +36 -5
  84. package/src/findings/visibility-narrowing.ts +40 -5
  85. package/src/generated-file.ts +16 -0
  86. package/src/git-ignore.ts +98 -0
  87. package/src/graph.ts +6 -0
  88. package/src/host.ts +5 -9
  89. package/src/import-bindings.ts +128 -0
  90. package/src/indexed-access.ts +152 -30
  91. package/src/interface-satisfaction.ts +33 -13
  92. package/src/intra-function-parts.ts +394 -59
  93. package/src/inventory.ts +71 -7
  94. package/src/kinds.ts +13 -13
  95. package/src/matrix.ts +104 -15
  96. package/src/mdx-entries.ts +241 -0
  97. package/src/module-declarations.ts +11 -13
  98. package/src/overrides.ts +51 -12
  99. package/src/print.ts +1 -0
  100. package/src/ref.ts +12 -0
  101. package/src/references.ts +48 -3
  102. package/src/report.ts +16 -3
  103. package/src/resolve.ts +3 -0
  104. package/src/roots.ts +163 -45
  105. package/src/schema.ts +1 -0
  106. package/src/serialization-contract.ts +55 -4
  107. package/src/session.ts +7 -0
  108. package/src/source-files.ts +66 -16
  109. package/src/suppress-file.ts +12 -1
  110. package/src/suppress-inline.ts +18 -1
  111. package/src/suppress.ts +10 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +155 -18
  115. package/src/type-errors.ts +38 -6
  116. package/src/type-parameter-reads.ts +227 -0
  117. package/src/verbs/analyze.ts +22 -0
  118. package/src/verbs/explain.ts +5 -2
  119. package/src/version.ts +1 -1
  120. package/src/workflow-steps.ts +127 -22
  121. package/src/workspace-programs.ts +7 -3
  122. package/src/workspace-resolution.ts +62 -10
@@ -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,8 @@ import {
29
28
  isExpressionStatement,
30
29
  isFunctionDeclaration,
31
30
  isFunctionExpression,
31
+ isForInStatement,
32
+ isForOfStatement,
32
33
  isFunctionLikeDeclaration,
33
34
  isHeritageClause,
34
35
  isIdentifier,
@@ -46,6 +47,7 @@ import {
46
47
  isObjectLiteralExpression,
47
48
  isParenthesizedExpression,
48
49
  isPrefixUnaryExpression,
50
+ isPropertyAssignment,
49
51
  isPropertyAccessExpression,
50
52
  isQualifiedName,
51
53
  isReturnStatement,
@@ -56,6 +58,7 @@ import {
56
58
  isThrowStatement,
57
59
  isTypeAliasDeclaration,
58
60
  isTypeReferenceNode,
61
+ isVariableDeclarationList,
59
62
  isVariableStatement,
60
63
  isVoidExpression,
61
64
  NodeFlags,
@@ -66,15 +69,27 @@ import {
66
69
  type CallExpression,
67
70
  type CatchClause,
68
71
  type ConstructorDeclaration,
72
+ type ForInStatement,
73
+ type ForOfStatement,
74
+ type FunctionLikeDeclaration,
69
75
  type FunctionExpression,
70
76
  type Identifier,
71
77
  type Node,
72
78
  type SourceFile,
73
79
  type Statement,
80
+ type VariableDeclarationList,
74
81
  } from "@typescript/native/ast";
82
+ import { SignatureKind, type Type } from "@typescript/native/sync";
75
83
  import type { FindingPosition } from "./finding.ts";
84
+ import {
85
+ exportsLocally,
86
+ implicitReads,
87
+ importBindings,
88
+ type JsxOptions,
89
+ } from "./import-bindings.ts";
76
90
  import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
77
91
  import { positionKey, renderPosition } from "./position.ts";
92
+ import { importRef } from "./ref.ts";
78
93
  import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
79
94
  import type { Reference } from "./references.ts";
80
95
  import type { ProjectView } from "./session.ts";
@@ -116,6 +131,14 @@ interface ParameterPlace {
116
131
  readonly beforeARead: boolean;
117
132
  /** Whether no declaration of the inventory declares the function, which is then a value. */
118
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;
119
142
  }
120
143
 
121
144
  /** What one declaration's signature answers to beside its body. */
@@ -129,9 +152,18 @@ interface SignatureFact {
129
152
  readonly constructor: boolean;
130
153
  }
131
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
+
132
163
  /** What one project answers about the parts of its declarations. */
133
164
  export interface ProjectParts {
134
165
  readonly parts: readonly PartFact[];
166
+ readonly imports: readonly ImportFact[];
135
167
  readonly signatures: readonly SignatureFact[];
136
168
  /** Each name written as the callee of a call, by position, and what the call does with its value. */
137
169
  readonly calls: ReadonlyMap<string, CallUse>;
@@ -510,10 +542,49 @@ interface ParameterName {
510
542
  /** The parameter's index in its list. */
511
543
  readonly at: number;
512
544
  readonly pattern: boolean;
513
- /** 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
+ */
514
549
  readonly exempt: boolean;
515
550
  }
516
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
+
517
588
  /** What one file's walk collects before the batch that resolves its names. */
518
589
  interface FileWalk {
519
590
  readonly declared: Declared[];
@@ -524,6 +595,8 @@ interface FileWalk {
524
595
  readonly holder: string;
525
596
  }[];
526
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 }[];
527
600
  readonly unreachable: { readonly statement: Statement; readonly holder: string }[];
528
601
  readonly cases: { readonly clause: Node; readonly text: string; readonly holder: string }[];
529
602
  }
@@ -548,8 +621,13 @@ export function projectParts<Brand>(
548
621
  const constructions = new Set<string>();
549
622
  const sites: { readonly file: SourceFile; readonly declared: Declared }[] = [];
550
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;
551
628
 
552
629
  for (const file of files) {
630
+ const checked = checkedByTheCompiler(file, options.checkJs === true);
553
631
  // A parameter property's own parts stay with the parameter when its member goes.
554
632
  const declarationAt = (node: Node): string | undefined =>
555
633
  isParameterProperty(node) ? undefined : held.declarations.get(nodeKey(file, node));
@@ -569,6 +647,7 @@ export function projectParts<Brand>(
569
647
  values: [],
570
648
  catches: [],
571
649
  blocks: [],
650
+ loops: [],
572
651
  unreachable: [],
573
652
  cases: [],
574
653
  };
@@ -667,6 +746,18 @@ export function projectParts<Brand>(
667
746
  if (isBlock(node) && silenced === 0 && holderOf(node) !== undefined) {
668
747
  walk.blocks.push(node);
669
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
+ }
670
761
  if (isBlock(node) || isCaseClause(node) || isDefaultClause(node)) {
671
762
  statementsOf(node.statements);
672
763
  return;
@@ -699,7 +790,44 @@ export function projectParts<Brand>(
699
790
  for (const store of stores) {
700
791
  asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
701
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);
702
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
+ }
703
831
 
704
832
  for (const one of walk.declared) {
705
833
  const constructor = isConstructorDeclaration(one.node);
@@ -714,7 +842,10 @@ export function projectParts<Brand>(
714
842
  });
715
843
  const reader = readers.get(one.signature);
716
844
  if (reader !== undefined) {
717
- 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
+ });
718
849
  parts.push(...found.parts);
719
850
  contextual.push(...found.contextual);
720
851
  }
@@ -722,15 +853,10 @@ export function projectParts<Brand>(
722
853
  for (const one of walk.values) {
723
854
  const reader = readers.get(one.signature);
724
855
  if (reader !== undefined) {
725
- const found = parameterParts(
726
- file,
727
- targetRoot,
728
- one.holder,
729
- one.signature,
730
- reader,
731
- ids,
732
- true,
733
- );
856
+ const found = parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, {
857
+ inValue: true,
858
+ checked,
859
+ });
734
860
  parts.push(...found.parts);
735
861
  contextual.push(...found.contextual);
736
862
  }
@@ -746,6 +872,19 @@ export function projectParts<Brand>(
746
872
  used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
747
873
  });
748
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
+ }
749
888
  walk.blocks.forEach((block, at) => {
750
889
  const holder = holderOf(block);
751
890
  if (holder !== undefined) {
@@ -779,6 +918,7 @@ export function projectParts<Brand>(
779
918
  parts.push(...resultParts(project, targetRoot, sites, references, calls));
780
919
  return {
781
920
  parts: withContextualRead(project, parts, contextual),
921
+ imports,
782
922
  signatures,
783
923
  calls,
784
924
  specifiers,
@@ -807,7 +947,9 @@ function valueSignature(node: Node): SignatureNode | undefined {
807
947
  /**
808
948
  * Every name one function's parameters bind, a binding pattern's names one by one. A
809
949
  * `this` parameter receives no argument, a decorated one is read by its decorator, and a
810
- * 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.
811
953
  */
812
954
  function parameterNames(signature: SignatureNode): ParameterName[] {
813
955
  const found: ParameterName[] = [];
@@ -820,7 +962,8 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
820
962
  continue;
821
963
  }
822
964
  if (isIdentifier(element.name)) {
823
- const exempt = rest && element.dotDotDotToken === undefined;
965
+ const exempt =
966
+ (rest && element.dotDotDotToken === undefined) || declaredUnread(element.name);
824
967
  found.push({ name: element.name, at, pattern: true, exempt });
825
968
  } else {
826
969
  patternNames(element.name, at);
@@ -837,7 +980,7 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
837
980
  }
838
981
  if (isIdentifier(name)) {
839
982
  if (name.text !== "this") {
840
- found.push({ name, at, pattern: false, exempt: false });
983
+ found.push({ name, at, pattern: false, exempt: declaredUnread(name) });
841
984
  }
842
985
  return;
843
986
  }
@@ -850,13 +993,110 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
850
993
  interface ContextualParameter {
851
994
  readonly part: PartFact;
852
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));
1028
+ }
1029
+
1030
+ /**
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
+ });
853
1092
  }
854
1093
 
855
1094
  /**
856
- * The parts with each name of `contextual` read where the compiler gives its function a
857
- * contextual type: the inference of the call holding the function may read a parameter's
858
- * annotation, and the code handing the argument may act on what an object pattern reads.
859
- * A question the checker leaves unanswered keeps the name read.
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.
860
1100
  */
861
1101
  function withContextualRead<Brand>(
862
1102
  project: ProjectView<Brand>,
@@ -868,9 +1108,28 @@ function withContextualRead<Brand>(
868
1108
  }
869
1109
  const signatures = [...new Set(contextual.map((one) => one.signature))];
870
1110
  const answers = project.queries.contextualTypes(signatures);
871
- const typed = new Set(signatures.filter((_signature, at) => answers[at] !== undefined));
872
- const read = new Set(contextual.filter((one) => typed.has(one.signature)).map((one) => one.part));
873
- 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);
874
1133
  }
875
1134
 
876
1135
  /**
@@ -886,7 +1145,7 @@ function parameterParts(
886
1145
  signature: SignatureNode,
887
1146
  reader: { readonly names: readonly ParameterName[]; readonly uses: readonly Identifier[] },
888
1147
  ids: ReadonlyMap<Node, Answer<number>>,
889
- inValue: boolean,
1148
+ place: Pick<ParameterPlace, "inValue" | "checked">,
890
1149
  ): { readonly parts: PartFact[]; readonly contextual: ContextualParameter[] } {
891
1150
  // A function that reads its `arguments` reads every parameter by position. A nested
892
1151
  // function other than an arrow has an `arguments` object of its own.
@@ -917,7 +1176,7 @@ function parameterParts(
917
1176
  position: spanOf(file, root, name),
918
1177
  message: `parameter ${name.text} is never read in the body`,
919
1178
  used: read[index] === true,
920
- parameter: { beforeARead: !pattern && at < lastRead, inValue },
1179
+ parameter: { beforeARead: !pattern && at < lastRead, ...place },
921
1180
  };
922
1181
  found.push(part);
923
1182
  const parameter = signature.parameters[at];
@@ -927,7 +1186,12 @@ function parameterParts(
927
1186
  parameter !== undefined &&
928
1187
  (parameter.type !== undefined || isObjectBindingPattern(parameter.name))
929
1188
  ) {
930
- contextual.push({ part, signature: expression });
1189
+ contextual.push({
1190
+ part,
1191
+ signature: expression,
1192
+ at,
1193
+ pattern: isObjectBindingPattern(parameter.name),
1194
+ });
931
1195
  }
932
1196
  });
933
1197
  return { parts: found, contextual };
@@ -939,44 +1203,85 @@ interface Local {
939
1203
  readonly statement: number;
940
1204
  readonly initialized: boolean;
941
1205
  readonly uses: readonly Identifier[];
1206
+ /** A `var` of a nested block, whose function scope outlives the block's end. */
1207
+ readonly outlives: boolean;
942
1208
  }
943
1209
 
944
1210
  /**
945
1211
  * The names one declaration binds that a write of its initializer stores each on its own:
946
- * 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.
947
1215
  */
948
- function arrayPatternNames(name: BindingName): readonly Identifier[] {
1216
+ function boundNames(name: BindingName): readonly Identifier[] {
949
1217
  if (isIdentifier(name)) {
950
1218
  return [name];
951
1219
  }
952
- if (!isArrayBindingPattern(name)) {
953
- 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
+ }
954
1243
  }
955
- return name.elements.flatMap((element) =>
956
- isBindingElement(element) && element.name !== undefined ? arrayPatternNames(element.name) : [],
957
- );
958
1244
  }
959
1245
 
960
- /** 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
+ */
961
1251
  function localsOf(block: Block): readonly Local[] {
962
- 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;
963
1260
  block.statements.forEach((statement, at) => {
964
1261
  if (!isVariableStatement(statement)) {
965
1262
  return;
966
1263
  }
967
1264
  const list = statement.declarationList;
968
- if (
969
- (list.flags & (NodeFlags.Let | NodeFlags.Const)) === 0 ||
970
- (list.flags & NodeFlags.Using) !== 0
971
- ) {
1265
+ const blockScoped = (list.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0;
1266
+ if ((!blockScoped && scope === undefined) || (list.flags & NodeFlags.Using) !== 0) {
972
1267
  return;
973
1268
  }
974
1269
  for (const declaration of list.declarations) {
975
- 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
+ }
976
1280
  declared.push({
977
1281
  name,
978
1282
  statement: at,
979
1283
  initialized: declaration.initializer !== undefined,
1284
+ outlives: !blockScoped && outlives,
980
1285
  });
981
1286
  }
982
1287
  }
@@ -991,14 +1296,23 @@ function localsOf(block: Block): readonly Local[] {
991
1296
  }));
992
1297
  }
993
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
+
994
1309
  /**
995
1310
  * The writes to the locals of one block that no read reaches. A write is a declaration's
996
- * initializer or a plain assignment written as a statement of the block itself; it is
997
- * dead when the statements after it in the block read the local nowhere before the next
998
- * such write to it, or before the block ends, which ends the local's scope. A statement
999
- * that writes the local inside a nested construct ends the search with the write kept,
1000
- * and a local a nested function names is no subject, because the function may read it
1001
- * 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.
1002
1316
  */
1003
1317
  function deadStores(
1004
1318
  file: SourceFile,
@@ -1009,8 +1323,22 @@ function deadStores(
1009
1323
  ids: ReadonlyMap<Node, Answer<number>>,
1010
1324
  ): PartFact[] {
1011
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
+ });
1012
1335
  for (const local of locals) {
1013
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
+ }
1014
1342
  if (uses.some((use) => inNestedFunction(use, block))) {
1015
1343
  continue;
1016
1344
  }
@@ -1050,7 +1378,7 @@ function deadStores(
1050
1378
  if (sameStatement.length > 0) {
1051
1379
  continue;
1052
1380
  }
1053
- let dead = true;
1381
+ let dead = !local.outlives;
1054
1382
  for (let at = write.at + 1; at < statements.length; at += 1) {
1055
1383
  const statement = statements[at];
1056
1384
  if (statement === undefined) {
@@ -1067,19 +1395,9 @@ function deadStores(
1067
1395
  break;
1068
1396
  }
1069
1397
  }
1070
- if (!dead) {
1071
- continue;
1398
+ if (dead) {
1399
+ found.push(deadAt(local, write.target));
1072
1400
  }
1073
- const position = spanOf(file, root, write.target);
1074
- found.push({
1075
- code: DEAD_STORE,
1076
- declaration: holder,
1077
- kind: "store",
1078
- name: local.name.text,
1079
- position,
1080
- message: `value written to ${local.name.text} is never read`,
1081
- used: false,
1082
- });
1083
1401
  }
1084
1402
  }
1085
1403
  return found;
@@ -1142,6 +1460,8 @@ function resultParts<Brand>(
1142
1460
  export interface IntraFunctionFacts {
1143
1461
  /** Every part of the run, each once, in use where any project found it in use. */
1144
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[];
1145
1465
  /** The declarations whose signature is free to change. */
1146
1466
  readonly free: ReadonlySet<string>;
1147
1467
  /** The declarations whose signature is free but for a use as a value. */
@@ -1172,6 +1492,15 @@ export function intraFunctionFacts(
1172
1492
  const known = parts.get(key);
1173
1493
  parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
1174
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
+ }
1175
1504
  const calls = new Map<string, CallUse>();
1176
1505
  const specifiers = new Set<string>();
1177
1506
  for (const one of projects) {
@@ -1231,5 +1560,11 @@ export function intraFunctionFacts(
1231
1560
  .filter(([id, all]) => all && !valued.has(id) && !sequenced.has(id))
1232
1561
  .map(([id]) => id),
1233
1562
  );
1234
- return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
1563
+ return {
1564
+ parts: [...parts.values()],
1565
+ imports: [...imports.values()],
1566
+ free,
1567
+ freeButValued,
1568
+ discardedEverywhere,
1569
+ };
1235
1570
  }