@cplieger/deadset-ts 3.12.1 → 3.13.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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/src/analysis.js +100 -27
  3. package/dist/src/components.js +4 -2
  4. package/dist/src/configured-declarations.js +36 -0
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/edges.js +4 -1
  7. package/dist/src/findings/completion.js +37 -8
  8. package/dist/src/findings/emitters.js +5 -5
  9. package/dist/src/findings/intra-function.js +91 -2
  10. package/dist/src/findings/self-check.js +30 -7
  11. package/dist/src/findings/unused-declarations.js +15 -3
  12. package/dist/src/findings-pass.js +18 -11
  13. package/dist/src/intra-function-parts.js +643 -0
  14. package/dist/src/kinds.js +337 -35
  15. package/dist/src/reflective-lookup.js +35 -2
  16. package/dist/src/report.js +11 -2
  17. package/dist/src/reporters/sarif.js +20 -15
  18. package/dist/src/reporters/template.js +64 -5
  19. package/dist/src/serialization-contract.js +41 -22
  20. package/dist/src/signatures.js +32 -0
  21. package/dist/src/suppress-file.js +27 -6
  22. package/dist/src/suppress-inline.js +8 -1
  23. package/dist/src/suppress.js +27 -0
  24. package/dist/src/value-reach.js +33 -9
  25. package/dist/src/verbs/analyze.js +19 -11
  26. package/dist/src/verbs/describe.js +12 -8
  27. package/dist/src/version.js +1 -1
  28. package/jsr.json +1 -1
  29. package/package.json +1 -1
  30. package/src/analysis.ts +167 -48
  31. package/src/components.ts +4 -2
  32. package/src/configured-declarations.ts +57 -1
  33. package/src/conformance-record.ts +2 -2
  34. package/src/edges.ts +4 -1
  35. package/src/finding.ts +5 -1
  36. package/src/findings/completion.ts +46 -10
  37. package/src/findings/emitter.ts +3 -0
  38. package/src/findings/emitters.ts +9 -5
  39. package/src/findings/intra-function.ts +106 -3
  40. package/src/findings/self-check.ts +48 -7
  41. package/src/findings/unused-declarations.ts +18 -4
  42. package/src/findings-pass.ts +26 -11
  43. package/src/intra-function-parts.ts +871 -0
  44. package/src/kinds.ts +355 -41
  45. package/src/reflective-lookup.ts +42 -2
  46. package/src/report.ts +18 -2
  47. package/src/reporters/sarif.ts +24 -16
  48. package/src/reporters/template.ts +69 -6
  49. package/src/serialization-contract.ts +47 -30
  50. package/src/signatures.ts +55 -0
  51. package/src/suppress-file.ts +34 -6
  52. package/src/suppress-inline.ts +8 -1
  53. package/src/suppress.ts +31 -0
  54. package/src/value-reach.ts +53 -12
  55. package/src/verbs/analyze.ts +26 -11
  56. package/src/verbs/describe.ts +13 -9
  57. package/src/version.ts +1 -1
  58. package/dist/src/reporters/rules.js +0 -178
  59. package/src/reporters/rules.ts +0 -195
package/src/analysis.ts CHANGED
@@ -16,6 +16,11 @@ import {
16
16
  moduleIdentity,
17
17
  type LoadedConsumer,
18
18
  } from "./consumers.ts";
19
+ import {
20
+ configuredDeclarations,
21
+ configuredRef,
22
+ declarationsNamed,
23
+ } from "./configured-declarations.ts";
19
24
  import { decorator } from "./decorator.ts";
20
25
  import { dependenciesOf, projectNeeds } from "./dependencies.ts";
21
26
  import { deprecatedDeclarations } from "./deprecation.ts";
