@cplieger/deadset-ts 3.13.0 → 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 (88) hide show
  1. package/README.md +3 -3
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +119 -23
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/configured-declarations.js +22 -9
  8. package/dist/src/dependencies.js +34 -17
  9. package/dist/src/deprecation.js +35 -24
  10. package/dist/src/destructuring.js +37 -29
  11. package/dist/src/discover.js +105 -28
  12. package/dist/src/entry-points.js +7 -0
  13. package/dist/src/enum-group.js +46 -40
  14. package/dist/src/exempt.js +42 -1
  15. package/dist/src/findings/emitters.js +15 -1
  16. package/dist/src/findings/interfaces.js +7 -1
  17. package/dist/src/framework-lifecycle.js +8 -6
  18. package/dist/src/implementations.js +45 -23
  19. package/dist/src/interface-satisfaction.js +30 -27
  20. package/dist/src/intra-function-parts.js +37 -20
  21. package/dist/src/inventory.js +85 -4
  22. package/dist/src/json-chunks.js +76 -0
  23. package/dist/src/partial.js +23 -0
  24. package/dist/src/query.js +207 -0
  25. package/dist/src/references.js +92 -20
  26. package/dist/src/reflective-lookup.js +18 -9
  27. package/dist/src/report.js +3 -1
  28. package/dist/src/reporters/annotations.js +9 -5
  29. package/dist/src/reporters/json.js +3 -2
  30. package/dist/src/reporters/sarif.js +2 -1
  31. package/dist/src/reporters/template.js +48 -7
  32. package/dist/src/reporters/text.js +8 -6
  33. package/dist/src/roots.js +27 -11
  34. package/dist/src/serialization-contract.js +12 -10
  35. package/dist/src/session.js +79 -39
  36. package/dist/src/suppress-file.js +2 -1
  37. package/dist/src/sweep.js +4 -1
  38. package/dist/src/value-reach.js +14 -19
  39. package/dist/src/verbs/analyze.js +20 -3
  40. package/dist/src/verbs/explain.js +22 -9
  41. package/dist/src/verbs/print-projects.js +40 -7
  42. package/dist/src/verbs/print-retained.js +6 -2
  43. package/dist/src/verbs/print-roots.js +5 -1
  44. package/jsr.json +1 -1
  45. package/package.json +3 -3
  46. package/src/alias-chain.ts +75 -11
  47. package/src/analysis.ts +223 -61
  48. package/src/calls.ts +10 -9
  49. package/src/class-members.ts +10 -11
  50. package/src/configured-declarations.ts +29 -13
  51. package/src/dependencies.ts +42 -20
  52. package/src/deprecation.ts +53 -27
  53. package/src/destructuring.ts +50 -39
  54. package/src/discover.ts +177 -33
  55. package/src/entry-points.ts +7 -0
  56. package/src/enum-group.ts +56 -42
  57. package/src/exempt.ts +50 -2
  58. package/src/findings/emitters.ts +18 -1
  59. package/src/findings/interfaces.ts +7 -1
  60. package/src/framework-lifecycle.ts +10 -6
  61. package/src/host.ts +5 -3
  62. package/src/implementations.ts +59 -23
  63. package/src/interface-satisfaction.ts +30 -27
  64. package/src/intra-function-parts.ts +43 -25
  65. package/src/inventory.ts +97 -4
  66. package/src/json-chunks.ts +87 -0
  67. package/src/partial.ts +40 -0
  68. package/src/query.ts +522 -0
  69. package/src/references.ts +119 -38
  70. package/src/reflective-lookup.ts +21 -11
  71. package/src/report.ts +14 -2
  72. package/src/reporters/annotations.ts +23 -21
  73. package/src/reporters/json.ts +3 -2
  74. package/src/reporters/reporter.ts +2 -2
  75. package/src/reporters/sarif.ts +3 -2
  76. package/src/reporters/template.ts +50 -8
  77. package/src/reporters/text.ts +12 -12
  78. package/src/roots.ts +34 -12
  79. package/src/serialization-contract.ts +12 -10
  80. package/src/session.ts +146 -58
  81. package/src/suppress-file.ts +6 -2
  82. package/src/sweep.ts +9 -4
  83. package/src/value-reach.ts +20 -23
  84. package/src/verbs/analyze.ts +25 -3
  85. package/src/verbs/explain.ts +20 -9
  86. package/src/verbs/print-projects.ts +53 -7
  87. package/src/verbs/print-retained.ts +6 -2
  88. package/src/verbs/print-roots.ts +5 -1
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
  }
