@cplieger/deadset-ts 4.0.1 → 5.1.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 (138) 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 +301 -41
  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 +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. 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,22 @@ import {
33
34
  type LocatedDiagnostic,
34
35
  type NotBuilt,
35
36
  } from "./discover.ts";
37
+ import {
38
+ conventionFailuresFor,
39
+ generatedFiles,
40
+ readConventions,
41
+ type AppliedConvention,
42
+ type ConventionManifest,
43
+ } from "./conventions.ts";
36
44
  import { readEdgeSides } from "./edges.ts";
37
45
  import { enumGroup } from "./enum-group.ts";
46
+ import { generatedFile } from "./generated-file.ts";
47
+ import { sourceFilesOf } from "./source-files.ts";
38
48
  import {
39
- computeExemptions,
49
+ detectExemptions,
40
50
  disabledClasses,
41
51
  exemptionsOf,
52
+ holdingWhile,
42
53
  isUnansweredRecord,
43
54
  retainedIn,
44
55
  type Detector,
@@ -63,6 +74,7 @@ import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-fun
63
74
  import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
64
75
  import { readManifest } from "./manifest.ts";
65
76
  import {
77
+ liveFilesAt,
66
78
  matrixOf,
67
79
  referenceKey,
68
80
  sweepMatrix,
@@ -70,21 +82,32 @@ import {
70
82
  type Matrix,
71
83
  type SweepResult,
72
84
  } from "./matrix.ts";
73
- import { relativePath, resolvePath } from "./paths.ts";
85
+ import { joinPath, relativePath, resolvePath } from "./paths.ts";
74
86
  import type { UnansweredQuestion } from "./query.ts";
75
- import { byPosition, type Position } from "./position.ts";
87
+ import { byPosition, isComponentFile, type Position } from "./position.ts";
88
+ import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
89
+ import { SetupError, setupLine, type SetupFailure } from "./setup-failure.ts";
90
+ import {
91
+ pathsOf,
92
+ readErrors,
93
+ skippedRoots,
94
+ skippedUnits,
95
+ type SkippedUnit,
96
+ } from "./type-errors.ts";
76
97
  import { reflectiveLookup } from "./reflective-lookup.ts";
77
98
  import { references, testFileRulesOf, type Reference, type TestFileRule } from "./references.ts";
78
99
  import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
79
100
  import type { Scope } from "./scope.ts";
80
101
  import { serializationContract } from "./serialization-contract.ts";
81
- import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
102
+ import { supportReferences, testSupportFiles } from "./test-support.ts";
103
+ import { diagnosticsOf, ownedAs, runSession, type Engine, type ProjectView } from "./session.ts";
82
104
  import { accessorsOf, storesOf } from "./stores.ts";
83
105
  import { isPartKind, type SuppressionRecord, type Suppressions, type Verdict } from "./suppress.ts";
84
106
  import { bindRows, readBaseline, readIgnoreFile, type Recorded } from "./suppress-file.ts";
85
107
  import { inlineDirectives, inlineSuppressions, type InlineDirective } from "./suppress-inline.ts";
86
108
  import type { Exemption, Mode, SweepInput } from "./sweep.ts";
87
109
  import { readTemplates, templateField } from "./template-field.ts";
110
+ import { ownErrors, workspaceRun } from "./workspace-run.ts";
88
111
 
89
112
  /** The setting whose source decides where a finding about a configured root sits. */
90
113
  const ROOTS_SETTING = "roots.patterns";
@@ -156,6 +179,24 @@ interface ProjectRead {
156
179
  readonly configFile: string;
157
180
  readonly held: Inventory;
158
181
  readonly rooted: Roots;
182
+ /**
183
+ * The paths below the target root of the workspace-package files the project holds
184
+ * only for the references they make.
185
+ */
186
+ readonly referenceOnly: readonly string[];
187
+ /** Every type error of the project's own files that skipped a unit. */
188
+ readonly typeErrorSkips: readonly TypeErrorSkip[];
189
+ /** The lines each of those skips withholds every finding on. */
190
+ readonly skipped: readonly SkippedUnit[];
191
+ /** The project's generated files, by path below the target root, each with the row naming it. */
192
+ readonly generated: ReadonlyMap<string, string>;
193
+ }
194
+
195
+ /** One target project the run analyzes, as its stage is handed it. */
196
+ interface OpenedProject {
197
+ readonly configuration: string;
198
+ /** Enumerates and roots the project; called only inside the stage it was handed to. */
199
+ readonly read: () => ProjectRead;
159
200
  }
160
201
 
161
202
  /** What a run read of the consumers its scope declares. */
@@ -174,22 +215,48 @@ interface ReadProjects<Answer> {
174
215
  readonly notBuilt: readonly NotBuilt[];
175
216
  /** Every question the checker could not answer, each once. */
176
217
  readonly unanswered: readonly UnansweredQuestion[];
218
+ /** Per configuration whose component files the run cannot read, the reason. */
219
+ readonly componentsUnread: readonly ComponentsUnread[];
220
+ /** The convention rows the run applied. */
221
+ readonly conventionsApplied: readonly AppliedConvention[];
222
+ }
223
+
224
+ /**
225
+ * The manifests the analysis reads, each once: the target's own, then every workspace
226
+ * member's below the target root.
227
+ */
228
+ function conventionManifests(
229
+ host: Host,
230
+ targetRoot: string,
231
+ memberDirs: readonly string[],
232
+ ): readonly ConventionManifest[] {
233
+ const found = new Map<string, ConventionManifest>();
234
+ if (host.kindOf(joinPath(targetRoot, "package.json")) === "file") {
235
+ found.set(targetRoot, { dir: targetRoot, path: "package.json" });
236
+ }
237
+ for (const dir of memberDirs) {
238
+ const below = relativePath(targetRoot, dir);
239
+ if (below !== undefined && !found.has(dir)) {
240
+ found.set(dir, { dir, path: `${below}/package.json` });
241
+ }
242
+ }
243
+ return [...found.values()];
177
244
  }
178
245
 
179
246
  /**
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.
247
+ * Reads every project the scope discovers, and each declared consumer's, in one snapshot,
248
+ * each checked for errors first. `stage` is handed a target project and enumerates and roots
249
+ * it if it reads it; a consumer project is read for its references to the target. A type error
250
+ * in an own file skips the unit holding it. A guess that does not load or meets a setup failure
251
+ * is dropped into `notBuilt` (see {@link isGuess}); any other such project fails the run, as
252
+ * does a run whose every guess was dropped.
186
253
  */
187
254
  function readProjects<Answer>(
188
255
  engine: Engine,
189
256
  host: Host,
190
257
  scope: Scope,
191
258
  config: Config,
192
- stage: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Answer,
259
+ stage: <Brand>(project: ProjectView<Brand>, opened: OpenedProject) => Answer,
193
260
  ): ReadProjects<Answer> {
194
261
  const targetRoot = scope.target.path;
195
262
  const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
@@ -200,14 +267,31 @@ function readProjects<Answer>(
200
267
  load.configFiles.map((configFile) => [configFile, load.consumer] as const),
201
268
  ),
202
269
  );
270
+ const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
271
+ const workspace = workspaceRun(
272
+ engine,
273
+ host,
274
+ absoluteRoot,
275
+ discovered.configFiles,
276
+ readManifest(host, targetRoot),
277
+ );
278
+ const conventions = readConventions(
279
+ host,
280
+ (fileName, text) => engine.parseSourceFile(fileName, text),
281
+ conventionManifests(host, absoluteRoot, workspace?.memberDirs ?? []),
282
+ config.ts.disabledConventions,
283
+ );
203
284
  const options = {
204
- manifest: readManifest(host, targetRoot),
285
+ host,
286
+ manifest: workspace?.manifest ?? readManifest(host, targetRoot),
287
+ workspaceEntries: workspace?.entries,
205
288
  patterns: config.rootPatterns,
206
289
  entryFiles: config.ts.entryFiles,
290
+ conventions,
207
291
  testFiles: config.ts.testFiles,
292
+ parse: (fileName: string, text: string) => engine.parseSourceFile(fileName, text),
208
293
  publishedAPI: config.targetKind === "library",
209
294
  };
210
- const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
211
295
  const notBuilt: NotBuilt[] = [...discovered.notBuilt];
212
296
  const dropped = (configFile: string, error: string): void => {
213
297
  notBuilt.push({
@@ -217,13 +301,42 @@ function readProjects<Answer>(
217
301
  });
218
302
  };
219
303
  const failures: LocatedDiagnostic[] = [];
304
+ const unbuildable: SetupFailure[] = [];
305
+ const setupDropped: SetupFailure[] = [];
220
306
  const consumed = new Map<string, Reference>();
307
+ const setupMet = (configFile: string, met: readonly SetupFailure[]): void => {
308
+ const [first] = met;
309
+ if (first !== undefined && isGuess(discovered, configFile)) {
310
+ dropped(configFile, setupLine(first));
311
+ setupDropped.push(...met);
312
+ } else {
313
+ unbuildable.push(...met);
314
+ }
315
+ };
221
316
  const session = runSession(
222
317
  engine,
223
318
  [...discovered.configFiles, ...consumerOf.keys()],
224
- (project) => {
225
- const errors = projectErrors(diagnosticsOf(project));
226
- const consumer = consumerOf.get(project.configFile);
319
+ (opened) => {
320
+ const consumer = consumerOf.get(opened.configFile);
321
+ const setup = consumer === undefined ? workspace?.unbuildable(opened.configFile) : undefined;
322
+ if (setup !== undefined) {
323
+ setupMet(opened.configFile, [
324
+ { setupClass: "workspace-member-without-source", detail: setup },
325
+ ]);
326
+ return undefined;
327
+ }
328
+ const referenceOnly =
329
+ workspace === undefined || consumer !== undefined
330
+ ? () => false
331
+ : workspace.referenceOnly(opened.configFile, opened.rootFiles);
332
+ const project =
333
+ workspace === undefined || consumer !== undefined
334
+ ? opened
335
+ : ownedAs(opened, (file, fromExternalLibrary) =>
336
+ workspace.ownFile(file, fromExternalLibrary),
337
+ );
338
+ const raw = projectErrors(diagnosticsOf(project));
339
+ const errors = { load: raw.load, check: ownErrors(raw.check, referenceOnly) };
227
340
  const [unloaded] = errors.load;
228
341
  if (
229
342
  unloaded !== undefined &&
@@ -236,8 +349,45 @@ function readProjects<Answer>(
236
349
  );
237
350
  return undefined;
238
351
  }
239
- if (errors.load.length > 0 || errors.check.length > 0) {
240
- failures.push(...errors.load, ...errors.check);
352
+ if (errors.load.length > 0) {
353
+ failures.push(...errors.load);
354
+ return undefined;
355
+ }
356
+ const reading = readErrors(project, errors.check, {
357
+ host,
358
+ targetRoot:
359
+ consumer === undefined
360
+ ? absoluteRoot
361
+ : resolvePath(host.workingDirectory(), consumer.path),
362
+ paths: pathsOf(engine, project.configFile),
363
+ });
364
+ if (reading.failing.length > 0) {
365
+ failures.push(...reading.failing);
366
+ return undefined;
367
+ }
368
+ if (consumer !== undefined && reading.missing.length > 0) {
369
+ unbuildable.push(
370
+ ...reading.missing.map((one) => ({
371
+ setupClass: "missing-consumer" as const,
372
+ detail: `the consumer ${consumer.path} does not build, its dependencies not installed: ${one.detail}`,
373
+ })),
374
+ );
375
+ return undefined;
376
+ }
377
+ if (reading.missing.length > 0) {
378
+ setupMet(project.configFile, reading.missing);
379
+ return undefined;
380
+ }
381
+ const unconventional =
382
+ consumer === undefined
383
+ ? conventionFailuresFor(
384
+ conventions,
385
+ project.configFile,
386
+ project.ownSourceFiles().map((file) => file.fileName),
387
+ )
388
+ : [];
389
+ if (unconventional.length > 0) {
390
+ setupMet(project.configFile, unconventional);
241
391
  return undefined;
242
392
  }
243
393
  if (consumer !== undefined) {
@@ -255,12 +405,39 @@ function readProjects<Answer>(
255
405
  }
256
406
  return undefined;
257
407
  }
258
- const held = inventory(project, host, targetRoot);
259
- const rooted = roots(project, held, targetRoot, options);
260
408
  const configuration = ids.get(project.configFile) ?? project.configFile;
261
- return stage(project, { configuration, configFile: project.configFile, held, rooted });
409
+ const read = (): ProjectRead => {
410
+ const held = inventory(project, host, targetRoot);
411
+ const found = roots(project, held, targetRoot, options);
412
+ const kept = skippedRoots(project, held, reading.skips);
413
+ const rooted =
414
+ kept.length === 0
415
+ ? found
416
+ : {
417
+ ...found,
418
+ liveUnderReachability: [
419
+ ...found.liveUnderReachability,
420
+ ...kept.map((id) => ({ id, kind: "type-error" as const, source: "" })),
421
+ ],
422
+ };
423
+ return {
424
+ configuration,
425
+ configFile: project.configFile,
426
+ held,
427
+ rooted,
428
+ typeErrorSkips: reading.skips.map((one) => one.record),
429
+ skipped: skippedUnits(reading.skips, absoluteRoot),
430
+ referenceOnly: project
431
+ .ownSourceFiles()
432
+ .filter((file) => referenceOnly(file.fileName))
433
+ .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
434
+ generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
435
+ };
436
+ };
437
+ return stage(project, { configuration, read });
262
438
  },
263
439
  discovered.derived ? "record" : "refuse",
440
+ workspace?.programs,
264
441
  );
265
442
  for (const configFile of session.unopened) {
266
443
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
@@ -268,6 +445,9 @@ function readProjects<Answer>(
268
445
  }
269
446
  dropped(configFile, "the compiler opened no project for the configuration");
270
447
  }
448
+ if (unbuildable.length > 0) {
449
+ throw new SetupError(unbuildable);
450
+ }
271
451
  if (failures.length > 0) {
272
452
  throw new DiscoveryError(
273
453
  `${String(failures.length)} error(s), so no answer was produced`,
@@ -276,6 +456,9 @@ function readProjects<Answer>(
276
456
  }
277
457
  const answers = session.projects.filter((answer): answer is Answer => answer !== undefined);
278
458
  const [first] = notBuilt;
459
+ if (answers.length === 0 && setupDropped.length > 0) {
460
+ throw new SetupError(setupDropped);
461
+ }
279
462
  if (answers.length === 0 && first !== undefined) {
280
463
  throw new DiscoveryError(
281
464
  `no configuration discovery derived could be built; ${first.id}: ${first.error}`,
@@ -290,9 +473,37 @@ function readProjects<Answer>(
290
473
  },
291
474
  notBuilt,
292
475
  unanswered: session.unanswered,
476
+ componentsUnread: workspace?.componentsUnread ?? [],
477
+ conventionsApplied: conventions.applied,
293
478
  };
294
479
  }
295
480
 
481
+ /** The projects one run analyzes, and what reading them left out. */
482
+ interface RunProjects {
483
+ /** Each configuration the run analyzes, by the name discovery gives its project, in discovery order. */
484
+ readonly configurations: readonly string[];
485
+ /** The derived configurations the run dropped. */
486
+ readonly notBuilt: readonly NotBuilt[];
487
+ /** Every question the checker could not answer, each once. */
488
+ readonly unanswered: readonly UnansweredQuestion[];
489
+ }
490
+
491
+ /**
492
+ * The projects the run the scope and the configuration describe analyzes, each read
493
+ * for the errors and setup failures that decide whether it is analyzed, dropped or
494
+ * fails the run, exactly as the analysis reads them.
495
+ */
496
+ export function runProjects(engine: Engine, host: Host, scope: Scope, config: Config): RunProjects {
497
+ const read = readProjects(
498
+ engine,
499
+ host,
500
+ scope,
501
+ config,
502
+ (_project, opened) => opened.configuration,
503
+ );
504
+ return { configurations: read.answers, notBuilt: read.notBuilt, unanswered: read.unanswered };
505
+ }
506
+
296
507
  /**
297
508
  * The root set of the run the scope and the configuration describe.
298
509
  *
@@ -307,7 +518,7 @@ export function runRoots(
307
518
  provenance: Provenance,
308
519
  ): RunRoots {
309
520
  const targetRoot = scope.target.path;
310
- const read = readProjects(engine, host, scope, config, (_project, projectRead) => projectRead);
521
+ const read = readProjects(engine, host, scope, config, (_project, opened) => opened.read());
311
522
  const projects = read.answers;
312
523
 
313
524
  const configurations: string[] = [];
@@ -361,6 +572,8 @@ export function runRoots(
361
572
  export interface RunSweep {
362
573
  readonly matrix: Matrix;
363
574
  readonly sweep: SweepResult;
575
+ /** The exemption records that hold under the sweep. */
576
+ readonly exempt: readonly Exemption[];
364
577
  /**
365
578
  * The exemption records that held back a declaration some configuration of the run
366
579
  * would otherwise judge dead, in the declarations' site order.
@@ -375,12 +588,25 @@ export interface RunSweep {
375
588
  * such a declaration keeps live. The run reports none of them.
376
589
  */
377
590
  readonly heldByUnanswered: readonly string[];
591
+ /** The lines of the units type errors skipped, on which the run reports nothing. */
592
+ readonly skipped: readonly SkippedUnit[];
593
+ /** The component files the run read, by path below the target root, each once. */
594
+ readonly componentFiles: readonly string[];
595
+ }
596
+
597
+ /** One `<script` start tag of a component file the run read no block for. */
598
+ export interface ComponentWarning {
599
+ /** The file, below the target root. */
600
+ readonly path: string;
601
+ readonly line: number;
602
+ readonly reason: string;
378
603
  }
379
604
 
380
605
  /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
381
606
  const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
382
607
  ["interface-satisfaction", interfaceSatisfaction],
383
608
  ["enum-group", enumGroup],
609
+ ["generated-file", generatedFile],
384
610
  ["template-field", templateField],
385
611
  ["reflective-lookup", reflectiveLookup],
386
612
  ["decorator", decorator],
@@ -396,6 +622,11 @@ interface SweptProject<Extra> {
396
622
  readonly exempt: readonly Exemption[];
397
623
  /** The declarations of the project an unanswered question could have kept live. */
398
624
  readonly unanswered: readonly string[];
625
+ readonly typeErrorSkips: readonly TypeErrorSkip[];
626
+ readonly skipped: readonly SkippedUnit[];
627
+ /** The component files the project holds of its own, by path below the target root. */
628
+ readonly componentFiles: readonly string[];
629
+ readonly componentWarnings: readonly ComponentWarning[];
399
630
  readonly extra: Extra;
400
631
  }
401
632
 
@@ -412,6 +643,10 @@ interface ReadRun<Extra> {
412
643
  readonly notBuilt: readonly NotBuilt[];
413
644
  /** Every question the checker could not answer, each once. */
414
645
  readonly unanswered: readonly UnansweredQuestion[];
646
+ /** Per configuration whose component files the run cannot read, the reason. */
647
+ readonly componentsUnread: readonly ComponentsUnread[];
648
+ /** The convention rows the run applied. */
649
+ readonly conventionsApplied: readonly AppliedConvention[];
415
650
  }
416
651
 
417
652
  /**
@@ -439,48 +674,78 @@ function readRun<Extra>(
439
674
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
440
675
  : readTemplates(host, targetRoot, config.analysis);
441
676
  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
- );
677
+ const read = readProjects<SweptProject<Extra>>(engine, host, scope, config, (project, opened) => {
678
+ const projectRead = opened.read();
679
+ const resolved = references(project, projectRead.held, targetRoot, {
680
+ testFiles: config.ts.testFiles,
681
+ });
682
+ const support = testSupportFiles(
683
+ projectRead.held.symbols,
684
+ resolved.references,
685
+ projectRead.rooted.liveUnderReachability,
686
+ resolved.testFilePaths,
687
+ );
688
+ const detected = detectExemptions(
689
+ {
690
+ project,
691
+ held: projectRead.held,
692
+ targetRoot,
693
+ templates,
694
+ ts: config.ts,
695
+ consumers,
696
+ ...(config.analysis.generatedFiles === "include"
697
+ ? {}
698
+ : { generated: projectRead.generated }),
699
+ },
700
+ detectors,
701
+ {
702
+ disabled,
703
+ mode,
704
+ testFiles: new Set([...resolved.testFilePaths, ...support]),
705
+ },
706
+ );
707
+ const exempt = detected.records;
708
+ const made = supportReferences(resolved.references, support);
709
+ const configured: Configured = {
710
+ configuration: projectRead.configuration,
711
+ symbols: projectRead.held.symbols,
712
+ references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
713
+ roots: projectRead.rooted.liveUnderReachability,
714
+ testFiles: resolved.testFilePaths,
715
+ ...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
716
+ ...(projectRead.referenceOnly.length === 0
717
+ ? {}
718
+ : { referenceOnly: projectRead.referenceOnly }),
719
+ };
720
+ const unanswered = [
721
+ ...projectRead.held.unanswered,
722
+ ...projectRead.rooted.unanswered,
723
+ ...made
724
+ .filter((reference) => reference.resolution === "by-name")
725
+ .map((reference) => reference.to),
726
+ ...exempt.filter(isUnansweredRecord).map((record) => record.id),
727
+ ];
728
+ const components = project.ownSourceFiles().filter(isComponentFile);
729
+ const pathOf = (file: (typeof components)[number]): string =>
730
+ relativePath(targetRoot, file.fileName) ?? file.fileName;
731
+ return {
732
+ configFile: projectRead.configFile,
733
+ configured,
734
+ exempt,
735
+ unanswered,
736
+ typeErrorSkips: projectRead.typeErrorSkips,
737
+ skipped: projectRead.skipped,
738
+ componentFiles: components.map(pathOf),
739
+ componentWarnings: components.flatMap((file) =>
740
+ readComponent(file.originalText).warnings.map((warning) => ({
741
+ path: pathOf(file),
742
+ line: lineOf(file.originalText, warning.offset),
743
+ reason: warning.reason,
744
+ })),
745
+ ),
746
+ extra: extra(project, projectRead, made),
747
+ };
748
+ });
484
749
  const consumed = read.consumers.references;
485
750
  const projects = read.answers.map((one) =>
486
751
  consumed.length === 0
@@ -506,9 +771,93 @@ function readRun<Extra>(
506
771
  consumers: read.consumers.loaded,
507
772
  notBuilt: read.notBuilt,
508
773
  unanswered: read.unanswered,
774
+ componentsUnread: read.componentsUnread,
775
+ conventionsApplied: read.conventionsApplied,
509
776
  };
510
777
  }
511
778
 
779
+ /**
780
+ * The run swept under one set of marks with the records that hold. A record a component
781
+ * file's markup holds counts only while the file is live, and what it holds may be what
782
+ * keeps the file live, so each file's markup joins only once a sweep without it finds the
783
+ * file live, until a sweep finds no further file live.
784
+ */
785
+ function holdingSweep<Extra>(
786
+ read: ReadRun<Extra>,
787
+ input: Pick<SweepInput, "marked" | "mode">,
788
+ ): { readonly swept: SweepInput; readonly result: SweepResult } {
789
+ const paths = new Set(read.exempt.flatMap((record) => record.whileLive ?? []));
790
+ let live = new Set<string>();
791
+ for (;;) {
792
+ const swept: SweepInput = {
793
+ ...input,
794
+ exempt: holdingWhile(read.exempt, live),
795
+ unanswered: read.unansweredHeld,
796
+ };
797
+ const result = sweepMatrix(read.matrix, swept);
798
+ const next = new Set(liveFilesAt(read.matrix, result, paths).map((one) => one.position.path));
799
+ if (next.size === live.size) {
800
+ return { swept, result };
801
+ }
802
+ live = next;
803
+ }
804
+ }
805
+
806
+ /**
807
+ * The test references exemption evidence written in a test file makes: each is a read of
808
+ * the declaration it would retain, made by the test file at the evidence's site.
809
+ */
810
+ function evidenceReferences(
811
+ symbols: readonly InventorySymbol[],
812
+ inTestFiles: readonly Exemption[],
813
+ ): readonly Reference[] {
814
+ if (inTestFiles.length === 0) {
815
+ return [];
816
+ }
817
+ const files = new Map(
818
+ symbols
819
+ .filter((symbol) => symbol.kind === "file")
820
+ .map((symbol) => [symbol.position.path, symbol.id]),
821
+ );
822
+ return inTestFiles.flatMap((record) => {
823
+ const from = files.get(record.site.path);
824
+ return from === undefined
825
+ ? []
826
+ : [
827
+ {
828
+ from,
829
+ to: record.id,
830
+ position: record.site,
831
+ use: "read",
832
+ resolution: "syntax",
833
+ test: true,
834
+ },
835
+ ];
836
+ });
837
+ }
838
+
839
+ /**
840
+ * The declarations every reference the mode counts stores into, a loaded consumer's
841
+ * reference counting as a read: what the write-only kind would report but for a record.
842
+ */
843
+ function writtenOnly<Extra>(read: ReadRun<Extra>, mode: Mode): ReadonlySet<string> {
844
+ const written = new Set<string>();
845
+ const readSome = new Set<string>();
846
+ for (const { configured } of read.projects) {
847
+ for (const reference of configured.references) {
848
+ if (mode.production && reference.test) {
849
+ continue;
850
+ }
851
+ if (reference.use === "write" && reference.consumer === undefined) {
852
+ written.add(reference.to);
853
+ } else {
854
+ readSome.add(reference.to);
855
+ }
856
+ }
857
+ }
858
+ return new Set([...written].filter((id) => !readSome.has(id)));
859
+ }
860
+
512
861
  /**
513
862
  * The sweep of a run read once, under one set of marks. Where a question went unanswered,
514
863
  * the run is swept once more without what that question could have kept live and without
@@ -520,8 +869,8 @@ function sweptOf<Extra>(
520
869
  read: ReadRun<Extra>,
521
870
  input: Pick<SweepInput, "marked" | "mode">,
522
871
  ): RunSweep {
523
- const swept: SweepInput = { ...input, exempt: read.exempt, unanswered: read.unansweredHeld };
524
- const result = sweepMatrix(read.matrix, swept);
872
+ const { swept, result } = holdingSweep(read, input);
873
+ const exempt = swept.exempt ?? [];
525
874
  const held = new Set(read.unansweredHeld);
526
875
  if (held.size > 0) {
527
876
  // What a declaration reads where the checker did not answer is unknown, so a rule
@@ -542,7 +891,7 @@ function sweptOf<Extra>(
542
891
  );
543
892
  const bare: SweepInput = {
544
893
  ...swept,
545
- exempt: read.exempt.filter((record) => !isUnansweredRecord(record)),
894
+ exempt: exempt.filter((record) => !isUnansweredRecord(record)),
546
895
  unanswered: [],
547
896
  };
548
897
  for (const candidate of sweepMatrix(answered, bare).candidates) {
@@ -558,10 +907,13 @@ function sweptOf<Extra>(
558
907
  return {
559
908
  matrix: read.matrix,
560
909
  sweep: result,
561
- retained: retainedIn(read.matrix, swept),
910
+ exempt,
911
+ retained: retainedIn(read.matrix, swept, writtenOnly(read, input.mode)),
562
912
  notBuilt: read.notBuilt,
563
913
  unanswered: read.unanswered,
564
914
  heldByUnanswered: [...held],
915
+ skipped: read.projects.flatMap((one) => one.skipped),
916
+ componentFiles: [...new Set(read.projects.flatMap((one) => one.componentFiles))].sort(compare),
565
917
  };
566
918
  }
567
919
 
@@ -596,6 +948,7 @@ interface EmitterExtra {
596
948
  readonly needs: ReturnType<typeof projectNeeds>;
597
949
  readonly implementations: ReturnType<typeof implementations>;
598
950
  readonly heldByInclusion: readonly string[];
951
+ readonly generated: readonly string[];
599
952
  readonly rooted: Roots;
600
953
  readonly directives: readonly InlineDirective[];
601
954
  readonly declarationsNamed: readonly boolean[];
@@ -622,11 +975,16 @@ function readEmitterRun(
622
975
  (project, read, references) => ({
623
976
  deprecation: deprecatedDeclarations(project, read.held),
624
977
  accessors: accessorsOf(project, read.held),
625
- needs: projectNeeds(project, read.held, host),
978
+ needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) =>
979
+ engine.parseSourceFile(fileName, text),
980
+ ),
626
981
  implementations: implementations(project, read.held),
627
982
  heldByInclusion: heldByInclusion(project, targetRoot),
983
+ generated: [...read.generated.keys()],
628
984
  rooted: read.rooted,
629
- directives: inlineDirectives(project, read.held, targetRoot),
985
+ directives: inlineDirectives(project, read.held, targetRoot).filter(
986
+ (directive) => !read.referenceOnly.includes(directive.site.path),
987
+ ),
630
988
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
631
989
  parts: projectParts(project, read.held, references, targetRoot),
632
990
  }),
@@ -663,7 +1021,7 @@ function emitterInputOver(
663
1021
  references: one.configured.references,
664
1022
  accessors: one.extra.accessors,
665
1023
  })),
666
- read.exempt,
1024
+ swept.exempt,
667
1025
  input.mode,
668
1026
  ),
669
1027
  dependencies: dependenciesOf(
@@ -676,6 +1034,7 @@ function emitterInputOver(
676
1034
  files: {
677
1035
  tree: sourceTree(host, targetRoot),
678
1036
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
1037
+ generated: new Set(projects.flatMap((one) => one.extra.generated)),
679
1038
  },
680
1039
  boundary: {
681
1040
  consumers: {
@@ -689,7 +1048,7 @@ function emitterInputOver(
689
1048
  intraFunction: intraFunctionFacts(
690
1049
  projects.map((one) => one.extra.parts),
691
1050
  projects.flatMap((one) => one.configured.references),
692
- new Set([...read.exempt.map((one) => one.id), ...swept.heldByUnanswered]),
1051
+ new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered]),
693
1052
  ),
694
1053
  };
695
1054
  }
@@ -768,10 +1127,21 @@ function readAnalysis(
768
1127
  testFileRules: testFileRulesOf(
769
1128
  config.ts.testFiles,
770
1129
  read.projects.flatMap((one) => one.configured.testFiles),
1130
+ read.projects.flatMap((one) => one.configured.supportFiles ?? []),
771
1131
  ),
1132
+ typeErrorSkips: read.projects.flatMap((one) => one.typeErrorSkips),
1133
+ componentWarnings: [
1134
+ ...new Map(
1135
+ read.projects
1136
+ .flatMap((one) => one.componentWarnings)
1137
+ .map((one) => [`${one.path}:${String(one.line)}:${one.reason}`, one] as const),
1138
+ ).values(),
1139
+ ],
772
1140
  consumers: read.consumers,
773
1141
  notBuilt: read.notBuilt,
774
1142
  unanswered: read.unanswered,
1143
+ componentsUnread: read.componentsUnread,
1144
+ conventionsApplied: read.conventionsApplied,
775
1145
  },
776
1146
  configured: read.projects.map((one) => one.configured),
777
1147
  symbols,
@@ -842,6 +1212,20 @@ export interface RunFacts {
842
1212
  readonly notBuilt: readonly NotBuilt[];
843
1213
  /** Every question the checker could not answer, each once. */
844
1214
  readonly unanswered: readonly UnansweredQuestion[];
1215
+ /** Every type error that skipped a unit, in the run's order. */
1216
+ readonly typeErrorSkips: readonly TypeErrorSkip[];
1217
+ /** Every `<script` start tag a component file holds that no block was read for, each once. */
1218
+ readonly componentWarnings: readonly ComponentWarning[];
1219
+ /** Per configuration whose component files the run cannot read, the reason. */
1220
+ readonly componentsUnread: readonly ComponentsUnread[];
1221
+ /** The convention rows the run applied. */
1222
+ readonly conventionsApplied: readonly AppliedConvention[];
1223
+ }
1224
+
1225
+ /** One configuration whose component files a run cannot read. */
1226
+ export interface ComponentsUnread {
1227
+ readonly configFile: string;
1228
+ readonly reason: string;
845
1229
  }
846
1230
 
847
1231
  /** The findings of one run, beside what a report states about the run itself. */
@@ -874,6 +1258,25 @@ export function runAnalysis(
874
1258
  return analysisOf(reader, inputs);
875
1259
  }
876
1260
 
1261
+ /**
1262
+ * One count per configuration of the run in which the checker left a question unanswered:
1263
+ * the questions asked in its project, and the declarations of its own the run holds for them.
1264
+ */
1265
+ export function unansweredCounts(analysis: RunAnalysis): readonly UnansweredCount[] {
1266
+ const held = new Set(analysis.swept.heldByUnanswered);
1267
+ return analysis.run.projects.flatMap((project) => {
1268
+ const questions = analysis.run.unanswered.filter(
1269
+ (one) => one.configFile === project.configFile,
1270
+ ).length;
1271
+ if (questions === 0) {
1272
+ return [];
1273
+ }
1274
+ const symbols = analysis.configured.find((one) => one.configuration === project.id)?.symbols;
1275
+ const declarations = new Set((symbols ?? []).map((one) => one.id).filter((id) => held.has(id)));
1276
+ return [{ configuration: project.id, questions, declarations: declarations.size }];
1277
+ });
1278
+ }
1279
+
877
1280
  function analysisOf(reader: AnalysisReader, inputs: AnalysisInputs): RunAnalysis {
878
1281
  return {
879
1282
  result: findingsPass(inputs),