@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
@@ -1,14 +1,14 @@
1
+ import { jsonChunks } from "../json-chunks.js";
1
2
  import { KINDS } from "../kinds.js";
2
3
  import { lineHashes, symbolFingerprint } from "./fingerprint.js";
3
4
  import { RenderError } from "./reporter.js";
4
- import { RULE_TEXTS } from "./rules.js";
5
5
  import { utf8Bytes } from "./sha256.js";
6
6
  const SCHEMA = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
7
7
  const VERSION = "2.1.0";
8
8
  /** The unit a finding's column counts. */
9
9
  const COLUMN_KIND = "utf16CodeUnits";
10
10
  const URI_BASE_ID = "%SRCROOT%";
11
- const ROOT_DESCRIPTION = "The target root, the directory the analyzer was run on.";
11
+ const ROOT_DESCRIPTION = "The target root.";
12
12
  /** The most related locations one result carries; the report stays complete beyond them. */
13
13
  const MAX_RELATED = 100;
14
14
  const INDENT = 2;
@@ -32,23 +32,23 @@ function firstSentence(text) {
32
32
  }
33
33
  return text;
34
34
  }
35
+ /** Every live kind whose languages include one of the run's, in code order. */
36
+ function kindsOf(languages) {
37
+ return [...KINDS.values()].filter((kind) => kind.languages.some((language) => languages.includes(language)));
38
+ }
35
39
  /**
36
- * One rule per live kind of this analyzer's language, in code order, whether or not a result
40
+ * One rule per live kind of the run's languages, in code order, whether or not a result
37
41
  * names it, so a rule's index is the same in every run and under every configuration.
38
42
  */
