@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
@@ -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
 
@@ -115,7 +116,11 @@ const ESCAPES: Readonly<Record<string, string>> = {
115
116
  const ESCAPE_WIDTHS: Readonly<Record<string, number>> = { x: 2, u: 4, U: 8 };
116
117
 
117
118
  const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/uy;
118
- const NUMBER = /[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/uy;
119
+ /** A number as the lexer reads one, before the subset decides whether it is a decimal integer. */
120
+ const NUMBER = /[+-]?[0-9][0-9A-Za-z_.]*/uy;
121
+
122
+ /** The one number form the subset admits: a decimal integer with no leading zero. */
123
+ const DECIMAL_INTEGER = /^[+-]?(?:0|[1-9][0-9]*)$/u;
119
124
 
120
125
  function lineOf(source: string, at: number): number {
121
126
  let line = 1;
@@ -224,6 +229,11 @@ function lexAction(
224
229
  NUMBER.lastIndex = i;
225
230
  const number = NUMBER.exec(source)?.[0];
226
231
  if (number !== undefined) {
232
+ if (!DECIMAL_INTEGER.test(number)) {
233
+ throw new TemplateError(
234
+ `line ${String(line)}: ${JSON.stringify(number)} is not a decimal integer`,
235
+ );
236
+ }
227
237
  tokens.push({ t: "lit", value: Number(number) });
228
238
  i += number.length;
229
239
  continue;
@@ -636,7 +646,61 @@ function compareBasic(name: string, a: Value, b: Value): number {
636
646
  if (kind === "boolean") {
637
647
  return Number.NaN;
638
648
  }
639
- return (a as number | string) < (b as number | string) ? -1 : 1;
649
+ if (kind === "string") {
650
+ return byCodePoints(a as string, b as string);
651
+ }
652
+ return (a as number) < (b as number) ? -1 : 1;
653
+ }
654
+
655
+ /**
656
+ * Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
657
+ * code points: the order of their UTF-16 code units differs above U+FFFF.
658
+ */
659
+ function byCodePoints(a: string, b: string): number {
660
+ let at = 0;
661
+ while (at < a.length && at < b.length) {
662
+ const x = a.codePointAt(at) ?? 0;
663
+ const y = b.codePointAt(at) ?? 0;
664
+ if (x !== y) {
665
+ return x < y ? -1 : 1;
666
+ }
667
+ at += x > 0xffff ? 2 : 1;
668
+ }
669
+ return a.length - b.length;
670
+ }
671
+
672
+ /** The escapes `%q` writes for the control characters that have one of their own. */
673
+ const QUOTED_ESCAPES: Readonly<Record<string, string>> = {
674
+ '"': '\\"',
675
+ "\\": "\\\\",
676
+ "\u0007": "\\a",
677
+ "\b": "\\b",
678
+ "\f": "\\f",
679
+ "\n": "\\n",
680
+ "\r": "\\r",
681
+ "\t": "\\t",
682
+ "\v": "\\v",
683
+ };
684
+
685
+ /**
686
+ * The text `%q` writes: double quotes around the text, a quote and a backslash escaped,
687
+ * each control character with an escape of its own written as it, every other character
688
+ * below U+0020 and U+007F written as `\x` and two lowercase hexadecimal digits, and every
689
+ * other character as itself.
690
+ */
691
+ function goQuoted(text: string): string {
692
+ let out = '"';
693
+ for (const char of text) {
694
+ const code = char.codePointAt(0) ?? 0;
695
+ if (Object.hasOwn(QUOTED_ESCAPES, char)) {
696
+ out += QUOTED_ESCAPES[char] ?? "";
697
+ } else if (code < 0x20 || code === 0x7f) {
698
+ out += `\\x${code.toString(16).padStart(2, "0")}`;
699
+ } else {
700
+ out += char;
701
+ }
702
+ }
703
+ return `${out}"`;
640
704
  }
641
705
 
642
706
  /** `printf`'s verbs, each over one operand. */
@@ -663,10 +727,10 @@ function printf(format: string, args: readonly Value[]): string {
663
727
  switch (verb) {
664
728
  case "v":
665
729
  case "s":
666
- out += formatted(arg, false);
730
+ out += formatted(arg, true);
667
731
  break;
668
732
  case "q":
669
- out += JSON.stringify(formatted(arg, false));
733
+ out += goQuoted(formatted(arg, true));
670
734
  break;
671
735
  case "d":
672
736
  if (typeof arg !== "number" || !Number.isInteger(arg)) {
@@ -768,11 +832,11 @@ function call(name: string, args: readonly Value[]): Value {
768
832
  return args
769
833
  .map((arg, i) => {
770
834
  const spaced = i > 0 && typeof arg !== "string" && typeof args[i - 1] !== "string";
771
- return `${spaced ? " " : ""}${formatted(arg, false)}`;
835
+ return `${spaced ? " " : ""}${formatted(arg, true)}`;
772
836
  })
773
837
  .join("");
774
838
  case "println":
775
- return `${args.map((arg) => formatted(arg, false)).join(" ")}\n`;
839
+ return `${args.map((arg) => formatted(arg, true)).join(" ")}\n`;
776
840
  case "printf":
777
841
  if (typeof first !== "string") {
778
842
  throw new RenderError("printf takes a format string first");
@@ -786,12 +850,24 @@ function call(name: string, args: readonly Value[]): Value {
786
850
  /** One execution of a template over a document. */
787
851
  class Execution {
788
852
  private readonly scopes: Map<string, Value>[];
789
- out = "";
853
+ /** The rendering so far, in pieces of about {@link CHUNK_LENGTH}, the last still growing. */
854
+ readonly pieces: string[] = [""];
790
855
 
791
856
  constructor(root: Value) {
792
857
  this.scopes = [new Map([["$", root]])];
793
858
  }
794
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
+
795
871
  private lookup(name: string): Value {
796
872
  for (let i = this.scopes.length - 1; i >= 0; i -= 1) {
797
873
  const scope = this.scopes[i];
@@ -900,12 +976,12 @@ class Execution {
900
976
  for (const node of nodes) {
901
977
  switch (node.k) {
902
978
  case "text":
903
- this.out += node.text;
979
+ this.write(node.text);
904
980
  break;
905
981
  case "action": {
906
982
  const value = this.pipeline(node.pipe, dot, true);
907
983
  if (node.pipe.vars.length === 0) {
908
- this.out += formatted(value, false);
984
+ this.write(formatted(value, false));
909
985
  }
910
986
  break;
911
987
  }
@@ -978,12 +1054,41 @@ class Execution {
978
1054
  }
979
1055
 
980
1056
  /**
981
- * The template's rendering of one document. The rendering is built whole, so a template
982
- * 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.
983
1088
  */
984
- export function renderTemplate(template: Template, document: unknown): string {
985
- 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;
986
1091
  const execution = new Execution(root);
987
1092
  execution.run(template.nodes, root);
988
- return execution.out;
1093
+ return execution.pieces.filter((piece) => piece !== "");
989
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
- };
package/src/roots.ts CHANGED
@@ -15,6 +15,7 @@ import { globExpression } from "./glob.ts";
15
15
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
16
16
  import type { Manifest, ManifestEntry } from "./manifest.ts";
17
17
  import { byPosition } from "./position.ts";
18
+ import { UNANSWERED } from "./query.ts";
18
19
  import type { ProjectView } from "./session.ts";
19
20
  import { sourceFilesOf } from "./source-files.ts";
20
21
 
@@ -70,6 +71,12 @@ export interface Roots {
70
71
  * {@link unmatchedEverywhere} is what turns one into the other.
71
72
  */
72
73
  readonly unmatched: readonly string[];
74
+ /**
75
+ * Every declaration of the project, where the checker left the module or export table
76
+ * of an entry or a published file unanswered, and none otherwise: which of them that
77
+ * file exports is unknown.
78
+ */
79
+ readonly unanswered: readonly string[];
73
80
  }
74
81
 
75
82
  /** What decides the roots beside the declarations themselves. */
@@ -225,7 +232,7 @@ export function roots<Brand>(
225
232
  enter(point.file, point.rule, point.source, point.exports);
226
233
  }
227
234
 
228
- const exportsOf = exportTables(project, held, [
235
+ const { tables: exportsOf, answered } = exportTables(project, held, [
229
236
  ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
230
237
  ...published,
231
238
  ]);
@@ -258,47 +265,62 @@ export function roots<Brand>(
258
265
  return site || compare(a.kind, b.kind) || compare(a.source, b.source);
259
266
  });
260
267
 
261
- return { configFile: project.configFile, liveUnderReachability: ordered, unmatched };
268
+ return {
269
+ configFile: project.configFile,
270
+ liveUnderReachability: ordered,
271
+ unmatched,
272
+ unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
273
+ };
262
274
  }
263
275
 
264
276
  /**
265
277
  * Every declaration each file's module exports, by the file's name: the declaration
266
278
  * each name of the export table is, and every link of a re-export's chain. A name
267
- * whose declaration is outside the project's own files belongs to another program
268
- * and names nothing here.
269
- *
270
- * One batch resolves every file to its module and one request per module reads
271
- * its export table, star re-exports resolved into it. A file that is no module
272
- * exports nothing.
279
+ * whose declaration is outside the project's own files names nothing here. One batch
280
+ * resolves every file to its module and one request per module reads its export
281
+ * table, star re-exports resolved into it. A file that is no module exports nothing.
282
+ * `answered` is false where the checker left a table or a link of a chain unanswered.
273
283
  */
274
284
  function exportTables<Brand>(
275
285
  project: ProjectView<Brand>,
276
286
  held: Inventory,
277
287
  wanted: readonly SourceFile[],
278
- ): ReadonlyMap<string, readonly string[]> {
288
+ ): { readonly tables: ReadonlyMap<string, readonly string[]>; readonly answered: boolean } {
279
289
  const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
280
290
  const tables = new Map<string, readonly string[]>();
291
+ let answered = true;
281
292
  if (unique.length === 0) {
282
- return tables;
293
+ return { tables, answered };
283
294
  }
284
295
  const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
285
296
  const chains = aliasChains(project, held);
286
297
 
298
+ const topLevel = (file: SourceFile): readonly string[] => {
299
+ const fileId = held.declarations.get(nodeKey(file, file));
300
+ return held.symbols.filter((symbol) => symbol.parent === fileId).map((symbol) => symbol.id);
301
+ };
287
302
  unique.forEach((file, index) => {
288
303
  const module = modules[index];
289
304
  if (module === undefined) {
290
305
  tables.set(file.fileName, []);
291
306
  return;
292
307
  }
308
+ const table = module === UNANSWERED ? UNANSWERED : project.queries.exportsOfModule(module);
309
+ if (table === UNANSWERED) {
310
+ answered = false;
311
+ tables.set(file.fileName, topLevel(file));
312
+ return;
313
+ }
293
314
  const ids = new Set<string>();
294
- for (const exported of project.checker.getExportsOfModule(module)) {
315
+ for (const exported of table) {
295
316
  for (const target of chains.chainOf(exported)) {
317
+ answered &&= !target.guessed;
296
318
  ids.add(target.id);
297
319
  }
298
320
  }
299
321
  tables.set(file.fileName, [...ids]);
300
322
  });
301
- return tables;
323
+ return { tables, answered };
302
324
  }
303
325
 
304
326
  /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
@@ -1,11 +1,12 @@
1
1
  /**
2
- * The serialization-contract class: the properties of a class whose values reach
3
- * `JSON.stringify` or a call `ts.serializers` names, and those and `toJSON` and
4
- * `toString` where a value is passed to an `unknown` or `any` parameter of a function
5
- * outside the analysis. A function of the project handing such a parameter of its own
6
- * to a destination is one, holding back what that destination does, to a fixpoint. A
7
- * function invoked through `Function.prototype.call`, `apply` or `bind` is the callee,
8
- * given the arguments those methods pass it.
2
+ * The serialization-contract class: the data members and `toJSON` of every class,
3
+ * interface and object type a value's static type names, and of every such type its
4
+ * data members' types reach, where the value reaches `JSON.stringify` or a call
5
+ * `ts.serializers` names, and those and `toString` where it is passed to an `unknown`
6
+ * or `any` parameter of a function outside the analysis. A function of the project
7
+ * handing such a parameter of its own to a destination is one, holding back what that
8
+ * destination does, to a fixpoint. A function invoked through `Function.prototype.call`,
9
+ * `apply` or `bind` is the callee, given the arguments those methods pass it.
9
10
  */
10
11
 
11
12
  import {
@@ -56,8 +57,9 @@ import {
56
57
  import type { DetectorInput, Evidence } from "./exempt.ts";
57
58
  import { nodeKey, type Inventory } from "./inventory.ts";
58
59
  import { renderPosition } from "./position.ts";
60
+ import { must } from "./query.ts";
59
61
  import type { ProjectView } from "./session.ts";
60
- import { valueReach } from "./value-reach.ts";
62
+ import { valueReach, type Reached } from "./value-reach.ts";
61
63
 
62
64
  /** The serializer every run reads, whatever the configuration names. */
63
65
  const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
@@ -65,6 +67,9 @@ const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.string
65
67
  /** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
66
68
  const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
67
69
 
70
+ /** The method a serializer calls on a value that carries one, wherever the value goes. */
71
+ const SERIALIZED_CONVERSION = "toJSON";
72
+
68
73
  /** The parameter types that keep nothing of the value they are given. */
69
74
  const ERASED = TypeFlags.Any | TypeFlags.Unknown;
70
75
 
@@ -312,12 +317,13 @@ function erasedParameters<Brand>(
312
317
  if (!parameter.rest) {
313
318
  return (type.flags & ERASED) !== 0;
314
319
  }
315
- const [element] = type.isTypeReference() ? project.checker.getTypeArguments(type) : [];
320
+ const [element] = must(project.queries.typeArguments(type));
316
321
  return element !== undefined && (element.flags & ERASED) !== 0;
317
322
  });
318
323
  const bySymbol = new Map<number, Parameter>();
319
324
  project
320
325
  .symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
326
+ .map(must)
321
327
  .forEach((symbol, index) => {
322
328
  const parameter = erased[index];
323
329
  if (symbol !== undefined && parameter !== undefined) {
@@ -343,11 +349,11 @@ function handedOn<Brand>(
343
349
  ),
344
350
  );
345
351
  const plain = candidates.filter((one) => !one.value.shorthand);
346
- const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
352
+ const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name))).map(must);
347
353
  const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
348
354
  return candidates.flatMap((one) => {
349
355
  const symbol = one.value.shorthand
350
- ? project.shorthandValueAt(project.handle(one.value.name))
356
+ ? must(project.shorthandValueAt(project.handle(one.value.name)))
351
357
  : symbolOf.get(one);
352
358
  const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
353
359
  return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
@@ -378,8 +384,8 @@ function destinations<Brand>(
378
384
  walked: Walked,
379
385
  ): Destinations {
380
386
  const { project, held } = input;
381
- const checker = project.checker;
382
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
387
+ const queries = project.queries;
388
+ const own = project.ownPaths();
383
389
  const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(
384
390
  namesADeclaration,
385
391
  );
@@ -418,7 +424,7 @@ function destinations<Brand>(
418
424
  form !== undefined &&
419
425
  method !== undefined &&
420
426
  outside(method) &&
421
- FUNCTION_INTERFACES.has(method.getParent()?.name ?? "")
427
+ FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")
422
428
  ) {
423
429
  return form.invocation;
424
430
  }
@@ -435,16 +441,16 @@ function destinations<Brand>(
435
441
  const invoked = invocation(call);
436
442
  // Through a method of the prototype the resolved signature is the method's, so
437
443
  // the parameters are read off the callee's own signatures, any one erasing.
438
- const type = invoked.indirect ? checker.getTypeAtLocation(invoked.callee) : undefined;
444
+ const type = invoked.indirect ? must(queries.typeAt(invoked.callee)) : undefined;
439
445
  const signatures = invoked.indirect
440
446
  ? type === undefined
441
447
  ? []
442
- : checker.getSignaturesOfType(type, SignatureKind.Call)
443
- : [checker.getResolvedSignature(call)].filter((one) => one !== undefined);
448
+ : must(queries.signaturesOf(type, SignatureKind.Call))
449
+ : [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
444
450
  indexes = new Set(
445
451
  invoked.arguments.flatMap((_unused, index) =>
446
452
  signatures.some((signature) => {
447
- const parameter = checker.getParameterType(signature, index);
453
+ const parameter = must(queries.parameterType(signature, index));
448
454
  return parameter !== undefined && (parameter.flags & ERASED) !== 0;
449
455
  })
450
456
  ? [index]
@@ -521,10 +527,12 @@ function destinations<Brand>(
521
527
  return { invocation, at, mayReceive };
522
528
  }
523
529
 
524
- /** One argument whose value carries classes of the target into a destination. */
530
+ /** One argument whose value carries classes and object types of the target into a destination. */
525
531
  interface Flow {
526
532
  readonly site: Node;
527
533
  readonly classes: ReadonlySet<string>;
534
+ /** The members of the interfaces and object types the value carries. */
535
+ readonly members: readonly TSSymbol[];
528
536
  readonly to: Destination;
529
537
  }
530
538
 
@@ -554,31 +562,34 @@ function flowsOf<Brand>(
554
562
  );
555
563
  return arguments_.flatMap((one) => {
556
564
  const type = typeOf.get(one.value);
557
- let classes: ReadonlySet<string> = new Set<string>();
565
+ let reached: Reached = { classes: new Set<string>(), members: [] };
558
566
  if (one.value.kind === SyntaxKind.ThisKeyword) {
559
- classes = new Set(one.site.self === undefined ? [] : [one.site.self]);
567
+ reached = {
568
+ classes: new Set(one.site.self === undefined ? [] : [one.site.self]),
569
+ members: [],
570
+ };
560
571
  } else if (type !== undefined) {
561
- classes = reach.classesOf(type);
572
+ reached = reach.reachedFrom(type);
562
573
  }
563
- const destination = classes.size === 0 ? undefined : to.at(one.site.call, one.argument);
564
- return destination === undefined ? [] : [{ site: one.value, classes, to: destination }];
574
+ const carries = reached.classes.size > 0 || reached.members.length > 0;
575
+ const destination = carries ? to.at(one.site.call, one.argument) : undefined;
576
+ return destination === undefined
577
+ ? []
578
+ : [{ site: one.value, classes: reached.classes, members: reached.members, to: destination }];
565
579
  });
566
580
  }
567
581
 
568
582
  /**
569
- * The serialization-contract detector: each property of each class whose values reach
570
- * a destination, and the two conversion methods where the destination is outside the
571
- * analysis, recorded at the argument with the callee named.
583
+ * The serialization-contract detector: each data member and `toJSON` of each class,
584
+ * interface and object type a value that reaches a destination carries, and `toString`
585
+ * as well where the destination is outside the analysis, recorded at the argument with
586
+ * the callee named.
572
587
  */
573
588
  export function serializationContract<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
574
589
  const { project, held, targetRoot } = input;
575
590
  const walked = walkProject(project, held);
576
- const flows = flowsOf(
577
- project,
578
- held,
579
- walked.sites,
580
- destinations(input, aliasChains(project, held), walked),
581
- );
591
+ const chains = aliasChains(project, held);
592
+ const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
582
593
  if (flows.length === 0) {
583
594
  return [];
584
595
  }
@@ -589,22 +600,30 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
589
600
  "instance",
590
601
  );
591
602
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
603
+ // A destination holds back a data member and `toJSON`, and `toString` as well where
604
+ // the value left the analysis.
605
+ const holdsBack = (flags: SymbolFlags, name: string, to: Destination): boolean =>
606
+ (flags & SymbolFlags.Property) !== 0 ||
607
+ ((flags & SymbolFlags.Method) !== 0 &&
608
+ (name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
592
609
  const found: Evidence[] = [];
593
610
  for (const flow of flows) {
594
611
  const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
595
612
  const detail = `passed to ${flow.to.callee}`;
596
613
  for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
597
614
  const symbol = byId.get(member.id);
598
- const isProperty = (member.flags & SymbolFlags.Property) !== 0;
599
- const converts =
600
- flow.to.reach === "conversions" &&
601
- (member.flags & SymbolFlags.Method) !== 0 &&
602
- symbol !== undefined &&
603
- CONVERSIONS.has(memberComponent(symbol, byId) ?? "");
604
- if (isProperty || converts) {
615
+ const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
616
+ if (holdsBack(member.flags, name, flow.to)) {
605
617
  found.push({ id: member.id, detail, site });
606
618
  }
607
619
  }
620
+ for (const member of flow.members) {
621
+ if (holdsBack(member.flags, member.name, flow.to)) {
622
+ for (const id of chains.declarationsOf(member)) {
623
+ found.push({ id, detail, site });
624
+ }
625
+ }
626
+ }
608
627
  }
609
628
  return found;
610
629
  }