@cplieger/deadset-ts 3.7.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/findings/completion.js +30 -9
- package/dist/src/findings/emitters.js +42 -6
- package/dist/src/findings/self-check.js +127 -2
- 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/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/findings/boundary.ts +0 -11
- package/src/findings/completion.ts +46 -12
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +81 -7
- package/src/findings/self-check.ts +205 -2
- package/src/findings/visibility-narrowing.ts +10 -52
- package/src/findings-pass.ts +145 -0
- package/src/json-document.ts +130 -0
- 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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inline directive: `deadset:ignore`, the first token of a `//` line comment, naming
|
|
3
|
+
* one or more codes and a reason, and binding to every declaration that begins on the line
|
|
4
|
+
* below it.
|
|
5
|
+
*/
|
|
6
|
+
import { SyntaxKind } from "@typescript/native/unstable/ast";
|
|
7
|
+
import { nodeKey } from "./inventory.js";
|
|
8
|
+
import { byPosition, positionKey, renderPosition } from "./position.js";
|
|
9
|
+
import { SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
|
|
10
|
+
/**
|
|
11
|
+
* The three expressions the grammar applies to the text of each line comment, in this
|
|
12
|
+
* order: a comment in the namespace, a well-formed directive, and a directive that lacks
|
|
13
|
+
* only its reason. A test pins each to the line the grammar page carries.
|
|
14
|
+
*/
|
|
15
|
+
export const DIRECTIVE_EXPRESSIONS = {
|
|
16
|
+
candidate: String.raw `^//[ \t]*deadset:`,
|
|
17
|
+
wellFormed: String.raw `^//[ \t]*deadset:ignore[ \t]+(?<codes>DS[0-9]{4}(?:,DS[0-9]{4})*)[ \t]+--[ \t]+(?<reason>[^ \t\r\n][^\r\n]*?)[ \t]*$`,
|
|
18
|
+
noReason: String.raw `^//[ \t]*deadset:ignore[ \t]+(?<codes>DS[0-9]{4}(?:,DS[0-9]{4})*)(?:[ \t]+--)?[ \t]*$`,
|
|
19
|
+
};
|
|
20
|
+
const CANDIDATE = new RegExp(DIRECTIVE_EXPRESSIONS.candidate);
|
|
21
|
+
const WELL_FORMED = new RegExp(DIRECTIVE_EXPRESSIONS.wellFormed);
|
|
22
|
+
const NO_REASON = new RegExp(DIRECTIVE_EXPRESSIONS.noReason);
|
|
23
|
+
/** The form a malformed directive is told to take. */
|
|
24
|
+
const DIRECTIVE_FORM = "// deadset:ignore DS0000[,DS0000...] -- <reason>";
|
|
25
|
+
/** Applies the grammar's three expressions to the text of one comment, in order. */
|
|
26
|
+
export function classifyComment(text) {
|
|
27
|
+
if (!CANDIDATE.test(text)) {
|
|
28
|
+
return { is: "none" };
|
|
29
|
+
}
|
|
30
|
+
const groups = WELL_FORMED.exec(text)?.groups;
|
|
31
|
+
if (groups !== undefined) {
|
|
32
|
+
return {
|
|
33
|
+
is: "directive",
|
|
34
|
+
codes: (groups["codes"] ?? "").split(","),
|
|
35
|
+
reason: groups["reason"] ?? "",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const bare = NO_REASON.exec(text)?.groups;
|
|
39
|
+
if (bare !== undefined) {
|
|
40
|
+
return { is: "no-reason", codes: (bare["codes"] ?? "").split(",") };
|
|
41
|
+
}
|
|
42
|
+
return { is: "malformed" };
|
|
43
|
+
}
|
|
44
|
+
/** The literal tokens whose text may hold `//` or `/*` without starting a comment. */
|
|
45
|
+
const LITERALS = new Set([
|
|
46
|
+
SyntaxKind.StringLiteral,
|
|
47
|
+
SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
48
|
+
SyntaxKind.TemplateHead,
|
|
49
|
+
SyntaxKind.TemplateMiddle,
|
|
50
|
+
SyntaxKind.TemplateTail,
|
|
51
|
+
SyntaxKind.RegularExpressionLiteral,
|
|
52
|
+
SyntaxKind.JsxText,
|
|
53
|
+
]);
|
|
54
|
+
/** Whether one character ends a line, under the language's line terminators. */
|
|
55
|
+
function endsLine(char) {
|
|
56
|
+
return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029";
|
|
57
|
+
}
|
|
58
|
+
/** The offset where the line holding `from` ends. */
|
|
59
|
+
function lineEnd(text, from) {
|
|
60
|
+
let at = from;
|
|
61
|
+
while (at < text.length && !endsLine(text[at])) {
|
|
62
|
+
at += 1;
|
|
63
|
+
}
|
|
64
|
+
return at;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The offset of the first token at or after `from`: whitespace, comments and a leading
|
|
68
|
+
* hashbang are trivia.
|
|
69
|
+
*/
|
|
70
|
+
export function tokenStart(text, from) {
|
|
71
|
+
let at = from;
|
|
72
|
+
if (at === 0 && text.startsWith("#!")) {
|
|
73
|
+
at = lineEnd(text, 0);
|
|
74
|
+
}
|
|
75
|
+
for (;;) {
|
|
76
|
+
if (/\s/u.test(text[at] ?? "")) {
|
|
77
|
+
at += 1;
|
|
78
|
+
}
|
|
79
|
+
else if (text.startsWith("//", at)) {
|
|
80
|
+
at = lineEnd(text, at);
|
|
81
|
+
}
|
|
82
|
+
else if (text.startsWith("/*", at)) {
|
|
83
|
+
const close = text.indexOf("*/", at + 2);
|
|
84
|
+
at = close < 0 ? text.length : close + 2;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
return at;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Every `//` line comment of one file, in file order. The text between tokens is read as
|
|
93
|
+
* the language reads it: a block comment holds no line comment, and the literal tokens the
|
|
94
|
+
* syntax tree holds are skipped, so a `//` inside a string, a template or a regular
|
|
95
|
+
* expression starts nothing.
|
|
96
|
+
*/
|
|
97
|
+
export function lineComments(file) {
|
|
98
|
+
const text = file.text;
|
|
99
|
+
const spans = [];
|
|
100
|
+
const visit = (node) => {
|
|
101
|
+
if (LITERALS.has(node.kind)) {
|
|
102
|
+
spans.push({ start: tokenStart(text, node.pos), end: node.end });
|
|
103
|
+
}
|
|
104
|
+
node.forEachChild(visit);
|
|
105
|
+
};
|
|
106
|
+
file.forEachChild(visit);
|
|
107
|
+
spans.sort((a, b) => a.start - b.start);
|
|
108
|
+
const found = [];
|
|
109
|
+
let next = 0;
|
|
110
|
+
let at = 0;
|
|
111
|
+
while (at < text.length) {
|
|
112
|
+
while (next < spans.length && (spans[next]?.start ?? 0) < at) {
|
|
113
|
+
next += 1;
|
|
114
|
+
}
|
|
115
|
+
const span = spans[next];
|
|
116
|
+
if (span?.start === at) {
|
|
117
|
+
at = Math.max(span.end, at + 1);
|
|
118
|
+
}
|
|
119
|
+
else if (text.startsWith("//", at)) {
|
|
120
|
+
const end = lineEnd(text, at);
|
|
121
|
+
found.push({ offset: at, text: text.slice(at, end) });
|
|
122
|
+
at = end;
|
|
123
|
+
}
|
|
124
|
+
else if (text.startsWith("/*", at)) {
|
|
125
|
+
const close = text.indexOf("*/", at + 2);
|
|
126
|
+
at = close < 0 ? text.length : close + 2;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
at += 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The declarations of one file by the line each begins on, the line of its first token: a
|
|
136
|
+
* decorator, a modifier or the keyword. A variable's first token is its statement's
|
|
137
|
+
* keyword, and a variable written on a later line of the statement also begins there.
|
|
138
|
+
*/
|
|
139
|
+
function declarationLines(file, held, symbols) {
|
|
140
|
+
const below = new Map();
|
|
141
|
+
const add = (offset, one) => {
|
|
142
|
+
const line = file.getLineAndCharacterOfPosition(tokenStart(file.text, offset)).line + 1;
|
|
143
|
+
const at = below.get(line) ?? [];
|
|
144
|
+
if (!at.some((known) => known.id === one.id)) {
|
|
145
|
+
at.push(one);
|
|
146
|
+
}
|
|
147
|
+
below.set(line, at);
|
|
148
|
+
};
|
|
149
|
+
const visit = (node) => {
|
|
150
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
151
|
+
const one = id === undefined ? undefined : symbols.get(id);
|
|
152
|
+
if (one !== undefined) {
|
|
153
|
+
add(node.pos, one);
|
|
154
|
+
const list = node.parent;
|
|
155
|
+
if (node.kind === SyntaxKind.VariableDeclaration &&
|
|
156
|
+
list.kind === SyntaxKind.VariableDeclarationList &&
|
|
157
|
+
list.parent.kind === SyntaxKind.VariableStatement) {
|
|
158
|
+
add(list.parent.pos, one);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
node.forEachChild(visit);
|
|
162
|
+
};
|
|
163
|
+
file.forEachChild(visit);
|
|
164
|
+
return below;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Every comment in the namespace one project's own files hold, each with the declarations
|
|
168
|
+
* it binds, read while the project's view is open.
|
|
169
|
+
*/
|
|
170
|
+
export function inlineDirectives(project, held, targetRoot) {
|
|
171
|
+
const symbols = new Map(held.symbols
|
|
172
|
+
.filter((symbol) => symbol.kind !== "file")
|
|
173
|
+
.map((symbol) => [symbol.id, { id: symbol.id, ref: symbol.ref, position: symbol.position }]));
|
|
174
|
+
const found = [];
|
|
175
|
+
for (const file of project.ownSourceFiles()) {
|
|
176
|
+
const comments = lineComments(file).filter((comment) => CANDIDATE.test(comment.text));
|
|
177
|
+
if (comments.length === 0) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const below = declarationLines(file, held, symbols);
|
|
181
|
+
for (const comment of comments) {
|
|
182
|
+
const site = renderPosition(file, targetRoot, comment.offset);
|
|
183
|
+
found.push({
|
|
184
|
+
site,
|
|
185
|
+
text: comment.text,
|
|
186
|
+
directive: classifyComment(comment.text),
|
|
187
|
+
bound: [...(below.get(site.line + 1) ?? [])],
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return found;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The inline directives of a run's projects, each bound to the declarations that begin on
|
|
195
|
+
* the line below it, one directive binding the union of what each project holding its file
|
|
196
|
+
* declares there. A directive that lacks only its reason is one {@link Refusal} per code;
|
|
197
|
+
* any other malformed comment in the namespace ends the read with a
|
|
198
|
+
* {@link SuppressionError} naming the first one written.
|
|
199
|
+
*/
|
|
200
|
+
export function inlineSuppressions(perProject) {
|
|
201
|
+
const merged = new Map();
|
|
202
|
+
for (const written of perProject.flat()) {
|
|
203
|
+
const key = positionKey(written.site);
|
|
204
|
+
const known = merged.get(key);
|
|
205
|
+
if (known === undefined) {
|
|
206
|
+
merged.set(key, { ...written, bound: [...written.bound] });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
for (const one of written.bound) {
|
|
210
|
+
if (!known.bound.some((other) => other.id === one.id)) {
|
|
211
|
+
known.bound.push(one);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const ordered = [...merged.values()].sort((a, b) => byPosition(a.site, b.site));
|
|
216
|
+
const records = [];
|
|
217
|
+
const refusals = [];
|
|
218
|
+
for (const { site, text, directive, bound } of ordered) {
|
|
219
|
+
switch (directive.is) {
|
|
220
|
+
case "none":
|
|
221
|
+
break;
|
|
222
|
+
case "malformed":
|
|
223
|
+
throw new SuppressionError("inline", siteText(site), JSON.stringify(text), DIRECTIVE_FORM);
|
|
224
|
+
case "no-reason":
|
|
225
|
+
for (const code of directive.codes) {
|
|
226
|
+
refusals.push({
|
|
227
|
+
reported: SUPPRESSION_WITHOUT_REASON,
|
|
228
|
+
code,
|
|
229
|
+
symbol: "",
|
|
230
|
+
path: site.path,
|
|
231
|
+
reason: "",
|
|
232
|
+
site,
|
|
233
|
+
mechanism: "inline",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case "directive": {
|
|
238
|
+
const declared = [...bound].sort((a, b) => byPosition(a.position, b.position));
|
|
239
|
+
for (const code of directive.codes) {
|
|
240
|
+
const base = {
|
|
241
|
+
code,
|
|
242
|
+
path: site.path,
|
|
243
|
+
reason: directive.reason,
|
|
244
|
+
site,
|
|
245
|
+
mechanism: "inline",
|
|
246
|
+
};
|
|
247
|
+
if (declared.length === 0) {
|
|
248
|
+
records.push({ ...base, symbol: "", bound: "" });
|
|
249
|
+
}
|
|
250
|
+
for (const one of declared) {
|
|
251
|
+
records.push({ ...base, symbol: one.ref, bound: one.id });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { records, refusals };
|
|
259
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The suppression grammar's records, and the ledger that decides what each one did. A
|
|
3
|
+
* record is one code bound to one site with its reason, from an inline directive, an
|
|
4
|
+
* ignore entry or a baseline row. A bound record marks its declaration live before the
|
|
5
|
+
* sweep, and is in effect when it withholds the finding its code would have produced
|
|
6
|
+
* there, dormant when the configuration withholds that finding anyway, and stale otherwise.
|
|
7
|
+
*/
|
|
8
|
+
import { ConfigError } from "./config.js";
|
|
9
|
+
import { positionKey } from "./position.js";
|
|
10
|
+
import { isRef } from "./ref.js";
|
|
11
|
+
/** The code a suppression that carries no reason is reported under. */
|
|
12
|
+
export const SUPPRESSION_WITHOUT_REASON = "DS1701";
|
|
13
|
+
/** The code an ignore entry or a baseline row that names no path is reported under. */
|
|
14
|
+
export const UNSCOPED_ENTRY = "DS1702";
|
|
15
|
+
/** The code a suppression that withholds nothing is reported under. */
|
|
16
|
+
export const STALE_SUPPRESSION = "DS1703";
|
|
17
|
+
/** A target that carries no suppression. */
|
|
18
|
+
export const NO_SUPPRESSIONS = { records: [], refusals: [] };
|
|
19
|
+
/**
|
|
20
|
+
* A directive, an entry or a document the grammar refuses before any finding exists: an
|
|
21
|
+
* instruction the analysis cannot carry out. It is a malformed input, so the command line
|
|
22
|
+
* answers it with the usage code.
|
|
23
|
+
*/
|
|
24
|
+
export class SuppressionError extends ConfigError {
|
|
25
|
+
constructor(mechanism, at, text, want) {
|
|
26
|
+
super("malformed", at, `${mechanism} ${at}: ${text}: want ${want}`);
|
|
27
|
+
this.name = "SuppressionError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Where a position is written, as an error names it. */
|
|
31
|
+
export function siteText(site) {
|
|
32
|
+
return site.line > 0 ? positionKey(site) : site.path;
|
|
33
|
+
}
|
|
34
|
+
/** An issue-kind code: `DS` and four digits. */
|
|
35
|
+
export const CODE_FORM = /^DS[0-9]{4}$/u;
|
|
36
|
+
/**
|
|
37
|
+
* A path inside the target: relative to the target root, the solidus as separator, no
|
|
38
|
+
* leading `./`, no trailing solidus, no `.` or `..` segment, never absolute.
|
|
39
|
+
*/
|
|
40
|
+
export const PATH_EXPRESSION = String.raw `^(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+)(?:/(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+))*$`;
|
|
41
|
+
const PATH_FORM = new RegExp(PATH_EXPRESSION, "u");
|
|
42
|
+
/** Whether one value is a path in the form an entry's `path` takes. */
|
|
43
|
+
export function isEntryPath(value) {
|
|
44
|
+
return PATH_FORM.test(value);
|
|
45
|
+
}
|
|
46
|
+
const GO_IDENT = String.raw `(?:[A-Za-z_]|[^\x00-\x7F])(?:[A-Za-z0-9_]|[^\x00-\x7F])*`;
|
|
47
|
+
const GO_PATH = String.raw `[A-Za-z0-9._~-]+(?:/[A-Za-z0-9._~-]+)*`;
|
|
48
|
+
const GO_VERSION = String.raw `[A-Za-z0-9.+-]+`;
|
|
49
|
+
const GO_FILE = String.raw `[^/\\:#\r\n]+\.go`;
|
|
50
|
+
/**
|
|
51
|
+
* The reference forms of the Go analyzer's grammar. One target holds one ignore file,
|
|
52
|
+
* whose entries may name a symbol of either language, so an entry naming a Go symbol is
|
|
53
|
+
* well formed here and binds nothing.
|
|
54
|
+
*/
|
|
55
|
+
export const GO_REF_EXPRESSIONS = [
|
|
56
|
+
`^go://${GO_PATH}#$`,
|
|
57
|
+
`^go://${GO_PATH}#${GO_IDENT}$`,
|
|
58
|
+
`^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})+$`,
|
|
59
|
+
`^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})?\\[${GO_IDENT}\\]$`,
|
|
60
|
+
`^go://${GO_PATH}#${GO_FILE}:file$`,
|
|
61
|
+
`^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:require$`,
|
|
62
|
+
`^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:replace$`,
|
|
63
|
+
];
|
|
64
|
+
const GO_REF_FORMS = GO_REF_EXPRESSIONS.map((expression) => new RegExp(expression, "u"));
|
|
65
|
+
/** Whether one value is a stable symbol reference of either language's grammar. */
|
|
66
|
+
export function isSymbolRef(value) {
|
|
67
|
+
return isRef(value) || GO_REF_FORMS.some((form) => form.test(value));
|
|
68
|
+
}
|
|
69
|
+
/** Whether a reason is absent, empty or whitespace only, which the grammar reads as none. */
|
|
70
|
+
export function blankReason(reason) {
|
|
71
|
+
return reason === undefined || /^[ \t\r\n]*$/u.test(reason);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The subject kinds that are a row of a document rather than a declaration of the program.
|
|
75
|
+
* A record binds to a declaration, so nothing withholds a finding about one.
|
|
76
|
+
*/
|
|
77
|
+
const ROW_SUBJECTS = new Set([
|
|
78
|
+
"file",
|
|
79
|
+
"dependency",
|
|
80
|
+
"module-directive",
|
|
81
|
+
"root",
|
|
82
|
+
"suppression",
|
|
83
|
+
"edge",
|
|
84
|
+
]);
|
|
85
|
+
/** Whether a finding's subject is a row of a document, which no record can withhold. */
|
|
86
|
+
export function isRowSubject(kind) {
|
|
87
|
+
return ROW_SUBJECTS.has(kind);
|
|
88
|
+
}
|
|
89
|
+
/** One finding's identity: its code and the position of the thing it names. */
|
|
90
|
+
function claimKey(finding) {
|
|
91
|
+
return `${finding.code}\u0000${positionKey(finding.position)}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether one record names one finding. A record reaches a finding in two ways: it bound
|
|
95
|
+
* the declaration at the finding's position, or it names the finding's reference and the
|
|
96
|
+
* file the finding is reported in, each exactly. The second is how an entry copied from a
|
|
97
|
+
* finding about a part of a declaration reaches the part.
|
|
98
|
+
*/
|
|
99
|
+
export function pairs(record, finding) {
|
|
100
|
+
if (record.code !== finding.code || ROW_SUBJECTS.has(finding.symbol.kind)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (record.bound !== "" && record.bound === positionKey(finding.position)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return record.symbol === finding.symbol.ref && record.path === finding.position.path;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Decides every record against `would`, the findings the run would produce were no record
|
|
110
|
+
* there: swept with no mark, where a kind about liveness reports the marked declaration,
|
|
111
|
+
* and as swept, where a kind whose subject the sweep holds live reports it. The first
|
|
112
|
+
* record in reading order claims a finding, so a second naming it is stale. A record whose
|
|
113
|
+
* code the dials withhold from its declaration, or whose claimed finding they withhold, is
|
|
114
|
+
* dormant.
|
|
115
|
+
*/
|
|
116
|
+
export function ledgerOf(records, would, dials) {
|
|
117
|
+
const taken = new Set();
|
|
118
|
+
const verdicts = [];
|
|
119
|
+
const claims = [];
|
|
120
|
+
for (const record of records) {
|
|
121
|
+
if (record.bound !== "" && dials.withholdsCode(record.code, record.bound)) {
|
|
122
|
+
verdicts.push("dormant");
|
|
123
|
+
claims.push(undefined);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const claimed = would.find((finding) => !taken.has(claimKey(finding)) && pairs(record, finding));
|
|
127
|
+
claims.push(claimed);
|
|
128
|
+
if (claimed === undefined) {
|
|
129
|
+
verdicts.push("stale");
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
taken.add(claimKey(claimed));
|
|
133
|
+
verdicts.push(dials.withholds(claimed) ? "dormant" : "in-effect");
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
verdicts,
|
|
137
|
+
claims,
|
|
138
|
+
withheld: (finding) => taken.has(claimKey(finding)) && !ROW_SUBJECTS.has(finding.symbol.kind),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** The suppression counts of one run's records and the ledger over them. */
|
|
142
|
+
export function suppressionCounts(records, ledger) {
|
|
143
|
+
const sites = new Set(records.map((record) => `${record.mechanism}\u0000${positionKey(record.site)}`));
|
|
144
|
+
return {
|
|
145
|
+
inEffect: ledger.verdicts.filter((verdict) => verdict === "in-effect").length,
|
|
146
|
+
reasonsRecorded: sites.size,
|
|
147
|
+
};
|
|
148
|
+
}
|
package/jsr.json
CHANGED