@cplieger/deadset-ts 6.1.1-dev.1 → 7.0.0-dev.2

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 (98) hide show
  1. package/dist/src/analysis.js +45 -32
  2. package/dist/src/component-layer.js +1 -1
  3. package/dist/src/configuration-files.js +227 -27
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/contextual-uses.js +68 -10
  6. package/dist/src/convention-rows.js +48 -0
  7. package/dist/src/conventions.js +80 -0
  8. package/dist/src/dependencies.js +121 -46
  9. package/dist/src/discover.js +1 -1
  10. package/dist/src/edges.js +1 -1
  11. package/dist/src/exempt-classes.js +9 -22
  12. package/dist/src/exempt.js +3 -15
  13. package/dist/src/findings/completion.js +41 -11
  14. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  15. package/dist/src/findings/emitters.js +0 -4
  16. package/dist/src/findings/self-check.js +14 -10
  17. package/dist/src/html-entries.js +29 -14
  18. package/dist/src/indexed-access.js +377 -81
  19. package/dist/src/intra-function-parts.js +57 -11
  20. package/dist/src/inventory.js +3 -12
  21. package/dist/src/kinds.js +6 -6
  22. package/dist/src/references.js +62 -23
  23. package/dist/src/report.js +3 -0
  24. package/dist/src/roots.js +96 -7
  25. package/dist/src/run.js +0 -2
  26. package/dist/src/schema.js +1 -1
  27. package/dist/src/scope.js +2 -2
  28. package/dist/src/session.js +6 -2
  29. package/dist/src/source-files.js +1 -0
  30. package/dist/src/suppress-inline.js +1 -1
  31. package/dist/src/suppress.js +15 -16
  32. package/dist/src/sweep.js +29 -17
  33. package/dist/src/symbol-keys.js +114 -0
  34. package/dist/src/test-joins.js +101 -0
  35. package/dist/src/version.js +1 -1
  36. package/dist/src/workflow-steps.js +21 -15
  37. package/dist/src/workspace-programs.js +1 -4
  38. package/dist/src/workspace-resolution.js +0 -2
  39. package/dist/src/workspace.js +0 -3
  40. package/jsr.json +1 -1
  41. package/package.json +2 -1
  42. package/src/analysis.ts +55 -47
  43. package/src/calls.ts +3 -3
  44. package/src/component-files.ts +7 -7
  45. package/src/component-layer.ts +2 -2
  46. package/src/components.ts +1 -1
  47. package/src/config.ts +4 -4
  48. package/src/configuration-files.ts +246 -28
  49. package/src/conformance-record.ts +2 -2
  50. package/src/contextual-uses.ts +99 -13
  51. package/src/convention-rows.ts +76 -0
  52. package/src/conventions.ts +102 -0
  53. package/src/dependencies.ts +148 -56
  54. package/src/discover.ts +3 -3
  55. package/src/edges.ts +3 -3
  56. package/src/entry-points.ts +1 -1
  57. package/src/exempt-classes.ts +10 -24
  58. package/src/exempt.ts +5 -29
  59. package/src/file-facts.ts +1 -1
  60. package/src/finding.ts +5 -1
  61. package/src/findings/completion.ts +53 -11
  62. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  63. package/src/findings/emitters.ts +3 -8
  64. package/src/findings/self-check.ts +17 -14
  65. package/src/findings-pass.ts +2 -2
  66. package/src/graph.ts +2 -2
  67. package/src/html-entries.ts +34 -16
  68. package/src/indexed-access.ts +476 -102
  69. package/src/intra-function-parts.ts +84 -12
  70. package/src/inventory.ts +5 -33
  71. package/src/json-document.ts +1 -1
  72. package/src/kinds.ts +7 -6
  73. package/src/manifest.ts +1 -1
  74. package/src/namespace-objects.ts +1 -1
  75. package/src/pnpm-workspace.ts +1 -1
  76. package/src/references.ts +93 -33
  77. package/src/report.ts +8 -4
  78. package/src/roots.ts +129 -9
  79. package/src/run.ts +0 -2
  80. package/src/schema.ts +2 -2
  81. package/src/scope.ts +2 -2
  82. package/src/session.ts +7 -3
  83. package/src/source-files.ts +3 -0
  84. package/src/stores.ts +1 -1
  85. package/src/suppress-inline.ts +3 -3
  86. package/src/suppress.ts +21 -24
  87. package/src/sweep.ts +29 -17
  88. package/src/symbol-keys.ts +172 -0
  89. package/src/template-field.ts +1 -1
  90. package/src/test-joins.ts +120 -0
  91. package/src/value-reach.ts +1 -1
  92. package/src/verbs/verb.ts +1 -4
  93. package/src/version.ts +1 -1
  94. package/src/workflow-steps.ts +23 -16
  95. package/src/workspace-programs.ts +3 -10
  96. package/src/workspace-resolution.ts +0 -3
  97. package/src/workspace-run.ts +1 -1
  98. package/src/workspace.ts +1 -9
