@cplieger/deadset-ts 3.13.0 → 4.0.1

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 (92) hide show
  1. package/README.md +67 -49
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +120 -24
  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 +106 -29
  12. package/dist/src/entry-point-gaps.js +2 -2
  13. package/dist/src/entry-points.js +7 -0
  14. package/dist/src/enum-group.js +46 -40
  15. package/dist/src/exempt.js +42 -1
  16. package/dist/src/findings/emitters.js +15 -1
  17. package/dist/src/findings/interfaces.js +7 -1
  18. package/dist/src/framework-lifecycle.js +8 -6
  19. package/dist/src/implementations.js +45 -23
  20. package/dist/src/interface-satisfaction.js +30 -27
  21. package/dist/src/intra-function-parts.js +37 -20
  22. package/dist/src/inventory.js +85 -4
  23. package/dist/src/json-chunks.js +76 -0
  24. package/dist/src/partial.js +23 -0
  25. package/dist/src/query.js +207 -0
  26. package/dist/src/references.js +92 -20
  27. package/dist/src/reflective-lookup.js +18 -9
  28. package/dist/src/report.js +3 -1
  29. package/dist/src/reporters/annotations.js +9 -5
  30. package/dist/src/reporters/json.js +3 -2
  31. package/dist/src/reporters/sarif.js +2 -1
  32. package/dist/src/reporters/template.js +51 -10
  33. package/dist/src/reporters/text.js +8 -6
  34. package/dist/src/roots.js +27 -11
  35. package/dist/src/schema.js +2 -2
  36. package/dist/src/serialization-contract.js +12 -10
  37. package/dist/src/session.js +79 -39
  38. package/dist/src/suppress-file.js +2 -1
  39. package/dist/src/sweep.js +4 -1
  40. package/dist/src/value-reach.js +14 -19
  41. package/dist/src/verbs/analyze.js +20 -3
  42. package/dist/src/verbs/explain.js +22 -9
  43. package/dist/src/verbs/print-projects.js +41 -8
  44. package/dist/src/verbs/print-retained.js +6 -2
  45. package/dist/src/verbs/print-roots.js +5 -1
  46. package/jsr.json +1 -1
  47. package/package.json +3 -3
  48. package/src/alias-chain.ts +75 -11
  49. package/src/analysis.ts +224 -62
  50. package/src/calls.ts +10 -9
  51. package/src/class-members.ts +10 -11
  52. package/src/configured-declarations.ts +29 -13
  53. package/src/dependencies.ts +42 -20
  54. package/src/deprecation.ts +53 -27
  55. package/src/destructuring.ts +50 -39
  56. package/src/discover.ts +178 -34
  57. package/src/entry-point-gaps.ts +2 -2
  58. package/src/entry-points.ts +7 -0
  59. package/src/enum-group.ts +56 -42
  60. package/src/exempt.ts +50 -2
  61. package/src/findings/emitters.ts +18 -1
  62. package/src/findings/interfaces.ts +7 -1
  63. package/src/framework-lifecycle.ts +10 -6
  64. package/src/host.ts +5 -3
  65. package/src/implementations.ts +59 -23
  66. package/src/interface-satisfaction.ts +30 -27
  67. package/src/intra-function-parts.ts +43 -25
  68. package/src/inventory.ts +97 -4
  69. package/src/json-chunks.ts +87 -0
  70. package/src/partial.ts +40 -0
  71. package/src/query.ts +522 -0
  72. package/src/references.ts +119 -38
  73. package/src/reflective-lookup.ts +21 -11
  74. package/src/report.ts +14 -2
  75. package/src/reporters/annotations.ts +23 -21
  76. package/src/reporters/json.ts +3 -2
  77. package/src/reporters/reporter.ts +2 -2
  78. package/src/reporters/sarif.ts +3 -2
  79. package/src/reporters/template.ts +53 -11
  80. package/src/reporters/text.ts +12 -12
  81. package/src/roots.ts +34 -12
  82. package/src/schema.ts +2 -2
  83. package/src/serialization-contract.ts +12 -10
  84. package/src/session.ts +146 -58
  85. package/src/suppress-file.ts +6 -2
  86. package/src/sweep.ts +9 -4
  87. package/src/value-reach.ts +20 -23
  88. package/src/verbs/analyze.ts +25 -3
  89. package/src/verbs/explain.ts +20 -9
  90. package/src/verbs/print-projects.ts +54 -8
  91. package/src/verbs/print-retained.ts +6 -2
  92. package/src/verbs/print-roots.ts +5 -1
