@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
@@ -4,10 +4,10 @@
4
4
  * call sites that use or discard it, each statement control flow cannot reach, each
5
5
  * write to a local that no read reaches, and each case an earlier case covers.
6
6
  */
7
- import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
7
+ import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
8
8
  import { nodeKey } from "./inventory.js";
9
9
  import { positionKey, renderPosition } from "./position.js";
10
- import { DEFAULT_BATCH_CAP } from "./references.js";
10
+ import { isAnswered, UNANSWERED } from "./query.js";
11
11
  import { signatureOf } from "./signatures.js";
12
12
  /** The codes of the parts this module reads. */
13
13
  export const UNUSED_PARAMETER = "DS1801";
@@ -195,8 +195,15 @@ function extendedNames(files) {
195
195
  const visit = (node) => {
196
196
  if (isHeritageClause(node) && node.token === SyntaxKind.ExtendsKeyword) {
197
197
  for (const type of node.types) {
198
- const expression = unwrapped(type.expression);
199
- const named = isPropertyAccessExpression(expression) ? expression.name : expression;
198
+ // A class's clause holds expressions; an interface's holds type references.
199
+ let named;
200
+ if (isTypeReferenceNode(type)) {
201
+ named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
202
+ }
203
+ else {
204
+ const expression = unwrapped(type.expression);
205
+ named = isPropertyAccessExpression(expression) ? expression.name : expression;
206
+ }
200
207
  if (isIdentifier(named)) {
201
208
  names.add(named.text);
202
209
  }
@@ -230,31 +237,33 @@ function hierarchyFixes(declared, extended) {
230
237
  }
231
238
  return owner.name !== undefined && extended.has(owner.name.text);
232
239
  }
233
- /** The symbol identifier each of a batch of identifiers resolves to, where the batch answers. */
240
+ /**
241
+ * The symbol identifier each of a batch of identifiers resolves to, where the batch
242
+ * resolves it, and {@link UNANSWERED} where the checker did not answer.
243
+ */
234
244
  function resolvedIds(project, nodes) {
235
245
  const ids = new Map();
236
- for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
237
- const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
238
- project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
239
- const node = run[index];
240
- if (symbol !== undefined && node !== undefined) {
241
- ids.set(node, symbol.id);
242
- }
243
- });
244
- }
246
+ project.symbolsAt(nodes.map((node) => project.handle(node))).forEach((symbol, index) => {
247
+ const node = nodes[index];
248
+ if (node !== undefined && symbol !== undefined) {
249
+ ids.set(node, symbol === UNANSWERED ? UNANSWERED : symbol.id);
250
+ }
251
+ });
245
252
  return ids;
246
253
  }
247
254
  /**
248
255
  * Whether one identifier reads the binding a name node declares: it resolves to the
249
- * same symbol, or the checker resolves it to nothing, which is read as a use rather than
250
- * risk a finding; a shorthand property reads the binding of its own name.
256
+ * same symbol, or the checker resolves it to nothing or does not answer for either
257
+ * name, which is read as a use rather than risk a finding; a shorthand property reads
258
+ * the binding of its own name.
251
259
  */
252
260
  function readsBinding(use, declared, ids) {
253
261
  if (isShorthandPropertyAssignment(use.parent)) {
254
262
  return true;
255
263
  }
256
264
  const at = ids.get(use);
257
- return at === undefined || at === ids.get(declared);
265
+ const own = ids.get(declared);
266
+ return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
258
267
  }
