@cplieger/deadset-ts 7.0.0-dev.1 → 7.0.0-dev.3
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 +95 -60
- package/dist/src/augmentations.js +14 -12
- package/dist/src/component-layer.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +230 -30
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +104 -13
- package/dist/src/convention-rows.js +10 -0
- package/dist/src/conventions.js +35 -1
- package/dist/src/dependencies.js +90 -32
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +78 -16
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -5
- package/dist/src/findings/self-check.js +27 -10
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -119
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +28 -12
- package/dist/src/keyof-comparisons.js +170 -0
- package/dist/src/kinds.js +17 -7
- package/dist/src/manifest.js +6 -16
- package/dist/src/matrix.js +20 -2
- package/dist/src/query.js +3 -0
- package/dist/src/references.js +14 -3
- package/dist/src/report.js +4 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -8
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +64 -35
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-reach.js +7 -7
- package/dist/src/verbs/analyze.js +11 -9
- package/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +145 -82
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +104 -22
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +250 -31
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +135 -17
- package/src/convention-rows.ts +23 -0
- package/src/conventions.ts +53 -1
- package/src/dependencies.ts +119 -38
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +1 -1
- package/src/erased-stores.ts +413 -0
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +3 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +97 -21
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +4 -9
- package/src/findings/self-check.ts +31 -14
- package/src/findings-pass.ts +14 -7
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -137
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +37 -33
- package/src/json-document.ts +1 -1
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +7 -22
- package/src/matrix.ts +34 -2
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/query.ts +10 -0
- package/src/references.ts +30 -7
- package/src/report.ts +11 -5
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -31
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +17 -7
- package/src/suppress.ts +79 -47
- package/src/template-field.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
|
@@ -9,6 +9,7 @@ import { componentId, type Component } from "../components.ts";
|
|
|
9
9
|
import type { Cascade, Confidence, Severity } from "../config.ts";
|
|
10
10
|
import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
|
|
11
11
|
import { OUTSIDE, type Graph } from "../graph.ts";
|
|
12
|
+
import type { SymbolKind } from "../inventory.ts";
|
|
12
13
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
13
14
|
import { byPosition, positionKey } from "../position.ts";
|
|
14
15
|
import { declarationKey } from "../ref.ts";
|
|
@@ -17,6 +18,17 @@ import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
|
|
|
17
18
|
import type { Boundary } from "./boundary.ts";
|
|
18
19
|
import type { EmitterInput } from "./emitter.ts";
|
|
19
20
|
|
|
21
|
+
/** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
|
|
22
|
+
const MANUAL = "manual";
|
|
23
|
+
|
|
24
|
+
/** The member kinds an object property can declare, which an `in` expression tests by name. */
|
|
25
|
+
const IN_TESTED: ReadonlySet<SymbolKind> = new Set([
|
|
26
|
+
"class-member",
|
|
27
|
+
"method",
|
|
28
|
+
"type-member",
|
|
29
|
+
"interface-method",
|
|
30
|
+
]);
|
|
31
|
+
|
|
20
32
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
21
33
|
const RANK: Readonly<Record<Confidence, number>> = { certain: 3, probable: 2, possible: 1 };
|
|
22
34
|
|
|
@@ -54,19 +66,43 @@ function publishedIds(union: Graph): ReadonlySet<string> {
|
|
|
54
66
|
);
|
|
55
67
|
}
|
|
56
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The members whose name an `in` expression of a production file tests by a literal: a
|
|
71
|
+
* value narrowed that way is read back by the name, which no type links to the member.
|
|
72
|
+
*/
|
|
73
|
+
function testedByIn(input: EmitterInput): ReadonlySet<string> {
|
|
74
|
+
const names = input.files.inOperands ?? new Set<string>();
|
|
75
|
+
const union = input.swept.matrix.union;
|
|
76
|
+
if (names.size === 0) {
|
|
77
|
+
return names;
|
|
78
|
+
}
|
|
79
|
+
return new Set(
|
|
80
|
+
union.symbols.flatMap((symbol, at) => {
|
|
81
|
+
const parent = union.symbols[union.parent[at] ?? OUTSIDE];
|
|
82
|
+
return IN_TESTED.has(symbol.kind) &&
|
|
83
|
+
parent !== undefined &&
|
|
84
|
+
symbol.name.startsWith(`${parent.name}.`) &&
|
|
85
|
+
names.has(symbol.name.slice(parent.name.length + 1))
|
|
86
|
+
? [symbol.id]
|
|
87
|
+
: [];
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
57
92
|
/**
|
|
58
93
|
* The reachability class of one declaration of the run. A declaration of a library's
|
|
59
94
|
* published API, a public member of a published type included, is `certain` where the
|
|
60
95
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
61
96
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
62
|
-
* consumer set.
|
|
63
|
-
*
|
|
97
|
+
* consumer set. Test-support code test code references, outside the test-of-dead-code
|
|
98
|
+
* rule, what it contains, and a member an `in` expression tests by name are `possible`.
|
|
64
99
|
*/
|
|
65
100
|
function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
101
|
+
const tested = testedByIn(input);
|
|
66
102
|
const published = publishedIds(input.swept.matrix.union);
|
|
67
103
|
const usedByTests = new Set(
|
|
68
104
|
input.swept.sweep.candidates
|
|
69
|
-
.filter((candidate) => candidate.heldByTests
|
|
105
|
+
.filter((candidate) => candidate.heldByTests !== undefined)
|
|
70
106
|
.map((candidate) => candidate.id),
|
|
71
107
|
);
|
|
72
108
|
const { declared } = input.boundary.consumers;
|
|
@@ -77,13 +113,42 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
77
113
|
? "certain"
|
|
78
114
|
: "probable";
|
|
79
115
|
return (id) => {
|
|
80
|
-
if (usedByTests.has(id)) {
|
|
116
|
+
if (usedByTests.has(id) || tested.has(id)) {
|
|
81
117
|
return "possible";
|
|
82
118
|
}
|
|
83
119
|
return published.has(id) ? outside : "certain";
|
|
84
120
|
};
|
|
85
121
|
}
|
|
86
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The confidence ceiling of a test of dead code: the lowest reachability class among the
|
|
125
|
+
* target symbols it references, because its deadness is theirs, whether or not they share
|
|
126
|
+
* its component. Every other declaration has none below `certain`.
|
|
127
|
+
*/
|
|
128
|
+
function testCeilings(
|
|
129
|
+
input: EmitterInput,
|
|
130
|
+
classOf: (id: string) => Confidence,
|
|
131
|
+
): (id: string) => Confidence {
|
|
132
|
+
const union = input.swept.matrix.union;
|
|
133
|
+
const tests = new Set(
|
|
134
|
+
input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
|
|
135
|
+
);
|
|
136
|
+
return (id) => {
|
|
137
|
+
let lowest: Confidence = "certain";
|
|
138
|
+
if (!tests.has(id)) {
|
|
139
|
+
return lowest;
|
|
140
|
+
}
|
|
141
|
+
for (const edge of union.out[union.at(id)] ?? []) {
|
|
142
|
+
const target = union.symbols[edge.to];
|
|
143
|
+
const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
|
|
144
|
+
if (target !== undefined && !testCode && union.subject[edge.to] === true) {
|
|
145
|
+
lowest = capped(lowest, classOf(target.id));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return lowest;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
87
152
|
/**
|
|
88
153
|
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
89
154
|
* its members listed in the canonical order where the run lists a component in full.
|
|
@@ -154,7 +219,11 @@ export function completed(
|
|
|
154
219
|
);
|
|
155
220
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
156
221
|
const classOf = reachabilityClasses(input);
|
|
222
|
+
const ceilingOf = testCeilings(input, classOf);
|
|
157
223
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
224
|
+
const held = new Set(
|
|
225
|
+
swept.sweep.components.filter((one) => one.held).flatMap((one) => one.members),
|
|
226
|
+
);
|
|
158
227
|
let minted = swept.sweep.components.length;
|
|
159
228
|
const dead = new Set(swept.sweep.components.map((component) => component.id));
|
|
160
229
|
const fallingWith = new Map<string, number>();
|
|
@@ -212,6 +281,18 @@ export function completed(
|
|
|
212
281
|
const reachabilityClass =
|
|
213
282
|
judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
|
|
214
283
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
284
|
+
const fixability = generated
|
|
285
|
+
? "none"
|
|
286
|
+
: found !== undefined &&
|
|
287
|
+
symbol !== undefined &&
|
|
288
|
+
held.has(symbol.id) &&
|
|
289
|
+
row.findingFixabilities?.includes(MANUAL) === true
|
|
290
|
+
? MANUAL
|
|
291
|
+
: row.fixability;
|
|
292
|
+
const lastUses =
|
|
293
|
+
fixability === "deletable" && candidate !== undefined
|
|
294
|
+
? input.dependencies.lastUses.get(candidate.id)
|
|
295
|
+
: undefined;
|
|
215
296
|
return {
|
|
216
297
|
code: finding.code,
|
|
217
298
|
kind: row.name,
|
|
@@ -219,7 +300,10 @@ export function completed(
|
|
|
219
300
|
position: finding.position,
|
|
220
301
|
symbol: finding.symbol,
|
|
221
302
|
reachabilityClass,
|
|
222
|
-
confidence: capped(
|
|
303
|
+
confidence: capped(
|
|
304
|
+
capped(reachabilityClass, row.maxClass),
|
|
305
|
+
judged === undefined ? "certain" : ceilingOf(judged.id),
|
|
306
|
+
),
|
|
223
307
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
224
308
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
225
309
|
generated,
|
|
@@ -231,10 +315,13 @@ export function completed(
|
|
|
231
315
|
? swept.matrix.configurations
|
|
232
316
|
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
233
317
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
234
|
-
fixability
|
|
318
|
+
fixability,
|
|
235
319
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
236
320
|
message: finding.message,
|
|
237
|
-
details:
|
|
321
|
+
details:
|
|
322
|
+
lastUses === undefined
|
|
323
|
+
? (finding.details ?? {})
|
|
324
|
+
: { ...finding.details, removesLastUseOf: lastUses },
|
|
238
325
|
};
|
|
239
326
|
});
|
|
240
327
|
return componentCapped(done, dead);
|
|
@@ -278,33 +365,22 @@ function dialed(
|
|
|
278
365
|
* below the minimum confidence is withheld, and so is every finding of the component of a
|
|
279
366
|
* root the dials withhold, because a symbol that falls with a root is dead only through it
|
|
280
367
|
* and its deletion does not compile while the root stays. A withheld finding that is no
|
|
281
|
-
* root withholds itself alone.
|
|
282
|
-
* would report there, completed, is withheld by its own severity or confidence.
|
|
368
|
+
* root withholds itself alone.
|
|
283
369
|
*/
|
|
284
370
|
export function dialsOf(
|
|
285
371
|
input: EmitterInput,
|
|
286
372
|
findings: readonly CompletedFinding[],
|
|
287
373
|
): Dials<CompletedFinding> {
|
|
288
374
|
const minConfidence = input.config.analysis.minConfidence;
|
|
289
|
-
const classOf = reachabilityClasses(input);
|
|
290
375
|
const components = new Set(
|
|
291
376
|
findings
|
|
292
377
|
.filter((finding) => finding.component.root && dialed(finding, minConfidence))
|
|
293
378
|
.map((finding) => finding.component.id),
|
|
294
379
|
);
|
|
295
380
|
return {
|
|
296
|
-
|
|
381
|
+
allows: (code) => {
|
|
297
382
|
const row = KINDS.get(code);
|
|
298
|
-
return (
|
|
299
|
-
row !== undefined &&
|
|
300
|
-
dialed(
|
|
301
|
-
{
|
|
302
|
-
severity: severityOf(input, code, row.defaultSeverity),
|
|
303
|
-
confidence: capped(classOf(id), row.maxClass),
|
|
304
|
-
},
|
|
305
|
-
minConfidence,
|
|
306
|
-
)
|
|
307
|
-
);
|
|
383
|
+
return row !== undefined && severityOf(input, code, row.defaultSeverity) === "allow";
|
|
308
384
|
},
|
|
309
385
|
withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
|
|
310
386
|
};
|
|
@@ -19,7 +19,7 @@ function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
|
19
19
|
return dependencies.declared
|
|
20
20
|
.filter(
|
|
21
21
|
(dependency) =>
|
|
22
|
-
!dependencies.needed.has(dependency
|
|
22
|
+
!dependencies.needed.has(declarationKey(dependency)) &&
|
|
23
23
|
!dependencies.ran.has(declarationKey(dependency)),
|
|
24
24
|
)
|
|
25
25
|
.map((dependency) => ({
|
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. */
|
|
@@ -116,7 +116,7 @@ function dialsOver(
|
|
|
116
116
|
const before = dialsOf(suppressed.unmarked, unmarked);
|
|
117
117
|
const fromUnmarked = new Set(unmarked);
|
|
118
118
|
return {
|
|
119
|
-
|
|
119
|
+
allows: marked.allows,
|
|
120
120
|
withholds: (finding) =>
|
|
121
121
|
fromUnmarked.has(finding) ? before.withholds(finding) : marked.withholds(finding),
|
|
122
122
|
};
|
|
@@ -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,11 @@
|
|
|
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
|
+
isDependencyCode,
|
|
8
|
+
isUnsuppressibleCode,
|
|
6
9
|
STALE_SUPPRESSION,
|
|
7
10
|
SUPPRESSION_WITHOUT_REASON,
|
|
8
11
|
type Mechanism,
|
|
@@ -58,14 +61,6 @@ interface UnmatchedDeclaration {
|
|
|
58
61
|
readonly document: string;
|
|
59
62
|
}
|
|
60
63
|
|
|
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
64
|
/** The words a message names a mechanism by. */
|
|
70
65
|
const WRITTEN: Readonly<Record<Mechanism, string>> = {
|
|
71
66
|
inline: "inline directive",
|
|
@@ -122,7 +117,7 @@ function writtenEntry(refused: Refusal): SuppressionEntry {
|
|
|
122
117
|
}
|
|
123
118
|
|
|
124
119
|
/** Each file of the run by its path, as the reference of the file it is. */
|
|
125
|
-
|
|
120
|
+
type FileRefs = (path: string) => string | undefined;
|
|
126
121
|
|
|
127
122
|
/**
|
|
128
123
|
* The reference a finding about a suppression names: the symbol it names or bound, else
|
|
@@ -133,10 +128,7 @@ function suppressionRef(symbol: string, path: string, files: FileRefs): string {
|
|
|
133
128
|
}
|
|
134
129
|
|
|
135
130
|
/** 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[] {
|
|
131
|
+
function refusedSuppressions(refusals: readonly Refusal[], files: FileRefs): readonly Finding[] {
|
|
140
132
|
return refusals.map((refused) =>
|
|
141
133
|
aboutSuppression(
|
|
142
134
|
refused.reported,
|
|
@@ -190,7 +182,32 @@ export function staleSuppressions(
|
|
|
190
182
|
const reports = [...new Set(records.flatMap((one) => one.reports))]
|
|
191
183
|
.filter((code) => !codes.includes(code))
|
|
192
184
|
.sort();
|
|
193
|
-
const
|
|
185
|
+
const rows = codes.filter(isUnsuppressibleCode);
|
|
186
|
+
const dependencies = first.mechanism === "inline" ? codes.filter(isDependencyCode) : [];
|
|
187
|
+
const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
|
|
188
|
+
const remedy =
|
|
189
|
+
settable.length === 0
|
|
190
|
+
? "fix what the finding names instead"
|
|
191
|
+
: `fix what the finding names or set the severity of ${spelled(settable)} instead`;
|
|
192
|
+
const declared = reports.length === 0 ? "" : `its declaration reports ${spelled(reports)}`;
|
|
193
|
+
const because = [
|
|
194
|
+
...(rows.length > 0
|
|
195
|
+
? [
|
|
196
|
+
`${spelled(rows)} ${rows.length === 1 ? "reports a row" : "report rows"} of a document, which no suppression can name; ${remedy}`,
|
|
197
|
+
]
|
|
198
|
+
: []),
|
|
199
|
+
...(dependencies.length > 0
|
|
200
|
+
? [
|
|
201
|
+
`${spelled(dependencies)} ${dependencies.length === 1 ? "reports a dependency row" : "report dependency rows"}, which only an ignore entry or a baseline row can name`,
|
|
202
|
+
]
|
|
203
|
+
: []),
|
|
204
|
+
];
|
|
205
|
+
const instead =
|
|
206
|
+
because.length > 0
|
|
207
|
+
? `, because ${because.join("; ")}${declared === "" ? "" : `; ${declared}`}`
|
|
208
|
+
: declared === ""
|
|
209
|
+
? ""
|
|
210
|
+
: `, and ${declared}`;
|
|
194
211
|
return {
|
|
195
212
|
code: STALE_SUPPRESSION,
|
|
196
213
|
mechanism: first.mechanism,
|
package/src/findings-pass.ts
CHANGED
|
@@ -15,12 +15,18 @@ import {
|
|
|
15
15
|
type StaleRecord,
|
|
16
16
|
type StaleSuppression,
|
|
17
17
|
} from "./findings/self-check.ts";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
isDependencySubject,
|
|
20
|
+
isRowSubject,
|
|
21
|
+
pairs,
|
|
22
|
+
suppressionCounts,
|
|
23
|
+
type Ledger,
|
|
24
|
+
} from "./suppress.ts";
|
|
19
25
|
import type { Provenance, Recorded } from "./suppress-file.ts";
|
|
20
26
|
import { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.ts";
|
|
21
27
|
|
|
22
28
|
/** The counts a report's totals carry beside its findings. */
|
|
23
|
-
|
|
29
|
+
interface PassTotals {
|
|
24
30
|
readonly suppressionsInEffect: number;
|
|
25
31
|
readonly reasonsRecorded: number;
|
|
26
32
|
readonly staleSuppressions: number;
|
|
@@ -40,7 +46,7 @@ export interface PassResult {
|
|
|
40
46
|
readonly edgeEvaluations: readonly EdgeEvaluation<CompletedFinding>[];
|
|
41
47
|
readonly totals: PassTotals;
|
|
42
48
|
/** What every suppression record did, in reading order. */
|
|
43
|
-
readonly ledger: Ledger
|
|
49
|
+
readonly ledger: Ledger;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/** The exit code of a report holding at least one pending finding. */
|
|
@@ -134,14 +140,15 @@ export interface RecordableFinding {
|
|
|
134
140
|
|
|
135
141
|
/**
|
|
136
142
|
* The rows one round of a baseline write records from its findings, in their order. A
|
|
137
|
-
* finding about a row of a document is not recorded: no record
|
|
138
|
-
* recording it would be stale on the first read back. Two findings
|
|
139
|
-
* same row, two parts of one declaration under one code, record it
|
|
143
|
+
* finding about a row of a document other than a dependency row is not recorded: no record
|
|
144
|
+
* withholds one, so a row recording it would be stale on the first read back. Two findings
|
|
145
|
+
* that would record the same row, two parts of one declaration under one code, record it
|
|
146
|
+
* once.
|
|
140
147
|
*/
|
|
141
148
|
export function recordedFindings(findings: readonly RecordableFinding[]): readonly Recorded[] {
|
|
142
149
|
const rows = new Map<string, Recorded>();
|
|
143
150
|
for (const finding of findings) {
|
|
144
|
-
if (isRowSubject(finding.symbol.kind)) {
|
|
151
|
+
if (isRowSubject(finding.symbol.kind) && !isDependencySubject(finding.symbol.kind)) {
|
|
145
152
|
continue;
|
|
146
153
|
}
|
|
147
154
|
const row = { code: finding.code, symbol: finding.symbol.ref, path: finding.position.path };
|
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
|
@@ -16,7 +16,7 @@ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
|
16
16
|
import type { SourceFiles } from "./source-files.ts";
|
|
17
17
|
|
|
18
18
|
/** One file a script loads, and the value or specifier that named it. */
|
|
19
|
-
|
|
19
|
+
interface HTMLEntry {
|
|
20
20
|
readonly file: SourceFile;
|
|
21
21
|
readonly source: string;
|
|
22
22
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The property names `in` tests in a project's production files. A value narrowed by
|
|
3
|
+
* `"name" in value` is read back by that name, often through `unknown`, where no type
|
|
4
|
+
* links the read to a declaration, so only a text match can tell which member it reads.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isBinaryExpression,
|
|
9
|
+
isNoSubstitutionTemplateLiteral,
|
|
10
|
+
isStringLiteral,
|
|
11
|
+
SyntaxKind,
|
|
12
|
+
type Node,
|
|
13
|
+
} from "@typescript/native/unstable/ast";
|
|
14
|
+
import type { ProjectView } from "./session.ts";
|
|
15
|
+
import { sourceFilesOf } from "./source-files.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The text of each string literal, or template literal with no substitution, written as
|
|
19
|
+
* the left operand of an `in` expression in a file of the project that is no test file.
|
|
20
|
+
* `testFiles` holds the test files by their path below the target root.
|
|
21
|
+
*/
|
|
22
|
+
export function inOperandNames<Brand>(
|
|
23
|
+
project: ProjectView<Brand>,
|
|
24
|
+
targetRoot: string,
|
|
25
|
+
testFiles: readonly string[],
|
|
26
|
+
): readonly string[] {
|
|
27
|
+
const tests = new Set(testFiles);
|
|
28
|
+
const found = new Set<string>();
|
|
29
|
+
const visit = (node: Node): void => {
|
|
30
|
+
if (
|
|
31
|
+
isBinaryExpression(node) &&
|
|
32
|
+
node.operatorToken.kind === SyntaxKind.InKeyword &&
|
|
33
|
+
(isStringLiteral(node.left) || isNoSubstitutionTemplateLiteral(node.left))
|
|
34
|
+
) {
|
|
35
|
+
found.add(node.left.text);
|
|
36
|
+
}
|
|
37
|
+
node.forEachChild(visit);
|
|
38
|
+
};
|
|
39
|
+
for (const [path, file] of sourceFilesOf(project, targetRoot).byPath) {
|
|
40
|
+
if (!tests.has(path)) {
|
|
41
|
+
file.forEachChild(visit);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [...found].sort();
|
|
45
|
+
}
|