39
- function rules() {
40
- return [...RULE_TEXTS].map(([code, text]) => {
41
- const kind = KINDS.get(code);
42
- if (kind === undefined) {
43
- throw new RenderError(`the rule ${code} names no live kind`);
44
- }
43
+ function rules(kinds) {
44
+ return kinds.map((kind) => {
45
45
  return {
46
- id: code,
46
+ id: kind.code,
47
47
  name: kind.name,
48
- shortDescription: { text: firstSentence(text.rule) },
49
- fullDescription: { text: text.rule },
48
+ shortDescription: { text: firstSentence(kind.rule) },
49
+ fullDescription: { text: kind.rule },
50
50
  help: {
51
- text: text.precondition === undefined ? text.rule : `${text.rule}\n\n${text.precondition}`,
51
+ text: kind.precondition === undefined ? kind.rule : `${kind.rule}\n\n${kind.precondition}`,
52
52
  },
53
53
  defaultConfiguration: { level: LEVEL[kind.defaultSeverity] },
54
54
  properties: {
@@ -120,6 +120,11 @@ function relatedOf(found) {
120
120
  for (const at of found.details.write_positions ?? []) {
121
121
  related.push({ label: "write", at, endLine: at.end_line });
122
122
  }
123
+ for (const one of found.component.members ?? []) {
124
+ if (one.ref !== found.symbol.ref) {
125
+ related.push({ label: "member", at: one.position, endLine: one.position.end_line });
126
+ }
127
+ }
123
128
  return related.slice(0, MAX_RELATED);
124
129
  }
125
130
  /** The finding's message, then a link to each related location, since only a linked one shows. */
@@ -202,8 +207,9 @@ function staleResult(stale, index, hashOf) {
202
207
  * rendering, because a line fingerprint computed from other bytes opens a second alert.
203
208
  */
204
209
  export function sarif(report, options) {
205
- const ruleList = rules();
206
- const index = new Map([...RULE_TEXTS.keys()].map((code, at) => [code, at]));
210
+ const kinds = kindsOf(report.analyzer.languages);
211
+ const ruleList = rules(kinds);
212
+ const index = new Map(kinds.map((kind, at) => [kind.code, at]));
207
213
  const hashOf = lineHashCache(options.readSource);
208
214
  const results = [
209
215
  ...report.findings.map((found) => findingResult(found, index, hashOf)),
@@ -231,5 +237,5 @@ export function sarif(report, options) {
231
237
  },
232
238
  ],
233
239
  };
234
- return `${JSON.stringify(log, null, INDENT)}\n`;
240
+ return jsonChunks(log, INDENT);
235
241
  }
@@ -6,6 +6,7 @@
6
6
  * and `println`. Any other function, `define`, `template` and `block` are refused at parse,
7
7
  * and a member the document does not carry fails the rendering.
8
8
  */
9
+ import { CHUNK_LENGTH } from "../json-chunks.js";
9
10
  import { RenderError } from "./reporter.js";
10
11
  import { utf8Bytes } from "./sha256.js";
11
12
  /** A template that does not parse, which refuses the invocation before any analysis. */
@@ -49,7 +50,10 @@ const ESCAPES = {
49
50
  /** The number of hexadecimal digits each numeric escape takes. */
50
51
  const ESCAPE_WIDTHS = { x: 2, u: 4, U: 8 };
51
52
  const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/uy;
52
- const NUMBER = /[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/uy;
53
+ /** A number as the lexer reads one, before the subset decides whether it is a decimal integer. */
54
+ const NUMBER = /[+-]?[0-9][0-9A-Za-z_.]*/uy;
55
+ /** The one number form the subset admits: a decimal integer with no leading zero. */
56
+ const DECIMAL_INTEGER = /^[+-]?(?:0|[1-9][0-9]*)$/u;
53
57
  function lineOf(source, at) {
54
58
  let line = 1;
55
59
  for (let i = 0; i < at; i += 1) {
@@ -153,6 +157,9 @@ function lexAction(source, at, line) {
153
157
  NUMBER.lastIndex = i;
154
158
  const number = NUMBER.exec(source)?.[0];
155
159
  if (number !== undefined) {
160
+ if (!DECIMAL_INTEGER.test(number)) {
161
+ throw new TemplateError(`line ${String(line)}: ${JSON.stringify(number)} is not a decimal integer`);
162
+ }
156
163
  tokens.push({ t: "lit", value: Number(number) });
157
164
  i += number.length;
158
165
  continue;
@@ -530,8 +537,61 @@ function compareBasic(name, a, b) {
530
537
  if (kind === "boolean") {
531
538
  return Number.NaN;
532
539
  }
540
+ if (kind === "string") {
541
+ return byCodePoints(a, b);
542
+ }
533
543
  return a < b ? -1 : 1;
534
544
  }
545
+ /**
546
+ * Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
547
+ * code points: the order of their UTF-16 code units differs above U+FFFF.
548
+ */
549
+ function byCodePoints(a, b) {
550
+ let at = 0;
551
+ while (at < a.length && at < b.length) {
552
+ const x = a.codePointAt(at) ?? 0;
553
+ const y = b.codePointAt(at) ?? 0;
554
+ if (x !== y) {
555
+ return x < y ? -1 : 1;
556
+ }
557
+ at += x > 0xffff ? 2 : 1;
558
+ }
559
+ return a.length - b.length;
560
+ }
561
+ /** The escapes `%q` writes for the control characters that have one of their own. */
562
+ const QUOTED_ESCAPES = {
563
+ '"': '\\"',
564
+ "\\": "\\\\",
565
+ "\u0007": "\\a",
566
+ "\b": "\\b",
567
+ "\f": "\\f",
568
+ "\n": "\\n",
569
+ "\r": "\\r",
570
+ "\t": "\\t",
571
+ "\v": "\\v",
572
+ };
573
+ /**
574
+ * The text `%q` writes: double quotes around the text, a quote and a backslash escaped,
575
+ * each control character with an escape of its own written as it, every other character
576
+ * below U+0020 and U+007F written as `\x` and two lowercase hexadecimal digits, and every
577
+ * other character as itself.
578
+ */
579
+ function goQuoted(text) {
580
+ let out = '"';
581
+ for (const char of text) {
582
+ const code = char.codePointAt(0) ?? 0;
583
+ if (Object.hasOwn(QUOTED_ESCAPES, char)) {
584
+ out += QUOTED_ESCAPES[char] ?? "";
585
+ }
586
+ else if (code < 0x20 || code === 0x7f) {
587
+ out += `\\x${code.toString(16).padStart(2, "0")}`;
588
+ }
589
+ else {
590
+ out += char;
591
+ }
592
+ }
593
+ return `${out}"`;
594
+ }
535
595
  /** `printf`'s verbs, each over one operand. */
536
596
  function printf(format, args) {
537
597
  let out = "";
@@ -556,10 +616,10 @@ function printf(format, args) {
556
616
  switch (verb) {
557
617
  case "v":
558
618
  case "s":
559
- out += formatted(arg, false);
619
+ out += formatted(arg, true);
560
620
  break;
561
621
  case "q":
562
- out += JSON.stringify(formatted(arg, false));
622
+ out += goQuoted(formatted(arg, true));
563
623
  break;
564
624
  case "d":
565
625
  if (typeof arg !== "number" || !Number.isInteger(arg)) {
@@ -659,11 +719,11 @@ function call(name, args) {
659
719
  return args
660
720
  .map((arg, i) => {
661
721
  const spaced = i > 0 && typeof arg !== "string" && typeof args[i - 1] !== "string";
662
- return `${spaced ? " " : ""}${formatted(arg, false)}`;
722
+ return `${spaced ? " " : ""}${formatted(arg, true)}`;
663
723
  })
664
724
  .join("");
665
725
  case "println":
666
- return `${args.map((arg) => formatted(arg, false)).join(" ")}\n`;
726
+ return `${args.map((arg) => formatted(arg, true)).join(" ")}\n`;
667
727
  case "printf":
668
728
  if (typeof first !== "string") {
669
729
  throw new RenderError("printf takes a format string first");
@@ -676,10 +736,22 @@ function call(name, args) {
676
736
  /** One execution of a template over a document. */
677
737
  class Execution {
678
738
  scopes;
679
- out = "";
739
+ /** The rendering so far, in pieces of about {@link CHUNK_LENGTH}, the last still growing. */
740
+ pieces = [""];
680
741
  constructor(root) {
681
742
  this.scopes = [new Map([["$", root]])];
682
743
  }
744
+ write(text) {
745
+ const last = this.pieces.length - 1;
746
+ const held = (this.pieces[last] ?? "") + text;
747
+ if (held.length >= CHUNK_LENGTH) {
748
+ this.pieces[last] = held;
749
+ this.pieces.push("");
750
+ }
751
+ else {
752
+ this.pieces[last] = held;
753
+ }
754
+ }
683
755
  lookup(name) {
684
756
  for (let i = this.scopes.length - 1; i >= 0; i -= 1) {
685
757
  const scope = this.scopes[i];
@@ -783,12 +855,12 @@ class Execution {
783
855
  for (const node of nodes) {
784
856
  switch (node.k) {
785
857
  case "text":
786
- this.out += node.text;
858
+ this.write(node.text);
787
859
  break;
788
860
  case "action": {
789
861
  const value = this.pipeline(node.pipe, dot, true);
790
862
  if (node.pipe.vars.length === 0) {
791
- this.out += formatted(value, false);
863
+ this.write(formatted(value, false));
792
864
  }
793
865
  break;
794
866
  }
@@ -866,12 +938,40 @@ class Execution {
866
938
  }
867
939
  }
868
940
  /**
869
- * The template's rendering of one document. The rendering is built whole, so a template
870
- * that fails partway renders nothing, and the failure names what it could not evaluate.
941
+ * One document as the JSON value it encodes: what `JSON.stringify` would write for it,
942
+ * read back, without writing it. A member holding nothing is left out, and an array
943
+ * element holding nothing, like a number that is not finite, is null.
944
+ */
945
+ function plainOf(value) {
946
+ if (Array.isArray(value)) {
947
+ return value.map((element) => plainOf(element) ?? null);
948
+ }
949
+ if (typeof value === "object" && value !== null) {
950
+ const held = {};
951
+ for (const [key, member] of Object.entries(value)) {
952
+ const plain = plainOf(member);
953
+ if (plain !== undefined) {
954
+ held[key] = plain;
955
+ }
956
+ }
957
+ return held;
958
+ }
959
+ if (typeof value === "number") {
960
+ return Number.isFinite(value) ? value : null;
961
+ }
962
+ if (typeof value === "string" || typeof value === "boolean" || value === null) {
963
+ return value;
964
+ }
965
+ return undefined;
966
+ }
967
+ /**
968
+ * The template's rendering of one document, in pieces of about {@link CHUNK_LENGTH}.
969
+ * The rendering is complete before it is answered, so a template that fails partway
970
+ * renders nothing, and the failure names what it could not evaluate.
871
971
  */
872
972
  export function renderTemplate(template, document) {
873
- const root = JSON.parse(JSON.stringify(document));
973
+ const root = plainOf(document) ?? null;
874
974
  const execution = new Execution(root);
875
975
  execution.run(template.nodes, root);
876
- return execution.out;
976
+ return execution.pieces.filter((piece) => piece !== "");
877
977
  }
@@ -1,3 +1,4 @@
1
+ import { coalesced } from "../json-chunks.js";
1
2
  /** The kind token and the confidence a stale suppression is rendered with, both fixed. */
2
3
  const STALE_KIND = "suppression";
3
4
  const STALE_CONFIDENCE = "certain";
@@ -24,11 +25,12 @@ function summary(totals) {
24
25
  * summary is deliberately not in that shape, so a filter for finding lines yields exactly
25
26
  * them. Nothing else is written: no timestamp, duration or host detail.
26
27
  */
27
- export const text = (report) => {
28
- const lines = report.findings.map((found) => `${found.position.path}:${String(found.position.line)}:${String(found.position.column)}: ${found.symbol.kind} ${found.symbol.name}: ${found.message} [${found.confidence}] (${found.code})`);
28
+ export const text = (report) => coalesced((function* lines() {
29
+ for (const found of report.findings) {
30
+ 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`;
31
+ }
29
32
  for (const stale of report.stale_suppressions) {
30
- lines.push(`${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})`);
33
+ yield `${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})\n`;
31
34
  }
32
- lines.push(summary(report.totals));
33
- return `${lines.join("\n")}\n`;
34
- };
35
+ yield `${summary(report.totals)}\n`;
36
+ })());
package/dist/src/roots.js CHANGED
@@ -12,6 +12,7 @@ import { entryPoints } from "./entry-points.js";
12
12
  import { globExpression } from "./glob.js";
13
13
  import { nodeKey } from "./inventory.js";
14
14
  import { byPosition } from "./position.js";
15
+ import { UNANSWERED } from "./query.js";
15
16
  import { sourceFilesOf } from "./source-files.js";
16
17
  /** Whether a configured string carries either special character. */
17
18
  function isPattern(text) {
@@ -142,7 +143,7 @@ export function roots(project, held, targetRoot, options) {
142
143
  for (const point of entryPoints(project, files, options.testFiles)) {
143
144
  enter(point.file, point.rule, point.source, point.exports);
144
145
  }
145
- const exportsOf = exportTables(project, held, [
146
+ const { tables: exportsOf, answered } = exportTables(project, held, [
146
147
  ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
147
148
  ...published,
148
149
  ]);
@@ -171,41 +172,56 @@ export function roots(project, held, targetRoot, options) {
171
172
  const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
172
173
  return site || compare(a.kind, b.kind) || compare(a.source, b.source);
173
174
  });
174
- return { configFile: project.configFile, liveUnderReachability: ordered, unmatched };
175
+ return {
176
+ configFile: project.configFile,
177
+ liveUnderReachability: ordered,
178
+ unmatched,
179
+ unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
180
+ };
175
181
  }
176
182
  /**
177
183
  * Every declaration each file's module exports, by the file's name: the declaration
178
184
  * each name of the export table is, and every link of a re-export's chain. A name
179
- * whose declaration is outside the project's own files belongs to another program
180
- * and names nothing here.
181
- *
182
- * One batch resolves every file to its module and one request per module reads
183
- * its export table, star re-exports resolved into it. A file that is no module
184
- * exports nothing.
185
+ * whose declaration is outside the project's own files names nothing here. One batch
186
+ * resolves every file to its module and one request per module reads its export
187
+ * table, star re-exports resolved into it. A file that is no module exports nothing.
188
+ * `answered` is false where the checker left a table or a link of a chain unanswered.
185
189
  */
186
190
  function exportTables(project, held, wanted) {
187
191
  const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
188
192
  const tables = new Map();
193
+ let answered = true;
189
194
  if (unique.length === 0) {
190
- return tables;
195
+ return { tables, answered };
191
196
  }
192
197
  const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
193
198
  const chains = aliasChains(project, held);
199
+ const topLevel = (file) => {
200
+ const fileId = held.declarations.get(nodeKey(file, file));
201
+ return held.symbols.filter((symbol) => symbol.parent === fileId).map((symbol) => symbol.id);
202
+ };
194
203
  unique.forEach((file, index) => {
195
204
  const module = modules[index];
196
205
  if (module === undefined) {
197
206
  tables.set(file.fileName, []);
198
207
  return;
199
208
  }
209
+ const table = module === UNANSWERED ? UNANSWERED : project.queries.exportsOfModule(module);
210
+ if (table === UNANSWERED) {
211
+ answered = false;
212
+ tables.set(file.fileName, topLevel(file));
213
+ return;
214
+ }
200
215
  const ids = new Set();
201
- for (const exported of project.checker.getExportsOfModule(module)) {
216
+ for (const exported of table) {
202
217
  for (const target of chains.chainOf(exported)) {
218
+ answered &&= !target.guessed;
203
219
  ids.add(target.id);
204
220
  }
205
221
  }
206
222
  tables.set(file.fileName, [...ids]);
207
223
  });
208
- return tables;
224
+ return { tables, answered };
209
225
  }
210
226
  /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
211
227
  const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
@@ -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
  import { isArrayLiteralExpression, isArrowFunction, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
11
12
  import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/unstable/sync";
@@ -15,11 +16,14 @@ import { classMembers, memberComponent, typesAt } from "./class-members.js";
15
16
  import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
16
17
  import { nodeKey } from "./inventory.js";
17
18
  import { renderPosition } from "./position.js";
19
+ import { must } from "./query.js";
18
20
  import { valueReach } from "./value-reach.js";
19
21
  /** The serializer every run reads, whatever the configuration names. */
20
22
  const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
21
23
  /** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
22
24
  const CONVERSIONS = new Set(["toJSON", "toString"]);
25
+ /** The method a serializer calls on a value that carries one, wherever the value goes. */
26
+ const SERIALIZED_CONVERSION = "toJSON";
23
27
  /** The parameter types that keep nothing of the value they are given. */
24
28
  const ERASED = TypeFlags.Any | TypeFlags.Unknown;
25
29
  /** The wider of two reaches, where either may be absent. */
@@ -182,12 +186,13 @@ function erasedParameters(project, parameters) {
182
186
  if (!parameter.rest) {
183
187
  return (type.flags & ERASED) !== 0;
184
188
  }
185
- const [element] = type.isTypeReference() ? project.checker.getTypeArguments(type) : [];
189
+ const [element] = must(project.queries.typeArguments(type));
186
190
  return element !== undefined && (element.flags & ERASED) !== 0;
187
191
  });
188
192
  const bySymbol = new Map();
189
193
  project
190
194
  .symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
195
+ .map(must)
191
196
  .forEach((symbol, index) => {
192
197
  const parameter = erased[index];
193
198
  if (symbol !== undefined && parameter !== undefined) {
@@ -203,11 +208,11 @@ function handedOn(project, sites, erased, invocationOf) {
203
208
  .filter((value) => spellings.has(value.name.getText()))
204
209
  .map((value) => ({ site, argument: index, value }))));
205
210
  const plain = candidates.filter((one) => !one.value.shorthand);
206
- const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
211
+ const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name))).map(must);
207
212
  const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
208
213
  return candidates.flatMap((one) => {
209
214
  const symbol = one.value.shorthand
210
- ? project.shorthandValueAt(project.handle(one.value.name))
215
+ ? must(project.shorthandValueAt(project.handle(one.value.name)))
211
216
  : symbolOf.get(one);
212
217
  const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
213
218
  return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
@@ -223,8 +228,8 @@ function handedOn(project, sites, erased, invocationOf) {
223
228
  */
224
229
  function destinations(input, chains, walked) {
225
230
  const { project, held } = input;
226
- const checker = project.checker;
227
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
231
+ const queries = project.queries;
232
+ const own = project.ownPaths();
228
233
  const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(namesADeclaration);
229
234
  const forms = new Map();
230
235
  for (const site of walked.sites) {
@@ -256,7 +261,7 @@ function destinations(input, chains, walked) {
256
261
  if (form !== undefined &&
257
262
  method !== undefined &&
258
263
  outside(method) &&
259
- FUNCTION_INTERFACES.has(method.getParent()?.name ?? "")) {
264
+ FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")) {
260
265
  return form.invocation;
261
266
  }
262
267
  return { callee: call.expression, arguments: call.arguments, indirect: false };
@@ -272,14 +277,14 @@ function destinations(input, chains, walked) {
272
277
  const invoked = invocation(call);
273
278
  // Through a method of the prototype the resolved signature is the method's, so
274
279
  // the parameters are read off the callee's own signatures, any one erasing.
275
- const type = invoked.indirect ? checker.getTypeAtLocation(invoked.callee) : undefined;
280
+ const type = invoked.indirect ? must(queries.typeAt(invoked.callee)) : undefined;
276
281
  const signatures = invoked.indirect
277
282
  ? type === undefined
278
283
  ? []
279
- : checker.getSignaturesOfType(type, SignatureKind.Call)
280
- : [checker.getResolvedSignature(call)].filter((one) => one !== undefined);
284
+ : must(queries.signaturesOf(type, SignatureKind.Call))
285
+ : [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
281
286
  indexes = new Set(invoked.arguments.flatMap((_unused, index) => signatures.some((signature) => {
282
- const parameter = checker.getParameterType(signature, index);
287
+ const parameter = must(queries.parameterType(signature, index));
283
288
  return parameter !== undefined && (parameter.flags & ERASED) !== 0;
284
289
  })
285
290
  ? [index]
@@ -360,46 +365,62 @@ function flowsOf(project, held, sites, to) {
360
365
  const typeOf = new Map(asked.map((one, index) => [one.value, types[index]]));
361
366
  return arguments_.flatMap((one) => {
362
367
  const type = typeOf.get(one.value);
363
- let classes = new Set();
368
+ let reached = { classes: new Set(), members: [] };
364
369
  if (one.value.kind === SyntaxKind.ThisKeyword) {
365
- classes = new Set(one.site.self === undefined ? [] : [one.site.self]);
370
+ reached = {
371
+ classes: new Set(one.site.self === undefined ? [] : [one.site.self]),
372
+ members: [],
373
+ };
366
374
  }
367
375
  else if (type !== undefined) {
368
- classes = reach.classesOf(type);
376
+ reached = reach.reachedFrom(type);
369
377
  }
370
- const destination = classes.size === 0 ? undefined : to.at(one.site.call, one.argument);
371
- return destination === undefined ? [] : [{ site: one.value, classes, to: destination }];
378
+ const carries = reached.classes.size > 0 || reached.members.length > 0;
379
+ const destination = carries ? to.at(one.site.call, one.argument) : undefined;
380
+ return destination === undefined
381
+ ? []
382
+ : [{ site: one.value, classes: reached.classes, members: reached.members, to: destination }];
372
383
  });
373
384
  }
374
385
  /**
375
- * The serialization-contract detector: each property of each class whose values reach
376
- * a destination, and the two conversion methods where the destination is outside the
377
- * analysis, recorded at the argument with the callee named.
386
+ * The serialization-contract detector: each data member and `toJSON` of each class,
387
+ * interface and object type a value that reaches a destination carries, and `toString`
388
+ * as well where the destination is outside the analysis, recorded at the argument with
389
+ * the callee named.
378
390
  */
379
391
  export function serializationContract(input) {
380
392
  const { project, held, targetRoot } = input;
381
393
  const walked = walkProject(project, held);
382
- const flows = flowsOf(project, held, walked.sites, destinations(input, aliasChains(project, held), walked));
394
+ const chains = aliasChains(project, held);
395
+ const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
383
396
  if (flows.length === 0) {
384
397
  return [];
385
398
  }
386
399
  const members = classMembers(project, held, new Set(flows.flatMap((flow) => [...flow.classes])), "instance");
387
400
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
401
+ // A destination holds back a data member and `toJSON`, and `toString` as well where
402
+ // the value left the analysis.
403
+ const holdsBack = (flags, name, to) => (flags & SymbolFlags.Property) !== 0 ||
404
+ ((flags & SymbolFlags.Method) !== 0 &&
405
+ (name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
388
406
  const found = [];
389
407
  for (const flow of flows) {
390
408
  const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
391
409
  const detail = `passed to ${flow.to.callee}`;
392
410
  for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
393
411
  const symbol = byId.get(member.id);
394
- const isProperty = (member.flags & SymbolFlags.Property) !== 0;
395
- const converts = flow.to.reach === "conversions" &&
396
- (member.flags & SymbolFlags.Method) !== 0 &&
397
- symbol !== undefined &&
398
- CONVERSIONS.has(memberComponent(symbol, byId) ?? "");
399
- if (isProperty || converts) {
412
+ const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
413
+ if (holdsBack(member.flags, name, flow.to)) {
400
414
  found.push({ id: member.id, detail, site });
401
415
  }
402
416
  }
417
+ for (const member of flow.members) {
418
+ if (holdsBack(member.flags, member.name, flow.to)) {
419
+ for (const id of chains.declarationsOf(member)) {
420
+ found.push({ id, detail, site });
421
+ }
422
+ }
423
+ }
403
424
  }
404
425
  return found;
405
426
  }