@cplieger/deadset-ts 3.12.1 → 4.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 (120) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +217 -48
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/components.js +4 -2
  8. package/dist/src/configured-declarations.js +56 -7
  9. package/dist/src/conformance-record.js +2 -2
  10. package/dist/src/dependencies.js +34 -17
  11. package/dist/src/deprecation.js +35 -24
  12. package/dist/src/destructuring.js +37 -29
  13. package/dist/src/discover.js +105 -28
  14. package/dist/src/edges.js +4 -1
  15. package/dist/src/entry-points.js +7 -0
  16. package/dist/src/enum-group.js +46 -40
  17. package/dist/src/exempt.js +42 -1
  18. package/dist/src/findings/completion.js +37 -8
  19. package/dist/src/findings/emitters.js +20 -6
  20. package/dist/src/findings/interfaces.js +7 -1
  21. package/dist/src/findings/intra-function.js +91 -2
  22. package/dist/src/findings/self-check.js +30 -7
  23. package/dist/src/findings/unused-declarations.js +15 -3
  24. package/dist/src/findings-pass.js +18 -11
  25. package/dist/src/framework-lifecycle.js +8 -6
  26. package/dist/src/implementations.js +45 -23
  27. package/dist/src/interface-satisfaction.js +30 -27
  28. package/dist/src/intra-function-parts.js +660 -0
  29. package/dist/src/inventory.js +85 -4
  30. package/dist/src/json-chunks.js +76 -0
  31. package/dist/src/kinds.js +337 -35
  32. package/dist/src/partial.js +23 -0
  33. package/dist/src/query.js +207 -0
  34. package/dist/src/references.js +92 -20
  35. package/dist/src/reflective-lookup.js +46 -4
  36. package/dist/src/report.js +14 -3
  37. package/dist/src/reporters/annotations.js +9 -5
  38. package/dist/src/reporters/json.js +3 -2
  39. package/dist/src/reporters/sarif.js +22 -16
  40. package/dist/src/reporters/template.js +112 -12
  41. package/dist/src/reporters/text.js +8 -6
  42. package/dist/src/roots.js +27 -11
  43. package/dist/src/serialization-contract.js +53 -32
  44. package/dist/src/session.js +79 -39
  45. package/dist/src/signatures.js +32 -0
  46. package/dist/src/suppress-file.js +29 -7
  47. package/dist/src/suppress-inline.js +8 -1
  48. package/dist/src/suppress.js +27 -0
  49. package/dist/src/sweep.js +4 -1
  50. package/dist/src/value-reach.js +44 -25
  51. package/dist/src/verbs/analyze.js +39 -14
  52. package/dist/src/verbs/describe.js +12 -8
  53. package/dist/src/verbs/explain.js +22 -9
  54. package/dist/src/verbs/print-projects.js +40 -7
  55. package/dist/src/verbs/print-retained.js +6 -2
  56. package/dist/src/verbs/print-roots.js +5 -1
  57. package/dist/src/version.js +1 -1
  58. package/jsr.json +1 -1
  59. package/package.json +3 -3
  60. package/src/alias-chain.ts +75 -11
  61. package/src/analysis.ts +385 -104
  62. package/src/calls.ts +10 -9
  63. package/src/class-members.ts +10 -11
  64. package/src/components.ts +4 -2
  65. package/src/configured-declarations.ts +80 -8
  66. package/src/conformance-record.ts +2 -2
  67. package/src/dependencies.ts +42 -20
  68. package/src/deprecation.ts +53 -27
  69. package/src/destructuring.ts +50 -39
  70. package/src/discover.ts +177 -33
  71. package/src/edges.ts +4 -1
  72. package/src/entry-points.ts +7 -0
  73. package/src/enum-group.ts +56 -42
  74. package/src/exempt.ts +50 -2
  75. package/src/finding.ts +5 -1
  76. package/src/findings/completion.ts +46 -10
  77. package/src/findings/emitter.ts +3 -0
  78. package/src/findings/emitters.ts +27 -6
  79. package/src/findings/interfaces.ts +7 -1
  80. package/src/findings/intra-function.ts +106 -3
  81. package/src/findings/self-check.ts +48 -7
  82. package/src/findings/unused-declarations.ts +18 -4
  83. package/src/findings-pass.ts +26 -11
  84. package/src/framework-lifecycle.ts +10 -6
  85. package/src/host.ts +5 -3
  86. package/src/implementations.ts +59 -23
  87. package/src/interface-satisfaction.ts +30 -27
  88. package/src/intra-function-parts.ts +889 -0
  89. package/src/inventory.ts +97 -4
  90. package/src/json-chunks.ts +87 -0
  91. package/src/kinds.ts +355 -41
  92. package/src/partial.ts +40 -0
  93. package/src/query.ts +522 -0
  94. package/src/references.ts +119 -38
  95. package/src/reflective-lookup.ts +56 -6
  96. package/src/report.ts +32 -4
  97. package/src/reporters/annotations.ts +23 -21
  98. package/src/reporters/json.ts +3 -2
  99. package/src/reporters/reporter.ts +2 -2
  100. package/src/reporters/sarif.ts +27 -18
  101. package/src/reporters/template.ts +119 -14
  102. package/src/reporters/text.ts +12 -12
  103. package/src/roots.ts +34 -12
  104. package/src/serialization-contract.ts +59 -40
  105. package/src/session.ts +146 -58
  106. package/src/signatures.ts +55 -0
  107. package/src/suppress-file.ts +40 -8
  108. package/src/suppress-inline.ts +8 -1
  109. package/src/suppress.ts +31 -0
  110. package/src/sweep.ts +9 -4
  111. package/src/value-reach.ts +66 -28
  112. package/src/verbs/analyze.ts +51 -14
  113. package/src/verbs/describe.ts +13 -9
  114. package/src/verbs/explain.ts +20 -9
  115. package/src/verbs/print-projects.ts +53 -7
  116. package/src/verbs/print-retained.ts +6 -2
  117. package/src/verbs/print-roots.ts +5 -1
  118. package/src/version.ts +1 -1
  119. package/dist/src/reporters/rules.js +0 -178
  120. package/src/reporters/rules.ts +0 -195
