@cplieger/deadset-ts 3.12.0 → 3.13.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 +2 -2
- package/dist/src/analysis.js +100 -27
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +36 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/edges.js +4 -1
- package/dist/src/findings/completion.js +37 -8
- package/dist/src/findings/emitters.js +5 -5
- package/dist/src/findings/intra-function.js +91 -2
- package/dist/src/findings/self-check.js +30 -7
- package/dist/src/findings/unused-declarations.js +15 -3
- package/dist/src/findings-pass.js +18 -11
- package/dist/src/graph.js +7 -7
- package/dist/src/intra-function-parts.js +643 -0
- package/dist/src/kinds.js +337 -35
- package/dist/src/reflective-lookup.js +35 -2
- package/dist/src/report.js +11 -2
- package/dist/src/reporters/sarif.js +20 -15
- package/dist/src/reporters/template.js +64 -5
- package/dist/src/roots.js +13 -8
- package/dist/src/serialization-contract.js +41 -22
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +27 -6
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/value-reach.js +33 -9
- package/dist/src/verbs/analyze.js +19 -11
- package/dist/src/verbs/describe.js +12 -8
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +167 -48
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +57 -1
- package/src/conformance-record.ts +2 -2
- package/src/edges.ts +4 -1
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +46 -10
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +9 -5
- package/src/findings/intra-function.ts +106 -3
- package/src/findings/self-check.ts +48 -7
- package/src/findings/unused-declarations.ts +18 -4
- package/src/findings-pass.ts +26 -11
- package/src/graph.ts +7 -7
- package/src/intra-function-parts.ts +871 -0
- package/src/kinds.ts +355 -41
- package/src/reflective-lookup.ts +42 -2
- package/src/report.ts +18 -2
- package/src/reporters/sarif.ts +24 -16
- package/src/reporters/template.ts +69 -6
- package/src/roots.ts +14 -9
- package/src/serialization-contract.ts +47 -30
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +34 -6
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/value-reach.ts +53 -12
- package/src/verbs/analyze.ts +26 -11
- package/src/verbs/describe.ts +13 -9
- package/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
|
@@ -1,4 +1,107 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Finding } from "../finding.ts";
|
|
2
|
+
import { KINDS } from "../kinds.ts";
|
|
3
|
+
import { UNUSED_PARAMETER, UNUSED_RESULT, type PartFact } from "../intra-function-parts.ts";
|
|
4
|
+
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
2
5
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
6
|
+
/** A list of rules as a sentence names them. */
|
|
7
|
+
function spelled(rules: readonly string[]): string {
|
|
8
|
+
if (rules.length < 2) {
|
|
9
|
+
return rules.join("");
|
|
10
|
+
}
|
|
11
|
+
return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The clause a message ends with: the other rules that report the kind, or that none is known. */
|
|
15
|
+
function overlapClause(overlap: readonly string[]): string {
|
|
16
|
+
return overlap.length === 1 && overlap[0] === "none known"
|
|
17
|
+
? "no other rule is known to report it"
|
|
18
|
+
: `also reported by ${spelled(overlap)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether one part is reported. A parameter needs its function's signature free to
|
|
23
|
+
* change; a result needs that, every call in the loaded program to discard it, and every
|
|
24
|
+
* call site to be in the loaded program, so a function with a root or one a module
|
|
25
|
+
* exports to callers the run cannot see has no result to report. Every other part is
|
|
26
|
+
* reported wherever no project found it in use.
|
|
27
|
+
*/
|
|
28
|
+
function reported(
|
|
29
|
+
input: EmitterInput,
|
|
30
|
+
part: PartFact,
|
|
31
|
+
callersUnknown: ReadonlySet<string>,
|
|
32
|
+
): boolean {
|
|
33
|
+
if (part.used) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const { free, discardedEverywhere } = input.intraFunction;
|
|
37
|
+
switch (part.code) {
|
|
38
|
+
case UNUSED_PARAMETER:
|
|
39
|
+
return free.has(part.declaration);
|
|
40
|
+
case UNUSED_RESULT:
|
|
41
|
+
return (
|
|
42
|
+
free.has(part.declaration) &&
|
|
43
|
+
discardedEverywhere.has(part.declaration) &&
|
|
44
|
+
!callersUnknown.has(part.declaration)
|
|
45
|
+
);
|
|
46
|
+
default:
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The findings of the intra-function family, `DS1800` to `DS1899`, each about a part of
|
|
53
|
+
* the declaration its reference names, at the part's own position, whatever the sweep
|
|
54
|
+
* decided about that declaration. Each message names the other rules that report the
|
|
55
|
+
* kind, as the vocabulary lists them, and so does the finding's details.
|
|
56
|
+
*/
|
|
57
|
+
export const intraFunction: Emitter = (input) => {
|
|
58
|
+
const union = input.swept.matrix.union;
|
|
59
|
+
const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
60
|
+
const unknown = new Set(
|
|
61
|
+
union.rooted.flatMap((root) => {
|
|
62
|
+
const symbol = union.symbols[root.at];
|
|
63
|
+
return symbol === undefined ? [] : [symbol.id];
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
const { declared, loaded } = input.boundary.consumers;
|
|
67
|
+
const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
|
|
68
|
+
const published = (id: string): boolean => {
|
|
69
|
+
const symbol = byId.get(id);
|
|
70
|
+
return (
|
|
71
|
+
symbol !== undefined &&
|
|
72
|
+
symbol.kind !== "file" &&
|
|
73
|
+
(symbol.exported || published(symbol.parent))
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
if (!closed) {
|
|
77
|
+
for (const part of input.intraFunction.parts) {
|
|
78
|
+
if (published(part.declaration)) {
|
|
79
|
+
unknown.add(part.declaration);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const found: Finding[] = [];
|
|
84
|
+
for (const part of input.intraFunction.parts) {
|
|
85
|
+
const declaration = byId.get(part.declaration);
|
|
86
|
+
const overlap = KINDS.get(part.code)?.overlap ?? [];
|
|
87
|
+
if (declaration === undefined || !reported(input, part, unknown)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
found.push({
|
|
91
|
+
code: part.code,
|
|
92
|
+
position: part.position,
|
|
93
|
+
symbol: {
|
|
94
|
+
ref: declaration.ref,
|
|
95
|
+
kind: part.kind,
|
|
96
|
+
name: part.name,
|
|
97
|
+
sizeLines: part.position.endLine - part.position.line + 1,
|
|
98
|
+
},
|
|
99
|
+
message: `${part.message}; ${overlapClause(overlap)}`,
|
|
100
|
+
details: {
|
|
101
|
+
...(part.kind === "store" ? { writePositions: [part.position] } : {}),
|
|
102
|
+
overlap,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return found;
|
|
107
|
+
};
|
|
@@ -14,6 +14,12 @@ import type { Emitter } from "./emitter.ts";
|
|
|
14
14
|
/** The code a configured root that matches nothing is reported under. */
|
|
15
15
|
const UNMATCHED_ROOT = "DS1704";
|
|
16
16
|
|
|
17
|
+
/** The code a configured declaration that names nothing is reported under. */
|
|
18
|
+
const UNMATCHED_DECLARATION = "DS1706";
|
|
19
|
+
|
|
20
|
+
/** The subject kind of a finding about a configured declaration. */
|
|
21
|
+
const DECLARATION_SUBJECT = "configured-declaration";
|
|
22
|
+
|
|
17
23
|
/** The subject kind of a finding about a suppression record. */
|
|
18
24
|
const SUPPRESSION_SUBJECT = "suppression";
|
|
19
25
|
|
|
@@ -38,6 +44,18 @@ export interface SelfCheckFacts {
|
|
|
38
44
|
readonly unmatchedRoots: readonly string[];
|
|
39
45
|
/** The document below the target root that declared the roots, empty where none did. */
|
|
40
46
|
readonly rootsDocument: string;
|
|
47
|
+
/** Every configured declaration that named no declaration in any project, in reading order. */
|
|
48
|
+
readonly unmatchedDeclarations: readonly UnmatchedDeclaration[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One configured declaration that named nothing, and the document that configured it. */
|
|
52
|
+
interface UnmatchedDeclaration {
|
|
53
|
+
/** The entry as a finding's subject reference spells it. */
|
|
54
|
+
readonly ref: string;
|
|
55
|
+
/** The dotted path of the key that holds the entry. */
|
|
56
|
+
readonly key: string;
|
|
57
|
+
/** The document below the target root that declared the key, empty where none did. */
|
|
58
|
+
readonly document: string;
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
/** A run with nothing for the self-check family to report. */
|
|
@@ -45,6 +63,7 @@ export const NO_SELF_CHECK: SelfCheckFacts = {
|
|
|
45
63
|
refusals: [],
|
|
46
64
|
unmatchedRoots: [],
|
|
47
65
|
rootsDocument: "",
|
|
66
|
+
unmatchedDeclarations: [],
|
|
48
67
|
};
|
|
49
68
|
|
|
50
69
|
/** The words a message names a mechanism by. */
|
|
@@ -188,6 +207,31 @@ export function staleSuppressions(
|
|
|
188
207
|
});
|
|
189
208
|
}
|
|
190
209
|
|
|
210
|
+
/** The first position of the document that configured a setting, or of the conventional one. */
|
|
211
|
+
function documentPosition(document: string): Finding["position"] {
|
|
212
|
+
return {
|
|
213
|
+
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
214
|
+
line: DOCUMENT_POSITION,
|
|
215
|
+
column: DOCUMENT_POSITION,
|
|
216
|
+
endLine: DOCUMENT_POSITION,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* One finding per configured declaration that named no declaration in any project, at the
|
|
222
|
+
* first position of the document that configured its key, as a configured root's is.
|
|
223
|
+
*/
|
|
224
|
+
function unmatchedDeclarationFindings(
|
|
225
|
+
unmatched: readonly UnmatchedDeclaration[],
|
|
226
|
+
): readonly Finding[] {
|
|
227
|
+
return unmatched.map((one) => ({
|
|
228
|
+
code: UNMATCHED_DECLARATION,
|
|
229
|
+
position: documentPosition(one.document),
|
|
230
|
+
symbol: { ref: one.ref, kind: DECLARATION_SUBJECT, name: one.ref, sizeLines: 1 },
|
|
231
|
+
message: `${one.key} entry names no declaration in any project`,
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
|
|
191
235
|
/**
|
|
192
236
|
* One finding per configured root or pattern that named nothing in any project. A finding
|
|
193
237
|
* names the document that declared the roots, or the conventional repository configuration
|
|
@@ -199,12 +243,7 @@ export function unmatchedRootFindings(
|
|
|
199
243
|
sources: readonly string[],
|
|
200
244
|
document: string,
|
|
201
245
|
): readonly Finding[] {
|
|
202
|
-
const position =
|
|
203
|
-
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
204
|
-
line: DOCUMENT_POSITION,
|
|
205
|
-
column: DOCUMENT_POSITION,
|
|
206
|
-
endLine: DOCUMENT_POSITION,
|
|
207
|
-
};
|
|
246
|
+
const position = documentPosition(document);
|
|
208
247
|
return sources.map((source) => ({
|
|
209
248
|
code: UNMATCHED_ROOT,
|
|
210
249
|
position,
|
|
@@ -218,11 +257,13 @@ export function unmatchedRootFindings(
|
|
|
218
257
|
|
|
219
258
|
/**
|
|
220
259
|
* 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
|
|
260
|
+
* carrying no reason or naming no path, and a configured root or declaration that named
|
|
261
|
+
* nothing. A
|
|
222
262
|
* suppression that matched nothing is a record of the report's stale suppressions, which
|
|
223
263
|
* {@link staleSuppressions} builds, and a stale cross-language edge is the merge's to report.
|
|
224
264
|
*/
|
|
225
265
|
export const selfCheck: Emitter = ({ selfCheck: facts, swept }) => [
|
|
226
266
|
...refusedSuppressions(facts.refusals, fileRefsOf(swept.matrix.union.symbols)),
|
|
227
267
|
...unmatchedRootFindings(facts.unmatchedRoots, facts.rootsDocument),
|
|
268
|
+
...unmatchedDeclarationFindings(facts.unmatchedDeclarations),
|
|
228
269
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Finding } from "../finding.ts";
|
|
2
2
|
import type { InventorySymbol, SymbolKind } from "../inventory.ts";
|
|
3
3
|
import type { MatrixCandidate } from "../matrix.ts";
|
|
4
|
-
import type { Emitter } from "./emitter.ts";
|
|
4
|
+
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
5
5
|
import { unreachableExports } from "./visibility-narrowing.ts";
|
|
6
6
|
|
|
7
7
|
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
@@ -137,7 +137,17 @@ function messageOf(code: string, judged: Judged): string {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
140
|
-
export const unusedDeclarations: Emitter = (input) =>
|
|
140
|
+
export const unusedDeclarations: Emitter = (input) => familyFindings(input, false);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The finding each member of a dead container would carry were its container live: what
|
|
144
|
+
* a record on such a member is matched against, since the report carries the container's
|
|
145
|
+
* finding rather than one of the member's own.
|
|
146
|
+
*/
|
|
147
|
+
export const fallenMembers: Emitter = (input) => familyFindings(input, true);
|
|
148
|
+
|
|
149
|
+
/** The family's findings, or, where `fallen` is set, its dead containers' members' alone. */
|
|
150
|
+
function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
141
151
|
const union = input.swept.matrix.union;
|
|
142
152
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
143
153
|
const unreachable = unreachableExports(input);
|
|
@@ -149,11 +159,15 @@ export const unusedDeclarations: Emitter = (input) => {
|
|
|
149
159
|
if (symbol === undefined) {
|
|
150
160
|
continue;
|
|
151
161
|
}
|
|
162
|
+
const containerDead = dead.has(symbol.parent);
|
|
163
|
+
if (fallen !== containerDead) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
152
166
|
const judged: Judged = {
|
|
153
167
|
candidate,
|
|
154
168
|
symbol,
|
|
155
169
|
test: union.test[at] === true,
|
|
156
|
-
containerDead:
|
|
170
|
+
containerDead: containerDead && !fallen,
|
|
157
171
|
deprecated: input.deprecated.has(symbol.id),
|
|
158
172
|
unreachable: unreachable.has(symbol.id),
|
|
159
173
|
};
|
|
@@ -175,4 +189,4 @@ export const unusedDeclarations: Emitter = (input) => {
|
|
|
175
189
|
});
|
|
176
190
|
}
|
|
177
191
|
return found;
|
|
178
|
-
}
|
|
192
|
+
}
|
package/src/findings-pass.ts
CHANGED
|
@@ -122,19 +122,34 @@ export function verdictOf(result: PassResult, config: Config): number {
|
|
|
122
122
|
return failing ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** What a baseline row is recorded from: a finding's code, subject and file. */
|
|
126
|
+
export interface RecordableFinding {
|
|
127
|
+
readonly code: string;
|
|
128
|
+
readonly symbol: { readonly ref: string; readonly kind: string };
|
|
129
|
+
readonly position: { readonly path: string };
|
|
130
|
+
}
|
|
131
|
+
|
|
125
132
|
/**
|
|
126
|
-
* The
|
|
127
|
-
* row of a document is not recorded: no record withholds one, so a row
|
|
128
|
-
* be stale on the first read back.
|
|
133
|
+
* The rows one round of a baseline write records from its findings, in their order. A
|
|
134
|
+
* finding about a row of a document is not recorded: no record withholds one, so a row
|
|
135
|
+
* recording it would be stale on the first read back. Two findings that would record the
|
|
136
|
+
* same row, two parts of one declaration under one code, record it once.
|
|
129
137
|
*/
|
|
130
|
-
export function recordedFindings(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
export function recordedFindings(findings: readonly RecordableFinding[]): readonly Recorded[] {
|
|
139
|
+
const rows = new Map<string, Recorded>();
|
|
140
|
+
for (const finding of findings) {
|
|
141
|
+
if (isRowSubject(finding.symbol.kind)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const row = { code: finding.code, symbol: finding.symbol.ref, path: finding.position.path };
|
|
145
|
+
rows.set(rowKey(row), rows.get(rowKey(row)) ?? row);
|
|
146
|
+
}
|
|
147
|
+
return [...rows.values()];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** One row's identity: its code, its symbol and its path. */
|
|
151
|
+
export function rowKey(row: Recorded): string {
|
|
152
|
+
return `${row.code}\u0000${row.symbol}\u0000${row.path}`;
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
/** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
|
package/src/graph.ts
CHANGED
|
@@ -39,15 +39,15 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
42
|
-
* tool reads the configuration it loads, a runtime runs a test file, a setup file
|
|
43
|
-
* worker
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* tool reads the configuration it loads, a runtime runs a test file, a setup file, a
|
|
43
|
+
* worker and a command, the manifest names what enters the package, and an entry file, a
|
|
44
|
+
* configured root and a pattern assert a caller. A library's published API only supposes
|
|
45
|
+
* a consumer the run may not see, so it is live under reachability alone.
|
|
46
46
|
*/
|
|
47
47
|
const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
|
|
48
|
-
"entry-file":
|
|
49
|
-
"manifest-entry":
|
|
50
|
-
"manifest-binary":
|
|
48
|
+
"entry-file": true,
|
|
49
|
+
"manifest-entry": true,
|
|
50
|
+
"manifest-binary": true,
|
|
51
51
|
"published-api": false,
|
|
52
52
|
configured: true,
|
|
53
53
|
pattern: true,
|