@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/enum-group.ts CHANGED
@@ -33,7 +33,8 @@ import { aliasChains } from "./alias-chain.ts";
33
33
  import type { DetectorInput, Evidence } from "./exempt.ts";
34
34
  import { nodeKey } from "./inventory.ts";
35
35
  import { renderPosition } from "./position.ts";
36
- import { DEFAULT_BATCH_CAP } from "./references.ts";
36
+ import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
37
+ import type { ProjectView } from "./session.ts";
37
38
 
38
39
  /** The operand types a conversion produces an enum value from. */
39
40
  const NUMBER = TypeFlags.NumberLike;
@@ -146,25 +147,38 @@ function sitesOf(file: SourceFile): readonly Site[] {
146
147
  * union, and itself otherwise. An enum's own value, or one of its members, is a value the
147
148
  * source names, whichever number or string it also is.
148
149
  */
149
- function partsOf(type: Type): readonly Type[] {
150
- const parts = type.isUnionType() ? type.getTypes() : [type];
151
- return parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0);
150
+ function partsOf<Brand>(project: ProjectView<Brand>, type: Type): Answer<readonly Type[]> {
151
+ const parts = type.isUnionType() ? project.queries.constituents(type) : [type];
152
+ return isAnswered(parts)
153
+ ? parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0)
154
+ : parts;
152
155
  }
153
156
 
157
+ /** The detail a site records whose operand's type the checker could not answer. */
158
+ const UNTYPED_DETAIL = "converted from a value of unanswered type";
159
+
154
160
  /**
155
161
  * The detail one site records about the enum it produces, or undefined where the
156
162
  * operand's type converts nothing: a number, or a string an assertion converts, is
157
163
  * converted, a value of type `any` or `unknown` is decoded, and a value of an enum is one
158
164
  * the source names.
159
165
  */