package/src/references.ts CHANGED
@@ -28,12 +28,13 @@ import {
28
28
  type SourceFile,
29
29
  } from "@typescript/native/unstable/ast";
30
30
  import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
31
- import { aliasChains } from "./alias-chain.ts";
31
+ import { aliasChains, type ChainTarget } from "./alias-chain.ts";
32
32
  import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
33
33
  import { globExpression } from "./glob.ts";
34
- import { nodeKey, type Inventory } from "./inventory.ts";
34
+ import { declarationsByName, nodeKey, type Inventory } from "./inventory.ts";
35
35
  import { relativePath } from "./paths.ts";
36
36
  import { byPosition, renderPosition, type Position } from "./position.ts";
37
+ import { DEFAULT_BATCH_CAP, UNANSWERED, type Answer } from "./query.ts";
37
38
  import type { ProjectView } from "./session.ts";
38
39
 
39
40
  /**
@@ -63,21 +64,15 @@ import type { ProjectView } from "./session.ts";
63
64
  export type Use = "read" | "write" | "evaluation" | "decorator";
64
65
 
65
66
  /**
66
- * Which accessor answered for one reference, so that the cost of a run is attributable
67
- * and a table of references shows the path each row took.
68
- *
69
- * - `batch` is the batched lookup over a file's name nodes, which answers for almost
70
- * every one of them.
71
- * - `resolved-symbol` is the per-node lookup for the residue a batch left.
72
- * - `shorthand` is the per-node lookup for `{ a }`, whose name resolves to the property
73
- * the literal declares rather than to the declaration that name reads.
74
- * - `alias` is a step along an import or export chain, from one link to what it names.
75
- * - `syntax` is the tree alone, which says what a decorator is attached to.
76
- * - `destructured` is the property a pattern names, looked up on the type of the value
77
- * the pattern destructures.
67
+ * Which path answered for one reference: `batch`, the batched lookup over a file's name
68
+ * nodes; `resolved-symbol`, the per-node lookup for the residue a batch left;
69
+ * `shorthand`, the lookup for the value `{ a }` reads; `alias`, a step along an import
70
+ * or export chain; `syntax`, the tree alone, which says what a decorator is attached to;
71
+ * `destructured`, the property a pattern names, on the destructured value's type; and
72
+ * `by-name`, every declaration bearing the name a site spells, where the checker failed.
78
73
  */
79
74
  export type Resolution =
80
- "batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax" | "destructured";
75
+ "batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax" | "destructured" | "by-name";
81
76
 
82
77
  /** One use of one declaration by one declaration. */
