@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
package/src/roots.ts CHANGED
@@ -15,6 +15,7 @@ import { globExpression } from "./glob.ts";
15
15
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
16
16
  import type { Manifest, ManifestEntry } from "./manifest.ts";
17
17
  import { byPosition } from "./position.ts";
18
+ import { UNANSWERED } from "./query.ts";
18
19
  import type { ProjectView } from "./session.ts";
19
20
  import { sourceFilesOf } from "./source-files.ts";
20
21
 
@@ -70,6 +71,12 @@ export interface Roots {
70
71
  * {@link unmatchedEverywhere} is what turns one into the other.
71
72
  */
72
73
  readonly unmatched: readonly string[];
74
+ /**
75
+ * Every declaration of the project, where the checker left the module or export table
76
+ * of an entry or a published file unanswered, and none otherwise: which of them that
77
+ * file exports is unknown.
78
+ */
79
+ readonly unanswered: readonly string[];
73
80
  }
74
81
 
75
82
  /** What decides the roots beside the declarations themselves. */
@@ -225,7 +232,7 @@ export function roots<Brand>(
225
232
  enter(point.file, point.rule, point.source, point.exports);
226
233
  }
227
234
 
228
- const exportsOf = exportTables(project, held, [
235
+ const { tables: exportsOf, answered } = exportTables(project, held, [
229
236
  ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
230
237
  ...published,
231
238
  ]);
@@ -258,47 +265,62 @@ export function roots<Brand>(
258
265
  return site || compare(a.kind, b.kind) || compare(a.source, b.source);
259
266
  });
260
267
 
261
- return { configFile: project.configFile, liveUnderReachability: ordered, unmatched };
268
+ return {
269
+ configFile: project.configFile,
270
+ liveUnderReachability: ordered,
271
+ unmatched,
272
+ unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
273
+ };
262
274
  }
263
275
 
264
276
  /**
265
277
  * Every declaration each file's module exports, by the file's name: the declaration
266
278
  * each name of the export table is, and every link of a re-export's chain. A name
267
- * whose declaration is outside the project's own files belongs to another program
268
- * and names nothing here.
269
- *
270
- * One batch resolves every file to its module and one request per module reads
271
- * its export table, star re-exports resolved into it. A file that is no module
272
- * exports nothing.
279
+ * whose declaration is outside the project's own files names nothing here. One batch
280
+ * resolves every file to its module and one request per module reads its export
281
+ * table, star re-exports resolved into it. A file that is no module exports nothing.
282
+ * `answered` is false where the checker left a table or a link of a chain unanswered.
273
283
  */