259
268
  /**
260
269
  * The parts of one project's own declarations. `references` are the project's own
@@ -575,9 +584,17 @@ function resultParts(project, root, sites, references, calls) {
575
584
  if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
576
585
  continue;
577
586
  }
578
- const typed = project.checker.getSignatureFromDeclaration(signature);
579
- const result = typed === undefined ? undefined : project.checker.getReturnTypeOfSignature(typed);
580
- if (result === undefined || NO_VALUE.has(project.checker.typeToString(result))) {
587
+ // A question the checker leaves unanswered withholds the finding.
588
+ const typed = project.queries.signatureOf(signature);
589
+ if (typed === undefined || !isAnswered(typed)) {
590
+ continue;
591
+ }
592
+ const result = project.queries.returnTypeOf(typed);
593
+ if (result === undefined || !isAnswered(result)) {
594
+ continue;
595
+ }
596
+ const spelled = project.queries.typeToString(result);
597
+ if (!isAnswered(spelled) || NO_VALUE.has(spelled)) {
581
598
  continue;
582
599
  }
583
600
  const at = signature.type ?? node.name ?? node;
@@ -2,6 +2,7 @@ import { isClassDeclaration, isComputedPropertyName, isEnumDeclaration, isEnumMe
2
2
  import { dirnamePath, joinPath, relativePath } from "./paths.js";
3
3
  import { byPosition, positionKey, renderPosition } from "./position.js";
4
4
  import { computedComponent, nameComponent, renderRef } from "./ref.js";
5
+ import { isAnswered, UNANSWERED } from "./query.js";
5
6
  const MANIFEST = "package.json";
6
7
  /**
7
8
  * Resolves each file's package scope: the name of the nearest manifest at or above
@@ -493,12 +494,28 @@ export function inventory(project, host, targetRoot) {
493
494
  const handles = batched.map((node) => project.handle(node));
494
495
  const resolved = project.symbolsAt(handles);
495
496
  const symbolOf = new Map();
497
+ // A scope or a container whose symbol or table went unanswered keeps what its syntax
498
+ // declares, and everything under it is recorded unanswered.
499
+ const unansweredOwners = new Set();
496
500
  batched.forEach((node, index) => {
497
501
  const symbol = resolved[index];
498
- if (symbol !== undefined) {
502
+ if (typeof symbol === "object") {
499
503
  symbolOf.set(node, symbol);
500
504
  }
501
505
  });
506
+ resolved.slice(0, exportScopes.length).forEach((symbol, index) => {
507
+ const scope = exportScopes[index];
508
+ if (symbol === UNANSWERED && scope !== undefined) {
509
+ unansweredOwners.add(scope.owner);
510
+ }
511
+ });
512
+ containers
513
+ .filter((container) => container.nameNode !== undefined)
514
+ .forEach((container, index) => {
515
+ if (resolved[exportScopes.length + index] === UNANSWERED) {
516
+ unansweredOwners.add(container.owner);
517
+ }
518
+ });
502
519
  let exportTables = 0;
503
520
  for (const scope of exportScopes) {
504
521
  const symbol = symbolOf.get(scope.nameNode);
@@ -506,7 +523,11 @@ export function inventory(project, host, targetRoot) {
506
523
  continue;
507
524
  }
508
525
  exportTables += 1;
509
- for (const [, exported] of symbol.getExports()) {
526
+ const table = project.queries.exportsOf(symbol);
527
+ if (!isAnswered(table)) {
528
+ unansweredOwners.add(scope.owner);
529
+ }
530
+ for (const [, exported] of isAnswered(table) ? table : []) {
510
531
  for (const declaration of exported.declarations) {
511
532
  const held = project.declarationAt(declaration);
512
533
  if (held === undefined) {
@@ -527,11 +548,23 @@ export function inventory(project, host, targetRoot) {
527
548
  if (symbol !== undefined) {
528
549
  if (!container.membersAreExports) {
529
550
  memberTables += 1;
530
- outsideOwnFiles += readTable(container, symbol.getMembers(), seen, false);
551
+ const table = project.queries.membersOf(symbol);
552
+ if (isAnswered(table)) {
553
+ outsideOwnFiles += readTable(container, table, seen, false);
554
+ }
555
+ else {
556
+ unansweredOwners.add(container.owner);
557
+ }
531
558
  }
532
559
  if (container.membersAreExports || container.isClass) {
533
560
  memberTables += 1;
534
- outsideOwnFiles += readTable(container, symbol.getExports(), seen, container.isClass);
561
+ const table = project.queries.exportsOf(symbol);
562
+ if (isAnswered(table)) {
563
+ outsideOwnFiles += readTable(container, table, seen, container.isClass);
564
+ }
565
+ else {
566
+ unansweredOwners.add(container.owner);
567
+ }
535
568
  }
536
569
  }
537
570
  completeFromTree(container, seen);
@@ -635,6 +668,7 @@ export function inventory(project, host, targetRoot) {
635
668
  }
636
669
  return undefined;
637
670
  }
671
+ const byId = new Map(building.map((record) => [positionKey(record.position), record]));
638
672
  const symbols = building
639
673
  .map(render)
640
674
  .sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
@@ -645,8 +679,47 @@ export function inventory(project, host, targetRoot) {
645
679
  cost: { batches: 1, exportTables, memberTables },
646
680
  starReExports,
647
681
  outsideOwnFiles,
682
+ unanswered: symbols
683
+ .filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
684
+ .map((symbol) => symbol.id),
685
+ byName: namesOf(building),
648
686
  };
649
687
  }
688
+ /** Whether one declaration is, or is declared under, a scope or container left unanswered. */
689
+ function underUnanswered(record, owners) {
690
+ for (let at = record; at !== undefined; at = at.parent) {
691
+ if (owners.has(at)) {
692
+ return true;
693
+ }
694
+ }
695
+ return false;
696
+ }
697
+ /** Every declaration but a file, by each name a use may spell it with. */
698
+ function namesOf(building) {
699
+ const byName = new Map();
700
+ for (const record of building) {
701
+ if (record.kind === "file") {
702
+ continue;
703
+ }
704
+ const id = positionKey(record.position);
705
+ const names = record.typeParameter === undefined
706
+ ? [record.component.text, record.localName]
707
+ : [record.typeParameter];
708
+ for (const name of new Set(names)) {
709
+ if (name === undefined) {
710
+ continue;
711
+ }
712
+ const ids = byName.get(name);
713
+ if (ids === undefined) {
714
+ byName.set(name, [id]);
715
+ }
716
+ else if (!ids.includes(id)) {
717
+ ids.push(id);
718
+ }
719
+ }
720
+ }
721
+ return byName;
722
+ }
650
723
  /** The chain of components that names one declaration, read from the module inward. */
