@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
@@ -17,7 +17,7 @@ import {
17
17
  import { SymbolFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
18
18
  import { aliasChains } from "./alias-chain.ts";
19
19
  import { nodeKey, type Inventory } from "./inventory.ts";
20
- import { DEFAULT_BATCH_CAP } from "./references.ts";
20
+ import { must, Unanswerable } from "./query.ts";
21
21
  import type { ProjectView } from "./session.ts";
22
22
 
23
23
  /** What one run knows about how its interfaces are implemented. */
@@ -29,6 +29,13 @@ export interface Implementations {
29
29
  * class's implementation is a method whose body holds no statement.
30
30
  */
31
31
  readonly bodies: ReadonlyMap<string, ReadonlyMap<string, boolean>>;
32
+ /**
33
+ * The interface methods whose implementations a project could not read in full,
34
+ * because the checker left a question about them unanswered. Each is treated as a
35
+ * marker, whose every implementation is empty, so none is reported for want of an
36
+ * answer.
37
+ */
38
+ readonly unknown: ReadonlySet<string>;
32
39
  }
33
40
 
34
41
  /** One interface or class the inventory holds, by its declaration. */
@@ -97,24 +104,22 @@ function walk<Brand>(
97
104
  return { interfaces, classes, clauses };
98
105
  }
99
106
 
100
- /** The type at each node, asked in capped batches. */
107
+ /** The type at each node. */
101
108
  function typesAt<Brand>(project: ProjectView<Brand>, nodes: readonly Node[]): Map<Node, Type> {
102
109
  const typeAt = new Map<Node, Type>();
103
- for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
104
- const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
105
- project.checker.getTypeAtLocation(run).forEach((type, index) => {
106
- const node = run[index];
107
- if (type !== undefined && node !== undefined) {
108
- typeAt.set(node, type);
109
- }
110
- });
111
- }
110
+ project.queries.typesAt(nodes).forEach((answer, index) => {
111
+ const type = must(answer);
112
+ const node = nodes[index];
113
+ if (type !== undefined && node !== undefined) {
114
+ typeAt.set(node, type);
115
+ }
116
+ });
112
117
  return typeAt;
113
118
  }
114
119
 
115
120
  /** Whether one class member is a method whose every written body holds no statement. */
116
121
  function emptyImplementation<Brand>(project: ProjectView<Brand>, member: TSSymbol): boolean {
117
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
122
+ const own = project.ownPaths();
118
123
  let bodies = 0;
119
124
  for (const handle of member.declarations) {
120
125
  // A body outside the target is one this analysis cannot read, so it is not empty.
@@ -135,19 +140,41 @@ function emptyImplementation<Brand>(project: ProjectView<Brand>, member: TSSymbo
135
140
  return bodies > 0;
136
141
  }
137
142
 
138
- /** How one project's interfaces are implemented by its classes. */
143
+ /**
144
+ * How one project's interfaces are implemented by its classes. Where the checker leaves
145
+ * a question unanswered, what was read stands and every interface method of the project
146
+ * is named in {@link Implementations.unknown}.
147
+ */
139
148
  export function implementations<Brand>(
140
149
  project: ProjectView<Brand>,
141
150
  held: Inventory,
142
151
  ): Implementations {
143
152
  const classes = new Map<string, Set<string>>();
144
153
  const bodies = new Map<string, Map<string, boolean>>();
154
+ try {
155
+ implementedIn(project, held, classes, bodies);
156
+ return { classes, bodies, unknown: new Set() };
157
+ } catch (error: unknown) {
158
+ if (!(error instanceof Unanswerable)) {
159
+ throw error;
160
+ }
161
+ const methods = held.symbols.filter((symbol) => symbol.kind === "interface-method");
162
+ return { classes, bodies, unknown: new Set(methods.map((symbol) => symbol.id)) };
163
+ }
164
+ }
165
+
166
+ function implementedIn<Brand>(
167
+ project: ProjectView<Brand>,
168
+ held: Inventory,
169
+ classes: Map<string, Set<string>>,
170
+ bodies: Map<string, Map<string, boolean>>,
171
+ ): void {
145
172
  const walked = walk(project, held);
146
173
  if (walked.interfaces.length === 0 || walked.classes.length === 0) {
147
- return { classes, bodies };
174
+ return;
148
175
  }
149
176
 
150
- const checker = project.checker;
177
+ const queries = project.queries;
151
178
  const typeAt = typesAt(project, [
152
179
  ...walked.interfaces.map((one) => one.nameNode),
153
180
  ...walked.classes.map((one) => one.nameNode),
@@ -178,10 +205,10 @@ export function implementations<Brand>(
178
205
  }
179
206
 
180
207
  const membersOf = (at: InterfaceAt): readonly TSSymbol[] =>
181
- (at.members ??= checker.getPropertiesOfType(at.type));
208
+ (at.members ??= must(queries.propertiesOf(at.type)));
182
209
  const classMembers = (at: ClassAt): ReadonlyMap<string, TSSymbol> =>
183
210
  (at.members ??= new Map(
184
- checker.getPropertiesOfType(at.type).map((member) => [member.escapedName, member]),
211
+ must(queries.propertiesOf(at.type)).map((member) => [member.escapedName, member]),
185
212
  ));
186
213
 
187
214
  const methodIds = new Set(
@@ -233,12 +260,14 @@ export function implementations<Brand>(
233
260
  if (entry === undefined || implementer === undefined) {
234
261
  continue;
235
262
  }
236
- const at = byType.get(entry.isTypeReference() ? entry.getTarget().id : entry.id);
263
+ const at = byType.get(must(queries.targetOf(entry)).id);
237
264
  if (at !== undefined) {
238
265
  pair(at, implementer);
239
266
  }
240
267
  }
241
268
 
269
+ // Every pair whose names line up is asked about at once.
270
+ const candidates: { readonly at: InterfaceAt; readonly implementer: ClassAt }[] = [];
242
271
  for (const at of byType.values()) {
243
272
  const members = at.generic ? [] : membersOf(at);
244
273
  if (members.length === 0) {
@@ -252,18 +281,25 @@ export function implementations<Brand>(
252
281
  const written = classMembers(implementer);
253
282
  if (
254
283
  required.every((member) => written.has(member.escapedName)) &&
255
- members.some((member) => written.has(member.escapedName)) &&
256
- checker.isTypeAssignableTo(implementer.type, at.type)
284
+ members.some((member) => written.has(member.escapedName))
257
285
  ) {
258
- pair(at, implementer);
286
+ candidates.push({ at, implementer });
259
287
  }
260
288
  }
261
289
  }
262
- return { classes, bodies };
290
+ const assignable = queries.assignableEach(
291
+ candidates.map((one) => ({ source: one.implementer.type, target: one.at.type })),
292
+ );
293
+ candidates.forEach((one, index) => {
294
+ if (must(assignable[index] ?? false)) {
295
+ pair(one.at, one.implementer);
296
+ }
297
+ });
263
298
  }
264
299
 
265
300
  /** Every project's answers as one run's, each implementation once. */
266
301
  export function mergeImplementations(per: readonly Implementations[]): Implementations {
302
+ const unknown = new Set(per.flatMap((one) => [...one.unknown]));
267
303
  const classes = new Map<string, Set<string>>();
268
304
  const bodies = new Map<string, Map<string, boolean>>();
269
305
  for (const one of per) {
@@ -282,5 +318,5 @@ export function mergeImplementations(per: readonly Implementations[]): Implement
282
318
  }
283
319
  }
284
320
  }
285
- return { classes, bodies };
321
+ return { classes, bodies, unknown };
286
322
  }
@@ -52,7 +52,7 @@ import { aliasChains } from "./alias-chain.ts";
52
52
  import type { DetectorInput, Evidence } from "./exempt.ts";
53
53
  import { nodeKey } from "./inventory.ts";
54
54
  import { renderPosition } from "./position.ts";
55
- import { DEFAULT_BATCH_CAP } from "./references.ts";
55
+ import { must } from "./query.ts";
56
56
 
57
57
  /** One class the inventory holds, by its declaration. */
58
58
  interface ClassAt {
@@ -246,7 +246,7 @@ function walk(files: readonly SourceFile[], declarations: ReadonlyMap<string, st
246
246
  */
247
247
  export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
248
248
  const { project, held, targetRoot } = input;
249
- const checker = project.checker;
249
+ const queries = project.queries;
250
250
  const files = project.ownSourceFiles();
251
251
  const { classes, clauses, flows } = walk(files, held.declarations);
252
252
  if (classes.length === 0) {
@@ -261,15 +261,13 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
261
261
  ...flows.filter((flow) => flow.self === undefined).map((flow) => flow.value),
262
262
  ];
263
263
  const typeAt = new Map<Node, Type>();
264
- for (let from = 0; from < asked.length; from += DEFAULT_BATCH_CAP) {
265
- const run = asked.slice(from, from + DEFAULT_BATCH_CAP);
266
- checker.getTypeAtLocation(run).forEach((type, index) => {
267
- const node = run[index];
268
- if (type !== undefined && node !== undefined) {
269
- typeAt.set(node, type);
270
- }
271
- });
272
- }
264
+ queries.typesAt(asked).forEach((answer, index) => {
265
+ const type = must(answer);
266
+ const node = asked[index];
267
+ if (type !== undefined && node !== undefined) {
268
+ typeAt.set(node, type);
269
+ }
270
+ });
273
271
 
274
272
  const classOfType = new Map<number, ClassAt>();
275
273
  const instanceOf = new Map<string, Type>();
@@ -289,7 +287,7 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
289
287
  }
290
288
  let parts = constituents.get(type.id);
291
289
  if (parts === undefined) {
292
- parts = type.getTypes();
290
+ parts = must(queries.constituents(type));
293
291
  constituents.set(type.id, parts);
294
292
  }
295
293
  return parts;
@@ -307,7 +305,7 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
307
305
  return [];
308
306
  }
309
307
  if (!targets.has(part.id)) {
310
- targets.set(part.id, classOfType.get(part.getTarget().id));
308
+ targets.set(part.id, classOfType.get(must(queries.targetOf(part)).id));
311
309
  }
312
310
  const generic = targets.get(part.id);
313
311
  return generic === undefined ? [] : [{ owner: generic, type: part }];
@@ -318,7 +316,7 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
318
316
  const interfacesOf = (type: Type): readonly { type: Type; name: string }[] =>
319
317
  partsOf(type).flatMap((part) => {
320
318
  if (!interfaceNames.has(part.id)) {
321
- const symbol = part.isObjectType() ? part.getSymbol() : undefined;
319
+ const symbol = part.isObjectType() ? must(queries.symbolOfType(part)) : undefined;
322
320
  interfaceNames.set(
323
321
  part.id,
324
322
  symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
@@ -352,7 +350,8 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
352
350
  }
353
351
  }
354
352
 
355
- for (const flow of flows) {
353
+ // The positions of every flow carrying a class are asked for at once.
354
+ const carrying = flows.flatMap((flow) => {
356
355
  const self = flow.self === undefined ? undefined : instanceOf.get(flow.self.id);
357
356
  const valueType = typeAt.get(flow.value);
358
357
  const sources =
@@ -361,32 +360,35 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
361
360
  : valueType === undefined
362
361
  ? []
363
362
  : classesOf(valueType);
364
- if (sources.length === 0) {
365
- continue;
366
- }
367
- const position = checker.getContextualType(flow.value);
363
+ return sources.length === 0 ? [] : [{ flow, sources }];
364
+ });
365
+ const positions = queries.contextualTypes(carrying.map((one) => one.flow.value));
366
+ carrying.forEach(({ flow, sources }, index) => {
367
+ const position = must(positions[index]);
368
368
  if (position === undefined) {
369
- continue;
369
+ return;
370
370
  }
371
371
  for (const reached of interfacesOf(position)) {
372
372
  for (const source of sources) {
373
373
  reach(source.owner, source.type, reached.type, reached.name, flow.value);
374
374
  }
375
375
  }
376
- }
376
+ });
377
377
 
378
378
  const chains = aliasChains(project, held);
379
379
  const required = new Map<number, ReadonlySet<string>>();
380
380
  const members = new Map<string, ReadonlyMap<string, readonly string[]>>();
381
381
  const found: Evidence[] = [];
382
- for (const pair of pairs.values()) {
383
- if (!checker.isTypeAssignableTo(pair.source, pair.target)) {
382
+ const asking = [...pairs.values()];
383
+ const assignable = queries.assignableEach(asking);
384
+ for (const [index, pair] of asking.entries()) {
385
+ if (!must(assignable[index] ?? false)) {
384
386
  continue;
385
387
  }
386
388
  let names = required.get(pair.target.id);
387
389
  if (names === undefined) {
388
390
  names = new Set(
389
- checker.getPropertiesOfType(pair.target).map((property) => property.escapedName),
391
+ must(queries.propertiesOf(pair.target)).map((property) => property.escapedName),
390
392
  );
391
393
  required.set(pair.target.id, names);
392
394
  }
@@ -394,9 +396,10 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
394
396
  if (answering === undefined) {
395
397
  const instance = instanceOf.get(pair.owner.id) ?? pair.source;
396
398
  answering = new Map(
397
- checker
398
- .getPropertiesOfType(instance)
399
- .map((property) => [property.escapedName, chains.declarationsOf(property)]),
399
+ must(queries.propertiesOf(instance)).map((property) => [
400
+ property.escapedName,
401
+ chains.declarationsOf(property),
402
+ ]),
400
403
  );
401
404
  members.set(pair.owner.id, answering);
402
405
  }
@@ -38,12 +38,14 @@ import {
38
38
  isParenthesizedExpression,
39
39
  isPrefixUnaryExpression,
40
40
  isPropertyAccessExpression,
41
+ isQualifiedName,
41
42
  isReturnStatement,
42
43
  isShorthandPropertyAssignment,
43
44
  isStringLiteral,
44
45
  isSwitchStatement,
45
46
  isThrowStatement,
46
47
  isTypeAliasDeclaration,
48
+ isTypeReferenceNode,
47
49
  isVariableStatement,
48
50
  isVoidExpression,
49
51
  NodeFlags,
@@ -58,7 +60,8 @@ import {
58
60
  import type { FindingPosition } from "./finding.ts";
59
61
  import { nodeKey, type Inventory } from "./inventory.ts";
60
62
  import { positionKey, renderPosition } from "./position.ts";
61
- import { DEFAULT_BATCH_CAP, type Reference } from "./references.ts";
63
+ import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
64
+ import type { Reference } from "./references.ts";
62
65
  import type { ProjectView } from "./session.ts";
63
66
  import { signatureOf, type SignatureNode } from "./signatures.ts";
64
67
 
@@ -318,8 +321,14 @@ function extendedNames(files: readonly SourceFile[]): ReadonlySet<string> {
318
321
  const visit = (node: Node): void => {
319
322
  if (isHeritageClause(node) && node.token === SyntaxKind.ExtendsKeyword) {
320
323
  for (const type of node.types) {
321
- const expression = unwrapped(type.expression);
322
- const named = isPropertyAccessExpression(expression) ? expression.name : expression;
324
+ // A class's clause holds expressions; an interface's holds type references.
325
+ let named: Node;
326
+ if (isTypeReferenceNode(type)) {
327
+ named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
328
+ } else {
329
+ const expression = unwrapped(type.expression);
330
+ named = isPropertyAccessExpression(expression) ? expression.name : expression;
331
+ }
323
332
  if (isIdentifier(named)) {
324
333
  names.add(named.text);
325
334
  }
@@ -364,39 +373,41 @@ interface Declared {
364
373
  readonly signature: SignatureNode;
365
374
  }
366
375
 
367
- /** The symbol identifier each of a batch of identifiers resolves to, where the batch answers. */
376
+ /**
377
+ * The symbol identifier each of a batch of identifiers resolves to, where the batch
378
+ * resolves it, and {@link UNANSWERED} where the checker did not answer.
379
+ */
368
380
  function resolvedIds<Brand>(
369
381
  project: ProjectView<Brand>,
370
382
  nodes: readonly Identifier[],
371
- ): ReadonlyMap<Node, number> {
372
- const ids = new Map<Node, number>();
373
- for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
374
- const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
375
- project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
376
- const node = run[index];
377
- if (symbol !== undefined && node !== undefined) {
378
- ids.set(node, symbol.id);
379
- }
380
- });
381
- }
383
+ ): ReadonlyMap<Node, Answer<number>> {
384
+ const ids = new Map<Node, Answer<number>>();
385
+ project.symbolsAt(nodes.map((node) => project.handle(node))).forEach((symbol, index) => {
386
+ const node = nodes[index];
387
+ if (node !== undefined && symbol !== undefined) {
388
+ ids.set(node, symbol === UNANSWERED ? UNANSWERED : symbol.id);
389
+ }
390
+ });
382
391
  return ids;
383
392
  }
384
393
 
385
394
  /**
386
395
  * Whether one identifier reads the binding a name node declares: it resolves to the
387
- * same symbol, or the checker resolves it to nothing, which is read as a use rather than
388
- * risk a finding; a shorthand property reads the binding of its own name.
396
+ * same symbol, or the checker resolves it to nothing or does not answer for either
397
+ * name, which is read as a use rather than risk a finding; a shorthand property reads
398
+ * the binding of its own name.
389
399
  */
390
400
  function readsBinding(
391
401
  use: Identifier,
392
402
  declared: Identifier,
393
- ids: ReadonlyMap<Node, number>,
403
+ ids: ReadonlyMap<Node, Answer<number>>,
394
404
  ): boolean {
395
405
  if (isShorthandPropertyAssignment(use.parent)) {
396
406
  return true;
397
407
  }
398
408
  const at = ids.get(use);
399
- return at === undefined || at === ids.get(declared);
409
+ const own = ids.get(declared);
410
+ return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
400
411
  }
401
412
 
402
413
  /** What one file's walk collects before the batch that resolves its names. */
@@ -587,7 +598,7 @@ function parameterParts(
587
598
  root: string,
588
599
  one: Declared,
589
600
  uses: readonly Identifier[],
590
- ids: ReadonlyMap<Node, number>,
601
+ ids: ReadonlyMap<Node, Answer<number>>,
591
602
  ): PartFact[] {
592
603
  const signature = one.signature;
593
604
  // A function that reads `arguments` reads every parameter by position.
@@ -676,7 +687,7 @@ function deadStores(
676
687
  block: Block,
677
688
  holder: string,
678
689
  locals: readonly Local[],
679
- ids: ReadonlyMap<Node, number>,
690
+ ids: ReadonlyMap<Node, Answer<number>>,
680
691
  ): PartFact[] {
681
692
  const found: PartFact[] = [];
682
693
  for (const local of locals) {
@@ -779,10 +790,17 @@ function resultParts<Brand>(
779
790
  if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
780
791
  continue;
781
792
  }
782
- const typed = project.checker.getSignatureFromDeclaration(signature);
783
- const result =
784
- typed === undefined ? undefined : project.checker.getReturnTypeOfSignature(typed);
785
- if (result === undefined || NO_VALUE.has(project.checker.typeToString(result))) {
793
+ // A question the checker leaves unanswered withholds the finding.
794
+ const typed = project.queries.signatureOf(signature);
795
+ if (typed === undefined || !isAnswered(typed)) {
796
+ continue;
797
+ }
798
+ const result = project.queries.returnTypeOf(typed);
799
+ if (result === undefined || !isAnswered(result)) {
800
+ continue;
801
+ }
802
+ const spelled = project.queries.typeToString(result);
803
+ if (!isAnswered(spelled) || NO_VALUE.has(spelled)) {
786
804
  continue;
787
805
  }
788
806
  const at = signature.type ?? (node as { readonly name?: Node }).name ?? node;
package/src/inventory.ts CHANGED
@@ -33,6 +33,7 @@ import type { Host } from "./host.ts";
33
33
  import { dirnamePath, joinPath, relativePath } from "./paths.ts";
34
34
  import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
35
35
  import { computedComponent, nameComponent, renderRef, type Component, type Module } from "./ref.ts";
36
+ import { isAnswered, UNANSWERED } from "./query.ts";
36
37
  import type { Handle, ProjectView } from "./session.ts";
37
38
 
38
39
  /**
@@ -160,6 +161,17 @@ export interface Inventory {
160
161
  * that the omission is a number rather than a silence.
161
162
  */
162
163
  readonly outsideOwnFiles: number;
164
+ /**
165
+ * The declarations whose module, namespace or container table the checker did not
166
+ * answer, in position order: whether each is exported, and which members a table
167
+ * would have added, are unknown.
168
+ */
169
+ readonly unanswered: readonly string[];
170
+ /**
171
+ * Every declaration other than a file by each name a use may spell it with: its
172
+ * component, its declared name where that differs, and a type parameter's own name.
173
+ */
174
+ readonly byName: ReadonlyMap<string, readonly string[]>;
163
175
  }
164
176
 
165
177
  /** A declaration under construction, before its reference is rendered. */
@@ -758,12 +770,28 @@ export function inventory<Brand>(
758
770
  const handles: Handle<Brand>[] = batched.map((node) => project.handle(node));
759
771
  const resolved = project.symbolsAt(handles);
760
772
  const symbolOf = new Map<Node, TSSymbol>();
773
+ // A scope or a container whose symbol or table went unanswered keeps what its syntax
774
+ // declares, and everything under it is recorded unanswered.
775
+ const unansweredOwners = new Set<Building>();
761
776
  batched.forEach((node, index) => {
762
777
  const symbol = resolved[index];
763
- if (symbol !== undefined) {
778
+ if (typeof symbol === "object") {
764
779
  symbolOf.set(node, symbol);
765
780
  }
766
781
  });
782
+ resolved.slice(0, exportScopes.length).forEach((symbol, index) => {
783
+ const scope = exportScopes[index];
784
+ if (symbol === UNANSWERED && scope !== undefined) {
785
+ unansweredOwners.add(scope.owner);
786
+ }
787
+ });
788
+ containers
789
+ .filter((container) => container.nameNode !== undefined)
790
+ .forEach((container, index) => {
791
+ if (resolved[exportScopes.length + index] === UNANSWERED) {
792
+ unansweredOwners.add(container.owner);
793
+ }
794
+ });
767
795
 
768
796
  let exportTables = 0;
769
797
  for (const scope of exportScopes) {
@@ -772,7 +800,11 @@ export function inventory<Brand>(
772
800
  continue;
773
801
  }
774
802
  exportTables += 1;
775
- for (const [, exported] of symbol.getExports()) {
803
+ const table = project.queries.exportsOf(symbol);
804
+ if (!isAnswered(table)) {
805
+ unansweredOwners.add(scope.owner);
806
+ }
807
+ for (const [, exported] of isAnswered(table) ? table : []) {
776
808
  for (const declaration of exported.declarations) {
777
809
  const held = project.declarationAt(declaration);
778
810
  if (held === undefined) {
@@ -794,11 +826,21 @@ export function inventory<Brand>(
794
826
  if (symbol !== undefined) {
795
827
  if (!container.membersAreExports) {
796
828
  memberTables += 1;
797
- outsideOwnFiles += readTable(container, symbol.getMembers(), seen, false);
829
+ const table = project.queries.membersOf(symbol);
830
+ if (isAnswered(table)) {
831
+ outsideOwnFiles += readTable(container, table, seen, false);
832
+ } else {
833
+ unansweredOwners.add(container.owner);
834
+ }
798
835
  }
799
836
  if (container.membersAreExports || container.isClass) {
800
837
  memberTables += 1;
801
- outsideOwnFiles += readTable(container, symbol.getExports(), seen, container.isClass);
838
+ const table = project.queries.exportsOf(symbol);
839
+ if (isAnswered(table)) {
840
+ outsideOwnFiles += readTable(container, table, seen, container.isClass);
841
+ } else {
842
+ unansweredOwners.add(container.owner);
843
+ }
802
844
  }
803
845
  }
804
846
  completeFromTree(container, seen);
@@ -919,6 +961,7 @@ export function inventory<Brand>(
919
961
  return undefined;
920
962
  }
921
963
 
964
+ const byId = new Map(building.map((record) => [positionKey(record.position), record]));
922
965
  const symbols = building
923
966
  .map(render)
924
967
  .sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
@@ -932,9 +975,50 @@ export function inventory<Brand>(
932
975
  cost: { batches: 1, exportTables, memberTables },
933
976
  starReExports,
934
977
  outsideOwnFiles,
978
+ unanswered: symbols
979
+ .filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
980
+ .map((symbol) => symbol.id),
981
+ byName: namesOf(building),
935
982
  };
936
983
  }
937
984
 
985
+ /** Whether one declaration is, or is declared under, a scope or container left unanswered. */
986
+ function underUnanswered(record: Building | undefined, owners: ReadonlySet<Building>): boolean {
987
+ for (let at = record; at !== undefined; at = at.parent) {
988
+ if (owners.has(at)) {
989
+ return true;
990
+ }
991
+ }
992
+ return false;
993
+ }
994
+
995
+ /** Every declaration but a file, by each name a use may spell it with. */
996
+ function namesOf(building: readonly Building[]): ReadonlyMap<string, readonly string[]> {
997
+ const byName = new Map<string, string[]>();
998
+ for (const record of building) {
999
+ if (record.kind === "file") {
1000
+ continue;
1001
+ }
1002
+ const id = positionKey(record.position);
1003
+ const names =
1004
+ record.typeParameter === undefined
1005
+ ? [record.component.text, record.localName]
1006
+ : [record.typeParameter];
1007
+ for (const name of new Set(names)) {
1008
+ if (name === undefined) {
1009
+ continue;
1010
+ }
1011
+ const ids = byName.get(name);
1012
+ if (ids === undefined) {
1013
+ byName.set(name, [id]);
1014
+ } else if (!ids.includes(id)) {
1015
+ ids.push(id);
1016
+ }
1017
+ }
1018
+ }
1019
+ return byName;
1020
+ }
1021
+
938
1022
  /** The chain of components that names one declaration, read from the module inward. */
939
1023
  function chainOf(record: Building): readonly Component[] {
940
1024
  return [...record.parentChain, record.component];
@@ -984,3 +1068,12 @@ function refOf(record: Building, chain: readonly Component[]): string {
984
1068
  ...(record.typeParameter === undefined ? {} : { typeParameter: record.typeParameter }),
985
1069
  });
986
1070
  }
1071
+
1072
+ /**
1073
+ * Every declaration of an inventory other than a file, by each name a use may spell it
1074
+ * with. It is how a reference the checker could not resolve is still counted, as a
1075
+ * reference to every declaration that bears the name it spells.
1076
+ */
1077
+ export function declarationsByName(held: Inventory): ReadonlyMap<string, readonly string[]> {
1078
+ return held.byName;
1079
+ }