@@ -17,16 +17,16 @@ import {
17
17
  isStringLiteral,
18
18
  isNoSubstitutionTemplateLiteral,
19
19
  SyntaxKind,
20
+ type Identifier,
20
21
  type Node,
21
22
  type SourceFile,
22
23
  } from "@typescript/native/unstable/ast";
23
- import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
24
24
  import type { FindingPosition } from "./finding.ts";
25
25
  import type { Host } from "./host.ts";
26
26
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
27
27
  import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
28
28
  import type { DependencySection, Module } from "./ref.ts";
29
- import { DEFAULT_BATCH_CAP } from "./references.ts";
29
+ import { UNANSWERED } from "./query.ts";
30
30
  import type { Handle, ProjectView } from "./session.ts";
31
31
 
32
32
  const MANIFEST = "package.json";
@@ -70,6 +70,11 @@ export interface ProjectNeeds {
70
70
  * compiler configuration names, which no deletion removes.
71
71
  */
72
72
  readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
73
+ /**
74
+ * Whether the checker left a module specifier unanswered, so the project may use any
75
+ * dependency the manifest declares.
76
+ */
77
+ readonly unanswered: boolean;
73
78
  }
74
79
 
75
80
  /** The run's dependency evidence. */
@@ -299,10 +304,11 @@ export function projectNeeds<Brand>(
299
304
  };
300
305
  use([PROJECT_OWNER], packages);
301
306
 
307
+ let unanswered = false;
302
308
  for (const file of project.ownSourceFiles()) {
303
- fileNeeds(project, held, file, programPackages, use);
309
+ unanswered = fileNeeds(project, held, file, programPackages, use) || unanswered;
304
310
  }
305
- return { packages, uses };
311
+ return { packages, uses, unanswered };
306
312
  }
307
313
 
308
314
  /** The declarations of the inventory a use written at one node belongs to. */
@@ -329,13 +335,14 @@ function ownersOf(file: SourceFile, held: Inventory, node: Node): readonly strin
329
335
  return [PROJECT_OWNER];
330
336
  }
331
337
 
338
+ /** Records the needs of one file, and answers whether a specifier of it went unanswered. */
332
339
  function fileNeeds<Brand>(
333
340
  project: ProjectView<Brand>,
334
341
  held: Inventory,
335
342
  file: SourceFile,
336
343
  programPackages: ReadonlySet<string>,
337
344
  use: (owners: readonly string[], named: Iterable<string>) => void,
338
- ): void {
345
+ ): boolean {
339
346
  const specifiers = [...file.imports, ...file.moduleAugmentations].filter(isLiteralText);
340
347
  const bindings: { readonly node: Texted; readonly specifier: Texted }[] = [];
341
348
  const bound = new Set<Node>();
@@ -351,7 +358,7 @@ function fileNeeds<Brand>(
351
358
  }
352
359
  }
353
360
  const names = new Set(bindings.map(({ node }) => node.text));
