@cplieger/deadset-ts 3.12.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +217 -48
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/components.js +4 -2
  8. package/dist/src/configured-declarations.js +56 -7
  9. package/dist/src/conformance-record.js +2 -2
  10. package/dist/src/dependencies.js +34 -17
  11. package/dist/src/deprecation.js +35 -24
  12. package/dist/src/destructuring.js +37 -29
  13. package/dist/src/discover.js +105 -28
  14. package/dist/src/edges.js +4 -1
  15. package/dist/src/entry-points.js +7 -0
  16. package/dist/src/enum-group.js +46 -40
  17. package/dist/src/exempt.js +42 -1
  18. package/dist/src/findings/completion.js +37 -8
  19. package/dist/src/findings/emitters.js +20 -6
  20. package/dist/src/findings/interfaces.js +7 -1
  21. package/dist/src/findings/intra-function.js +91 -2
  22. package/dist/src/findings/self-check.js +30 -7
  23. package/dist/src/findings/unused-declarations.js +15 -3
  24. package/dist/src/findings-pass.js +18 -11
  25. package/dist/src/framework-lifecycle.js +8 -6
  26. package/dist/src/implementations.js +45 -23
  27. package/dist/src/interface-satisfaction.js +30 -27
  28. package/dist/src/intra-function-parts.js +660 -0
  29. package/dist/src/inventory.js +85 -4
  30. package/dist/src/json-chunks.js +76 -0
  31. package/dist/src/kinds.js +337 -35
  32. package/dist/src/partial.js +23 -0
  33. package/dist/src/query.js +207 -0
  34. package/dist/src/references.js +92 -20
  35. package/dist/src/reflective-lookup.js +46 -4
  36. package/dist/src/report.js +14 -3
  37. package/dist/src/reporters/annotations.js +9 -5
  38. package/dist/src/reporters/json.js +3 -2
  39. package/dist/src/reporters/sarif.js +22 -16
  40. package/dist/src/reporters/template.js +112 -12
  41. package/dist/src/reporters/text.js +8 -6
  42. package/dist/src/roots.js +27 -11
  43. package/dist/src/serialization-contract.js +53 -32
  44. package/dist/src/session.js +79 -39
  45. package/dist/src/signatures.js +32 -0
  46. package/dist/src/suppress-file.js +29 -7
  47. package/dist/src/suppress-inline.js +8 -1
  48. package/dist/src/suppress.js +27 -0
  49. package/dist/src/sweep.js +4 -1
  50. package/dist/src/value-reach.js +44 -25
  51. package/dist/src/verbs/analyze.js +39 -14
  52. package/dist/src/verbs/describe.js +12 -8
  53. package/dist/src/verbs/explain.js +22 -9
  54. package/dist/src/verbs/print-projects.js +40 -7
  55. package/dist/src/verbs/print-retained.js +6 -2
  56. package/dist/src/verbs/print-roots.js +5 -1
  57. package/dist/src/version.js +1 -1
  58. package/jsr.json +1 -1
  59. package/package.json +3 -3
  60. package/src/alias-chain.ts +75 -11
  61. package/src/analysis.ts +385 -104
  62. package/src/calls.ts +10 -9
  63. package/src/class-members.ts +10 -11
  64. package/src/components.ts +4 -2
  65. package/src/configured-declarations.ts +80 -8
  66. package/src/conformance-record.ts +2 -2
  67. package/src/dependencies.ts +42 -20
  68. package/src/deprecation.ts +53 -27
  69. package/src/destructuring.ts +50 -39
  70. package/src/discover.ts +177 -33
  71. package/src/edges.ts +4 -1
  72. package/src/entry-points.ts +7 -0
  73. package/src/enum-group.ts +56 -42
  74. package/src/exempt.ts +50 -2
  75. package/src/finding.ts +5 -1
  76. package/src/findings/completion.ts +46 -10
  77. package/src/findings/emitter.ts +3 -0
  78. package/src/findings/emitters.ts +27 -6
  79. package/src/findings/interfaces.ts +7 -1
  80. package/src/findings/intra-function.ts +106 -3
  81. package/src/findings/self-check.ts +48 -7
  82. package/src/findings/unused-declarations.ts +18 -4
  83. package/src/findings-pass.ts +26 -11
  84. package/src/framework-lifecycle.ts +10 -6
  85. package/src/host.ts +5 -3
  86. package/src/implementations.ts +59 -23
  87. package/src/interface-satisfaction.ts +30 -27
  88. package/src/intra-function-parts.ts +889 -0
  89. package/src/inventory.ts +97 -4
  90. package/src/json-chunks.ts +87 -0
  91. package/src/kinds.ts +355 -41
  92. package/src/partial.ts +40 -0
  93. package/src/query.ts +522 -0
  94. package/src/references.ts +119 -38
  95. package/src/reflective-lookup.ts +56 -6
  96. package/src/report.ts +32 -4
  97. package/src/reporters/annotations.ts +23 -21
  98. package/src/reporters/json.ts +3 -2
  99. package/src/reporters/reporter.ts +2 -2
  100. package/src/reporters/sarif.ts +27 -18
  101. package/src/reporters/template.ts +119 -14
  102. package/src/reporters/text.ts +12 -12
  103. package/src/roots.ts +34 -12
  104. package/src/serialization-contract.ts +59 -40
  105. package/src/session.ts +146 -58
  106. package/src/signatures.ts +55 -0
  107. package/src/suppress-file.ts +40 -8
  108. package/src/suppress-inline.ts +8 -1
  109. package/src/suppress.ts +31 -0
  110. package/src/sweep.ts +9 -4
  111. package/src/value-reach.ts +66 -28
  112. package/src/verbs/analyze.ts +51 -14
  113. package/src/verbs/describe.ts +13 -9
  114. package/src/verbs/explain.ts +20 -9
  115. package/src/verbs/print-projects.ts +53 -7
  116. package/src/verbs/print-retained.ts +6 -2
  117. package/src/verbs/print-roots.ts +5 -1
  118. package/src/version.ts +1 -1
  119. package/dist/src/reporters/rules.js +0 -178
  120. package/src/reporters/rules.ts +0 -195