160
- function detailOf(site: Site, enumName: string, operand: Type | undefined): string | undefined {
166
+ function detailOf<Brand>(
167
+ project: ProjectView<Brand>,
168
+ site: Site,
169
+ enumName: string,
170
+ operand: Answer<Type> | undefined,
171
+ ): string | undefined {
161
172
  if (site.form === "lookup") {
162
173
  return `looked up by an element access on ${enumName}`;
163
174
  }
164
175
  if (operand === undefined) {
165
176
  return undefined;
166
177
  }
167
- const parts = partsOf(operand);
178
+ const parts = operand === UNANSWERED ? UNANSWERED : partsOf(project, operand);
179
+ if (parts === UNANSWERED) {
180
+ return UNTYPED_DETAIL;
181
+ }
168
182
  if (parts.some((part) => (part.flags & NUMBER) !== 0)) {
169
183
  return "converted from number";
170
184
  }
@@ -204,35 +218,37 @@ export function enumGroup<Brand>(input: DetectorInput<Brand>): readonly Evidence
204
218
  }
205
219
 
206
220
  const sites = files.flatMap(sitesOf);
207
- const named = new Map<Site, string>();
221
+ const named = new Map<Site, readonly string[]>();
208
222
  const chains = aliasChains(project, held);
209
- for (let from = 0; from < sites.length; from += DEFAULT_BATCH_CAP) {
210
- const run = sites.slice(from, from + DEFAULT_BATCH_CAP);
211
- project.symbolsAt(run.map((site) => project.handle(site.name))).forEach((symbol, index) => {
212
- const site = run[index];
213
- if (symbol === undefined || site === undefined) {
214
- return;
215
- }
216
- const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
217
- if (target !== undefined) {
218
- named.set(site, target.id);
223
+ // A name the checker could not resolve may stand for any enum of the target it spells.
224
+ const enumsNamed = (text: string): readonly string[] =>
225
+ [...enums].flatMap(([id, name]) => (name === text ? [id] : []));
226
+ project.symbolsAt(sites.map((site) => project.handle(site.name))).forEach((symbol, index) => {
227
+ const site = sites[index];
228
+ if (symbol === undefined || site === undefined) {
229
+ return;
230
+ }
231
+ if (symbol === UNANSWERED) {
232
+ const ids = enumsNamed(site.name.getText());
233
+ if (ids.length > 0) {
234
+ named.set(site, ids);
219
235
  }
220
- });
221
- }
236
+ return;
237
+ }
238
+ const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
239
+ if (target !== undefined) {
240
+ named.set(site, [target.id]);
241
+ }
242
+ });
222
243
 
223
244
  const typed = sites.filter((site) => named.has(site) && site.operand !== undefined);
224
- const operandType = new Map<Site, Type>();
225
- for (let from = 0; from < typed.length; from += DEFAULT_BATCH_CAP) {
226
- const run = typed.slice(from, from + DEFAULT_BATCH_CAP);
227
- project.checker
228
- .getTypeAtLocation(run.map((site) => site.operand ?? site.at))
229
- .forEach((type, index) => {
230
- const site = run[index];
231
- if (type !== undefined && site !== undefined) {
232
- operandType.set(site, type);
233
- }
234
- });
235
- }
245
+ const operandType = new Map<Site, Answer<Type>>();
246
+ project.queries.typesAt(typed.map((site) => site.operand ?? site.at)).forEach((type, index) => {
247
+ const site = typed[index];
248
+ if (type !== undefined && site !== undefined) {
249
+ operandType.set(site, type);
250
+ }
251
+ });
236
252
 
237
253
  const members = new Map<string, string[]>();
238
254
  for (const symbol of held.symbols) {
@@ -248,17 +264,15 @@ export function enumGroup<Brand>(input: DetectorInput<Brand>): readonly Evidence
248
264
 
249
265
  const found: Evidence[] = [];
250
266
  for (const site of sites) {
251
- const enumId = named.get(site);
252
- if (enumId === undefined) {
253
- continue;
254
- }
255
- const detail = detailOf(site, enums.get(enumId) ?? "", operandType.get(site));
256
- if (detail === undefined) {
257
- continue;
258
- }
259
- const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
260
- for (const id of members.get(enumId) ?? []) {
261
- found.push({ id, detail, site: position });
267
+ for (const enumId of named.get(site) ?? []) {
268
+ const detail = detailOf(project, site, enums.get(enumId) ?? "", operandType.get(site));
269
+ if (detail === undefined) {
270
+ continue;
271
+ }
272
+ const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
273
+ for (const id of members.get(enumId) ?? []) {
274
+ found.push({ id, detail, site: position });
275
+ }
262
276
  }
263
277
  }
264
278
  return found;
package/src/exempt.ts CHANGED
@@ -18,9 +18,10 @@ import {
18
18
  type TSExemptionClass,
19
19
  type TypeScriptVisibility,
20
20
  } from "./exempt-classes.ts";
21
- import type { Inventory, InventorySymbol, Visibility } from "./inventory.ts";
21
+ import type { Inventory, InventorySymbol, SymbolKind, Visibility } from "./inventory.ts";
22
22
  import type { Matrix } from "./matrix.ts";
23
23
  import { byPosition, positionKey, type Position } from "./position.ts";
24
+ import { Unanswerable } from "./query.ts";
24
25
  import type { ProjectView } from "./session.ts";
25
26
  import type { Templates } from "./template-field.ts";
26
27
  import { sweep, type Exemption, type Mode, type SweepInput } from "./sweep.ts";
@@ -100,6 +101,53 @@ function retainable(allowed: TypeScriptVisibility, visibility: Visibility | unde
100
101
  return true;
101
102
  }
102
103
 
104
+ /**
105
+ * The kinds of declaration each class can hold back, which a detector that stops holds
106
+ * back whole. A class with no row here holds back every declaration but a file.
107
+ */
108
+ const RETAINABLE: ReadonlyMap<TSExemptionClass, ReadonlySet<SymbolKind>> = new Map([
109
+ ["interface-satisfaction", new Set<SymbolKind>(["method", "class-member"])],
110
+ ["enum-group", new Set<SymbolKind>(["enum-member"])],
111
+ ["injection-container", new Set<SymbolKind>(["method", "class-member"])],
112
+ ["framework-lifecycle", new Set<SymbolKind>(["method", "class-member"])],
113
+ [
114
+ "serialization-contract",
115
+ new Set<SymbolKind>(["method", "class-member", "interface-method", "type-member"]),
116
+ ],
117
+ ]);
118
+
119
+ /** The detail of a record a detector that could not finish holds. */
120
+ const UNANSWERED_DETAIL = "kept live by a question the checker did not answer";
121
+
122
+ /** Whether one record is held by a detector that could not finish. */
123
+ export function isUnansweredRecord(record: Exemption): boolean {
124
+ return record.detail === UNANSWERED_DETAIL;
125
+ }
126
+
127
+ /**
128
+ * What one detector holds back over one project. A detector that stops because the
129
+ * checker left a question it needs unanswered holds back every declaration of the
130
+ * project its class could hold back, each recorded at its own position, so the gap
131
+ * never yields a finding.
132
+ */
133
+ function detected<Brand>(
134
+ exemptionClass: TSExemptionClass,
135
+ detect: Detector,
136
+ input: DetectorInput<Brand>,
137
+ ): readonly Evidence[] {
138
+ try {
139
+ return detect(input);
140
+ } catch (error: unknown) {
141
+ if (!(error instanceof Unanswerable)) {
142
+ throw error;
143
+ }
144
+ const kinds = RETAINABLE.get(exemptionClass);
145
+ return input.held.symbols
146
+ .filter((symbol) => (kinds === undefined ? symbol.kind !== "file" : kinds.has(symbol.kind)))
147
+ .map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
148
+ }
149
+ }
150
+
103
151
  /**
104
152
  * The records one project's detectors found, each class in the vocabulary's order and
105
153
  * none of a switched-off class. Evidence on a private member the class's row says it
@@ -120,7 +168,7 @@ export function computeExemptions<Brand>(
120
168
  continue;
121
169
  }
122
170
  const allowed = typescriptVisibilityOf(exemptionClass);
123
- for (const evidence of detect(input)) {
171
+ for (const evidence of detected(exemptionClass, detect, input)) {
124
172
  if (!retainable(allowed, visibility.get(evidence.id))) {
125
173
  continue;
126
174
  }
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. */
@@ -24,6 +24,22 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
24
24
  ["intra-function", intraFunction],
25
25
  ]);
26
26
 
27
+ /**
28
+ * The references of every declaration an unanswered question could have kept live, which
29
+ * the run reports nothing about: a finding about one would be computed from the gap.
30
+ */
31
+ function heldRefs(input: EmitterInput): ReadonlySet<string> {
32
+ const held = new Set(input.swept.heldByUnanswered);
33
+ if (held.size === 0) {
34
+ return held;
35
+ }
36
+ return new Set(
37
+ input.swept.matrix.union.symbols
38
+ .filter((symbol) => held.has(symbol.id))
39
+ .map((symbol) => symbol.ref),
40
+ );
41
+ }
42
+
27
43
  /** What the suppressions of a run swept under their marks are decided against. */
28
44
  export interface Suppressed {
29
45
  /** The same run swept with no mark, which is where a kind about liveness reports a marked declaration. */
@@ -44,11 +60,14 @@ export interface DecidedFindings {
44
60
  readonly would: readonly CompletedFinding[];
45
61
  }
46
62
 
47
- /** Every family's findings over one sweep, completed by one step. */
48
- function completedOver(input: EmitterInput): readonly CompletedFinding[] {
63
+ /** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
64
+ function completedOver(
65
+ input: EmitterInput,
66
+ ...more: readonly Emitter[]
67
+ ): readonly CompletedFinding[] {
49
68
  return completed(
50
69
  input,
51
- [...EMITTERS.values()].flatMap((emit) => emit(input)),
70
+ [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)),
52
71
  );
53
72
  }
54
73
 
@@ -85,12 +104,14 @@ export function decidedFindings(
85
104
  suppressed: Suppressed = { unmarked: input, records: [] },
86
105
  ): DecidedFindings {
87
106
  const found = completedOver(input);
88
- const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked);
107
+ const unmarked =
108
+ suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
89
109
  const would = [...unmarked, ...found];
90
110
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
111
+ const unanswered = heldRefs(input);
91
112
  const kept = reportable(
92
113
  input,
93
- found.filter((finding) => !ledger.withheld(finding)),
114
+ found.filter((finding) => !ledger.withheld(finding) && !unanswered.has(finding.symbol.ref)),
94
115
  );
95
116
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
96
117
  return {
@@ -131,8 +131,14 @@ function unusedMessage(candidate: Candidate): string {
131
131
  : UNUSED_MESSAGE;
132
132
  }
133
133
 
134
- /** Whether one interface method has implementations and every one has an empty body. */
134
+ /**
135
+ * Whether one interface method has implementations and every one has an empty body, or
136
+ * is one whose implementations the run could not read in full.
137
+ */
135
138
  function marker(input: EmitterInput, method: string): boolean {
139
+ if (input.implementations.unknown.has(method)) {
140
+ return true;
141
+ }
136
142
  const bodies = [...(input.implementations.bodies.get(method)?.values() ?? [])];
137
143
  return bodies.length > 0 && bodies.every((empty) => empty);
138
144
  }
@@ -1,4 +1,107 @@
1
- import type { Emitter } from "./emitter.ts";
1
+ import type { Finding } from "../finding.ts";
2
+ import { KINDS } from "../kinds.ts";
3
+ import { UNUSED_PARAMETER, UNUSED_RESULT, type PartFact } from "../intra-function-parts.ts";
4
+ import type { Emitter, EmitterInput } from "./emitter.ts";
2
5
 
3
- /** The findings of the intra-function family, `DS1800` to `DS1899`. */
4
- export const intraFunction: Emitter = () => [];
6
+ /** A list of rules as a sentence names them. */
7
+ function spelled(rules: readonly string[]): string {
8
+ if (rules.length < 2) {
9
+ return rules.join("");
10
+ }
11
+ return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
12
+ }
13
+
14
+ /** The clause a message ends with: the other rules that report the kind, or that none is known. */
15
+ function overlapClause(overlap: readonly string[]): string {
16
+ return overlap.length === 1 && overlap[0] === "none known"
17
+ ? "no other rule is known to report it"
18
+ : `also reported by ${spelled(overlap)}`;
19
+ }
20
+
21
+ /**
22
+ * Whether one part is reported. A parameter needs its function's signature free to
23
+ * change; a result needs that, every call in the loaded program to discard it, and every
24
+ * call site to be in the loaded program, so a function with a root or one a module
25
+ * exports to callers the run cannot see has no result to report. Every other part is
26
+ * reported wherever no project found it in use.
27
+ */
28
+ function reported(
29
+ input: EmitterInput,
30
+ part: PartFact,
31
+ callersUnknown: ReadonlySet<string>,
32
+ ): boolean {
33
+ if (part.used) {
34
+ return false;
35
+ }
36
+ const { free, discardedEverywhere } = input.intraFunction;
37
+ switch (part.code) {
38
+ case UNUSED_PARAMETER:
39
+ return free.has(part.declaration);
40
+ case UNUSED_RESULT:
41
+ return (
42
+ free.has(part.declaration) &&
43
+ discardedEverywhere.has(part.declaration) &&
44
+ !callersUnknown.has(part.declaration)
45
+ );
46
+ default:
47
+ return true;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * The findings of the intra-function family, `DS1800` to `DS1899`, each about a part of
53
+ * the declaration its reference names, at the part's own position, whatever the sweep
54
+ * decided about that declaration. Each message names the other rules that report the
55
+ * kind, as the vocabulary lists them, and so does the finding's details.
56
+ */
57
+ export const intraFunction: Emitter = (input) => {
58
+ const union = input.swept.matrix.union;
59
+ const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
60
+ const unknown = new Set(
61
+ union.rooted.flatMap((root) => {
62
+ const symbol = union.symbols[root.at];
63
+ return symbol === undefined ? [] : [symbol.id];
64
+ }),
65
+ );
66
+ const { declared, loaded } = input.boundary.consumers;
67
+ const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
68
+ const published = (id: string): boolean => {
69
+ const symbol = byId.get(id);
70
+ return (
71
+ symbol !== undefined &&
72
+ symbol.kind !== "file" &&
73
+ (symbol.exported || published(symbol.parent))
74
+ );
75
+ };
76
+ if (!closed) {
77
+ for (const part of input.intraFunction.parts) {
78
+ if (published(part.declaration)) {
79
+ unknown.add(part.declaration);
80
+ }
81
+ }
82
+ }
83
+ const found: Finding[] = [];
84
+ for (const part of input.intraFunction.parts) {
85
+ const declaration = byId.get(part.declaration);
86
+ const overlap = KINDS.get(part.code)?.overlap ?? [];
87
+ if (declaration === undefined || !reported(input, part, unknown)) {
88
+ continue;
89
+ }
90
+ found.push({
91
+ code: part.code,
92
+ position: part.position,
93
+ symbol: {
94
+ ref: declaration.ref,
95
+ kind: part.kind,
96
+ name: part.name,
97
+ sizeLines: part.position.endLine - part.position.line + 1,
98
+ },
99
+ message: `${part.message}; ${overlapClause(overlap)}`,
100
+ details: {
101
+ ...(part.kind === "store" ? { writePositions: [part.position] } : {}),
102
+ overlap,
103
+ },
104
+ });
105
+ }
106
+ return found;
107
+ };
@@ -14,6 +14,12 @@ import type { Emitter } from "./emitter.ts";
14
14
  /** The code a configured root that matches nothing is reported under. */
15
15
  const UNMATCHED_ROOT = "DS1704";
16
16
 
17
+ /** The code a configured declaration that names nothing is reported under. */
18
+ const UNMATCHED_DECLARATION = "DS1706";
19
+
20
+ /** The subject kind of a finding about a configured declaration. */
21
+ const DECLARATION_SUBJECT = "configured-declaration";
22
+
17
23
  /** The subject kind of a finding about a suppression record. */
18
24
  const SUPPRESSION_SUBJECT = "suppression";
19
25
 
@@ -38,6 +44,18 @@ export interface SelfCheckFacts {
38
44
  readonly unmatchedRoots: readonly string[];
39
45
  /** The document below the target root that declared the roots, empty where none did. */
40
46
  readonly rootsDocument: string;
47
+ /** Every configured declaration that named no declaration in any project, in reading order. */
48
+ readonly unmatchedDeclarations: readonly UnmatchedDeclaration[];
49
+ }
50
+
51
+ /** One configured declaration that named nothing, and the document that configured it. */
52
+ interface UnmatchedDeclaration {
53
+ /** The entry as a finding's subject reference spells it. */
54
+ readonly ref: string;
55
+ /** The dotted path of the key that holds the entry. */
56
+ readonly key: string;
57
+ /** The document below the target root that declared the key, empty where none did. */
58
+ readonly document: string;
41
59
  }
42
60
 
43
61
  /** A run with nothing for the self-check family to report. */
@@ -45,6 +63,7 @@ export const NO_SELF_CHECK: SelfCheckFacts = {
45
63
  refusals: [],
46
64
  unmatchedRoots: [],
47
65
  rootsDocument: "",
66
+ unmatchedDeclarations: [],
48
67
  };
49
68
 
50
69
  /** The words a message names a mechanism by. */
@@ -188,6 +207,31 @@ export function staleSuppressions(
188
207
  });
189
208
  }
190
209
 
210
+ /** The first position of the document that configured a setting, or of the conventional one. */
211
+ function documentPosition(document: string): Finding["position"] {
212
+ return {
213
+ path: document === "" ? REPOSITORY_DOCUMENT : document,
214
+ line: DOCUMENT_POSITION,
215
+ column: DOCUMENT_POSITION,
216
+ endLine: DOCUMENT_POSITION,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * One finding per configured declaration that named no declaration in any project, at the
222
+ * first position of the document that configured its key, as a configured root's is.
223
+ */
224
+ function unmatchedDeclarationFindings(
225
+ unmatched: readonly UnmatchedDeclaration[],
226
+ ): readonly Finding[] {
227
+ return unmatched.map((one) => ({
228
+ code: UNMATCHED_DECLARATION,
229
+ position: documentPosition(one.document),
230
+ symbol: { ref: one.ref, kind: DECLARATION_SUBJECT, name: one.ref, sizeLines: 1 },
231
+ message: `${one.key} entry names no declaration in any project`,
232
+ }));
233
+ }
234
+
191
235
  /**
192
236
  * One finding per configured root or pattern that named nothing in any project. A finding
193
237
  * names the document that declared the roots, or the conventional repository configuration
@@ -199,12 +243,7 @@ export function unmatchedRootFindings(
199
243
  sources: readonly string[],
200
244
  document: string,
201
245
  ): readonly Finding[] {
202
- const position = {
203
- path: document === "" ? REPOSITORY_DOCUMENT : document,
204
- line: DOCUMENT_POSITION,
205
- column: DOCUMENT_POSITION,
206
- endLine: DOCUMENT_POSITION,
207
- };
246
+ const position = documentPosition(document);
208
247
  return sources.map((source) => ({
209
248
  code: UNMATCHED_ROOT,
210
249
  position,
@@ -218,11 +257,13 @@ export function unmatchedRootFindings(
218
257
 
219
258
  /**
220
259
  * The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
221
- * carrying no reason or naming no path, and a configured root that named nothing. A
260
+ * carrying no reason or naming no path, and a configured root or declaration that named
261
+ * nothing. A
222
262
  * suppression that matched nothing is a record of the report's stale suppressions, which
223
263
  * {@link staleSuppressions} builds, and a stale cross-language edge is the merge's to report.
224
264
  */
225
265
  export const selfCheck: Emitter = ({ selfCheck: facts, swept }) => [
226
266
  ...refusedSuppressions(facts.refusals, fileRefsOf(swept.matrix.union.symbols)),
227
267
  ...unmatchedRootFindings(facts.unmatchedRoots, facts.rootsDocument),
268
+ ...unmatchedDeclarationFindings(facts.unmatchedDeclarations),
228
269
  ];