@@ -24,10 +24,10 @@ import {
24
24
  type SourceFile,
25
25
  } from "@typescript/native/unstable/ast";
26
26
  import { TypeFlags, type Type } from "@typescript/native/unstable/sync";
27
- import { typesAt } from "./class-members.ts";
28
27
  import type { DetectorInput, Evidence } from "./exempt.ts";
29
28
  import { memberNames } from "./member-names.ts";
30
29
  import { renderPosition } from "./position.ts";
30
+ import { isAnswered, UNANSWERED } from "./query.ts";
31
31
  import type { ProjectView } from "./session.ts";
32
32
 
33
33
  /** The `Reflect` methods whose second argument is a property key. */
@@ -95,14 +95,17 @@ function lookupsOf(file: SourceFile): readonly Lookup[] {
95
95
  return found;
96
96
  }
97
97
 
98
- /** Whether a receiver's type is not `any` and declares a string index signature. */
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
+ */
99
103
  function readsAMapping<Brand>(project: ProjectView<Brand>, type: Type): boolean {
100
104
  if ((type.flags & TypeFlags.Any) !== 0) {
101
105
  return false;
102
106
  }
103
- return project.checker
104
- .getIndexInfosOfType(type)
105
- .some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
107
+ const infos = project.queries.indexInfosOf(type);
108
+ return isAnswered(infos) && infos.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
106
109
  }
107
110
 
108
111
  /**
@@ -119,7 +122,7 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
119
122
 
120
123
  // Whether each written `Reflect` is the global one, asked in one batch: a
121
124
  // declaration of that name in one of the project's own files shadows it.
122
- const ownFiles = new Set(files.map((file) => file.fileName));
125
+ const ownFiles = project.ownPaths();
123
126
  const reflects = lookups.flatMap((lookup) =>
124
127
  lookup.reflect === undefined ? [] : [lookup.reflect],
125
128
  );
@@ -127,9 +130,11 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
127
130
  if (reflects.length > 0) {
128
131
  project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
129
132
  const node = reflects[index];
130
- const declaredHere = symbol?.declarations.some((declaration) =>
131
- ownFiles.has(declaration.path),
132
- );
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));
133
138
  if (node !== undefined && declaredHere === false) {
134
139
  global.add(node);
135
140
  }
@@ -142,9 +147,14 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
142
147
  lookup.receiver === undefined ? [] : [lookup.receiver],
143
148
  );
144
149
  const mapped = new Set<Node>();
145
- typesAt(project, receivers).forEach((type, index) => {
150
+ project.queries.typesAt(receivers).forEach((type, index) => {
146
151
  const receiver = receivers[index];
147
- if (receiver !== undefined && type !== undefined && readsAMapping(project, type)) {
152
+ if (
153
+ receiver !== undefined &&
154
+ type !== undefined &&
155
+ isAnswered(type) &&
156
+ readsAMapping(project, type)
157
+ ) {
148
158
  mapped.add(receiver);
149
159
  }
150
160
  });
package/src/report.ts CHANGED
@@ -142,6 +142,15 @@ export interface ReportConfiguration {
142
142
  readonly project: string;
143
143
  }
144
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
+
145
154
  /** One consumer the run loaded, at the directory a report names it by. */
