@cplieger/deadset-ts 3.6.0 → 3.7.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.
package/src/finding.ts CHANGED
@@ -1,13 +1,14 @@
1
+ import type { Confidence, Severity } from "./config.ts";
2
+ import type { Fixability } from "./kinds.ts";
3
+ import type { DependencySection } from "./ref.ts";
4
+ import type { Relation } from "./sweep.ts";
5
+
1
6
  /**
2
- * The members of a finding its emitter decides: the code, where the subject is,
3
- * what the subject is, and the sentence the finding reports. They are the
4
- * Contract's finding members of the same names, spelled in this language's case.
5
- *
6
- * Every other member the finding schema requires follows from these and from the
7
- * run: the kind's name, its fixability and its ceiling come from the code's row of
8
- * the issue-kind vocabulary, the severity and the confidence from the resolved
9
- * configuration, the language from the analyzer. A report completes them, so an
10
- * emitter cannot state one that disagrees with the vocabulary.
7
+ * The finding: what an emitter decides about one subject, and every member the
8
+ * Contract's finding schema requires, spelled in this language's case. An emitter states
9
+ * the code, where the subject is, what it is, the sentence, and the details its code
10
+ * carries; the completion step reads every other member from the run and from the code's
11
+ * row of the issue-kind vocabulary, so no emitter states one that disagrees with either.
11
12
  */
12
13
 
13
14
  /** Where the subject of a finding is. */
@@ -34,10 +35,75 @@ export interface FindingSubject {
34
35
  readonly sizeLines: number;
35
36
  }
36
37
 
37
- /** One finding, as its emitter states it. */
38
+ /** A declaration a finding names beside the subject, with where it is written. */
39
+ export interface PositionedSymbol {
40
+ readonly ref: string;
41
+ readonly name: string;
42
+ readonly position: FindingPosition;
43
+ }
44
+
45
+ /**
46
+ * What only some codes carry. The finding schema's branches decide which code carries
47
+ * which member, and every member is absent under every other code.
48
+ */
49
+ export interface FindingDetails {
50
+ /**
51
+ * On `DS1101` and `DS1104`: the widest scope that holds every reference to the
52
+ * subject, which is the visibility its references support.
53
+ */
54
+ readonly narrowerVisibility?: "file" | "package";
55
+ /** On `DS1201` and `DS1203`: the classes implementing the interface, in site order. */
56
+ readonly implementations?: readonly PositionedSymbol[];
57
+ /** On `DS1301`: every position the subject is written at, in site order. */
58
+ readonly writePositions?: readonly FindingPosition[];
59
+ /** On `DS1601`: the manifest section that declares the dependency. */
60
+ readonly dependencyClass?: DependencySection;
61
+ }
62
+
63
+ /** One finding as its emitter states it. */
38
64
  export interface Finding {
39
65
  readonly code: string;
40
66
  readonly position: FindingPosition;
41
67
  readonly symbol: FindingSubject;
42
68
  readonly message: string;
69
+ /** Absent where the code carries nothing. */
70
+ readonly details?: FindingDetails;
71
+ }
72
+
73
+ /** The dead component a finding names, as the finding schema spells its members. */
74
+ export interface FindingComponent {
75
+ readonly id: string;
76
+ /** Whether the subject is a root member of the component. */
77
+ readonly root: boolean;
78
+ /** The number of the component's members. */
79
+ readonly symbolCount: number;
80
+ readonly deletableLines: number;
81
+ }
82
+
83
+ /** One finding with every member the finding schema requires. */
84
+ export interface CompletedFinding extends Finding {
85
+ /** The name of the code's row of the issue-kind vocabulary. */
86
+ readonly kind: string;
87
+ readonly language: "ts";
88
+ readonly reachabilityClass: Confidence;
89
+ /** The reachability class capped by the code's ceiling. */
90
+ readonly confidence: Confidence;
91
+ /**
92
+ * The relation that decided the subject: present on a declaration the sweep judged
93
+ * dead, absent on a live one and on a subject that is no declaration.
94
+ */
95
+ readonly livenessRelation?: Relation;
96
+ /** Whether every reference to the subject comes from a test file. */
97
+ readonly testOnly: boolean;
98
+ readonly generated: boolean;
99
+ readonly component: FindingComponent;
100
+ /** Empty on every finding: a declaration an exemption holds back is not reported. */
101
+ readonly retainedBy: readonly string[];
102
+ /** The configurations the finding holds in, in the run's order. */
103
+ readonly configurations: readonly string[];
104
+ /** The consumers the run loaded beside the target. */
105
+ readonly consumersLoaded: readonly string[];
106
+ readonly fixability: Fixability;
107
+ readonly severity: Severity;
108
+ readonly details: FindingDetails;
43
109
  }
