@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
@@ -2,15 +2,10 @@ import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecor
2
2
  import { aliasChains } from "./alias-chain.js";
3
3
  import { destructuring, propertyReadsOf } from "./destructuring.js";
4
4
  import { globExpression } from "./glob.js";
5
- import { nodeKey } from "./inventory.js";
5
+ import { declarationsByName, nodeKey } from "./inventory.js";
6
6
  import { relativePath } from "./paths.js";
7
7
  import { byPosition, renderPosition } from "./position.js";
8
- /**
9
- * The batch cap a run takes when the configuration names none. It bounds the size of
10
- * one answer rather than the work: the median file is one batch at this cap and the
11
- * largest production file a handful.
12
- */
13
- export const DEFAULT_BATCH_CAP = 4096;
8
+ import { DEFAULT_BATCH_CAP, UNANSWERED } from "./query.js";
14
9
  /**
15
10
  * The operators whose left side one assignment stores into. A compound assignment is
16
11
  * among them: it reads its target only to write the result back, so nothing else
@@ -294,6 +289,49 @@ export function testFileRulesOf(testFiles, paths) {
294
289
  })
295
290
  .sort((a, b) => compare(a.rule, b.rule));
296
291
  }
292
+ const importedCache = new WeakMap();
293
+ /**
294
+ * Each local name one file's imports bind, mapped to the names it imports: the
295
+ * exported name of a named import, `default` for a default import, and `undefined` for
296
+ * a namespace import or an import assignment, which binds a whole module.
297
+ */
298
+ function importedNames(file) {
299
+ const known = importedCache.get(file);
300
+ if (known !== undefined) {
301
+ return known;
302
+ }
303
+ const names = new Map();
304
+ const bind = (local, imported) => {
305
+ names.set(local, [...(names.get(local) ?? []), imported]);
306
+ };
307
+ for (const statement of file.statements) {
308
+ if (isImportEqualsDeclaration(statement)) {
309
+ bind(statement.name.text, undefined);
310
+ continue;
311
+ }
312
+ const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
313
+ if (clause === undefined) {
314
+ continue;
315
+ }
316
+ if (clause.name !== undefined) {
317
+ bind(clause.name.text, "default");
318
+ }
319
+ const bindings = clause.namedBindings;
320
+ if (bindings === undefined) {
321
+ continue;
322
+ }
323
+ if (bindings.kind === SyntaxKind.NamespaceImport) {
324
+ bind(bindings.name.text, undefined);
325
+ }
326
+ else {
327
+ for (const element of bindings.elements) {
328
+ bind(element.name.text, element.propertyName?.text ?? element.name.text);
329
+ }
330
+ }
331
+ }
332
+ importedCache.set(file, names);
333
+ return names;
334
+ }
297
335
  /** Two strings ordered bytewise, which is the order every set of a run is read in. */
