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

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/README.md +1 -1
  2. package/dist/src/analysis.js +61 -10
  3. package/dist/src/augmentations.js +63 -1
  4. package/dist/src/build-scripts.js +437 -0
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +56 -9
  7. package/dist/src/convention-rows.js +39 -2
  8. package/dist/src/conventions.js +15 -0
  9. package/dist/src/dependencies.js +98 -1
  10. package/dist/src/discover.js +14 -82
  11. package/dist/src/entry-points.js +9 -18
  12. package/dist/src/exempt.js +8 -0
  13. package/dist/src/findings/completion.js +66 -28
  14. package/dist/src/findings/intra-function.js +4 -2
  15. package/dist/src/findings/reads-and-writes.js +7 -6
  16. package/dist/src/findings/unused-declarations.js +4 -1
  17. package/dist/src/graph.js +5 -3
  18. package/dist/src/intra-function-parts.js +45 -17
  19. package/dist/src/kinds.js +8 -8
  20. package/dist/src/manifest.js +18 -0
  21. package/dist/src/matrix.js +38 -4
  22. package/dist/src/query.js +1 -0
  23. package/dist/src/report.js +23 -4
  24. package/dist/src/reporters/annotations.js +15 -8
  25. package/dist/src/reporters/sarif.js +12 -8
  26. package/dist/src/roots.js +197 -42
  27. package/dist/src/serialization-contract.js +590 -12
  28. package/dist/src/sweep.js +7 -9
  29. package/dist/src/test-globals.js +234 -0
  30. package/dist/src/type-errors.js +11 -3
  31. package/dist/src/unseen-loads.js +107 -0
  32. package/dist/src/verbs/analyze.js +24 -3
  33. package/dist/src/workflow-steps.js +32 -15
  34. package/dist/src/yaml-document.js +355 -0
  35. package/jsr.json +1 -1
  36. package/package.json +2 -2
  37. package/src/analysis.ts +109 -12
  38. package/src/augmentations.ts +77 -2
  39. package/src/build-scripts.ts +564 -0
  40. package/src/conformance-record.ts +1 -1
  41. package/src/contextual-uses.ts +64 -12
  42. package/src/convention-rows.ts +47 -2
  43. package/src/conventions.ts +16 -0
  44. package/src/dependencies.ts +110 -1
  45. package/src/discover.ts +14 -94
  46. package/src/entry-points.ts +10 -25
  47. package/src/exempt.ts +16 -0
  48. package/src/file-facts.ts +3 -0
  49. package/src/findings/boundary.ts +2 -0
  50. package/src/findings/completion.ts +76 -30
  51. package/src/findings/intra-function.ts +4 -2
  52. package/src/findings/reads-and-writes.ts +7 -6
  53. package/src/findings/unused-declarations.ts +6 -1
  54. package/src/graph.ts +5 -3
  55. package/src/intra-function-parts.ts +55 -16
  56. package/src/kinds.ts +8 -8
  57. package/src/manifest.ts +21 -3
  58. package/src/matrix.ts +51 -4
  59. package/src/query.ts +11 -0
  60. package/src/report.ts +56 -16
  61. package/src/reporters/annotations.ts +21 -10
  62. package/src/reporters/reporter.ts +1 -3
  63. package/src/reporters/sarif.ts +16 -8
  64. package/src/roots.ts +226 -46
  65. package/src/serialization-contract.ts +758 -12
  66. package/src/sweep.ts +23 -9
  67. package/src/test-globals.ts +275 -0
  68. package/src/type-errors.ts +14 -3
  69. package/src/unseen-loads.ts +145 -0
  70. package/src/verbs/analyze.ts +28 -3
  71. package/src/workflow-steps.ts +35 -17
  72. package/src/yaml-document.ts +377 -0
package/src/file-facts.ts CHANGED
@@ -11,6 +11,7 @@ import { packageScope } from "./inventory.ts";
11
11
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
12
12
  import { renderRef } from "./ref.ts";
13
13
  import type { ProjectView } from "./session.ts";
14
+ import type { UnseenLoad } from "./unseen-loads.ts";
14
15
 
15
16
  /** A file name the compiler reads as a source file. */
16
17
  const SOURCE_FILE = /\.(?:ts|tsx|mts|cts)$/u;
@@ -51,6 +52,8 @@ export interface FileFacts {
51
52
  readonly generated?: ReadonlySet<string>;
52
53
  /** The property names an `in` expression of a production file tests by a literal. */
53
54
  readonly inOperands?: ReadonlySet<string>;
55
+ /** The loads of production files whose target the analysis cannot resolve. */
56
+ readonly unseenLoads?: readonly UnseenLoad[];
54
57
  }
55
58
 
56
59
  /** The line of the last character of one text, counted from one; an empty text is one line. */
@@ -25,4 +25,6 @@ export interface Boundary {
25
25
  readonly encapsulated: boolean;
26
26
  /** Every side of a declared cross-language edge that names a symbol of this language. */
27
27
  readonly edges: readonly EdgeSide[];
28
+ /** The members a function outside the analysis fills, which it may read unseen. */
29
+ readonly filledOutside?: ReadonlySet<string>;
28
30
  }