@@ -1,16 +1,12 @@
1
1
  /**
2
- * The exemption classes of the Contract's vocabulary, in its order: each class's name,
3
- * the languages it runs on, the confidence it is recorded at and, for a class that runs
4
- * on TypeScript, which private members it may retain.
5
- *
6
- * The rows are written out rather than read from the Contract at run time, for the
7
- * reason the fixed severity codes are, and a test holds them equal to the Contract
8
- * release this analyzer implements. A class name is a type, so a detector table or a
9
- * record naming a class the vocabulary does not hold, or one that runs on another
10
- * language only, does not compile.
2
+ * The exemption classes of the Contract's vocabulary, in its order: each class's name, its
3
+ * languages and, for a TypeScript class, which private members it may retain. The rows are
4
+ * written out, as the fixed severity codes are, and a test holds them equal to the Contract.
5
+ * A class name is a type, so a table or a record naming a class outside the vocabulary, or
6
+ * one of another language only, does not compile.
11
7
  */
12
8
 
13
- import type { Confidence, Language } from "./config.ts";
9
+ import type { Language } from "./config.ts";
14
10
 
15
11
  /**
16
12
  * Whether a class may retain a member declared with the `private` modifier, and one
@@ -27,7 +23,6 @@ export interface TypeScriptVisibility {
27
23
  interface ExemptionClassRow {
28
24
  readonly class: string;
29
25
  readonly languages: readonly Language[];
30
- readonly confidence: Confidence;
31
26
  /** Present exactly on a class that runs on TypeScript. */
32
27
  readonly typescriptVisibility?: TypeScriptVisibility;
33
28
  }