298
336
  function compare(a, b) {
299
337
  if (a === b) {
@@ -347,7 +385,26 @@ export function references(project, held, targetRoot, options) {
347
385
  * Each later link is a declaration of its own that references the next, so one link
348
386
  * per re-export is what lets a chain be followed one declaration at a time.
349
387
  */
350
- const carriedBy = (link, symbol) => link.alias ? chains.nextDeclared(symbol) : chains.declarationsOf(symbol);
388
+ const carriedBy = (link, symbol) => link.alias
389
+ ? chains.nextDeclared(symbol)
390
+ : chains.declarationsOf(symbol).map((id) => ({ id, stepped: false, guessed: false }));
391
+ /**
392
+ * Every declaration bearing the name one node spells, or the name an import of its
393
+ * file binds that name to, which is what a reference the checker could not resolve is
394
+ * counted as: it may be a use of any of them, and a declaration it may use is kept
395
+ * live. A name a namespace import binds may be a use of any file.
396
+ */
397
+ const named = (node) => {
398
+ const byName = declarationsByName(held);
399
+ const text = node.getText();
400
+ const imported = importedNames(node.getSourceFile()).get(text) ?? [];
401
+ return [
402
+ ...new Set([
403
+ ...(byName.get(text) ?? []),
404
+ ...imported.flatMap((name) => name === undefined ? [...fileIds] : (byName.get(name) ?? [])),
405
+ ]),
406
+ ];
407
+ };
351
408
  /** The symbol one site resolves to, asking only the accessor the site's form names. */
352
409
  const symbolFor = (site, answered) => {
353
410
  if (site.shorthand !== undefined) {
@@ -355,7 +412,7 @@ export function references(project, held, targetRoot, options) {
355
412
  return project.shorthandValueAt(project.handle(site.shorthand));
356
413
  }
357
414
  const batched = answered.get(site.node);
358
- if (batched !== undefined) {
415
+ if (batched === UNANSWERED || typeof batched === "object") {
359
416
  return batched;
360
417
  }
361
418
  residueFallbacks += 1;
@@ -397,7 +454,7 @@ export function references(project, held, targetRoot, options) {
397
454
  batched += batching.length;
398
455
  for (let from = 0; from < batching.length; from += cap) {
399
456
  const run = batching.slice(from, from + cap);
400
- const answers = project.symbolsAt(run.map((node) => project.handle(node)));
457
+ const answers = project.symbolsAt(run.map((node) => project.handle(node)), cap);
401
458
  fileBatches += 1;
402
459
  run.forEach((node, index) => {
403
460
  const symbol = answers[index];
@@ -412,13 +469,16 @@ export function references(project, held, targetRoot, options) {
412
469
  continue;
413
470
  }
414
471
  const position = renderPosition(file, root, link.at.getStart());
415
- for (const id of carriedBy(link, symbol)) {
472
+ const carried = symbol === UNANSWERED
473
+ ? named(link.at).map((id) => ({ id, stepped: false, guessed: true }))
474
+ : carriedBy(link, symbol);
475
+ for (const target of carried) {
416
476
  found.push({
417
477
  from: link.from,
418
- to: id,
478
+ to: target.id,
419
479
  position,
420
480
  use: "read",
421
- resolution: link.alias ? "alias" : "batch",
481
+ resolution: target.guessed ? "by-name" : link.alias ? "alias" : "batch",
422
482
  test,
423
483
  });
424
484
  }
@@ -429,14 +489,16 @@ export function references(project, held, targetRoot, options) {
429
489
  continue;
430
490
  }
431
491
  const position = renderPosition(file, root, evaluation.node.getStart());
432
- for (const id of chains.declarationsOf(module)) {
492
+ // A module the checker could not resolve may be any file of the target.
493
+ const evaluated = module === UNANSWERED ? [...fileIds] : chains.declarationsOf(module);
494
+ for (const id of evaluated) {
433
495
  if (fileIds.has(id)) {
434
496
  found.push({
435
497
  from: evaluation.from,
436
498
  to: id,
437
499
  position,
438
500
  use: "evaluation",
439
- resolution: "batch",
501
+ resolution: module === UNANSWERED ? "by-name" : "batch",
440
502
  test,
441
503
  });
442
504
  }
@@ -450,13 +512,20 @@ export function references(project, held, targetRoot, options) {
450
512
  return;
451
513
  }
452
514
  const position = renderPosition(file, root, read.key.getStart());
453
- for (const target of chains.chainOf(symbol)) {
515
+ const targets = symbol === UNANSWERED
516
+ ? (declarationsByName(held).get(read.name) ?? []).map((id) => ({
517
+ id,
518
+ stepped: false,
519
+ guessed: true,
520
+ }))
521
+ : chains.chainOf(symbol);
522
+ for (const target of targets) {
454
523
  found.push({
455
524
  from: read.from,
456
525
  to: target.id,
457
526
  position,
458
527
  use: "read",
459
- resolution: "destructured",
528
+ resolution: target.guessed ? "by-name" : "destructured",
460
529
  test,
461
530
  });
462
531
  }
@@ -464,7 +533,7 @@ export function references(project, held, targetRoot, options) {
464
533
  for (const site of uses) {
465
534
  const direct = site.shorthand !== undefined
466
535
  ? "shorthand"
467
- : answered.has(site.node)
536
+ : typeof answered.get(site.node) === "object"
468
537
  ? "batch"
469
538
  : "resolved-symbol";
470
539
  const symbol = symbolFor(site, answered);
@@ -474,13 +543,16 @@ export function references(project, held, targetRoot, options) {
474
543
  const position = renderPosition(file, root, site.node.getStart());
475
544
  // A re-export something imports is used and so is the declaration behind it, so
476
545
  // the use names every link of the chain its name stands at the head of.
477
- for (const target of chains.chainOf(symbol)) {
546
+ const targets = symbol === UNANSWERED
547
+ ? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
548
+ : chains.chainOf(symbol);
549
+ for (const target of targets) {
478
550
  found.push({
479
551
  from: site.from,
480
552
  to: target.id,
481
553
  position,
482
554
  use: site.use,
483
- resolution: target.stepped ? "alias" : direct,
555
+ resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
484
556
  test,
485
557
  });
486
558
  }
@@ -14,9 +14,9 @@
14
14
  */
15
15
  import { isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
16
16
  import { TypeFlags } from "@typescript/native/unstable/sync";
17
- import { typesAt } from "./class-members.js";
18
17
  import { memberNames } from "./member-names.js";
19
18
  import { renderPosition } from "./position.js";
19
+ import { isAnswered, UNANSWERED } from "./query.js";
20
20
  /** The `Reflect` methods whose second argument is a property key. */
21
21
  const REFLECT_KEYED = new Set(["get", "set", "has"]);
22
22
  /** The text one key denotes, where it is literal text. */
@@ -67,14 +67,17 @@ function lookupsOf(file) {
67
67
  file.forEachChild(visit);
68
68
  return found;
69
69
  }
70
- /** Whether a receiver's type is not `any` and declares a string index signature. */
70
+ /**
71
+ * Whether a receiver's type is not `any` and declares a string index signature. A type
72
+ * whose index signatures the checker did not answer for is read as declaring none, so
73
+ * the lookup retains what it names.
74
+ */
71
75
  function readsAMapping(project, type) {
72
76
  if ((type.flags & TypeFlags.Any) !== 0) {
73
77
  return false;
74
78
  }
75
- return project.checker
76
- .getIndexInfosOfType(type)
77
- .some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
79
+ const infos = project.queries.indexInfosOf(type);
80
+ return isAnswered(infos) && infos.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
78
81
  }
79
82
  /**
80
83
  * The reflective-lookup detector: every member whose name a literal key of a lookup
@@ -89,13 +92,16 @@ export function reflectiveLookup(input) {
89
92
  }
90
93
  // Whether each written `Reflect` is the global one, asked in one batch: a
91
94
  // declaration of that name in one of the project's own files shadows it.
92
- const ownFiles = new Set(files.map((file) => file.fileName));
95
+ const ownFiles = project.ownPaths();
93
96
  const reflects = lookups.flatMap((lookup) => lookup.reflect === undefined ? [] : [lookup.reflect]);
94
97
  const global = new Set();
95
98
  if (reflects.length > 0) {
96
99
  project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
97
100
  const node = reflects[index];
98
- const declaredHere = symbol?.declarations.some((declaration) => ownFiles.has(declaration.path));
101
+ // A name the checker did not answer for is read as the global, which retains.
102
+ const declaredHere = symbol === UNANSWERED
103
+ ? false
104
+ : symbol?.declarations.some((declaration) => ownFiles.has(declaration.path));
99
105
  if (node !== undefined && declaredHere === false) {
100
106
  global.add(node);
101
107
  }
@@ -105,9 +111,12 @@ export function reflectiveLookup(input) {
105
111
  // reads that mapping rather than a member, asked in one batch.
106
112
  const receivers = lookups.flatMap((lookup) => lookup.receiver === undefined ? [] : [lookup.receiver]);
107
113
  const mapped = new Set();
108
- typesAt(project, receivers).forEach((type, index) => {
114
+ project.queries.typesAt(receivers).forEach((type, index) => {
109
115
  const receiver = receivers[index];
110
- if (receiver !== undefined && type !== undefined && readsAMapping(project, type)) {
116
+ if (receiver !== undefined &&
117
+ type !== undefined &&
118
+ isAnswered(type) &&
119
+ readsAMapping(project, type)) {
111
120
  mapped.add(receiver);
112
121
  }
113
122
  });
@@ -218,7 +218,9 @@ export function buildReport(input) {
218
218
  configurations: [...input.configurations]
219
219
  .sort((a, b) => compare(a.id, b.id))
220
220
  .map((one) => ({ id: one.id, project: one.project })),
221
- configurations_not_built: [],
221
+ configurations_not_built: [...input.notBuilt]
222
+ .sort((a, b) => compare(a.id, b.id))
223
+ .map((one) => ({ id: one.id, project: one.project, error: one.error })),
222
224
  consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
223
225
  findings,
224
226
  edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
@@ -1,4 +1,5 @@
1
1
  import { fails } from "../config.js";
2
+ import { coalesced } from "../json-chunks.js";
2
3
  import { KINDS } from "../kinds.js";
3
4
  /** The escapes a workflow command's message needs, so a line break does not end the command. */
4
5
  const DATA_ESCAPES = { "%": "%25", "\r": "%0D", "\n": "%0A" };
@@ -28,9 +29,12 @@ function command(level, at, endLine, title, message) {
28
29
  * because its kind is fixed on at the failing severity.
29
30
  */
30
31
  export function annotations(report, options) {
31
- const lines = report.findings.map((found) => command(fails(found.severity, options.failOn) ? "error" : "warning", found.position, found.position.end_line, `${found.code} ${found.kind}`, found.message));
32
- for (const stale of report.stale_suppressions) {
33
- lines.push(command("error", stale.position, stale.position.line, `${stale.code} ${kindName(stale.code)}`, stale.message));
34
- }
35
- return lines.map((line) => `${line}\n`).join("");
32
+ return coalesced((function* lines() {
33
+ for (const found of report.findings) {
34
+ yield `${command(fails(found.severity, options.failOn) ? "error" : "warning", found.position, found.position.end_line, `${found.code} ${found.kind}`, found.message)}\n`;
35
+ }
36
+ for (const stale of report.stale_suppressions) {
37
+ yield `${command("error", stale.position, stale.position.line, `${stale.code} ${kindName(stale.code)}`, stale.message)}\n`;
38
+ }
39
+ })());
36
40
  }
@@ -1,4 +1,5 @@
1
+ import { jsonChunks } from "../json-chunks.js";
1
2
  /** The indentation of the report document, which with the schema's member order fixes its bytes. */
2
3
  const INDENT = 2;
3
- /** The report document itself: the report, indented, with a closing newline. */
4
- export const json = (report) => `${JSON.stringify(report, null, INDENT)}\n`;
4
+ /** The report document itself, in pieces: the report, indented, with a closing newline. */
5
+ export const json = (report) => jsonChunks(report, INDENT);
@@ -1,3 +1,4 @@
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";
@@ -236,5 +237,5 @@ export function sarif(report, options) {
236
237
  },
237
238
  ],
238
239
  };
239
- return `${JSON.stringify(log, null, INDENT)}\n`;
240
+ return jsonChunks(log, INDENT);
240
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. */
@@ -735,10 +736,22 @@ function call(name, args) {
735
736
  /** One execution of a template over a document. */
736
737
  class Execution {
737
738
  scopes;
738
- out = "";
739
+ /** The rendering so far, in pieces of about {@link CHUNK_LENGTH}, the last still growing. */
740
+ pieces = [""];
739
741
  constructor(root) {
740
742
  this.scopes = [new Map([["$", root]])];
741
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
+ }
742
755
  lookup(name) {
743
756
  for (let i = this.scopes.length - 1; i >= 0; i -= 1) {
744
757
  const scope = this.scopes[i];
@@ -842,12 +855,12 @@ class Execution {
842
855
  for (const node of nodes) {
843
856
  switch (node.k) {
844
857
  case "text":
845
- this.out += node.text;
858
+ this.write(node.text);
846
859
  break;
847
860
  case "action": {
848
861
  const value = this.pipeline(node.pipe, dot, true);
849
862
  if (node.pipe.vars.length === 0) {
850
- this.out += formatted(value, false);
863
+ this.write(formatted(value, false));
851
864
  }
852
865
  break;
853
866
  }
@@ -925,12 +938,40 @@ class Execution {
925
938
  }
926
939
  }
927
940
  /**
928
- * The template's rendering of one document. The rendering is built whole, so a template
929
- * 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.
930
971
  */
931
972
  export function renderTemplate(template, document) {
932
- const root = JSON.parse(JSON.stringify(document));
973
+ const root = plainOf(document) ?? null;
933
974
  const execution = new Execution(root);
934
975
  execution.run(template.nodes, root);
935
- return execution.out;
976
+ return execution.pieces.filter((piece) => piece !== "");
936
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"]);
@@ -16,6 +16,7 @@ import { classMembers, memberComponent, typesAt } from "./class-members.js";
16
16
  import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
17
17
  import { nodeKey } from "./inventory.js";
18
18
  import { renderPosition } from "./position.js";
19
+ import { must } from "./query.js";
19
20
  import { valueReach } from "./value-reach.js";
20
21
  /** The serializer every run reads, whatever the configuration names. */
21
22
  const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
@@ -185,12 +186,13 @@ function erasedParameters(project, parameters) {
185
186
  if (!parameter.rest) {
186
187
  return (type.flags & ERASED) !== 0;
187
188
  }
188
- const [element] = type.isTypeReference() ? project.checker.getTypeArguments(type) : [];
189
+ const [element] = must(project.queries.typeArguments(type));
189
190
  return element !== undefined && (element.flags & ERASED) !== 0;
190
191
  });
191
192
  const bySymbol = new Map();
192
193
  project
193
194
  .symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
195
+ .map(must)
194
196
  .forEach((symbol, index) => {
195
197
  const parameter = erased[index];
196
198
  if (symbol !== undefined && parameter !== undefined) {
@@ -206,11 +208,11 @@ function handedOn(project, sites, erased, invocationOf) {
206
208
  .filter((value) => spellings.has(value.name.getText()))
207
209
  .map((value) => ({ site, argument: index, value }))));
208
210
  const plain = candidates.filter((one) => !one.value.shorthand);
209
- 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);
210
212
  const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
211
213
  return candidates.flatMap((one) => {
212
214
  const symbol = one.value.shorthand
213
- ? project.shorthandValueAt(project.handle(one.value.name))
215
+ ? must(project.shorthandValueAt(project.handle(one.value.name)))
214
216
  : symbolOf.get(one);
215
217
  const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
216
218
  return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
@@ -226,8 +228,8 @@ function handedOn(project, sites, erased, invocationOf) {
226
228
  */
227
229
  function destinations(input, chains, walked) {
228
230
  const { project, held } = input;
229
- const checker = project.checker;
230
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
231
+ const queries = project.queries;
232
+ const own = project.ownPaths();
231
233
  const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(namesADeclaration);
232
234
  const forms = new Map();
233
235
  for (const site of walked.sites) {
@@ -259,7 +261,7 @@ function destinations(input, chains, walked) {
259
261
  if (form !== undefined &&
260
262
  method !== undefined &&
261
263
  outside(method) &&
262
- FUNCTION_INTERFACES.has(method.getParent()?.name ?? "")) {
264
+ FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")) {
263
265
  return form.invocation;
264
266
  }
265
267
  return { callee: call.expression, arguments: call.arguments, indirect: false };
@@ -275,14 +277,14 @@ function destinations(input, chains, walked) {
275
277
  const invoked = invocation(call);
276
278
  // Through a method of the prototype the resolved signature is the method's, so
277
279
  // the parameters are read off the callee's own signatures, any one erasing.
278
- const type = invoked.indirect ? checker.getTypeAtLocation(invoked.callee) : undefined;
280
+ const type = invoked.indirect ? must(queries.typeAt(invoked.callee)) : undefined;
279
281
  const signatures = invoked.indirect
280
282
  ? type === undefined
281
283
  ? []
282
- : checker.getSignaturesOfType(type, SignatureKind.Call)
283
- : [checker.getResolvedSignature(call)].filter((one) => one !== undefined);
284
+ : must(queries.signaturesOf(type, SignatureKind.Call))
285
+ : [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
284
286
  indexes = new Set(invoked.arguments.flatMap((_unused, index) => signatures.some((signature) => {
285
- const parameter = checker.getParameterType(signature, index);
287
+ const parameter = must(queries.parameterType(signature, index));
286
288
  return parameter !== undefined && (parameter.flags & ERASED) !== 0;
287
289
  })
288
290
  ? [index]