83
78
  export interface Reference {
@@ -187,13 +182,6 @@ export interface ReferenceOptions {
187
182
  readonly consumer?: ConsumerSide;
188
183
  }
189
184
 
190
- /**
191
- * The batch cap a run takes when the configuration names none. It bounds the size of
192
- * one answer rather than the work: the median file is one batch at this cap and the
193
- * largest production file a handful.
194
- */
195
- export const DEFAULT_BATCH_CAP = 4096;
196
-
197
185
  /**
198
186
  * The operators whose left side one assignment stores into. A compound assignment is
199
187
  * among them: it reads its target only to write the result back, so nothing else
@@ -564,6 +552,53 @@ export function testFileRulesOf(
564
552
  .sort((a, b) => compare(a.rule, b.rule));
565
553
  }
566
554
 
555
+ const importedCache = new WeakMap<
556
+ SourceFile,
557
+ ReadonlyMap<string, readonly (string | undefined)[]>
558
+ >();
559
+
560
+ /**
561
+ * Each local name one file's imports bind, mapped to the names it imports: the
562
+ * exported name of a named import, `default` for a default import, and `undefined` for
563
+ * a namespace import or an import assignment, which binds a whole module.
564
+ */
565
+ function importedNames(file: SourceFile): ReadonlyMap<string, readonly (string | undefined)[]> {
566
+ const known = importedCache.get(file);
567
+ if (known !== undefined) {
568
+ return known;
569
+ }
570
+ const names = new Map<string, (string | undefined)[]>();
571
+ const bind = (local: string, imported: string | undefined): void => {
572
+ names.set(local, [...(names.get(local) ?? []), imported]);
573
+ };
574
+ for (const statement of file.statements) {
575
+ if (isImportEqualsDeclaration(statement)) {
576
+ bind(statement.name.text, undefined);
577
+ continue;
578
+ }
579
+ const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
580
+ if (clause === undefined) {
581
+ continue;
582
+ }
583
+ if (clause.name !== undefined) {
584
+ bind(clause.name.text, "default");
585
+ }
586
+ const bindings = clause.namedBindings;
587
+ if (bindings === undefined) {
588
+ continue;
589
+ }
590
+ if (bindings.kind === SyntaxKind.NamespaceImport) {
591
+ bind(bindings.name.text, undefined);
592
+ } else {
593
+ for (const element of bindings.elements) {
594
+ bind(element.name.text, element.propertyName?.text ?? element.name.text);
595
+ }
596
+ }
597
+ }
598
+ importedCache.set(file, names);
599
+ return names;
600
+ }
601
+
567
602
  /** Two strings ordered bytewise, which is the order every set of a run is read in. */
568
603
  function compare(a: string, b: string): number {
569
604
  if (a === b) {
@@ -633,17 +668,42 @@ export function references<Brand>(
633
668
  * Each later link is a declaration of its own that references the next, so one link
634
669
  * per re-export is what lets a chain be followed one declaration at a time.
635
670
  */
636
- const carriedBy = (link: Link, symbol: TSSymbol): readonly string[] =>
637
- link.alias ? chains.nextDeclared(symbol) : chains.declarationsOf(symbol);
671
+ const carriedBy = (link: Link, symbol: TSSymbol): readonly ChainTarget[] =>
672
+ link.alias
673
+ ? chains.nextDeclared(symbol)
674
+ : chains.declarationsOf(symbol).map((id) => ({ id, stepped: false, guessed: false }));
675
+
676
+ /**
677
+ * Every declaration bearing the name one node spells, or the name an import of its
678
+ * file binds that name to, which is what a reference the checker could not resolve is
679
+ * counted as: it may be a use of any of them, and a declaration it may use is kept
680
+ * live. A name a namespace import binds may be a use of any file.
681
+ */
682
+ const named = (node: Node): readonly string[] => {
683
+ const byName = declarationsByName(held);
684
+ const text = node.getText();
685
+ const imported = importedNames(node.getSourceFile()).get(text) ?? [];
686
+ return [
687
+ ...new Set([
688
+ ...(byName.get(text) ?? []),
689
+ ...imported.flatMap((name) =>
690
+ name === undefined ? [...fileIds] : (byName.get(name) ?? []),
691
+ ),
692
+ ]),
693
+ ];
694
+ };
638
695
 
639
696
  /** The symbol one site resolves to, asking only the accessor the site's form names. */
640
- const symbolFor = (site: Site, answered: ReadonlyMap<Node, TSSymbol>): TSSymbol | undefined => {
697
+ const symbolFor = (
698
+ site: Site,
699
+ answered: ReadonlyMap<Node, Answer<TSSymbol>>,
700
+ ): Answer<TSSymbol | undefined> => {
641
701
  if (site.shorthand !== undefined) {
642
702
  shorthandLookups += 1;
643
703
  return project.shorthandValueAt(project.handle(site.shorthand));
644
704
  }
645
705
  const batched = answered.get(site.node);
646
- if (batched !== undefined) {
706
+ if (batched === UNANSWERED || typeof batched === "object") {
647
707
  return batched;
648
708
  }
649
709
  residueFallbacks += 1;
@@ -688,11 +748,14 @@ export function references<Brand>(
688
748
  ...evaluations.map((evaluation) => evaluation.node),
689
749
  ]),
690
750
  ];
691
- const answered = new Map<Node, TSSymbol>();
751
+ const answered = new Map<Node, Answer<TSSymbol>>();
692
752
  batched += batching.length;
693
753
  for (let from = 0; from < batching.length; from += cap) {
694
754
  const run = batching.slice(from, from + cap);
695
- const answers = project.symbolsAt(run.map((node) => project.handle(node)));
755
+ const answers = project.symbolsAt(
756
+ run.map((node) => project.handle(node)),
757
+ cap,
758
+ );
696
759
  fileBatches += 1;
697
760
  run.forEach((node, index) => {
698
761
  const symbol = answers[index];
@@ -708,13 +771,17 @@ export function references<Brand>(
708
771
  continue;
709
772
  }
710
773
  const position = renderPosition(file, root, link.at.getStart());
711
- for (const id of carriedBy(link, symbol)) {
774
+ const carried =
775
+ symbol === UNANSWERED
776
+ ? named(link.at).map((id) => ({ id, stepped: false, guessed: true }))
777
+ : carriedBy(link, symbol);
778
+ for (const target of carried) {
712
779
  found.push({
713
780
  from: link.from,
714
- to: id,
781
+ to: target.id,
715
782
  position,
716
783
  use: "read",
717
- resolution: link.alias ? "alias" : "batch",
784
+ resolution: target.guessed ? "by-name" : link.alias ? "alias" : "batch",
718
785
  test,
719
786
  });
720
787
  }
@@ -726,14 +793,16 @@ export function references<Brand>(
726
793
  continue;
727
794
  }
728
795
  const position = renderPosition(file, root, evaluation.node.getStart());
729
- for (const id of chains.declarationsOf(module)) {
796
+ // A module the checker could not resolve may be any file of the target.
797
+ const evaluated = module === UNANSWERED ? [...fileIds] : chains.declarationsOf(module);
798
+ for (const id of evaluated) {
730
799
  if (fileIds.has(id)) {
731
800
  found.push({
732
801
  from: evaluation.from,
733
802
  to: id,
734
803
  position,
735
804
  use: "evaluation",
736
- resolution: "batch",
805
+ resolution: module === UNANSWERED ? "by-name" : "batch",
737
806
  test,
738
807
  });
739
808
  }
@@ -748,13 +817,21 @@ export function references<Brand>(
748
817
  return;
749
818
  }
750
819
  const position = renderPosition(file, root, read.key.getStart());
751
- for (const target of chains.chainOf(symbol)) {
820
+ const targets =
821
+ symbol === UNANSWERED
822
+ ? (declarationsByName(held).get(read.name) ?? []).map((id) => ({
823
+ id,
824
+ stepped: false,
825
+ guessed: true,
826
+ }))
827
+ : chains.chainOf(symbol);
828
+ for (const target of targets) {
752
829
  found.push({
753
830
  from: read.from,
754
831
  to: target.id,
755
832
  position,
756
833
  use: "read",
757
- resolution: "destructured",
834
+ resolution: target.guessed ? "by-name" : "destructured",
758
835
  test,
759
836
  });
760
837
  }
@@ -764,7 +841,7 @@ export function references<Brand>(
764
841
  const direct: Resolution =
765
842
  site.shorthand !== undefined
766
843
  ? "shorthand"
767
- : answered.has(site.node)
844
+ : typeof answered.get(site.node) === "object"
768
845
  ? "batch"
769
846
  : "resolved-symbol";
770
847
  const symbol = symbolFor(site, answered);
@@ -774,13 +851,17 @@ export function references<Brand>(
774
851
  const position = renderPosition(file, root, site.node.getStart());
775
852
  // A re-export something imports is used and so is the declaration behind it, so
776
853
  // the use names every link of the chain its name stands at the head of.
777
- for (const target of chains.chainOf(symbol)) {
854
+ const targets =
855
+ symbol === UNANSWERED
856
+ ? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
857
+ : chains.chainOf(symbol);
858
+ for (const target of targets) {
778
859
  found.push({
779
860
  from: site.from,
780
861
  to: target.id,
781
862
  position,
782
863
  use: site.use,
783
- resolution: target.stepped ? "alias" : direct,
864
+ resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
784
865
  test,
785
866
  });
786
867
  }
@@ -8,7 +8,9 @@
8
8
  * string reaches every member of that name on any type, static members included,
9
9
  * and every record names the string's own position. A key that is not literal text
10
10
  * holds nothing back: a computed name is the case no exact rule serves. `Reflect` is
11
- * the global one only: a local declaration of that name is a different object.
11
+ * the global one only: a local declaration of that name is a different object. An
12
+ * element access through a receiver whose type declares a string index signature, and is
13
+ * not `any`, reads that signature and holds nothing back.
12
14
  */
13
15
 
14
16
  import {
@@ -21,9 +23,12 @@ import {
21
23
  type Node,
22
24
  type SourceFile,
23
25
  } from "@typescript/native/unstable/ast";
26
+ import { TypeFlags, type Type } from "@typescript/native/unstable/sync";
24
27
  import type { DetectorInput, Evidence } from "./exempt.ts";
25
28
  import { memberNames } from "./member-names.ts";
26
29
  import { renderPosition } from "./position.ts";
30
+ import { isAnswered, UNANSWERED } from "./query.ts";
31
+ import type { ProjectView } from "./session.ts";
27
32
 
28
33
  /** The `Reflect` methods whose second argument is a property key. */
29
34
  const REFLECT_KEYED: ReadonlySet<string> = new Set(["get", "set", "has"]);
@@ -35,6 +40,8 @@ interface Lookup {
35
40
  readonly callee: string;
36
41
  /** The `Reflect` the call names, where the lookup is a `Reflect` call. */
37
42
  readonly reflect: Node | undefined;
43
+ /** The expression an element access reads from, where the lookup is one. */
44
+ readonly receiver: Node | undefined;
38
45
  }
39
46
 
40
47
  /** The text one key denotes, where it is literal text. */
@@ -57,6 +64,7 @@ function lookupsOf(file: SourceFile): readonly Lookup[] {
57
64
  name,
58
65
  callee: `[${JSON.stringify(name)}]`,
59
66
  reflect: undefined,
67
+ receiver: node.expression,
60
68
  });
61
69
  }
62
70
  } else if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
@@ -72,7 +80,13 @@ function lookupsOf(file: SourceFile): readonly Lookup[] {
72
80
  key !== undefined &&
73
81
  name !== undefined
74
82
  ) {
75
- found.push({ key, name, callee: `Reflect.${method.text}`, reflect: target });
83
+ found.push({
84
+ key,
85
+ name,
86
+ callee: `Reflect.${method.text}`,
87
+ reflect: target,
88
+ receiver: undefined,
89
+ });
76
90
  }
77
91
  }
78
92
  node.forEachChild(visit);
@@ -81,6 +95,19 @@ function lookupsOf(file: SourceFile): readonly Lookup[] {
81
95
  return found;
82
96
  }
83
97
 
98
+ /**
99
+ * Whether a receiver's type is not `any` and declares a string index signature. A type
100
+ * whose index signatures the checker did not answer for is read as declaring none, so
101
+ * the lookup retains what it names.
102
+ */
103
+ function readsAMapping<Brand>(project: ProjectView<Brand>, type: Type): boolean {
104
+ if ((type.flags & TypeFlags.Any) !== 0) {
105
+ return false;
106
+ }
107
+ const infos = project.queries.indexInfosOf(type);
108
+ return isAnswered(infos) && infos.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
109
+ }
110
+
84
111
  /**
85
112
  * The reflective-lookup detector: every member whose name a literal key of a lookup
86
113
  * spells, recorded at the key.
@@ -95,7 +122,7 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
95
122
 
96
123
  // Whether each written `Reflect` is the global one, asked in one batch: a
97
124
  // declaration of that name in one of the project's own files shadows it.
98
- const ownFiles = new Set(files.map((file) => file.fileName));
125
+ const ownFiles = project.ownPaths();
99
126
  const reflects = lookups.flatMap((lookup) =>
100
127
  lookup.reflect === undefined ? [] : [lookup.reflect],
101
128
  );
@@ -103,21 +130,44 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
103
130
  if (reflects.length > 0) {
104
131
  project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
105
132
  const node = reflects[index];
106
- const declaredHere = symbol?.declarations.some((declaration) =>
107
- ownFiles.has(declaration.path),
108
- );
133
+ // A name the checker did not answer for is read as the global, which retains.
134
+ const declaredHere =
135
+ symbol === UNANSWERED
136
+ ? false
137
+ : symbol?.declarations.some((declaration) => ownFiles.has(declaration.path));
109
138
  if (node !== undefined && declaredHere === false) {
110
139
  global.add(node);
111
140
  }
112
141
  });
113
142
  }
114
143
 
144
+ // An element access through a receiver that maps every string key to one value type
145
+ // reads that mapping rather than a member, asked in one batch.
146
+ const receivers = lookups.flatMap((lookup) =>
147
+ lookup.receiver === undefined ? [] : [lookup.receiver],
148
+ );
149
+ const mapped = new Set<Node>();
150
+ project.queries.typesAt(receivers).forEach((type, index) => {
151
+ const receiver = receivers[index];
152
+ if (
153
+ receiver !== undefined &&
154
+ type !== undefined &&
155
+ isAnswered(type) &&
156
+ readsAMapping(project, type)
157
+ ) {
158
+ mapped.add(receiver);
159
+ }
160
+ });
161
+
115
162
  const members = memberNames(files, held);
116
163
  const found: Evidence[] = [];
117
164
  for (const lookup of lookups) {
118
165
  if (lookup.reflect !== undefined && !global.has(lookup.reflect)) {
119
166
  continue;
120
167
  }
168
+ if (lookup.receiver !== undefined && mapped.has(lookup.receiver)) {
169
+ continue;
170
+ }
121
171
  const site = renderPosition(lookup.key.getSourceFile(), targetRoot, lookup.key.getStart());
122
172
  for (const member of members.get(lookup.name) ?? []) {
123
173
  found.push({ id: member.id, detail: `looked up by ${lookup.callee}`, site });
package/src/report.ts CHANGED
@@ -18,7 +18,13 @@ import type { StaleSuppression } from "./findings/self-check.ts";
18
18
  import type { TestFileRule } from "./references.ts";
19
19
 
20
20
  /** The version of the report schema this analyzer writes a report to. */
21
- export const SCHEMA_VERSION = "6.0.0";
21
+ const SCHEMA_VERSION = "6.1.0";
22
+
23
+ /**
24
+ * Every report schema version this analyzer reads: the schema versions the Contract it
25
+ * implements admits, each an instance of the one it writes.
26
+ */
27
+ export const SCHEMA_VERSIONS_ACCEPTED: readonly string[] = ["6.0.0", SCHEMA_VERSION];
22
28
 
23
29
  /** The name this analyzer writes every document under and mints every identifier with. */
24
30
  export const ANALYZER_NAME = "deadset-ts";
@@ -49,6 +55,7 @@ export interface WireDetails {
49
55
  readonly path?: string;
50
56
  readonly reason?: string;
51
57
  };
58
+ readonly overlap?: readonly string[];
52
59
  }
53
60
 
54
61
  /** One finding as the finding schema spells it. */
@@ -73,6 +80,11 @@ export interface WireFinding {
73
80
  readonly root: boolean;
74
81
  readonly symbol_count: number;
75
82
  readonly deletable_lines: number;
83
+ readonly members?: readonly {
84
+ readonly ref: string;
85
+ readonly name: string;
86
+ readonly position: WirePosition;
87
+ }[];
76
88
  };
77
89
  readonly retained_by: readonly string[];
78
90
  readonly configurations: readonly string[];
@@ -130,6 +142,15 @@ export interface ReportConfiguration {
130
142
  readonly project: string;
131
143
  }
132
144
 
145
+ /** One project configuration the run derived and could not build, with why. */
146
+ export interface ReportNotBuilt {
147
+ readonly id: string;
148
+ /** The compiler configuration file below the target root. */
149
+ readonly project: string;
150
+ /** The first line of the error that dropped it, naming no path outside the target. */
151
+ readonly error: string;
152
+ }
153
+
133
154
  /** One consumer the run loaded, at the directory a report names it by. */
134
155
  interface ReportConsumer {
135
156
  readonly id: string;
@@ -160,7 +181,7 @@ export interface Report {
160
181
  };
161
182
  readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
162
183
  readonly configurations: readonly ReportConfiguration[];
163
- readonly configurations_not_built: readonly never[];
184
+ readonly configurations_not_built: readonly ReportNotBuilt[];
164
185
  readonly consumers: {
165
186
  readonly declared: number;
166
187
  readonly loaded: readonly {
@@ -191,6 +212,7 @@ export interface ReportInput {
191
212
  readonly declaredGaps: readonly DeclaredGap[];
192
213
  readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
193
214
  readonly configurations: readonly ReportConfiguration[];
215
+ readonly notBuilt: readonly ReportNotBuilt[];
194
216
  readonly loaded: readonly ReportConsumer[];
195
217
  readonly unavailable: readonly UnavailableConsumer[];
196
218
  readonly result: PassResult;
@@ -247,6 +269,7 @@ function wireDetails(details: FindingDetails): WireDetails {
247
269
  ...(entry.reason === undefined ? {} : { reason: entry.reason }),
248
270
  },
249
271
  }),
272
+ ...(details.overlap === undefined ? {} : { overlap: details.overlap }),
250
273
  };
251
274
  }
252
275
 
@@ -275,6 +298,9 @@ export function wireFinding(finding: CompletedFinding): WireFinding {
275
298
  root: finding.component.root,
276
299
  symbol_count: finding.component.symbolCount,
277
300
  deletable_lines: finding.component.deletableLines,
301
+ ...(finding.component.members === undefined
302
+ ? {}
303
+ : { members: finding.component.members.map(wireSymbol) }),
278
304
  },
279
305
  retained_by: finding.retainedBy,
280
306
  configurations: finding.configurations,
@@ -409,7 +435,7 @@ export function buildReport(input: ReportInput): Report {
409
435
  name: ANALYZER_NAME,
410
436
  version: input.version,
411
437
  languages: [LANGUAGE],
412
- schema_versions_accepted: [SCHEMA_VERSION],
438
+ schema_versions_accepted: SCHEMA_VERSIONS_ACCEPTED,
413
439
  conformance: {
414
440
  corpus_version: input.conformance.corpusVersion,
415
441
  result: input.conformance.result,
@@ -420,7 +446,9 @@ export function buildReport(input: ReportInput): Report {
420
446
  configurations: [...input.configurations]
421
447
  .sort((a, b) => compare(a.id, b.id))
422
448
  .map((one) => ({ id: one.id, project: one.project })),
423
- configurations_not_built: [],
449
+ configurations_not_built: [...input.notBuilt]
450
+ .sort((a, b) => compare(a.id, b.id))
451
+ .map((one) => ({ id: one.id, project: one.project, error: one.error })),
424
452
  consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
425
453
  findings,
426
454
  edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
@@ -1,4 +1,5 @@
1
1
  import { fails } from "../config.ts";
2
+ import { coalesced } from "../json-chunks.ts";
2
3
  import { KINDS } from "../kinds.ts";
3
4
  import type { Report } from "../report.ts";
4
5
  import type { RenderOptions } from "./reporter.ts";
@@ -42,26 +43,27 @@ function command(
42
43
  * failing severity is an error and one below it a warning; a stale suppression is an error,
43
44
  * because its kind is fixed on at the failing severity.
44
45
  */
45
- export function annotations(report: Report, options: RenderOptions): string {
46
- const lines = report.findings.map((found) =>
47
- command(
48
- fails(found.severity, options.failOn) ? "error" : "warning",
49
- found.position,
50
- found.position.end_line,
51
- `${found.code} ${found.kind}`,
52
- found.message,
53
- ),
46
+ export function annotations(report: Report, options: RenderOptions): Iterable<string> {
47
+ return coalesced(
48
+ (function* lines(): Generator<string, void, undefined> {
49
+ for (const found of report.findings) {
50
+ yield `${command(
51
+ fails(found.severity, options.failOn) ? "error" : "warning",
52
+ found.position,
53
+ found.position.end_line,
54
+ `${found.code} ${found.kind}`,
55
+ found.message,
56
+ )}\n`;
57
+ }
58
+ for (const stale of report.stale_suppressions) {
59
+ yield `${command(
60
+ "error",
61
+ stale.position,
62
+ stale.position.line,
63
+ `${stale.code} ${kindName(stale.code)}`,
64
+ stale.message,
65
+ )}\n`;
66
+ }
67
+ })(),
54
68
  );
55
- for (const stale of report.stale_suppressions) {
56
- lines.push(
57
- command(
58
- "error",
59
- stale.position,
60
- stale.position.line,
61
- `${stale.code} ${kindName(stale.code)}`,
62
- stale.message,
63
- ),
64
- );
65
- }
66
- return lines.map((line) => `${line}\n`).join("");
67
69
  }
@@ -1,7 +1,8 @@
1
+ import { jsonChunks } from "../json-chunks.ts";
1
2
  import type { Report } from "../report.ts";
2
3
 
3
4
  /** The indentation of the report document, which with the schema's member order fixes its bytes. */
4
5
  const INDENT = 2;
5
6
 
6
- /** The report document itself: the report, indented, with a closing newline. */
7
- export const json = (report: Report): string => `${JSON.stringify(report, null, INDENT)}\n`;
7
+ /** The report document itself, in pieces: the report, indented, with a closing newline. */
8
+ export const json = (report: Report): Iterable<string> => jsonChunks(report, INDENT);
@@ -12,8 +12,8 @@ export interface RenderOptions {
12
12
  readonly template: Template | undefined;
13
13
  }
14
14
 
15
- /** One rendering of a report, as the text of the file it is written to. */
16
- type Reporter = (report: Report, options: RenderOptions) => string;
15
+ /** One rendering of a report, as the pieces of the file it is written to, in order. */
16
+ type Reporter = (report: Report, options: RenderOptions) => Iterable<string>;
17
17
 
18
18
  /**
19
19
  * One format this analyzer renders: its reporter, and the suffix appended to the report's
@@ -1,9 +1,9 @@
1
1
  import type { Confidence, Severity } from "../config.ts";
2
- import { KINDS } from "../kinds.ts";
2
+ import { jsonChunks } from "../json-chunks.ts";
3
+ import { KINDS, type KindRow } from "../kinds.ts";
3
4
  import type { Report, WireFinding, WireStaleSuppression } from "../report.ts";
4
5
  import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
5
6
  import { RenderError, type RenderOptions } from "./reporter.ts";
6
- import { RULE_TEXTS } from "./rules.ts";
7
7
  import { utf8Bytes } from "./sha256.ts";
8
8
 
9
9
  const SCHEMA =
@@ -12,7 +12,7 @@ const VERSION = "2.1.0";
12
12
  /** The unit a finding's column counts. */
13
13
  const COLUMN_KIND = "utf16CodeUnits";
14
14
  const URI_BASE_ID = "%SRCROOT%";
15
- const ROOT_DESCRIPTION = "The target root, the directory the analyzer was run on.";
15
+ const ROOT_DESCRIPTION = "The target root.";
16
16
 
17
17
  /** The most related locations one result carries; the report stays complete beyond them. */
18
18
  const MAX_RELATED = 100;
@@ -41,23 +41,26 @@ function firstSentence(text: string): string {
41
41
  return text;
42
42
  }
43
43
 
44
+ /** Every live kind whose languages include one of the run's, in code order. */
45
+ function kindsOf(languages: readonly string[]): readonly KindRow[] {
46
+ return [...KINDS.values()].filter((kind) =>
47
+ kind.languages.some((language) => languages.includes(language)),
48
+ );
49
+ }
50
+
44
51
  /**
45
- * One rule per live kind of this analyzer's language, in code order, whether or not a result
52
+ * One rule per live kind of the run's languages, in code order, whether or not a result
46
53
  * names it, so a rule's index is the same in every run and under every configuration.
47
54
  */
48
- function rules(): readonly Record<string, unknown>[] {
49
- return [...RULE_TEXTS].map(([code, text]) => {
50
- const kind = KINDS.get(code);
51
- if (kind === undefined) {
52
- throw new RenderError(`the rule ${code} names no live kind`);
53
- }
55
+ function rules(kinds: readonly KindRow[]): readonly Record<string, unknown>[] {
56
+ return kinds.map((kind) => {
54
57
  return {
55
- id: code,
58
+ id: kind.code,
56
59
  name: kind.name,
57
- shortDescription: { text: firstSentence(text.rule) },
58
- fullDescription: { text: text.rule },
60
+ shortDescription: { text: firstSentence(kind.rule) },
61
+ fullDescription: { text: kind.rule },
59
62
  help: {
60
- text: text.precondition === undefined ? text.rule : `${text.rule}\n\n${text.precondition}`,
63
+ text: kind.precondition === undefined ? kind.rule : `${kind.rule}\n\n${kind.precondition}`,
61
64
  },
62
65
  defaultConfiguration: { level: LEVEL[kind.defaultSeverity] },
63
66
  properties: {
@@ -155,6 +158,11 @@ function relatedOf(found: WireFinding): readonly Related[] {
155
158
  for (const at of found.details.write_positions ?? []) {
156
159
  related.push({ label: "write", at, endLine: at.end_line });
157
160
  }
161
+ for (const one of found.component.members ?? []) {
162
+ if (one.ref !== found.symbol.ref) {
163
+ related.push({ label: "member", at: one.position, endLine: one.position.end_line });
164
+ }
165
+ }
158
166
  return related.slice(0, MAX_RELATED);
159
167
  }
160
168
 
@@ -253,9 +261,10 @@ function staleResult(
253
261
  * count it. A file the rendering cannot read, or a line it does not hold, fails the
254
262
  * rendering, because a line fingerprint computed from other bytes opens a second alert.
255
263
  */
256
- export function sarif(report: Report, options: RenderOptions): string {
257
- const ruleList = rules();
258
- const index = new Map([...RULE_TEXTS.keys()].map((code, at) => [code, at]));
264
+ export function sarif(report: Report, options: RenderOptions): Iterable<string> {
265
+ const kinds = kindsOf(report.analyzer.languages);
266
+ const ruleList = rules(kinds);
267
+ const index = new Map(kinds.map((kind, at) => [kind.code, at]));
259
268
  const hashOf = lineHashCache(options.readSource);
260
269
  const results = [
261
270
  ...report.findings.map((found) => findingResult(found, index, hashOf)),
@@ -283,5 +292,5 @@ export function sarif(report: Report, options: RenderOptions): string {
283
292
  },
284
293
  ],
285
294
  };
286
- return `${JSON.stringify(log, null, INDENT)}\n`;
295
+ return jsonChunks(log, INDENT);
287
296
  }