@@ -37,59 +32,50 @@ export const EXEMPTION_CLASSES = [
37
32
  {
38
33
  class: "interface-satisfaction",
39
34
  languages: ["go", "ts"],
40
- confidence: "certain",
41
35
  typescriptVisibility: { private: false, privateName: false },
42
36
  },
43
- { class: "encoding-reflection", languages: ["go"], confidence: "certain" },
44
- { class: "format-verb-contract", languages: ["go"], confidence: "certain" },
45
- { class: "errors-duck-typing", languages: ["go"], confidence: "certain" },
37
+ { class: "encoding-reflection", languages: ["go"] },
38
+ { class: "format-verb-contract", languages: ["go"] },
39
+ { class: "errors-duck-typing", languages: ["go"] },
46
40
  {
47
41
  class: "enum-group",
48
42
  languages: ["go", "ts"],
49
- confidence: "certain",
50
43
  typescriptVisibility: { private: false, privateName: false },
51
44
  },
52
45
  {
53
46
  class: "generated-file",
54
47
  languages: ["go", "ts"],
55
- confidence: "certain",
56
48
  typescriptVisibility: { private: true, privateName: false },
57
49
  },
58
- { class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
50
+ { class: "linkname-cgo-asm-plugin", languages: ["go"] },
59
51
  {
60
52
  class: "template-field",
61
53
  languages: ["go", "ts"],
62
- confidence: "possible",
63
54
  typescriptVisibility: { private: true, privateName: false },
64
55
  },
65
56
  {
66
57
  class: "reflective-lookup",
67
58
  languages: ["go", "ts"],
68
- confidence: "possible",
69
59
  typescriptVisibility: { private: true, privateName: false },
70
60
  },
71
61
  {
72
62
  class: "decorator",
73
63
  languages: ["ts"],
74
- confidence: "certain",
75
64
  typescriptVisibility: { private: true, privateName: false },
76
65
  },
77
66
  {
78
67
  class: "injection-container",
79
68
  languages: ["ts"],
80
- confidence: "certain",
81
69
  typescriptVisibility: { private: true, privateName: false },
82
70
  },
83
71
  {
84
72
  class: "framework-lifecycle",
85
73
  languages: ["ts"],
86
- confidence: "certain",
87
74
  typescriptVisibility: { private: true, privateName: false },
88
75
  },
89
76
  {
90
77
  class: "serialization-contract",
91
78
  languages: ["ts"],
92
- confidence: "certain",
93
79
  typescriptVisibility: { private: true, privateName: false },
94
80
  },
95
81
  ] as const satisfies readonly ExemptionClassRow[];
package/src/exempt.ts CHANGED
@@ -64,9 +64,7 @@ export type Detector = <Brand>(input: DetectorInput<Brand>) => readonly Evidence
64
64
  export type Detectors = ReadonlyMap<TSExemptionClass, Detector>;
65
65
 
66
66
  /** What decides which records of one project stand. */
67
- export interface Holding {
68
- /** The classes the configuration switches off, whose detectors do not run. */
69
- readonly disabled: ReadonlySet<ExemptionClass>;
67
+ interface Holding {
70
68
  readonly mode: Mode;
71
69
  /** The paths the reference pass classified as test files. */
72
70
  readonly testFiles: ReadonlySet<string>;
@@ -154,23 +152,8 @@ function detected<Brand>(
154
152
  }
155
153
  }
156
154
 
157
- /**
158
- * The records one project's detectors found, each class in the vocabulary's order and
159
- * none of a switched-off class. Evidence on a private member the class's row says it
160
- * cannot retain is dropped first, whatever the detector found. Under a production mode
161
- * a record whose evidence is written in a test file holds nothing, as a reference a
162
- * test file makes is none there.
163
- */
164
- export function computeExemptions<Brand>(
165
- input: DetectorInput<Brand>,
166
- detectors: Detectors,
167
- holding: Holding,
168
- ): readonly Exemption[] {
169
- return detectExemptions(input, detectors, holding).records;
170
- }
171
-
172
155
  /** One project's records, and the evidence a production mode found in a test file. */
173
- export interface Detected {
156
+ interface Detected {
174
157
  readonly records: readonly Exemption[];
175
158
  /**
176
159
  * Under a production mode, each record whose evidence is written in a test file. It
@@ -180,18 +163,11 @@ export interface Detected {
180
163
  readonly inTestFiles: readonly Exemption[];
181
164
  }
182
165
 
183
- /** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
184
- export function detectExemptions<Brand>(
185
- input: DetectorInput<Brand>,
186
- detectors: Detectors,
187
- holding: Holding,
188
- ): Detected {
189
- return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
190
- }
191
-
192
166
  /**
193
167
  * Every record one project's detectors found, one per piece of evidence, before the
194
- * mode decides which of them hold: what {@link heldRecords} reads.
168
+ * mode decides which of them hold: what {@link heldRecords} reads. Classes run in the
169
+ * vocabulary's order and a switched-off class runs not at all; evidence on a private
170
+ * member the class's row says it cannot retain is dropped, whatever the detector found.
195
171
  */
196
172
  export function evidenceRecords<Brand>(
197
173
  input: DetectorInput<Brand>,
package/src/file-facts.ts CHANGED
@@ -22,7 +22,7 @@ const DECLARATION_FILE = /\.d\.(?:[^./]+\.)?[mc]?ts$/u;
22
22
  const LINE_BREAK = /\r\n|[\n\r\u2028\u2029]/gu;
23
23
 
24
24
  /** One source file below the target root. */
25
- export interface TreeFile {
25
+ interface TreeFile {
26
26
  /** The path below the target root, with the solidus as separator. */
27
27
  readonly path: string;
28
28
  /** The stable reference of the file's module. */
package/src/finding.ts CHANGED
@@ -25,7 +25,7 @@ export interface FindingPosition {
25
25
  }
26
26
 
27
27
  /** What a finding is about. */
28
- export interface FindingSubject {
28
+ interface FindingSubject {
29
29
  /** The stable symbol reference, or for a configured root the root as written. */
30
30
  readonly ref: string;
31
31
  /** The subject kind, from the closed vocabulary of the finding schema. */
@@ -65,6 +65,8 @@ export interface FindingDetails {
65
65
  readonly entry?: SuppressionEntry;
66
66
  /** On the intra-function kinds: the external rules that report the same kind, as the vocabulary lists them. */
67
67
  readonly overlap?: readonly string[];
68
+ /** On a deletable finding: the dependencies whose last use deleting the subject removes. */
69
+ readonly removesLastUseOf?: readonly string[];
68
70
  }
69
71
 
70
72
  /** One suppression in the four-member form the ignore file and the baseline share. */
@@ -83,6 +85,8 @@ export interface Finding {
83
85
  readonly message: string;
84
86
  /** Absent where the code carries nothing. */
85
87
  readonly details?: FindingDetails;
88
+ /** The reachability class of a subject that is no declaration, where it is not `certain`. */
89
+ readonly reachabilityClass?: Confidence;
86
90
  }
87
91
 
88
92
  /** The dead component a finding names, as the finding schema spells its members. */
@@ -84,6 +84,35 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
84
84
  };
85
85
  }
86
86
 
87
+ /**
88
+ * The confidence ceiling of a test of dead code: the lowest reachability class among the
89
+ * target symbols it references, because its deadness is theirs, whether or not they share
90
+ * its component. Every other declaration has none below `certain`.
91
+ */
92
+ function testCeilings(
93
+ input: EmitterInput,
94
+ classOf: (id: string) => Confidence,
95
+ ): (id: string) => Confidence {
96
+ const union = input.swept.matrix.union;
97
+ const tests = new Set(
98
+ input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
99
+ );
100
+ return (id) => {
101
+ let lowest: Confidence = "certain";
102
+ if (!tests.has(id)) {
103
+ return lowest;
104
+ }
105
+ for (const edge of union.out[union.at(id)] ?? []) {
106
+ const target = union.symbols[edge.to];
107
+ const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
108
+ if (target !== undefined && !testCode && union.subject[edge.to] === true) {
109
+ lowest = capped(lowest, classOf(target.id));
110
+ }
111
+ }
112
+ return lowest;
113
+ };
114
+ }
115
+
87
116
  /**
88
117
  * The component each dead declaration of the run falls in, and none for a live one, with
89
118
  * its members listed in the canonical order where the run lists a component in full.
@@ -136,12 +165,11 @@ function findingComponents(
136
165
 
137
166
  /**
138
167
  * Every finding of the run completed, in the order given. A dead declaration carries its
139
- * candidate's relation, counts, configurations and component; a live one carries no
140
- * relation and holds where it is declared; a file or a manifest row is `certain` and
141
- * holds in every configuration. A subject in no dead component gets one of its own,
142
- * numbered past the computed ones in finding order, so no two families mint one; nothing
143
- * falls with it, so its deletion removes no line. `kinds` is the vocabulary each code's
144
- * row is read from, the shipped one unless a caller supplies its own.
168
+ * candidate's relation, counts, configurations and component; a live one carries no relation and
169
+ * holds where it is declared; a file or a manifest row carries the class its emitter states,
170
+ * `certain` by default, and holds in every configuration. A subject in no dead component gets one
171
+ * of its own, numbered past the computed ones in finding order so no two families mint one, whose
172
+ * deletion removes no line. `kinds` defaults to the shipped vocabulary.
145
173
  */
146
174
  export function completed(
147
175
  input: EmitterInput,
@@ -155,6 +183,7 @@ export function completed(
155
183
  );
156
184
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
157
185
  const classOf = reachabilityClasses(input);
186
+ const ceilingOf = testCeilings(input, classOf);
158
187
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
159
188
  let minted = swept.sweep.components.length;
160
189
  const dead = new Set(swept.sweep.components.map((component) => component.id));
@@ -210,8 +239,14 @@ export function completed(
210
239
  candidate === undefined
211
240
  ? union.made[at]
212
241
  : { production: candidate.productionRefs, test: candidate.testRefs };
213
- const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
242
+ const reachabilityClass =
243
+ judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
214
244
  const generated = input.files.generated?.has(finding.position.path) === true;
245
+ const fixability = generated ? "none" : row.fixability;
246
+ const lastUses =
247
+ fixability === "deletable" && candidate !== undefined
248
+ ? input.dependencies.lastUses.get(candidate.id)
249
+ : undefined;
215
250
  return {
216
251
  code: finding.code,
217
252
  kind: row.name,
@@ -219,7 +254,10 @@ export function completed(
219
254
  position: finding.position,
220
255
  symbol: finding.symbol,
221
256
  reachabilityClass,
222
- confidence: capped(reachabilityClass, row.maxClass),
257
+ confidence: capped(
258
+ capped(reachabilityClass, row.maxClass),
259
+ judged === undefined ? "certain" : ceilingOf(judged.id),
260
+ ),
223
261
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
224
262
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
225
263
  generated,
@@ -231,10 +269,13 @@ export function completed(
231
269
  ? swept.matrix.configurations
232
270
  : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
233
271
  consumersLoaded: input.boundary.consumers.loaded,
234
- fixability: generated ? "none" : row.fixability,
272
+ fixability,
235
273
  severity: severityOf(input, finding.code, row.defaultSeverity),
236
274
  message: finding.message,
237
- details: finding.details ?? {},
275
+ details:
276
+ lastUses === undefined
277
+ ? (finding.details ?? {})
278
+ : { ...finding.details, removesLastUseOf: lastUses },
238
279
  };
239
280
  });
240
281
  return componentCapped(done, dead);
@@ -287,6 +328,7 @@ export function dialsOf(
287
328
  ): Dials<CompletedFinding> {
288
329
  const minConfidence = input.config.analysis.minConfidence;
289
330
  const classOf = reachabilityClasses(input);
331
+ const ceilingOf = testCeilings(input, classOf);
290
332
  const components = new Set(
291
333
  findings
292
334
  .filter((finding) => finding.component.root && dialed(finding, minConfidence))
@@ -300,7 +342,7 @@ export function dialsOf(
300
342
  dialed(
301
343
  {
302
344
  severity: severityOf(input, code, row.defaultSeverity),
303
- confidence: capped(classOf(id), row.maxClass),
345
+ confidence: capped(capped(classOf(id), row.maxClass), ceilingOf(id)),
304
346
  },
305
347
  minConfidence,
306
348
  )
@@ -1,4 +1,4 @@
1
- import type { Dependencies } from "../dependencies.ts";
1
+ import { declarationKey, type Dependencies } from "../dependencies.ts";
2
2
  import type { Finding } from "../finding.ts";
3
3
  import { renderRef, type DependencySection } from "../ref.ts";
4
4
  import type { Emitter } from "./emitter.ts";
@@ -11,39 +11,17 @@ const MESSAGES: Readonly<Record<DependencySection, string>> = {
11
11
  "peer-dependency": "no import in the target needs the declared peer dependency",
12
12
  };
13
13
 
14
- /**
15
- * The dependencies the target needs: every package the projects' uses name, every
16
- * declared dependency that ships a command, which a script, a workflow or a shell
17
- * runs outside anything the analysis reads, and every peer a needed package
18
- * requires, which the target satisfies by declaring it.
19
- */
20
- function neededBy(dependencies: Dependencies): ReadonlySet<string> {
21
- const needed = new Set(dependencies.needed);
22
- for (const [name, installed] of dependencies.installed) {
23
- if (installed.command) {
24
- needed.add(name);
25
- }
26
- }
27
- const pending = [...needed];
28
- for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
29
- for (const peer of dependencies.installed.get(name)?.peers ?? []) {
30
- if (!needed.has(peer)) {
31
- needed.add(peer);
32
- pending.push(peer);
33
- }
34
- }
35
- }
36
- return needed;
37
- }
38
-
39
14
  /**
40
15
  * A dependency, development dependency or peer dependency of a manifest the run reads
41
16
  * that the target does not need, at the position of its key.
42
17
  */
43
18
  function unusedDependencies(dependencies: Dependencies): Finding[] {
44
- const needed = neededBy(dependencies);
45
19
  return dependencies.declared
46
- .filter((dependency) => !needed.has(dependency.name))
20
+ .filter(
21
+ (dependency) =>
22
+ !dependencies.needed.has(dependency.name) &&
23
+ !dependencies.ran.has(declarationKey(dependency)),
24
+ )
47
25
  .map((dependency) => ({
48
26
  code: UNUSED_DEPENDENCY,
49
27
  position: dependency.position,
@@ -59,6 +37,9 @@ function unusedDependencies(dependencies: Dependencies): Finding[] {
59
37
  },
60
38
  message: MESSAGES[dependency.section],
61
39
  details: { dependencyClass: dependency.section },
40
+ ...(dependencies.unbuilt.has(dependency.position.path)
41
+ ? { reachabilityClass: "possible" as const }
42
+ : {}),
62
43
  }));
63
44
  }
64
45
 
@@ -67,7 +67,7 @@ function markupKeys(input: EmitterInput): ReadonlySet<string> {
67
67
  }
68
68
 
69
69
  /** What the suppressions of a run swept under their marks are decided against. */
70
- export interface Suppressed {
70
+ interface Suppressed {
71
71
  /** The same run swept with no mark, which is where a kind about liveness reports a marked declaration. */
72
72
  readonly unmarked: EmitterInput;
73
73
  /** The run's suppression records, in reading order. */
@@ -75,13 +75,13 @@ export interface Suppressed {
75
75
  }
76
76
 
77
77
  /** What the emitter table answers about one run. */
78
- export interface DecidedFindings {
78
+ interface DecidedFindings {
79
79
  /** Every finding the run reports, each family's in table order. */
80
80
  readonly findings: readonly CompletedFinding[];
81
81
  /** One evaluation per declared edge side of this language, ordered by edge, then by side. */
82
82
  readonly evaluations: readonly EdgeEvaluation<CompletedFinding>[];
83
83
  /** What every suppression record did, in reading order. */
84
- readonly ledger: Ledger<CompletedFinding>;
84
+ readonly ledger: Ledger;
85
85
  /** Every finding a record could name: the run's with no mark, then the run's as swept. */
86
86
  readonly would: readonly CompletedFinding[];
87
87
  /** The findings the minimum confidence withheld, by confidence. */
@@ -198,8 +198,3 @@ function withheldOf(input: EmitterInput, candidates: readonly CompletedFinding[]
198
198
  }
199
199
  return counts;
200
200
  }
201
-
202
- /** Every finding the run reports, each family's in table order. */
203
- export function findingsOf(input: EmitterInput): readonly CompletedFinding[] {
204
- return decidedFindings(input).findings;
205
- }
@@ -1,8 +1,10 @@
1
1
  import { REPOSITORY_DOCUMENT } from "../config.ts";
2
2
  import type { Finding, FindingDetails, SuppressionEntry } from "../finding.ts";
3
3
  import type { InventorySymbol } from "../inventory.ts";
4
+ import { FIXED_SEVERITY_CODES } from "../kinds.ts";
4
5
  import type { Position } from "../position.ts";
5
6
  import {
7
+ isRowCode,
6
8
  STALE_SUPPRESSION,
7
9
  SUPPRESSION_WITHOUT_REASON,
8
10
  type Mechanism,
@@ -58,14 +60,6 @@ interface UnmatchedDeclaration {
58
60
  readonly document: string;
59
61
  }
60
62
 
61
- /** A run with nothing for the self-check family to report. */
62
- export const NO_SELF_CHECK: SelfCheckFacts = {
63
- refusals: [],
64
- unmatchedRoots: [],
65
- rootsDocument: "",
66
- unmatchedDeclarations: [],
67
- };
68
-
69
63
  /** The words a message names a mechanism by. */
70
64
  const WRITTEN: Readonly<Record<Mechanism, string>> = {
71
65
  inline: "inline directive",
@@ -122,7 +116,7 @@ function writtenEntry(refused: Refusal): SuppressionEntry {
122
116
  }
123
117
 
124
118
  /** Each file of the run by its path, as the reference of the file it is. */
125
- export type FileRefs = (path: string) => string | undefined;
119
+ type FileRefs = (path: string) => string | undefined;
126
120
 
127
121
  /**
128
122
  * The reference a finding about a suppression names: the symbol it names or bound, else
@@ -133,10 +127,7 @@ function suppressionRef(symbol: string, path: string, files: FileRefs): string {
133
127
  }
134
128
 
135
129
  /** One finding per refused suppression: the grammar's reason refusal and its scope refusal. */
136
- export function refusedSuppressions(
137
- refusals: readonly Refusal[],
138
- files: FileRefs,
139
- ): readonly Finding[] {
130
+ function refusedSuppressions(refusals: readonly Refusal[], files: FileRefs): readonly Finding[] {
140
131
  return refusals.map((refused) =>
141
132
  aboutSuppression(
142
133
  refused.reported,
@@ -190,7 +181,19 @@ export function staleSuppressions(
190
181
  const reports = [...new Set(records.flatMap((one) => one.reports))]
191
182
  .filter((code) => !codes.includes(code))
192
183
  .sort();
193
- const instead = reports.length === 0 ? "" : `, and its declaration reports ${spelled(reports)}`;
184
+ const rows = codes.filter(isRowCode);
185
+ const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
186
+ const remedy =
187
+ settable.length === 0
188
+ ? "fix what the finding names instead"
189
+ : `fix what the finding names or set the severity of ${spelled(settable)} instead`;
190
+ const declared = reports.length === 0 ? "" : `its declaration reports ${spelled(reports)}`;
191
+ const instead =
192
+ rows.length > 0
193
+ ? `, because ${spelled(rows)} ${rows.length === 1 ? "reports a row" : "report rows"} of a document, which no suppression can name; ${remedy}${declared === "" ? "" : `; ${declared}`}`
194
+ : declared === ""
195
+ ? ""
196
+ : `, and ${declared}`;
194
197
  return {
195
198
  code: STALE_SUPPRESSION,
196
199
  mechanism: first.mechanism,
@@ -20,7 +20,7 @@ import type { Provenance, Recorded } from "./suppress-file.ts";
20
20
  import { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.ts";
21
21
 
22
22
  /** The counts a report's totals carry beside its findings. */
23
- export interface PassTotals {
23
+ interface PassTotals {
24
24
  readonly suppressionsInEffect: number;
25
25
  readonly reasonsRecorded: number;
26
26
  readonly staleSuppressions: number;
@@ -40,7 +40,7 @@ export interface PassResult {
40
40
  readonly edgeEvaluations: readonly EdgeEvaluation<CompletedFinding>[];
41
41
  readonly totals: PassTotals;
42
42
  /** What every suppression record did, in reading order. */
43
- readonly ledger: Ledger<CompletedFinding>;
43
+ readonly ledger: Ledger;
44
44
  }
45
45
 
46
46
  /** The exit code of a report holding at least one pending finding. */
package/src/graph.ts CHANGED
@@ -87,13 +87,13 @@ export interface Edge {
87
87
  }
88
88
 
89
89
  /** How many references one declaration carries, split by the classification of the file that made each. */
90
- export interface Counts {
90
+ interface Counts {
91
91
  readonly production: number;
92
92
  readonly test: number;
93
93
  }
94
94
 
95
95
  /** One root the inventory holds, at the position of the declaration it names. */
96
- export interface Rooted {
96
+ interface Rooted {
97
97
  readonly at: number;
98
98
  readonly kind: RootKind;
99
99
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
- * The files the module scripts of the target's HTML files load. An HTML file is read as
3
- * text and no markup is parsed, so a `<script>` inside a comment is read as well.
2
+ * The files the scripts of the target's HTML files load: a module script's, and a
3
+ * classic script's that names its file by `src`. An HTML file is read as text and no
4
+ * markup is parsed, so a `<script>` inside a comment is read as well.
4
5
  */
5
6
 
6
7
  import {
@@ -14,8 +15,8 @@ import type { Host } from "./host.ts";
14
15
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
15
16
  import type { SourceFiles } from "./source-files.ts";
16
17
 
17
- /** One file a module script loads, and the value or specifier that named it. */
18
- export interface HTMLEntry {
18
+ /** One file a script loads, and the value or specifier that named it. */
19
+ interface HTMLEntry {
19
20
  readonly file: SourceFile;
20
21
  readonly source: string;
21
22
  }
@@ -35,6 +36,9 @@ const ATTRIBUTE = /([^\s"'=<>/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)
35
36
  /** A value that names a resource on another origin, or by another scheme. */
36
37
  const ELSEWHERE = /^(?:\/\/|[A-Za-z][A-Za-z\d+.-]*:)/u;
37
38
 
39
+ /** The `type` values a classic script carries, compared in lowercase. */
40
+ const CLASSIC_TYPES: ReadonlySet<string> = new Set(["text/javascript", "application/javascript"]);
41
+
38
42
  /** The prefixes an inline script's specifier names a file of the target with. */
39
43
  const FILE_PREFIXES = ["/", "./", "../"] as const;
40
44
 
@@ -85,18 +89,22 @@ function specifiersOf(parse: ParseFile, fileName: string, content: string): read
85
89
  }
86
90
 
87
91
  /**
88
- * Every value a module script of one HTML file names a file by: a `src` attribute's
89
- * value, or each specifier an inline script imports or re-exports that starts with `/`,
90
- * `./` or `../`.
92
+ * Every value a script of one HTML file names a file by: the `src` attribute's value of a
93
+ * module script or of a classic one, or each specifier an inline module script imports or
94
+ * re-exports that starts with `/`, `./` or `../`.
91
95
  */
92
96
  function scriptValues(parse: ParseFile | undefined, path: string, text: string): readonly string[] {
93
97
  const values: string[] = [];
94
98
  for (const match of text.matchAll(SCRIPT_TAG)) {
95
99
  const held = attributes(match[1] ?? "");
96
- if (held.get("type") !== "module") {
100
+ const type = held.get("type");
101
+ const src = held.get("src");
102
+ if (type !== "module") {
103
+ if (src !== undefined && (type === undefined || CLASSIC_TYPES.has(type.toLowerCase()))) {
104
+ values.push(src);
105
+ }
97
106
  continue;
98
107
  }
99
- const src = held.get("src");
100
108
  if (src !== undefined) {
101
109
  values.push(src);
102
110
  continue;
@@ -131,11 +139,11 @@ function manifestDirectory(host: Host, root: string, dir: string): string {
131
139
  }
132
140
 
133
141
  /**
134
- * The own files the module scripts of every HTML file below the target root load. A value
142
+ * The own files the scripts of every HTML file below the target root load. A value
135
143
  * starting with a single `/` is read against the nearest manifest's directory, any other
136
- * against the HTML file's own, and either resolves as a configuration file's relative
137
- * specifier does. A value naming another origin or scheme, or no source file of the
138
- * program, loads nothing here.
144
+ * against the HTML file's own, as a configuration file's relative specifier. One naming no
145
+ * source file is read back through the project's emit mapping, and a `/` one also against
146
+ * an output directory that is the HTML file's own. Another origin or scheme loads nothing.
139
147
  */
140
148
  export function htmlEntries(
141
149
  host: Host,
@@ -157,12 +165,22 @@ export function htmlEntries(
157
165
  if (ELSEWHERE.test(value)) {
158
166
  continue;
159
167
  }
160
- const reached = value.startsWith("/")
161
- ? resolveRelativeSpecifier(host, manifestDirectory(host, root, dir), `.${value}`)
162
- : resolveRelativeSpecifier(host, dir, value);
168
+ const base = value.startsWith("/") ? manifestDirectory(host, root, dir) : dir;
169
+ const relative = value.startsWith("/") ? `.${value}` : value;
170
+ const reached = resolveRelativeSpecifier(host, base, relative);
163
171
  const file = reached === undefined ? undefined : files.byName.get(reached);
164
172
  if (file !== undefined) {
165
173
  found.push({ file, source: value });
174
+ continue;
175
+ }
176
+ const served = value.startsWith("/")
177
+ ? files.outputDirs.filter((outDir) => outDir === dir)
178
+ : [];
179
+ const emitting = [base, ...served].flatMap((against) =>
180
+ files.named(resolvePath(against, relative)),
181
+ );
182
+ for (const one of new Set(emitting)) {
183
+ found.push({ file: one, source: value });
166
184
  }
167
185
  }
168
186
  }