146
155
  interface ReportConsumer {
147
156
  readonly id: string;
@@ -172,7 +181,7 @@ export interface Report {
172
181
  };
173
182
  readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
174
183
  readonly configurations: readonly ReportConfiguration[];
175
- readonly configurations_not_built: readonly never[];
184
+ readonly configurations_not_built: readonly ReportNotBuilt[];
176
185
  readonly consumers: {
177
186
  readonly declared: number;
178
187
  readonly loaded: readonly {
@@ -203,6 +212,7 @@ export interface ReportInput {
203
212
  readonly declaredGaps: readonly DeclaredGap[];
204
213
  readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
205
214
  readonly configurations: readonly ReportConfiguration[];
215
+ readonly notBuilt: readonly ReportNotBuilt[];
206
216
  readonly loaded: readonly ReportConsumer[];
207
217
  readonly unavailable: readonly UnavailableConsumer[];
208
218
  readonly result: PassResult;
@@ -436,7 +446,9 @@ export function buildReport(input: ReportInput): Report {
436
446
  configurations: [...input.configurations]
437
447
  .sort((a, b) => compare(a.id, b.id))
438
448
  .map((one) => ({ id: one.id, project: one.project })),
439
- 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 })),
440
452
  consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
441
453
  findings,
442
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,4 +1,5 @@
1
1
  import type { Confidence, Severity } from "../config.ts";
2
+ import { jsonChunks } from "../json-chunks.ts";
2
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";
@@ -260,7 +261,7 @@ function staleResult(
260
261
  * count it. A file the rendering cannot read, or a line it does not hold, fails the
261
262
  * rendering, because a line fingerprint computed from other bytes opens a second alert.
262
263
  */
263
- export function sarif(report: Report, options: RenderOptions): string {
264
+ export function sarif(report: Report, options: RenderOptions): Iterable<string> {
264
265
  const kinds = kindsOf(report.analyzer.languages);
265
266
  const ruleList = rules(kinds);
266
267
  const index = new Map(kinds.map((kind, at) => [kind.code, at]));
@@ -291,5 +292,5 @@ export function sarif(report: Report, options: RenderOptions): string {
291
292
  },
292
293
  ],
293
294
  };
294
- return `${JSON.stringify(log, null, INDENT)}\n`;
295
+ return jsonChunks(log, INDENT);
295
296
  }
@@ -7,6 +7,7 @@
7
7
  * and a member the document does not carry fails the rendering.
8
8
  */
9
9
 
10
+ import { CHUNK_LENGTH } from "../json-chunks.ts";
10
11
  import { RenderError } from "./reporter.ts";
11
12
  import { utf8Bytes } from "./sha256.ts";
12
13
 
@@ -849,12 +850,24 @@ function call(name: string, args: readonly Value[]): Value {
849
850
  /** One execution of a template over a document. */
850
851
  class Execution {
851
852
  private readonly scopes: Map<string, Value>[];
852
- out = "";
853
+ /** The rendering so far, in pieces of about {@link CHUNK_LENGTH}, the last still growing. */
854
+ readonly pieces: string[] = [""];
853
855
 
854
856
  constructor(root: Value) {
855
857
  this.scopes = [new Map([["$", root]])];
856
858
  }
857
859
 
860
+ private write(text: string): void {
861
+ const last = this.pieces.length - 1;
862
+ const held = (this.pieces[last] ?? "") + text;
863
+ if (held.length >= CHUNK_LENGTH) {
864
+ this.pieces[last] = held;
865
+ this.pieces.push("");
866
+ } else {
867
+ this.pieces[last] = held;
868
+ }
869
+ }
870
+
858
871
  private lookup(name: string): Value {
859
872
  for (let i = this.scopes.length - 1; i >= 0; i -= 1) {
860
873
  const scope = this.scopes[i];
@@ -963,12 +976,12 @@ class Execution {
963
976
  for (const node of nodes) {
964
977
  switch (node.k) {
965
978
  case "text":
966
- this.out += node.text;
979
+ this.write(node.text);
967
980
  break;
968
981
  case "action": {
969
982
  const value = this.pipeline(node.pipe, dot, true);
970
983
  if (node.pipe.vars.length === 0) {
971
- this.out += formatted(value, false);
984
+ this.write(formatted(value, false));
972
985
  }
973
986
  break;
974
987
  }
@@ -1041,12 +1054,41 @@ class Execution {
1041
1054
  }
1042
1055
 
1043
1056
  /**
1044
- * The template's rendering of one document. The rendering is built whole, so a template
1045
- * that fails partway renders nothing, and the failure names what it could not evaluate.
1057
+ * One document as the JSON value it encodes: what `JSON.stringify` would write for it,
1058
+ * read back, without writing it. A member holding nothing is left out, and an array
1059
+ * element holding nothing, like a number that is not finite, is null.
1060
+ */
1061
+ function plainOf(value: unknown): Value | undefined {
1062
+ if (Array.isArray(value)) {
1063
+ return value.map((element: unknown) => plainOf(element) ?? null);
1064
+ }
1065
+ if (typeof value === "object" && value !== null) {
1066
+ const held: Record<string, Value> = {};
1067
+ for (const [key, member] of Object.entries(value)) {
1068
+ const plain = plainOf(member);
1069
+ if (plain !== undefined) {
1070
+ held[key] = plain;
1071
+ }
1072
+ }
1073
+ return held;
1074
+ }
1075
+ if (typeof value === "number") {
1076
+ return Number.isFinite(value) ? value : null;
1077
+ }
1078
+ if (typeof value === "string" || typeof value === "boolean" || value === null) {
1079
+ return value;
1080
+ }
1081
+ return undefined;
1082
+ }
1083
+
1084
+ /**
1085
+ * The template's rendering of one document, in pieces of about {@link CHUNK_LENGTH}.
1086
+ * The rendering is complete before it is answered, so a template that fails partway
1087
+ * renders nothing, and the failure names what it could not evaluate.
1046
1088
  */
1047
- export function renderTemplate(template: Template, document: unknown): string {
1048
- const root = JSON.parse(JSON.stringify(document)) as Value;
1089
+ export function renderTemplate(template: Template, document: unknown): readonly string[] {
1090
+ const root = plainOf(document) ?? null;
1049
1091
  const execution = new Execution(root);
1050
1092
  execution.run(template.nodes, root);
1051
- return execution.out;
1093
+ return execution.pieces.filter((piece) => piece !== "");
1052
1094
  }
@@ -1,3 +1,4 @@
1
+ import { coalesced } from "../json-chunks.ts";
1
2
  import type { Report, WireTotals } from "../report.ts";
2
3
 
3
4
  /** The kind token and the confidence a stale suppression is rendered with, both fixed. */
@@ -29,16 +30,15 @@ function summary(totals: WireTotals): string {
29
30
  * summary is deliberately not in that shape, so a filter for finding lines yields exactly
30
31
  * them. Nothing else is written: no timestamp, duration or host detail.
31
32
  */
32
- export const text = (report: Report): string => {
33
- const lines = report.findings.map(
34
- (found) =>
35
- `${found.position.path}:${String(found.position.line)}:${String(found.position.column)}: ${found.symbol.kind} ${found.symbol.name}: ${found.message} [${found.confidence}] (${found.code})`,
33
+ export const text = (report: Report): Iterable<string> =>
34
+ coalesced(
35
+ (function* lines(): Generator<string, void, undefined> {
36
+ for (const found of report.findings) {
37
+ yield `${found.position.path}:${String(found.position.line)}:${String(found.position.column)}: ${found.symbol.kind} ${found.symbol.name}: ${found.message} [${found.confidence}] (${found.code})\n`;
38
+ }
39
+ for (const stale of report.stale_suppressions) {
40
+ yield `${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})\n`;
41
+ }
42
+ yield `${summary(report.totals)}\n`;
43
+ })(),
36
44
  );
37
- for (const stale of report.stale_suppressions) {
38
- lines.push(
39
- `${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})`,
40
- );
41
- }
42
- lines.push(summary(report.totals));
43
- return `${lines.join("\n")}\n`;
44
- };