651
724
  function chainOf(record) {
652
725
  return [...record.parentChain, record.component];
@@ -691,3 +764,11 @@ function refOf(record, chain) {
691
764
  ...(record.typeParameter === undefined ? {} : { typeParameter: record.typeParameter }),
692
765
  });
693
766
  }
767
+ /**
768
+ * Every declaration of an inventory other than a file, by each name a use may spell it
769
+ * with. It is how a reference the checker could not resolve is still counted, as a
770
+ * reference to every declaration that bears the name it spells.
771
+ */
772
+ export function declarationsByName(held) {
773
+ return held.byName;
774
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * A document written in pieces, so no stage of writing it holds the whole document as
3
+ * one string: a string has a length limit far below what a large report reaches.
4
+ */
5
+ /** The length a piece grows to before it is handed on. */
6
+ export const CHUNK_LENGTH = 1 << 16;
7
+ /**
8
+ * The pieces of `pieces` joined into runs of about {@link CHUNK_LENGTH}, the last one
9
+ * shorter. A piece longer than that is handed on whole.
10
+ */
11
+ export function* coalesced(pieces) {
12
+ let held = [];
13
+ let length = 0;
14
+ for (const piece of pieces) {
15
+ held.push(piece);
16
+ length += piece.length;
17
+ if (length >= CHUNK_LENGTH) {
18
+ yield held.join("");
19
+ held = [];
20
+ length = 0;
21
+ }
22
+ }
23
+ if (length > 0) {
24
+ yield held.join("");
25
+ }
26
+ }
27
+ /** Whether `JSON.stringify` writes nothing for a value where an object member holds it. */
28
+ function omitted(value) {
29
+ return value === undefined || typeof value === "function" || typeof value === "symbol";
30
+ }
31
+ function* valueOf(value, prefix, step) {
32
+ // With no indentation the document is compact: no line breaks, and no space after a key.
33
+ const nl = step === "" ? "" : "\n";
34
+ if (Array.isArray(value)) {
35
+ if (value.length === 0) {
36
+ yield "[]";
37
+ return;
38
+ }
39
+ const inner = prefix + step;
40
+ yield "[";
41
+ for (let at = 0; at < value.length; at += 1) {
42
+ yield at === 0 ? `${nl}${inner}` : `,${nl}${inner}`;
43
+ const element = value[at];
44
+ yield* omitted(element) ? ["null"] : valueOf(element, inner, step);
45
+ }
46
+ yield `${nl}${prefix}]`;
47
+ return;
48
+ }
49
+ if (typeof value === "object" && value !== null) {
50
+ const inner = prefix + step;
51
+ let first = true;
52
+ for (const [key, member] of Object.entries(value)) {
53
+ if (omitted(member)) {
54
+ continue;
55
+ }
56
+ yield `${first ? "{" : ","}${nl}${inner}${JSON.stringify(key)}:${step === "" ? "" : " "}`;
57
+ first = false;
58
+ yield* valueOf(member, inner, step);
59
+ }
60
+ yield first ? "{}" : `${nl}${prefix}}`;
61
+ return;
62
+ }
63
+ yield JSON.stringify(value);
64
+ }
65
+ /**
66
+ * One JSON document in pieces whose concatenation is the value as `JSON.stringify`
67
+ * writes it at `indent` spaces, followed by a newline. The value is plain data: objects,
68
+ * arrays, strings, finite numbers, booleans and null, an object member holding nothing
69
+ * being left out as `JSON.stringify` leaves it out.
70
+ */
71
+ export function jsonChunks(value, indent) {
72
+ return coalesced((function* document() {
73
+ yield* valueOf(value, "", " ".repeat(indent));
74
+ yield "\n";
75
+ })());
76
+ }
@@ -0,0 +1,23 @@
1
+ /** A count with its noun, so a line reads for one as well as for several. */
2
+ function counted(n, one, many) {
3
+ return `${String(n)} ${n === 1 ? one : many}`;
4
+ }
5
+ /**
6
+ * One line per configuration the run dropped, and one counting the questions the checker
7
+ * left unanswered by project, so a reader of the error stream sees where the analysis was
8
+ * partial. The lines carry no prefix; the caller writes them as its own diagnostics.
9
+ */
10
+ export function partialNotes(partial) {
11
+ const notes = [...partial.notBuilt]
12
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
13
+ .map((one) => `the derived configuration ${one.id} was not built and is not analyzed: ${one.error}`);
14
+ if (partial.unanswered.length > 0) {
15
+ const byProject = new Map();
16
+ for (const question of partial.unanswered) {
17
+ byProject.set(question.configFile, (byProject.get(question.configFile) ?? 0) + 1);
18
+ }
19
+ const where = [...byProject].map(([configFile, n]) => `${configFile}: ${String(n)}`).join(", ");
20
+ notes.push(`the checker answered ${counted(partial.unanswered.length, "question", "questions")} with a failure (${where}); every declaration an answer could have kept live is kept live and reported by nothing`);
21
+ }
22
+ return notes;
23
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * The one way the analysis asks a project's checker anything. A question the compiler
3
+ * server panics on answers {@link UNANSWERED}, and a batch it panics on whole is asked
4
+ * again one question at a time; every caller reads an unanswered question as keeping a
5
+ * declaration live, and each is recorded once so a run can say where it was partial.
6
+ * Questions of one accessor about many locations go out together, through the array
7
+ * form where it has one and otherwise as one batch, one round trip per capped run.
8
+ */
9
+ import { isIdentifier } from "@typescript/native/unstable/ast";
10
+ /**
11
+ * The greatest number of locations one batched request asks about. A batch is
12
+ * answered by an array of its own length, so an uncapped batch over a large file
13
+ * makes one answer proportional to that file. The figure bounds the size of one
14
+ * answer rather than the work: the median file is one batch at this cap and the
15
+ * largest production file a handful.
16
+ */
17
+ export const DEFAULT_BATCH_CAP = 4096;
18
+ /** The answer to a question the checker could not answer. */
19
+ export const UNANSWERED = Symbol("unanswered");
20
+ /** Whether one answer is an answer. */
21
+ export function isAnswered(answer) {
22
+ return answer !== UNANSWERED;
23
+ }
24
+ /**
25
+ * The stop a pass raises where it cannot go on without an answer the checker did not
26
+ * give. Whoever runs the pass catches it and keeps live everything the pass could
27
+ * have kept live, so a pass that throws it never computes a finding from the gap.
28
+ */
29
+ export class Unanswerable extends Error {
30
+ constructor() {
31
+ super("the checker did not answer a question this pass needs");
32
+ this.name = "Unanswerable";
33
+ }
34
+ }
35
+ /** The answer itself, or the stop of a pass that cannot go on without it. */
36
+ export function must(answer) {
37
+ if (answer === UNANSWERED) {
38
+ throw new Unanswerable();
39
+ }
40
+ return answer;
41
+ }
42
+ /**
43
+ * Whether one failure is the server's report that a question panicked. The server
44
+ * recovers the panic and answers the question with its text, so the session
45
+ * survives it; any other failure, a closed channel among them, is not one this layer
46
+ * contains.
47
+ */
48
+ function panicked(error) {
49
+ return error instanceof Error && error.message.startsWith("panic:");
50
+ }
51
+ const ASK_DIRECTLY = (_accessor, _locations, question) => question();
52
+ /** What one run could not ask, each question once. */
53
+ export class UnansweredLog {
54
+ #seen = new Map();
55
+ /** Records one question, once however many times it is asked. */
56
+ note(question) {
57
+ const key = [question.configFile, question.accessor, question.location].join("\u0000");
58
+ if (!this.#seen.has(key)) {
59
+ this.#seen.set(key, question);
60
+ }
61
+ }
62
+ /** Every question recorded, in the order first asked. */
63
+ get questions() {
64
+ return [...this.#seen.values()];
65
+ }
66
+ }
67
+ function nodeLocation(node) {
68
+ return `${node.getSourceFile().fileName}:${String(node.pos)}`;
69
+ }
70
+ function symbolLocation(symbol) {
71
+ return `symbol ${String(symbol.id)} ${symbol.name}`;
72
+ }
73
+ function typeLocation(type) {
74
+ return `type ${String(type.id)}`;
75
+ }
76
+ /**
77
+ * The guarded questions over one project's checker. `configFile` names the project
78
+ * in the log, `batch` sends many questions in one round trip, `log` is the run's,
79
+ * shared by every project, and every question is put through `ask`.
80
+ */
81
+ export function queriesOf(checker, configFile, batch, log, ask = ASK_DIRECTLY) {
82
+ const note = (accessor, location) => {
83
+ log.note({ configFile, accessor, location });
84
+ };
85
+ const one = (accessor, location, question) => {
86
+ try {
87
+ return ask(accessor, () => [location()], question);
88
+ }
89
+ catch (error) {
90
+ if (!panicked(error)) {
91
+ throw error;
92
+ }
93
+ note(accessor, location());
94
+ return UNANSWERED;
95
+ }
96
+ };
97
+ /**
98
+ * One question per item of `items`, in runs of at most `cap`, each run in one
99
+ * round trip. A run fails as a whole only where the server panics outside any one
100
+ * question, and is then asked one question at a time.
101
+ */
102
+ const each = (accessor, items, cap, locate, question, alone) => {
103
+ function* guarded(item) {
104
+ try {
105
+ return yield* ask(accessor, () => [locate(item)], () => question(item));
106
+ }
107
+ catch (error) {
108
+ if (!panicked(error)) {
109
+ throw error;
110
+ }
111
+ note(accessor, locate(item));
112
+ return UNANSWERED;
113
+ }
114
+ }
115
+ const answers = [];
116
+ for (let from = 0; from < items.length; from += cap) {
117
+ const run = items.slice(from, from + cap);
118
+ try {
119
+ answers.push(...batch(run.map(guarded)));
120
+ }
121
+ catch (error) {
122
+ if (!panicked(error)) {
123
+ throw error;
124
+ }
125
+ answers.push(...run.map((item) => one(accessor, () => locate(item), () => alone(item))));
126
+ }
127
+ }
128
+ return answers;
129
+ };
130
+ /**
131
+ * One accessor's array form over `items`, in runs of at most `cap`. A run the server
132
+ * panics on is asked again as two halves, down to one item, so the items it can
133
+ * answer are answered and only an item that panics alone is unanswered.
134
+ */
135
+ const many = (accessor, items, cap, locate, question) => {
136
+ const answers = [];
137
+ const settle = (run) => {
138
+ try {
139
+ answers.push(...ask(accessor, () => run.map(locate), () => question(run)));
140
+ return;
141
+ }
142
+ catch (error) {
143
+ if (!panicked(error)) {
144
+ throw error;
145
+ }
146
+ }
147
+ const [only] = run;
148
+ if (run.length === 1 && only !== undefined) {
149
+ note(accessor, locate(only));
150
+ answers.push(UNANSWERED);
151
+ return;
152
+ }
153
+ const half = Math.ceil(run.length / 2);
154
+ settle(run.slice(0, half));
155
+ settle(run.slice(half));
156
+ };
157
+ for (let from = 0; from < items.length; from += cap) {
158
+ settle(items.slice(from, from + cap));
159
+ }
160
+ return answers;
161
+ };
162
+ return {
163
+ symbolsAt: (nodes, cap = DEFAULT_BATCH_CAP) => many("getSymbolAtLocation", nodes, cap, nodeLocation, (run) => checker.getSymbolAtLocation(run)),
164
+ resolvedSymbol: (node) => isIdentifier(node)
165
+ ? one("getResolvedSymbol", () => nodeLocation(node), () => checker.getResolvedSymbol(node))
166
+ : undefined,
167
+ shorthandValue: (node) => one("getShorthandAssignmentValueSymbol", () => nodeLocation(node), () => checker.getShorthandAssignmentValueSymbol(node)),
168
+ immediateAliased: (symbol) => one("getImmediateAliasedSymbol", () => symbolLocation(symbol), () => checker.getImmediateAliasedSymbol(symbol)),
169
+ aliased: (symbol) => one("getAliasedSymbol", () => symbolLocation(symbol), () => checker.getAliasedSymbol(symbol)),
170
+ resolveName: (name, meaning, location, excludeGlobals) => one("resolveName", () => (location === undefined ? `name ${name}` : nodeLocation(location)), () => checker.resolveName(name, meaning, location, excludeGlobals)),
171
+ exportsOfModule: (module) => one("getExportsOfModule", () => symbolLocation(module), () => checker.getExportsOfModule(module)),
172
+ membersOf: (symbol) => one("getMembers", () => symbolLocation(symbol), () => symbol.getMembers()),
173
+ exportsOf: (symbol) => one("getExports", () => symbolLocation(symbol), () => symbol.getExports()),
174
+ parentOf: (symbol) => one("getParent", () => symbolLocation(symbol), () => symbol.getParent()),
175
+ jsDocTags: (symbols, cap = DEFAULT_BATCH_CAP) => each("getJsDocTags", symbols, cap, symbolLocation, (symbol) => symbol.getJsDocTags.gen(checker), (symbol) => symbol.getJsDocTags(checker)),
176
+ typesAt: (nodes, cap = DEFAULT_BATCH_CAP) => many("getTypeAtLocation", nodes, cap, nodeLocation, (run) => checker.getTypeAtLocation(run)),
177
+ typeAt: (node) => one("getTypeAtLocation", () => nodeLocation(node), () => checker.getTypeAtLocation(node)),
178
+ typesOfSymbols: (symbols, cap = DEFAULT_BATCH_CAP) => many("getTypeOfSymbol", symbols, cap, symbolLocation, (run) => checker.getTypeOfSymbol(run)),
179
+ typeOfSymbol: (symbol) => one("getTypeOfSymbol", () => symbolLocation(symbol), () => checker.getTypeOfSymbol(symbol)),
180
+ declaredTypeOf: (symbol) => one("getDeclaredTypeOfSymbol", () => symbolLocation(symbol), () => checker.getDeclaredTypeOfSymbol(symbol)),
181
+ contextualTypes: (nodes, cap = DEFAULT_BATCH_CAP) => each("getContextualType", nodes, cap, nodeLocation, (node) => checker.getContextualType.gen(node), (node) => checker.getContextualType(node)),
182
+ propertiesOf: (type) => one("getPropertiesOfType", () => typeLocation(type), () => checker.getPropertiesOfType(type)),
183
+ indexInfosOf: (type) => one("getIndexInfosOfType", () => typeLocation(type), () => checker.getIndexInfosOfType(type)),
184
+ typeArguments: (type) => type.isTypeReference()
185
+ ? one("getTypeArguments", () => typeLocation(type), () => checker.getTypeArguments(type))
186
+ : [],
187
+ nonNullable: (type) => one("getNonNullableType", () => typeLocation(type), () => checker.getNonNullableType(type)),
188
+ isArray: (type) => one("isArrayType", () => typeLocation(type), () => checker.isArrayType(type)),
189
+ assignableEach: (pairs, cap = DEFAULT_BATCH_CAP) => each("isTypeAssignableTo", pairs, cap, (pair) => `${typeLocation(pair.source)} to ${typeLocation(pair.target)}`, (pair) => checker.isTypeAssignableTo.gen(pair.source, pair.target), (pair) => checker.isTypeAssignableTo(pair.source, pair.target)),
190
+ baseTypes: (type) => type.isClassOrInterface()
191
+ ? one("getBaseTypes", () => typeLocation(type), () => checker.getBaseTypes(type))
192
+ : [],
193
+ constituents: (type) => type.isUnionType() || type.isIntersectionType()
194
+ ? one("getTypes", () => typeLocation(type), () => type.getTypes())
195
+ : [type],
196
+ targetOf: (type) => type.isTypeReference()
197
+ ? one("getTarget", () => typeLocation(type), () => type.getTarget())
198
+ : type,
199
+ symbolOfType: (type) => one("getSymbol", () => typeLocation(type), () => type.getSymbol()),
200
+ signaturesOf: (type, kind) => one("getSignaturesOfType", () => typeLocation(type), () => checker.getSignaturesOfType(type, kind)),
201
+ resolvedSignature: (node) => one("getResolvedSignature", () => nodeLocation(node), () => checker.getResolvedSignature(node)),
202
+ parameterType: (signature, index) => one("getParameterType", () => `signature ${String(signature.id)} parameter ${String(index)}`, () => checker.getParameterType(signature, index)),
203
+ signatureOf: (declaration) => one("getSignatureFromDeclaration", () => nodeLocation(declaration), () => checker.getSignatureFromDeclaration(declaration)),
204
+ returnTypeOf: (signature) => one("getReturnTypeOfSignature", () => `signature ${String(signature.id)}`, () => checker.getReturnTypeOfSignature(signature)),
205
+ typeToString: (type) => one("typeToString", () => typeLocation(type), () => checker.typeToString(type)),
206
+ };
207
+ }