@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
|
@@ -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
|
+
}
|
package/src/host.ts
CHANGED
|
@@ -34,4 +34,9 @@ export interface Host {
|
|
|
34
34
|
* says so at the one moment the value is asked for.
|
|
35
35
|
*/
|
|
36
36
|
analyzerVersion(): string;
|
|
37
|
+
/**
|
|
38
|
+
* Writes one document whole, or a throw naming why it cannot. The write is atomic: a
|
|
39
|
+
* reader of the path sees the previous document or this one and never a part of it.
|
|
40
|
+
*/
|
|
41
|
+
writeFile(path: string, text: string): void;
|
|
37
42
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
|
|
6
|
+
import type { Position } from "./position.ts";
|
|
7
|
+
|
|
8
|
+
/** The token walk over a document `JSON.parse` accepted, so every token is well formed. */
|
|
9
|
+
class Walk {
|
|
10
|
+
readonly opened: number[] = [];
|
|
11
|
+
/** The member written twice, where one is, as the offset of its name. */
|
|
12
|
+
twice: { readonly name: string; readonly offset: number } | undefined;
|
|
13
|
+
private readonly text: string;
|
|
14
|
+
private readonly array: string;
|
|
15
|
+
private at = 0;
|
|
16
|
+
|
|
17
|
+
constructor(text: string, array: string) {
|
|
18
|
+
this.text = text;
|
|
19
|
+
this.array = array;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
run(): void {
|
|
23
|
+
this.value("document");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private skip(): void {
|
|
27
|
+
while (/[ \t\r\n]/u.test(this.text[this.at] ?? "")) {
|
|
28
|
+
this.at += 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private string(): string {
|
|
33
|
+
const start = this.at;
|
|
34
|
+
this.at += 1;
|
|
35
|
+
while (this.text[this.at] !== '"') {
|
|
36
|
+
this.at += this.text[this.at] === "\\" ? 2 : 1;
|
|
37
|
+
}
|
|
38
|
+
this.at += 1;
|
|
39
|
+
return JSON.parse(this.text.slice(start, this.at)) as string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private value(role: "document" | "array" | "record" | "other"): void {
|
|
43
|
+
this.skip();
|
|
44
|
+
const char = this.text[this.at];
|
|
45
|
+
if (char === "{") {
|
|
46
|
+
if (role === "record") {
|
|
47
|
+
this.opened.push(this.at);
|
|
48
|
+
}
|
|
49
|
+
this.object(role);
|
|
50
|
+
} else if (char === "[") {
|
|
51
|
+
this.list(role === "array" ? "record" : "other");
|
|
52
|
+
} else if (char === '"') {
|
|
53
|
+
this.string();
|
|
54
|
+
} else {
|
|
55
|
+
while (this.at < this.text.length && !/[,\]} \t\r\n]/u.test(this.text[this.at] ?? "")) {
|
|
56
|
+
this.at += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private object(role: string): void {
|
|
62
|
+
const seen = new Set<string>();
|
|
63
|
+
this.at += 1;
|
|
64
|
+
for (;;) {
|
|
65
|
+
this.skip();
|
|
66
|
+
if (this.text[this.at] === "}") {
|
|
67
|
+
this.at += 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (this.text[this.at] === ",") {
|
|
71
|
+
this.at += 1;
|
|
72
|
+
this.skip();
|
|
73
|
+
}
|
|
74
|
+
const offset = this.at;
|
|
75
|
+
const name = this.string();
|
|
76
|
+
if (seen.has(name) && this.twice === undefined) {
|
|
77
|
+
this.twice = { name, offset };
|
|
78
|
+
}
|
|
79
|
+
seen.add(name);
|
|
80
|
+
this.skip();
|
|
81
|
+
this.at += 1;
|
|
82
|
+
this.value(role === "document" && name === this.array ? "array" : "other");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private list(element: "record" | "other"): void {
|
|
87
|
+
this.at += 1;
|
|
88
|
+
for (;;) {
|
|
89
|
+
this.skip();
|
|
90
|
+
if (this.text[this.at] === "]") {
|
|
91
|
+
this.at += 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (this.text[this.at] === ",") {
|
|
95
|
+
this.at += 1;
|
|
96
|
+
}
|
|
97
|
+
this.value(element);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** What a walk of one document found. */
|
|
103
|
+
export interface DocumentWalk {
|
|
104
|
+
/** The offset of the brace opening each object of the document's array member, in order. */
|
|
105
|
+
readonly opened: readonly number[];
|
|
106
|
+
/** The first member one object writes twice, at any depth, as the offset of its name. */
|
|
107
|
+
readonly twice: { readonly name: string; readonly offset: number } | undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Walks a document `JSON.parse` accepted, whose top-level member `array` holds the
|
|
112
|
+
* records. A member one object writes twice reads as though it were written once, its
|
|
113
|
+
* later value silently replacing the earlier one, so a document holding one is one a
|
|
114
|
+
* reader names rather than reads.
|
|
115
|
+
*/
|
|
116
|
+
export function walkDocument(text: string, array: string): DocumentWalk {
|
|
117
|
+
const walk = new Walk(text, array);
|
|
118
|
+
walk.run();
|
|
119
|
+
return { opened: walk.opened, twice: walk.twice };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* One offset of a document as a finding carries it: the line counted from one, and the
|
|
124
|
+
* column counted from one in UTF-16 code units.
|
|
125
|
+
*/
|
|
126
|
+
export function positionAt(text: string, file: string, offset: number): Position {
|
|
127
|
+
const before = text.slice(0, offset);
|
|
128
|
+
const lineStart = before.lastIndexOf("\n") + 1;
|
|
129
|
+
return { path: file, line: before.split("\n").length, column: offset - lineStart + 1 };
|
|
130
|
+
}
|
package/src/references.ts
CHANGED
|
@@ -126,8 +126,6 @@ export interface References {
|
|
|
126
126
|
/** The compiler configuration the project was opened from. */
|
|
127
127
|
readonly configFile: string;
|
|
128
128
|
readonly references: readonly Reference[];
|
|
129
|
-
/** The rules that classified files as test files, ordered by rule. */
|
|
130
|
-
readonly testFileRules: readonly TestFileRule[];
|
|
131
129
|
/**
|
|
132
130
|
* Every file the rules classified as a test file, by its path below the target root,
|
|
133
131
|
* ascending. A declaration such a file holds is a test declaration, by the same
|
|
@@ -147,7 +145,7 @@ export interface ReferenceOptions {
|
|
|
147
145
|
readonly batchCap?: number;
|
|
148
146
|
/**
|
|
149
147
|
* Glob patterns, relative to the target root, naming the files that are test files.
|
|
150
|
-
* Each pattern is a rule of its own
|
|
148
|
+
* Each pattern is a rule of its own, which {@link testFileRulesOf} counts.
|
|
151
149
|
*/
|
|
152
150
|
readonly testFiles: readonly string[];
|
|
153
151
|
}
|
|
@@ -499,6 +497,26 @@ function patternRule(index: number): string {
|
|
|
499
497
|
return `test-file-pattern-${String(index + 1)}`;
|
|
500
498
|
}
|
|
501
499
|
|
|
500
|
+
/**
|
|
501
|
+
* The rules over the test files of several projects at once, ordered by rule: a file two
|
|
502
|
+
* projects hold is one file, so each rule counts the distinct paths it matches.
|
|
503
|
+
*/
|
|
504
|
+
export function testFileRulesOf(
|
|
505
|
+
testFiles: readonly string[],
|
|
506
|
+
paths: readonly string[],
|
|
507
|
+
): readonly TestFileRule[] {
|
|
508
|
+
const distinct = [...new Set(paths)];
|
|
509
|
+
return testFiles
|
|
510
|
+
.map((pattern, index) => {
|
|
511
|
+
const expression = globExpression(pattern);
|
|
512
|
+
return {
|
|
513
|
+
rule: patternRule(index),
|
|
514
|
+
matched: distinct.filter((path) => expression.test(path)).length,
|
|
515
|
+
};
|
|
516
|
+
})
|
|
517
|
+
.sort((a, b) => compare(a.rule, b.rule));
|
|
518
|
+
}
|
|
519
|
+
|
|
502
520
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
503
521
|
function compare(a: string, b: string): number {
|
|
504
522
|
if (a === b) {
|
|
@@ -509,7 +527,7 @@ function compare(a: string, b: string): number {
|
|
|
509
527
|
|
|
510
528
|
/**
|
|
511
529
|
* Every reference one project's own files make to the project's own declarations, in
|
|
512
|
-
* position order, and the test-file rules
|
|
530
|
+
* position order, and the files the test-file rules classified.
|
|
513
531
|
*
|
|
514
532
|
* `held` is the inventory of the same project, whose declaration map is what a resolved
|
|
515
533
|
* symbol is looked up in; `targetRoot` is the absolute path every position is rendered
|
|
@@ -530,7 +548,6 @@ export function references<Brand>(
|
|
|
530
548
|
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
531
549
|
const declarations = held.declarations;
|
|
532
550
|
const patterns = options.testFiles.map(globExpression);
|
|
533
|
-
const matched = options.testFiles.map(() => 0);
|
|
534
551
|
const found: Reference[] = [];
|
|
535
552
|
const tests: string[] = [];
|
|
536
553
|
const chains = aliasChains(project, held);
|
|
@@ -574,13 +591,7 @@ export function references<Brand>(
|
|
|
574
591
|
|
|
575
592
|
for (const file of files) {
|
|
576
593
|
const path = renderPosition(file, targetRoot, 0).path;
|
|
577
|
-
const
|
|
578
|
-
hits.forEach((hit, index) => {
|
|
579
|
-
if (hit) {
|
|
580
|
-
matched[index] = (matched[index] ?? 0) + 1;
|
|
581
|
-
}
|
|
582
|
-
});
|
|
583
|
-
const test = hits.includes(true);
|
|
594
|
+
const test = patterns.some((pattern) => pattern.test(path));
|
|
584
595
|
if (test) {
|
|
585
596
|
tests.push(path);
|
|
586
597
|
}
|
|
@@ -706,9 +717,6 @@ export function references<Brand>(
|
|
|
706
717
|
return {
|
|
707
718
|
configFile: project.configFile,
|
|
708
719
|
references: found,
|
|
709
|
-
testFileRules: options.testFiles
|
|
710
|
-
.map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
|
|
711
|
-
.sort((a, b) => compare(a.rule, b.rule)),
|
|
712
720
|
testFilePaths: tests.sort(compare),
|
|
713
721
|
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps: chains.steps },
|
|
714
722
|
};
|