@cplieger/deadset-ts 4.0.1 → 5.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 (117) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +229 -36
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +186 -0
  10. package/dist/src/conformance-record.js +2 -2
  11. package/dist/src/convention-rows.js +383 -0
  12. package/dist/src/conventions.js +456 -0
  13. package/dist/src/dependencies.js +30 -3
  14. package/dist/src/discover.js +10 -3
  15. package/dist/src/emit-map.js +72 -0
  16. package/dist/src/entry-point-gaps.js +81 -35
  17. package/dist/src/entry-points.js +47 -32
  18. package/dist/src/exempt.js +11 -3
  19. package/dist/src/findings/completion.js +19 -29
  20. package/dist/src/findings/emitters.js +22 -1
  21. package/dist/src/findings/unused-declarations.js +7 -2
  22. package/dist/src/graph.js +13 -5
  23. package/dist/src/inventory.js +4 -2
  24. package/dist/src/kinds.js +2 -2
  25. package/dist/src/manifest.js +37 -11
  26. package/dist/src/matrix.js +21 -13
  27. package/dist/src/partial.js +3 -1
  28. package/dist/src/pnpm-workspace.js +120 -0
  29. package/dist/src/position.js +17 -2
  30. package/dist/src/print.js +2 -0
  31. package/dist/src/references.js +209 -30
  32. package/dist/src/report.js +26 -6
  33. package/dist/src/reporters/annotations.js +10 -3
  34. package/dist/src/reporters/template.js +3 -0
  35. package/dist/src/resolve.js +31 -6
  36. package/dist/src/roots.js +37 -8
  37. package/dist/src/run.js +13 -3
  38. package/dist/src/schema.js +2 -0
  39. package/dist/src/session.js +141 -34
  40. package/dist/src/setup-failure.js +23 -0
  41. package/dist/src/source-files.js +4 -61
  42. package/dist/src/sweep.js +4 -2
  43. package/dist/src/template-field.js +38 -6
  44. package/dist/src/test-support.js +59 -0
  45. package/dist/src/type-errors.js +252 -0
  46. package/dist/src/type-query-alias.js +92 -0
  47. package/dist/src/verbs/analyze.js +24 -4
  48. package/dist/src/verbs/explain.js +4 -1
  49. package/dist/src/verbs/print-projects.js +17 -56
  50. package/dist/src/verbs/print-retained.js +7 -2
  51. package/dist/src/verbs/print-roots.js +7 -2
  52. package/dist/src/version-range.js +255 -0
  53. package/dist/src/version.js +1 -1
  54. package/dist/src/workspace-manifests.js +68 -0
  55. package/dist/src/workspace-programs.js +342 -0
  56. package/dist/src/workspace-resolution.js +307 -0
  57. package/dist/src/workspace-run.js +75 -0
  58. package/dist/src/workspace.js +358 -0
  59. package/jsr.json +1 -1
  60. package/package.json +2 -1
  61. package/src/analysis.ts +394 -70
  62. package/src/component-files.ts +718 -0
  63. package/src/component-layer.ts +160 -0
  64. package/src/config.ts +11 -2
  65. package/src/configuration-files.ts +235 -0
  66. package/src/conformance-record.ts +2 -2
  67. package/src/convention-rows.ts +440 -0
  68. package/src/conventions.ts +640 -0
  69. package/src/dependencies.ts +36 -2
  70. package/src/discover.ts +10 -6
  71. package/src/emit-map.ts +93 -0
  72. package/src/entry-point-gaps.ts +92 -38
  73. package/src/entry-points.ts +66 -36
  74. package/src/exempt.ts +21 -3
  75. package/src/findings/completion.ts +24 -33
  76. package/src/findings/emitters.ts +32 -1
  77. package/src/findings/unused-declarations.ts +9 -2
  78. package/src/graph.ts +15 -4
  79. package/src/host.ts +23 -0
  80. package/src/index.ts +1 -1
  81. package/src/inventory.ts +13 -2
  82. package/src/kinds.ts +2 -2
  83. package/src/manifest.ts +47 -14
  84. package/src/matrix.ts +43 -15
  85. package/src/partial.ts +6 -1
  86. package/src/pnpm-workspace.ts +135 -0
  87. package/src/position.ts +23 -2
  88. package/src/print.ts +2 -0
  89. package/src/references.ts +227 -28
  90. package/src/report.ts +56 -6
  91. package/src/reporters/annotations.ts +12 -3
  92. package/src/reporters/template.ts +3 -0
  93. package/src/resolve.ts +59 -6
  94. package/src/roots.ts +60 -9
  95. package/src/run.ts +15 -5
  96. package/src/schema.ts +2 -0
  97. package/src/session.ts +218 -37
  98. package/src/setup-failure.ts +39 -0
  99. package/src/source-files.ts +4 -66
  100. package/src/sweep.ts +9 -2
  101. package/src/template-field.ts +42 -7
  102. package/src/test-support.ts +77 -0
  103. package/src/type-errors.ts +340 -0
  104. package/src/type-query-alias.ts +133 -0
  105. package/src/verbs/analyze.ts +38 -4
  106. package/src/verbs/explain.ts +4 -1
  107. package/src/verbs/print-projects.ts +17 -69
  108. package/src/verbs/print-retained.ts +7 -2
  109. package/src/verbs/print-roots.ts +7 -2
  110. package/src/verbs/verb.ts +6 -3
  111. package/src/version-range.ts +309 -0
  112. package/src/version.ts +1 -1
  113. package/src/workspace-manifests.ts +100 -0
  114. package/src/workspace-programs.ts +486 -0
  115. package/src/workspace-resolution.ts +393 -0
  116. package/src/workspace-run.ts +148 -0
  117. package/src/workspace.ts +463 -0
