@cplieger/deadset-ts 5.2.3 → 6.0.0

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 (73) hide show
  1. package/dist/src/analysis.js +28 -5
  2. package/dist/src/augmentations.js +50 -0
  3. package/dist/src/components.js +15 -15
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/convention-rows.js +17 -2
  6. package/dist/src/conventions.js +82 -1
  7. package/dist/src/dependencies.js +42 -26
  8. package/dist/src/entry-points.js +28 -14
  9. package/dist/src/exempt.js +1 -0
  10. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  11. package/dist/src/findings/emitters.js +47 -12
  12. package/dist/src/findings/interfaces.js +4 -3
  13. package/dist/src/findings/intra-function.js +15 -2
  14. package/dist/src/findings/unused-declarations.js +13 -1
  15. package/dist/src/findings-pass.js +1 -0
  16. package/dist/src/graph.js +6 -6
  17. package/dist/src/implementations.js +1 -1
  18. package/dist/src/indexed-access.js +215 -0
  19. package/dist/src/interface-satisfaction.js +85 -23
  20. package/dist/src/intra-function-parts.js +139 -30
  21. package/dist/src/inventory.js +6 -1
  22. package/dist/src/kinds.js +13 -12
  23. package/dist/src/matrix.js +34 -9
  24. package/dist/src/module-declarations.js +180 -0
  25. package/dist/src/outside-tests.js +17 -15
  26. package/dist/src/overrides.js +70 -9
  27. package/dist/src/query.js +7 -0
  28. package/dist/src/ref.js +1 -0
  29. package/dist/src/references.js +6 -0
  30. package/dist/src/report.js +6 -1
  31. package/dist/src/reporters/sarif.js +3 -1
  32. package/dist/src/reporters/text.js +23 -3
  33. package/dist/src/roots.js +50 -10
  34. package/dist/src/version.js +1 -1
  35. package/dist/src/workflow-steps.js +82 -0
  36. package/jsr.json +1 -1
  37. package/package.json +7 -1
  38. package/src/analysis.ts +40 -4
  39. package/src/augmentations.ts +72 -0
  40. package/src/components.ts +15 -13
  41. package/src/conformance-record.ts +2 -2
  42. package/src/convention-rows.ts +25 -3
  43. package/src/conventions.ts +106 -2
  44. package/src/dependencies.ts +51 -31
  45. package/src/entry-points.ts +32 -19
  46. package/src/exempt.ts +4 -1
  47. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  48. package/src/findings/emitters.ts +71 -12
  49. package/src/findings/interfaces.ts +7 -6
  50. package/src/findings/intra-function.ts +20 -2
  51. package/src/findings/unused-declarations.ts +16 -1
  52. package/src/findings-pass.ts +4 -1
  53. package/src/graph.ts +6 -6
  54. package/src/implementations.ts +1 -1
  55. package/src/indexed-access.ts +295 -0
  56. package/src/interface-satisfaction.ts +118 -27
  57. package/src/intra-function-parts.ts +196 -35
  58. package/src/inventory.ts +8 -0
  59. package/src/kinds.ts +14 -12
  60. package/src/matrix.ts +51 -8
  61. package/src/module-declarations.ts +241 -0
  62. package/src/outside-tests.ts +18 -16
  63. package/src/overrides.ts +80 -14
  64. package/src/query.ts +28 -0
  65. package/src/ref.ts +1 -0
  66. package/src/references.ts +6 -0
  67. package/src/report.ts +11 -1
  68. package/src/reporters/sarif.ts +4 -1
  69. package/src/reporters/text.ts +25 -3
  70. package/src/roots.ts +56 -7
  71. package/src/sweep.ts +12 -0
  72. package/src/version.ts +1 -1
  73. package/src/workflow-steps.ts +104 -0