package/src/analysis.ts CHANGED
@@ -8,7 +8,6 @@
8
8
  * project's checker, and that holds only for views the session itself hands out.
9
9
  */
10
10
 
11
- import type { Diagnostic } from "@typescript/native/unstable/sync";
12
11
  import type { Config, Provenance } from "./config.ts";
13
12
  import {
14
13
  consumerLoads,
@@ -16,16 +15,31 @@ import {
16
15
  moduleIdentity,
17
16
  type LoadedConsumer,
18
17
  } from "./consumers.ts";
18
+ import {
19
+ configuredDeclarations,
20
+ configuredRef,
21
+ declarationsNamed,
22
+ } from "./configured-declarations.ts";
19
23
  import { decorator } from "./decorator.ts";
20
24
  import { dependenciesOf, projectNeeds } from "./dependencies.ts";
21
- import { deprecatedDeclarations } from "./deprecation.ts";
22
- import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.ts";
25
+ import { deprecatedDeclarations, type Deprecation } from "./deprecation.ts";
26
+ import {
27
+ discoverProjects,
28
+ DiscoveryError,
29
+ isGuess,
30
+ projectErrors,
31
+ renderDiagnostic,
32
+ targetRelative,
33
+ type LocatedDiagnostic,
34
+ type NotBuilt,
35
+ } from "./discover.ts";
23
36
  import { readEdgeSides } from "./edges.ts";
24
37
  import { enumGroup } from "./enum-group.ts";
25
38
  import {
26
39
  computeExemptions,
27
40
  disabledClasses,
28
41
  exemptionsOf,
42
+ isUnansweredRecord,
29
43
  retainedIn,
30
44
  type Detector,
31
45
  type Detectors,
@@ -33,7 +47,7 @@ import {
33
47
  import type { TSExemptionClass } from "./exempt-classes.ts";
34
48
  import { heldByInclusion, sourceTree } from "./file-facts.ts";
35
49
  import type { Finding } from "./finding.ts";
36
- import { findingsPass, type PassResult } from "./findings-pass.ts";
50
+ import { findingsPass, recordedFindings, rowKey, type PassResult } from "./findings-pass.ts";
37
51
  import type { EmitterInput } from "./findings/emitter.ts";
38
52
  import {
39
53
  NO_SELF_CHECK,
@@ -45,7 +59,8 @@ import type { Host } from "./host.ts";
45
59
  import { implementations, mergeImplementations } from "./implementations.ts";
46
60
  import { injectionContainer } from "./injection-container.ts";
47
61
  import { interfaceSatisfaction } from "./interface-satisfaction.ts";
48
- import { inventory, type Inventory } from "./inventory.ts";
62
+ import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
63
+ import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
49
64
  import { readManifest } from "./manifest.ts";
50
65
  import {
51
66
  matrixOf,
@@ -56,6 +71,7 @@ import {
56
71
  type SweepResult,
57
72
  } from "./matrix.ts";
58
73
  import { relativePath, resolvePath } from "./paths.ts";
74
+ import type { UnansweredQuestion } from "./query.ts";
59
75
  import { byPosition, type Position } from "./position.ts";
60
76
  import { reflectiveLookup } from "./reflective-lookup.ts";
61
77
  import { references, testFileRulesOf, type Reference, type TestFileRule } from "./references.ts";
@@ -64,8 +80,8 @@ import type { Scope } from "./scope.ts";
64
80
  import { serializationContract } from "./serialization-contract.ts";
65
81
  import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
66
82
  import { accessorsOf, storesOf } from "./stores.ts";
67
- import type { Suppressions } from "./suppress.ts";
68
- import { readBaseline, readIgnoreFile } from "./suppress-file.ts";
83
+ import { isPartKind, type SuppressionRecord, type Suppressions, type Verdict } from "./suppress.ts";
84
+ import { bindRows, readBaseline, readIgnoreFile, type Recorded } from "./suppress-file.ts";
69
85
  import { inlineDirectives, inlineSuppressions, type InlineDirective } from "./suppress-inline.ts";
70
86
  import type { Exemption, Mode, SweepInput } from "./sweep.ts";
71
87
  import { readTemplates, templateField } from "./template-field.ts";
@@ -99,6 +115,10 @@ export interface RunRoots {
99
115
  readonly roots: readonly RunRoot[];
100
116
  /** One finding per configured root or pattern that named nothing in any project. */
101
117
  readonly findings: readonly Finding[];
118
+ /** The derived configurations the run dropped. */
119
+ readonly notBuilt: readonly NotBuilt[];
120
+ /** Every question the checker could not answer, each once. */
121
+ readonly unanswered: readonly UnansweredQuestion[];
102
122
  }
103
123
 
104
124
  /**
@@ -150,15 +170,19 @@ interface ConsumerReads {
150
170
  interface ReadProjects<Answer> {
151
171
  readonly answers: readonly Answer[];
152
172
  readonly consumers: ConsumerReads;
173
+ /** The derived configurations the run dropped, in the order it met them. */
174
+ readonly notBuilt: readonly NotBuilt[];
175
+ /** Every question the checker could not answer, each once. */
176
+ readonly unanswered: readonly UnansweredQuestion[];
153
177
  }
154
178
 
155
179
  /**
156
- * Reads every project the scope discovers, and every project of every consumer it
157
- * declares, in one snapshot. Each is checked for errors first. A target project is then
158
- * enumerated and rooted, and `stage` reads the rest of what it needs from the project
159
- * while its view is open; a consumer project is read for its references to the target's
160
- * declarations. A project carrying an error fails the run with no answer, as every verb
161
- * that reads declarations does, and so does a declared consumer that cannot be loaded.
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.
162
186
  */
163
187
  function readProjects<Answer>(
164
188
  engine: Engine,
@@ -183,18 +207,39 @@ function readProjects<Answer>(
183
207
  testFiles: config.ts.testFiles,
184
208
  publishedAPI: config.targetKind === "library",
185
209
  };
186
- const failures: Diagnostic[] = [];
210
+ const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
211
+ const notBuilt: NotBuilt[] = [...discovered.notBuilt];
212
+ const dropped = (configFile: string, error: string): void => {
213
+ notBuilt.push({
214
+ id: ids.get(configFile) ?? configFile,
215
+ configFile,
216
+ error: targetRelative(error, absoluteRoot),
217
+ });
218
+ };
219
+ const failures: LocatedDiagnostic[] = [];
187
220
  const consumed = new Map<string, Reference>();
188
- const { projects } = runSession(
221
+ const session = runSession(
189
222
  engine,
190
223
  [...discovered.configFiles, ...consumerOf.keys()],
191
224
  (project) => {
192
- const errors = diagnosticErrors(diagnosticsOf(project));
193
- if (errors.length > 0) {
194
- failures.push(...errors);
225
+ const errors = projectErrors(diagnosticsOf(project));
226
+ const consumer = consumerOf.get(project.configFile);
227
+ const [unloaded] = errors.load;
228
+ if (
229
+ unloaded !== undefined &&
230
+ consumer === undefined &&
231
+ isGuess(discovered, project.configFile)
232
+ ) {
233
+ dropped(
234
+ project.configFile,
235
+ renderDiagnostic(unloaded, (path) => relativePath(absoluteRoot, path) ?? path),
236
+ );
237
+ return undefined;
238
+ }
239
+ if (errors.load.length > 0 || errors.check.length > 0) {
240
+ failures.push(...errors.load, ...errors.check);
195
241
  return undefined;
196
242
  }
197
- const consumer = consumerOf.get(project.configFile);
198
243
  if (consumer !== undefined) {
199
244
  for (const reference of consumerReferences(
200
245
  project,
@@ -215,19 +260,36 @@ function readProjects<Answer>(
215
260
  const configuration = ids.get(project.configFile) ?? project.configFile;
216
261
  return stage(project, { configuration, configFile: project.configFile, held, rooted });
217
262
  },
263
+ discovered.derived ? "record" : "refuse",
218
264
  );
265
+ for (const configFile of session.unopened) {
266
+ if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
267
+ throw new DiscoveryError(`${configFile} opened no project`, []);
268
+ }
269
+ dropped(configFile, "the compiler opened no project for the configuration");
270
+ }
219
271
  if (failures.length > 0) {
220
272
  throw new DiscoveryError(
221
273
  `${String(failures.length)} error(s); no answer was produced`,
222
274
  failures,
223
275
  );
224
276
  }
277
+ const answers = session.projects.filter((answer): answer is Answer => answer !== undefined);
278
+ const [first] = notBuilt;
279
+ if (answers.length === 0 && first !== undefined) {
280
+ throw new DiscoveryError(
281
+ `no configuration discovery derived could be built; ${first.id}: ${first.error}`,
282
+ [],
283
+ );
284
+ }
225
285
  return {
226
- answers: projects.filter((answer): answer is Answer => answer !== undefined),
286
+ answers,
227
287
  consumers: {
228
288
  loaded: loads.map((load) => load.consumer),
229
289
  references: [...consumed.values()],
230
290
  },
291
+ notBuilt,
292
+ unanswered: session.unanswered,
231
293
  };
232
294
  }
233
295
 
@@ -245,7 +307,8 @@ export function runRoots(
245
307
  provenance: Provenance,
246
308
  ): RunRoots {
247
309
  const targetRoot = scope.target.path;
248
- const projects = readProjects(engine, host, scope, config, (_project, read) => read).answers;
310
+ const read = readProjects(engine, host, scope, config, (_project, projectRead) => projectRead);
311
+ const projects = read.answers;
249
312
 
250
313
  const configurations: string[] = [];
251
314
  const merged = new Map<string, Omit<RunRoot, "configurations"> & { configurations: string[] }>();
@@ -289,6 +352,8 @@ export function runRoots(
289
352
  unmatched,
290
353
  documentOf(host, provenance, ROOTS_SETTING, targetRoot),
291
354
  ),
355
+ notBuilt: read.notBuilt,
356
+ unanswered: read.unanswered,
292
357
  };
293
358
  }
294
359
 
@@ -301,6 +366,15 @@ export interface RunSweep {
301
366
  * would otherwise judge dead, in the declarations' site order.
302
367
  */
303
368
  readonly retained: readonly Exemption[];
369
+ /** The derived configurations the run dropped. */
370
+ readonly notBuilt: readonly NotBuilt[];
371
+ /** Every question the checker could not answer, each once. */
372
+ readonly unanswered: readonly UnansweredQuestion[];
373
+ /**
374
+ * Every declaration an unanswered question could have kept live, and every one only
375
+ * such a declaration keeps live. The run reports none of them.
376
+ */
377
+ readonly heldByUnanswered: readonly string[];
304
378
  }
305
379
 
306
380
  /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
@@ -320,6 +394,8 @@ interface SweptProject<Extra> {
320
394
  readonly configFile: string;
321
395
  readonly configured: Configured;
322
396
  readonly exempt: readonly Exemption[];
397
+ /** The declarations of the project an unanswered question could have kept live. */
398
+ readonly unanswered: readonly string[];
323
399
  readonly extra: Extra;
324
400
  }
325
401
 
@@ -327,9 +403,15 @@ interface SweptProject<Extra> {
327
403
  interface ReadRun<Extra> {
328
404
  readonly matrix: Matrix;
329
405
  readonly exempt: readonly Exemption[];
406
+ /** The declarations of the run an unanswered question could have kept live, each once. */
407
+ readonly unansweredHeld: readonly string[];
330
408
  readonly projects: readonly SweptProject<Extra>[];
331
409
  /** Every declared consumer, each loaded, in the scope's order. */
332
410
  readonly consumers: readonly LoadedConsumer[];
411
+ /** The derived configurations the run dropped. */
412
+ readonly notBuilt: readonly NotBuilt[];
413
+ /** Every question the checker could not answer, each once. */
414
+ readonly unanswered: readonly UnansweredQuestion[];
333
415
  }
334
416
 
335
417
  /**
@@ -345,7 +427,11 @@ function readRun<Extra>(
345
427
  config: Config,
346
428
  mode: Mode,
347
429
  detectors: Detectors,
348
- extra: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Extra,
430
+ extra: <Brand>(
431
+ project: ProjectView<Brand>,
432
+ read: ProjectRead,
433
+ references: readonly Reference[],
434
+ ) => Extra,
349
435
  ): ReadRun<Extra> {
350
436
  const targetRoot = scope.target.path;
351
437
  const disabled = disabledClasses(config);
@@ -353,33 +439,48 @@ function readRun<Extra>(
353
439
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
354
440
  : readTemplates(host, targetRoot, config.analysis);
355
441
  const consumers = scope.consumers.map((consumer) => consumer.path);
356
- const read = readProjects(engine, host, scope, config, (project, projectRead) => {
357
- const resolved = references(project, projectRead.held, targetRoot, {
358
- testFiles: config.ts.testFiles,
359
- });
360
- const configured: Configured = {
361
- configuration: projectRead.configuration,
362
- symbols: projectRead.held.symbols,
363
- references: resolved.references,
364
- roots: projectRead.rooted.liveUnderReachability,
365
- testFiles: resolved.testFilePaths,
366
- };
367
- const exempt = computeExemptions(
368
- { project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers },
369
- detectors,
370
- {
371
- disabled,
372
- mode,
373
- testFiles: new Set(resolved.testFilePaths),
374
- },
375
- );
376
- return {
377
- configFile: projectRead.configFile,
378
- configured,
379
- exempt,
380
- extra: extra(project, projectRead),
381
- };
382
- });
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
+ );
383
484
  const consumed = read.consumers.references;
384
485
  const projects = read.answers.map((one) =>
385
486
  consumed.length === 0
@@ -395,21 +496,72 @@ function readRun<Extra>(
395
496
  return {
396
497
  matrix: matrixOf(projects.map((one) => one.configured)),
397
498
  exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
499
+ unansweredHeld: [
500
+ ...new Set([
501
+ ...read.answers.flatMap((one) => one.unanswered),
502
+ ...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
503
+ ]),
504
+ ],
398
505
  projects,
399
506
  consumers: read.consumers.loaded,
507
+ notBuilt: read.notBuilt,
508
+ unanswered: read.unanswered,
400
509
  };
401
510
  }
402
511
 
403
- /** The sweep of a run read once, under one set of marks. */
512
+ /**
513
+ * The sweep of a run read once, under one set of marks. Where a question went unanswered,
514
+ * the run is swept once more without what that question could have kept live and without
515
+ * the references that stood in for its answer, and every declaration that sweep judges
516
+ * dead and this one judges live, or dead by another rule, is held by the gap too, as is
517
+ * every declaration that makes such a reference.
518
+ */
404
519
  function sweptOf<Extra>(
405
520
  read: ReadRun<Extra>,
406
521
  input: Pick<SweepInput, "marked" | "mode">,
407
522
  ): RunSweep {
408
- const swept: SweepInput = { ...input, exempt: read.exempt };
523
+ const swept: SweepInput = { ...input, exempt: read.exempt, unanswered: read.unansweredHeld };
524
+ const result = sweepMatrix(read.matrix, swept);
525
+ const held = new Set(read.unansweredHeld);
526
+ if (held.size > 0) {
527
+ // What a declaration reads where the checker did not answer is unknown, so a rule
528
+ // over what it reads, the test of dead code among them, cannot judge it.
529
+ for (const { configured } of read.projects) {
530
+ for (const reference of configured.references) {
531
+ if (reference.resolution === "by-name") {
532
+ held.add(reference.from);
533
+ }
534
+ }
535
+ }
536
+ const dead = new Map(result.candidates.map((candidate) => [candidate.id, candidate]));
537
+ const answered = matrixOf(
538
+ read.projects.map(({ configured }) => ({
539
+ ...configured,
540
+ references: configured.references.filter((one) => one.resolution !== "by-name"),
541
+ })),
542
+ );
543
+ const bare: SweepInput = {
544
+ ...swept,
545
+ exempt: read.exempt.filter((record) => !isUnansweredRecord(record)),
546
+ unanswered: [],
547
+ };
548
+ for (const candidate of sweepMatrix(answered, bare).candidates) {
549
+ const kept = dead.get(candidate.id);
550
+ if (
551
+ kept?.testOfDeadCode !== candidate.testOfDeadCode ||
552
+ kept.relation !== candidate.relation
553
+ ) {
554
+ held.add(candidate.id);
555
+ }
556
+ }
557
+ }
409
558
  return {
410
559
  matrix: read.matrix,
411
- sweep: sweepMatrix(read.matrix, swept),
560
+ sweep: result,
412
561
  retained: retainedIn(read.matrix, swept),
562
+ notBuilt: read.notBuilt,
563
+ unanswered: read.unanswered,
564
+ heldByUnanswered: [...held],
413
565
  };
414
566
  }
415
567
 
@@ -439,13 +591,15 @@ export function runSweep(
439
591
 
440
592
  /** What each project contributes to the facts every emitter reads beside the sweep. */
441
593
  interface EmitterExtra {
442
- readonly deprecated: readonly string[];
594
+ readonly deprecation: Deprecation;
443
595
  readonly accessors: ReturnType<typeof accessorsOf>;
444
596
  readonly needs: ReturnType<typeof projectNeeds>;
445
597
  readonly implementations: ReturnType<typeof implementations>;
446
598
  readonly heldByInclusion: readonly string[];
447
599
  readonly rooted: Roots;
448
600
  readonly directives: readonly InlineDirective[];
601
+ readonly declarationsNamed: readonly boolean[];
602
+ readonly parts: ProjectParts;
449
603
  }
450
604
 
451
605
  /** Reads a run's projects with every per-project fact an emitter reads. */
@@ -458,15 +612,31 @@ function readEmitterRun(
458
612
  detectors: Detectors,
459
613
  ): ReadRun<EmitterExtra> {
460
614
  const targetRoot = scope.target.path;
461
- return readRun(engine, host, scope, config, mode, detectors, (project, read) => ({
462
- deprecated: deprecatedDeclarations(project, read.held),
463
- accessors: accessorsOf(project, read.held),
464
- needs: projectNeeds(project, read.held, host),
465
- implementations: implementations(project, read.held),
466
- heldByInclusion: heldByInclusion(project, targetRoot),
467
- rooted: read.rooted,
468
- directives: inlineDirectives(project, read.held, targetRoot),
469
- }));
615
+ const run = readRun(
616
+ engine,
617
+ host,
618
+ scope,
619
+ config,
620
+ mode,
621
+ detectors,
622
+ (project, read, references) => ({
623
+ deprecation: deprecatedDeclarations(project, read.held),
624
+ accessors: accessorsOf(project, read.held),
625
+ needs: projectNeeds(project, read.held, host),
626
+ implementations: implementations(project, read.held),
627
+ heldByInclusion: heldByInclusion(project, targetRoot),
628
+ rooted: read.rooted,
629
+ directives: inlineDirectives(project, read.held, targetRoot),
630
+ declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
631
+ parts: projectParts(project, read.held, references, targetRoot),
632
+ }),
633
+ );
634
+ // Whether a declaration carries the marker decides its code, so one whose marker went
635
+ // unread is reported by nothing.
636
+ const unread = run.projects.flatMap((one) => one.extra.deprecation.unanswered);
637
+ return unread.length === 0
638
+ ? run
639
+ : { ...run, unansweredHeld: [...new Set([...run.unansweredHeld, ...unread])] };
470
640
  }
471
641
 
472
642
  /**
@@ -487,7 +657,7 @@ function emitterInputOver(
487
657
  return {
488
658
  config,
489
659
  swept,
490
- deprecated: new Set(projects.flatMap((one) => one.extra.deprecated)),
660
+ deprecated: new Set(projects.flatMap((one) => one.extra.deprecation.deprecated)),
491
661
  stores: storesOf(
492
662
  projects.map((one) => ({
493
663
  references: one.configured.references,
@@ -516,6 +686,11 @@ function emitterInputOver(
516
686
  edges: readEdgeSides(host, targetRoot),
517
687
  },
518
688
  selfCheck,
689
+ intraFunction: intraFunctionFacts(
690
+ projects.map((one) => one.extra.parts),
691
+ projects.flatMap((one) => one.configured.references),
692
+ new Set([...read.exempt.map((one) => one.id), ...swept.heldByUnanswered]),
693
+ ),
519
694
  };
520
695
  }
521
696
 
@@ -548,15 +723,25 @@ export interface AnalysisInputs {
548
723
  readonly suppressions: Suppressions;
549
724
  }
550
725
 
726
+ /** One run read once, and analyzed under any set of baseline rows without reading it again. */
727
+ interface AnalysisReader {
728
+ readonly run: RunFacts;
729
+ readonly configured: readonly Configured[];
730
+ /** The declarations of the run, which a baseline row binds against. */
731
+ readonly symbols: readonly InventorySymbol[];
732
+ /** The inputs of the run under the directives, the ignore file and the baseline given. */
733
+ readonly inputsUnder: (baseline: Suppressions) => AnalysisInputs;
734
+ }
735
+
551
736
  /**
552
737
  * Reads the run the scope and the configuration describe for its findings. The inline
553
- * directives, the ignore file's entries and the baseline's rows are read and bound before
554
- * any sweep, and the run is swept once under every bound record's mark and once under
555
- * none, so a record is decided against the finding its code would have produced. The
738
+ * directives, the ignore file's entries and the baseline's rows are bound before any
739
+ * sweep, and the run is swept once under every bound record's mark and once under none,
740
+ * so a record is decided against the finding its code would have produced. The
556
741
  * self-check family reports the refused suppressions and the roots that named nothing.
557
742
  * Beside the inputs it answers the projects and test-file rules a report names.
558
743
  */
559
- function runAnalysisInputs(
744
+ function readAnalysis(
560
745
  engine: Engine,
561
746
  host: Host,
562
747
  scope: Scope,
@@ -564,40 +749,17 @@ function runAnalysisInputs(
564
749
  provenance: Provenance,
565
750
  mode: Mode,
566
751
  detectors: Detectors,
567
- ): {
568
- readonly inputs: AnalysisInputs;
569
- readonly run: RunFacts;
570
- readonly configured: readonly Configured[];
571
- } {
752
+ ): AnalysisReader {
572
753
  const targetRoot = scope.target.path;
573
754
  const read = readEmitterRun(engine, host, scope, config, mode, detectors);
574
755
  const symbols = read.matrix.union.symbols;
575
- const documents = [
756
+ const fixed = [
576
757
  inlineSuppressions(read.projects.map((one) => one.extra.directives)),
577
758
  readIgnoreFile(host, targetRoot, symbols),
578
- readBaseline(host, targetRoot, symbols),
579
759
  ];
580
- const suppressions: Suppressions = {
581
- records: documents.flatMap((one) => one.records),
582
- refusals: documents.flatMap((one) => one.refusals),
583
- };
584
- const facts: SelfCheckFacts = {
585
- refusals: suppressions.refusals,
586
- unmatchedRoots: unmatchedEverywhere(
587
- config.rootPatterns,
588
- read.projects.map((one) => one.extra.rooted),
589
- ),
590
- rootsDocument: documentOf(host, provenance, ROOTS_SETTING, targetRoot),
591
- };
592
- const marked = [...new Set(suppressions.records.map((record) => record.bound))].filter(
593
- (id) => id !== "",
594
- );
760
+ const inputsUnder = (baseline: Suppressions): AnalysisInputs =>
761
+ inputsOver(host, scope, config, provenance, mode, read, [...fixed, baseline]);
595
762
  return {
596
- inputs: {
597
- marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
598
- unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
599
- suppressions,
600
- },
601
763
  run: {
602
764
  projects: read.projects.map((one) => ({
603
765
  id: one.configured.configuration,
@@ -608,8 +770,56 @@ function runAnalysisInputs(
608
770
  read.projects.flatMap((one) => one.configured.testFiles),
609
771
  ),
610
772
  consumers: read.consumers,
773
+ notBuilt: read.notBuilt,
774
+ unanswered: read.unanswered,
611
775
  },
612
776
  configured: read.projects.map((one) => one.configured),
777
+ symbols,
778
+ inputsUnder,
779
+ };
780
+ }
781
+
782
+ /** The inputs of one read run under the suppression documents given, in reading order. */
783
+ function inputsOver(
784
+ host: Host,
785
+ scope: Scope,
786
+ config: Config,
787
+ provenance: Provenance,
788
+ mode: Mode,
789
+ read: ReadRun<EmitterExtra>,
790
+ documents: readonly Suppressions[],
791
+ ): AnalysisInputs {
792
+ const targetRoot = scope.target.path;
793
+ const suppressions: Suppressions = {
794
+ records: documents.flatMap((one) => one.records),
795
+ refusals: documents.flatMap((one) => one.refusals),
796
+ };
797
+ const facts: SelfCheckFacts = {
798
+ refusals: suppressions.refusals,
799
+ unmatchedRoots: unmatchedEverywhere(
800
+ config.rootPatterns,
801
+ read.projects.map((one) => one.extra.rooted),
802
+ ),
803
+ rootsDocument: documentOf(host, provenance, ROOTS_SETTING, targetRoot),
804
+ unmatchedDeclarations: configuredDeclarations(config)
805
+ .filter((_, at) => !read.projects.some((one) => one.extra.declarationsNamed[at] === true))
806
+ .map((one) => ({
807
+ ref: configuredRef(one.entry),
808
+ key: one.key,
809
+ document: documentOf(host, provenance, one.key, targetRoot),
810
+ })),
811
+ };
812
+ const marked = [
813
+ ...new Set(
814
+ suppressions.records
815
+ .filter((record) => !isPartKind(record.code))
816
+ .map((record) => record.bound),
817
+ ),
818
+ ].filter((id) => id !== "");
819
+ return {
820
+ marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
821
+ unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
822
+ suppressions,
613
823
  };
614
824
  }
615
825
 
@@ -628,6 +838,10 @@ export interface RunFacts {
628
838
  readonly testFileRules: readonly TestFileRule[];
629
839
  /** Every consumer the run loaded beside the target, in the scope's order. */
630
840
  readonly consumers: readonly LoadedConsumer[];
841
+ /** The derived configurations the run dropped. */
842
+ readonly notBuilt: readonly NotBuilt[];
843
+ /** Every question the checker could not answer, each once. */
844
+ readonly unanswered: readonly UnansweredQuestion[];
631
845
  }
632
846
 
633
847
  /** The findings of one run, beside what a report states about the run itself. */
@@ -655,14 +869,81 @@ export function runAnalysis(
655
869
  mode: Mode,
656
870
  detectors: Detectors = DETECTORS,
657
871
  ): RunAnalysis {
658
- const { inputs, run, configured } = runAnalysisInputs(
659
- engine,
660
- host,
661
- scope,
662
- config,
663
- provenance,
664
- mode,
665
- detectors,
666
- );
667
- return { result: findingsPass(inputs), run, swept: inputs.marked.swept, configured };
872
+ const reader = readAnalysis(engine, host, scope, config, provenance, mode, detectors);
873
+ const inputs = reader.inputsUnder(readBaseline(host, scope.target.path, reader.symbols));
874
+ return analysisOf(reader, inputs);
875
+ }
876
+
877
+ function analysisOf(reader: AnalysisReader, inputs: AnalysisInputs): RunAnalysis {
878
+ return {
879
+ result: findingsPass(inputs),
880
+ run: reader.run,
881
+ swept: inputs.marked.swept,
882
+ configured: reader.configured,
883
+ };
884
+ }
885
+
886
+ /** A baseline write: the analysis of its first round, and the rows its fixpoint holds. */
887
+ interface BaselineWrite {
888
+ /** The first round, which reads no baseline. */
889
+ readonly first: RunAnalysis;
890
+ /** The rows that remain when a round records no row and removes none, in recording order. */
891
+ readonly rows: readonly Recorded[];
892
+ }
893
+
894
+ /**
895
+ * The fixpoint a baseline write records. The first round reads no baseline; each later
896
+ * round reads the rows recorded so far, records a row for each recordable finding no row
897
+ * already names, and removes every row it reports stale. A removed row is not recorded
898
+ * again, so the write ends.
899
+ */
900
+ export function runBaselineWrite(
901
+ engine: Engine,
902
+ host: Host,
903
+ scope: Scope,
904
+ config: Config,
905
+ provenance: Provenance,
906
+ mode: Mode,
907
+ reason: string,
908
+ detectors: Detectors = DETECTORS,
909
+ ): BaselineWrite {
910
+ const reader = readAnalysis(engine, host, scope, config, provenance, mode, detectors);
911
+ const first = analysisOf(reader, reader.inputsUnder({ records: [], refusals: [] }));
912
+ let rows: readonly Recorded[] = [];
913
+ const removed = new Set<string>();
914
+ let result = first.result;
915
+ let stale: ReadonlySet<string> = new Set<string>();
916
+ for (;;) {
917
+ const held = new Set(rows.map(rowKey));
918
+ const added = recordedFindings(result.findings).filter(
919
+ (row) => !held.has(rowKey(row)) && !removed.has(rowKey(row)),
920
+ );
921
+ if (added.length === 0 && stale.size === 0) {
922
+ return { first, rows };
923
+ }
924
+ for (const key of stale) {
925
+ removed.add(key);
926
+ }
927
+ rows = [...rows.filter((row) => !stale.has(rowKey(row))), ...added];
928
+ const inputs = reader.inputsUnder(bindRows(rows, reason, reader.symbols));
929
+ result = findingsPass(inputs);
930
+ stale = staleRows(inputs.suppressions.records, result.ledger.verdicts);
931
+ }
932
+ }
933
+
934
+ /** The baseline rows a round reports stale: every record a row binds to matched nothing. */
935
+ function staleRows(
936
+ records: readonly SuppressionRecord[],
937
+ verdicts: readonly Verdict[],
938
+ ): ReadonlySet<string> {
939
+ const live = new Set<string>();
940
+ const stale = new Set<string>();
941
+ records.forEach((record, at) => {
942
+ if (record.mechanism !== "baseline") {
943
+ return;
944
+ }
945
+ const key = rowKey(record);
946
+ (verdicts[at] === "stale" ? stale : live).add(key);
947
+ });
948
+ return new Set([...stale].filter((key) => !live.has(key)));
668
949
  }