354
- const candidates: Node[] = [];
361
+ const candidates: Identifier[] = [];
355
362
  const visit = (node: Node): void => {
356
363
  if (isIdentifier(node) && names.has(node.text) && !bound.has(node)) {
357
364
  candidates.push(node);
@@ -363,13 +370,8 @@ function fileNeeds<Brand>(
363
370
  }
364
371
 
365
372
  const asked = [...specifiers, ...bindings.map(({ node }) => node), ...candidates];
366
- const resolved: (TSSymbol | undefined)[] = [];
367
- for (let at = 0; at < asked.length; at += DEFAULT_BATCH_CAP) {
368
- const handles: Handle<Brand>[] = asked
369
- .slice(at, at + DEFAULT_BATCH_CAP)
370
- .map((node) => project.handle(node));
371
- resolved.push(...project.symbolsAt(handles));
372
- }
373
+ const handles: Handle<Brand>[] = asked.map((node) => project.handle(node));
374
+ const resolved = project.symbolsAt(handles);
373
375
 
374
376
  const packagesOf = new Map<Node, readonly string[]>();
375
377
  specifiers.forEach((specifier, index) => {
@@ -378,7 +380,8 @@ function fileNeeds<Brand>(
378
380
  if (byName !== undefined) {
379
381
  named.add(byName);
380
382
  }
381
- for (const declaration of resolved[index]?.declarations ?? []) {
383
+ const symbol = resolved[index];
384
+ for (const declaration of typeof symbol === "object" ? symbol.declarations : []) {
382
385
  const byPath = packageOfPath(declaration.path);
383
386
  if (byPath !== undefined) {
384
387
  named.add(byPath);
@@ -387,11 +390,19 @@ function fileNeeds<Brand>(
387
390
  packagesOf.set(specifier, [...named]);
388
391
  });
389
392
 
393
+ // A name the checker could not resolve counts as a use of every package a binding of
394
+ // that name brings in, so a gap never makes a dependency read as unused.
390
395
  const bySymbol = new Map<number, readonly string[]>();
391
- bindings.forEach(({ specifier }, index) => {
396
+ const byName = new Map<string, string[]>();
397
+ const unresolvedNames = new Set<string>();
398
+ bindings.forEach(({ node, specifier }, index) => {
399
+ const packages = packagesOf.get(specifier) ?? [];
400
+ byName.set(node.text, [...(byName.get(node.text) ?? []), ...packages]);
392
401
  const symbol = resolved[specifiers.length + index];
393
- if (symbol !== undefined) {
394
- bySymbol.set(symbol.id, packagesOf.get(specifier) ?? []);
402
+ if (symbol === UNANSWERED) {
403
+ unresolvedNames.add(node.text);
404
+ } else if (symbol !== undefined) {
405
+ bySymbol.set(symbol.id, packages);
395
406
  }
396
407
  });
397
408
 
@@ -409,7 +420,13 @@ function fileNeeds<Brand>(
409
420
  if (parent !== undefined && isShorthandPropertyAssignment(parent) && parent.name === node) {
410
421
  symbol = project.shorthandValueAt(project.handle(node));
411
422
  }
412
- const named = symbol === undefined ? undefined : bySymbol.get(symbol.id);
423
+ const named =
424
+ symbol === UNANSWERED || (unresolvedNames.has(node.text) && symbol === undefined)
425
+ ? byName.get(node.text)
426
+ : symbol === undefined
427
+ ? undefined
428
+ : (bySymbol.get(symbol.id) ??
429
+ (unresolvedNames.has(node.text) ? byName.get(node.text) : undefined));
413
430
  if (named !== undefined) {
414
431
  use(ownersOf(file, held, node), named);
415
432
  }
@@ -418,6 +435,7 @@ function fileNeeds<Brand>(
418
435
  for (const reference of file.typeReferenceDirectives) {
419
436
  use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
420
437
  }
438
+ return resolved.slice(0, specifiers.length).includes(UNANSWERED);
421
439
  }
422
440
 
423
441
  /** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
@@ -591,12 +609,16 @@ export function dependenciesOf(
591
609
  const needed = new Set<string>();
592
610
  const uses = new Map<string, Set<string>>();
593
611
  for (const project of perProject) {
594
- project.packages.forEach((one) => needed.add(one));
595
- for (const [id, used] of project.uses) {
612
+ // A specifier the checker left unanswered may resolve to any declared dependency,
613
+ // and what the configuration needs is never deleted, so it is needed there.
614
+ const unknown = project.unanswered ? declared.map((dependency) => dependency.name) : [];
615
+ const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)] as const];
616
+ for (const [id, used] of [...project.uses, ...held]) {
596
617
  const merged = uses.get(id) ?? new Set<string>();
597
618
  used.forEach((one) => merged.add(one));
598
619
  uses.set(id, merged);
599
620
  }
621
+ [...project.packages, ...unknown].forEach((one) => needed.add(one));
600
622
  }
601
623
  const users = new Map<string, number>();
602
624
  for (const used of uses.values()) {
@@ -8,9 +8,8 @@
8
8
  */
9
9
 
10
10
  import { isVariableStatement, SyntaxKind, type Node } from "@typescript/native/unstable/ast";
11
- import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
12
11
  import { nodeKey, type Inventory } from "./inventory.ts";
13
- import { DEFAULT_BATCH_CAP } from "./references.ts";
12
+ import { isAnswered, UNANSWERED } from "./query.ts";
14
13
  import type { Handle, ProjectView } from "./session.ts";
15
14
 
16
15
  /** The tag the TypeScript tools recognize as marking a declaration deprecated. */
@@ -31,19 +30,31 @@ function nameNode(node: Node): Node {
31
30
  return (node as { readonly name?: Node }).name ?? node;
32
31
  }
33
32
 
33
+ /** Which declarations of one project carry the deprecation marker. */
34
+ export interface Deprecation {
35
+ /** Every declaration that carries it, in the inventory's order. */
36
+ readonly deprecated: readonly string[];
37
+ /**
38
+ * Every declaration whose symbol or tags the checker left unanswered where the
39
+ * project's files hold the tag, in the inventory's order: whether it carries the
40
+ * marker is unknown.
41
+ */
42
+ readonly unanswered: readonly string[];
43
+ }
44
+
34
45
  /**
35
- * The identifiers of every declaration of one project that carries the deprecation
36
- * marker, in the inventory's order.
46
+ * The declarations of one project that carry the deprecation marker.
37
47
  *
38
48
  * The checker is asked only for a symbol with a declaration in a file whose tree holds
39
49
  * the tag, so a project whose files hold none costs no round trip, and one that does
40
- * costs one batch per file and one request per symbol that could carry the tag.
50
+ * costs one batch per file and one for the tags of the symbols that could carry it.
41
51
  */
42
52
  export function deprecatedDeclarations<Brand>(
43
53
  project: ProjectView<Brand>,
44
54
  held: Inventory,
45
- ): readonly string[] {
55
+ ): Deprecation {
46
56
  const marked = new Set<string>();
57
+ const unknown = new Set<string>();
47
58
  const markedPaths = new Set<string>();
48
59
  const declared: { readonly id: string; readonly handle: Handle<Brand> }[][] = [];
49
60
 
@@ -72,30 +83,45 @@ export function deprecatedDeclarations<Brand>(
72
83
  }
73
84
 
74
85
  if (markedPaths.size > 0) {
75
- const asked = new Map<number, boolean>();
76
- const carriesTag = (symbol: TSSymbol): boolean => {
77
- const known = asked.get(symbol.id);
78
- if (known !== undefined) {
79
- return known;
86
+ const pairs = declared.flatMap((named) => {
87
+ const symbols = project.symbolsAt(named.map((one) => one.handle));
88
+ return named.flatMap((one, at) => {
89
+ const symbol = symbols[at];
90
+ if (symbol === UNANSWERED) {
91
+ unknown.add(one.id);
92
+ }
93
+ return typeof symbol === "object" &&
94
+ symbol.declarations.some((declaration) => markedPaths.has(declaration.path))
95
+ ? [{ id: one.id, symbol }]
96
+ : [];
97
+ });
98
+ });
99
+ const distinct = [...new Map(pairs.map((one) => [one.symbol.id, one.symbol])).values()];
100
+ const tags = project.queries.jsDocTags(distinct);
101
+ const unread = new Set(
102
+ distinct.filter((_, at) => tags[at] === UNANSWERED).map((one) => one.id),
103
+ );
104
+ const carries = new Set(
105
+ distinct.flatMap((symbol, at) => {
106
+ const read = tags[at];
107
+ return read !== undefined &&
108
+ isAnswered(read) &&
109
+ read.some((tag) => tag.name === DEPRECATED_TAG)
110
+ ? [symbol.id]
111
+ : [];
112
+ }),
113
+ );
114
+ for (const one of pairs) {
115
+ if (carries.has(one.symbol.id)) {
116
+ marked.add(one.id);
80
117
  }
81
- const carries =
82
- symbol.declarations.some((declaration) => markedPaths.has(declaration.path)) &&
83
- symbol.getJsDocTags(project.checker).some((tag) => tag.name === DEPRECATED_TAG);
84
- asked.set(symbol.id, carries);
85
- return carries;
86
- };
87
- for (const named of declared) {
88
- for (let start = 0; start < named.length; start += DEFAULT_BATCH_CAP) {
89
- const batch = named.slice(start, start + DEFAULT_BATCH_CAP);
90
- project.symbolsAt(batch.map((one) => one.handle)).forEach((symbol, at) => {
91
- const one = batch[at];
92
- if (symbol !== undefined && one !== undefined && carriesTag(symbol)) {
93
- marked.add(one.id);
94
- }
95
- });
118
+ if (unread.has(one.symbol.id)) {
119
+ unknown.add(one.id);
96
120
  }
97
121
  }
98
122
  }
99
123
 
100
- return held.symbols.filter((symbol) => marked.has(symbol.id)).map((symbol) => symbol.id);
124
+ const inOrder = (ids: ReadonlySet<string>): readonly string[] =>
125
+ held.symbols.filter((symbol) => ids.has(symbol.id)).map((symbol) => symbol.id);
126
+ return { deprecated: inOrder(marked), unanswered: inOrder(unknown) };
101
127
  }
@@ -25,6 +25,7 @@ import {
25
25
  type Node,
26
26
  } from "@typescript/native/unstable/ast";
27
27
  import { TypeFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
28
+ import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
28
29
  import type { ProjectView } from "./session.ts";
29
30
 
30
31
  /** One property one object pattern reads. */
@@ -212,97 +213,102 @@ interface DestructuringCost {
212
213
  interface Destructuring {
213
214
  /**
214
215
  * The property symbol each read names, in the order given, `undefined` where the
215
- * destructured value's type declares no property of that name or is not known.
216
+ * destructured value's type declares no property of that name or is not known, and
217
+ * {@link UNANSWERED} where a question about the value's type went unanswered.
216
218
  */
217
- resolve(reads: readonly PropertyRead[]): readonly (TSSymbol | undefined)[];
219
+ resolve(reads: readonly PropertyRead[]): readonly Answer<TSSymbol | undefined>[];
218
220
  readonly cost: DestructuringCost;
219
221
  }
220
222
 
223
+ /** A type along a destructuring, or the mark that a question about it went unanswered. */
224
+ type Found = Answer<Type | undefined>;
225
+
221
226
  /**
222
227
  * The resolution over one project, its batches capped at `cap`. Property tables are
223
228
  * read once per type for the whole project, since one type is destructured in many
224
229
  * places and a table answers every name of it.
225
230
  */
226
231
  export function destructuring<Brand>(project: ProjectView<Brand>, cap: number): Destructuring {
227
- const checker = project.checker;
228
- const tables = new Map<number, ReadonlyMap<string, TSSymbol>>();
232
+ const queries = project.queries;
233
+ const tables = new Map<number, Answer<ReadonlyMap<string, TSSymbol>>>();
229
234
  let patternBatches = 0;
230
235
  let patternLookups = 0;
231
236
 
232
- const typesAt = (nodes: readonly Node[]): (Type | undefined)[] => {
233
- const types: (Type | undefined)[] = [];
234
- for (let from = 0; from < nodes.length; from += cap) {
235
- types.push(...checker.getTypeAtLocation(nodes.slice(from, from + cap)));
236
- patternBatches += 1;
237
- }
238
- return types;
239
- };
240
-
241
- const tableOf = (type: Type): ReadonlyMap<string, TSSymbol> => {
237
+ const tableOf = (type: Type): Answer<ReadonlyMap<string, TSSymbol>> => {
242
238
  const known = tables.get(type.id);
243
239
  if (known !== undefined) {
244
240
  return known;
245
241
  }
246
- const table = new Map<string, TSSymbol>();
242
+ let table: Answer<ReadonlyMap<string, TSSymbol>> = new Map<string, TSSymbol>();
247
243
  if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
248
244
  patternLookups += 1;
249
- for (const property of checker.getPropertiesOfType(type)) {
250
- table.set(property.name, property);
251
- }
245
+ const properties = queries.propertiesOf(type);
246
+ table = isAnswered(properties)
247
+ ? new Map(properties.map((property) => [property.name, property]))
248
+ : UNANSWERED;
252
249
  }
253
250
  tables.set(type.id, table);
254
251
  return table;
255
252
  };
256
253
 
257
- const definedOf = (type: Type): Type | undefined => {
254
+ const definedOf = (type: Type): Found => {
258
255
  if ((type.flags & TypeFlags.Union) === 0) {
259
256
  return type;
260
257
  }
261
258
  patternLookups += 1;
262
- return checker.getNonNullableType(type);
259
+ return queries.nonNullable(type);
263
260
  };
264
261
 
265
262
  /**
266
263
  * The type one element of an array or tuple value has: a tuple's element at `index`,
267
264
  * an array's element type at any index, and `undefined` for a tuple iterated whole.
268
265
  */
269
- const elementOf = (type: Type, index: number | undefined): Type | undefined => {
266
+ const elementOf = (type: Type, index: number | undefined): Found => {
270
267
  if (!type.isTypeReference()) {
271
268
  return undefined;
272
269
  }
273
- patternLookups += 1;
274
- if (checker.isTupleType(type)) {
270
+ if (type.isTupleType()) {
275
271
  if (index === undefined) {
276
272
  return undefined;
277
273
  }
278
274
  patternLookups += 1;
279
- return checker.getTypeArguments(type)[index];
275
+ const elements = queries.typeArguments(type);
276
+ return isAnswered(elements) ? elements[index] : UNANSWERED;
280
277
  }
281
278
  patternLookups += 1;
282
- if (!checker.isArrayType(type)) {
279
+ const array = queries.isArray(type);
280
+ if (!isAnswered(array)) {
281
+ return UNANSWERED;
282
+ }
283
+ if (!array) {
283
284
  return undefined;
284
285
  }
285
286
  patternLookups += 1;
286
- return checker.getTypeArguments(type)[0];
287
+ const elements = queries.typeArguments(type);
288
+ return isAnswered(elements) ? elements[0] : UNANSWERED;
287
289
  };
288
290
 
289
- const step = (type: Type, next: Step): Type | undefined => {
291
+ const step = (type: Type, next: Step): Found => {
290
292
  const defined = definedOf(type);
291
- if (defined === undefined) {
292
- return undefined;
293
+ if (defined === undefined || defined === UNANSWERED) {
294
+ return defined;
293
295
  }
294
296
  if (next.kind === "property") {
295
- const property = tableOf(defined).get(next.name);
297
+ const table = tableOf(defined);
298
+ if (!isAnswered(table)) {
299
+ return UNANSWERED;
300
+ }
301
+ const property = table.get(next.name);
296
302
  if (property === undefined) {
297
303
  return undefined;
298
304
  }
299
305
  patternLookups += 1;
300
- return checker.getTypeOfSymbol(property);
306
+ return queries.typeOfSymbol(property);
301
307
  }
302
308
  return elementOf(defined, next.kind === "element" ? next.index : undefined);
303
309
  };
304
310
 
305
- const resolve = (reads: readonly PropertyRead[]): readonly (TSSymbol | undefined)[] => {
311
+ const resolve = (reads: readonly PropertyRead[]): readonly Answer<TSSymbol | undefined>[] => {
306
312
  const patterns = [...new Set(reads.map((read) => read.pattern))];
307
313
  const sources = new Map<Node, Source | undefined>();
308
314
  const asking = new Set<Node>();
@@ -317,15 +323,16 @@ export function destructuring<Brand>(project: ProjectView<Brand>, cap: number):
317
323
  asking.add(source.expression);
318
324
  }
319
325
  }
320
- const answered = new Map<Node, Type>();
321
326
  const asked = [...asking];
322
- typesAt(asked).forEach((type, index) => {
327
+ patternBatches += Math.ceil(asked.length / cap);
328
+ const answered = new Map<Node, Found>();
329
+ queries.typesAt(asked, cap).forEach((type, index) => {
323
330
  const node = asked[index];
324
- if (type !== undefined && node !== undefined) {
331
+ if (node !== undefined) {
325
332
  answered.set(node, type);
326
333
  }
327
334
  });
328
- const patternTypes = new Map<Node, Type | undefined>();
335
+ const patternTypes = new Map<Node, Found>();
329
336
  for (const pattern of patterns) {
330
337
  const source = sources.get(pattern);
331
338
  if (source === undefined) {
@@ -334,13 +341,17 @@ export function destructuring<Brand>(project: ProjectView<Brand>, cap: number):
334
341
  }
335
342
  let type = answered.get(source.expression);
336
343
  for (const next of source.steps) {
337
- type = type === undefined ? undefined : step(type, next);
344
+ type = type === undefined || type === UNANSWERED ? type : step(type, next);
338
345
  }
339
- patternTypes.set(pattern, type === undefined ? undefined : definedOf(type));
346
+ patternTypes.set(pattern, type === undefined || type === UNANSWERED ? type : definedOf(type));
340
347
  }
341
348
  return reads.map((read) => {
342
349
  const type = patternTypes.get(read.pattern);
343
- return type === undefined ? undefined : tableOf(type).get(read.name);
350
+ if (type === undefined || type === UNANSWERED) {
351
+ return type;
352
+ }
353
+ const table = tableOf(type);
354
+ return isAnswered(table) ? table.get(read.name) : UNANSWERED;
344
355
  });
345
356
  };
346
357