package/src/analysis.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * project's checker, and that holds only for views the session itself hands out.
9
9
  */
10
10
 
11
+ import { lineOf, readComponent } from "./component-files.ts";
11
12
  import type { Config, Provenance } from "./config.ts";
12
13
  import {
13
14
  consumerLoads,
@@ -33,12 +34,19 @@ import {
33
34
  type LocatedDiagnostic,
34
35
  type NotBuilt,
35
36
  } from "./discover.ts";
37
+ import {
38
+ conventionFailuresFor,
39
+ readConventions,
40
+ type AppliedConvention,
41
+ type ConventionManifest,
42
+ } from "./conventions.ts";
36
43
  import { readEdgeSides } from "./edges.ts";
37
44
  import { enumGroup } from "./enum-group.ts";
38
45
  import {
39
46
  computeExemptions,
40
47
  disabledClasses,
41
48
  exemptionsOf,
49
+ holdingWhile,
42
50
  isUnansweredRecord,
43
51
  retainedIn,
44
52
  type Detector,
@@ -63,6 +71,7 @@ import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-fun
63
71
  import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
64
72
  import { readManifest } from "./manifest.ts";
65
73
  import {
74
+ liveFilesAt,
66
75
  matrixOf,
67
76
  referenceKey,
68
77
  sweepMatrix,
@@ -70,21 +79,32 @@ import {
70
79
  type Matrix,
71
80
  type SweepResult,
72
81
  } from "./matrix.ts";
73
- import { relativePath, resolvePath } from "./paths.ts";
82
+ import { joinPath, relativePath, resolvePath } from "./paths.ts";
74
83
  import type { UnansweredQuestion } from "./query.ts";
75
- import { byPosition, type Position } from "./position.ts";
84
+ import { byPosition, isComponentFile, type Position } from "./position.ts";
85
+ import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
86
+ import { SetupError, setupLine, type SetupFailure } from "./setup-failure.ts";
87
+ import {
88
+ pathsOf,
89
+ readErrors,
90
+ skippedRoots,
91
+ skippedUnits,
92
+ type SkippedUnit,
93
+ } from "./type-errors.ts";
76
94
  import { reflectiveLookup } from "./reflective-lookup.ts";
77
95
  import { references, testFileRulesOf, type Reference, type TestFileRule } from "./references.ts";
78
96
  import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
79
97
  import type { Scope } from "./scope.ts";
80
98
  import { serializationContract } from "./serialization-contract.ts";
81
- import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
99
+ import { supportReferences, testSupportFiles } from "./test-support.ts";
100
+ import { diagnosticsOf, ownedAs, runSession, type Engine, type ProjectView } from "./session.ts";
82
101
  import { accessorsOf, storesOf } from "./stores.ts";
83
102
  import { isPartKind, type SuppressionRecord, type Suppressions, type Verdict } from "./suppress.ts";
84
103
  import { bindRows, readBaseline, readIgnoreFile, type Recorded } from "./suppress-file.ts";
85
104
  import { inlineDirectives, inlineSuppressions, type InlineDirective } from "./suppress-inline.ts";
86
105
  import type { Exemption, Mode, SweepInput } from "./sweep.ts";
87
106
  import { readTemplates, templateField } from "./template-field.ts";
107
+ import { ownErrors, workspaceRun } from "./workspace-run.ts";
88
108
 
89
109
  /** The setting whose source decides where a finding about a configured root sits. */
90
110
  const ROOTS_SETTING = "roots.patterns";
@@ -156,6 +176,22 @@ interface ProjectRead {
156
176
  readonly configFile: string;
157
177
  readonly held: Inventory;
158
178
  readonly rooted: Roots;
179
+ /**
180
+ * The paths below the target root of the workspace-package files the project holds
181
+ * only for the references they make.
182
+ */
183
+ readonly referenceOnly: readonly string[];
184
+ /** Every type error of the project's own files that skipped a unit. */
185
+ readonly typeErrorSkips: readonly TypeErrorSkip[];
186
+ /** The lines each of those skips withholds every finding on. */
187
+ readonly skipped: readonly SkippedUnit[];
188
+ }
189
+
190
+ /** One target project the run analyzes, as its stage is handed it. */
191
+ interface OpenedProject {
192
+ readonly configuration: string;
193
+ /** Enumerates and roots the project; called only inside the stage it was handed to. */
194
+ readonly read: () => ProjectRead;
159
195
  }
160
196
 
161
197
  /** What a run read of the consumers its scope declares. */
@@ -174,22 +210,48 @@ interface ReadProjects<Answer> {
174
210
  readonly notBuilt: readonly NotBuilt[];
175
211
  /** Every question the checker could not answer, each once. */
176
212
  readonly unanswered: readonly UnansweredQuestion[];
213
+ /** Per configuration whose component files the run cannot read, the reason. */
214
+ readonly componentsUnread: readonly ComponentsUnread[];
215
+ /** The convention rows the run applied. */
216
+ readonly conventionsApplied: readonly AppliedConvention[];
177
217
  }
178
218
 
179
219
  /**
180
- * Reads every project the scope discovers, and every project of each consumer it
181
- * declares, in one snapshot, each checked for errors first. A target project is
182
- * enumerated, rooted and handed to `stage`; a consumer project is read for its references
183
- * to the target. A guess that does not load is dropped into `notBuilt` (see
184
- * {@link isGuess}); any other project that does not load or carries an error fails the
185
- * run, as does a run whose every guess was dropped.
220
+ * The manifests the analysis reads, each once: the target's own, then every workspace
221
+ * member's below the target root.
222
+ */
223
+ function conventionManifests(
224
+ host: Host,
225
+ targetRoot: string,
226
+ memberDirs: readonly string[],
227
+ ): readonly ConventionManifest[] {
228
+ const found = new Map<string, ConventionManifest>();
229
+ if (host.kindOf(joinPath(targetRoot, "package.json")) === "file") {
230
+ found.set(targetRoot, { dir: targetRoot, path: "package.json" });
231
+ }
232
+ for (const dir of memberDirs) {
233
+ const below = relativePath(targetRoot, dir);
234
+ if (below !== undefined && !found.has(dir)) {
235
+ found.set(dir, { dir, path: `${below}/package.json` });
236
+ }
237
+ }
238
+ return [...found.values()];
239
+ }
240
+
241
+ /**
242
+ * Reads every project the scope discovers, and each declared consumer's, in one snapshot,
243
+ * each checked for errors first. `stage` is handed a target project and enumerates and roots
244
+ * it if it reads it; a consumer project is read for its references to the target. A type error
245
+ * in an own file skips the unit holding it. A guess that does not load or meets a setup failure
246
+ * is dropped into `notBuilt` (see {@link isGuess}); any other such project fails the run, as
247
+ * does a run whose every guess was dropped.
186
248
  */
187
249
  function readProjects<Answer>(
188
250
  engine: Engine,
189
251
  host: Host,
190
252
  scope: Scope,
191
253
  config: Config,
192
- stage: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Answer,
254
+ stage: <Brand>(project: ProjectView<Brand>, opened: OpenedProject) => Answer,
193
255
  ): ReadProjects<Answer> {
194
256
  const targetRoot = scope.target.path;
195
257
  const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
@@ -200,14 +262,30 @@ function readProjects<Answer>(
200
262
  load.configFiles.map((configFile) => [configFile, load.consumer] as const),
201
263
  ),
202
264
  );
265
+ const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
266
+ const workspace = workspaceRun(
267
+ engine,
268
+ host,
269
+ absoluteRoot,
270
+ discovered.configFiles,
271
+ readManifest(host, targetRoot),
272
+ );
273
+ const conventions = readConventions(
274
+ host,
275
+ (fileName, text) => engine.parseSourceFile(fileName, text),
276
+ conventionManifests(host, absoluteRoot, workspace?.memberDirs ?? []),
277
+ config.ts.disabledConventions,
278
+ );
203
279
  const options = {
204
- manifest: readManifest(host, targetRoot),
280
+ host,
281
+ manifest: workspace?.manifest ?? readManifest(host, targetRoot),
282
+ workspaceEntries: workspace?.entries,
205
283
  patterns: config.rootPatterns,
206
284
  entryFiles: config.ts.entryFiles,
285
+ conventions,
207
286
  testFiles: config.ts.testFiles,
208
287
  publishedAPI: config.targetKind === "library",
209
288
  };
210
- const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
211
289
  const notBuilt: NotBuilt[] = [...discovered.notBuilt];
212
290
  const dropped = (configFile: string, error: string): void => {
213
291
  notBuilt.push({
@@ -217,13 +295,42 @@ function readProjects<Answer>(
217
295
  });
218
296
  };
219
297
  const failures: LocatedDiagnostic[] = [];
298
+ const unbuildable: SetupFailure[] = [];
299
+ const setupDropped: SetupFailure[] = [];
220
300
  const consumed = new Map<string, Reference>();
301
+ const setupMet = (configFile: string, met: readonly SetupFailure[]): void => {
302
+ const [first] = met;
303
+ if (first !== undefined && isGuess(discovered, configFile)) {
304
+ dropped(configFile, setupLine(first));
305
+ setupDropped.push(...met);
306
+ } else {
307
+ unbuildable.push(...met);
308
+ }
309
+ };
221
310
  const session = runSession(
222
311
  engine,
223
312
  [...discovered.configFiles, ...consumerOf.keys()],
224
- (project) => {
225
- const errors = projectErrors(diagnosticsOf(project));
226
- const consumer = consumerOf.get(project.configFile);
313
+ (opened) => {
314
+ const consumer = consumerOf.get(opened.configFile);
315
+ const setup = consumer === undefined ? workspace?.unbuildable(opened.configFile) : undefined;
316
+ if (setup !== undefined) {
317
+ setupMet(opened.configFile, [
318
+ { setupClass: "workspace-member-without-source", detail: setup },
319
+ ]);
320
+ return undefined;
321
+ }
322
+ const referenceOnly =
323
+ workspace === undefined || consumer !== undefined
324
+ ? () => false
325
+ : workspace.referenceOnly(opened.configFile, opened.rootFiles);
326
+ const project =
327
+ workspace === undefined || consumer !== undefined
328
+ ? opened
329
+ : ownedAs(opened, (file, fromExternalLibrary) =>
330
+ workspace.ownFile(file, fromExternalLibrary),
331
+ );
332
+ const raw = projectErrors(diagnosticsOf(project));
333
+ const errors = { load: raw.load, check: ownErrors(raw.check, referenceOnly) };
227
334
  const [unloaded] = errors.load;
228
335
  if (
229
336
  unloaded !== undefined &&
@@ -236,8 +343,45 @@ function readProjects<Answer>(
236
343
  );
237
344
  return undefined;
238
345
  }
239
- if (errors.load.length > 0 || errors.check.length > 0) {
240
- failures.push(...errors.load, ...errors.check);
346
+ if (errors.load.length > 0) {
347
+ failures.push(...errors.load);
348
+ return undefined;
349
+ }
350
+ const reading = readErrors(project, errors.check, {
351
+ host,
352
+ targetRoot:
353
+ consumer === undefined
354
+ ? absoluteRoot
355
+ : resolvePath(host.workingDirectory(), consumer.path),
356
+ paths: pathsOf(engine, project.configFile),
357
+ });
358
+ if (reading.failing.length > 0) {
359
+ failures.push(...reading.failing);
360
+ return undefined;
361
+ }
362
+ if (consumer !== undefined && reading.missing.length > 0) {
363
+ unbuildable.push(
364
+ ...reading.missing.map((one) => ({
365
+ setupClass: "missing-consumer" as const,
366
+ detail: `the consumer ${consumer.path} does not build, its dependencies not installed: ${one.detail}`,
367
+ })),
368
+ );
369
+ return undefined;
370
+ }
371
+ if (reading.missing.length > 0) {
372
+ setupMet(project.configFile, reading.missing);
373
+ return undefined;
374
+ }
375
+ const unconventional =
376
+ consumer === undefined
377
+ ? conventionFailuresFor(
378
+ conventions,
379
+ project.configFile,
380
+ project.ownSourceFiles().map((file) => file.fileName),
381
+ )
382
+ : [];
383
+ if (unconventional.length > 0) {
384
+ setupMet(project.configFile, unconventional);
241
385
  return undefined;
242
386
  }
243
387
  if (consumer !== undefined) {
@@ -255,12 +399,38 @@ function readProjects<Answer>(
255
399
  }
256
400
  return undefined;
257
401
  }
258
- const held = inventory(project, host, targetRoot);
259
- const rooted = roots(project, held, targetRoot, options);
260
402
  const configuration = ids.get(project.configFile) ?? project.configFile;
261
- return stage(project, { configuration, configFile: project.configFile, held, rooted });
403
+ const read = (): ProjectRead => {
404
+ const held = inventory(project, host, targetRoot);
405
+ const found = roots(project, held, targetRoot, options);
406
+ const kept = skippedRoots(project, held, reading.skips);
407
+ const rooted =
408
+ kept.length === 0
409
+ ? found
410
+ : {
411
+ ...found,
412
+ liveUnderReachability: [
413
+ ...found.liveUnderReachability,
414
+ ...kept.map((id) => ({ id, kind: "type-error" as const, source: "" })),
415
+ ],
416
+ };
417
+ return {
418
+ configuration,
419
+ configFile: project.configFile,
420
+ held,
421
+ rooted,
422
+ typeErrorSkips: reading.skips.map((one) => one.record),
423
+ skipped: skippedUnits(reading.skips, absoluteRoot),
424
+ referenceOnly: project
425
+ .ownSourceFiles()
426
+ .filter((file) => referenceOnly(file.fileName))
427
+ .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
428
+ };
429
+ };
430
+ return stage(project, { configuration, read });
262
431
  },
263
432
  discovered.derived ? "record" : "refuse",
433
+ workspace?.programs,
264
434
  );
265
435
  for (const configFile of session.unopened) {
266
436
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
@@ -268,6 +438,9 @@ function readProjects<Answer>(
268
438
  }
269
439
  dropped(configFile, "the compiler opened no project for the configuration");
270
440
  }
441
+ if (unbuildable.length > 0) {
442
+ throw new SetupError(unbuildable);
443
+ }
271
444
  if (failures.length > 0) {
272
445
  throw new DiscoveryError(
273
446
  `${String(failures.length)} error(s), so no answer was produced`,
@@ -276,6 +449,9 @@ function readProjects<Answer>(
276
449
  }
277
450
  const answers = session.projects.filter((answer): answer is Answer => answer !== undefined);
278
451
  const [first] = notBuilt;
452
+ if (answers.length === 0 && setupDropped.length > 0) {
453
+ throw new SetupError(setupDropped);
454
+ }
279
455
  if (answers.length === 0 && first !== undefined) {
280
456
  throw new DiscoveryError(
281
457
  `no configuration discovery derived could be built; ${first.id}: ${first.error}`,
@@ -290,9 +466,37 @@ function readProjects<Answer>(
290
466
  },
291
467
  notBuilt,
292
468
  unanswered: session.unanswered,
469
+ componentsUnread: workspace?.componentsUnread ?? [],
470
+ conventionsApplied: conventions.applied,
293
471
  };
294
472
  }
295
473
 
474
+ /** The projects one run analyzes, and what reading them left out. */
475
+ interface RunProjects {
476
+ /** Each configuration the run analyzes, by the name discovery gives its project, in discovery order. */
477
+ readonly configurations: readonly string[];
478
+ /** The derived configurations the run dropped. */
479
+ readonly notBuilt: readonly NotBuilt[];
480
+ /** Every question the checker could not answer, each once. */
481
+ readonly unanswered: readonly UnansweredQuestion[];
482
+ }
483
+
484
+ /**
485
+ * The projects the run the scope and the configuration describe analyzes, each read
486
+ * for the errors and setup failures that decide whether it is analyzed, dropped or
487
+ * fails the run, exactly as the analysis reads them.
488
+ */
489
+ export function runProjects(engine: Engine, host: Host, scope: Scope, config: Config): RunProjects {
490
+ const read = readProjects(
491
+ engine,
492
+ host,
493
+ scope,
494
+ config,
495
+ (_project, opened) => opened.configuration,
496
+ );
497
+ return { configurations: read.answers, notBuilt: read.notBuilt, unanswered: read.unanswered };
498
+ }
499
+
296
500
  /**
297
501
  * The root set of the run the scope and the configuration describe.
298
502
  *
@@ -307,7 +511,7 @@ export function runRoots(
307
511
  provenance: Provenance,
308
512
  ): RunRoots {
309
513
  const targetRoot = scope.target.path;
310
- const read = readProjects(engine, host, scope, config, (_project, projectRead) => projectRead);
514
+ const read = readProjects(engine, host, scope, config, (_project, opened) => opened.read());
311
515
  const projects = read.answers;
312
516
 
313
517
  const configurations: string[] = [];
@@ -361,6 +565,8 @@ export function runRoots(
361
565
  export interface RunSweep {
362
566
  readonly matrix: Matrix;
363
567
  readonly sweep: SweepResult;
568
+ /** The exemption records that hold under the sweep. */
569
+ readonly exempt: readonly Exemption[];
364
570
  /**
365
571
  * The exemption records that held back a declaration some configuration of the run
366
572
  * would otherwise judge dead, in the declarations' site order.
@@ -375,6 +581,18 @@ export interface RunSweep {
375
581
  * such a declaration keeps live. The run reports none of them.
376
582
  */
377
583
  readonly heldByUnanswered: readonly string[];
584
+ /** The lines of the units type errors skipped, on which the run reports nothing. */
585
+ readonly skipped: readonly SkippedUnit[];
586
+ /** The component files the run read, by path below the target root, each once. */
587
+ readonly componentFiles: readonly string[];
588
+ }
589
+
590
+ /** One `<script` start tag of a component file the run read no block for. */
591
+ export interface ComponentWarning {
592
+ /** The file, below the target root. */
593
+ readonly path: string;
594
+ readonly line: number;
595
+ readonly reason: string;
378
596
  }
379
597
 
380
598
  /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
@@ -396,6 +614,11 @@ interface SweptProject<Extra> {
396
614
  readonly exempt: readonly Exemption[];
397
615
  /** The declarations of the project an unanswered question could have kept live. */
398
616
  readonly unanswered: readonly string[];
617
+ readonly typeErrorSkips: readonly TypeErrorSkip[];
618
+ readonly skipped: readonly SkippedUnit[];
619
+ /** The component files the project holds of its own, by path below the target root. */
620
+ readonly componentFiles: readonly string[];
621
+ readonly componentWarnings: readonly ComponentWarning[];
399
622
  readonly extra: Extra;
400
623
  }
401
624
 
@@ -412,6 +635,10 @@ interface ReadRun<Extra> {
412
635
  readonly notBuilt: readonly NotBuilt[];
413
636
  /** Every question the checker could not answer, each once. */
414
637
  readonly unanswered: readonly UnansweredQuestion[];
638
+ /** Per configuration whose component files the run cannot read, the reason. */
639
+ readonly componentsUnread: readonly ComponentsUnread[];
640
+ /** The convention rows the run applied. */
641
+ readonly conventionsApplied: readonly AppliedConvention[];
415
642
  }
416
643
 
417
644
  /**
@@ -439,48 +666,67 @@ function readRun<Extra>(
439
666
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
440
667
  : readTemplates(host, targetRoot, config.analysis);
441
668
  const consumers = scope.consumers.map((consumer) => consumer.path);
442
- const read = readProjects<SweptProject<Extra>>(
443
- engine,
444
- host,
445
- scope,
446
- config,
447
- (project, projectRead) => {
448
- const resolved = references(project, projectRead.held, targetRoot, {
449
- testFiles: config.ts.testFiles,
450
- });
451
- const configured: Configured = {
452
- configuration: projectRead.configuration,
453
- symbols: projectRead.held.symbols,
454
- references: resolved.references,
455
- roots: projectRead.rooted.liveUnderReachability,
456
- testFiles: resolved.testFilePaths,
457
- };
458
- const exempt = computeExemptions(
459
- { project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers },
460
- detectors,
461
- {
462
- disabled,
463
- mode,
464
- testFiles: new Set(resolved.testFilePaths),
465
- },
466
- );
467
- const unanswered = [
468
- ...projectRead.held.unanswered,
469
- ...projectRead.rooted.unanswered,
470
- ...resolved.references
471
- .filter((reference) => reference.resolution === "by-name")
472
- .map((reference) => reference.to),
473
- ...exempt.filter(isUnansweredRecord).map((record) => record.id),
474
- ];
475
- return {
476
- configFile: projectRead.configFile,
477
- configured,
478
- exempt,
479
- unanswered,
480
- extra: extra(project, projectRead, resolved.references),
481
- };
482
- },
483
- );
669
+ const read = readProjects<SweptProject<Extra>>(engine, host, scope, config, (project, opened) => {
670
+ const projectRead = opened.read();
671
+ const resolved = references(project, projectRead.held, targetRoot, {
672
+ testFiles: config.ts.testFiles,
673
+ });
674
+ const support = testSupportFiles(
675
+ projectRead.held.symbols,
676
+ resolved.references,
677
+ projectRead.rooted.liveUnderReachability,
678
+ resolved.testFilePaths,
679
+ );
680
+ const made = supportReferences(resolved.references, support);
681
+ const configured: Configured = {
682
+ configuration: projectRead.configuration,
683
+ symbols: projectRead.held.symbols,
684
+ references: made,
685
+ roots: projectRead.rooted.liveUnderReachability,
686
+ testFiles: resolved.testFilePaths,
687
+ ...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
688
+ ...(projectRead.referenceOnly.length === 0
689
+ ? {}
690
+ : { referenceOnly: projectRead.referenceOnly }),
691
+ };
692
+ const exempt = computeExemptions(
693
+ { project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers },
694
+ detectors,
695
+ {
696
+ disabled,
697
+ mode,
698
+ testFiles: new Set([...resolved.testFilePaths, ...support]),
699
+ },
700
+ );
701
+ const unanswered = [
702
+ ...projectRead.held.unanswered,
703
+ ...projectRead.rooted.unanswered,
704
+ ...made
705
+ .filter((reference) => reference.resolution === "by-name")
706
+ .map((reference) => reference.to),
707
+ ...exempt.filter(isUnansweredRecord).map((record) => record.id),
708
+ ];
709
+ const components = project.ownSourceFiles().filter(isComponentFile);
710
+ const pathOf = (file: (typeof components)[number]): string =>
711
+ relativePath(targetRoot, file.fileName) ?? file.fileName;
712
+ return {
713
+ configFile: projectRead.configFile,
714
+ configured,
715
+ exempt,
716
+ unanswered,
717
+ typeErrorSkips: projectRead.typeErrorSkips,
718
+ skipped: projectRead.skipped,
719
+ componentFiles: components.map(pathOf),
720
+ componentWarnings: components.flatMap((file) =>
721
+ readComponent(file.originalText).warnings.map((warning) => ({
722
+ path: pathOf(file),
723
+ line: lineOf(file.originalText, warning.offset),
724
+ reason: warning.reason,
725
+ })),
726
+ ),
727
+ extra: extra(project, projectRead, made),
728
+ };
729
+ });
484
730
  const consumed = read.consumers.references;
485
731
  const projects = read.answers.map((one) =>
486
732
  consumed.length === 0
@@ -506,9 +752,38 @@ function readRun<Extra>(
506
752
  consumers: read.consumers.loaded,
507
753
  notBuilt: read.notBuilt,
508
754
  unanswered: read.unanswered,
755
+ componentsUnread: read.componentsUnread,
756
+ conventionsApplied: read.conventionsApplied,
509
757
  };
510
758
  }
511
759
 
760
+ /**
761
+ * The run swept under one set of marks with the records that hold. A record a component
762
+ * file's markup holds counts only while the file is live, and what it holds may be what
763
+ * keeps the file live, so each file's markup joins only once a sweep without it finds the
764
+ * file live, until a sweep finds no further file live.
765
+ */
766
+ function holdingSweep<Extra>(
767
+ read: ReadRun<Extra>,
768
+ input: Pick<SweepInput, "marked" | "mode">,
769
+ ): { readonly swept: SweepInput; readonly result: SweepResult } {
770
+ const paths = new Set(read.exempt.flatMap((record) => record.whileLive ?? []));
771
+ let live = new Set<string>();
772
+ for (;;) {
773
+ const swept: SweepInput = {
774
+ ...input,
775
+ exempt: holdingWhile(read.exempt, live),
776
+ unanswered: read.unansweredHeld,
777
+ };
778
+ const result = sweepMatrix(read.matrix, swept);
779
+ const next = new Set(liveFilesAt(read.matrix, result, paths).map((one) => one.position.path));
780
+ if (next.size === live.size) {
781
+ return { swept, result };
782
+ }
783
+ live = next;
784
+ }
785
+ }
786
+
512
787
  /**
513
788
  * The sweep of a run read once, under one set of marks. Where a question went unanswered,
514
789
  * the run is swept once more without what that question could have kept live and without
@@ -520,8 +795,8 @@ function sweptOf<Extra>(
520
795
  read: ReadRun<Extra>,
521
796
  input: Pick<SweepInput, "marked" | "mode">,
522
797
  ): RunSweep {
523
- const swept: SweepInput = { ...input, exempt: read.exempt, unanswered: read.unansweredHeld };
524
- const result = sweepMatrix(read.matrix, swept);
798
+ const { swept, result } = holdingSweep(read, input);
799
+ const exempt = swept.exempt ?? [];
525
800
  const held = new Set(read.unansweredHeld);
526
801
  if (held.size > 0) {
527
802
  // What a declaration reads where the checker did not answer is unknown, so a rule
@@ -542,7 +817,7 @@ function sweptOf<Extra>(
542
817
  );
543
818
  const bare: SweepInput = {
544
819
  ...swept,
545
- exempt: read.exempt.filter((record) => !isUnansweredRecord(record)),
820
+ exempt: exempt.filter((record) => !isUnansweredRecord(record)),
546
821
  unanswered: [],
547
822
  };
548
823
  for (const candidate of sweepMatrix(answered, bare).candidates) {
@@ -558,10 +833,13 @@ function sweptOf<Extra>(
558
833
  return {
559
834
  matrix: read.matrix,
560
835
  sweep: result,
836
+ exempt,
561
837
  retained: retainedIn(read.matrix, swept),
562
838
  notBuilt: read.notBuilt,
563
839
  unanswered: read.unanswered,
564
840
  heldByUnanswered: [...held],
841
+ skipped: read.projects.flatMap((one) => one.skipped),
842
+ componentFiles: [...new Set(read.projects.flatMap((one) => one.componentFiles))].sort(compare),
565
843
  };
566
844
  }
567
845
 
@@ -622,11 +900,13 @@ function readEmitterRun(
622
900
  (project, read, references) => ({
623
901
  deprecation: deprecatedDeclarations(project, read.held),
624
902
  accessors: accessorsOf(project, read.held),
625
- needs: projectNeeds(project, read.held, host),
903
+ needs: projectNeeds(project, read.held, host, targetRoot),
626
904
  implementations: implementations(project, read.held),
627
905
  heldByInclusion: heldByInclusion(project, targetRoot),
628
906
  rooted: read.rooted,
629
- directives: inlineDirectives(project, read.held, targetRoot),
907
+ directives: inlineDirectives(project, read.held, targetRoot).filter(
908
+ (directive) => !read.referenceOnly.includes(directive.site.path),
909
+ ),
630
910
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
631
911
  parts: projectParts(project, read.held, references, targetRoot),
632
912
  }),
@@ -663,7 +943,7 @@ function emitterInputOver(
663
943
  references: one.configured.references,
664
944
  accessors: one.extra.accessors,
665
945
  })),
666
- read.exempt,
946
+ swept.exempt,
667
947
  input.mode,
668
948
  ),
669
949
  dependencies: dependenciesOf(
@@ -689,7 +969,7 @@ function emitterInputOver(
689
969
  intraFunction: intraFunctionFacts(
690
970
  projects.map((one) => one.extra.parts),
691
971
  projects.flatMap((one) => one.configured.references),
692
- new Set([...read.exempt.map((one) => one.id), ...swept.heldByUnanswered]),
972
+ new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered]),
693
973
  ),
694
974
  };
695
975
  }
@@ -768,10 +1048,21 @@ function readAnalysis(
768
1048
  testFileRules: testFileRulesOf(
769
1049
  config.ts.testFiles,
770
1050
  read.projects.flatMap((one) => one.configured.testFiles),
1051
+ read.projects.flatMap((one) => one.configured.supportFiles ?? []),
771
1052
  ),
1053
+ typeErrorSkips: read.projects.flatMap((one) => one.typeErrorSkips),
1054
+ componentWarnings: [
1055
+ ...new Map(
1056
+ read.projects
1057
+ .flatMap((one) => one.componentWarnings)
1058
+ .map((one) => [`${one.path}:${String(one.line)}:${one.reason}`, one] as const),
1059
+ ).values(),
1060
+ ],
772
1061
  consumers: read.consumers,
773
1062
  notBuilt: read.notBuilt,
774
1063
  unanswered: read.unanswered,
1064
+ componentsUnread: read.componentsUnread,
1065
+ conventionsApplied: read.conventionsApplied,
775
1066
  },
776
1067
  configured: read.projects.map((one) => one.configured),
777
1068
  symbols,
@@ -842,6 +1133,20 @@ export interface RunFacts {
842
1133
  readonly notBuilt: readonly NotBuilt[];
843
1134
  /** Every question the checker could not answer, each once. */
844
1135
  readonly unanswered: readonly UnansweredQuestion[];
1136
+ /** Every type error that skipped a unit, in the run's order. */
1137
+ readonly typeErrorSkips: readonly TypeErrorSkip[];
1138
+ /** Every `<script` start tag a component file holds that no block was read for, each once. */
1139
+ readonly componentWarnings: readonly ComponentWarning[];
1140
+ /** Per configuration whose component files the run cannot read, the reason. */
1141
+ readonly componentsUnread: readonly ComponentsUnread[];
1142
+ /** The convention rows the run applied. */
1143
+ readonly conventionsApplied: readonly AppliedConvention[];
1144
+ }
1145
+
1146
+ /** One configuration whose component files a run cannot read. */
1147
+ export interface ComponentsUnread {
1148
+ readonly configFile: string;
1149
+ readonly reason: string;
845
1150
  }
846
1151
 
847
1152
  /** The findings of one run, beside what a report states about the run itself. */
@@ -874,6 +1179,25 @@ export function runAnalysis(
874
1179
  return analysisOf(reader, inputs);
875
1180
  }
876
1181
 
1182
+ /**
1183
+ * One count per configuration of the run in which the checker left a question unanswered:
1184
+ * the questions asked in its project, and the declarations of its own the run holds for them.
1185
+ */
1186
+ export function unansweredCounts(analysis: RunAnalysis): readonly UnansweredCount[] {
1187
+ const held = new Set(analysis.swept.heldByUnanswered);
1188
+ return analysis.run.projects.flatMap((project) => {
1189
+ const questions = analysis.run.unanswered.filter(
1190
+ (one) => one.configFile === project.configFile,
1191
+ ).length;
1192
+ if (questions === 0) {
1193
+ return [];
1194
+ }
1195
+ const symbols = analysis.configured.find((one) => one.configuration === project.id)?.symbols;
1196
+ const declarations = new Set((symbols ?? []).map((one) => one.id).filter((id) => held.has(id)));
1197
+ return [{ configuration: project.id, questions, declarations: declarations.size }];
1198
+ });
1199
+ }
1200
+
877
1201
  function analysisOf(reader: AnalysisReader, inputs: AnalysisInputs): RunAnalysis {
878
1202
  return {
879
1203
  result: findingsPass(inputs),