@@ -1,50 +1,23 @@
1
1
  /**
2
- * What a finding about a declaration carries beyond what its emitter decides, read from
3
- * the run: the reachability class and the confidence, the relation that decided the
4
- * subject, the dead component it falls in, the configurations it holds in, and the
5
- * severity the configuration gives its code. The dials withhold what they name and
6
- * every finding that falls with a root they withhold.
2
+ * What every finding carries beyond what its emitter decides, read from the run and from
3
+ * the code's row of the issue-kind vocabulary, and the dials that withhold a finding
4
+ * together with every finding that falls with a root they withhold.
7
5
  */
8
6
 
9
7
  import type { RunSweep } from "../analysis.ts";
10
8
  import { componentId, type Component } from "../components.ts";
11
9
  import type { Confidence, Config, Severity } from "../config.ts";
12
- import type { Finding } from "../finding.ts";
10
+ import type { CompletedFinding, Finding, FindingComponent } from "../finding.ts";
11
+ import { OUTSIDE } from "../graph.ts";
12
+ import { KINDS } from "../kinds.ts";
13
13
  import { FAMILY_KEY_LENGTH } from "../resolve.ts";
14
- import type { Relation } from "../sweep.ts";
15
-
16
- /** The dead component a finding names, as the finding schema spells its members. */
17
- export interface FindingComponent {
18
- readonly id: string;
19
- /** Whether the subject is a root member of the component. */
20
- readonly root: boolean;
21
- /** The number of the component's members. */
22
- readonly symbolCount: number;
23
- readonly deletableLines: number;
24
- }
25
-
26
- /** One finding with the members the run decides, in this language's case. */
27
- export interface CompletedFinding extends Finding {
28
- readonly reachabilityClass: Confidence;
29
- /** The reachability class capped by the code's ceiling. */
30
- readonly confidence: Confidence;
31
- /** The relation that decided the subject, absent where none did. */
32
- readonly livenessRelation?: Relation;
33
- /** Whether every reference to the subject comes from a test file. */
34
- readonly testOnly: boolean;
35
- readonly component: FindingComponent;
36
- /** The configurations the finding holds in, in the run's order. */
37
- readonly configurations: readonly string[];
38
- readonly severity: Severity;
39
- /** The per-code members of the finding schema's `details`, empty for a code with none. */
40
- readonly details: Readonly<Record<string, unknown>>;
41
- }
14
+ import type { EmitterInput } from "./emitter.ts";
42
15
 
43
16
  /** The classes ranked by the claim each makes, the strongest highest. */
44
- export const RANK: Readonly<Record<Confidence, number>> = { certain: 3, probable: 2, possible: 1 };
17
+ const RANK: Readonly<Record<Confidence, number>> = { certain: 3, probable: 2, possible: 1 };
45
18
 
46
19
  /** The weaker of two classes, which is a class capped by a ceiling. */