@@ -112,10 +112,12 @@ function testedByIn(input: EmitterInput): ReadonlySet<string> {
112
112
  * published API, a public member of a published type included, is `certain` where the
113
113
  * run loaded every consumer the scope declared and declared one, `probable` where one did
114
114
  * not load, and `possible` where it declared none, whatever the configuration says of the
115
- * consumer set. A member an `in` expression tests by name is `possible`.
115
+ * consumer set. A member an `in` expression tests by name, and one a function outside the
116
+ * analysis fills, is `possible`.
116
117
  */
117
118
  function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
118
119
  const tested = testedByIn(input);
120
+ const filled = input.boundary.filledOutside ?? new Set<string>();
119
121
  const published = publishedIds(input.swept.matrix.union);
120
122
  const { declared } = input.boundary.consumers;
121
123
  const outside: Confidence =
@@ -125,13 +127,27 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
125
127
  ? "certain"
126
128
  : "probable";
127
129
  return (id) => {
128
- if (tested.has(id)) {
130
+ if (tested.has(id) || filled.has(id)) {
129
131
  return "possible";
130
132
  }
131
133
  return published.has(id) ? outside : "certain";
132
134
  };
133
135
  }
134
136
 
137
+ /**
138
+ * Whether one test of dead code references a target symbol that is published API of a run
139
+ * that declares no consumer, which outside programs the run cannot see may use, so the
140
+ * target counts as live for the test and the test is no test of dead code.
141
+ */
142
+ export function testsUnseenAPI(input: EmitterInput): (id: string) => boolean {
143
+ if (input.boundary.consumers.declared.length > 0) {
144
+ return () => false;
145
+ }
146
+ const union = input.swept.matrix.union;
147
+ const published = publishedIds(union);
148
+ return (id) => testTargets(union, id).some((target) => published.has(target));
149
+ }
150
+
135
151
  /** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
136
152
  function testTargets(union: Graph, id: string): readonly string[] {
137
153
  return (union.out[union.at(id)] ?? []).flatMap((edge) => {
@@ -197,17 +213,21 @@ function testUsedSubjects(input: EmitterInput): {
197
213
 
198
214
  /**
199
215
  * The ceiling the roots the run found put on one dead subject: a subject other declarations
200
- * reference, found dead only because no root reaches them, is `probable` unless
201
- * the configuration declares the roots complete, because a root the analysis cannot see
202
- * would make it live; every other declaration has none below `certain`.
216
+ * reference, found dead only because no root reaches them, is `probable` unless the
217
+ * configuration declares the roots complete and no configuration holding the subject loads
218
+ * a module the analysis cannot resolve, because a root the analysis cannot see would make
219
+ * it live; every other declaration has none below `certain`.
203
220
  */
204
221
  function rootsCeiling(
205
222
  input: EmitterInput,
206
223
  finding: Finding,
207
224
  candidate: Candidate | undefined,
225
+ configurations: readonly string[],
208
226
  ): Confidence {
209
227
  const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
210
- return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
228
+ const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
229
+ const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
230
+ return unreachable && !complete ? "probable" : "certain";
211
231
  }
212
232
 
213
233
  /**
@@ -225,9 +245,28 @@ function findingComponents(
225
245
  held.set(member, component);
226
246
  }
227
247
  }
248
+ const listed = membersListed(swept);
249
+ return (id) => {
250
+ const component = held.get(id);
251
+ return component === undefined
252
+ ? undefined
253
+ : {
254
+ id: component.id,
255
+ root: component.roots.includes(id),
256
+ symbolCount: component.members.length,
257
+ deletableLines: component.deletableLines,
258
+ ...(cascade === "full" ? { members: listed(component.members) } : {}),
259
+ };
260
+ };
261
+ }
262
+
263
+ /** The members of a component as the run lists them, positioned and in the canonical order. */
264
+ function membersListed(
265
+ swept: RunSweep,
266
+ ): (members: readonly string[]) => readonly PositionedSymbol[] {
228
267
  const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
229
- const listed = (component: Component): readonly PositionedSymbol[] =>
230
- component.members
268
+ return (members) =>
269
+ members
231
270
  .flatMap((member) => {
232
271
  const symbol = symbols.get(member);
233
272
  return symbol === undefined
@@ -244,18 +283,6 @@ function findingComponents(
244
283
  ];
245
284
  })
246
285
  .sort((a, b) => byPosition(a.position, b.position));
247
- return (id) => {
248
- const component = held.get(id);
249
- return component === undefined
250
- ? undefined
251
- : {
252
- id: component.id,
253
- root: component.roots.includes(id),
254
- symbolCount: component.members.length,
255
- deletableLines: component.deletableLines,
256
- ...(cascade === "full" ? { members: listed(component) } : {}),
257
- };
258
- };
259
286
  }
260
287
 
261
288
  /**
@@ -357,12 +384,13 @@ export function completed(
357
384
  // A join keeps its first component's identifier, so a minted one counts past every computed one.
358
385
  let minted = computed;
359
386
  const dead = new Set(components.map((component) => component.id));
360
- const fallingWith = new Map<string, number>();
361
- for (const { heldByTests } of swept.sweep.candidates) {
387
+ const fallingWith = new Map<string, string[]>();
388
+ for (const { id, heldByTests } of swept.sweep.candidates) {
362
389
  if (heldByTests !== undefined) {
363
- fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
390
+ fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
364
391
  }
365
392
  }
393
+ const listed = membersListed(swept);
366
394
 
367
395
  const done = findings.map((finding): CompletedFinding => {
368
396
  const row = kinds.get(finding.code);
@@ -413,11 +441,25 @@ export function completed(
413
441
  let component = found;
414
442
  if (component === undefined) {
415
443
  minted += 1;
444
+ const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
416
445
  component = {
417
446
  id: componentId(minted),
418
447
  root: true,
419
- symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
448
+ symbolCount: falling?.length ?? 1,
420
449
  deletableLines: 0,
450
+ ...(input.config.reporters.cascade !== "full"
451
+ ? {}
452
+ : falling === undefined
453
+ ? {
454
+ members: [
455
+ {
456
+ ref: finding.symbol.ref,
457
+ name: finding.symbol.name,
458
+ position: finding.position,
459
+ },
460
+ ],
461
+ }
462
+ : { members: listed(falling) }),
421
463
  };
422
464
  }
423
465
  const counts =
@@ -431,8 +473,16 @@ export function completed(
431
473
  ? candidate !== undefined && testUsed.declarations.has(judged.id)
432
474
  : finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
433
475
  const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
476
+ const configurations =
477
+ candidate?.configurations ??
478
+ (judged === undefined && !part
479
+ ? swept.matrix.configurations
480
+ : (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
434
481
  const untested = capped(
435
- capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate)),
482
+ capped(
483
+ capped(ownClass, row.maxClass),
484
+ rootsCeiling(input, finding, candidate, configurations),
485
+ ),
436
486
  judged === undefined ? "certain" : ceilingOf(judged.id),
437
487
  );
438
488
  const generated = input.files.generated?.has(finding.position.path) === true;
@@ -466,11 +516,7 @@ export function completed(
466
516
  generated,
467
517
  component,
468
518
  retainedBy: [],
469
- configurations:
470
- candidate?.configurations ??
471
- (judged === undefined && !part
472
- ? swept.matrix.configurations
473
- : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
519
+ configurations,
474
520
  consumersLoaded: input.boundary.consumers.loaded,
475
521
  fixability,
476
522
  severity: severityOf(input, finding.code, row.defaultSeverity),
@@ -42,7 +42,8 @@ type ParameterVerdict =
42
42
  * caller may pass a function used as a value more arguments than it declares, so there one
43
43
  * a read parameter follows is renamed to an underscore name, which the compilers read as
44
44
  * declared unread, and one no read parameter follows is deleted: kept where the call's
45
- * inference may read its annotation, at `probable` where nothing reads the annotation.
45
+ * inference may read its annotation, at `probable` where nothing reads the annotation or
46
+ * where it is a name a contextually typed object pattern binds.
46
47
  */
47
48
  function parameterVerdict(
48
49
  part: PartFact,
@@ -63,7 +64,8 @@ function parameterVerdict(
63
64
  if (place?.annotated === "steers") {
64
65
  return undefined;
65
66
  }
66
- return { fix: "delete", confidence: place?.annotated === "free" ? "probable" : "certain" };
67
+ const unseen = place?.annotated === "free" || place?.contextualPattern === true;
68
+ return { fix: "delete", confidence: unseen ? "probable" : "certain" };
67
69
  }
68
70
 
69
71
  /**
@@ -8,8 +8,8 @@ import type { Emitter, EmitterInput } from "./emitter.ts";
8
8
  /** The codes of this family. */
9
9
  type ReadsAndWritesCode = "DS1301" | "DS1302" | "DS1303";
10
10
 
11
- /** The containers whose type parameters `DS1303` reports: a function and a class method. */
12
- const SIGNATURES: ReadonlySet<string> = new Set(["function", "method"]);
11
+ /** The containers whose type parameters `DS1303` reports: a function, a class method and a type alias. */
12
+ const SIGNATURES: ReadonlySet<string> = new Set(["function", "method", "type"]);
13
13
 
14
14
  /** What the family reads of one run, indexed by declaration. */
15
15
  interface Run {
@@ -225,10 +225,11 @@ function unusedEnumMembers(run: Run): readonly Finding[] {
225
225
  }
226
226
 
227
227
  /**
228
- * `DS1303`: a type parameter of a function or a class method that neither the
229
- * declaration's signature nor its body names. A type parameter of a class, an interface,
230
- * a type alias or a method signature belongs to a type declaration and is never
231
- * reported, because a phantom parameter makes two instantiations distinct types.
228
+ * `DS1303`: a type parameter of a function, a class method or a type alias that neither
229
+ * the declaration's signature nor its body names. An alias names no type of its own, so
230
+ * a parameter its type never names makes no two instantiations distinct. One of a class,
231
+ * an interface or a method signature is never reported: a class or an interface merges
232
+ * only with declarations of the same parameters.
232
233
  */
233
234
  function unusedTypeParameters(run: Run): readonly Finding[] {
234
235
  return unnamed(run, "type-parameter")
@@ -2,6 +2,7 @@ import type { Finding } from "../finding.ts";
2
2
  import type { InventorySymbol, SymbolKind } from "../inventory.ts";
3
3
  import type { MatrixCandidate } from "../matrix.ts";
4
4
  import type { Emitter, EmitterInput } from "./emitter.ts";
5
+ import { testsUnseenAPI } from "./completion.ts";
5
6
  import { unreachableExports } from "./visibility-narrowing.ts";
6
7
 
7
8
  /** The kinds of declaration that are members of a class, an interface, a type or an enum. */
@@ -55,6 +56,8 @@ interface Judged {
55
56
  readonly deprecated: boolean;
56
57
  /** Whether the unreachable-export kind reports it. */
57
58
  readonly unreachable: boolean;
59
+ /** Whether it is a test with a target that is published API no declared consumer judges. */
60
+ readonly testsUnseenAPI: boolean;
58
61
  }
59
62
 
60
63
  /**
@@ -68,7 +71,7 @@ interface Judged {
68
71
  function codeOf(judged: Judged): string | undefined {
69
72
  const { candidate, symbol } = judged;
70
73
  if (candidate.testOfDeadCode) {
71
- return "DS1005";
74
+ return judged.testsUnseenAPI ? undefined : "DS1005";
72
75
  }
73
76
  if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
74
77
  return undefined;
@@ -241,6 +244,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
241
244
  const union = input.swept.matrix.union;
242
245
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
243
246
  const unreachable = unreachableExports(input);
247
+ const unseenAPI = testsUnseenAPI(input);
244
248
  let brands: ReadonlySet<string> | undefined;
245
249
 
246
250
  const found: Finding[] = [];
@@ -260,6 +264,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
260
264
  test: union.test[at] === true,
261
265
  deprecated: input.deprecated.has(symbol.id),
262
266
  unreachable: unreachable.has(symbol.id),
267
+ testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
263
268
  };
264
269
  const code = codeOf(judged);
265
270
  if (
package/src/graph.ts CHANGED
@@ -41,9 +41,9 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
41
41
  * Whether a root of each kind names a caller, which holds it live under both relations: a
42
42
  * tool reads what it loads and what its strings name, a runtime runs a test, setup or
43
43
  * worker file, a page's module script, a command, a script or a workflow step, the manifest
44
- * names what enters the package, an alias global stands for its export, and an entry file,
45
- * a configured root and a pattern assert a caller. A library's published API only supposes
46
- * an unseen consumer, so it is live under reachability alone.
44
+ * names what enters the package, an alias global stands for its export, an outside
45
+ * declaration names a global interface merging into it, and an entry file, a configured root
46
+ * and a pattern assert a caller. A published API supposes a consumer: reachability alone.
47
47
  */
48
48
  const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
49
49
  "entry-file": true,
@@ -56,8 +56,10 @@ const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
56
56
  pattern: true,
57
57
  "type-error": true,
58
58
  "type-query-alias": true,
59
+ "global-augmentation": true,
59
60
  "configuration-file": true,
60
61
  "configuration-string": true,
62
+ "bundler-entry": true,
61
63
  "test-runner": true,
62
64
  "browser-tests": true,
63
65
  worker: true,
@@ -137,6 +137,12 @@ interface ParameterPlace {
137
137
  * so the annotation may steer the call's inference.
138
138
  */
139
139
  readonly annotated?: "steers" | "free";
140
+ /**
141
+ * Whether the name is one an object binding pattern of a parameter binds, and the
142
+ * compiler gives its function a contextual type: the code handing the argument may act on
143
+ * the pattern's read of the property, which the program does not show.
144
+ */
145
+ readonly contextualPattern?: true;
140
146
  /** Whether the compiler checks the parameter's file, so its unused-parameter rule may report it. */
141
147
  readonly checked: boolean;
142
148
  }
@@ -1093,10 +1099,9 @@ function calleeSteers<Brand>(
1093
1099
 
1094
1100
  /**
1095
1101
  * 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.
1102
+ * contextual type: a name an object pattern binds is marked as one, and an annotated
1103
+ * parameter with whether the call's inference may read its annotation. A question the
1104
+ * checker leaves unanswered keeps the name read.
1100
1105
  */
1101
1106
  function withContextualRead<Brand>(
1102
1107
  project: ProjectView<Brand>,
@@ -1118,15 +1123,17 @@ function withContextualRead<Brand>(
1118
1123
  const place = one.part.parameter;
1119
1124
  judged.set(
1120
1125
  one.part,
1121
- one.pattern || answer === UNANSWERED || place === undefined
1126
+ answer === UNANSWERED || place === undefined
1122
1127
  ? { ...one.part, used: true }
1123
- : {
1124
- ...one.part,
1125
- parameter: {
1126
- ...place,
1127
- annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
1128
+ : one.pattern
1129
+ ? { ...one.part, parameter: { ...place, contextualPattern: true } }
1130
+ : {
1131
+ ...one.part,
1132
+ parameter: {
1133
+ ...place,
1134
+ annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
1135
+ },
1128
1136
  },
1129
- },
1130
1137
  );
1131
1138
  }
1132
1139
  return parts.map((part) => judged.get(part) ?? part);
@@ -1205,6 +1212,8 @@ interface Local {
1205
1212
  readonly uses: readonly Identifier[];
1206
1213
  /** A `var` of a nested block, whose function scope outlives the block's end. */
1207
1214
  readonly outlives: boolean;
1215
+ /** A function declaration, written at the start of the block whatever its place. */
1216
+ readonly hoisted: boolean;
1208
1217
  }
1209
1218
 
1210
1219
  /**
@@ -1244,9 +1253,10 @@ function functionOf(block: Block): FunctionLikeDeclaration | undefined {
1244
1253
  }
1245
1254
 
1246
1255
  /**
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.
1256
+ * The locals one block declares with `let`, `const`, `var` or a function declaration
1257
+ * inside a function, each with the names of the block that spell it. A `var` of a nested
1258
+ * block is one where no nested function of its function names it, since the function scope
1259
+ * holds it past the block's end.
1250
1260
  */
1251
1261
  function localsOf(block: Block): readonly Local[] {
1252
1262
  const declared: {
@@ -1254,10 +1264,23 @@ function localsOf(block: Block): readonly Local[] {
1254
1264
  statement: number;
1255
1265
  initialized: boolean;
1256
1266
  outlives: boolean;
1267
+ hoisted: boolean;
1257
1268
  }[] = [];
1258
1269
  const scope = functionOf(block);
1259
1270
  const outlives = scope?.body !== block;
1260
1271
  block.statements.forEach((statement, at) => {
1272
+ if (isFunctionDeclaration(statement)) {
1273
+ if (scope !== undefined && statement.name !== undefined && statement.body !== undefined) {
1274
+ declared.push({
1275
+ name: statement.name,
1276
+ statement: at,
1277
+ initialized: true,
1278
+ outlives: false,
1279
+ hoisted: true,
1280
+ });
1281
+ }
1282
+ return;
1283
+ }
1261
1284
  if (!isVariableStatement(statement)) {
1262
1285
  return;
1263
1286
  }
@@ -1267,6 +1290,11 @@ function localsOf(block: Block): readonly Local[] {
1267
1290
  return;
1268
1291
  }
1269
1292
  for (const declaration of list.declarations) {
1293
+ // A write to a local typed `never` is the compiler's exhaustiveness check, which
1294
+ // fails to compile once a case goes unhandled.
1295
+ if (declaration.type?.kind === SyntaxKind.NeverKeyword) {
1296
+ continue;
1297
+ }
1270
1298
  for (const name of boundNames(declaration.name)) {
1271
1299
  if (
1272
1300
  !blockScoped &&
@@ -1282,6 +1310,7 @@ function localsOf(block: Block): readonly Local[] {
1282
1310
  statement: at,
1283
1311
  initialized: declaration.initializer !== undefined,
1284
1312
  outlives: !blockScoped && outlives,
1313
+ hoisted: false,
1285
1314
  });
1286
1315
  }
1287
1316
  }
@@ -1333,12 +1362,22 @@ function deadStores(
1333
1362
  used: false,
1334
1363
  });
1335
1364
  for (const local of locals) {
1336
- const uses = local.uses.filter((use) => readsBinding(use, local.name, ids));
1365
+ const own = local.hoisted ? block.statements[local.statement] : undefined;
1366
+ // A function's reads of itself keep it no more than its absence would.
1367
+ const uses = local.uses.filter(
1368
+ (use) => readsBinding(use, local.name, ids) && (own === undefined || !within(use, own)),
1369
+ );
1337
1370
  if (!local.outlives && uses.every(isPlainWrite)) {
1338
- const targets = [...(local.initialized ? [local.name] : []), ...uses];
1371
+ // A declaration with no initializer writes `undefined`, the one store left to name.
1372
+ const writes = [...(local.initialized ? [local.name] : []), ...uses];
1373
+ const targets = writes.length === 0 ? [local.name] : writes;
1339
1374
  found.push(...targets.map((target) => deadAt(local, target)));
1340
1375
  continue;
1341
1376
  }
1377
+ // A read anywhere in the block reads a function declaration, which is written first.
1378
+ if (local.hoisted) {
1379
+ continue;
1380
+ }
1342
1381
  if (uses.some((use) => inNestedFunction(use, block))) {
1343
1382
  continue;
1344
1383
  }
package/src/kinds.ts CHANGED
@@ -87,7 +87,7 @@ const ROWS: readonly KindRow[] = [
87
87
  code: "DS1005",
88
88
  name: "test-of-dead-code",
89
89
  languages: ["go", "ts"],
90
- rule: "A test symbol whose set of referenced target symbols is non-empty and every member of that set is reported dead. In TypeScript an ambient declaration, one a declaration file holds or one a `declare` modifier carries or a declaration carrying one holds, describes code the target does not implement, so it is no target symbol of that set. A test that references at least one live target symbol is never reported, no notion of a test's subject and no name matching enters the rule, and the message states the rule. The test joins the dead component of the symbols it references, and it and each of them count as referencing each other, so the test is a member of their cycle and never a dead symbol outside it.",
90
+ rule: "A test symbol whose set of referenced target symbols is non-empty and every member of that set is reported dead. In TypeScript an ambient declaration, one a declaration file holds or one a `declare` modifier carries or a declaration carrying one holds, describes code the target does not implement, so it is no target symbol of that set. A test that references at least one live target symbol is never reported, no notion of a test's subject and no name matching enters the rule, and the message states the rule. A production declaration whose reachability class is `possible` because programs outside the analysis may use it, being published API of a library whose run declares no consumer, and in Go a declaration of a package no build links or an export of a unit a dropped module requires, counts as a live target symbol of every test that references it, so such a test is no test of dead code. The declaration is still reported at `possible`. The test joins the dead component of the symbols it references, and it and each of them count as referencing each other, so the test is a member of their cycle and never a dead symbol outside it.",
91
91
  defaultSeverity: "deny",
92
92
  maxClass: "certain",
93
93
  fixability: "deletable",
@@ -146,7 +146,7 @@ const ROWS: readonly KindRow[] = [
146
146
  languages: ["ts"],
147
147
  rule: "An exported declaration used only inside its own file, reported as a candidate for removing the export keyword. The finding names the narrower visibility the references support.",
148
148
  precondition:
149
- 'Closed world only. Reported in a file that is not an entry file, a file only a configuration string or a script token names being none, because each runs the file and reads no name it exports, and a test file being one only for the exports its test runner calls, as grammar/analysis.md "Test code" states, and a file a Markdown or MDX document imports one only for the exports the import reads, as grammar/analysis.md "Markdown and MDX documents" states, and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file. Where the edge\'s other side is unknown to the analyzer, the finding is emitted pending. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.',
149
+ 'Closed world only. Reported in a file that is not an entry file, a file only a configuration string or a script token names being none, because each runs the file and reads no name it exports, while a file a build script\'s bundler call names is one, because the bundle exposes what it exports, and a test file being one only for the exports its test runner calls, as grammar/analysis.md "Test code" states, and a file a Markdown or MDX document imports one only for the exports the import reads, as grammar/analysis.md "Markdown and MDX documents" states, and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file. Where the edge\'s other side is unknown to the analyzer, the finding is emitted pending. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.',
150
150
  defaultSeverity: "warn",
151
151
  maxClass: "certain",
152
152
  fixability: "narrowable",
@@ -191,7 +191,7 @@ const ROWS: readonly KindRow[] = [
191
191
  code: "DS1301",
192
192
  name: "write-only-symbol",
193
193
  languages: ["go", "ts"],
194
- rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. In Go a read counts only from a declaration that is live under the finding's liveness relation, which is reference counting: a read from a declaration nothing references, whose own finding has the reachability class `certain`, is no read, so a declaration that only such declarations read is reported where a declaration something references writes it. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. A TypeScript parameter property, a constructor parameter that carries a modifier, declares a member of its class that the constructor writes with its argument, so the member is judged under this rule, and under every rule that reads a class member, as a member the class body declares is. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
194
+ rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. In Go a read counts only from a declaration that is live under the finding's liveness relation, which is reference counting: a read from a declaration nothing references, whose own finding has the reachability class `certain`, is no read, so a declaration that only such declarations read is reported where a declaration something references writes it. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. A TypeScript parameter property, a constructor parameter that carries a modifier, declares a member of its class that the constructor writes with its argument, so the member is judged under this rule, and under every rule that reads a class member, as a member the class body declares is. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A literal whose type the compiler infers, as one a callback returns to a generic call, writes in the same way the member of each property's name that each object type a value of the literal's type reaches declares, through array elements, type arguments and the parameters and results of functions, so `ids.map((id) => ({ id }))` returned as `Row[]` writes `Row.id`. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
195
195
  defaultSeverity: "deny",
196
196
  maxClass: "certain",
197
197
  fixability: "deletable",
@@ -212,9 +212,9 @@ const ROWS: readonly KindRow[] = [
212
212
  code: "DS1303",
213
213
  name: "unused-type-parameter",
214
214
  languages: ["go", "ts"],
215
- rule: "A type parameter of a function or method that no part of the declaration's signature and no part of the declaration's body names, at the certain class. The deletion is local to the declaration and the explicit instantiations the reference set already lists. A type parameter of a function or method that is itself dead falls with it and is reported under no code of its own.",
215
+ rule: "A type parameter of a function, a method or a TypeScript type alias that no part of the declaration's signature, body or aliased type names, at the certain class. The deletion is local to the declaration and the explicit instantiations the reference set already lists. A type parameter of a declaration that is itself dead falls with it and is reported under no code of its own.",
216
216
  precondition:
217
- "Function and method type parameters only. A type parameter of a type declaration is never reported, because a phantom type parameter such as `type ID[T any] int` makes two instantiations distinct types while naming the parameter nowhere, so deleting it changes the program. A method signature an interface or an object type declares belongs to that type declaration, so its type parameters are never reported either.",
217
+ "Function and method type parameters, and in TypeScript those of a type alias. A TypeScript alias names no type of its own, so two instantiations that differ only in a parameter its type never names are one type. A Go type declaration's type parameter is never reported, because a phantom type parameter such as `type ID[T any] int` makes two instantiations distinct types while naming the parameter nowhere, so deleting it changes the program. Nor is one of a TypeScript class or interface, which merges only with declarations of the same type parameters. A method signature an interface or an object type declares belongs to that type declaration, so its type parameters are never reported either.",
218
218
  defaultSeverity: "deny",
219
219
  maxClass: "certain",
220
220
  fixability: "deletable",
@@ -332,7 +332,7 @@ const ROWS: readonly KindRow[] = [
332
332
  languages: ["go", "ts"],
333
333
  rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
334
334
  precondition:
335
- "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, is not used as a value, is not a go:linkname or cgo target, is not a go-fuzz entry point, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. In Go a function literal that is the callee of the call it is written in, the call of a `go` or a `defer` statement included, is not used as a value, wherever the literal is written. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported for deletion when its body reads no parameter after it, and an unread parameter before the last read one is reported for a rename to its name with a leading underscore, which keeps every argument in place and which the compilers' unused-parameter rules read as declared unread, unless its name already starts with one. A parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type, and that carries a type annotation, is not reported for deletion where the inference of the call holding the function may read the annotation, being where the callee is generic and its parameter type at the function's place names a type parameter at the parameter's place, or where the function is written inside an object literal such a call is passed, and it is reported for deletion at `probable` where it carries an annotation no such inference reads, because nothing in the program shows that an outside reader ignores it. Such a parameter is not reported when it is an object binding pattern, because the pattern reads each property it names from the argument and the code that hands the argument may act on that read. No name such a pattern binds is reported either. Any other TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. A TypeScript constructor is used as a value where its class is used as a value other than as the callee of a `new` expression or the class an `extends` clause names, and its signature is not free where its class carries a decorator or a parameter of the constructor carries one, because the code that decorator or a container runs may construct the class. A parameter property is read by the write to its member the constructor makes, so it is never reported under this kind, and its member is judged under DS1301. A TypeScript name a parameter binds, whole or through a binding pattern, that starts with an underscore is never reported, because a leading underscore is the spelling unused-parameter rules read as declared unread, and no other name exempts a parameter. A Go parameter named by the blank identifier declares no name a body could read and is never reported.",
335
+ "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, is not used as a value, is not a go:linkname or cgo target, is not a go-fuzz entry point, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. In Go a function literal that is the callee of the call it is written in, the call of a `go` or a `defer` statement included, is not used as a value, wherever the literal is written. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported for deletion when its body reads no parameter after it, and an unread parameter before the last read one is reported for a rename to its name with a leading underscore, which keeps every argument in place and which the compilers' unused-parameter rules read as declared unread, unless its name already starts with one. A parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type, and that carries a type annotation, is not reported for deletion where the inference of the call holding the function may read the annotation, being where the callee is generic and its parameter type at the function's place names a type parameter at the parameter's place, or where the function is written inside an object literal such a call is passed, and it is reported for deletion at `probable` where it carries an annotation no such inference reads, because nothing in the program shows that an outside reader ignores it. A name an object binding pattern of a parameter of such a function binds, annotated or not, that the body never reads is reported for deletion at `probable`, because the pattern reads the property it names from the argument and the code that hands the argument may act on that read, which nothing in the program shows. Any other TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. A TypeScript constructor is used as a value where its class is used as a value other than as the callee of a `new` expression or the class an `extends` clause names, and its signature is not free where its class carries a decorator or a parameter of the constructor carries one, because the code that decorator or a container runs may construct the class. A parameter property is read by the write to its member the constructor makes, so it is never reported under this kind, and its member is judged under DS1301. A TypeScript name a parameter binds, whole or through a binding pattern, that starts with an underscore is never reported, because a leading underscore is the spelling unused-parameter rules read as declared unread, and no other name exempts a parameter. A Go parameter named by the blank identifier declares no name a body could read and is never reported.",
336
336
  defaultSeverity: "warn",
337
337
  maxClass: "certain",
338
338
  fixability: "manual",
@@ -358,7 +358,7 @@ const ROWS: readonly KindRow[] = [
358
358
  languages: ["go", "ts"],
359
359
  rule: "A result value that no call site of the function uses, on a function whose signature is free to change.",
360
360
  precondition:
361
- "The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible. A call that is the operand of an await expression written as a statement of its own uses a result whose type has a member named then, a promise or another thenable, because the caller sequences on it, and discards any other result. A call written as a statement of its own, or as the operand of void, discards its result whatever its type.",
361
+ "The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible. A function a program run by naming the file names, a file under the `ignore` tag or a script of a dropped module, has call sites outside the loaded graph. A call that is the operand of an await expression written as a statement of its own uses a result whose type has a member named then, a promise or another thenable, because the caller sequences on it, and discards any other result. A call written as a statement of its own, or as the operand of void, discards its result whatever its type.",
362
362
  defaultSeverity: "warn",
363
363
  maxClass: "certain",
364
364
  fixability: "manual",
@@ -382,7 +382,7 @@ const ROWS: readonly KindRow[] = [
382
382
  languages: ["go", "ts"],
383
383
  rule: "A write to a local variable with no read before the next write to it or the end of its scope, computed on the same read-and-write classification the write-only-symbol kind uses. The finding names the write position.",
384
384
  precondition:
385
- "In TypeScript the binding of a catch clause is a local variable the clause writes when it catches, so a binding that is a name and that the clause's block never reads is a dead store, positioned at the binding: the clause written with no binding behaves the same. A TypeScript local is a name a `let` or `const` declaration of a block binds, or a `var` declaration of a block of a function, a name an object or array binding pattern of one binds at any depth included, except a name with a default, whose removal skips the default's evaluation, and a name beside a rest element of its object pattern, whose removal changes what the rest holds. A `var` of a block nested in its function's body is judged in that block, its last write kept because the function scope reads on past the block's end, and is no subject where a nested function of its function names it. A local a nested function reads is no subject, because the function may read it at any time, and every write to a local nothing reads is dead, one a nested function makes included, each positioned at the name it writes. The variable a `let` or `const` declaration of a `for…of` or `for…in` statement binds is written at each iteration, so one the loop body never reads is a dead store positioned at the binding, except a name that starts with an underscore, because a loop must bind a name and that spelling declares it unread. In Go a function literal a package-level variable's initializer holds is a function body of its own, and a finding about one of its stores carries the reference of the variable the literal initializes, which for a blank variable is its package's, as `grammar/symbol-ref.md` states for a blank-identifier declaration. In Go a range statement stores its key and its value at the start of every iteration, and an empty range stores neither. In Go a function literal reads each variable it names where it is written, and a store that a literal naming its variable may precede on some path of the control-flow graph, the literal of an earlier iteration of a loop included, is not dead, because a call of the literal the graph does not order may read it. A store no such literal precedes is judged as any other. A literal that is the callee of the call it is written in, outside a `go` or `defer` statement, runs there, so it reads its variables at that call and precedes no later store.",
385
+ "In TypeScript the binding of a catch clause is a local variable the clause writes when it catches, so a binding that is a name and that the clause's block never reads is a dead store, positioned at the binding: the clause written with no binding behaves the same. A TypeScript local is a name a `let` or `const` declaration of a block binds, or a `var` declaration of a block of a function, a name an object or array binding pattern of one binds at any depth included, except a name with a default, whose removal skips the default's evaluation, and a name beside a rest element of its object pattern, whose removal changes what the rest holds. A function declaration of a block of a function is a TypeScript local too, written with its function when the block starts, wherever it stands in the block, so one that nothing outside its own body reads is a dead store positioned at its name, and a read anywhere else in the block reads it. A `let` or `var` declaration with no initializer writes `undefined`, so one that nothing reads and nothing writes is a dead store positioned at its name. A local whose declaration carries the type annotation `never` is no subject: a write to it is the compiler's exhaustiveness check, which fails to compile once a case goes unhandled, so deleting it changes what the program checks. A `var` of a block nested in its function's body is judged in that block, its last write kept because the function scope reads on past the block's end, and is no subject where a nested function of its function names it. A local a nested function reads is no subject, because the function may read it at any time, and every write to a local nothing reads is dead, one a nested function makes included, each positioned at the name it writes. The variable a `let` or `const` declaration of a `for…of` or `for…in` statement binds is written at each iteration, so one the loop body never reads is a dead store positioned at the binding, except a name that starts with an underscore, because a loop must bind a name and that spelling declares it unread. In Go a function literal a package-level variable's initializer holds is a function body of its own, and a finding about one of its stores carries the reference of the variable the literal initializes, which for a blank variable is its package's, as `grammar/symbol-ref.md` states for a blank-identifier declaration. In Go a range statement stores its key and its value at the start of every iteration, and an empty range stores neither. In Go a function literal reads each variable it names where it is written, and a store that a literal naming its variable may precede on some path of the control-flow graph, the literal of an earlier iteration of a loop included, is not dead, because a call of the literal the graph does not order may read it. A store no such literal precedes is judged as any other. A literal that is the callee of the call it is written in, outside a `go` or `defer` statement, runs there, so it reads its variables at that call and precedes no later store.",
386
386
  defaultSeverity: "deny",
387
387
  maxClass: "certain",
388
388
  fixability: "deletable",