@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,28 +1,12 @@
|
|
|
1
1
|
import { positionKey } from "../position.js";
|
|
2
|
-
import { findingComponents, RANK, reachabilityClasses, severityOf, } from "./completion.js";
|
|
3
|
-
/**
|
|
4
|
-
* Each code's severity where the configuration sets none, as the issue-kind vocabulary
|
|
5
|
-
* states it.
|
|
6
|
-
*/
|
|
7
|
-
export const DEFAULT_SEVERITY = {
|
|
8
|
-
DS1301: "deny",
|
|
9
|
-
DS1302: "deny",
|
|
10
|
-
DS1303: "deny",
|
|
11
|
-
};
|
|
12
2
|
/** The containers whose type parameters `DS1303` reports: a function and a class method. */
|
|
13
3
|
const SIGNATURES = new Set(["function", "method"]);
|
|
14
|
-
/** The severity the configuration gives one code of the family. */
|
|
15
|
-
function severityIn(input, code) {
|
|
16
|
-
return severityOf(input.config, code, DEFAULT_SEVERITY[code]);
|
|
17
|
-
}
|
|
18
4
|
function indexed(input) {
|
|
19
5
|
const { matrix, sweep } = input.swept;
|
|
20
6
|
return {
|
|
21
7
|
input,
|
|
22
8
|
symbols: new Map(matrix.union.symbols.map((symbol) => [symbol.id, symbol])),
|
|
23
9
|
candidates: new Map(sweep.candidates.map((candidate) => [candidate.id, candidate])),
|
|
24
|
-
componentOf: findingComponents(input.swept),
|
|
25
|
-
classOf: reachabilityClasses(input.swept),
|
|
26
10
|
};
|
|
27
11
|
}
|
|
28
12
|
/** Where one declaration is, as a finding states it. */
|
|
@@ -34,11 +18,9 @@ function positionOf(symbol) {
|
|
|
34
18
|
endLine: Math.max(symbol.position.line, symbol.endLine),
|
|
35
19
|
};
|
|
36
20
|
}
|
|
37
|
-
/** The finding
|
|
38
|
-
function findingAbout(
|
|
21
|
+
/** The finding one code makes about one declaration. */
|
|
22
|
+
function findingAbout(symbol, code, message, details) {
|
|
39
23
|
const position = positionOf(symbol);
|
|
40
|
-
const reachabilityClass = run.classOf(symbol.id);
|
|
41
|
-
const component = run.componentOf(symbol.id);
|
|
42
24
|
return {
|
|
43
25
|
code,
|
|
44
26
|
position,
|
|
@@ -49,14 +31,7 @@ function findingAbout(run, symbol, code, message, facts) {
|
|
|
49
31
|
sizeLines: position.endLine - position.line + 1,
|
|
50
32
|
},
|
|
51
33
|
message,
|
|
52
|
-
|
|
53
|
-
confidence: reachabilityClass,
|
|
54
|
-
...(facts.livenessRelation === undefined ? {} : { livenessRelation: facts.livenessRelation }),
|
|
55
|
-
testOnly: facts.testOnly,
|
|
56
|
-
...(component === undefined ? {} : { component }),
|
|
57
|
-
configurations: facts.configurations,
|
|
58
|
-
severity: severityIn(run.input, code),
|
|
59
|
-
details: facts.details,
|
|
34
|
+
...(details === undefined ? {} : { details }),
|
|
60
35
|
};
|
|
61
36
|
}
|
|
62
37
|
/**
|
|
@@ -85,15 +60,9 @@ function writeOnlySymbols(run) {
|
|
|
85
60
|
for (const reference of stores.references) {
|
|
86
61
|
let held = uses.get(reference.to);
|
|
87
62
|
if (held === undefined) {
|
|
88
|
-
held = { writes: [], reads: 0
|
|
63
|
+
held = { writes: [], reads: 0 };
|
|
89
64
|
uses.set(reference.to, held);
|
|
90
65
|
}
|
|
91
|
-
if (reference.test) {
|
|
92
|
-
held.test += 1;
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
held.production += 1;
|
|
96
|
-
}
|
|
97
66
|
if (reference.use !== "write") {
|
|
98
67
|
held.reads += 1;
|
|
99
68
|
continue;
|
|
@@ -105,7 +74,7 @@ function writeOnlySymbols(run) {
|
|
|
105
74
|
}
|
|
106
75
|
}
|
|
107
76
|
const found = [];
|
|
108
|
-
matrix.union.symbols
|
|
77
|
+
for (const symbol of matrix.union.symbols) {
|
|
109
78
|
const held = uses.get(symbol.id);
|
|
110
79
|
if (held === undefined ||
|
|
111
80
|
held.writes.length === 0 ||
|
|
@@ -113,16 +82,14 @@ function writeOnlySymbols(run) {
|
|
|
113
82
|
!writable(run, symbol) ||
|
|
114
83
|
stores.exempt.has(symbol.id) ||
|
|
115
84
|
run.candidates.has(symbol.id)) {
|
|
116
|
-
|
|
85
|
+
continue;
|
|
117
86
|
}
|
|
118
87
|
const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
|
|
119
88
|
const word = symbol.kind === "variable" ? "variable" : "member";
|
|
120
|
-
found.push(findingAbout(
|
|
121
|
-
|
|
122
|
-
configurations: matrix.heldIn[at] ?? [],
|
|
123
|
-
details: { writePositions: held.writes },
|
|
89
|
+
found.push(findingAbout(symbol, "DS1301", `${word} ${symbol.name} is written ${writes} and never read`, {
|
|
90
|
+
writePositions: held.writes,
|
|
124
91
|
}));
|
|
125
|
-
}
|
|
92
|
+
}
|
|
126
93
|
return found;
|
|
127
94
|
}
|
|
128
95
|
/**
|
|
@@ -139,7 +106,7 @@ function unnamed(run, kind) {
|
|
|
139
106
|
run.candidates.has(symbol.parent)) {
|
|
140
107
|
return [];
|
|
141
108
|
}
|
|
142
|
-
return [
|
|
109
|
+
return [symbol];
|
|
143
110
|
});
|
|
144
111
|
}
|
|
145
112
|
/**
|
|
@@ -148,12 +115,7 @@ function unnamed(run, kind) {
|
|
|
148
115
|
* this kind.
|
|
149
116
|
*/
|
|
150
117
|
function unusedEnumMembers(run) {
|
|
151
|
-
return unnamed(run, "enum-member").map((
|
|
152
|
-
livenessRelation: candidate.relation,
|
|
153
|
-
testOnly: false,
|
|
154
|
-
configurations: candidate.configurations,
|
|
155
|
-
details: {},
|
|
156
|
-
}));
|
|
118
|
+
return unnamed(run, "enum-member").map((symbol) => findingAbout(symbol, "DS1302", `enum member ${symbol.name} is named nowhere`));
|
|
157
119
|
}
|
|
158
120
|
/**
|
|
159
121
|
* `DS1303`: a type parameter of a function or a class method that neither the
|
|
@@ -163,13 +125,8 @@ function unusedEnumMembers(run) {
|
|
|
163
125
|
*/
|
|
164
126
|
function unusedTypeParameters(run) {
|
|
165
127
|
return unnamed(run, "type-parameter")
|
|
166
|
-
.filter((
|
|
167
|
-
.map((
|
|
168
|
-
livenessRelation: candidate.relation,
|
|
169
|
-
testOnly: false,
|
|
170
|
-
configurations: candidate.configurations,
|
|
171
|
-
details: {},
|
|
172
|
-
}));
|
|
128
|
+
.filter((symbol) => SIGNATURES.has(run.symbols.get(symbol.parent)?.kind ?? ""))
|
|
129
|
+
.map((symbol) => findingAbout(symbol, "DS1303", `type parameter ${symbol.name} is named in neither its signature nor its body`));
|
|
173
130
|
}
|
|
174
131
|
/**
|
|
175
132
|
* The references of the declarations the write-only kind reports, whatever the
|
|
@@ -179,21 +136,11 @@ function unusedTypeParameters(run) {
|
|
|
179
136
|
export function writeOnlyDeclarations(input) {
|
|
180
137
|
return new Set(writeOnlySymbols(indexed(input)).map((finding) => finding.symbol.ref));
|
|
181
138
|
}
|
|
182
|
-
/** Each code's rule, in code order. */
|
|
183
|
-
const RULES = [
|
|
184
|
-
["DS1301", writeOnlySymbols],
|
|
185
|
-
["DS1302", unusedEnumMembers],
|
|
186
|
-
["DS1303", unusedTypeParameters],
|
|
187
|
-
];
|
|
188
139
|
/**
|
|
189
|
-
* The findings of the reads-and-writes family, `DS1300` to `DS1399`,
|
|
190
|
-
*
|
|
191
|
-
* confidence is below the configured minimum is withheld.
|
|
140
|
+
* The findings of the reads-and-writes family, `DS1300` to `DS1399`, in code order and
|
|
141
|
+
* each code's in site order.
|
|
192
142
|
*/
|
|
193
143
|
export const readsAndWrites = (input) => {
|
|
194
144
|
const run = indexed(input);
|
|
195
|
-
|
|
196
|
-
return RULES.flatMap(([code, rule]) => severityIn(input, code) === "allow"
|
|
197
|
-
? []
|
|
198
|
-
: rule(run).filter((finding) => RANK[finding.confidence] >= least));
|
|
145
|
+
return [...writeOnlySymbols(run), ...unusedEnumMembers(run), ...unusedTypeParameters(run)];
|
|
199
146
|
};
|
|
@@ -1,2 +1,127 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { REPOSITORY_DOCUMENT } from "../config.js";
|
|
2
|
+
import { STALE_SUPPRESSION, SUPPRESSION_WITHOUT_REASON, } from "../suppress.js";
|
|
3
|
+
/** The code a configured root that matches nothing is reported under. */
|
|
4
|
+
const UNMATCHED_ROOT = "DS1704";
|
|
5
|
+
/** The subject kind of a finding about a suppression record. */
|
|
6
|
+
const SUPPRESSION_SUBJECT = "suppression";
|
|
7
|
+
/** The subject kind of a finding about a configured root. */
|
|
8
|
+
const ROOT_SUBJECT = "root";
|
|
9
|
+
/** The fixed position of a finding about a document rather than about a line of one. */
|
|
10
|
+
const DOCUMENT_POSITION = 1;
|
|
11
|
+
/** A run with nothing for the self-check family to report. */
|
|
12
|
+
export const NO_SELF_CHECK = {
|
|
13
|
+
refusals: [],
|
|
14
|
+
unmatchedRoots: [],
|
|
15
|
+
rootsDocument: "",
|
|
16
|
+
};
|
|
17
|
+
/** The words a message names a mechanism by. */
|
|
18
|
+
const WRITTEN = {
|
|
19
|
+
inline: "inline directive",
|
|
20
|
+
ignore: "ignore entry",
|
|
21
|
+
baseline: "baseline row",
|
|
22
|
+
};
|
|
23
|
+
/** The file references of a run's declarations, by path. */
|
|
24
|
+
export function fileRefsOf(symbols) {
|
|
25
|
+
const byPath = new Map(symbols
|
|
26
|
+
.filter((symbol) => symbol.kind === "file")
|
|
27
|
+
.map((symbol) => [symbol.position.path, symbol.ref]));
|
|
28
|
+
return (path) => byPath.get(path);
|
|
29
|
+
}
|
|
30
|
+
/** A list of codes as a sentence names them. */
|
|
31
|
+
function spelled(codes) {
|
|
32
|
+
if (codes.length < 2) {
|
|
33
|
+
return codes.join("");
|
|
34
|
+
}
|
|
35
|
+
return `${codes.slice(0, -1).join(", ")} and ${codes.at(-1) ?? ""}`;
|
|
36
|
+
}
|
|
37
|
+
/** A finding about one suppression, at the suppression's own site. */
|
|
38
|
+
function aboutSuppression(code, ref, site, message) {
|
|
39
|
+
return {
|
|
40
|
+
code,
|
|
41
|
+
position: { ...site, endLine: site.line },
|
|
42
|
+
symbol: { ref, kind: SUPPRESSION_SUBJECT, name: ref, sizeLines: 1 },
|
|
43
|
+
message,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The reference a finding about a suppression names: the symbol it names or bound, else
|
|
48
|
+
* the file form of the document that holds it, else that document's path.
|
|
49
|
+
*/
|
|
50
|
+
function suppressionRef(symbol, path, files) {
|
|
51
|
+
return symbol === "" ? (files(path) ?? path) : symbol;
|
|
52
|
+
}
|
|
53
|
+
/** One finding per refused suppression: the grammar's reason refusal and its scope refusal. */
|
|
54
|
+
export function refusedSuppressions(refusals, files) {
|
|
55
|
+
return refusals.map((refused) => aboutSuppression(refused.reported, suppressionRef(refused.symbol, refused.path, files), refused.site, `${WRITTEN[refused.mechanism]} for ${refused.code} ${refused.reported === SUPPRESSION_WITHOUT_REASON
|
|
56
|
+
? "carries no reason"
|
|
57
|
+
: "names a symbol and no path"}`));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* One record per site at which a suppression matched nothing, whatever the number of
|
|
61
|
+
* codes or declarations written there: the first record names the entry, and the message
|
|
62
|
+
* names every code the site's records named and every code the declaration reports
|
|
63
|
+
* instead, so the maintainer corrects the code rather than hunting for it.
|
|
64
|
+
*/
|
|
65
|
+
export function staleSuppressions(stale, files) {
|
|
66
|
+
const bySite = new Map();
|
|
67
|
+
for (const one of stale) {
|
|
68
|
+
const { site, mechanism } = one.record;
|
|
69
|
+
const key = `${mechanism}\u0000${site.path}\u0000${String(site.line)}\u0000${String(site.column)}`;
|
|
70
|
+
const held = bySite.get(key) ?? { first: one.record, records: [] };
|
|
71
|
+
held.records.push(one);
|
|
72
|
+
bySite.set(key, held);
|
|
73
|
+
}
|
|
74
|
+
return [...bySite.values()].map(({ first, records }) => {
|
|
75
|
+
const codes = [...new Set(records.map((one) => one.record.code))].sort();
|
|
76
|
+
const reports = [...new Set(records.flatMap((one) => one.reports))]
|
|
77
|
+
.filter((code) => !codes.includes(code))
|
|
78
|
+
.sort();
|
|
79
|
+
const instead = reports.length === 0 ? "" : `, and its declaration reports ${spelled(reports)}`;
|
|
80
|
+
return {
|
|
81
|
+
code: STALE_SUPPRESSION,
|
|
82
|
+
mechanism: first.mechanism,
|
|
83
|
+
entry: {
|
|
84
|
+
code: first.code,
|
|
85
|
+
...(first.symbol === "" ? {} : { symbol: first.symbol }),
|
|
86
|
+
path: first.path,
|
|
87
|
+
reason: first.reason,
|
|
88
|
+
},
|
|
89
|
+
position: first.site,
|
|
90
|
+
symbol: suppressionRef(first.symbol, first.path, files),
|
|
91
|
+
message: `${WRITTEN[first.mechanism]} for ${spelled(codes)} matches no current finding${instead}`,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* One finding per configured root or pattern that named nothing in any project. A finding
|
|
97
|
+
* names the document that declared the roots, or the conventional repository configuration
|
|
98
|
+
* where no document of the target did, and every finding of one run sits at that
|
|
99
|
+
* document's first position: the roots are an array whose members carry no line of their
|
|
100
|
+
* own, so the string each finding names tells one from another.
|
|
101
|
+
*/
|
|
102
|
+
export function unmatchedRootFindings(sources, document) {
|
|
103
|
+
const position = {
|
|
104
|
+
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
105
|
+
line: DOCUMENT_POSITION,
|
|
106
|
+
column: DOCUMENT_POSITION,
|
|
107
|
+
endLine: DOCUMENT_POSITION,
|
|
108
|
+
};
|
|
109
|
+
return sources.map((source) => ({
|
|
110
|
+
code: UNMATCHED_ROOT,
|
|
111
|
+
position,
|
|
112
|
+
symbol: { ref: source, kind: ROOT_SUBJECT, name: source, sizeLines: 1 },
|
|
113
|
+
message: source.includes("*") || source.includes("?")
|
|
114
|
+
? "configured root pattern matches no symbol of the inventory"
|
|
115
|
+
: "configured root matches no symbol of the inventory",
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
|
|
120
|
+
* carrying no reason or naming no path, and a configured root that named nothing. A
|
|
121
|
+
* suppression that matched nothing is a record of the report's stale suppressions, which
|
|
122
|
+
* {@link staleSuppressions} builds, and a stale cross-language edge is the merge's to report.
|
|
123
|
+
*/
|
|
124
|
+
export const selfCheck = ({ selfCheck: facts, swept }) => [
|
|
125
|
+
...refusedSuppressions(facts.refusals, fileRefsOf(swept.matrix.union.symbols)),
|
|
126
|
+
...unmatchedRootFindings(facts.unmatchedRoots, facts.rootsDocument),
|
|
127
|
+
];
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
import { capped, findingComponents, reachabilityClasses, reportable, severityOf, } from "./completion.js";
|
|
2
1
|
import { unreachableExports } from "./visibility-narrowing.js";
|
|
3
|
-
/** The codes of the unused-declarations family, each by its row of the vocabulary. */
|
|
4
|
-
export const UNUSED_DECLARATION_KINDS = new Map([
|
|
5
|
-
["DS1001", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
6
|
-
["DS1002", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
7
|
-
["DS1003", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
8
|
-
["DS1004", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
9
|
-
["DS1005", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
10
|
-
["DS1006", { defaultSeverity: "deny", maxClass: "certain" }],
|
|
11
|
-
]);
|
|
12
2
|
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
13
3
|
const MEMBERS = new Set([
|
|
14
4
|
"method",
|
|
@@ -120,19 +110,13 @@ function messageOf(code, judged) {
|
|
|
120
110
|
}
|
|
121
111
|
return `${subject} has no reference in the target`;
|
|
122
112
|
}
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
export function unusedDeclarationFindings(input) {
|
|
128
|
-
const { config, swept } = input;
|
|
129
|
-
const union = swept.matrix.union;
|
|
130
|
-
const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
|
|
131
|
-
const classOf = reachabilityClasses(swept);
|
|
132
|
-
const componentOf = findingComponents(swept);
|
|
113
|
+
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
114
|
+
export const unusedDeclarations = (input) => {
|
|
115
|
+
const union = input.swept.matrix.union;
|
|
116
|
+
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
133
117
|
const unreachable = unreachableExports(input);
|
|
134
118
|
const found = [];
|
|
135
|
-
for (const candidate of swept.sweep.candidates) {
|
|
119
|
+
for (const candidate of input.swept.sweep.candidates) {
|
|
136
120
|
const at = union.at(candidate.id);
|
|
137
121
|
const symbol = union.symbols[at];
|
|
138
122
|
if (symbol === undefined) {
|
|
@@ -147,16 +131,10 @@ export function unusedDeclarationFindings(input) {
|
|
|
147
131
|
unreachable: unreachable.has(symbol.id),
|
|
148
132
|
};
|
|
149
133
|
const code = codeOf(judged);
|
|
150
|
-
|
|
151
|
-
if (code === undefined || row === undefined) {
|
|
134
|
+
if (code === undefined) {
|
|
152
135
|
continue;
|
|
153
136
|
}
|
|
154
|
-
const component = componentOf(symbol.id);
|
|
155
|
-
if (component === undefined) {
|
|
156
|
-
throw new Error(`${symbol.id} is reported dead and falls in no dead component`);
|
|
157
|
-
}
|
|
158
137
|
const endLine = Math.max(symbol.position.line, symbol.endLine);
|
|
159
|
-
const reachabilityClass = classOf(symbol.id);
|
|
160
138
|
found.push({
|
|
161
139
|
code,
|
|
162
140
|
position: { ...symbol.position, endLine },
|
|
@@ -167,17 +145,7 @@ export function unusedDeclarationFindings(input) {
|
|
|
167
145
|
sizeLines: endLine - symbol.position.line + 1,
|
|
168
146
|
},
|
|
169
147
|
message: messageOf(code, judged),
|
|
170
|
-
reachabilityClass,
|
|
171
|
-
confidence: capped(reachabilityClass, row.maxClass),
|
|
172
|
-
livenessRelation: candidate.relation,
|
|
173
|
-
testOnly: candidate.productionRefs === 0 && candidate.testRefs > 0,
|
|
174
|
-
component,
|
|
175
|
-
configurations: candidate.configurations,
|
|
176
|
-
severity: severityOf(config, code, row.defaultSeverity),
|
|
177
|
-
details: {},
|
|
178
148
|
});
|
|
179
149
|
}
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
183
|
-
export const unusedDeclarations = unusedDeclarationFindings;
|
|
150
|
+
return found;
|
|
151
|
+
};
|
|
@@ -164,49 +164,29 @@ function unreachable(facts, at, symbol, candidate, input) {
|
|
|
164
164
|
: "has no reference in the target";
|
|
165
165
|
return findingOf(symbol, UNREACHABLE_EXPORT, `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`);
|
|
166
166
|
}
|
|
167
|
-
/** Two strings ordered bytewise. */
|
|
168
|
-
function compare(a, b) {
|
|
169
|
-
if (a === b) {
|
|
170
|
-
return 0;
|
|
171
|
-
}
|
|
172
|
-
return a < b ? -1 : 1;
|
|
173
|
-
}
|
|
174
167
|
/**
|
|
175
|
-
* The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* file and the package, and whether it is a use is decided on the side in the other
|
|
180
|
-
* language.
|
|
168
|
+
* The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`. A finding
|
|
169
|
+
* about a symbol a declared cross-language edge names is reported here like any other, and
|
|
170
|
+
* the edge's evaluation then holds it pending, because the edge stands for a reference from
|
|
171
|
+
* outside the file and the package whose use is decided in the other language.
|
|
181
172
|
*/
|
|
182
173
|
export function narrowings(input) {
|
|
183
|
-
const { boundary } = input;
|
|
184
174
|
const facts = factsOf(input.swept.matrix.union);
|
|
185
|
-
const { declared, loaded } = boundary.consumers;
|
|
175
|
+
const { declared, loaded } = input.boundary.consumers;
|
|
186
176
|
const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
|
|
187
177
|
const world = { closed, writeOnly: writeOnlyDeclarations(input) };
|
|
188
178
|
const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
189
179
|
const findings = [];
|
|
190
|
-
const pending = [];
|
|
191
180
|
facts.graph.symbols.forEach((symbol, at) => {
|
|
192
181
|
const candidate = candidates.get(symbol.id);
|
|
193
182
|
const found = candidate === undefined
|
|
194
183
|
? narrowing(facts, at, symbol, world)
|
|
195
184
|
: unreachable(facts, at, symbol, candidate, input);
|
|
196
|
-
if (found
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const named = boundary.edges.filter((side) => side.symbol === symbol.ref);
|
|
200
|
-
if (named.length === 0) {
|
|
185
|
+
if (found !== undefined) {
|
|
201
186
|
findings.push(found);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
for (const side of named) {
|
|
205
|
-
pending.push({ ...side, finding: found });
|
|
206
187
|
}
|
|
207
188
|
});
|
|
208
|
-
|
|
209
|
-
return { findings, pending };
|
|
189
|
+
return findings;
|
|
210
190
|
}
|
|
211
191
|
/**
|
|
212
192
|
* The declarations the unreachable-export kind reports, whether or not a declared edge
|
|
@@ -224,16 +204,5 @@ export function unreachableExports(input) {
|
|
|
224
204
|
}
|
|
225
205
|
return found;
|
|
226
206
|
}
|
|
227
|
-
/**
|
|
228
|
-
|
|
229
|
-
* declared edge holds pending belongs in that edge's evaluation, which this analyzer
|
|
230
|
-
* does not publish yet, so a run that holds one is refused rather than reported
|
|
231
|
-
* without it.
|
|
232
|
-
*/
|
|
233
|
-
export const visibilityNarrowing = (input) => {
|
|
234
|
-
const { findings, pending } = narrowings(input);
|
|
235
|
-
if (pending.length > 0) {
|
|
236
|
-
throw new Error(`${String(pending.length)} finding(s) a declared edge holds pending, and no edge evaluation is published`);
|
|
237
|
-
}
|
|
238
|
-
return findings;
|
|
239
|
-
};
|
|
207
|
+
/** The findings of the visibility-narrowing family, `DS1100` to `DS1199`. */
|
|
208
|
+
export const visibilityNarrowing = narrowings;
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
import { decidedFindings } from "./findings/emitters.js";
|
|
7
|
+
import { fileRefsOf, staleSuppressions, } from "./findings/self-check.js";
|
|
8
|
+
import { isRowSubject, pairs, suppressionCounts } from "./suppress.js";
|
|
9
|
+
import { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.js";
|
|
10
|
+
/** The exit code of a report holding at least one pending finding. */
|
|
11
|
+
export const EXIT_PENDING = 4;
|
|
12
|
+
/** The severities ranked, the strongest highest. */
|
|
13
|
+
const SEVERITY_RANK = { allow: 0, warn: 1, deny: 2 };
|
|
14
|
+
/** Two strings ordered bytewise. */
|
|
15
|
+
function compare(a, b) {
|
|
16
|
+
if (a === b) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
return a < b ? -1 : 1;
|
|
20
|
+
}
|
|
21
|
+
/** The canonical order: path, line, column, code, then the symbol reference. */
|
|
22
|
+
function canonical(a, b) {
|
|
23
|
+
return (compare(a.position.path, b.position.path) ||
|
|
24
|
+
a.position.line - b.position.line ||
|
|
25
|
+
a.position.column - b.position.column ||
|
|
26
|
+
compare(a.code, b.code) ||
|
|
27
|
+
compare(a.symbol.ref, b.symbol.ref));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The pass over one run. The emitter table decides every record against every finding the
|
|
31
|
+
* run would produce with no record there, withholds what the records claim, and evaluates
|
|
32
|
+
* the declared edges; a stale record then names the codes its declaration reports instead.
|
|
33
|
+
*/
|
|
34
|
+
export function findingsPass(inputs) {
|
|
35
|
+
const { marked, unmarked, suppressions } = inputs;
|
|
36
|
+
const decided = decidedFindings(marked, { unmarked, records: suppressions.records });
|
|
37
|
+
const { ledger, would } = decided;
|
|
38
|
+
const stale = [];
|
|
39
|
+
suppressions.records.forEach((record, at) => {
|
|
40
|
+
if (ledger.verdicts[at] !== "stale") {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const reports = would
|
|
44
|
+
.filter((finding) => finding.code !== record.code && pairs({ ...record, code: finding.code }, finding))
|
|
45
|
+
.map((finding) => finding.code);
|
|
46
|
+
stale.push({ record, reports });
|
|
47
|
+
});
|
|
48
|
+
const records = staleSuppressions(stale, fileRefsOf(marked.swept.matrix.union.symbols));
|
|
49
|
+
const counts = suppressionCounts(suppressions.records, ledger);
|
|
50
|
+
return {
|
|
51
|
+
findings: [...decided.findings].sort(canonical),
|
|
52
|
+
staleSuppressions: records,
|
|
53
|
+
edgeEvaluations: decided.evaluations,
|
|
54
|
+
totals: {
|
|
55
|
+
suppressionsInEffect: counts.inEffect,
|
|
56
|
+
reasonsRecorded: counts.reasonsRecorded,
|
|
57
|
+
staleSuppressions: records.length,
|
|
58
|
+
pending: decided.evaluations.filter((one) => one.finding !== undefined).length,
|
|
59
|
+
},
|
|
60
|
+
ledger,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The exit code of a report the pass answered: pending when an evaluation holds a finding,
|
|
65
|
+
* which outranks the rest because an unmerged report is not an answer; findings when a
|
|
66
|
+
* suppression is stale or a finding's severity reaches the configured failing severity;
|
|
67
|
+
* clean otherwise.
|
|
68
|
+
*/
|
|
69
|
+
export function verdictOf(result, config) {
|
|
70
|
+
if (result.totals.pending > 0) {
|
|
71
|
+
return EXIT_PENDING;
|
|
72
|
+
}
|
|
73
|
+
if (result.totals.staleSuppressions > 0) {
|
|
74
|
+
return EXIT_FINDINGS;
|
|
75
|
+
}
|
|
76
|
+
const failing = SEVERITY_RANK[config.reporters.failOn];
|
|
77
|
+
const fails = result.findings.some((finding) => SEVERITY_RANK[finding.severity] >= failing);
|
|
78
|
+
return fails ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The findings of a report as a baseline records them, in report order. A finding about a
|
|
82
|
+
* row of a document is not recorded: no record withholds one, so a row recording it would
|
|
83
|
+
* be stale on the first read back.
|
|
84
|
+
*/
|
|
85
|
+
export function recordedFindings(result) {
|
|
86
|
+
return result.findings
|
|
87
|
+
.filter((finding) => !isRowSubject(finding.symbol.kind))
|
|
88
|
+
.map((finding) => ({
|
|
89
|
+
code: finding.code,
|
|
90
|
+
symbol: finding.symbol.ref,
|
|
91
|
+
path: finding.position.path,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
/** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
|
|
95
|
+
export function analyzerProvenance(version) {
|
|
96
|
+
return { analyzer: "deadset-ts", version };
|
|
97
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A read of a strict JSON document beside its parse: what a parse into a value cannot
|
|
3
|
+
* report, the offset of each record of one array and a member written twice.
|
|
4
|
+
*/
|
|
5
|
+
/** The token walk over a document `JSON.parse` accepted, so every token is well formed. */
|
|
6
|
+
class Walk {
|
|
7
|
+
opened = [];
|
|
8
|
+
/** The member written twice, where one is, as the offset of its name. */
|
|
9
|
+
twice;
|
|
10
|
+
text;
|
|
11
|
+
array;
|
|
12
|
+
at = 0;
|
|
13
|
+
constructor(text, array) {
|
|
14
|
+
this.text = text;
|
|
15
|
+
this.array = array;
|
|
16
|
+
}
|
|
17
|
+
run() {
|
|
18
|
+
this.value("document");
|
|
19
|
+
}
|
|
20
|
+
skip() {
|
|
21
|
+
while (/[ \t\r\n]/u.test(this.text[this.at] ?? "")) {
|
|
22
|
+
this.at += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
string() {
|
|
26
|
+
const start = this.at;
|
|
27
|
+
this.at += 1;
|
|
28
|
+
while (this.text[this.at] !== '"') {
|
|
29
|
+
this.at += this.text[this.at] === "\\" ? 2 : 1;
|
|
30
|
+
}
|
|
31
|
+
this.at += 1;
|
|
32
|
+
return JSON.parse(this.text.slice(start, this.at));
|
|
33
|
+
}
|
|
34
|
+
value(role) {
|
|
35
|
+
this.skip();
|
|
36
|
+
const char = this.text[this.at];
|
|
37
|
+
if (char === "{") {
|
|
38
|
+
if (role === "record") {
|
|
39
|
+
this.opened.push(this.at);
|
|
40
|
+
}
|
|
41
|
+
this.object(role);
|
|
42
|
+
}
|
|
43
|
+
else if (char === "[") {
|
|
44
|
+
this.list(role === "array" ? "record" : "other");
|
|
45
|
+
}
|
|
46
|
+
else if (char === '"') {
|
|
47
|
+
this.string();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
while (this.at < this.text.length && !/[,\]} \t\r\n]/u.test(this.text[this.at] ?? "")) {
|
|
51
|
+
this.at += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
object(role) {
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
this.at += 1;
|
|
58
|
+
for (;;) {
|
|
59
|
+
this.skip();
|
|
60
|
+
if (this.text[this.at] === "}") {
|
|
61
|
+
this.at += 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.text[this.at] === ",") {
|
|
65
|
+
this.at += 1;
|
|
66
|
+
this.skip();
|
|
67
|
+
}
|
|
68
|
+
const offset = this.at;
|
|
69
|
+
const name = this.string();
|
|
70
|
+
if (seen.has(name) && this.twice === undefined) {
|
|
71
|
+
this.twice = { name, offset };
|
|
72
|
+
}
|
|
73
|
+
seen.add(name);
|
|
74
|
+
this.skip();
|
|
75
|
+
this.at += 1;
|
|
76
|
+
this.value(role === "document" && name === this.array ? "array" : "other");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
list(element) {
|
|
80
|
+
this.at += 1;
|
|
81
|
+
for (;;) {
|
|
82
|
+
this.skip();
|
|
83
|
+
if (this.text[this.at] === "]") {
|
|
84
|
+
this.at += 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (this.text[this.at] === ",") {
|
|
88
|
+
this.at += 1;
|
|
89
|
+
}
|
|
90
|
+
this.value(element);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Walks a document `JSON.parse` accepted, whose top-level member `array` holds the
|
|
96
|
+
* records. A member one object writes twice reads as though it were written once, its
|
|
97
|
+
* later value silently replacing the earlier one, so a document holding one is one a
|
|
98
|
+
* reader names rather than reads.
|
|
99
|
+
*/
|
|
100
|
+
export function walkDocument(text, array) {
|
|
101
|
+
const walk = new Walk(text, array);
|
|
102
|
+
walk.run();
|
|
103
|
+
return { opened: walk.opened, twice: walk.twice };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One offset of a document as a finding carries it: the line counted from one, and the
|
|
107
|
+
* column counted from one in UTF-16 code units.
|
|
108
|
+
*/
|
|
109
|
+
export function positionAt(text, file, offset) {
|
|
110
|
+
const before = text.slice(0, offset);
|
|
111
|
+
const lineStart = before.lastIndexOf("\n") + 1;
|
|
112
|
+
return { path: file, line: before.split("\n").length, column: offset - lineStart + 1 };
|
|
113
|
+
}
|