@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
@@ -7,6 +7,7 @@
7
7
  import { isCallExpression, isExportDeclaration, isExternalModuleReference, isImportDeclaration, isImportEqualsDeclaration, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
8
8
  import { SymbolFlags, } from "@typescript/native/unstable/sync";
9
9
  import { resolvedTargets } from "./calls.js";
10
+ import { must, Unanswerable } from "./query.js";
10
11
  import { nameComponent } from "./ref.js";
11
12
  /** The meanings a name of the global scope is looked up under: any of them. */
12
13
  const ANY_MEANING = SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace;
@@ -55,7 +56,7 @@ function unaliased(project, symbol) {
55
56
  if ((symbol.flags & SymbolFlags.Alias) === 0) {
56
57
  return symbol;
57
58
  }
58
- const target = project.checker.getAliasedSymbol(symbol);
59
+ const target = must(project.queries.aliased(symbol));
59
60
  return target.declarations.length === 0 ? undefined : target;
60
61
  }
61
62
  /** Whether one symbol is a member a class declares: a property, a method or an accessor. */
@@ -68,20 +69,20 @@ function isClassMember(symbol) {
68
69
  * with the static suffix names one of the first and a component without it names an
69
70
  * instance member or one of the second.
70
71
  */
71
- function candidatesOf(container, isStatic) {
72
- const exported = [...container.getExports().values()];
72
+ function candidatesOf(project, container, isStatic) {
73
+ const exported = [...must(project.queries.exportsOf(container)).values()];
73
74
  if (isStatic) {
74
75
  return exported.filter(isClassMember);
75
76
  }
76
77
  const isClass = (container.flags & SymbolFlags.Class) !== 0;
77
78
  return [
78
- ...container.getMembers().values(),
79
+ ...must(project.queries.membersOf(container)).values(),
79
80
  ...exported.filter((member) => !isClass || !isClassMember(member)),
80
81
  ];
81
82
  }
82
83
  /** The member of one container a path component spells, followed through any alias. */
83
84
  function memberOf(project, container, component, isStatic) {
84
- const member = candidatesOf(container, isStatic).find((candidate) => nameComponent(candidate.name) === component);
85
+ const member = candidatesOf(project, container, isStatic).find((candidate) => nameComponent(candidate.name) === component);
85
86
  return member === undefined ? undefined : unaliased(project, member);
86
87
  }
87
88
  /** The declaration a path names from one starting symbol, component by component. */
@@ -131,7 +132,7 @@ function modulesNamed(project, specifiers) {
131
132
  file.forEachChild(visit);
132
133
  }
133
134
  const specified = [...written];
134
- const symbols = project.symbolsAt(specified.map(([, node]) => project.handle(node)));
135
+ const symbols = project.symbolsAt(specified.map(([, node]) => project.handle(node))).map(must);
135
136
  const modules = new Map();
136
137
  specified.forEach(([name], index) => {
137
138
  const symbol = symbols[index];
@@ -171,7 +172,7 @@ export function resolveEntries(project, held, entries) {
171
172
  start = module === undefined ? undefined : memberOf(project, module, first, false);
172
173
  }
173
174
  else if (nameComponent(first) === first) {
174
- const global = project.checker.resolveName(first, ANY_MEANING, undefined, false);
175
+ const global = must(project.queries.resolveName(first, ANY_MEANING, undefined, false));
175
176
  start = global === undefined ? undefined : unaliased(project, global);
176
177
  }
177
178
  const declared = walkPath(project, start, rest, isStatic);
@@ -247,9 +248,21 @@ export function configuredDeclarations(config) {
247
248
  ...config.ts.serializers.map((entry) => ({ key: "ts.serializers", entry })),
248
249
  ];
249
250
  }
250
- /** Whether each configured entry names a declaration in one project, in the entries' order. */
251
+ /**
252
+ * Whether each configured entry names a declaration in one project, in the entries'
253
+ * order. Where the checker leaves a question of the resolution unanswered, every entry
254
+ * counts as naming one, so no entry is reported as naming nothing on a gap.
255
+ */
251
256
  export function declarationsNamed(project, held, configured) {
252
- return resolveEntries(project, held, configured.map((one) => one.entry)).map(namesADeclaration);
257
+ try {
258
+ return resolveEntries(project, held, configured.map((one) => one.entry)).map(namesADeclaration);
259
+ }
260
+ catch (error) {
261
+ if (!(error instanceof Unanswerable)) {
262
+ throw error;
263
+ }
264
+ return configured.map(() => true);
265
+ }
253
266
  }
254
267
  /**
255
268
  * The reference a finding about a configured entry names it by, the entry as the
@@ -8,7 +8,7 @@
8
8
  import { isExportDeclaration, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isShorthandPropertyAssignment, isStringLiteral, isNoSubstitutionTemplateLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
9
9
  import { nodeKey, packageScope } from "./inventory.js";
10
10
  import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.js";
11
- import { DEFAULT_BATCH_CAP } from "./references.js";
11
+ import { UNANSWERED } from "./query.js";
12
12
  const MANIFEST = "package.json";
13
13
  const MODULES_DIR = "node_modules";
14
14
  const MODULES_SEGMENT = `/${MODULES_DIR}/`;
@@ -207,10 +207,11 @@ export function projectNeeds(project, held, host) {
207
207
  }
208
208
  };
209
209
  use([PROJECT_OWNER], packages);
210
+ let unanswered = false;
210
211
  for (const file of project.ownSourceFiles()) {
211
- fileNeeds(project, held, file, programPackages, use);
212
+ unanswered = fileNeeds(project, held, file, programPackages, use) || unanswered;
212
213
  }
213
- return { packages, uses };
214
+ return { packages, uses, unanswered };
214
215
  }
215
216
  /** The declarations of the inventory a use written at one node belongs to. */
216
217
  function ownersOf(file, held, node) {
@@ -235,6 +236,7 @@ function ownersOf(file, held, node) {
235
236
  }
236
237
  return [PROJECT_OWNER];
237
238
  }
239
+ /** Records the needs of one file, and answers whether a specifier of it went unanswered. */
238
240
  function fileNeeds(project, held, file, programPackages, use) {
239
241
  const specifiers = [...file.imports, ...file.moduleAugmentations].filter(isLiteralText);
240
242
  const bindings = [];
@@ -262,13 +264,8 @@ function fileNeeds(project, held, file, programPackages, use) {
262
264
  visit(file);
263
265
  }
264
266
  const asked = [...specifiers, ...bindings.map(({ node }) => node), ...candidates];
265
- const resolved = [];
266
- for (let at = 0; at < asked.length; at += DEFAULT_BATCH_CAP) {
267
- const handles = asked
268
- .slice(at, at + DEFAULT_BATCH_CAP)
269
- .map((node) => project.handle(node));
270
- resolved.push(...project.symbolsAt(handles));
271
- }
267
+ const handles = asked.map((node) => project.handle(node));
268
+ const resolved = project.symbolsAt(handles);
272
269
  const packagesOf = new Map();
273
270
  specifiers.forEach((specifier, index) => {
274
271
  const named = new Set();
@@ -276,7 +273,8 @@ function fileNeeds(project, held, file, programPackages, use) {
276
273
  if (byName !== undefined) {
277
274
  named.add(byName);
278
275
  }
279
- for (const declaration of resolved[index]?.declarations ?? []) {
276
+ const symbol = resolved[index];
277
+ for (const declaration of typeof symbol === "object" ? symbol.declarations : []) {
280
278
  const byPath = packageOfPath(declaration.path);
281
279
  if (byPath !== undefined) {
282
280
  named.add(byPath);
@@ -284,11 +282,20 @@ function fileNeeds(project, held, file, programPackages, use) {
284
282
  }
285
283
  packagesOf.set(specifier, [...named]);
286
284
  });
285
+ // A name the checker could not resolve counts as a use of every package a binding of
286
+ // that name brings in, so a gap never makes a dependency read as unused.
287
287
  const bySymbol = new Map();
288
- bindings.forEach(({ specifier }, index) => {
288
+ const byName = new Map();
289
+ const unresolvedNames = new Set();
290
+ bindings.forEach(({ node, specifier }, index) => {
291
+ const packages = packagesOf.get(specifier) ?? [];
292
+ byName.set(node.text, [...(byName.get(node.text) ?? []), ...packages]);
289
293
  const symbol = resolved[specifiers.length + index];
290
- if (symbol !== undefined) {
291
- bySymbol.set(symbol.id, packagesOf.get(specifier) ?? []);
294
+ if (symbol === UNANSWERED) {
295
+ unresolvedNames.add(node.text);
296
+ }
297
+ else if (symbol !== undefined) {
298
+ bySymbol.set(symbol.id, packages);
292
299
  }
293
300
  });
294
301
  for (const specifier of specifiers) {
@@ -304,7 +311,12 @@ function fileNeeds(project, held, file, programPackages, use) {
304
311
  if (parent !== undefined && isShorthandPropertyAssignment(parent) && parent.name === node) {
305
312
  symbol = project.shorthandValueAt(project.handle(node));
306
313
  }
307
- const named = symbol === undefined ? undefined : bySymbol.get(symbol.id);
314
+ const named = symbol === UNANSWERED || (unresolvedNames.has(node.text) && symbol === undefined)
315
+ ? byName.get(node.text)
316
+ : symbol === undefined
317
+ ? undefined
318
+ : (bySymbol.get(symbol.id) ??
319
+ (unresolvedNames.has(node.text) ? byName.get(node.text) : undefined));
308
320
  if (named !== undefined) {
309
321
  use(ownersOf(file, held, node), named);
310
322
  }
@@ -312,6 +324,7 @@ function fileNeeds(project, held, file, programPackages, use) {
312
324
  for (const reference of file.typeReferenceDirectives) {
313
325
  use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
314
326
  }
327
+ return resolved.slice(0, specifiers.length).includes(UNANSWERED);
315
328
  }
316
329
  /** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
317
330
  function valueEnd(text, at) {
@@ -470,12 +483,16 @@ export function dependenciesOf(host, targetRoot, perProject, candidates) {
470
483
  const needed = new Set();
471
484
  const uses = new Map();
472
485
  for (const project of perProject) {
473
- project.packages.forEach((one) => needed.add(one));
474
- for (const [id, used] of project.uses) {
486
+ // A specifier the checker left unanswered may resolve to any declared dependency,
487
+ // and what the configuration needs is never deleted, so it is needed there.
488
+ const unknown = project.unanswered ? declared.map((dependency) => dependency.name) : [];
489
+ const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)]];
490
+ for (const [id, used] of [...project.uses, ...held]) {
475
491
  const merged = uses.get(id) ?? new Set();
476
492
  used.forEach((one) => merged.add(one));
477
493
  uses.set(id, merged);
478
494
  }
495
+ [...project.packages, ...unknown].forEach((one) => needed.add(one));
479
496
  }
480
497
  const users = new Map();
481
498
  for (const used of uses.values()) {
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { isVariableStatement, SyntaxKind } from "@typescript/native/unstable/ast";
10
10
  import { nodeKey } from "./inventory.js";
11
- import { DEFAULT_BATCH_CAP } from "./references.js";
11
+ import { isAnswered, UNANSWERED } from "./query.js";
12
12
  /** The tag the TypeScript tools recognize as marking a declaration deprecated. */
13
13
  const DEPRECATED_TAG = "deprecated";
14
14
  /** Whether one node's own documentation carries the deprecation tag. */
@@ -21,15 +21,15 @@ function nameNode(node) {
21
21
  return node.name ?? node;
22
22
  }
23
23
  /**
24
- * The identifiers of every declaration of one project that carries the deprecation
25
- * marker, in the inventory's order.
24
+ * The declarations of one project that carry the deprecation marker.
26
25
  *
27
26
  * The checker is asked only for a symbol with a declaration in a file whose tree holds
28
27
  * the tag, so a project whose files hold none costs no round trip, and one that does
29
- * costs one batch per file and one request per symbol that could carry the tag.
28
+ * costs one batch per file and one for the tags of the symbols that could carry it.
30
29
  */
31
30
  export function deprecatedDeclarations(project, held) {
32
31
  const marked = new Set();
32
+ const unknown = new Set();
33
33
  const markedPaths = new Set();
34
34
  const declared = [];
35
35
  for (const file of project.ownSourceFiles()) {
@@ -56,28 +56,39 @@ export function deprecatedDeclarations(project, held) {
56
56
  declared.push(named);
57
57
  }
58
58
  if (markedPaths.size > 0) {
59
- const asked = new Map();
60
- const carriesTag = (symbol) => {
61
- const known = asked.get(symbol.id);
62
- if (known !== undefined) {
63
- return known;
59
+ const pairs = declared.flatMap((named) => {
60
+ const symbols = project.symbolsAt(named.map((one) => one.handle));
61
+ return named.flatMap((one, at) => {
62
+ const symbol = symbols[at];
63
+ if (symbol === UNANSWERED) {
64
+ unknown.add(one.id);
65
+ }
66
+ return typeof symbol === "object" &&
67
+ symbol.declarations.some((declaration) => markedPaths.has(declaration.path))
68
+ ? [{ id: one.id, symbol }]
69
+ : [];
70
+ });
71
+ });
72
+ const distinct = [...new Map(pairs.map((one) => [one.symbol.id, one.symbol])).values()];
73
+ const tags = project.queries.jsDocTags(distinct);
74
+ const unread = new Set(distinct.filter((_, at) => tags[at] === UNANSWERED).map((one) => one.id));
75
+ const carries = new Set(distinct.flatMap((symbol, at) => {
76
+ const read = tags[at];
77
+ return read !== undefined &&
78
+ isAnswered(read) &&
79
+ read.some((tag) => tag.name === DEPRECATED_TAG)
80
+ ? [symbol.id]
81
+ : [];
82
+ }));
83
+ for (const one of pairs) {
84
+ if (carries.has(one.symbol.id)) {
85
+ marked.add(one.id);
64
86
  }
65
- const carries = symbol.declarations.some((declaration) => markedPaths.has(declaration.path)) &&
66
- symbol.getJsDocTags(project.checker).some((tag) => tag.name === DEPRECATED_TAG);
67
- asked.set(symbol.id, carries);
68
- return carries;
69
- };
70
- for (const named of declared) {
71
- for (let start = 0; start < named.length; start += DEFAULT_BATCH_CAP) {
72
- const batch = named.slice(start, start + DEFAULT_BATCH_CAP);
73
- project.symbolsAt(batch.map((one) => one.handle)).forEach((symbol, at) => {
74
- const one = batch[at];
75
- if (symbol !== undefined && one !== undefined && carriesTag(symbol)) {
76
- marked.add(one.id);
77
- }
78
- });
87
+ if (unread.has(one.symbol.id)) {
88
+ unknown.add(one.id);
79
89
  }
80
90
  }
81
91
  }
82
- return held.symbols.filter((symbol) => marked.has(symbol.id)).map((symbol) => symbol.id);
92
+ const inOrder = (ids) => held.symbols.filter((symbol) => ids.has(symbol.id)).map((symbol) => symbol.id);
93
+ return { deprecated: inOrder(marked), unanswered: inOrder(unknown) };
83
94
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { isArrayLiteralExpression, isBinaryExpression, isForOfStatement, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isShorthandPropertyAssignment, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
9
9
  import { TypeFlags } from "@typescript/native/unstable/sync";
10
+ import { isAnswered, UNANSWERED } from "./query.js";
10
11
  /** The text a property name spells, or `undefined` for a computed name. */
11
12
  function nameText(node) {
12
13
  if (node === undefined) {
@@ -151,29 +152,22 @@ function sourceOf(target) {
151
152
  * places and a table answers every name of it.
152
153
  */
153
154
  export function destructuring(project, cap) {
154
- const checker = project.checker;
155
+ const queries = project.queries;
155
156
  const tables = new Map();
156
157
  let patternBatches = 0;
157
158
  let patternLookups = 0;
158
- const typesAt = (nodes) => {
159
- const types = [];
160
- for (let from = 0; from < nodes.length; from += cap) {
161
- types.push(...checker.getTypeAtLocation(nodes.slice(from, from + cap)));
162
- patternBatches += 1;
163
- }
164
- return types;
165
- };
166
159
  const tableOf = (type) => {
167
160
  const known = tables.get(type.id);
168
161
  if (known !== undefined) {
169
162
  return known;
170
163
  }
171
- const table = new Map();
164
+ let table = new Map();
172
165
  if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
173
166
  patternLookups += 1;
174
- for (const property of checker.getPropertiesOfType(type)) {
175
- table.set(property.name, property);
176
- }
167
+ const properties = queries.propertiesOf(type);
168
+ table = isAnswered(properties)
169
+ ? new Map(properties.map((property) => [property.name, property]))
170
+ : UNANSWERED;
177
171
  }
178
172
  tables.set(type.id, table);
179
173
  return table;
@@ -183,7 +177,7 @@ export function destructuring(project, cap) {
183
177
  return type;
184
178
  }
185
179
  patternLookups += 1;
186
- return checker.getNonNullableType(type);
180
+ return queries.nonNullable(type);
187
181
  };
188
182
  /**
189
183
  * The type one element of an array or tuple value has: a tuple's element at `index`,
@@ -193,33 +187,42 @@ export function destructuring(project, cap) {
193
187
  if (!type.isTypeReference()) {
194
188
  return undefined;
195
189
  }
196
- patternLookups += 1;
197
- if (checker.isTupleType(type)) {
190
+ if (type.isTupleType()) {
198
191
  if (index === undefined) {
199
192
  return undefined;
200
193
  }
201
194
  patternLookups += 1;
202
- return checker.getTypeArguments(type)[index];
195
+ const elements = queries.typeArguments(type);
196
+ return isAnswered(elements) ? elements[index] : UNANSWERED;
203
197
  }
204
198
  patternLookups += 1;
205
- if (!checker.isArrayType(type)) {
199
+ const array = queries.isArray(type);
200
+ if (!isAnswered(array)) {
201
+ return UNANSWERED;
202
+ }
203
+ if (!array) {
206
204
  return undefined;
207
205
  }
208
206
  patternLookups += 1;
209
- return checker.getTypeArguments(type)[0];
207
+ const elements = queries.typeArguments(type);
208
+ return isAnswered(elements) ? elements[0] : UNANSWERED;
210
209
  };
211
210
  const step = (type, next) => {
212
211
  const defined = definedOf(type);
213
- if (defined === undefined) {
214
- return undefined;
212
+ if (defined === undefined || defined === UNANSWERED) {
213
+ return defined;
215
214
  }
216
215
  if (next.kind === "property") {
217
- const property = tableOf(defined).get(next.name);
216
+ const table = tableOf(defined);
217
+ if (!isAnswered(table)) {
218
+ return UNANSWERED;
219
+ }
220
+ const property = table.get(next.name);
218
221
  if (property === undefined) {
219
222
  return undefined;
220
223
  }
221
224
  patternLookups += 1;
222
- return checker.getTypeOfSymbol(property);
225
+ return queries.typeOfSymbol(property);
223
226
  }
224
227
  return elementOf(defined, next.kind === "element" ? next.index : undefined);
225
228
  };
@@ -238,11 +241,12 @@ export function destructuring(project, cap) {
238
241
  asking.add(source.expression);
239
242
  }
240
243
  }
241
- const answered = new Map();
242
244
  const asked = [...asking];
243
- typesAt(asked).forEach((type, index) => {
245
+ patternBatches += Math.ceil(asked.length / cap);
246
+ const answered = new Map();
247
+ queries.typesAt(asked, cap).forEach((type, index) => {
244
248
  const node = asked[index];
245
- if (type !== undefined && node !== undefined) {
249
+ if (node !== undefined) {
246
250
  answered.set(node, type);
247
251
  }
248
252
  });
@@ -255,13 +259,17 @@ export function destructuring(project, cap) {
255
259
  }
256
260
  let type = answered.get(source.expression);
257
261
  for (const next of source.steps) {
258
- type = type === undefined ? undefined : step(type, next);
262
+ type = type === undefined || type === UNANSWERED ? type : step(type, next);
259
263
  }
260
- patternTypes.set(pattern, type === undefined ? undefined : definedOf(type));
264
+ patternTypes.set(pattern, type === undefined || type === UNANSWERED ? type : definedOf(type));
261
265
  }
262
266
  return reads.map((read) => {
263
267
  const type = patternTypes.get(read.pattern);
264
- return type === undefined ? undefined : tableOf(type).get(read.name);
268
+ if (type === undefined || type === UNANSWERED) {
269
+ return type;
270
+ }
271
+ const table = tableOf(type);
272
+ return isAnswered(table) ? table.get(read.name) : UNANSWERED;
265
273
  });
266
274
  };
267
275
  return {
@@ -19,14 +19,35 @@ export class DiscoveryError extends Error {
19
19
  this.diagnostics = diagnostics;
20
20
  }
21
21
  }
22
+ /** The category an error diagnostic carries, which the protocol sends as a number. */
23
+ const ERROR_CATEGORY = DiagnosticCategory.Error;
22
24
  /** Whether one diagnostic is an error, which is what fails a run closed. */
23
25
  export function isError(diagnostic) {
24
- return diagnostic.category === DiagnosticCategory.Error;
26
+ return diagnostic.category === ERROR_CATEGORY;
25
27
  }
26
- /** One diagnostic as a line naming its position, its code and its message. */
27
- export function renderDiagnostic(diagnostic) {
28
- const where = diagnostic.fileName ?? "<no file>";
29
- return `${where}:${String(diagnostic.pos)}: TS${String(diagnostic.code)}: ${diagnostic.text}`;
28
+ /**
29
+ * One diagnostic as a line naming its file, its line and column, its code and its
30
+ * message: `path:line:column: TScode: message`, the position left out where the
31
+ * diagnostic names none and the file where it names none. `display` spells the
32
+ * file's path; it answers the path itself where it is not given.
33
+ */
34
+ export function renderDiagnostic(located, display = (path) => path) {
35
+ const { diagnostic, at } = located;
36
+ const message = `TS${String(diagnostic.code)}: ${diagnostic.text}`;
37
+ if (diagnostic.fileName === undefined) {
38
+ return message;
39
+ }
40
+ const where = display(diagnostic.fileName);
41
+ return at === undefined
42
+ ? `${where}: ${message}`
43
+ : `${where}:${String(at.line)}:${String(at.column)}: ${message}`;
44
+ }
45
+ /** Each diagnostic with the line and column its offset falls on. */
46
+ function locatedIn(diagnostics, locate) {
47
+ return diagnostics.map((diagnostic) => ({
48
+ diagnostic,
49
+ at: diagnostic.fileName === undefined ? undefined : locate(diagnostic.fileName, diagnostic.pos),
50
+ }));
30
51
  }
31
52
  /**
32
53
  * Every error a project's three diagnostic sets carry, in the order the compiler
@@ -38,6 +59,32 @@ export function renderDiagnostic(diagnostic) {
38
59
  export function diagnosticErrors(sets) {
39
60
  return [...sets.configParsing, ...sets.syntactic, ...sets.semantic].filter(isError);
40
61
  }
62
+ /** The errors one open project carries, each located in the file it names. */
63
+ export function projectErrors(sets) {
64
+ const load = sets.configParsing.filter(isError);
65
+ return {
66
+ load: locatedIn(load, sets.locate),
67
+ check: load.length > 0 ? [] : locatedIn(diagnosticErrors(sets), sets.locate),
68
+ };
69
+ }
70
+ /**
71
+ * The first line of one error about the target, every mention of the target root
72
+ * spelled relative to it: a path below the root loses the root and its separator, and
73
+ * the root itself is `.`.
74
+ */
75
+ export function targetRelative(text, targetRoot) {
76
+ const root = targetRoot.endsWith("/") ? targetRoot.slice(0, -1) : targetRoot;
77
+ const [first = ""] = text.split(/\r?\n/u);
78
+ const spelled = first.split(`${root}/`).join("").split(root).join(".").trim();
79
+ return spelled === "" ? "the configuration could not be built" : spelled;
80
+ }
81
+ /**
82
+ * Whether one configuration is the analyzer's own guess: discovery derived it and the
83
+ * scope did not name it. A guess that does not load is dropped; any other ends the run.
84
+ */
85
+ export function isGuess(discovery, configFile) {
86
+ return discovery.derived && !discovery.fromScope.includes(configFile);
87
+ }
41
88
  function isRecord(value) {
42
89
  return typeof value === "object" && value !== null && !Array.isArray(value);
43
90
  }
@@ -130,9 +177,11 @@ function scopeConfigFiles(host, scope) {
130
177
  /**
131
178
  * Every configuration file reached from `pending`, in the order discovery reads them:
132
179
  * each once, each followed by the configurations it references, transitively. A
133
- * configuration the compiler cannot read ends the run naming it.
180
+ * configuration the compiler cannot read ends the run naming it, unless `unreadable`
181
+ * is given, which is then told the configuration and why, and the walk goes on
182
+ * without it.
134
183
  */
135
- function derivedFrom(engine, host, pending) {
184
+ function derivedFrom(engine, host, pending, unreadable) {
136
185
  const seen = new Set();
137
186
  const found = [];
138
187
  while (pending.length > 0) {
@@ -145,7 +194,14 @@ function derivedFrom(engine, host, pending) {
145
194
  continue;
146
195
  }
147
196
  seen.add(path);
148
- parseOrRefuse(engine, path, path);
197
+ const reason = unreadableReason(engine, path);
198
+ if (reason !== undefined) {
199
+ if (unreadable === undefined) {
200
+ throw new DiscoveryError(`${path} cannot be read as a compiler configuration: ${reason}`, []);
201
+ }
202
+ unreadable(path, reason);
203
+ continue;
204
+ }
149
205
  found.push(path);
150
206
  pending.push(...referencesOf(host, path));
151
207
  }
@@ -179,18 +235,25 @@ export function consumerProjects(engine, host, root) {
179
235
  function derivedId(targetRoot, configFile) {
180
236
  return relativePath(targetRoot, configFile) ?? configFile;
181
237
  }
238
+ /** Why the compiler cannot read one configuration file, or undefined where it can. */
239
+ function unreadableReason(engine, configFile) {
240
+ try {
241
+ engine.parseConfigFile(configFile);
242
+ return undefined;
243
+ }
244
+ catch (error) {
245
+ return error instanceof Error ? error.message : String(error);
246
+ }
247
+ }
182
248
  /**
183
249
  * Resolves one compiler configuration file, or ends the run naming it: a project
184
250
  * opened from a configuration the compiler cannot read would be analyzed with no
185
251
  * type information.
186
252
  */
187
253
  function parseOrRefuse(engine, configFile, named) {
188
- try {
189
- engine.parseConfigFile(configFile);
190
- }
191
- catch (error) {
192
- throw new DiscoveryError(`${named} cannot be read as a compiler configuration: ` +
193
- (error instanceof Error ? error.message : String(error)), []);
254
+ const reason = unreadableReason(engine, configFile);
255
+ if (reason !== undefined) {
256
+ throw new DiscoveryError(`${named} cannot be read as a compiler configuration: ${reason}`, []);
194
257
  }
195
258
  }
196
259
  /**
@@ -232,25 +295,41 @@ function declaredProjects(engine, host, targetRoot, entries) {
232
295
  "into the program that references it, so the matrix names it too", []);
233
296
  }
234
297
  }
235
- return { projects, configFiles: projects.map((project) => project.configFile), fromScope: [] };
298
+ return {
299
+ projects,
300
+ derived: false,
301
+ notBuilt: [],
302
+ configFiles: projects.map((project) => project.configFile),
303
+ fromScope: [],
304
+ };
236
305
  }
237
306
  /**
238
307
  * The projects a run derives where the matrix declares none, in the order discovery
239
308
  * reads them: the paths the scope names, then every compiler configuration under
240
309
  * the target root that is not inside an ignored directory, then the configurations
241
- * each of those references, transitively. Each configuration appears once.
310
+ * each of those references, transitively. Each configuration appears once. A
311
+ * configuration the scope names directly is the caller's own and ends the run where
312
+ * it cannot be read; one the walk found is dropped and named.
242
313
  */
243
314
  function derivedProjects(engine, host, scope) {
315
+ const targetRoot = scope.target.path;
244
316
  const fromScope = scopeConfigFiles(host, scope);
245
- const configFiles = derivedFrom(engine, host, [
246
- ...fromScope,
247
- ...configFilesUnder(host, scope.target.path),
248
- ]);
317
+ const notBuilt = [];
318
+ const configFiles = derivedFrom(engine, host, [...fromScope, ...configFilesUnder(host, targetRoot)], (configFile, reason) => {
319
+ if (fromScope.includes(configFile)) {
320
+ throw new DiscoveryError(`${configFile} cannot be read as a compiler configuration: ${reason}`, []);
321
+ }
322
+ notBuilt.push({
323
+ id: derivedId(targetRoot, configFile),
324
+ configFile,
325
+ error: targetRelative(reason, resolvePath(host.workingDirectory(), targetRoot)),
326
+ });
327
+ });
249
328
  const projects = configFiles.map((configFile) => ({
250
- id: derivedId(scope.target.path, configFile),
329
+ id: derivedId(targetRoot, configFile),
251
330
  configFile,
252
331
  }));
253
- return { projects, configFiles, fromScope };
332
+ return { projects, derived: true, notBuilt, configFiles, fromScope };
254
333
  }
255
334
  /**
256
335
  * Every project one run analyzes.
@@ -262,12 +341,10 @@ function derivedProjects(engine, host, scope) {
262
341
  * scope and the target tree. A platform entry names a configuration of another
263
342
  * language and plays no part here.
264
343
  *
265
- * A configuration file the compiler cannot read ends the run with the failure code,
266
- * so a project that would have been analyzed with no type information is never
267
- * analyzed at all, whether the matrix declared it or discovery derived it. What a
268
- * configuration's own contents say is refused once its project is open, by
269
- * {@link diagnosticErrors} over the three sets a program reports, because a
270
- * resolution carries no diagnostics.
344
+ * A configuration file the compiler cannot read is never analyzed. One the matrix or
345
+ * the scope names ends the run with the failure code; a guess is dropped and named in
346
+ * {@link Discovery.notBuilt}. What a configuration's own contents say is read once its
347
+ * project is open, by {@link diagnosticErrors}, because a resolution carries none.
271
348
  *
272
349
  * Nothing reads a build output: a project is the files its configuration names, so a
273
350
  * package whose sources ship as TypeScript is analyzed as it stands.
@@ -14,6 +14,7 @@ import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAss
14
14
  import { SymbolFlags } from "@typescript/native/unstable/sync";
15
15
  import { globExpression } from "./glob.js";
16
16
  import { dirnamePath, relativePath, resolvePath } from "./paths.js";
17
+ import { UNANSWERED } from "./query.js";
17
18
  /** The extensions the test runner and the browser-test runner load a configuration from. */
18
19
  const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
19
20
  /** The extensions the linter's flat configuration and the mutation tester's are loaded from. */
@@ -331,8 +332,14 @@ function workersOf(project, file, files) {
331
332
  const names = sites.flatMap((site) => site.platformNames);
332
333
  const symbols = project.symbolsAt(names.map((name) => project.handle(name)));
333
334
  const platform = new Set();
335
+ // A name the checker could not resolve is read as the platform's, so a worker it
336
+ // stands on is rooted rather than reported.
334
337
  names.forEach((name, index) => {
335
338
  const symbol = symbols[index];
339
+ if (symbol === UNANSWERED) {
340
+ platform.add(name);
341
+ return;
342
+ }
336
343
  if (symbol !== undefined &&
337
344
  (symbol.flags & SymbolFlags.Alias) === 0 &&
338
345
  symbol.declarations.length > 0 &&