@cplieger/deadset-ts 3.7.0 → 3.9.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/README.md +13 -13
- package/dist/bin/node-host.js +32 -2
- package/dist/src/analysis.js +111 -26
- package/dist/src/conformance.js +22 -0
- package/dist/src/edges.js +190 -0
- package/dist/src/findings/completion.js +39 -12
- package/dist/src/findings/emitters.js +42 -6
- package/dist/src/findings/self-check.js +139 -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/references.js +18 -12
- package/dist/src/report.js +237 -0
- package/dist/src/reporters/json.js +4 -0
- package/dist/src/reporters/reporter.js +1 -0
- package/dist/src/reporters/reporters.js +10 -0
- package/dist/src/reporters/text.js +34 -0
- package/dist/src/roots.js +0 -34
- package/dist/src/run.js +23 -10
- package/dist/src/suppress-file.js +196 -0
- package/dist/src/suppress-inline.js +259 -0
- package/dist/src/suppress.js +148 -0
- package/dist/src/verbs/analyze.js +184 -0
- package/dist/src/verbs/describe.js +45 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +230 -35
- package/src/conformance.ts +34 -0
- package/src/edges.ts +241 -0
- package/src/finding.ts +13 -0
- package/src/findings/boundary.ts +0 -11
- package/src/findings/completion.ts +54 -15
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +81 -7
- package/src/findings/self-check.ts +226 -2
- package/src/findings/visibility-narrowing.ts +10 -52
- package/src/findings-pass.ts +145 -0
- package/src/host.ts +5 -0
- package/src/json-document.ts +130 -0
- package/src/references.ts +23 -15
- package/src/report.ts +430 -0
- package/src/reporters/json.ts +7 -0
- package/src/reporters/reporter.ts +18 -0
- package/src/reporters/reporters.ts +13 -0
- package/src/reporters/text.ts +45 -0
- package/src/roots.ts +0 -39
- package/src/run.ts +40 -12
- package/src/suppress-file.ts +283 -0
- package/src/suppress-inline.ts +318 -0
- package/src/suppress.ts +262 -0
- package/src/verbs/analyze.ts +234 -0
- package/src/verbs/describe.ts +46 -0
- package/src/verbs/verb.ts +13 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { evaluateEdges } from "../edges.js";
|
|
2
|
+
import { ledgerOf } from "../suppress.js";
|
|
3
|
+
import { completed, dialsOf, reportable } from "./completion.js";
|
|
1
4
|
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
|
|
2
|
-
import { completed, reportable } from "./completion.js";
|
|
3
5
|
import { interfaces } from "./interfaces.js";
|
|
4
6
|
import { intraFunction } from "./intra-function.js";
|
|
5
7
|
import { nonCodeArtifacts } from "./non-code-artifacts.js";
|
|
@@ -18,12 +20,46 @@ export const EMITTERS = new Map([
|
|
|
18
20
|
["self-check", selfCheck],
|
|
19
21
|
["intra-function", intraFunction],
|
|
20
22
|
]);
|
|
23
|
+
/** Every family's findings over one sweep, completed by one step. */
|
|
24
|
+
function completedOver(input) {
|
|
25
|
+
return completed(input, [...EMITTERS.values()].flatMap((emit) => emit(input)));
|
|
26
|
+
}
|
|
21
27
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* family reports withholds what another family reports in its component.
|
|
28
|
+
* The dials over the findings a record could name, each read against the sweep that
|
|
29
|
+
* produced it, so a component one sweep numbers withholds nothing of the other's.
|
|
25
30
|
*/
|
|
31
|
+
function dialsOver(input, found, suppressed, unmarked) {
|
|
32
|
+
const marked = dialsOf(input, found);
|
|
33
|
+
const before = dialsOf(suppressed.unmarked, unmarked);
|
|
34
|
+
const fromUnmarked = new Set(unmarked);
|
|
35
|
+
return {
|
|
36
|
+
withholdsCode: marked.withholdsCode,
|
|
37
|
+
withholds: (finding) => fromUnmarked.has(finding) ? before.withholds(finding) : marked.withholds(finding),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Every finding of one run swept under its suppressions' marks, decided in one pass: every
|
|
42
|
+
* family's findings completed by one step, so every component number is minted once, a
|
|
43
|
+
* pending finding's among them; the finding each record claims withheld, whatever its
|
|
44
|
+
* kind; the rest withheld by the dials, so a root one family reports withholds what
|
|
45
|
+
* another family reports in its component; and every declared edge evaluated over what
|
|
46
|
+
* remains, so a finding it holds pending is in its evaluation and nowhere else.
|
|
47
|
+
*/
|
|
48
|
+
export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
|
|
49
|
+
const found = completedOver(input);
|
|
50
|
+
const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked);
|
|
51
|
+
const would = [...unmarked, ...found];
|
|
52
|
+
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
53
|
+
const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding)));
|
|
54
|
+
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
55
|
+
return {
|
|
56
|
+
findings: evaluated.findings,
|
|
57
|
+
evaluations: evaluated.evaluations,
|
|
58
|
+
ledger,
|
|
59
|
+
would,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Every finding the run reports, each family's in table order. */
|
|
26
63
|
export function findingsOf(input) {
|
|
27
|
-
|
|
28
|
-
return reportable(completed(input, found), input.config.analysis.minConfidence);
|
|
64
|
+
return decidedFindings(input).findings;
|
|
29
65
|
}
|
|
@@ -1,2 +1,139 @@
|
|
|
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, details) {
|
|
39
|
+
return {
|
|
40
|
+
code,
|
|
41
|
+
position: { ...site, endLine: site.line },
|
|
42
|
+
symbol: { ref, kind: SUPPRESSION_SUBJECT, name: ref, sizeLines: 1 },
|
|
43
|
+
message,
|
|
44
|
+
details,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Whether a reason holds a character other than whitespace. */
|
|
48
|
+
const STATED = /[^ \t\r\n]/u;
|
|
49
|
+
/** A refused suppression as written: a member it lacks is absent. */
|
|
50
|
+
function writtenEntry(refused) {
|
|
51
|
+
return {
|
|
52
|
+
code: refused.code,
|
|
53
|
+
...(refused.symbol === "" ? {} : { symbol: refused.symbol }),
|
|
54
|
+
...(refused.path === "" ? {} : { path: refused.path }),
|
|
55
|
+
...(STATED.test(refused.reason) ? { reason: refused.reason } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The reference a finding about a suppression names: the symbol it names or bound, else
|
|
60
|
+
* the file form of the document that holds it, else that document's path.
|
|
61
|
+
*/
|
|
62
|
+
function suppressionRef(symbol, path, files) {
|
|
63
|
+
return symbol === "" ? (files(path) ?? path) : symbol;
|
|
64
|
+
}
|
|
65
|
+
/** One finding per refused suppression: the grammar's reason refusal and its scope refusal. */
|
|
66
|
+
export function refusedSuppressions(refusals, files) {
|
|
67
|
+
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
|
|
68
|
+
? "carries no reason"
|
|
69
|
+
: "names a symbol and no path"}`, { mechanism: refused.mechanism, entry: writtenEntry(refused) }));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One record per site at which a suppression matched nothing, whatever the number of
|
|
73
|
+
* codes or declarations written there: the first record names the entry, and the message
|
|
74
|
+
* names every code the site's records named and every code the declaration reports
|
|
75
|
+
* instead, so the maintainer corrects the code rather than hunting for it.
|
|
76
|
+
*/
|
|
77
|
+
export function staleSuppressions(stale, files) {
|
|
78
|
+
const bySite = new Map();
|
|
79
|
+
for (const one of stale) {
|
|
80
|
+
const { site, mechanism } = one.record;
|
|
81
|
+
const key = `${mechanism}\u0000${site.path}\u0000${String(site.line)}\u0000${String(site.column)}`;
|
|
82
|
+
const held = bySite.get(key) ?? { first: one.record, records: [] };
|
|
83
|
+
held.records.push(one);
|
|
84
|
+
bySite.set(key, held);
|
|
85
|
+
}
|
|
86
|
+
return [...bySite.values()].map(({ first, records }) => {
|
|
87
|
+
const codes = [...new Set(records.map((one) => one.record.code))].sort();
|
|
88
|
+
const reports = [...new Set(records.flatMap((one) => one.reports))]
|
|
89
|
+
.filter((code) => !codes.includes(code))
|
|
90
|
+
.sort();
|
|
91
|
+
const instead = reports.length === 0 ? "" : `, and its declaration reports ${spelled(reports)}`;
|
|
92
|
+
return {
|
|
93
|
+
code: STALE_SUPPRESSION,
|
|
94
|
+
mechanism: first.mechanism,
|
|
95
|
+
entry: {
|
|
96
|
+
code: first.code,
|
|
97
|
+
...(first.symbol === "" ? {} : { symbol: first.symbol }),
|
|
98
|
+
path: first.path,
|
|
99
|
+
reason: first.reason,
|
|
100
|
+
},
|
|
101
|
+
position: first.site,
|
|
102
|
+
symbol: suppressionRef(first.symbol, first.path, files),
|
|
103
|
+
message: `${WRITTEN[first.mechanism]} for ${spelled(codes)} matches no current finding${instead}`,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One finding per configured root or pattern that named nothing in any project. A finding
|
|
109
|
+
* names the document that declared the roots, or the conventional repository configuration
|
|
110
|
+
* where no document of the target did, and every finding of one run sits at that
|
|
111
|
+
* document's first position: the roots are an array whose members carry no line of their
|
|
112
|
+
* own, so the string each finding names tells one from another.
|
|
113
|
+
*/
|
|
114
|
+
export function unmatchedRootFindings(sources, document) {
|
|
115
|
+
const position = {
|
|
116
|
+
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
117
|
+
line: DOCUMENT_POSITION,
|
|
118
|
+
column: DOCUMENT_POSITION,
|
|
119
|
+
endLine: DOCUMENT_POSITION,
|
|
120
|
+
};
|
|
121
|
+
return sources.map((source) => ({
|
|
122
|
+
code: UNMATCHED_ROOT,
|
|
123
|
+
position,
|
|
124
|
+
symbol: { ref: source, kind: ROOT_SUBJECT, name: source, sizeLines: 1 },
|
|
125
|
+
message: source.includes("*") || source.includes("?")
|
|
126
|
+
? "configured root pattern matches no symbol of the inventory"
|
|
127
|
+
: "configured root matches no symbol of the inventory",
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
|
|
132
|
+
* carrying no reason or naming no path, and a configured root that named nothing. A
|
|
133
|
+
* suppression that matched nothing is a record of the report's stale suppressions, which
|
|
134
|
+
* {@link staleSuppressions} builds, and a stale cross-language edge is the merge's to report.
|
|
135
|
+
*/
|
|
136
|
+
export const selfCheck = ({ selfCheck: facts, swept }) => [
|
|
137
|
+
...refusedSuppressions(facts.refusals, fileRefsOf(swept.matrix.union.symbols)),
|
|
138
|
+
...unmatchedRootFindings(facts.unmatchedRoots, facts.rootsDocument),
|
|
139
|
+
];
|
|
@@ -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
|
+
}
|
package/dist/src/references.js
CHANGED
|
@@ -272,6 +272,22 @@ function sitesOf(file, declarations, fileId) {
|
|
|
272
272
|
function patternRule(index) {
|
|
273
273
|
return `test-file-pattern-${String(index + 1)}`;
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* The rules over the test files of several projects at once, ordered by rule: a file two
|
|
277
|
+
* projects hold is one file, so each rule counts the distinct paths it matches.
|
|
278
|
+
*/
|
|
279
|
+
export function testFileRulesOf(testFiles, paths) {
|
|
280
|
+
const distinct = [...new Set(paths)];
|
|
281
|
+
return testFiles
|
|
282
|
+
.map((pattern, index) => {
|
|
283
|
+
const expression = globExpression(pattern);
|
|
284
|
+
return {
|
|
285
|
+
rule: patternRule(index),
|
|
286
|
+
matched: distinct.filter((path) => expression.test(path)).length,
|
|
287
|
+
};
|
|
288
|
+
})
|
|
289
|
+
.sort((a, b) => compare(a.rule, b.rule));
|
|
290
|
+
}
|
|
275
291
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
276
292
|
function compare(a, b) {
|
|
277
293
|
if (a === b) {
|
|
@@ -281,7 +297,7 @@ function compare(a, b) {
|
|
|
281
297
|
}
|
|
282
298
|
/**
|
|
283
299
|
* Every reference one project's own files make to the project's own declarations, in
|
|
284
|
-
* position order, and the test-file rules
|
|
300
|
+
* position order, and the files the test-file rules classified.
|
|
285
301
|
*
|
|
286
302
|
* `held` is the inventory of the same project, whose declaration map is what a resolved
|
|
287
303
|
* symbol is looked up in; `targetRoot` is the absolute path every position is rendered
|
|
@@ -297,7 +313,6 @@ export function references(project, held, targetRoot, options) {
|
|
|
297
313
|
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
298
314
|
const declarations = held.declarations;
|
|
299
315
|
const patterns = options.testFiles.map(globExpression);
|
|
300
|
-
const matched = options.testFiles.map(() => 0);
|
|
301
316
|
const found = [];
|
|
302
317
|
const tests = [];
|
|
303
318
|
const chains = aliasChains(project, held);
|
|
@@ -332,13 +347,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
332
347
|
};
|
|
333
348
|
for (const file of files) {
|
|
334
349
|
const path = renderPosition(file, targetRoot, 0).path;
|
|
335
|
-
const
|
|
336
|
-
hits.forEach((hit, index) => {
|
|
337
|
-
if (hit) {
|
|
338
|
-
matched[index] = (matched[index] ?? 0) + 1;
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
const test = hits.includes(true);
|
|
350
|
+
const test = patterns.some((pattern) => pattern.test(path));
|
|
342
351
|
if (test) {
|
|
343
352
|
tests.push(path);
|
|
344
353
|
}
|
|
@@ -449,9 +458,6 @@ export function references(project, held, targetRoot, options) {
|
|
|
449
458
|
return {
|
|
450
459
|
configFile: project.configFile,
|
|
451
460
|
references: found,
|
|
452
|
-
testFileRules: options.testFiles
|
|
453
|
-
.map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
|
|
454
|
-
.sort((a, b) => compare(a.rule, b.rule)),
|
|
455
461
|
testFilePaths: tests.sort(compare),
|
|
456
462
|
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps: chains.steps },
|
|
457
463
|
};
|