274
284
  function exportTables<Brand>(
275
285
  project: ProjectView<Brand>,
276
286
  held: Inventory,
277
287
  wanted: readonly SourceFile[],
278
- ): ReadonlyMap<string, readonly string[]> {
288
+ ): { readonly tables: ReadonlyMap<string, readonly string[]>; readonly answered: boolean } {
279
289
  const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
280
290
  const tables = new Map<string, readonly string[]>();
291
+ let answered = true;
281
292
  if (unique.length === 0) {
282
- return tables;
293
+ return { tables, answered };
283
294
  }
284
295
  const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
285
296
  const chains = aliasChains(project, held);
286
297
 
298
+ const topLevel = (file: SourceFile): readonly string[] => {
299
+ const fileId = held.declarations.get(nodeKey(file, file));
300
+ return held.symbols.filter((symbol) => symbol.parent === fileId).map((symbol) => symbol.id);
301
+ };
287
302
  unique.forEach((file, index) => {
288
303
  const module = modules[index];
289
304
  if (module === undefined) {
290
305
  tables.set(file.fileName, []);
291
306
  return;
292
307
  }
308
+ const table = module === UNANSWERED ? UNANSWERED : project.queries.exportsOfModule(module);
309
+ if (table === UNANSWERED) {
310
+ answered = false;
311
+ tables.set(file.fileName, topLevel(file));
312
+ return;
313
+ }
293
314
  const ids = new Set<string>();
294
- for (const exported of project.checker.getExportsOfModule(module)) {
315
+ for (const exported of table) {
295
316
  for (const target of chains.chainOf(exported)) {
317
+ answered &&= !target.guessed;
296
318
  ids.add(target.id);
297
319
  }
298
320
  }
299
321
  tables.set(file.fileName, [...ids]);
300
322
  });
301
- return tables;
323
+ return { tables, answered };
302
324
  }
303
325
 
304
326
  /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
package/src/schema.ts CHANGED
@@ -261,9 +261,9 @@ export function unimplementedKey(label: string, path: string): ConfigError {
261
261
  const nearest = nearestKey(path);
262
262
  let hint = "";
263
263
  if (nearest === path) {
264
- hint = `; ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
264
+ hint = `. ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
265
265
  } else if (nearest !== "") {
266
- hint = `; the nearest implemented key is ${JSON.stringify(nearest)}`;
266
+ hint = `. The nearest implemented key is ${JSON.stringify(nearest)}`;
267
267
  }
268
268
  return new ConfigError(
269
269
  "unimplemented-key",
@@ -57,6 +57,7 @@ import {
57
57
  import type { DetectorInput, Evidence } from "./exempt.ts";
58
58
  import { nodeKey, type Inventory } from "./inventory.ts";
59
59
  import { renderPosition } from "./position.ts";
60
+ import { must } from "./query.ts";
60
61
  import type { ProjectView } from "./session.ts";
61
62
  import { valueReach, type Reached } from "./value-reach.ts";
62
63
 
@@ -316,12 +317,13 @@ function erasedParameters<Brand>(
316
317
  if (!parameter.rest) {
317
318
  return (type.flags & ERASED) !== 0;
318
319
  }
319
- const [element] = type.isTypeReference() ? project.checker.getTypeArguments(type) : [];
320
+ const [element] = must(project.queries.typeArguments(type));
320
321
  return element !== undefined && (element.flags & ERASED) !== 0;
321
322
  });
322
323
  const bySymbol = new Map<number, Parameter>();
323
324
  project
324
325
  .symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
326
+ .map(must)
325
327
  .forEach((symbol, index) => {
326
328
  const parameter = erased[index];
327
329
  if (symbol !== undefined && parameter !== undefined) {
@@ -347,11 +349,11 @@ function handedOn<Brand>(
347
349
  ),
348
350
  );
349
351
  const plain = candidates.filter((one) => !one.value.shorthand);
350
- const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
352
+ const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name))).map(must);
351
353
  const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
352
354
  return candidates.flatMap((one) => {
353
355
  const symbol = one.value.shorthand
354
- ? project.shorthandValueAt(project.handle(one.value.name))
356
+ ? must(project.shorthandValueAt(project.handle(one.value.name)))
355
357
  : symbolOf.get(one);
356
358
  const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
357
359
  return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
@@ -382,8 +384,8 @@ function destinations<Brand>(
382
384
  walked: Walked,
383
385
  ): Destinations {
384
386
  const { project, held } = input;
385
- const checker = project.checker;
386
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
387
+ const queries = project.queries;
388
+ const own = project.ownPaths();
387
389
  const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(
388
390
  namesADeclaration,
389
391
  );
@@ -422,7 +424,7 @@ function destinations<Brand>(
422
424
  form !== undefined &&
423
425
  method !== undefined &&
424
426
  outside(method) &&
425
- FUNCTION_INTERFACES.has(method.getParent()?.name ?? "")
427
+ FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")
426
428
  ) {
427
429
  return form.invocation;
428
430
  }
@@ -439,16 +441,16 @@ function destinations<Brand>(
439
441
  const invoked = invocation(call);
440
442
  // Through a method of the prototype the resolved signature is the method's, so
441
443
  // the parameters are read off the callee's own signatures, any one erasing.
442
- const type = invoked.indirect ? checker.getTypeAtLocation(invoked.callee) : undefined;
444
+ const type = invoked.indirect ? must(queries.typeAt(invoked.callee)) : undefined;
443
445
  const signatures = invoked.indirect
444
446
  ? type === undefined
445
447
  ? []
446
- : checker.getSignaturesOfType(type, SignatureKind.Call)
447
- : [checker.getResolvedSignature(call)].filter((one) => one !== undefined);
448
+ : must(queries.signaturesOf(type, SignatureKind.Call))
449
+ : [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
448
450
  indexes = new Set(
449
451
  invoked.arguments.flatMap((_unused, index) =>
450
452
  signatures.some((signature) => {
451
- const parameter = checker.getParameterType(signature, index);
453
+ const parameter = must(queries.parameterType(signature, index));
452
454
  return parameter !== undefined && (parameter.flags & ERASED) !== 0;
453
455
  })
454
456
  ? [index]
package/src/session.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
2
  import type {
3
- Checker,
3
+ APIRequestGenerator,
4
4
  Diagnostic,
5
5
  DocumentIdentifier,
6
6
  NodeHandle,
@@ -10,9 +10,15 @@ import type {
10
10
  Symbol as TSSymbol,
11
11
  TimingInfo,
12
12
  } from "@typescript/native/unstable/sync";
13
- import { isIdentifier } from "@typescript/native/unstable/ast";
14
13
  import type { Node, SourceFile } from "@typescript/native/unstable/ast";
15
14
  import { DiscoveryError } from "./discover.ts";
15
+ import {
16
+ queriesOf,
17
+ UnansweredLog,
18
+ type Answer,
19
+ type Queries,
20
+ type UnansweredQuestion,
21
+ } from "./query.ts";
16
22
 
17
23
  /**
18
24
  * The compiler the analysis reads through. It is a client over a protocol to a
@@ -31,7 +37,15 @@ export interface Engine {
31
37
  */
32
38
  parseConfigFile(file: DocumentIdentifier): ReturnType<API["parseConfigFile"]>;
33
39
  /** Opens one snapshot holding every project named. */
34
- updateSnapshot(openProjects: readonly string[]): Snapshot;
40
+ createSnapshot(openProjects: readonly string[]): Snapshot;
41
+ /** Sends many questions in one round trip and answers each, in order. */
42
+ batch<T>(questions: readonly APIRequestGenerator<T>[]): T[];
43
+ /**
44
+ * Puts one question to a project's checker, by the accessor's name and the locations
45
+ * it asks about. Every guarded question passes through here, so a caller that
46
+ * observes or fails questions does it at this seam.
47
+ */
48
+ ask<T>(accessor: string, locations: () => readonly string[], question: () => T): T;
35
49
  /** Round-trip latency, bytes transferred and server time, when collection is on. */
36
50
  getTimingInfo(): TimingInfo;
37
51
  /** Releases the client and the process behind it. */
@@ -56,7 +70,10 @@ export function openEngine(options: EngineOptions): Engine {
56
70
  let closed = false;
57
71
  return {
58
72
  parseConfigFile: (file) => api.parseConfigFile(file),
59
- updateSnapshot: (openProjects) => api.updateSnapshot({ openProjects: [...openProjects] }),
73
+ createSnapshot: (openProjects) => api.createSnapshot({ openProjects: [...openProjects] }),
74
+ batch: <T>(questions: readonly APIRequestGenerator<T>[]) =>
75
+ api.batch<readonly APIRequestGenerator<T>[]>(...questions),
76
+ ask: (_accessor, _locations, question) => question(),
60
77
  getTimingInfo: () => api.getTimingInfo(),
61
78
  close: () => {
62
79
  if (closed) {
@@ -90,38 +107,45 @@ export interface ProjectView<Brand> {
90
107
  readonly configFile: string;
91
108
  /** The project's program, whose walk is local to this process. */
92
109
  readonly program: Program;
93
- /** The project's checker. Every call on it is a round trip. */
94
- readonly checker: Checker;
110
+ /**
111
+ * The questions the project's checker answers, each guarded. Every one is a round
112
+ * trip, and one may go unanswered.
113
+ */
114
+ readonly queries: Queries;
95
115
  /** The source files the program holds that are the target's own. */
96
116
  ownSourceFiles(): readonly SourceFile[];
117
+ /**
118
+ * The keys of the target's own files, which is how a declaration handle names the
119
+ * file it is in.
120
+ */
121
+ ownPaths(): ReadonlySet<string>;
97
122
  /** A handle to one node of this project's program. */
98
123
  handle(node: Node): Handle<Brand>;
99
124
  /**
100
- * The symbols a batch of this project's nodes resolve to, one round trip for
101
- * the batch, in the order the handles were given. The batch is how a file's
102
- * identifiers reach the checker, and `undefined` marks a node the batch did not
103
- * resolve.
125
+ * The symbols a batch of this project's nodes resolve to, in the order the handles
126
+ * were given, in runs of at most `cap`. The batch is how a file's identifiers reach
127
+ * the checker, and `undefined` marks a node the batch did not resolve.
104
128
  */
105
- symbolsAt(handles: readonly Handle<Brand>[]): readonly (TSSymbol | undefined)[];
129
+ symbolsAt(handles: readonly Handle<Brand>[], cap?: number): Answer<TSSymbol | undefined>[];
106
130
  /**
107
131
  * The symbol one node resolves to, asked for that node alone. The accessor behind
108
- * it takes one identifier and offers no array overload, so it is one round trip per
109
- * node and it answers only for the residue a batch left unresolved. A node that is
110
- * not an identifier has no such answer.
132
+ * it takes one identifier and offers no array form, so it answers only for the
133
+ * residue a batch left unresolved. A node that is not an identifier has no such
134
+ * answer.
111
135
  */
112
- resolvedSymbolAt(handle: Handle<Brand>): TSSymbol | undefined;
136
+ resolvedSymbolAt(handle: Handle<Brand>): Answer<TSSymbol | undefined>;
113
137
  /**
114
138
  * The symbol the value of one shorthand property assignment names, which is the
115
139
  * declaration `{ a }` reads; the symbol the property's own name resolves to is the
116
140
  * property the literal declares. The handle names the assignment, not its name.
117
141
  */
118
- shorthandValueAt(handle: Handle<Brand>): TSSymbol | undefined;
142
+ shorthandValueAt(handle: Handle<Brand>): Answer<TSSymbol | undefined>;
119
143
  /**
120
144
  * The symbol one alias names, one link along: an import or export specifier
121
145
  * resolves to the declaration it carries forward, or to the next alias of a chain.
122
146
  * `undefined` where the symbol is no alias, or where the alias resolves to nothing.
123
147
  */
124
- aliasStepOf(symbol: TSSymbol): TSSymbol | undefined;
148
+ aliasStepOf(symbol: TSSymbol): Answer<TSSymbol | undefined>;
125
149
  /**
126
150
  * The node one symbol's declaration handle names, read in this project's program,
127
151
  * or `undefined` where this project's program does not hold that file.
@@ -142,49 +166,70 @@ export interface ProjectView<Brand> {
142
166
  */
143
167
  export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
144
168
 
145
- function viewOf<Brand>(project: Project): ProjectView<Brand> {
169
+ function viewOf<Brand>(
170
+ project: Project,
171
+ configFile: string,
172
+ engine: Engine,
173
+ log: UnansweredLog,
174
+ ): ProjectView<Brand> {
175
+ const queries = queriesOf(
176
+ project.checker,
177
+ configFile,
178
+ (questions) => engine.batch(questions),
179
+ log,
180
+ (accessor, locations, question) => engine.ask(accessor, locations, question),
181
+ );
146
182
  // The snapshot is never updated, so a project's own file set is the same answer
147
183
  // every time it is asked for. It is read once because the question is not free:
148
184
  // deciding it consults the metadata of every file the program holds, the compiler's
149
185
  // own library files included, and a run asks for the set once per pass.
150
186
  let ownFiles: readonly SourceFile[] | undefined;
187
+ let ownKeys: ReadonlySet<string> | undefined;
188
+ const ownSourceFiles = (): readonly SourceFile[] => {
189
+ ownFiles ??= project.program
190
+ .getSourceFileNames()
191
+ // The metadata decides which files belong to the target, and it is read by
192
+ // name. Fetching each file first and filtering afterwards costs one round
193
+ // trip per library file the answer then discards, and a program holds far
194
+ // more library files than target files.
195
+ .filter((name) => {
196
+ const metadata = project.program.getSourceFileMetadata(name);
197
+ return (
198
+ metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary
199
+ );
200
+ })
201
+ .map((name) => project.program.getSourceFile(name))
202
+ .filter((file): file is SourceFile => file !== undefined);
203
+ return ownFiles;
204
+ };
151
205
  return {
152
- configFile: project.configFileName,
206
+ configFile,
153
207
  program: project.program,
154
- checker: project.checker,
155
- ownSourceFiles: () => {
156
- ownFiles ??= project.program
157
- .getSourceFileNames()
158
- // The metadata decides which files belong to the target, and it is read by
159
- // name. Fetching each file first and filtering afterwards costs one round
160
- // trip per library file the answer then discards, and a program holds far
161
- // more library files than target files.
162
- .filter((name) => {
163
- const metadata = project.program.getSourceFileMetadata(name);
164
- return (
165
- metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary
166
- );
167
- })
168
- .map((name) => project.program.getSourceFile(name))
169
- .filter((file): file is SourceFile => file !== undefined);
170
- return ownFiles;
208
+ queries,
209
+ ownSourceFiles,
210
+ ownPaths: () => {
211
+ ownKeys ??= new Set(ownSourceFiles().map((file) => file.path));
212
+ return ownKeys;
171
213
  },
172
214
  handle: (node) => ({ node }),
173
- symbolsAt: (handles) => project.checker.getSymbolAtLocation(handles.map(({ node }) => node)),
174
- resolvedSymbolAt: ({ node }) =>
175
- isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
176
- shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
215
+ symbolsAt: (handles, cap) =>
216
+ queries.symbolsAt(
217
+ handles.map(({ node }) => node),
218
+ cap,
219
+ ),
220
+ resolvedSymbolAt: ({ node }) => queries.resolvedSymbol(node),
221
+ shorthandValueAt: ({ node }) => queries.shorthandValue(node),
177
222
  aliasStepOf: (symbol) => {
178
223
  // The step is asked of an alias only: asked of any other symbol, the compiler
179
224
  // fails an assertion and the session ends.
180
225
  if ((symbol.flags & SymbolFlags.Alias) === 0) {
181
226
  return undefined;
182
227
  }
183
- const held = project.checker.getImmediateAliasedSymbol(symbol);
228
+ const held = queries.immediateAliased(symbol);
184
229
  // An alias that resolves to nothing answers with the checker's own unknown
185
230
  // symbol, which declares nothing; a symbol that is no alias answers with
186
231
  // nothing at all.
187
- return held === undefined || held.declarations.length === 0 ? undefined : held;
232
+ return typeof held === "object" && held.declarations.length === 0 ? undefined : held;
188
233
  },
189
234
  declarationAt: (handle) => {
190
235
  const node = handle.resolve(project);
@@ -203,11 +248,17 @@ export function narrowedTo<Brand>(
203
248
  keep: (file: SourceFile) => boolean,
204
249
  ): ProjectView<Brand> {
205
250
  let kept: readonly SourceFile[] | undefined;
251
+ let keys: ReadonlySet<string> | undefined;
252
+ const ownSourceFiles = (): readonly SourceFile[] => {
253
+ kept ??= project.ownSourceFiles().filter(keep);
254
+ return kept;
255
+ };
206
256
  return {
207
257
  ...project,
208
- ownSourceFiles: () => {
209
- kept ??= project.ownSourceFiles().filter(keep);
210
- return kept;
258
+ ownSourceFiles,
259
+ ownPaths: () => {
260
+ keys ??= new Set(ownSourceFiles().map((file) => file.path));
261
+ return keys;
211
262
  },
212
263
  };
213
264
  }
@@ -218,6 +269,11 @@ export interface ProjectDiagnostics {
218
269
  readonly syntactic: readonly Diagnostic[];
219
270
  readonly semantic: readonly Diagnostic[];
220
271
  readonly configParsing: readonly Diagnostic[];
272
+ /** The line and column of one offset into one of the program's files, counted from one. */
273
+ readonly locate: (
274
+ fileName: string,
275
+ offset: number,
276
+ ) => { line: number; column: number } | undefined;
221
277
  }
222
278
 
223
279
  /** The three diagnostic sets one project carries, read before any analysis. */
@@ -227,15 +283,31 @@ export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagno
227
283
  syntactic: project.program.getSyntacticDiagnostics(),
228
284
  semantic: project.program.getSemanticDiagnostics(),
229
285
  configParsing: project.program.getConfigFileParsingDiagnostics(),
286
+ locate: (fileName, offset) => {
287
+ const file = project.program.getSourceFile(fileName);
288
+ if (file === undefined || offset < 0 || offset > file.text.length) {
289
+ return undefined;
290
+ }
291
+ const { line, character } = file.getLineAndCharacterOfPosition(offset);
292
+ return { line: line + 1, column: character + 1 };
293
+ },
230
294
  };
231
295
  }
232
296
 
233
297
  /** One run's result: what the visitor answered for each project, and what it cost. */
234
298
  export interface SessionResult<Result> {
299
+ /** One answer per configuration the snapshot opened a project for, in the order named. */
235
300
  readonly projects: readonly Result[];
301
+ /** The configurations the snapshot opened no project for, in the order named. */
302
+ readonly unopened: readonly string[];
236
303
  readonly timing: TimingInfo;
304
+ /** Every question the checker could not answer, each once, in the order first asked. */
305
+ readonly unanswered: readonly UnansweredQuestion[];
237
306
  }
238
307
 
308
+ /** What a run does about a configuration the snapshot opened no project for. */
309
+ type Unopened = "refuse" | "record";
310
+
239
311
  /**
240
312
  * Runs `visit` over every project the compiler configurations name, inside one
241
313
  * snapshot of one client, in the order they are named, and answers in that order.
@@ -244,30 +316,46 @@ export interface SessionResult<Result> {
244
316
  * with its own program and checker; it is never updated, because the analysis
245
317
  * reads and the tree does not change under it. The order the snapshot lists its
246
318
  * projects in is the compiler's own, so each named configuration is looked up in it
247
- * instead, and a configuration the snapshot opened no project for ends the run: the
248
- * run would otherwise answer for fewer projects than it was asked to analyze. The
249
- * snapshot is disposed once when the last project has been visited, and the client
250
- * is released after it, whether the visit completed or threw.
319
+ * instead. A configuration the snapshot opened no project for ends the run under
320
+ * `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
321
+ * the last project has been visited, and the client is released after it, whether the
322
+ * visit completed or threw.
251
323
  */
252
324
  export function runSession<Result>(
253
325
  engine: Engine,
254
326
  configFiles: readonly string[],
255
327
  visit: ProjectVisitor<Result>,
328
+ unopened: Unopened = "refuse",
256
329
  ): SessionResult<Result> {
257
330
  try {
258
- const snapshot = engine.updateSnapshot(configFiles);
331
+ const snapshot = engine.createSnapshot(configFiles);
259
332
  try {
260
- const opened = new Map(
261
- snapshot.getProjects().map((project) => [project.configFileName, project]),
262
- );
263
- const projects = configFiles.map((configFile) => {
333
+ const opened = new Map<string, Project>();
334
+ for (const project of snapshot.getProjects()) {
335
+ if (project.configFileName !== undefined) {
336
+ opened.set(project.configFileName, project);
337
+ }
338
+ }
339
+ const log = new UnansweredLog();
340
+ const projects: Result[] = [];
341
+ const missing: string[] = [];
342
+ for (const configFile of configFiles) {
264
343
  const project = opened.get(configFile);
265
344
  if (project === undefined) {
266
- throw new DiscoveryError(`${configFile} opened no project`, []);
345
+ if (unopened === "refuse") {
346
+ throw new DiscoveryError(`${configFile} opened no project`, []);
347
+ }
348
+ missing.push(configFile);
349
+ continue;
267
350
  }
268
- return visit(viewOf(project));
269
- });
270
- return { projects, timing: engine.getTimingInfo() };
351
+ projects.push(visit(viewOf(project, configFile, engine, log)));
352
+ }
353
+ return {
354
+ projects,
355
+ unopened: missing,
356
+ timing: engine.getTimingInfo(),
357
+ unanswered: log.questions,
358
+ };
271
359
  } finally {
272
360
  snapshot.dispose();
273
361
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { Host } from "./host.ts";
9
9
  import type { InventorySymbol } from "./inventory.ts";
10
+ import { jsonChunks } from "./json-chunks.ts";
10
11
  import { joinPath } from "./paths.ts";
11
12
  import { positionAt, walkDocument } from "./json-document.ts";
12
13
  import type { Position } from "./position.ts";
@@ -292,7 +293,10 @@ export function rowReason(provenance: Provenance): string {
292
293
  * identity missing its name or version, are refused rather than written, because the
293
294
  * grammar requires a reason on every row.
294
295
  */
295
- export function writeBaseline(findings: readonly Recorded[], provenance: Provenance): string {
296
+ export function writeBaseline(
297
+ findings: readonly Recorded[],
298
+ provenance: Provenance,
299
+ ): Iterable<string> {
296
300
  if (provenance.analyzer === "" || provenance.version === "") {
297
301
  throw new Error(
298
302
  `a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`,
@@ -307,5 +311,5 @@ export function writeBaseline(findings: readonly Recorded[], provenance: Provena
307
311
  }
308
312
  return { code: found.code, symbol: found.symbol, path: found.path, reason };
309
313
  });
310
- return `${JSON.stringify({ description: BASELINE_DESCRIPTION, baseline }, null, 2)}\n`;
314
+ return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
311
315
  }
package/src/sweep.ts CHANGED
@@ -51,6 +51,11 @@ export interface SweepInput {
51
51
  * holds nothing back.
52
52
  */
53
53
  readonly exempt?: readonly Exemption[];
54
+ /**
55
+ * The declarations a question the checker did not answer could have kept live. Each
56
+ * is held as an exempt declaration is. Absent, the sweep holds nothing back for one.
57
+ */
58
+ readonly unanswered?: readonly string[];
54
59
  readonly mode: Mode;
55
60
  }
56
61
 
@@ -94,10 +99,10 @@ export interface Liveness {
94
99
  */
95
100
  export function sweep(graph: Graph, input: SweepInput): Liveness {
96
101
  const marked = flagged(graph, input.marked);
97
- const exempt = flagged(
98
- graph,
99
- (input.exempt ?? []).map((record) => record.id),
100
- );
102
+ const exempt = flagged(graph, [
103
+ ...(input.exempt ?? []).map((record) => record.id),
104
+ ...(input.unanswered ?? []),
105
+ ]);
101
106
  const held = marked.map((mark, at) => mark || exempt[at] === true);
102
107
  // Every root, every mark and every exemption seeds reachability. A root of a kind that
103
108
  // names a caller, and a mark, are live under reference counting as well; a root that