@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.
- package/dist/src/analysis.js +45 -32
- package/dist/src/component-layer.js +1 -1
- package/dist/src/configuration-files.js +227 -27
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +68 -10
- package/dist/src/convention-rows.js +48 -0
- package/dist/src/conventions.js +80 -0
- package/dist/src/dependencies.js +121 -46
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +41 -11
- package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
- package/dist/src/findings/emitters.js +0 -4
- package/dist/src/findings/self-check.js +14 -10
- package/dist/src/html-entries.js +29 -14
- package/dist/src/indexed-access.js +377 -81
- package/dist/src/intra-function-parts.js +57 -11
- package/dist/src/inventory.js +3 -12
- package/dist/src/kinds.js +6 -6
- package/dist/src/references.js +62 -23
- package/dist/src/report.js +3 -0
- package/dist/src/roots.js +96 -7
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/session.js +6 -2
- package/dist/src/source-files.js +1 -0
- package/dist/src/suppress-inline.js +1 -1
- package/dist/src/suppress.js +15 -16
- package/dist/src/sweep.js +29 -17
- package/dist/src/symbol-keys.js +114 -0
- package/dist/src/test-joins.js +101 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +21 -15
- package/dist/src/workspace-programs.js +1 -4
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +55 -47
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +1 -1
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +246 -28
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +99 -13
- package/src/convention-rows.ts +76 -0
- package/src/conventions.ts +102 -0
- package/src/dependencies.ts +148 -56
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/entry-points.ts +1 -1
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +1 -1
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +53 -11
- package/src/findings/dependencies-and-module-machinery.ts +9 -28
- package/src/findings/emitters.ts +3 -8
- package/src/findings/self-check.ts +17 -14
- package/src/findings-pass.ts +2 -2
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +34 -16
- package/src/indexed-access.ts +476 -102
- package/src/intra-function-parts.ts +84 -12
- package/src/inventory.ts +5 -33
- package/src/json-document.ts +1 -1
- package/src/kinds.ts +7 -6
- package/src/manifest.ts +1 -1
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/references.ts +93 -33
- package/src/report.ts +8 -4
- package/src/roots.ts +129 -9
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/session.ts +7 -3
- package/src/source-files.ts +3 -0
- package/src/stores.ts +1 -1
- package/src/suppress-inline.ts +3 -3
- package/src/suppress.ts +21 -24
- package/src/sweep.ts +29 -17
- package/src/symbol-keys.ts +172 -0
- package/src/template-field.ts +1 -1
- package/src/test-joins.ts +120 -0
- package/src/value-reach.ts +1 -1
- package/src/verbs/verb.ts +1 -4
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +23 -16
- package/src/workspace-programs.ts +3 -10
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
package/src/exempt-classes.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The exemption classes of the Contract's vocabulary, in its order: each class's name,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
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"]
|
|
44
|
-
{ class: "format-verb-contract", languages: ["go"]
|
|
45
|
-
{ class: "errors-duck-typing", languages: ["go"]
|
|
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"]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
141
|
-
* holds in every configuration. A subject in no dead component gets one
|
|
142
|
-
* numbered past the computed ones in finding order
|
|
143
|
-
*
|
|
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 =
|
|
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(
|
|
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
|
|
272
|
+
fixability,
|
|
235
273
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
236
274
|
message: finding.message,
|
|
237
|
-
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
|
|
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(
|
|
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
|
|
package/src/findings/emitters.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
package/src/findings-pass.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
interface Rooted {
|
|
97
97
|
readonly at: number;
|
|
98
98
|
readonly kind: RootKind;
|
|
99
99
|
}
|
package/src/html-entries.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The files the
|
|
3
|
-
*
|
|
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
|
|
18
|
-
|
|
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
|
|
89
|
-
*
|
|
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
|
-
|
|
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
|
|
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,
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
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
|
}
|