@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
package/src/findings/boundary.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { Finding } from "../finding.ts";
|
|
2
|
-
|
|
3
1
|
/** Which side of a declared cross-language edge, as the edges document spells it. */
|
|
4
2
|
export type EdgeSideName = "provides" | "used_by";
|
|
5
3
|
|
|
@@ -12,15 +10,6 @@ export interface EdgeSide {
|
|
|
12
10
|
readonly symbol: string;
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
/**
|
|
16
|
-
* One finding a declared edge holds back: the finding the analyzer would report about
|
|
17
|
-
* the symbol one side of the edge names, which a report carries only inside that side's
|
|
18
|
-
* evaluation, because the code that may use the symbol is in the other language.
|
|
19
|
-
*/
|
|
20
|
-
export interface PendingFinding extends EdgeSide {
|
|
21
|
-
readonly finding: Finding;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
13
|
/** What the run knows about the code outside the target that can reach its declarations. */
|
|
25
14
|
export interface Boundary {
|
|
26
15
|
readonly consumers: {
|
|
@@ -11,6 +11,7 @@ import type { CompletedFinding, Finding, FindingComponent } from "../finding.ts"
|
|
|
11
11
|
import { OUTSIDE } from "../graph.ts";
|
|
12
12
|
import { KINDS } from "../kinds.ts";
|
|
13
13
|
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
14
|
+
import { isRowSubject, type Dials } from "../suppress.ts";
|
|
14
15
|
import type { EmitterInput } from "./emitter.ts";
|
|
15
16
|
|
|
16
17
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
@@ -74,7 +75,8 @@ function findingComponents(swept: RunSweep): (id: string) => FindingComponent |
|
|
|
74
75
|
* candidate's relation, counts, configurations and component; a live one carries no
|
|
75
76
|
* relation and holds where it is declared; a file or a manifest row is `certain` and
|
|
76
77
|
* holds in every configuration. A subject in no dead component gets one of its own,
|
|
77
|
-
* numbered past the computed ones in finding order, so no two families mint one
|
|
78
|
+
* numbered past the computed ones in finding order, so no two families mint one; nothing
|
|
79
|
+
* falls with it, so its deletion removes no line.
|
|
78
80
|
*/
|
|
79
81
|
export function completed(
|
|
80
82
|
input: EmitterInput,
|
|
@@ -93,7 +95,11 @@ export function completed(
|
|
|
93
95
|
if (row === undefined) {
|
|
94
96
|
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
95
97
|
}
|
|
96
|
-
|
|
98
|
+
// A row's reference may spell a declaration's, as an ignore entry naming one does, and
|
|
99
|
+
// a row is still never the declaration.
|
|
100
|
+
const at = isRowSubject(finding.symbol.kind)
|
|
101
|
+
? OUTSIDE
|
|
102
|
+
: (positions.get(finding.symbol.ref) ?? OUTSIDE);
|
|
97
103
|
const symbol = union.symbols[at];
|
|
98
104
|
const judged = union.subject[at] === true ? symbol : undefined;
|
|
99
105
|
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
@@ -108,7 +114,7 @@ export function completed(
|
|
|
108
114
|
id: componentId(minted),
|
|
109
115
|
root: true,
|
|
110
116
|
symbolCount: 1,
|
|
111
|
-
deletableLines:
|
|
117
|
+
deletableLines: 0,
|
|
112
118
|
};
|
|
113
119
|
}
|
|
114
120
|
const counts =
|
|
@@ -141,23 +147,56 @@ export function completed(
|
|
|
141
147
|
});
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
151
|
+
function dialed(
|
|
152
|
+
finding: Pick<CompletedFinding, "severity" | "confidence">,
|
|
153
|
+
minConfidence: Confidence,
|
|
154
|
+
): boolean {
|
|
155
|
+
return finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
156
|
+
}
|
|
157
|
+
|
|
144
158
|
/**
|
|
145
|
-
* The
|
|
146
|
-
* is withheld, and so is every finding of the component of a
|
|
147
|
-
* because a symbol that falls with a root is dead only through it
|
|
148
|
-
* does not compile while the root stays. A withheld finding that is no
|
|
149
|
-
* itself alone.
|
|
159
|
+
* The configuration's dials over one run's completed findings. A finding at `allow` or
|
|
160
|
+
* below the minimum confidence is withheld, and so is every finding of the component of a
|
|
161
|
+
* root the dials withhold, because a symbol that falls with a root is dead only through it
|
|
162
|
+
* and its deletion does not compile while the root stays. A withheld finding that is no
|
|
163
|
+
* root withholds itself alone. A code is withheld from a declaration when the finding it
|
|
164
|
+
* would report there, completed, is withheld by its own severity or confidence.
|
|
150
165
|
*/
|
|
151
|
-
export function
|
|
166
|
+
export function dialsOf(
|
|
167
|
+
input: EmitterInput,
|
|
152
168
|
findings: readonly CompletedFinding[],
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
169
|
+
): Dials<CompletedFinding> {
|
|
170
|
+
const minConfidence = input.config.analysis.minConfidence;
|
|
171
|
+
const classOf = reachabilityClasses(input.swept);
|
|
157
172
|
const components = new Set(
|
|
158
173
|
findings
|
|
159
|
-
.filter((finding) => finding.component.root && dialed(finding))
|
|
174
|
+
.filter((finding) => finding.component.root && dialed(finding, minConfidence))
|
|
160
175
|
.map((finding) => finding.component.id),
|
|
161
176
|
);
|
|
162
|
-
return
|
|
177
|
+
return {
|
|
178
|
+
withholdsCode: (code, id) => {
|
|
179
|
+
const row = KINDS.get(code);
|
|
180
|
+
return (
|
|
181
|
+
row !== undefined &&
|
|
182
|
+
dialed(
|
|
183
|
+
{
|
|
184
|
+
severity: severityOf(input.config, code, row.defaultSeverity),
|
|
185
|
+
confidence: capped(classOf(id), row.maxClass),
|
|
186
|
+
},
|
|
187
|
+
minConfidence,
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The findings of one run's completed findings that no dial withholds, in the order given. */
|
|
196
|
+
export function reportable(
|
|
197
|
+
input: EmitterInput,
|
|
198
|
+
findings: readonly CompletedFinding[],
|
|
199
|
+
): readonly CompletedFinding[] {
|
|
200
|
+
const dials = dialsOf(input, findings);
|
|
201
|
+
return findings.filter((finding) => !dials.withholds(finding));
|
|
163
202
|
}
|
package/src/findings/emitter.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Finding } from "../finding.ts";
|
|
|
6
6
|
import type { Implementations } from "../implementations.ts";
|
|
7
7
|
import type { Stores } from "../stores.ts";
|
|
8
8
|
import type { Boundary } from "./boundary.ts";
|
|
9
|
+
import type { SelfCheckFacts } from "./self-check.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* What every emitter reads: the configuration the run resolved, the run's sweep, and
|
|
@@ -27,6 +28,8 @@ export interface EmitterInput {
|
|
|
27
28
|
readonly files: FileFacts;
|
|
28
29
|
/** What can reach the target from outside it: consumers, declared edges, and `exports`. */
|
|
29
30
|
readonly boundary: Boundary;
|
|
31
|
+
/** The run's suppression outcomes and configured roots, which the self-check family reports. */
|
|
32
|
+
readonly selfCheck: SelfCheckFacts;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/**
|
package/src/findings/emitters.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
|
|
2
2
|
import type { CompletedFinding } from "../finding.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { ledgerOf, type Dials, type Ledger, type SuppressionRecord } from "../suppress.ts";
|
|
4
|
+
import { completed, dialsOf, reportable } from "./completion.ts";
|
|
5
|
+
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.ts";
|
|
4
6
|
import type { Emitter, EmitterInput, Emitters } from "./emitter.ts";
|
|
5
7
|
import { interfaces } from "./interfaces.ts";
|
|
6
8
|
import { intraFunction } from "./intra-function.ts";
|
|
@@ -22,12 +24,84 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
|
|
|
22
24
|
["intra-function", intraFunction],
|
|
23
25
|
]);
|
|
24
26
|
|
|
27
|
+
/** What the suppressions of a run swept under their marks are decided against. */
|
|
28
|
+
export interface Suppressed {
|
|
29
|
+
/** The same run swept with no mark, which is where a kind about liveness reports a marked declaration. */
|
|
30
|
+
readonly unmarked: EmitterInput;
|
|
31
|
+
/** The run's suppression records, in reading order. */
|
|
32
|
+
readonly records: readonly SuppressionRecord[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What the emitter table answers about one run. */
|
|
36
|
+
export interface DecidedFindings {
|
|
37
|
+
/** Every finding the run reports, each family's in table order. */
|
|
38
|
+
readonly findings: readonly CompletedFinding[];
|
|
39
|
+
/** One evaluation per declared edge side of this language, ordered by edge, then by side. */
|
|
40
|
+
readonly evaluations: readonly EdgeEvaluation<CompletedFinding>[];
|
|
41
|
+
/** What every suppression record did, in reading order. */
|
|
42
|
+
readonly ledger: Ledger<CompletedFinding>;
|
|
43
|
+
/** Every finding a record could name: the run's with no mark, then the run's as swept. */
|
|
44
|
+
readonly would: readonly CompletedFinding[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Every family's findings over one sweep, completed by one step. */
|
|
48
|
+
function completedOver(input: EmitterInput): readonly CompletedFinding[] {
|
|
49
|
+
return completed(
|
|
50
|
+
input,
|
|
51
|
+
[...EMITTERS.values()].flatMap((emit) => emit(input)),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* family reports withholds what another family reports in its component.
|
|
56
|
+
* The dials over the findings a record could name, each read against the sweep that
|
|
57
|
+
* produced it, so a component one sweep numbers withholds nothing of the other's.
|
|
29
58
|
*/
|
|
59
|
+
function dialsOver(
|
|
60
|
+
input: EmitterInput,
|
|
61
|
+
found: readonly CompletedFinding[],
|
|
62
|
+
suppressed: Suppressed,
|
|
63
|
+
unmarked: readonly CompletedFinding[],
|
|
64
|
+
): Dials<CompletedFinding> {
|
|
65
|
+
const marked = dialsOf(input, found);
|
|
66
|
+
const before = dialsOf(suppressed.unmarked, unmarked);
|
|
67
|
+
const fromUnmarked = new Set(unmarked);
|
|
68
|
+
return {
|
|
69
|
+
withholdsCode: marked.withholdsCode,
|
|
70
|
+
withholds: (finding) =>
|
|
71
|
+
fromUnmarked.has(finding) ? before.withholds(finding) : marked.withholds(finding),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Every finding of one run swept under its suppressions' marks, decided in one pass: every
|
|
77
|
+
* family's findings completed by one step, so every component number is minted once, a
|
|
78
|
+
* pending finding's among them; the finding each record claims withheld, whatever its
|
|
79
|
+
* kind; the rest withheld by the dials, so a root one family reports withholds what
|
|
80
|
+
* another family reports in its component; and every declared edge evaluated over what
|
|
81
|
+
* remains, so a finding it holds pending is in its evaluation and nowhere else.
|
|
82
|
+
*/
|
|
83
|
+
export function decidedFindings(
|
|
84
|
+
input: EmitterInput,
|
|
85
|
+
suppressed: Suppressed = { unmarked: input, records: [] },
|
|
86
|
+
): DecidedFindings {
|
|
87
|
+
const found = completedOver(input);
|
|
88
|
+
const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked);
|
|
89
|
+
const would = [...unmarked, ...found];
|
|
90
|
+
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
91
|
+
const kept = reportable(
|
|
92
|
+
input,
|
|
93
|
+
found.filter((finding) => !ledger.withheld(finding)),
|
|
94
|
+
);
|
|
95
|
+
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
96
|
+
return {
|
|
97
|
+
findings: evaluated.findings,
|
|
98
|
+
evaluations: evaluated.evaluations,
|
|
99
|
+
ledger,
|
|
100
|
+
would,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Every finding the run reports, each family's in table order. */
|
|
30
105
|
export function findingsOf(input: EmitterInput): readonly CompletedFinding[] {
|
|
31
|
-
|
|
32
|
-
return reportable(completed(input, found), input.config.analysis.minConfidence);
|
|
106
|
+
return decidedFindings(input).findings;
|
|
33
107
|
}
|
|
@@ -1,4 +1,228 @@
|
|
|
1
|
+
import { REPOSITORY_DOCUMENT } from "../config.ts";
|
|
2
|
+
import type { Finding, FindingDetails, SuppressionEntry } from "../finding.ts";
|
|
3
|
+
import type { InventorySymbol } from "../inventory.ts";
|
|
4
|
+
import type { Position } from "../position.ts";
|
|
5
|
+
import {
|
|
6
|
+
STALE_SUPPRESSION,
|
|
7
|
+
SUPPRESSION_WITHOUT_REASON,
|
|
8
|
+
type Mechanism,
|
|
9
|
+
type Refusal,
|
|
10
|
+
type SuppressionRecord,
|
|
11
|
+
} from "../suppress.ts";
|
|
1
12
|
import type { Emitter } from "./emitter.ts";
|
|
2
13
|
|
|
3
|
-
/** The
|
|
4
|
-
|
|
14
|
+
/** The code a configured root that matches nothing is reported under. */
|
|
15
|
+
const UNMATCHED_ROOT = "DS1704";
|
|
16
|
+
|
|
17
|
+
/** The subject kind of a finding about a suppression record. */
|
|
18
|
+
const SUPPRESSION_SUBJECT = "suppression";
|
|
19
|
+
|
|
20
|
+
/** The subject kind of a finding about a configured root. */
|
|
21
|
+
const ROOT_SUBJECT = "root";
|
|
22
|
+
|
|
23
|
+
/** The fixed position of a finding about a document rather than about a line of one. */
|
|
24
|
+
const DOCUMENT_POSITION = 1;
|
|
25
|
+
|
|
26
|
+
/** One record no finding matched, and the codes its declaration reports instead. */
|
|
27
|
+
export interface StaleRecord {
|
|
28
|
+
readonly record: SuppressionRecord;
|
|
29
|
+
/** The codes the bound declaration would report, none of them the record's own. */
|
|
30
|
+
readonly reports: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the self-check family reads: the run's refused suppressions and its configured roots. */
|
|
34
|
+
export interface SelfCheckFacts {
|
|
35
|
+
/** The suppressions the grammar refused with a finding, in reading order. */
|
|
36
|
+
readonly refusals: readonly Refusal[];
|
|
37
|
+
/** Every configured root or pattern that matched nothing in any project. */
|
|
38
|
+
readonly unmatchedRoots: readonly string[];
|
|
39
|
+
/** The document below the target root that declared the roots, empty where none did. */
|
|
40
|
+
readonly rootsDocument: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A run with nothing for the self-check family to report. */
|
|
44
|
+
export const NO_SELF_CHECK: SelfCheckFacts = {
|
|
45
|
+
refusals: [],
|
|
46
|
+
unmatchedRoots: [],
|
|
47
|
+
rootsDocument: "",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The words a message names a mechanism by. */
|
|
51
|
+
const WRITTEN: Readonly<Record<Mechanism, string>> = {
|
|
52
|
+
inline: "inline directive",
|
|
53
|
+
ignore: "ignore entry",
|
|
54
|
+
baseline: "baseline row",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** The file references of a run's declarations, by path. */
|
|
58
|
+
export function fileRefsOf(symbols: readonly InventorySymbol[]): FileRefs {
|
|
59
|
+
const byPath = new Map(
|
|
60
|
+
symbols
|
|
61
|
+
.filter((symbol) => symbol.kind === "file")
|
|
62
|
+
.map((symbol) => [symbol.position.path, symbol.ref]),
|
|
63
|
+
);
|
|
64
|
+
return (path) => byPath.get(path);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A list of codes as a sentence names them. */
|
|
68
|
+
function spelled(codes: readonly string[]): string {
|
|
69
|
+
if (codes.length < 2) {
|
|
70
|
+
return codes.join("");
|
|
71
|
+
}
|
|
72
|
+
return `${codes.slice(0, -1).join(", ")} and ${codes.at(-1) ?? ""}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A finding about one suppression, at the suppression's own site. */
|
|
76
|
+
function aboutSuppression(
|
|
77
|
+
code: string,
|
|
78
|
+
ref: string,
|
|
79
|
+
site: Position,
|
|
80
|
+
message: string,
|
|
81
|
+
details: FindingDetails,
|
|
82
|
+
): Finding {
|
|
83
|
+
return {
|
|
84
|
+
code,
|
|
85
|
+
position: { ...site, endLine: site.line },
|
|
86
|
+
symbol: { ref, kind: SUPPRESSION_SUBJECT, name: ref, sizeLines: 1 },
|
|
87
|
+
message,
|
|
88
|
+
details,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Whether a reason holds a character other than whitespace. */
|
|
93
|
+
const STATED = /[^ \t\r\n]/u;
|
|
94
|
+
|
|
95
|
+
/** A refused suppression as written: a member it lacks is absent. */
|
|
96
|
+
function writtenEntry(refused: Refusal): SuppressionEntry {
|
|
97
|
+
return {
|
|
98
|
+
code: refused.code,
|
|
99
|
+
...(refused.symbol === "" ? {} : { symbol: refused.symbol }),
|
|
100
|
+
...(refused.path === "" ? {} : { path: refused.path }),
|
|
101
|
+
...(STATED.test(refused.reason) ? { reason: refused.reason } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Each file of the run by its path, as the reference of the file it is. */
|
|
106
|
+
export type FileRefs = (path: string) => string | undefined;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The reference a finding about a suppression names: the symbol it names or bound, else
|
|
110
|
+
* the file form of the document that holds it, else that document's path.
|
|
111
|
+
*/
|
|
112
|
+
function suppressionRef(symbol: string, path: string, files: FileRefs): string {
|
|
113
|
+
return symbol === "" ? (files(path) ?? path) : symbol;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** One finding per refused suppression: the grammar's reason refusal and its scope refusal. */
|
|
117
|
+
export function refusedSuppressions(
|
|
118
|
+
refusals: readonly Refusal[],
|
|
119
|
+
files: FileRefs,
|
|
120
|
+
): readonly Finding[] {
|
|
121
|
+
return refusals.map((refused) =>
|
|
122
|
+
aboutSuppression(
|
|
123
|
+
refused.reported,
|
|
124
|
+
suppressionRef(refused.symbol, refused.path, files),
|
|
125
|
+
refused.site,
|
|
126
|
+
`${WRITTEN[refused.mechanism]} for ${refused.code} ${
|
|
127
|
+
refused.reported === SUPPRESSION_WITHOUT_REASON
|
|
128
|
+
? "carries no reason"
|
|
129
|
+
: "names a symbol and no path"
|
|
130
|
+
}`,
|
|
131
|
+
{ mechanism: refused.mechanism, entry: writtenEntry(refused) },
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** One stale suppression as the report's stale-suppression record carries it. */
|
|
137
|
+
export interface StaleSuppression {
|
|
138
|
+
readonly code: typeof STALE_SUPPRESSION;
|
|
139
|
+
readonly mechanism: Mechanism;
|
|
140
|
+
readonly entry: {
|
|
141
|
+
readonly code: string;
|
|
142
|
+
readonly symbol?: string;
|
|
143
|
+
readonly path: string;
|
|
144
|
+
readonly reason: string;
|
|
145
|
+
};
|
|
146
|
+
readonly position: Position;
|
|
147
|
+
readonly symbol: string;
|
|
148
|
+
readonly message: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* One record per site at which a suppression matched nothing, whatever the number of
|
|
153
|
+
* codes or declarations written there: the first record names the entry, and the message
|
|
154
|
+
* names every code the site's records named and every code the declaration reports
|
|
155
|
+
* instead, so the maintainer corrects the code rather than hunting for it.
|
|
156
|
+
*/
|
|
157
|
+
export function staleSuppressions(
|
|
158
|
+
stale: readonly StaleRecord[],
|
|
159
|
+
files: FileRefs,
|
|
160
|
+
): readonly StaleSuppression[] {
|
|
161
|
+
const bySite = new Map<string, { first: SuppressionRecord; records: StaleRecord[] }>();
|
|
162
|
+
for (const one of stale) {
|
|
163
|
+
const { site, mechanism } = one.record;
|
|
164
|
+
const key = `${mechanism}\u0000${site.path}\u0000${String(site.line)}\u0000${String(site.column)}`;
|
|
165
|
+
const held = bySite.get(key) ?? { first: one.record, records: [] };
|
|
166
|
+
held.records.push(one);
|
|
167
|
+
bySite.set(key, held);
|
|
168
|
+
}
|
|
169
|
+
return [...bySite.values()].map(({ first, records }) => {
|
|
170
|
+
const codes = [...new Set(records.map((one) => one.record.code))].sort();
|
|
171
|
+
const reports = [...new Set(records.flatMap((one) => one.reports))]
|
|
172
|
+
.filter((code) => !codes.includes(code))
|
|
173
|
+
.sort();
|
|
174
|
+
const instead = reports.length === 0 ? "" : `, and its declaration reports ${spelled(reports)}`;
|
|
175
|
+
return {
|
|
176
|
+
code: STALE_SUPPRESSION,
|
|
177
|
+
mechanism: first.mechanism,
|
|
178
|
+
entry: {
|
|
179
|
+
code: first.code,
|
|
180
|
+
...(first.symbol === "" ? {} : { symbol: first.symbol }),
|
|
181
|
+
path: first.path,
|
|
182
|
+
reason: first.reason,
|
|
183
|
+
},
|
|
184
|
+
position: first.site,
|
|
185
|
+
symbol: suppressionRef(first.symbol, first.path, files),
|
|
186
|
+
message: `${WRITTEN[first.mechanism]} for ${spelled(codes)} matches no current finding${instead}`,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* One finding per configured root or pattern that named nothing in any project. A finding
|
|
193
|
+
* names the document that declared the roots, or the conventional repository configuration
|
|
194
|
+
* where no document of the target did, and every finding of one run sits at that
|
|
195
|
+
* document's first position: the roots are an array whose members carry no line of their
|
|
196
|
+
* own, so the string each finding names tells one from another.
|
|
197
|
+
*/
|
|
198
|
+
export function unmatchedRootFindings(
|
|
199
|
+
sources: readonly string[],
|
|
200
|
+
document: string,
|
|
201
|
+
): readonly Finding[] {
|
|
202
|
+
const position = {
|
|
203
|
+
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
204
|
+
line: DOCUMENT_POSITION,
|
|
205
|
+
column: DOCUMENT_POSITION,
|
|
206
|
+
endLine: DOCUMENT_POSITION,
|
|
207
|
+
};
|
|
208
|
+
return sources.map((source) => ({
|
|
209
|
+
code: UNMATCHED_ROOT,
|
|
210
|
+
position,
|
|
211
|
+
symbol: { ref: source, kind: ROOT_SUBJECT, name: source, sizeLines: 1 },
|
|
212
|
+
message:
|
|
213
|
+
source.includes("*") || source.includes("?")
|
|
214
|
+
? "configured root pattern matches no symbol of the inventory"
|
|
215
|
+
: "configured root matches no symbol of the inventory",
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
|
|
221
|
+
* carrying no reason or naming no path, and a configured root that named nothing. A
|
|
222
|
+
* suppression that matched nothing is a record of the report's stale suppressions, which
|
|
223
|
+
* {@link staleSuppressions} builds, and a stale cross-language edge is the merge's to report.
|
|
224
|
+
*/
|
|
225
|
+
export const selfCheck: Emitter = ({ selfCheck: facts, swept }) => [
|
|
226
|
+
...refusedSuppressions(facts.refusals, fileRefsOf(swept.matrix.union.symbols)),
|
|
227
|
+
...unmatchedRootFindings(facts.unmatchedRoots, facts.rootsDocument),
|
|
228
|
+
];
|
|
@@ -3,7 +3,6 @@ import { OUTSIDE, type Graph } from "../graph.ts";
|
|
|
3
3
|
import type { InventorySymbol, SymbolKind } from "../inventory.ts";
|
|
4
4
|
import type { MatrixCandidate } from "../matrix.ts";
|
|
5
5
|
import type { RootKind } from "../roots.ts";
|
|
6
|
-
import type { PendingFinding } from "./boundary.ts";
|
|
7
6
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
8
7
|
import { writeOnlyDeclarations } from "./reads-and-writes.ts";
|
|
9
8
|
|
|
@@ -33,13 +32,6 @@ const WORDS: Partial<Readonly<Record<SymbolKind, string>>> = {
|
|
|
33
32
|
"export-alias": "re-export",
|
|
34
33
|
};
|
|
35
34
|
|
|
36
|
-
/** What the family found: the findings it reports, and those a declared edge holds pending. */
|
|
37
|
-
export interface Narrowings {
|
|
38
|
-
readonly findings: readonly Finding[];
|
|
39
|
-
/** Ordered by edge, then by side. */
|
|
40
|
-
readonly pending: readonly PendingFinding[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
35
|
/** Per declaration of the run, the facts both rules read. */
|
|
44
36
|
interface Facts {
|
|
45
37
|
readonly graph: Graph;
|
|
@@ -236,26 +228,15 @@ function unreachable(
|
|
|
236
228
|
);
|
|
237
229
|
}
|
|
238
230
|
|
|
239
|
-
/** Two strings ordered bytewise. */
|
|
240
|
-
function compare(a: string, b: string): number {
|
|
241
|
-
if (a === b) {
|
|
242
|
-
return 0;
|
|
243
|
-
}
|
|
244
|
-
return a < b ? -1 : 1;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
231
|
/**
|
|
248
|
-
* The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`.
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* file and the package, and whether it is a use is decided on the side in the other
|
|
253
|
-
* language.
|
|
232
|
+
* The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`. A finding
|
|
233
|
+
* about a symbol a declared cross-language edge names is reported here like any other, and
|
|
234
|
+
* the edge's evaluation then holds it pending, because the edge stands for a reference from
|
|
235
|
+
* outside the file and the package whose use is decided in the other language.
|
|
254
236
|
*/
|
|
255
|
-
export function narrowings(input: EmitterInput):
|
|
256
|
-
const { boundary } = input;
|
|
237
|
+
export function narrowings(input: EmitterInput): readonly Finding[] {
|
|
257
238
|
const facts = factsOf(input.swept.matrix.union);
|
|
258
|
-
const { declared, loaded } = boundary.consumers;
|
|
239
|
+
const { declared, loaded } = input.boundary.consumers;
|
|
259
240
|
const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
|
|
260
241
|
const world = { closed, writeOnly: writeOnlyDeclarations(input) };
|
|
261
242
|
const candidates = new Map(
|
|
@@ -263,27 +244,17 @@ export function narrowings(input: EmitterInput): Narrowings {
|
|
|
263
244
|
);
|
|
264
245
|
|
|
265
246
|
const findings: Finding[] = [];
|
|
266
|
-
const pending: PendingFinding[] = [];
|
|
267
247
|
facts.graph.symbols.forEach((symbol, at) => {
|
|
268
248
|
const candidate = candidates.get(symbol.id);
|
|
269
249
|
const found =
|
|
270
250
|
candidate === undefined
|
|
271
251
|
? narrowing(facts, at, symbol, world)
|
|
272
252
|
: unreachable(facts, at, symbol, candidate, input);
|
|
273
|
-
if (found
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const named = boundary.edges.filter((side) => side.symbol === symbol.ref);
|
|
277
|
-
if (named.length === 0) {
|
|
253
|
+
if (found !== undefined) {
|
|
278
254
|
findings.push(found);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
for (const side of named) {
|
|
282
|
-
pending.push({ ...side, finding: found });
|
|
283
255
|
}
|
|
284
256
|
});
|
|
285
|
-
|
|
286
|
-
return { findings, pending };
|
|
257
|
+
return findings;
|
|
287
258
|
}
|
|
288
259
|
|
|
289
260
|
/**
|
|
@@ -303,18 +274,5 @@ export function unreachableExports(input: EmitterInput): ReadonlySet<string> {
|
|
|
303
274
|
return found;
|
|
304
275
|
}
|
|
305
276
|
|
|
306
|
-
/**
|
|
307
|
-
|
|
308
|
-
* declared edge holds pending belongs in that edge's evaluation, which this analyzer
|
|
309
|
-
* does not publish yet, so a run that holds one is refused rather than reported
|
|
310
|
-
* without it.
|
|
311
|
-
*/
|
|
312
|
-
export const visibilityNarrowing: Emitter = (input) => {
|
|
313
|
-
const { findings, pending } = narrowings(input);
|
|
314
|
-
if (pending.length > 0) {
|
|
315
|
-
throw new Error(
|
|
316
|
-
`${String(pending.length)} finding(s) a declared edge holds pending, and no edge evaluation is published`,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
return findings;
|
|
320
|
-
};
|
|
277
|
+
/** The findings of the visibility-narrowing family, `DS1100` to `DS1199`. */
|
|
278
|
+
export const visibilityNarrowing: Emitter = narrowings;
|