@@ -33,7 +38,7 @@ import {
33
38
  import type { TSExemptionClass } from "./exempt-classes.ts";
34
39
  import { heldByInclusion, sourceTree } from "./file-facts.ts";
35
40
  import type { Finding } from "./finding.ts";
36
- import { findingsPass, type PassResult } from "./findings-pass.ts";
41
+ import { findingsPass, recordedFindings, rowKey, type PassResult } from "./findings-pass.ts";
37
42
  import type { EmitterInput } from "./findings/emitter.ts";
38
43
  import {
39
44
  NO_SELF_CHECK,
@@ -45,7 +50,8 @@ import type { Host } from "./host.ts";
45
50
  import { implementations, mergeImplementations } from "./implementations.ts";
46
51
  import { injectionContainer } from "./injection-container.ts";
47
52
  import { interfaceSatisfaction } from "./interface-satisfaction.ts";
48
- import { inventory, type Inventory } from "./inventory.ts";
53
+ import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
54
+ import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
49
55
  import { readManifest } from "./manifest.ts";
50
56
  import {
51
57
  matrixOf,
@@ -64,8 +70,8 @@ import type { Scope } from "./scope.ts";
64
70
  import { serializationContract } from "./serialization-contract.ts";
65
71
  import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
66
72
  import { accessorsOf, storesOf } from "./stores.ts";
67
- import type { Suppressions } from "./suppress.ts";
68
- import { readBaseline, readIgnoreFile } from "./suppress-file.ts";
73
+ import { isPartKind, type SuppressionRecord, type Suppressions, type Verdict } from "./suppress.ts";
74
+ import { bindRows, readBaseline, readIgnoreFile, type Recorded } from "./suppress-file.ts";
69
75
  import { inlineDirectives, inlineSuppressions, type InlineDirective } from "./suppress-inline.ts";
70
76
  import type { Exemption, Mode, SweepInput } from "./sweep.ts";
71
77
  import { readTemplates, templateField } from "./template-field.ts";
@@ -345,7 +351,11 @@ function readRun<Extra>(
345
351
  config: Config,
346
352
  mode: Mode,
347
353
  detectors: Detectors,
348
- extra: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Extra,
354
+ extra: <Brand>(
355
+ project: ProjectView<Brand>,
356
+ read: ProjectRead,
357
+ references: readonly Reference[],
358
+ ) => Extra,
349
359
  ): ReadRun<Extra> {
350
360
  const targetRoot = scope.target.path;
351
361
  const disabled = disabledClasses(config);
@@ -377,7 +387,7 @@ function readRun<Extra>(
377
387
  configFile: projectRead.configFile,
378
388
  configured,
379
389
  exempt,
380
- extra: extra(project, projectRead),
390
+ extra: extra(project, projectRead, resolved.references),
381
391
  };
382
392
  });
383
393
  const consumed = read.consumers.references;
@@ -446,6 +456,8 @@ interface EmitterExtra {
446
456
  readonly heldByInclusion: readonly string[];
447
457
  readonly rooted: Roots;
448
458
  readonly directives: readonly InlineDirective[];
459
+ readonly declarationsNamed: readonly boolean[];
460
+ readonly parts: ProjectParts;
449
461
  }
450
462
 
451
463
  /** Reads a run's projects with every per-project fact an emitter reads. */
@@ -458,7 +470,7 @@ function readEmitterRun(
458
470
  detectors: Detectors,
459
471
  ): ReadRun<EmitterExtra> {
460
472
  const targetRoot = scope.target.path;
461
- return readRun(engine, host, scope, config, mode, detectors, (project, read) => ({
473
+ return readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
462
474
  deprecated: deprecatedDeclarations(project, read.held),
463
475
  accessors: accessorsOf(project, read.held),
464
476
  needs: projectNeeds(project, read.held, host),
@@ -466,6 +478,8 @@ function readEmitterRun(
466
478
  heldByInclusion: heldByInclusion(project, targetRoot),
467
479
  rooted: read.rooted,
468
480
  directives: inlineDirectives(project, read.held, targetRoot),
481
+ declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
482
+ parts: projectParts(project, read.held, references, targetRoot),
469
483
  }));
470
484
  }
471
485
 
@@ -516,6 +530,11 @@ function emitterInputOver(
516
530
  edges: readEdgeSides(host, targetRoot),
517
531
  },
518
532
  selfCheck,
533
+ intraFunction: intraFunctionFacts(
534
+ projects.map((one) => one.extra.parts),
535
+ projects.flatMap((one) => one.configured.references),
536
+ new Set(read.exempt.map((one) => one.id)),
537
+ ),
519
538
  };
520
539
  }
521
540
 
@@ -548,15 +567,25 @@ export interface AnalysisInputs {
548
567
  readonly suppressions: Suppressions;
549
568
  }
550
569
 
570
+ /** One run read once, and analyzed under any set of baseline rows without reading it again. */
571
+ interface AnalysisReader {
572
+ readonly run: RunFacts;
573
+ readonly configured: readonly Configured[];
574
+ /** The declarations of the run, which a baseline row binds against. */
575
+ readonly symbols: readonly InventorySymbol[];
576
+ /** The inputs of the run under the directives, the ignore file and the baseline given. */
577
+ readonly inputsUnder: (baseline: Suppressions) => AnalysisInputs;
578
+ }
579
+
551
580
  /**
552
581
  * 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
582
+ * directives, the ignore file's entries and the baseline's rows are bound before any
583
+ * sweep, and the run is swept once under every bound record's mark and once under none,
584
+ * so a record is decided against the finding its code would have produced. The
556
585
  * self-check family reports the refused suppressions and the roots that named nothing.
557
586
  * Beside the inputs it answers the projects and test-file rules a report names.
558
587
  */
559
- function runAnalysisInputs(
588
+ function readAnalysis(
560
589
  engine: Engine,
561
590
  host: Host,
562
591
  scope: Scope,
@@ -564,40 +593,17 @@ function runAnalysisInputs(
564
593
  provenance: Provenance,
565
594
  mode: Mode,
566
595
  detectors: Detectors,
567
- ): {
568
- readonly inputs: AnalysisInputs;
569
- readonly run: RunFacts;
570
- readonly configured: readonly Configured[];
571
- } {
596
+ ): AnalysisReader {
572
597
  const targetRoot = scope.target.path;
573
598
  const read = readEmitterRun(engine, host, scope, config, mode, detectors);
574
599
  const symbols = read.matrix.union.symbols;
575
- const documents = [
600
+ const fixed = [
576
601
  inlineSuppressions(read.projects.map((one) => one.extra.directives)),
577
602
  readIgnoreFile(host, targetRoot, symbols),
578
- readBaseline(host, targetRoot, symbols),
579
603
  ];
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
- );
604
+ const inputsUnder = (baseline: Suppressions): AnalysisInputs =>
605
+ inputsOver(host, scope, config, provenance, mode, read, [...fixed, baseline]);
595
606
  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
607
  run: {
602
608
  projects: read.projects.map((one) => ({
603
609
  id: one.configured.configuration,
@@ -610,6 +616,52 @@ function runAnalysisInputs(
610
616
  consumers: read.consumers,
611
617
  },
612
618
  configured: read.projects.map((one) => one.configured),
619
+ symbols,
620
+ inputsUnder,
621
+ };
622
+ }
623
+
624
+ /** The inputs of one read run under the suppression documents given, in reading order. */
625
+ function inputsOver(
626
+ host: Host,
627
+ scope: Scope,
628
+ config: Config,
629
+ provenance: Provenance,
630
+ mode: Mode,
631
+ read: ReadRun<EmitterExtra>,
632
+ documents: readonly Suppressions[],
633
+ ): AnalysisInputs {
634
+ const targetRoot = scope.target.path;
635
+ const suppressions: Suppressions = {
636
+ records: documents.flatMap((one) => one.records),
637
+ refusals: documents.flatMap((one) => one.refusals),
638
+ };
639
+ const facts: SelfCheckFacts = {
640
+ refusals: suppressions.refusals,
641
+ unmatchedRoots: unmatchedEverywhere(
642
+ config.rootPatterns,
643
+ read.projects.map((one) => one.extra.rooted),
644
+ ),
645
+ rootsDocument: documentOf(host, provenance, ROOTS_SETTING, targetRoot),
646
+ unmatchedDeclarations: configuredDeclarations(config)
647
+ .filter((_, at) => !read.projects.some((one) => one.extra.declarationsNamed[at] === true))
648
+ .map((one) => ({
649
+ ref: configuredRef(one.entry),
650
+ key: one.key,
651
+ document: documentOf(host, provenance, one.key, targetRoot),
652
+ })),
653
+ };
654
+ const marked = [
655
+ ...new Set(
656
+ suppressions.records
657
+ .filter((record) => !isPartKind(record.code))
658
+ .map((record) => record.bound),
659
+ ),
660
+ ].filter((id) => id !== "");
661
+ return {
662
+ marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
663
+ unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
664
+ suppressions,
613
665
  };
614
666
  }
615
667
 
@@ -655,14 +707,81 @@ export function runAnalysis(
655
707
  mode: Mode,
656
708
  detectors: Detectors = DETECTORS,
657
709
  ): 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 };
710
+ const reader = readAnalysis(engine, host, scope, config, provenance, mode, detectors);
711
+ const inputs = reader.inputsUnder(readBaseline(host, scope.target.path, reader.symbols));
712
+ return analysisOf(reader, inputs);
713
+ }
714
+
715
+ function analysisOf(reader: AnalysisReader, inputs: AnalysisInputs): RunAnalysis {
716
+ return {
717
+ result: findingsPass(inputs),
718
+ run: reader.run,
719
+ swept: inputs.marked.swept,
720
+ configured: reader.configured,
721
+ };
722
+ }
723
+
724
+ /** A baseline write: the analysis of its first round, and the rows its fixpoint holds. */
725
+ interface BaselineWrite {
726
+ /** The first round, which reads no baseline. */
727
+ readonly first: RunAnalysis;
728
+ /** The rows that remain when a round records no row and removes none, in recording order. */
729
+ readonly rows: readonly Recorded[];
730
+ }
731
+
732
+ /**
733
+ * The fixpoint a baseline write records. The first round reads no baseline; each later
734
+ * round reads the rows recorded so far, records a row for each recordable finding no row
735
+ * already names, and removes every row it reports stale. A removed row is not recorded
736
+ * again, so the write ends.
737
+ */
738
+ export function runBaselineWrite(
739
+ engine: Engine,
740
+ host: Host,
741
+ scope: Scope,
742
+ config: Config,
743
+ provenance: Provenance,
744
+ mode: Mode,
745
+ reason: string,
746
+ detectors: Detectors = DETECTORS,
747
+ ): BaselineWrite {
748
+ const reader = readAnalysis(engine, host, scope, config, provenance, mode, detectors);
749
+ const first = analysisOf(reader, reader.inputsUnder({ records: [], refusals: [] }));
750
+ let rows: readonly Recorded[] = [];
751
+ const removed = new Set<string>();
752
+ let result = first.result;
753
+ let stale: ReadonlySet<string> = new Set<string>();
754
+ for (;;) {
755
+ const held = new Set(rows.map(rowKey));
756
+ const added = recordedFindings(result.findings).filter(
757
+ (row) => !held.has(rowKey(row)) && !removed.has(rowKey(row)),
758
+ );
759
+ if (added.length === 0 && stale.size === 0) {
760
+ return { first, rows };
761
+ }
762
+ for (const key of stale) {
763
+ removed.add(key);
764
+ }
765
+ rows = [...rows.filter((row) => !stale.has(rowKey(row))), ...added];
766
+ const inputs = reader.inputsUnder(bindRows(rows, reason, reader.symbols));
767
+ result = findingsPass(inputs);
768
+ stale = staleRows(inputs.suppressions.records, result.ledger.verdicts);
769
+ }
770
+ }
771
+
772
+ /** The baseline rows a round reports stale: every record a row binds to matched nothing. */
773
+ function staleRows(
774
+ records: readonly SuppressionRecord[],
775
+ verdicts: readonly Verdict[],
776
+ ): ReadonlySet<string> {
777
+ const live = new Set<string>();
778
+ const stale = new Set<string>();
779
+ records.forEach((record, at) => {
780
+ if (record.mechanism !== "baseline") {
781
+ return;
782
+ }
783
+ const key = rowKey(record);
784
+ (verdicts[at] === "stale" ? stale : live).add(key);
785
+ });
786
+ return new Set([...stale].filter((key) => !live.has(key)));
668
787
  }
package/src/components.ts CHANGED
@@ -132,7 +132,8 @@ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number
132
132
  }
133
133
 
134
134
  /**
135
- * The dead declarations by their place in the subgraph, and the edges between them: each
135
+ * The dead declarations by their place in the subgraph, a test file's declaration only
136
+ * where it is a test of dead code, and the edges between them: each
136
137
  * reference one makes to another, an edge each way between a dead member and its dead
137
138
  * container (one direction alone leaves the member a component the container reaches),
138
139
  * and an edge back from each production target of an admitted test, which puts the test in
@@ -145,7 +146,8 @@ function deadSubgraph(
145
146
  ): { readonly at: readonly number[]; readonly adjacent: readonly (readonly number[])[] } {
146
147
  const at: number[] = [];
147
148
  const position = graph.symbols.map((_symbol, index) => {
148
- if (dead[index] !== true) {
149
+ // A test file's declaration belongs to a component only as a test of dead code.
150
+ if (dead[index] !== true || (graph.test[index] === true && testOfDeadCode[index] !== true)) {
149
151
  return UNVISITED;
150
152
  }
151
153
  at.push(index);
@@ -23,7 +23,7 @@ import {
23
23
  } from "@typescript/native/unstable/sync";
24
24
  import type { AliasChains } from "./alias-chain.ts";
25
25
  import { resolvedTargets } from "./calls.ts";
26
- import type { DeclarationEntry } from "./config.ts";
26
+ import type { Config, DeclarationEntry } from "./config.ts";
27
27
  import type { Inventory } from "./inventory.ts";
28
28
  import { nameComponent } from "./ref.ts";
29
29
  import type { ProjectView } from "./session.ts";
@@ -306,3 +306,59 @@ export function spelledEntry(entry: DeclarationEntry, held: Inventory): string {
306
306
  return entry.global;
307
307
  }
308
308
  }
309
+
310
+ /** One entry of a configuration key that names a declaration, and the key's dotted path. */
311
+ interface ConfiguredDeclaration {
312
+ readonly key: string;
313
+ readonly entry: DeclarationEntry;
314
+ }
315
+
316
+ /** The key under which a lifecycle contract's declarations are configured. */
317
+ const LIFECYCLE_KEY = "ts.lifecycle_contracts";
318
+
319
+ /**
320
+ * Every entry of the three keys that name declarations, in the configuration's order:
321
+ * the injection registrations, each lifecycle contract's components and then its bases,
322
+ * and the serializers.
323
+ */
324
+ export function configuredDeclarations(config: Config): readonly ConfiguredDeclaration[] {
325
+ return [
326
+ ...config.ts.injectionRegistrations.map((entry) => ({
327
+ key: "ts.injection_registrations",
328
+ entry,
329
+ })),
330
+ ...config.ts.lifecycleContracts.flatMap((contract) =>
331
+ [...contract.components, ...contract.bases].map((entry) => ({ key: LIFECYCLE_KEY, entry })),
332
+ ),
333
+ ...config.ts.serializers.map((entry) => ({ key: "ts.serializers", entry })),
334
+ ];
335
+ }
336
+
337
+ /** Whether each configured entry names a declaration in one project, in the entries' order. */
338
+ export function declarationsNamed<Brand>(
339
+ project: ProjectView<Brand>,
340
+ held: Inventory,
341
+ configured: readonly ConfiguredDeclaration[],
342
+ ): readonly boolean[] {
343
+ return resolveEntries(
344
+ project,
345
+ held,
346
+ configured.map((one) => one.entry),
347
+ ).map(namesADeclaration);
348
+ }
349
+
350
+ /**
351
+ * The reference a finding about a configured entry names it by, the entry as the
352
+ * configuration spells it: a symbol entry's reference, a module entry's module, a number
353
+ * sign and its name, and a global entry's path after a number sign.
354
+ */
355
+ export function configuredRef(entry: DeclarationEntry): string {
356
+ switch (entry.shape) {
357
+ case "symbol":
358
+ return entry.symbol;
359
+ case "module":
360
+ return `${entry.module}#${entry.name}`;
361
+ case "global":
362
+ return `#${entry.global}`;
363
+ }
364
+ }
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "1.9.0",
5
+ corpusVersion: "2.0.0",
6
6
  result: "pass",
7
- digest: "sha256:3e9e40876551faa2c196311bf06d47d220ab5127a9fb8921969aae552c42e824",
7
+ digest: "sha256:1274315595d7b944d22eb0126081e24cf293c9fe53a236f4d263c714644f32ee",
8
8
  },
9
9
  gaps: [
10
10
  {
package/src/edges.ts CHANGED
@@ -13,6 +13,7 @@ import type { InventorySymbol } from "./inventory.ts";
13
13
  import { positionAt, walkDocument } from "./json-document.ts";
14
14
  import { joinPath } from "./paths.ts";
15
15
  import { positionKey } from "./position.ts";
16
+ import { isPartSubject } from "./suppress.ts";
16
17
 
17
18
  /** The name and the location the Contract fixes for the edges document. */
18
19
  export const EDGES_FILE = "deadset-edges.json";
@@ -219,7 +220,9 @@ export function evaluateEdges<F extends Finding>(
219
220
  }
220
221
  const held = new Map<string, number>();
221
222
  findings.forEach((finding, at) => {
222
- held.set(positionKey(finding.position), at);
223
+ if (!isPartSubject(finding.symbol.kind)) {
224
+ held.set(positionKey(finding.position), at);
225
+ }
223
226
  });
224
227
 
225
228
  const pending = new Set<number>();
package/src/finding.ts CHANGED
@@ -55,7 +55,7 @@ export interface FindingDetails {
55
55
  readonly narrowerVisibility?: "file" | "package";
56
56
  /** On `DS1201` and `DS1203`: the classes implementing the interface, in site order. */
57
57
  readonly implementations?: readonly PositionedSymbol[];
58
- /** On `DS1301`: every position the subject is written at, in site order. */
58
+ /** On `DS1301` and `DS1807`: every position the subject is written at, in site order. */
59
59
  readonly writePositions?: readonly FindingPosition[];
60
60
  /** On `DS1601`: the manifest section that declares the dependency. */
61
61
  readonly dependencyClass?: DependencySection;
@@ -63,6 +63,8 @@ export interface FindingDetails {
63
63
  readonly mechanism?: Mechanism;
64
64
  /** On `DS1701` and `DS1702`: the refused suppression as written, a member absent where it lacks one. */
65
65
  readonly entry?: SuppressionEntry;
66
+ /** On the intra-function kinds: the external rules that report the same kind, as the vocabulary lists them. */
67
+ readonly overlap?: readonly string[];
66
68
  }
67
69
 
68
70
  /** One suppression in the four-member form the ignore file and the baseline share. */
@@ -91,6 +93,8 @@ export interface FindingComponent {
91
93
  /** The number of the component's members. */
92
94
  readonly symbolCount: number;
93
95
  readonly deletableLines: number;
96
+ /** Every member, the subject included, where the run lists a component in full. */
97
+ readonly members?: readonly PositionedSymbol[];
94
98
  }
95
99
 
96
100
  /** One finding with every member the finding schema requires. */
@@ -6,12 +6,13 @@
6
6
 
7
7
  import type { RunSweep } from "../analysis.ts";
8
8
  import { componentId, type Component } from "../components.ts";
9
- import type { Confidence, Severity } from "../config.ts";
10
- import type { CompletedFinding, Finding, FindingComponent } from "../finding.ts";
9
+ import type { Cascade, Confidence, Severity } from "../config.ts";
10
+ import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
11
11
  import { OUTSIDE } from "../graph.ts";
12
12
  import { KINDS, type KindRow } from "../kinds.ts";
13
+ import { byPosition } from "../position.ts";
13
14
  import { FAMILY_KEY_LENGTH } from "../resolve.ts";
14
- import { isRowSubject, type Dials } from "../suppress.ts";
15
+ import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
15
16
  import type { Boundary } from "./boundary.ts";
16
17
  import type { EmitterInput } from "./emitter.ts";
17
18
 
@@ -83,14 +84,42 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
83
84
  return (id) => (published.has(id) ? outside : "certain");
84
85
  }
85
86
 
86
- /** The component each dead declaration of the run falls in, and none for a live one. */
87
- function findingComponents(swept: RunSweep): (id: string) => FindingComponent | undefined {
87
+ /**
88
+ * The component each dead declaration of the run falls in, and none for a live one, with
89
+ * its members listed in the canonical order where the run lists a component in full.
90
+ */
91
+ function findingComponents(
92
+ swept: RunSweep,
93
+ cascade: Cascade,
94
+ ): (id: string) => FindingComponent | undefined {
88
95
  const held = new Map<string, Component>();
89
96
  for (const component of swept.sweep.components) {
90
97
  for (const member of component.members) {
91
98
  held.set(member, component);
92
99
  }
93
100
  }
101
+ const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
102
+ const listed = (component: Component): readonly PositionedSymbol[] =>
103
+ component.members
104
+ .flatMap((member) => {
105
+ const symbol = symbols.get(member);
106
+ return symbol === undefined
107
+ ? []
108
+ : [
109
+ {
110
+ ref: symbol.ref,
111
+ name: symbol.name,
112
+ position: {
113
+ ...symbol.position,
114
+ endLine: Math.max(symbol.position.line, symbol.endLine),
115
+ },
116
+ },
117
+ ];
118
+ })
119
+ .sort(
120
+ (a, b) =>
121
+ byPosition(a.position, b.position) || (a.ref === b.ref ? 0 : a.ref < b.ref ? -1 : 1),
122
+ );
94
123
  return (id) => {
95
124
  const component = held.get(id);
96
125
  return component === undefined
@@ -100,6 +129,7 @@ function findingComponents(swept: RunSweep): (id: string) => FindingComponent |
100
129
  root: component.roots.includes(id),
101
130
  symbolCount: component.members.length,
102
131
  deletableLines: component.deletableLines,
132
+ ...(cascade === "full" ? { members: listed(component) } : {}),
103
133
  };
104
134
  };
105
135
  }
@@ -123,7 +153,7 @@ export function completed(
123
153
  const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
124
154
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
125
155
  const classOf = reachabilityClasses(input);
126
- const componentOf = findingComponents(swept);
156
+ const componentOf = findingComponents(swept, input.config.reporters.cascade);
127
157
  let minted = swept.sweep.components.length;
128
158
 
129
159
  return findings.map((finding): CompletedFinding => {
@@ -136,11 +166,15 @@ export function completed(
136
166
  const at = isRowSubject(finding.symbol.kind)
137
167
  ? OUTSIDE
138
168
  : (positions.get(finding.symbol.ref) ?? OUTSIDE);
169
+ // A part names the declaration that holds it, and is decided inside it rather than by
170
+ // the declaration's liveness, so it carries none of the declaration's sweep facts.
171
+ const part = isPartSubject(finding.symbol.kind);
139
172
  const symbol = union.symbols[at];
140
- const judged = union.subject[at] === true ? symbol : undefined;
173
+ const judged = !part && union.subject[at] === true ? symbol : undefined;
141
174
  const candidate = judged === undefined ? undefined : candidates.get(judged.id);
142
- const found = symbol === undefined ? undefined : componentOf(symbol.id);
143
- if (candidate !== undefined && found === undefined) {
175
+ const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
176
+ // A test file's declaration is a component member only as a test of dead code.
177
+ if (candidate !== undefined && found === undefined && union.test[at] !== true) {
144
178
  throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
145
179
  }
146
180
  let component = found;
@@ -173,7 +207,9 @@ export function completed(
173
207
  retainedBy: [],
174
208
  configurations:
175
209
  candidate?.configurations ??
176
- (judged === undefined ? swept.matrix.configurations : (swept.matrix.heldIn[at] ?? [])),
210
+ (judged === undefined && !part
211
+ ? swept.matrix.configurations
212
+ : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
177
213
  consumersLoaded: input.boundary.consumers.loaded,
178
214
  fixability: row.fixability,
179
215
  severity: severityOf(input, finding.code, row.defaultSeverity),
@@ -4,6 +4,7 @@ import type { Dependencies } from "../dependencies.ts";
4
4
  import type { FileFacts } from "../file-facts.ts";
5
5
  import type { Finding } from "../finding.ts";
6
6
  import type { Implementations } from "../implementations.ts";
7
+ import type { IntraFunctionFacts } from "../intra-function-parts.ts";
7
8
  import type { Stores } from "../stores.ts";
8
9
  import type { Boundary } from "./boundary.ts";
9
10
  import type { SelfCheckFacts } from "./self-check.ts";
@@ -30,6 +31,8 @@ export interface EmitterInput {
30
31
  readonly boundary: Boundary;
31
32
  /** The run's suppression outcomes and configured roots, which the self-check family reports. */
32
33
  readonly selfCheck: SelfCheckFacts;
34
+ /** The parts of the run's declarations, which the intra-function family reports. */
35
+ readonly intraFunction: IntraFunctionFacts;
33
36
  }
34
37
 
35
38
  /**
@@ -9,7 +9,7 @@ import { intraFunction } from "./intra-function.ts";
9
9
  import { nonCodeArtifacts } from "./non-code-artifacts.ts";
10
10
  import { readsAndWrites } from "./reads-and-writes.ts";
11
11
  import { selfCheck } from "./self-check.ts";
12
- import { unusedDeclarations } from "./unused-declarations.ts";
12
+ import { fallenMembers, unusedDeclarations } from "./unused-declarations.ts";
13
13
  import { visibilityNarrowing } from "./visibility-narrowing.ts";
14
14
 
15
15
  /** Every kind family this analyzer reports, each by its emitter, in code order. */
@@ -44,11 +44,14 @@ export interface DecidedFindings {
44
44
  readonly would: readonly CompletedFinding[];
45
45
  }
46
46
 
47
- /** Every family's findings over one sweep, completed by one step. */
48
- function completedOver(input: EmitterInput): readonly CompletedFinding[] {
47
+ /** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
48
+ function completedOver(
49
+ input: EmitterInput,
50
+ ...more: readonly Emitter[]
51
+ ): readonly CompletedFinding[] {
49
52
  return completed(
50
53
  input,
51
- [...EMITTERS.values()].flatMap((emit) => emit(input)),
54
+ [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)),
52
55
  );
53
56
  }
54
57
 
@@ -85,7 +88,8 @@ export function decidedFindings(
85
88
  suppressed: Suppressed = { unmarked: input, records: [] },
86
89
  ): DecidedFindings {
87
90
  const found = completedOver(input);
88
- const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked);
91
+ const unmarked =
92
+ suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
89
93
  const would = [...unmarked, ...found];
90
94
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
91
95
  const kept = reportable(