47
- export function capped(found: Confidence, ceiling: Confidence): Confidence {
20
+ function capped(found: Confidence, ceiling: Confidence): Confidence {
48
21
  return RANK[found] <= RANK[ceiling] ? found : ceiling;
49
22
  }
50
23
 
@@ -52,7 +25,7 @@ export function capped(found: Confidence, ceiling: Confidence): Confidence {
52
25
  * The severity the configuration gives one code: the key naming the code, else the key
53
26
  * naming its family, else the code's default.
54
27
  */
55
- export function severityOf(config: Config, code: string, fallback: Severity): Severity {
28
+ function severityOf(config: Config, code: string, fallback: Severity): Severity {
56
29
  return (
57
30
  config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
58
31
  );
@@ -65,7 +38,7 @@ export function severityOf(config: Config, code: string, fallback: Severity): Se
65
38
  * in the loaded program, a private member and a `#private` name among them, and is
66
39
  * `certain`.
67
40
  */
68
- export function reachabilityClasses(swept: RunSweep): (id: string) => Confidence {
41
+ function reachabilityClasses(swept: RunSweep): (id: string) => Confidence {
69
42
  const union = swept.matrix.union;
70
43
  const published = new Set(
71
44
  union.rooted
@@ -76,7 +49,7 @@ export function reachabilityClasses(swept: RunSweep): (id: string) => Confidence
76
49
  }
77
50
 
78
51
  /** The component each dead declaration of the run falls in, and none for a live one. */
79
- export function findingComponents(swept: RunSweep): (id: string) => FindingComponent | undefined {
52
+ function findingComponents(swept: RunSweep): (id: string) => FindingComponent | undefined {
80
53
  const held = new Map<string, Component>();
81
54
  for (const component of swept.sweep.components) {
82
55
  for (const member of component.members) {
@@ -96,35 +69,74 @@ export function findingComponents(swept: RunSweep): (id: string) => FindingCompo
96
69
  };
97
70
  }
98
71
 
99
- /** One finding with the component it names. */
100
- export type PlacedFinding = Finding & { readonly component: FindingComponent };
101
-
102
72
  /**
103
- * Every finding of the run, each with its component, in the order given: the dead
104
- * component its subject falls in. A subject in none, a live declaration, a file no
105
- * program builds or a manifest row, falls with nothing and is given a component of its
106
- * own, numbered past the run's computed components in the order the findings come, so
107
- * no two families mint one identifier.
73
+ * Every finding of the run completed, in the order given. A dead declaration carries its
74
+ * candidate's relation, counts, configurations and component; a live one carries no
75
+ * relation and holds where it is declared; a file or a manifest row is `certain` and
76
+ * holds in every configuration. A subject in no dead component gets one of its own,
77
+ * numbered past the computed ones in finding order, so no two families mint one.
108
78
  */
109
- export function placed(swept: RunSweep, findings: readonly Finding[]): readonly PlacedFinding[] {
79
+ export function completed(
80
+ input: EmitterInput,
81
+ findings: readonly Finding[],
82
+ ): readonly CompletedFinding[] {
83
+ const { swept } = input;
84
+ const union = swept.matrix.union;
85
+ const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
86
+ const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
87
+ const classOf = reachabilityClasses(swept);
110
88
  const componentOf = findingComponents(swept);
111
- const ids = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.ref, symbol.id]));
112
89
  let minted = swept.sweep.components.length;
113
- return findings.map((finding) => {
114
- const id = ids.get(finding.symbol.ref);
115
- const component = id === undefined ? undefined : componentOf(id);
116
- if (component !== undefined) {
117
- return { ...finding, component };
90
+
91
+ return findings.map((finding): CompletedFinding => {
92
+ const row = KINDS.get(finding.code);
93
+ if (row === undefined) {
94
+ throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
118
95
  }
119
- minted += 1;
120
- return {
121
- ...finding,
122
- component: {
96
+ const at = positions.get(finding.symbol.ref) ?? OUTSIDE;
97
+ const symbol = union.symbols[at];
98
+ const judged = union.subject[at] === true ? symbol : undefined;
99
+ const candidate = judged === undefined ? undefined : candidates.get(judged.id);
100
+ const found = symbol === undefined ? undefined : componentOf(symbol.id);
101
+ if (candidate !== undefined && found === undefined) {
102
+ throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
103
+ }
104
+ let component = found;
105
+ if (component === undefined) {
106
+ minted += 1;
107
+ component = {
123
108
  id: componentId(minted),
124
109
  root: true,
125
110
  symbolCount: 1,
126
111
  deletableLines: finding.symbol.sizeLines,
127
- },
112
+ };
113
+ }
114
+ const counts =
115
+ candidate === undefined
116
+ ? union.made[at]
117
+ : { production: candidate.productionRefs, test: candidate.testRefs };
118
+ const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
119
+ return {
120
+ code: finding.code,
121
+ kind: row.name,
122
+ language: "ts",
123
+ position: finding.position,
124
+ symbol: finding.symbol,
125
+ reachabilityClass,
126
+ confidence: capped(reachabilityClass, row.maxClass),
127
+ ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
128
+ testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
129
+ generated: false,
130
+ component,
131
+ retainedBy: [],
132
+ configurations:
133
+ candidate?.configurations ??
134
+ (judged === undefined ? swept.matrix.configurations : (swept.matrix.heldIn[at] ?? [])),
135
+ consumersLoaded: input.boundary.consumers.loaded,
136
+ fixability: row.fixability,
137
+ severity: severityOf(input.config, finding.code, row.defaultSeverity),
138
+ message: finding.message,
139
+ details: finding.details ?? {},
128
140
  };
129
141
  });
130
142
  }
@@ -1,26 +1,16 @@
1
- import type { Config, Severity } from "../config.ts";
2
1
  import type { Dependencies } from "../dependencies.ts";
3
2
  import type { Finding } from "../finding.ts";
4
3
  import { renderRef, type DependencySection } from "../ref.ts";
5
- import { severityOf } from "./completion.ts";
6
4
  import type { Emitter } from "./emitter.ts";
7
5
 
8
6
  const UNUSED_DEPENDENCY = "DS1601";
9
7
 
10
- /** The severity the kind ships at, which the configuration's keys override. */
11
- const UNUSED_DEPENDENCY_SEVERITY: Severity = "deny";
12
-
13
8
  const MESSAGES: Readonly<Record<DependencySection, string>> = {
14
9
  dependency: "no import in the target needs the declared dependency",
15
10
  "dev-dependency": "no import in the target needs the declared development dependency",
16
11
  "peer-dependency": "no import in the target needs the declared peer dependency",
17
12
  };
18
13
 
19
- /** An unused-dependency finding, with the section that declares the dependency. */
20
- export interface UnusedDependencyFinding extends Finding {
21
- readonly details: { readonly dependencyClass: DependencySection };
22
- }
23
-
24
14
  /**
25
15
  * The dependencies the target needs: every package the projects' uses name, every
26
16
  * declared dependency that ships a command, which a script, a workflow or a shell
@@ -48,14 +38,9 @@ function neededBy(dependencies: Dependencies): ReadonlySet<string> {
48
38
 
49
39
  /**
50
40
  * A dependency, development dependency or peer dependency of the target's manifest
51
- * that the target does not need, at the position of its key. Nothing is reported at
52
- * `allow`. Every subject is a manifest entry, which is `certain` whatever the
53
- * consumers, so no minimum confidence withholds one.
41
+ * that the target does not need, at the position of its key.
54
42
  */
55
- function unusedDependencies(config: Config, dependencies: Dependencies): UnusedDependencyFinding[] {
56
- if (severityOf(config, UNUSED_DEPENDENCY, UNUSED_DEPENDENCY_SEVERITY) === "allow") {
57
- return [];
58
- }
43
+ function unusedDependencies(dependencies: Dependencies): Finding[] {
59
44
  const needed = neededBy(dependencies);
60
45
  return dependencies.declared
61
46
  .filter((dependency) => !needed.has(dependency.name))
@@ -78,5 +63,5 @@ function unusedDependencies(config: Config, dependencies: Dependencies): UnusedD
78
63
  }
79
64
 
80
65
  /** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
81
- export const dependenciesAndModuleMachinery: Emitter = ({ config, dependencies }) =>
82
- unusedDependencies(config, dependencies);
66
+ export const dependenciesAndModuleMachinery: Emitter = ({ dependencies }) =>
67
+ unusedDependencies(dependencies);
@@ -30,8 +30,9 @@ export interface EmitterInput {
30
30
  }
31
31
 
32
32
  /**
33
- * One kind family's rule over one run: the findings of the codes in its family's range
34
- * of the Contract's code space, in no particular order.
33
+ * One kind family's rule over one run: every finding of the codes in its family's range
34
+ * of the Contract's code space, whatever severity and minimum confidence the
35
+ * configuration sets, in no particular order.
35
36
  */
36
37
  export type Emitter = (input: EmitterInput) => readonly Finding[];
37
38
 
@@ -1,5 +1,6 @@
1
1
  import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.ts";
2
- import { placed, type PlacedFinding } from "./completion.ts";
2
+ import type { CompletedFinding } from "../finding.ts";
3
+ import { completed, reportable } from "./completion.ts";
3
4
  import type { Emitter, EmitterInput, Emitters } from "./emitter.ts";
4
5
  import { interfaces } from "./interfaces.ts";
5
6
  import { intraFunction } from "./intra-function.ts";
@@ -22,12 +23,11 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
22
23
  ]);
23
24
 
24
25
  /**
25
- * Every finding of the run, each family's in table order, each placed in its component
26
- * by the one step that numbers the components minted for subjects that fall with none.
26
+ * Every finding of the run the dials do not withhold, each family's in table order: every
27
+ * family's findings completed by one step, then withheld by one step, so a root one
28
+ * family reports withholds what another family reports in its component.
27
29
  */
28
- export function findingsOf(input: EmitterInput): readonly PlacedFinding[] {
29
- return placed(
30
- input.swept,
31
- [...EMITTERS.values()].flatMap((emit) => emit(input)),
32
- );
30
+ export function findingsOf(input: EmitterInput): readonly CompletedFinding[] {
31
+ const found = [...EMITTERS.values()].flatMap((emit) => emit(input));
32
+ return reportable(completed(input, found), input.config.analysis.minConfidence);
33
33
  }
@@ -1,4 +1,4 @@
1
- import type { Finding, FindingPosition } from "../finding.ts";
1
+ import type { Finding, FindingPosition, PositionedSymbol } from "../finding.ts";
2
2
  import type { InventorySymbol } from "../inventory.ts";
3
3
  import { byPosition } from "../position.ts";
4
4
  import type { Candidate } from "../sweep.ts";
@@ -14,18 +14,6 @@ const UNUSED_DEAD_MESSAGE =
14
14
  const UNUSED_TEST_MESSAGE = "the interface is named as a type only from test files";
15
15
  const UNCALLED_MESSAGE = "no call site invokes or selects the method through the interface";
16
16
 
17
- /** One class a finding names as an implementation of its interface. */
18
- interface Implementation {
19
- readonly ref: string;
20
- readonly name: string;
21
- readonly position: FindingPosition;
22
- }
23
-
24
- /** A finding of this family, with the implementations its details carry. */
25
- interface InterfaceFinding extends Finding {
26
- readonly details: { readonly implementations: readonly Implementation[] };
27
- }
28
-
29
17
  /** Where one declaration is written, as a finding carries it. */
30
18
  function positionOf(symbol: InventorySymbol): FindingPosition {
31
19
  return { ...symbol.position, endLine: Math.max(symbol.position.line, symbol.endLine) };
@@ -44,7 +32,7 @@ function indexed(input: EmitterInput): {
44
32
  readonly symbolOf: (id: string) => InventorySymbol | undefined;
45
33
  readonly inTest: (id: string) => boolean;
46
34
  readonly candidateOf: (id: string) => Candidate | undefined;
47
- readonly implementationsOf: (id: string) => readonly Implementation[];
35
+ readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
48
36
  } {
49
37
  const union = input.swept.matrix.union;
50
38
  const symbols = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
@@ -70,8 +58,8 @@ function findingOf(
70
58
  code: string,
71
59
  symbol: InventorySymbol,
72
60
  message: string,
73
- implementations: readonly Implementation[],
74
- ): InterfaceFinding {
61
+ implementations: readonly PositionedSymbol[],
62
+ ): Finding {
75
63
  const position = positionOf(symbol);
76
64
  return {
77
65
  code,
@@ -96,7 +84,7 @@ function findingOf(
96
84
  */
97
85
  export const interfaces: Emitter = (input) => {
98
86
  const run = indexed(input);
99
- const found: InterfaceFinding[] = [];
87
+ const found: Finding[] = [];
100
88
  for (const candidate of input.swept.sweep.candidates) {
101
89
  const symbol = run.symbolOf(candidate.id);
102
90
  if (symbol === undefined || run.inTest(candidate.id)) {