@@ -12,8 +12,10 @@ import {
12
12
  isBinaryExpression,
13
13
  isBlock,
14
14
  isBreakStatement,
15
+ isBindingElement,
15
16
  isCallExpression,
16
17
  isCaseClause,
18
+ isCatchClause,
17
19
  isClassDeclaration,
18
20
  isClassExpression,
19
21
  isContinueStatement,
@@ -24,6 +26,7 @@ import {
24
26
  isExportSpecifier,
25
27
  isExpressionStatement,
26
28
  isFunctionDeclaration,
29
+ isFunctionExpression,
27
30
  isHeritageClause,
28
31
  isIdentifier,
29
32
  isIfStatement,
@@ -35,6 +38,8 @@ import {
35
38
  isNamespaceImport,
36
39
  isNoSubstitutionTemplateLiteral,
37
40
  isNumericLiteral,
41
+ isObjectBindingPattern,
42
+ isObjectLiteralExpression,
38
43
  isParenthesizedExpression,
39
44
  isPrefixUnaryExpression,
40
45
  isPropertyAccessExpression,
@@ -50,8 +55,10 @@ import {
50
55
  isVoidExpression,
51
56
  NodeFlags,
52
57
  SyntaxKind,
58
+ type BindingName,
53
59
  type Block,
54
60
  type CallExpression,
61
+ type CatchClause,
55
62
  type Identifier,
56
63
  type Node,
57
64
  type SourceFile,
@@ -81,6 +88,16 @@ export interface PartFact {
81
88
  readonly message: string;
82
89
  /** Whether the project found the part in use, which takes it out of the answer. */
83
90
  readonly used: boolean;
91
+ /** Where the part is a parameter's name, what a function used as a value decides it by. */
92
+ readonly parameter?: ParameterPlace;
93
+ }
94
+
95
+ /** What decides a parameter of a function used as a value: a caller may pass it more arguments. */
96
+ interface ParameterPlace {
97
+ /** Whether a parameter after this one, which is no binding-pattern name, is read. */
98
+ readonly beforeARead: boolean;
99
+ /** Whether no declaration of the inventory declares the function, which is then a value. */
100
+ readonly inValue: boolean;
84
101
  }
85
102
 
86
103
  /** What one declaration's signature answers to beside its body. */
@@ -119,9 +136,6 @@ const NO_VALUE: ReadonlySet<string> = new Set([
119
136
  "Promise<never>",
120
137
  ]);
121
138
 
122
- /** The prefix that marks a parameter as unused on purpose, as the compiler's own rule reads it. */
123
- const UNUSED_PREFIX = "_";
124
-
125
139
  /** The rendered position of one node, from its first token to its last line. */
126
140
  function spanOf(file: SourceFile, root: string, node: Node): FindingPosition {
127
141
  const start = renderPosition(file, root, node.getStart());
@@ -410,9 +424,31 @@ function readsBinding(
410
424
  return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
411
425
  }
412
426
 
427
+ /** A function written as a value that no inventory declaration declares, and what holds it. */
428
+ interface ValueFunction {
429
+ readonly holder: string;
430
+ readonly signature: SignatureNode;
431
+ }
432
+
433
+ /** One name a parameter binds, whole or through a binding pattern. */
434
+ interface ParameterName {
435
+ readonly name: Identifier;
436
+ /** The parameter's index in its list. */
437
+ readonly at: number;
438
+ readonly pattern: boolean;
439
+ /** A name beside a rest element of its object pattern: removing it changes what the rest holds. */
440
+ readonly exempt: boolean;
441
+ }
442
+
413
443
  /** What one file's walk collects before the batch that resolves its names. */
414
444
  interface FileWalk {
415
445
  readonly declared: Declared[];
446
+ readonly values: ValueFunction[];
447
+ readonly catches: {
448
+ readonly clause: CatchClause;
449
+ readonly name: Identifier;
450
+ readonly holder: string;
451
+ }[];
416
452
  readonly blocks: Block[];
417
453
  readonly unreachable: { readonly statement: Statement; readonly holder: string }[];
418
454
  readonly cases: { readonly clause: Node; readonly text: string; readonly holder: string }[];
@@ -443,6 +479,8 @@ export function projectParts<Brand>(
443
479
  const id = held.declarations.get(nodeKey(file, node));
444
480
  return id === undefined || kinds.get(id) === "file" ? undefined : id;
445
481
  };
482
+ // A part no declaration holds belongs to the file, which is judged while the file is live.
483
+ const fileId = held.declarations.get(nodeKey(file, file));
446
484
  const holderOf = (node: Node): string | undefined => {
447
485
  for (let at = node.parent; at.kind !== SyntaxKind.SourceFile; at = at.parent) {
448
486
  const id = declarationAt(at);
@@ -450,9 +488,17 @@ export function projectParts<Brand>(
450
488
  return id;
451
489
  }
452
490
  }
453
- return undefined;
491
+ return fileId;
454
492
  };
455
- const walk: FileWalk = { declared: [], blocks: [], unreachable: [], cases: [] };
493
+ const walk: FileWalk = {
494
+ declared: [],
495
+ values: [],
496
+ catches: [],
497
+ blocks: [],
498
+ unreachable: [],
499
+ cases: [],
500
+ };
501
+ const covered = new Set<Node>();
456
502
  const position = (node: Node): string =>
457
503
  positionKey(renderPosition(file, targetRoot, node.getStart()));
458
504
 
@@ -483,6 +529,19 @@ export function projectParts<Brand>(
483
529
  const signature = id === undefined ? undefined : signatureOf(node);
484
530
  if (id !== undefined && signature !== undefined) {
485
531
  walk.declared.push({ id, node, signature });
532
+ covered.add(signature);
533
+ }
534
+ const value = covered.has(node) ? undefined : valueSignature(node);
535
+ const valueHolder = id ?? holderOf(node);
536
+ if (value !== undefined && valueHolder !== undefined && !isStub(value)) {
537
+ walk.values.push({ holder: valueHolder, signature: value });
538
+ }
539
+ if (isCatchClause(node) && node.variableDeclaration !== undefined) {
540
+ const name = node.variableDeclaration.name;
541
+ const holder = holderOf(node);
542
+ if (isIdentifier(name) && holder !== undefined && silenced === 0) {
543
+ walk.catches.push({ clause: node, name, holder });
544
+ }
486
545
  }
487
546
  if (isCallExpression(node)) {
488
547
  const name = calleeName(node);
@@ -532,16 +591,24 @@ export function projectParts<Brand>(
532
591
 
533
592
  // One batch per file resolves every name a parameter or a local is read through.
534
593
  const asked: Identifier[] = [];
535
- const readers = new Map<Declared, { names: Identifier[]; uses: Identifier[] }>();
536
- for (const one of walk.declared) {
537
- const names = one.signature.parameters
538
- .map((parameter) => parameter.name)
539
- .filter(isIdentifier);
540
- const spelled = new Set(names.map((name) => name.text));
541
- const uses = identifiersNaming(one.signature, spelled).filter((use) => !names.includes(use));
542
- readers.set(one, { names, uses });
543
- asked.push(...names, ...uses);
594
+ const readers = new Map<SignatureNode, { names: ParameterName[]; uses: Identifier[] }>();
595
+ for (const signature of [
596
+ ...walk.declared.map((one) => one.signature),
597
+ ...walk.values.map((one) => one.signature),
598
+ ]) {
599
+ const names = parameterNames(signature);
600
+ const declaredNames = new Set<Node>(names.map((one) => one.name));
601
+ const spelled = new Set(names.map((one) => one.name.text));
602
+ const uses = identifiersNaming(signature, spelled).filter((use) => !declaredNames.has(use));
603
+ readers.set(signature, { names, uses });
604
+ asked.push(...names.map((one) => one.name), ...uses);
544
605
  }
606
+ const caught = walk.catches.map(({ clause, name }) =>
607
+ identifiersNaming(clause.block, new Set([name.text])),
608
+ );
609
+ walk.catches.forEach(({ name }, at) => {
610
+ asked.push(name, ...(caught[at] ?? []));
611
+ });
545
612
  const stores = walk.blocks.map((block) => localsOf(block));
546
613
  for (const store of stores) {
547
614
  asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
@@ -555,9 +622,30 @@ export function projectParts<Brand>(
555
622
  stub: isStub(one.signature),
556
623
  fixed: hierarchyFixes(one.node, extended),
557
624
  });
558
- const { uses } = readers.get(one) ?? { uses: [] };
559
- parts.push(...parameterParts(file, targetRoot, one, uses, ids));
625
+ const reader = readers.get(one.signature);
626
+ if (reader !== undefined) {
627
+ parts.push(...parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false));
628
+ }
629
+ }
630
+ for (const one of walk.values) {
631
+ const reader = readers.get(one.signature);
632
+ if (reader !== undefined) {
633
+ parts.push(
634
+ ...parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, true),
635
+ );
636
+ }
560
637
  }
638
+ walk.catches.forEach(({ name, holder }, at) => {
639
+ parts.push({
640
+ code: DEAD_STORE,
641
+ declaration: holder,
642
+ kind: "store",
643
+ name: name.text,
644
+ position: spanOf(file, targetRoot, name),
645
+ message: `value written to ${name.text} is never read`,
646
+ used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
647
+ });
648
+ });
561
649
  walk.blocks.forEach((block, at) => {
562
650
  const holder = holderOf(block);
563
651
  if (holder !== undefined) {
@@ -592,41 +680,107 @@ export function projectParts<Brand>(
592
680
  return { parts, signatures, calls, specifiers };
593
681
  }
594
682
 
595
- /** Each named parameter of one function and whether the function reads it. */
683
+ /**
684
+ * A function written in place that no declaration declares: an arrow, a function
685
+ * expression, a nested function declaration, or a method of an object literal.
686
+ */
687
+ function valueSignature(node: Node): SignatureNode | undefined {
688
+ if (isArrowFunction(node) || isFunctionExpression(node)) {
689
+ return node;
690
+ }
691
+ if (isFunctionDeclaration(node)) {
692
+ return node.body === undefined ? undefined : node;
693
+ }
694
+ return isMethodDeclaration(node) &&
695
+ node.body !== undefined &&
696
+ isObjectLiteralExpression(node.parent)
697
+ ? node
698
+ : undefined;
699
+ }
700
+
701
+ /**
702
+ * Every name one function's parameters bind, a binding pattern's names one by one. A
703
+ * `this` parameter receives no argument and a decorated one is read by its decorator, so
704
+ * neither binds a subject.
705
+ */
706
+ function parameterNames(signature: SignatureNode): ParameterName[] {
707
+ const found: ParameterName[] = [];
708
+ const patternNames = (pattern: BindingName, at: number): void => {
709
+ if (isIdentifier(pattern)) {
710
+ found.push({ name: pattern, at, pattern: true, exempt: false });
711
+ return;
712
+ }
713
+ const rest =
714
+ isObjectBindingPattern(pattern) &&
715
+ pattern.elements.some((element) => element.dotDotDotToken !== undefined);
716
+ for (const element of pattern.elements) {
717
+ if (!isBindingElement(element) || element.name === undefined) {
718
+ continue;
719
+ }
720
+ if (isIdentifier(element.name)) {
721
+ const exempt = rest && element.dotDotDotToken === undefined;
722
+ found.push({ name: element.name, at, pattern: true, exempt });
723
+ } else {
724
+ patternNames(element.name, at);
725
+ }
726
+ }
727
+ };
728
+ signature.parameters.forEach((parameter, at) => {
729
+ const name = parameter.name;
730
+ if (
731
+ ((parameter as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(isDecorator)
732
+ ) {
733
+ return;
734
+ }
735
+ if (isIdentifier(name)) {
736
+ if (name.text !== "this") {
737
+ found.push({ name, at, pattern: false, exempt: false });
738
+ }
739
+ return;
740
+ }
741
+ patternNames(name, at);
742
+ });
743
+ return found;
744
+ }
745
+
746
+ /**
747
+ * Each name one function's parameters bind and whether the function reads it. A binding
748
+ * pattern counts as read where its function reads any name it binds.
749
+ */
596
750
  function parameterParts(
597
751
  file: SourceFile,
598
752
  root: string,
599
- one: Declared,
600
- uses: readonly Identifier[],
753
+ declaration: string,
754
+ signature: SignatureNode,
755
+ reader: { readonly names: readonly ParameterName[]; readonly uses: readonly Identifier[] },
601
756
  ids: ReadonlyMap<Node, Answer<number>>,
757
+ inValue: boolean,
602
758
  ): PartFact[] {
603
- const signature = one.signature;
604
759
  // A function that reads `arguments` reads every parameter by position.
605
760
  const readsArguments =
606
761
  !isArrowFunction(signature) && identifiersNaming(signature, new Set(["arguments"])).length > 0;
762
+ const read = reader.names.map(
763
+ ({ name }) =>
764
+ readsArguments ||
765
+ reader.uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
766
+ );
767
+ const lastRead = Math.max(-1, ...reader.names.filter((_, at) => read[at]).map((one) => one.at));
607
768
  const found: PartFact[] = [];
608
- for (const parameter of signature.parameters) {
609
- const name = parameter.name;
610
- if (
611
- !isIdentifier(name) ||
612
- name.text === "this" ||
613
- name.text.startsWith(UNUSED_PREFIX) ||
614
- ((parameter as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(isDecorator)
615
- ) {
616
- continue;
769
+ reader.names.forEach(({ name, at, pattern, exempt }, index) => {
770
+ if (exempt) {
771
+ return;
617
772
  }
618
773
  found.push({
619
774
  code: UNUSED_PARAMETER,
620
- declaration: one.id,
775
+ declaration,
621
776
  kind: "parameter",
622
777
  name: name.text,
623
778
  position: spanOf(file, root, name),
624
779
  message: `parameter ${name.text} is never read in the body`,
625
- used:
626
- readsArguments ||
627
- uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
780
+ used: read[index] === true,
781
+ parameter: { beforeARead: !pattern && at < lastRead, inValue },
628
782
  });
629
- }
783
+ });
630
784
  return found;
631
785
  }
632
786
 
@@ -823,6 +977,8 @@ export interface IntraFunctionFacts {
823
977
  readonly parts: readonly PartFact[];
824
978
  /** The declarations whose signature is free to change. */
825
979
  readonly free: ReadonlySet<string>;
980
+ /** The declarations whose signature is free but for a use as a value. */
981
+ readonly freeButValued: ReadonlySet<string>;
826
982
  /**
827
983
  * The declarations every reference of which is a call in the loaded program that
828
984
  * discards the call's value, with at least one such call.
@@ -882,8 +1038,13 @@ export function intraFunctionFacts(
882
1038
  .map((one) => one.declaration)
883
1039
  .filter((id) => !fixed.has(id) && !exempted.has(id) && !valued.has(id)),
884
1040
  );
1041
+ const freeButValued = new Set(
1042
+ signatures
1043
+ .map((one) => one.declaration)
1044
+ .filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)),
1045
+ );
885
1046
  const discardedEverywhere = new Set(
886
1047
  [...called].filter(([id, all]) => all && !valued.has(id)).map(([id]) => id),
887
1048
  );
888
- return { parts: [...parts.values()], free, discardedEverywhere };
1049
+ return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
889
1050
  }
package/src/inventory.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  isPropertyDeclaration,
21
21
  isPropertySignatureDeclaration,
22
22
  isSetAccessorDeclaration,
23
+ isStringLiteral,
23
24
  isTypeAliasDeclaration,
24
25
  isTypeLiteralNode,
25
26
  isUnionTypeNode,
@@ -178,6 +179,8 @@ export interface Inventory {
178
179
  * component, its declared name where that differs, and a type parameter's own name.
179
180
  */
180
181
  readonly byName: ReadonlyMap<string, readonly string[]>;
182
+ /** Each `declare module` declaration whose name is a string literal, by id, to that name. */
183
+ readonly modules: ReadonlyMap<string, string>;
181
184
  }
182
185
 
183
186
  /** A declaration under construction, before its reference is rendered. */
@@ -532,6 +535,7 @@ export function inventory<Brand>(
532
535
  const exportScopes: ExportScope[] = [];
533
536
  const containers: Container[] = [];
534
537
  const starReExports: string[] = [];
538
+ const modules: (readonly [Building, string])[] = [];
535
539
  let outsideOwnFiles = 0;
536
540
 
537
541
  const keep = (record: Building): Building => {
@@ -737,6 +741,9 @@ export function inventory<Brand>(
737
741
  localName: undefined,
738
742
  });
739
743
  exportScopes.push({ owner: record, nameNode: node.name });
744
+ if (isStringLiteral(node.name)) {
745
+ modules.push([record, node.name.text]);
746
+ }
740
747
  const body = node.body;
741
748
  if (body === undefined) {
742
749
  return;
@@ -1022,6 +1029,7 @@ export function inventory<Brand>(
1022
1029
  .filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
1023
1030
  .map((symbol) => symbol.id),
1024
1031
  byName: namesOf(building),
1032
+ modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
1025
1033
  };
1026
1034
  }
1027
1035
 
package/src/kinds.ts CHANGED
@@ -101,9 +101,9 @@ const ROWS: readonly KindRow[] = [
101
101
  code: "DS1101",
102
102
  name: "unnecessary-export",
103
103
  languages: ["go", "ts"],
104
- rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method, and three subjects are excluded: an interface method, whose exportedness is the contract of the interface that declares it; a struct field, which an encoder reads by name; and a method that satisfies an interface some symbol uses as a type, which cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference; where the edge's other side is unknown to the analyzer the finding is emitted pending.",
104
+ rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
105
105
  precondition:
106
- "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. 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. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
106
+ "Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. 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. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
107
107
  defaultSeverity: "warn",
108
108
  maxClass: "certain",
109
109
  fixability: "narrowable",
@@ -113,7 +113,7 @@ const ROWS: readonly KindRow[] = [
113
113
  code: "DS1102",
114
114
  name: "unnecessary-exposure",
115
115
  languages: ["go"],
116
- rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method, and three subjects are excluded: an interface method, whose exportedness is the contract of the interface that declares it; a struct field, which an encoder reads by name; and a method that satisfies an interface some symbol uses as a type, which cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference; where the edge's other side is unknown to the analyzer the finding is emitted pending.",
116
+ rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
117
117
  precondition:
118
118
  "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. 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. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the target module references that function or method, because the caller holds values of the type, and moving it behind an internal boundary would leave an exported signature naming a type its callers cannot name.",
119
119
  defaultSeverity: "warn",
@@ -137,7 +137,7 @@ const ROWS: readonly KindRow[] = [
137
137
  languages: ["ts"],
138
138
  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.",
139
139
  precondition:
140
- "Closed world only. Reported in a file that is not an entry file 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.",
140
+ "Closed world only. Reported in a file that is not an entry file 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.",
141
141
  defaultSeverity: "warn",
142
142
  maxClass: "certain",
143
143
  fixability: "narrowable",
@@ -157,9 +157,9 @@ const ROWS: readonly KindRow[] = [
157
157
  code: "DS1203",
158
158
  name: "uncalled-interface-method",
159
159
  languages: ["go", "ts"],
160
- rule: "An interface method that no call site invokes or selects through the interface, whatever the number of implementations. The finding names the concrete implementations and their positions.",
160
+ rule: "An interface method that no call site invokes or selects through the interface, whatever the number of implementations. The finding names the concrete implementations and their positions. Each method that implements it and that nothing else keeps is reported under the code its own references select. Such a method counts as referenced by the interface method alone, so it belongs to this finding's dead component, whose root member is the interface method.",
161
161
  precondition:
162
- "Exempt: in Go, every method of an interface that declares an unexported method, the sum-type shape whose method set exists to restrict the implementors, a shape a TypeScript interface cannot take because it declares no member less visible than itself; and, in both languages, every marker method, an interface method whose every implementation carries an empty body.",
162
+ "In Go, every method of an interface that declares an unexported method is exempt. That is the sum-type shape, whose method set exists to restrict the implementors, and a TypeScript interface cannot take it because it declares no member less visible than itself. In both languages, every marker method is exempt, an interface method whose every implementation carries an empty body.",
163
163
  defaultSeverity: "warn",
164
164
  maxClass: "certain",
165
165
  fixability: "manual",
@@ -213,7 +213,7 @@ const ROWS: readonly KindRow[] = [
213
213
  languages: ["go", "ts"],
214
214
  rule: "A source file no configuration in the build matrix builds. On the Go side the finding names the build constraint that excluded the file. A file whose build constraint is the ignore tag is never reported: the toolchain applies no build constraint to a file named on its own command line, so that tag is the toolchain's convention for a file built by hand. A file any other custom tag excludes is reported under a matrix the configuration declares complete, because completeness is the maintainer's assertion that the listed configurations are every one the target builds, and a configuration the maintainer builds by hand belongs in that list.",
215
215
  precondition:
216
- 'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled; such a file is recorded as excluded by cgo rather than as never built.',
216
+ 'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled. Such a file is recorded as excluded by cgo rather than as never built.',
217
217
  defaultSeverity: "deny",
218
218
  maxClass: "certain",
219
219
  fixability: "deletable",
@@ -235,7 +235,7 @@ const ROWS: readonly KindRow[] = [
235
235
  languages: ["go", "ts"],
236
236
  rule: "A directly declared dependency that no import in the target needs. On the Go side, a direct require whose module provides no package any target package or test variant imports. On the TypeScript side, a manifest dependency, development dependency or peer dependency the project's import closure does not need. A deletion finding whose fix would remove the last use of a dependency names that dependency.",
237
237
  precondition:
238
- "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and three more dependencies are needed: one whose installed manifest declares a command, because a command is run by name rather than imported; one that the installed manifest of a needed dependency declares as a peer and does not mark optional; and, to a fixpoint, the required peers of every dependency so held. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
238
+ "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and four more dependencies are needed. The first is one whose installed manifest declares a command, because a command is run by name rather than imported. The second is one that the installed manifest of a needed dependency declares as a peer and does not mark optional. The third is every required peer of a dependency so held, to a fixpoint. The fourth is one that a string names under a manifest key of an applied convention row, as grammar/analysis.md states. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
239
239
  defaultSeverity: "deny",
240
240
  maxClass: "certain",
241
241
  fixability: "manual",
@@ -247,7 +247,7 @@ const ROWS: readonly KindRow[] = [
247
247
  languages: ["go"],
248
248
  rule: "A replace directive whose target module is absent from the build list, the one case in which the directive is exactly a no-op.",
249
249
  precondition:
250
- "Only a replace whose target is absent from the build list. No other module-file directive is reported: not an exclude directive, because not selected is a counterfactual about a resolution that did not happen; not a workspace use entry and not a tool directive, because each is an external entry point whose invocation lives outside anything the analysis reads.",
250
+ "Only a replace whose target is absent from the build list. No other module-file directive is reported. An exclude directive is not, because not selected is a counterfactual about a resolution that did not happen. A workspace use entry and a tool directive are not, because each is an external entry point whose invocation lives outside anything the analysis reads.",
251
251
  defaultSeverity: "warn",
252
252
  maxClass: "certain",
253
253
  fixability: "deletable",
@@ -267,7 +267,7 @@ const ROWS: readonly KindRow[] = [
267
267
  code: "DS1702",
268
268
  name: "unscoped-ignore-entry",
269
269
  languages: ["go", "ts"],
270
- rule: "An ignore-file entry or a baseline row that names a symbol and no file path. The entry is reported rather than matched, so a bare name cannot mask a match anywhere else in the project; the inline directive is scoped by its position and cannot be unscoped.",
270
+ rule: "An ignore-file entry or a baseline row that names a symbol and no file path. The entry is reported rather than matched, so a bare name cannot mask a match anywhere else in the project. The inline directive is scoped by its position and cannot be unscoped.",
271
271
  defaultSeverity: "deny",
272
272
  maxClass: "certain",
273
273
  fixability: "none",
@@ -319,7 +319,7 @@ const ROWS: readonly KindRow[] = [
319
319
  languages: ["go", "ts"],
320
320
  rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
321
321
  precondition:
322
- "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, 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 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.",
322
+ "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, 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 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 when its body reads no parameter after it, and an unread parameter before the last read one is not. A 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. No name exempts a parameter. A TypeScript parameter whose name starts with an underscore is judged as any other is, and a Go parameter named by the blank identifier declares no name a body could read and is never reported.",
323
323
  defaultSeverity: "warn",
324
324
  maxClass: "certain",
325
325
  fixability: "manual",
@@ -367,11 +367,13 @@ const ROWS: readonly KindRow[] = [
367
367
  name: "dead-store",
368
368
  languages: ["go", "ts"],
369
369
  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.",
370
+ precondition:
371
+ "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.",
370
372
  defaultSeverity: "deny",
371
373
  maxClass: "certain",
372
374
  fixability: "deletable",
373
375
  fixed: false,
374
- overlap: ["eslint no-useless-assignment"],
376
+ overlap: ["eslint no-useless-assignment", "@typescript-eslint/no-unused-vars"],
375
377
  },
376
378
  {
377
379
  code: "DS1809",
package/src/matrix.ts CHANGED
@@ -10,7 +10,14 @@ import type { InventorySymbol } from "./inventory.ts";
10
10
  import { byPosition } from "./position.ts";
11
11
  import type { Reference } from "./references.ts";
12
12
  import type { Root } from "./roots.ts";
13
- import { sweep, type Candidate, type Liveness, type Relation, type SweepInput } from "./sweep.ts";
13
+ import {
14
+ reachedWithTheTests,
15
+ sweep,
16
+ type Candidate,
17
+ type Liveness,
18
+ type Relation,
19
+ type SweepInput,
20
+ } from "./sweep.ts";
14
21
 
15
22
  /** What the passes answered for one configuration of the run. */
16
23
  export interface Configured {
@@ -62,6 +69,12 @@ export interface MatrixCandidate extends Candidate {
62
69
  * declaration it contains is a member of a dead component.
63
70
  */
64
71
  readonly heldByTests: string | undefined;
72
+ /**
73
+ * Whether a test file declares it, no test of dead code, and the run that counts test
74
+ * references does not reach it: it is reported as a production declaration of its
75
+ * shape nothing references would be.
76
+ */
77
+ readonly unreferencedTest: boolean;
65
78
  }
66
79
 
67
80
  /** One run's sweep. */
@@ -186,32 +199,61 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
186
199
  const dead = union.symbols.map(() => false);
187
200
  const testOfDeadCode = union.symbols.map(() => false);
188
201
  const heldBy: (string | undefined)[] = union.symbols.map(() => undefined);
189
- const candidates: MatrixCandidate[] = [];
190
- union.symbols.forEach((symbol, at) => {
202
+ const unreferencedTest = union.symbols.map(() => false);
203
+ const testReached = reachedWithTheTests(union, [
204
+ ...input.marked,
205
+ ...(input.exempt ?? []).map((record) => record.id),
206
+ ...(input.unanswered ?? []),
207
+ ]);
208
+ const judged = union.symbols.map((symbol, at): readonly Candidate[] | undefined => {
191
209
  const held = matrix.heldIn[at] ?? [];
192
210
  const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
193
- if (held.length === 0 || found.some((candidate) => candidate === undefined)) {
211
+ return held.length === 0 || found.some((candidate) => candidate === undefined)
212
+ ? undefined
213
+ : found.filter((candidate): candidate is Candidate => candidate !== undefined);
214
+ });
215
+ // A configuration that does not hold a test's subject's caller finds the subject dead,
216
+ // so a test is of dead code only where every production subject it names is dead in
217
+ // every configuration that holds it.
218
+ const subjectsDead = (at: number): boolean =>
219
+ (union.out[at] ?? []).every(
220
+ (edge) =>
221
+ union.test[edge.to] === true ||
222
+ union.support[edge.to] === true ||
223
+ union.subject[edge.to] !== true ||
224
+ judged[edge.to] !== undefined,
225
+ );
226
+ const candidates: MatrixCandidate[] = [];
227
+ union.symbols.forEach((symbol, at) => {
228
+ const all = judged[at];
229
+ if (all === undefined) {
194
230
  return;
195
231
  }
196
- const all = found.filter((candidate): candidate is Candidate => candidate !== undefined);
197
- const admitted = all.every((candidate) => candidate.testOfDeadCode);
232
+ const held = matrix.heldIn[at] ?? [];
233
+ const admitted = all.every((candidate) => candidate.testOfDeadCode) && subjectsDead(at);
198
234
  const testRefs = union.made[at]?.test ?? 0;
199
235
  const heldByTests =
200
236
  heldBy[union.parent[at] ?? OUTSIDE] ??
201
237
  (union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
238
+ const unreferenced = union.test[at] === true && !admitted && testReached[at] !== true;
202
239
  dead[at] = true;
203
240
  testOfDeadCode[at] = admitted;
204
241
  heldBy[at] = heldByTests;
242
+ unreferencedTest[at] = unreferenced;
243
+ const productionRefs = union.made[at]?.production ?? 0;
244
+ const counted = all.every((candidate) => candidate.relation === "reference-counting");
205
245
  candidates.push({
206
246
  id: symbol.id,
207
- productionRefs: union.made[at]?.production ?? 0,
247
+ productionRefs,
208
248
  testRefs,
209
- relation: all.every((candidate) => candidate.relation === "reference-counting")
249
+ // The run that judges a test-file declaration counts its test references.
250
+ relation: (unreferenced ? productionRefs + testRefs === 0 : counted)
210
251
  ? "reference-counting"
211
252
  : "reachability",
212
253
  testOfDeadCode: admitted,
213
254
  configurations: held,
214
255
  heldByTests,
256
+ unreferencedTest: unreferenced,
215
257
  });
216
258
  });
217
259
 
@@ -222,6 +264,7 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
222
264
  union,
223
265
  dead.map((one, at) => one && heldBy[at] === undefined),
224
266
  testOfDeadCode,
267
+ unreferencedTest,
225
268
  ),
226
269
  };
227
270
  }