@cplieger/deadset-ts 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/analysis.js +92 -23
- package/dist/src/edges.js +190 -0
- package/dist/src/finding.js +0 -11
- package/dist/src/findings/completion.js +92 -35
- package/dist/src/findings/dependencies-and-module-machinery.js +3 -11
- package/dist/src/findings/emitters.js +42 -4
- package/dist/src/findings/reads-and-writes.js +16 -69
- package/dist/src/findings/self-check.js +127 -2
- package/dist/src/findings/unused-declarations.js +8 -40
- package/dist/src/findings/visibility-narrowing.js +9 -40
- package/dist/src/findings-pass.js +97 -0
- package/dist/src/json-document.js +113 -0
- package/dist/src/kinds.js +47 -45
- package/dist/src/roots.js +0 -34
- package/dist/src/suppress-file.js +196 -0
- package/dist/src/suppress-inline.js +259 -0
- package/dist/src/suppress.js +148 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +176 -32
- package/src/edges.ts +241 -0
- package/src/finding.ts +76 -10
- package/src/findings/boundary.ts +0 -11
- package/src/findings/completion.ts +117 -71
- package/src/findings/dependencies-and-module-machinery.ts +4 -19
- package/src/findings/emitter.ts +6 -2
- package/src/findings/emitters.ts +81 -7
- package/src/findings/interfaces.ts +5 -17
- package/src/findings/reads-and-writes.ts +37 -151
- package/src/findings/self-check.ts +205 -2
- package/src/findings/unused-declarations.ts +11 -58
- package/src/findings/visibility-narrowing.ts +11 -58
- package/src/findings-pass.ts +145 -0
- package/src/json-document.ts +130 -0
- package/src/kinds.ts +79 -45
- package/src/roots.ts +0 -39
- package/src/suppress-file.ts +283 -0
- package/src/suppress-inline.ts +318 -0
- package/src/suppress.ts +262 -0
|
@@ -1,4 +1,207 @@
|
|
|
1
|
+
import { REPOSITORY_DOCUMENT } from "../config.ts";
|
|
2
|
+
import type { Finding } from "../finding.ts";
|
|
3
|
+
import type { InventorySymbol } from "../inventory.ts";
|
|
4
|
+
import type { Position } from "../position.ts";
|
|
5
|
+
import {
|
|
6
|
+
STALE_SUPPRESSION,
|
|
7
|
+
SUPPRESSION_WITHOUT_REASON,
|
|
8
|
+
type Mechanism,
|
|
9
|
+
type Refusal,
|
|
10
|
+
type SuppressionRecord,
|
|
11
|
+
} from "../suppress.ts";
|
|
1
12
|
import type { Emitter } from "./emitter.ts";
|
|
2
13
|
|
|
3
|
-
/** The
|
|
4
|
-
|
|
14
|
+
/** The code a configured root that matches nothing is reported under. */
|
|
15
|
+
const UNMATCHED_ROOT = "DS1704";
|
|
16
|
+
|
|
17
|
+
/** The subject kind of a finding about a suppression record. */
|
|
18
|
+
const SUPPRESSION_SUBJECT = "suppression";
|
|
19
|
+
|
|
20
|
+
/** The subject kind of a finding about a configured root. */
|
|
21
|
+
const ROOT_SUBJECT = "root";
|
|
22
|
+
|
|
23
|
+
/** The fixed position of a finding about a document rather than about a line of one. */
|
|
24
|
+
const DOCUMENT_POSITION = 1;
|
|
25
|
+
|
|
26
|
+
/** One record no finding matched, and the codes its declaration reports instead. */
|
|
27
|
+
export interface StaleRecord {
|
|
28
|
+
readonly record: SuppressionRecord;
|
|
29
|
+
/** The codes the bound declaration would report, none of them the record's own. */
|
|
30
|
+
readonly reports: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the self-check family reads: the run's refused suppressions and its configured roots. */
|
|
34
|
+
export interface SelfCheckFacts {
|
|
35
|
+
/** The suppressions the grammar refused with a finding, in reading order. */
|
|
36
|
+
readonly refusals: readonly Refusal[];
|
|
37
|
+
/** Every configured root or pattern that matched nothing in any project. */
|
|
38
|
+
readonly unmatchedRoots: readonly string[];
|
|
39
|
+
/** The document below the target root that declared the roots, empty where none did. */
|
|
40
|
+
readonly rootsDocument: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A run with nothing for the self-check family to report. */
|
|
44
|
+
export const NO_SELF_CHECK: SelfCheckFacts = {
|
|
45
|
+
refusals: [],
|
|
46
|
+
unmatchedRoots: [],
|
|
47
|
+
rootsDocument: "",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The words a message names a mechanism by. */
|
|
51
|
+
const WRITTEN: Readonly<Record<Mechanism, string>> = {
|
|
52
|
+
inline: "inline directive",
|
|
53
|
+
ignore: "ignore entry",
|
|
54
|
+
baseline: "baseline row",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** The file references of a run's declarations, by path. */
|
|
58
|
+
export function fileRefsOf(symbols: readonly InventorySymbol[]): FileRefs {
|
|
59
|
+
const byPath = new Map(
|
|
60
|
+
symbols
|
|
61
|
+
.filter((symbol) => symbol.kind === "file")
|
|
62
|
+
.map((symbol) => [symbol.position.path, symbol.ref]),
|
|
63
|
+
);
|
|
64
|
+
return (path) => byPath.get(path);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A list of codes as a sentence names them. */
|
|
68
|
+
function spelled(codes: readonly string[]): string {
|
|
69
|
+
if (codes.length < 2) {
|
|
70
|
+
return codes.join("");
|
|
71
|
+
}
|
|
72
|
+
return `${codes.slice(0, -1).join(", ")} and ${codes.at(-1) ?? ""}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A finding about one suppression, at the suppression's own site. */
|
|
76
|
+
function aboutSuppression(code: string, ref: string, site: Position, message: string): Finding {
|
|
77
|
+
return {
|
|
78
|
+
code,
|
|
79
|
+
position: { ...site, endLine: site.line },
|
|
80
|
+
symbol: { ref, kind: SUPPRESSION_SUBJECT, name: ref, sizeLines: 1 },
|
|
81
|
+
message,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Each file of the run by its path, as the reference of the file it is. */
|
|
86
|
+
export type FileRefs = (path: string) => string | undefined;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The reference a finding about a suppression names: the symbol it names or bound, else
|
|
90
|
+
* the file form of the document that holds it, else that document's path.
|
|
91
|
+
*/
|
|
92
|
+
function suppressionRef(symbol: string, path: string, files: FileRefs): string {
|
|
93
|
+
return symbol === "" ? (files(path) ?? path) : symbol;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One finding per refused suppression: the grammar's reason refusal and its scope refusal. */
|
|
97
|
+
export function refusedSuppressions(
|
|
98
|
+
refusals: readonly Refusal[],
|
|
99
|
+
files: FileRefs,
|
|
100
|
+
): readonly Finding[] {
|
|
101
|
+
return refusals.map((refused) =>
|
|
102
|
+
aboutSuppression(
|
|
103
|
+
refused.reported,
|
|
104
|
+
suppressionRef(refused.symbol, refused.path, files),
|
|
105
|
+
refused.site,
|
|
106
|
+
`${WRITTEN[refused.mechanism]} for ${refused.code} ${
|
|
107
|
+
refused.reported === SUPPRESSION_WITHOUT_REASON
|
|
108
|
+
? "carries no reason"
|
|
109
|
+
: "names a symbol and no path"
|
|
110
|
+
}`,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** One stale suppression as the report's stale-suppression record carries it. */
|
|
116
|
+
export interface StaleSuppression {
|
|
117
|
+
readonly code: typeof STALE_SUPPRESSION;
|
|
118
|
+
readonly mechanism: Mechanism;
|
|
119
|
+
readonly entry: {
|
|
120
|
+
readonly code: string;
|
|
121
|
+
readonly symbol?: string;
|
|
122
|
+
readonly path: string;
|
|
123
|
+
readonly reason: string;
|
|
124
|
+
};
|
|
125
|
+
readonly position: Position;
|
|
126
|
+
readonly symbol: string;
|
|
127
|
+
readonly message: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One record per site at which a suppression matched nothing, whatever the number of
|
|
132
|
+
* codes or declarations written there: the first record names the entry, and the message
|
|
133
|
+
* names every code the site's records named and every code the declaration reports
|
|
134
|
+
* instead, so the maintainer corrects the code rather than hunting for it.
|
|
135
|
+
*/
|
|
136
|
+
export function staleSuppressions(
|
|
137
|
+
stale: readonly StaleRecord[],
|
|
138
|
+
files: FileRefs,
|
|
139
|
+
): readonly StaleSuppression[] {
|
|
140
|
+
const bySite = new Map<string, { first: SuppressionRecord; records: StaleRecord[] }>();
|
|
141
|
+
for (const one of stale) {
|
|
142
|
+
const { site, mechanism } = one.record;
|
|
143
|
+
const key = `${mechanism}\u0000${site.path}\u0000${String(site.line)}\u0000${String(site.column)}`;
|
|
144
|
+
const held = bySite.get(key) ?? { first: one.record, records: [] };
|
|
145
|
+
held.records.push(one);
|
|
146
|
+
bySite.set(key, held);
|
|
147
|
+
}
|
|
148
|
+
return [...bySite.values()].map(({ first, records }) => {
|
|
149
|
+
const codes = [...new Set(records.map((one) => one.record.code))].sort();
|
|
150
|
+
const reports = [...new Set(records.flatMap((one) => one.reports))]
|
|
151
|
+
.filter((code) => !codes.includes(code))
|
|
152
|
+
.sort();
|
|
153
|
+
const instead = reports.length === 0 ? "" : `, and its declaration reports ${spelled(reports)}`;
|
|
154
|
+
return {
|
|
155
|
+
code: STALE_SUPPRESSION,
|
|
156
|
+
mechanism: first.mechanism,
|
|
157
|
+
entry: {
|
|
158
|
+
code: first.code,
|
|
159
|
+
...(first.symbol === "" ? {} : { symbol: first.symbol }),
|
|
160
|
+
path: first.path,
|
|
161
|
+
reason: first.reason,
|
|
162
|
+
},
|
|
163
|
+
position: first.site,
|
|
164
|
+
symbol: suppressionRef(first.symbol, first.path, files),
|
|
165
|
+
message: `${WRITTEN[first.mechanism]} for ${spelled(codes)} matches no current finding${instead}`,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* One finding per configured root or pattern that named nothing in any project. A finding
|
|
172
|
+
* names the document that declared the roots, or the conventional repository configuration
|
|
173
|
+
* where no document of the target did, and every finding of one run sits at that
|
|
174
|
+
* document's first position: the roots are an array whose members carry no line of their
|
|
175
|
+
* own, so the string each finding names tells one from another.
|
|
176
|
+
*/
|
|
177
|
+
export function unmatchedRootFindings(
|
|
178
|
+
sources: readonly string[],
|
|
179
|
+
document: string,
|
|
180
|
+
): readonly Finding[] {
|
|
181
|
+
const position = {
|
|
182
|
+
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
183
|
+
line: DOCUMENT_POSITION,
|
|
184
|
+
column: DOCUMENT_POSITION,
|
|
185
|
+
endLine: DOCUMENT_POSITION,
|
|
186
|
+
};
|
|
187
|
+
return sources.map((source) => ({
|
|
188
|
+
code: UNMATCHED_ROOT,
|
|
189
|
+
position,
|
|
190
|
+
symbol: { ref: source, kind: ROOT_SUBJECT, name: source, sizeLines: 1 },
|
|
191
|
+
message:
|
|
192
|
+
source.includes("*") || source.includes("?")
|
|
193
|
+
? "configured root pattern matches no symbol of the inventory"
|
|
194
|
+
: "configured root matches no symbol of the inventory",
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
|
|
200
|
+
* carrying no reason or naming no path, and a configured root that named nothing. A
|
|
201
|
+
* suppression that matched nothing is a record of the report's stale suppressions, which
|
|
202
|
+
* {@link staleSuppressions} builds, and a stale cross-language edge is the merge's to report.
|
|
203
|
+
*/
|
|
204
|
+
export const selfCheck: Emitter = ({ selfCheck: facts, swept }) => [
|
|
205
|
+
...refusedSuppressions(facts.refusals, fileRefsOf(swept.matrix.union.symbols)),
|
|
206
|
+
...unmatchedRootFindings(facts.unmatchedRoots, facts.rootsDocument),
|
|
207
|
+
];
|
|
@@ -1,33 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Finding } from "../finding.ts";
|
|
2
2
|
import type { InventorySymbol, SymbolKind } from "../inventory.ts";
|
|
3
3
|
import type { MatrixCandidate } from "../matrix.ts";
|
|
4
|
-
import {
|
|
5
|
-
capped,
|
|
6
|
-
findingComponents,
|
|
7
|
-
reachabilityClasses,
|
|
8
|
-
reportable,
|
|
9
|
-
severityOf,
|
|
10
|
-
type CompletedFinding,
|
|
11
|
-
} from "./completion.ts";
|
|
12
|
-
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
4
|
+
import type { Emitter } from "./emitter.ts";
|
|
13
5
|
import { unreachableExports } from "./visibility-narrowing.ts";
|
|
14
6
|
|
|
15
|
-
/** What the issue-kind vocabulary states about one code of the family. */
|
|
16
|
-
interface KindRow {
|
|
17
|
-
readonly defaultSeverity: Severity;
|
|
18
|
-
readonly maxClass: Confidence;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** The codes of the unused-declarations family, each by its row of the vocabulary. */
|
|
22
|
-
export const UNUSED_DECLARATION_KINDS: ReadonlyMap<string, KindRow> = new Map([
|
|
23
|
-
["DS1001", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
24
|
-
["DS1002", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
25
|
-
["DS1003", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
26
|
-
["DS1004", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
27
|
-
["DS1005", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
28
|
-
["DS1006", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
7
|
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
32
8
|
const MEMBERS: ReadonlySet<SymbolKind> = new Set([
|
|
33
9
|
"method",
|
|
@@ -160,20 +136,14 @@ function messageOf(code: string, judged: Judged): string {
|
|
|
160
136
|
return `${subject} has no reference in the target`;
|
|
161
137
|
}
|
|
162
138
|
|
|
163
|
-
/**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
export function unusedDeclarationFindings(input: EmitterInput): readonly CompletedFinding[] {
|
|
168
|
-
const { config, swept } = input;
|
|
169
|
-
const union = swept.matrix.union;
|
|
170
|
-
const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
|
|
171
|
-
const classOf = reachabilityClasses(swept);
|
|
172
|
-
const componentOf = findingComponents(swept);
|
|
139
|
+
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
140
|
+
export const unusedDeclarations: Emitter = (input) => {
|
|
141
|
+
const union = input.swept.matrix.union;
|
|
142
|
+
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
173
143
|
const unreachable = unreachableExports(input);
|
|
174
144
|
|
|
175
|
-
const found:
|
|
176
|
-
for (const candidate of swept.sweep.candidates) {
|
|
145
|
+
const found: Finding[] = [];
|
|
146
|
+
for (const candidate of input.swept.sweep.candidates) {
|
|
177
147
|
const at = union.at(candidate.id);
|
|
178
148
|
const symbol = union.symbols[at];
|
|
179
149
|
if (symbol === undefined) {
|
|
@@ -188,16 +158,10 @@ export function unusedDeclarationFindings(input: EmitterInput): readonly Complet
|
|
|
188
158
|
unreachable: unreachable.has(symbol.id),
|
|
189
159
|
};
|
|
190
160
|
const code = codeOf(judged);
|
|
191
|
-
|
|
192
|
-
if (code === undefined || row === undefined) {
|
|
161
|
+
if (code === undefined) {
|
|
193
162
|
continue;
|
|
194
163
|
}
|
|
195
|
-
const component = componentOf(symbol.id);
|
|
196
|
-
if (component === undefined) {
|
|
197
|
-
throw new Error(`${symbol.id} is reported dead and falls in no dead component`);
|
|
198
|
-
}
|
|
199
164
|
const endLine = Math.max(symbol.position.line, symbol.endLine);
|
|
200
|
-
const reachabilityClass = classOf(symbol.id);
|
|
201
165
|
found.push({
|
|
202
166
|
code,
|
|
203
167
|
position: { ...symbol.position, endLine },
|
|
@@ -208,18 +172,7 @@ export function unusedDeclarationFindings(input: EmitterInput): readonly Complet
|
|
|
208
172
|
sizeLines: endLine - symbol.position.line + 1,
|
|
209
173
|
},
|
|
210
174
|
message: messageOf(code, judged),
|
|
211
|
-
reachabilityClass,
|
|
212
|
-
confidence: capped(reachabilityClass, row.maxClass),
|
|
213
|
-
livenessRelation: candidate.relation,
|
|
214
|
-
testOnly: candidate.productionRefs === 0 && candidate.testRefs > 0,
|
|
215
|
-
component,
|
|
216
|
-
configurations: candidate.configurations,
|
|
217
|
-
severity: severityOf(config, code, row.defaultSeverity),
|
|
218
|
-
details: {},
|
|
219
175
|
});
|
|
220
176
|
}
|
|
221
|
-
return
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
225
|
-
export const unusedDeclarations: Emitter = unusedDeclarationFindings;
|
|
177
|
+
return found;
|
|
178
|
+
};
|
|
@@ -3,7 +3,6 @@ import { OUTSIDE, type Graph } from "../graph.ts";
|
|
|
3
3
|
import type { InventorySymbol, SymbolKind } from "../inventory.ts";
|
|
4
4
|
import type { MatrixCandidate } from "../matrix.ts";
|
|
5
5
|
import type { RootKind } from "../roots.ts";
|
|
6
|
-
import type { PendingFinding } from "./boundary.ts";
|
|
7
6
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
8
7
|
import { writeOnlyDeclarations } from "./reads-and-writes.ts";
|
|
9
8
|
|
|
@@ -33,13 +32,6 @@ const WORDS: Partial<Readonly<Record<SymbolKind, string>>> = {
|
|
|
33
32
|
"export-alias": "re-export",
|
|
34
33
|
};
|
|
35
34
|
|
|
36
|
-
/** What the family found: the findings it reports, and those a declared edge holds pending. */
|
|
37
|
-
export interface Narrowings {
|
|
38
|
-
readonly findings: readonly Finding[];
|
|
39
|
-
/** Ordered by edge, then by side. */
|
|
40
|
-
readonly pending: readonly PendingFinding[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
35
|
/** Per declaration of the run, the facts both rules read. */
|
|
44
36
|
interface Facts {
|
|
45
37
|
readonly graph: Graph;
|
|
@@ -126,18 +118,13 @@ function wordOf(symbol: InventorySymbol): string {
|
|
|
126
118
|
return WORDS[symbol.kind] ?? symbol.kind;
|
|
127
119
|
}
|
|
128
120
|
|
|
129
|
-
/** A finding of this family, with the visibility a narrowing would give its subject. */
|
|
130
|
-
interface NarrowingFinding extends Finding {
|
|
131
|
-
readonly details: { readonly narrowerVisibility?: "file" | "package" };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
121
|
/** The visibility each narrowing code names, which only the narrowing codes carry. */
|
|
135
122
|
const NARROWER: Readonly<Record<string, "file" | "package">> = {
|
|
136
123
|
[REDUNDANT_EXPORT_KEYWORD]: "file",
|
|
137
124
|
[UNNECESSARY_EXPORT]: "package",
|
|
138
125
|
};
|
|
139
126
|
|
|
140
|
-
function findingOf(symbol: InventorySymbol, code: string, message: string):
|
|
127
|
+
function findingOf(symbol: InventorySymbol, code: string, message: string): Finding {
|
|
141
128
|
const { path, line, column } = symbol.position;
|
|
142
129
|
const narrower = Object.hasOwn(NARROWER, code) ? NARROWER[code] : undefined;
|
|
143
130
|
const endLine = Math.max(line, symbol.endLine);
|
|
@@ -241,26 +228,15 @@ function unreachable(
|
|
|
241
228
|
);
|
|
242
229
|
}
|
|
243
230
|
|
|
244
|
-
/** Two strings ordered bytewise. */
|
|
245
|
-
function compare(a: string, b: string): number {
|
|
246
|
-
if (a === b) {
|
|
247
|
-
return 0;
|
|
248
|
-
}
|
|
249
|
-
return a < b ? -1 : 1;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
231
|
/**
|
|
253
|
-
* The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`.
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* file and the package, and whether it is a use is decided on the side in the other
|
|
258
|
-
* language.
|
|
232
|
+
* The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`. A finding
|
|
233
|
+
* about a symbol a declared cross-language edge names is reported here like any other, and
|
|
234
|
+
* the edge's evaluation then holds it pending, because the edge stands for a reference from
|
|
235
|
+
* outside the file and the package whose use is decided in the other language.
|
|
259
236
|
*/
|
|
260
|
-
export function narrowings(input: EmitterInput):
|
|
261
|
-
const { boundary } = input;
|
|
237
|
+
export function narrowings(input: EmitterInput): readonly Finding[] {
|
|
262
238
|
const facts = factsOf(input.swept.matrix.union);
|
|
263
|
-
const { declared, loaded } = boundary.consumers;
|
|
239
|
+
const { declared, loaded } = input.boundary.consumers;
|
|
264
240
|
const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
|
|
265
241
|
const world = { closed, writeOnly: writeOnlyDeclarations(input) };
|
|
266
242
|
const candidates = new Map(
|
|
@@ -268,27 +244,17 @@ export function narrowings(input: EmitterInput): Narrowings {
|
|
|
268
244
|
);
|
|
269
245
|
|
|
270
246
|
const findings: Finding[] = [];
|
|
271
|
-
const pending: PendingFinding[] = [];
|
|
272
247
|
facts.graph.symbols.forEach((symbol, at) => {
|
|
273
248
|
const candidate = candidates.get(symbol.id);
|
|
274
249
|
const found =
|
|
275
250
|
candidate === undefined
|
|
276
251
|
? narrowing(facts, at, symbol, world)
|
|
277
252
|
: unreachable(facts, at, symbol, candidate, input);
|
|
278
|
-
if (found
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
const named = boundary.edges.filter((side) => side.symbol === symbol.ref);
|
|
282
|
-
if (named.length === 0) {
|
|
253
|
+
if (found !== undefined) {
|
|
283
254
|
findings.push(found);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
for (const side of named) {
|
|
287
|
-
pending.push({ ...side, finding: found });
|
|
288
255
|
}
|
|
289
256
|
});
|
|
290
|
-
|
|
291
|
-
return { findings, pending };
|
|
257
|
+
return findings;
|
|
292
258
|
}
|
|
293
259
|
|
|
294
260
|
/**
|
|
@@ -308,18 +274,5 @@ export function unreachableExports(input: EmitterInput): ReadonlySet<string> {
|
|
|
308
274
|
return found;
|
|
309
275
|
}
|
|
310
276
|
|
|
311
|
-
/**
|
|
312
|
-
|
|
313
|
-
* declared edge holds pending belongs in that edge's evaluation, which this analyzer
|
|
314
|
-
* does not publish yet, so a run that holds one is refused rather than reported
|
|
315
|
-
* without it.
|
|
316
|
-
*/
|
|
317
|
-
export const visibilityNarrowing: Emitter = (input) => {
|
|
318
|
-
const { findings, pending } = narrowings(input);
|
|
319
|
-
if (pending.length > 0) {
|
|
320
|
-
throw new Error(
|
|
321
|
-
`${String(pending.length)} finding(s) a declared edge holds pending, and no edge evaluation is published`,
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
return findings;
|
|
325
|
-
};
|
|
277
|
+
/** The findings of the visibility-narrowing family, `DS1100` to `DS1199`. */
|
|
278
|
+
export const visibilityNarrowing: Emitter = narrowings;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The findings pass over one run: the emitter table's findings with the suppressions
|
|
3
|
+
* applied and the declared edges evaluated, and the stale suppressions, the totals and the
|
|
4
|
+
* verdict a report carries beside them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AnalysisInputs } from "./analysis.ts";
|
|
8
|
+
import type { Config, Severity } from "./config.ts";
|
|
9
|
+
import type { EdgeEvaluation } from "./edges.ts";
|
|
10
|
+
import type { CompletedFinding, Finding } from "./finding.ts";
|
|
11
|
+
import { decidedFindings } from "./findings/emitters.ts";
|
|
12
|
+
import {
|
|
13
|
+
fileRefsOf,
|
|
14
|
+
staleSuppressions,
|
|
15
|
+
type StaleRecord,
|
|
16
|
+
type StaleSuppression,
|
|
17
|
+
} from "./findings/self-check.ts";
|
|
18
|
+
import { isRowSubject, pairs, suppressionCounts, type Ledger } from "./suppress.ts";
|
|
19
|
+
import type { Provenance, Recorded } from "./suppress-file.ts";
|
|
20
|
+
import { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.ts";
|
|
21
|
+
|
|
22
|
+
/** The counts a report's totals carry beside its findings. */
|
|
23
|
+
export interface PassTotals {
|
|
24
|
+
readonly suppressionsInEffect: number;
|
|
25
|
+
readonly reasonsRecorded: number;
|
|
26
|
+
readonly staleSuppressions: number;
|
|
27
|
+
/** The evaluations that hold a finding pending. */
|
|
28
|
+
readonly pending: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What the pass answers about one run. */
|
|
32
|
+
export interface PassResult {
|
|
33
|
+
/** Every finding the report publishes, in the canonical order. */
|
|
34
|
+
readonly findings: readonly CompletedFinding[];
|
|
35
|
+
/** One record per site at which a suppression matched nothing, in reading order. */
|
|
36
|
+
readonly staleSuppressions: readonly StaleSuppression[];
|
|
37
|
+
/** One evaluation per declared edge side of this language. */
|
|
38
|
+
readonly edgeEvaluations: readonly EdgeEvaluation<CompletedFinding>[];
|
|
39
|
+
readonly totals: PassTotals;
|
|
40
|
+
/** What every suppression record did, in reading order. */
|
|
41
|
+
readonly ledger: Ledger<CompletedFinding>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The exit code of a report holding at least one pending finding. */
|
|
45
|
+
export const EXIT_PENDING = 4;
|
|
46
|
+
|
|
47
|
+
/** The severities ranked, the strongest highest. */
|
|
48
|
+
const SEVERITY_RANK: Readonly<Record<Severity, number>> = { allow: 0, warn: 1, deny: 2 };
|
|
49
|
+
|
|
50
|
+
/** Two strings ordered bytewise. */
|
|
51
|
+
function compare(a: string, b: string): number {
|
|
52
|
+
if (a === b) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
return a < b ? -1 : 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The canonical order: path, line, column, code, then the symbol reference. */
|
|
59
|
+
function canonical(a: Finding, b: Finding): number {
|
|
60
|
+
return (
|
|
61
|
+
compare(a.position.path, b.position.path) ||
|
|
62
|
+
a.position.line - b.position.line ||
|
|
63
|
+
a.position.column - b.position.column ||
|
|
64
|
+
compare(a.code, b.code) ||
|
|
65
|
+
compare(a.symbol.ref, b.symbol.ref)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The pass over one run. The emitter table decides every record against every finding the
|
|
71
|
+
* run would produce with no record there, withholds what the records claim, and evaluates
|
|
72
|
+
* the declared edges; a stale record then names the codes its declaration reports instead.
|
|
73
|
+
*/
|
|
74
|
+
export function findingsPass(inputs: AnalysisInputs): PassResult {
|
|
75
|
+
const { marked, unmarked, suppressions } = inputs;
|
|
76
|
+
const decided = decidedFindings(marked, { unmarked, records: suppressions.records });
|
|
77
|
+
const { ledger, would } = decided;
|
|
78
|
+
|
|
79
|
+
const stale: StaleRecord[] = [];
|
|
80
|
+
suppressions.records.forEach((record, at) => {
|
|
81
|
+
if (ledger.verdicts[at] !== "stale") {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const reports = would
|
|
85
|
+
.filter(
|
|
86
|
+
(finding) =>
|
|
87
|
+
finding.code !== record.code && pairs({ ...record, code: finding.code }, finding),
|
|
88
|
+
)
|
|
89
|
+
.map((finding) => finding.code);
|
|
90
|
+
stale.push({ record, reports });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const records = staleSuppressions(stale, fileRefsOf(marked.swept.matrix.union.symbols));
|
|
94
|
+
const counts = suppressionCounts(suppressions.records, ledger);
|
|
95
|
+
return {
|
|
96
|
+
findings: [...decided.findings].sort(canonical),
|
|
97
|
+
staleSuppressions: records,
|
|
98
|
+
edgeEvaluations: decided.evaluations,
|
|
99
|
+
totals: {
|
|
100
|
+
suppressionsInEffect: counts.inEffect,
|
|
101
|
+
reasonsRecorded: counts.reasonsRecorded,
|
|
102
|
+
staleSuppressions: records.length,
|
|
103
|
+
pending: decided.evaluations.filter((one) => one.finding !== undefined).length,
|
|
104
|
+
},
|
|
105
|
+
ledger,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The exit code of a report the pass answered: pending when an evaluation holds a finding,
|
|
111
|
+
* which outranks the rest because an unmerged report is not an answer; findings when a
|
|
112
|
+
* suppression is stale or a finding's severity reaches the configured failing severity;
|
|
113
|
+
* clean otherwise.
|
|
114
|
+
*/
|
|
115
|
+
export function verdictOf(result: PassResult, config: Config): number {
|
|
116
|
+
if (result.totals.pending > 0) {
|
|
117
|
+
return EXIT_PENDING;
|
|
118
|
+
}
|
|
119
|
+
if (result.totals.staleSuppressions > 0) {
|
|
120
|
+
return EXIT_FINDINGS;
|
|
121
|
+
}
|
|
122
|
+
const failing = SEVERITY_RANK[config.reporters.failOn];
|
|
123
|
+
const fails = result.findings.some((finding) => SEVERITY_RANK[finding.severity] >= failing);
|
|
124
|
+
return fails ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The findings of a report as a baseline records them, in report order. A finding about a
|
|
129
|
+
* row of a document is not recorded: no record withholds one, so a row recording it would
|
|
130
|
+
* be stale on the first read back.
|
|
131
|
+
*/
|
|
132
|
+
export function recordedFindings(result: PassResult): readonly Recorded[] {
|
|
133
|
+
return result.findings
|
|
134
|
+
.filter((finding) => !isRowSubject(finding.symbol.kind))
|
|
135
|
+
.map((finding) => ({
|
|
136
|
+
code: finding.code,
|
|
137
|
+
symbol: finding.symbol.ref,
|
|
138
|
+
path: finding.position.path,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
|
|
143
|
+
export function analyzerProvenance(version: string): Provenance {
|
|
144
|
+
return { analyzer: "deadset-ts", version };
|
|
145
|
+
}
|