@cplieger/deadset-ts 3.12.1 → 4.0.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 +4 -4
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +217 -48
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +56 -7
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/dependencies.js +34 -17
- package/dist/src/deprecation.js +35 -24
- package/dist/src/destructuring.js +37 -29
- package/dist/src/discover.js +105 -28
- package/dist/src/edges.js +4 -1
- package/dist/src/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/completion.js +37 -8
- package/dist/src/findings/emitters.js +20 -6
- package/dist/src/findings/interfaces.js +7 -1
- 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/framework-lifecycle.js +8 -6
- package/dist/src/implementations.js +45 -23
- package/dist/src/interface-satisfaction.js +30 -27
- package/dist/src/intra-function-parts.js +660 -0
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/kinds.js +337 -35
- package/dist/src/partial.js +23 -0
- package/dist/src/query.js +207 -0
- package/dist/src/references.js +92 -20
- package/dist/src/reflective-lookup.js +46 -4
- package/dist/src/report.js +14 -3
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +22 -16
- package/dist/src/reporters/template.js +112 -12
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +53 -32
- package/dist/src/session.js +79 -39
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +29 -7
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +44 -25
- package/dist/src/verbs/analyze.js +39 -14
- package/dist/src/verbs/describe.js +12 -8
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +40 -7
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +385 -104
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +80 -8
- package/src/conformance-record.ts +2 -2
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +177 -33
- package/src/edges.ts +4 -1
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- 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 +27 -6
- package/src/findings/interfaces.ts +7 -1
- 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/framework-lifecycle.ts +10 -6
- package/src/host.ts +5 -3
- package/src/implementations.ts +59 -23
- package/src/interface-satisfaction.ts +30 -27
- package/src/intra-function-parts.ts +889 -0
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/kinds.ts +355 -41
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +56 -6
- package/src/report.ts +32 -4
- package/src/reporters/annotations.ts +23 -21
- package/src/reporters/json.ts +3 -2
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/sarif.ts +27 -18
- package/src/reporters/template.ts +119 -14
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +59 -40
- package/src/session.ts +146 -58
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +40 -8
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +66 -28
- package/src/verbs/analyze.ts +51 -14
- package/src/verbs/describe.ts +13 -9
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +53 -7
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
- package/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
package/src/enum-group.ts
CHANGED
|
@@ -33,7 +33,8 @@ import { aliasChains } from "./alias-chain.ts";
|
|
|
33
33
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
34
34
|
import { nodeKey } from "./inventory.ts";
|
|
35
35
|
import { renderPosition } from "./position.ts";
|
|
36
|
-
import {
|
|
36
|
+
import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
|
|
37
|
+
import type { ProjectView } from "./session.ts";
|
|
37
38
|
|
|
38
39
|
/** The operand types a conversion produces an enum value from. */
|
|
39
40
|
const NUMBER = TypeFlags.NumberLike;
|
|
@@ -146,25 +147,38 @@ function sitesOf(file: SourceFile): readonly Site[] {
|
|
|
146
147
|
* union, and itself otherwise. An enum's own value, or one of its members, is a value the
|
|
147
148
|
* source names, whichever number or string it also is.
|
|
148
149
|
*/
|
|
149
|
-
function partsOf(type: Type): readonly Type[] {
|
|
150
|
-
const parts = type.isUnionType() ?
|
|
151
|
-
return parts
|
|
150
|
+
function partsOf<Brand>(project: ProjectView<Brand>, type: Type): Answer<readonly Type[]> {
|
|
151
|
+
const parts = type.isUnionType() ? project.queries.constituents(type) : [type];
|
|
152
|
+
return isAnswered(parts)
|
|
153
|
+
? parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0)
|
|
154
|
+
: parts;
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
/** The detail a site records whose operand's type the checker could not answer. */
|
|
158
|
+
const UNTYPED_DETAIL = "converted from a value of unanswered type";
|
|
159
|
+
|
|
154
160
|
/**
|
|
155
161
|
* The detail one site records about the enum it produces, or undefined where the
|
|
156
162
|
* operand's type converts nothing: a number, or a string an assertion converts, is
|
|
157
163
|
* converted, a value of type `any` or `unknown` is decoded, and a value of an enum is one
|
|
158
164
|
* the source names.
|
|
159
165
|
*/
|
|
160
|
-
function detailOf(
|
|
166
|
+
function detailOf<Brand>(
|
|
167
|
+
project: ProjectView<Brand>,
|
|
168
|
+
site: Site,
|
|
169
|
+
enumName: string,
|
|
170
|
+
operand: Answer<Type> | undefined,
|
|
171
|
+
): string | undefined {
|
|
161
172
|
if (site.form === "lookup") {
|
|
162
173
|
return `looked up by an element access on ${enumName}`;
|
|
163
174
|
}
|
|
164
175
|
if (operand === undefined) {
|
|
165
176
|
return undefined;
|
|
166
177
|
}
|
|
167
|
-
const parts = partsOf(operand);
|
|
178
|
+
const parts = operand === UNANSWERED ? UNANSWERED : partsOf(project, operand);
|
|
179
|
+
if (parts === UNANSWERED) {
|
|
180
|
+
return UNTYPED_DETAIL;
|
|
181
|
+
}
|
|
168
182
|
if (parts.some((part) => (part.flags & NUMBER) !== 0)) {
|
|
169
183
|
return "converted from number";
|
|
170
184
|
}
|
|
@@ -204,35 +218,37 @@ export function enumGroup<Brand>(input: DetectorInput<Brand>): readonly Evidence
|
|
|
204
218
|
}
|
|
205
219
|
|
|
206
220
|
const sites = files.flatMap(sitesOf);
|
|
207
|
-
const named = new Map<Site, string>();
|
|
221
|
+
const named = new Map<Site, readonly string[]>();
|
|
208
222
|
const chains = aliasChains(project, held);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
223
|
+
// A name the checker could not resolve may stand for any enum of the target it spells.
|
|
224
|
+
const enumsNamed = (text: string): readonly string[] =>
|
|
225
|
+
[...enums].flatMap(([id, name]) => (name === text ? [id] : []));
|
|
226
|
+
project.symbolsAt(sites.map((site) => project.handle(site.name))).forEach((symbol, index) => {
|
|
227
|
+
const site = sites[index];
|
|
228
|
+
if (symbol === undefined || site === undefined) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (symbol === UNANSWERED) {
|
|
232
|
+
const ids = enumsNamed(site.name.getText());
|
|
233
|
+
if (ids.length > 0) {
|
|
234
|
+
named.set(site, ids);
|
|
219
235
|
}
|
|
220
|
-
|
|
221
|
-
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
|
|
239
|
+
if (target !== undefined) {
|
|
240
|
+
named.set(site, [target.id]);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
222
243
|
|
|
223
244
|
const typed = sites.filter((site) => named.has(site) && site.operand !== undefined);
|
|
224
|
-
const operandType = new Map<Site, Type
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (type !== undefined && site !== undefined) {
|
|
232
|
-
operandType.set(site, type);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
}
|
|
245
|
+
const operandType = new Map<Site, Answer<Type>>();
|
|
246
|
+
project.queries.typesAt(typed.map((site) => site.operand ?? site.at)).forEach((type, index) => {
|
|
247
|
+
const site = typed[index];
|
|
248
|
+
if (type !== undefined && site !== undefined) {
|
|
249
|
+
operandType.set(site, type);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
236
252
|
|
|
237
253
|
const members = new Map<string, string[]>();
|
|
238
254
|
for (const symbol of held.symbols) {
|
|
@@ -248,17 +264,15 @@ export function enumGroup<Brand>(input: DetectorInput<Brand>): readonly Evidence
|
|
|
248
264
|
|
|
249
265
|
const found: Evidence[] = [];
|
|
250
266
|
for (const site of sites) {
|
|
251
|
-
const enumId
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
for (const id of members.get(enumId) ?? []) {
|
|
261
|
-
found.push({ id, detail, site: position });
|
|
267
|
+
for (const enumId of named.get(site) ?? []) {
|
|
268
|
+
const detail = detailOf(project, site, enums.get(enumId) ?? "", operandType.get(site));
|
|
269
|
+
if (detail === undefined) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
|
|
273
|
+
for (const id of members.get(enumId) ?? []) {
|
|
274
|
+
found.push({ id, detail, site: position });
|
|
275
|
+
}
|
|
262
276
|
}
|
|
263
277
|
}
|
|
264
278
|
return found;
|
package/src/exempt.ts
CHANGED
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
type TSExemptionClass,
|
|
19
19
|
type TypeScriptVisibility,
|
|
20
20
|
} from "./exempt-classes.ts";
|
|
21
|
-
import type { Inventory, InventorySymbol, Visibility } from "./inventory.ts";
|
|
21
|
+
import type { Inventory, InventorySymbol, SymbolKind, Visibility } from "./inventory.ts";
|
|
22
22
|
import type { Matrix } from "./matrix.ts";
|
|
23
23
|
import { byPosition, positionKey, type Position } from "./position.ts";
|
|
24
|
+
import { Unanswerable } from "./query.ts";
|
|
24
25
|
import type { ProjectView } from "./session.ts";
|
|
25
26
|
import type { Templates } from "./template-field.ts";
|
|
26
27
|
import { sweep, type Exemption, type Mode, type SweepInput } from "./sweep.ts";
|
|
@@ -100,6 +101,53 @@ function retainable(allowed: TypeScriptVisibility, visibility: Visibility | unde
|
|
|
100
101
|
return true;
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The kinds of declaration each class can hold back, which a detector that stops holds
|
|
106
|
+
* back whole. A class with no row here holds back every declaration but a file.
|
|
107
|
+
*/
|
|
108
|
+
const RETAINABLE: ReadonlyMap<TSExemptionClass, ReadonlySet<SymbolKind>> = new Map([
|
|
109
|
+
["interface-satisfaction", new Set<SymbolKind>(["method", "class-member"])],
|
|
110
|
+
["enum-group", new Set<SymbolKind>(["enum-member"])],
|
|
111
|
+
["injection-container", new Set<SymbolKind>(["method", "class-member"])],
|
|
112
|
+
["framework-lifecycle", new Set<SymbolKind>(["method", "class-member"])],
|
|
113
|
+
[
|
|
114
|
+
"serialization-contract",
|
|
115
|
+
new Set<SymbolKind>(["method", "class-member", "interface-method", "type-member"]),
|
|
116
|
+
],
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
/** The detail of a record a detector that could not finish holds. */
|
|
120
|
+
const UNANSWERED_DETAIL = "kept live by a question the checker did not answer";
|
|
121
|
+
|
|
122
|
+
/** Whether one record is held by a detector that could not finish. */
|
|
123
|
+
export function isUnansweredRecord(record: Exemption): boolean {
|
|
124
|
+
return record.detail === UNANSWERED_DETAIL;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* What one detector holds back over one project. A detector that stops because the
|
|
129
|
+
* checker left a question it needs unanswered holds back every declaration of the
|
|
130
|
+
* project its class could hold back, each recorded at its own position, so the gap
|
|
131
|
+
* never yields a finding.
|
|
132
|
+
*/
|
|
133
|
+
function detected<Brand>(
|
|
134
|
+
exemptionClass: TSExemptionClass,
|
|
135
|
+
detect: Detector,
|
|
136
|
+
input: DetectorInput<Brand>,
|
|
137
|
+
): readonly Evidence[] {
|
|
138
|
+
try {
|
|
139
|
+
return detect(input);
|
|
140
|
+
} catch (error: unknown) {
|
|
141
|
+
if (!(error instanceof Unanswerable)) {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
const kinds = RETAINABLE.get(exemptionClass);
|
|
145
|
+
return input.held.symbols
|
|
146
|
+
.filter((symbol) => (kinds === undefined ? symbol.kind !== "file" : kinds.has(symbol.kind)))
|
|
147
|
+
.map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
103
151
|
/**
|
|
104
152
|
* The records one project's detectors found, each class in the vocabulary's order and
|
|
105
153
|
* none of a switched-off class. Evidence on a private member the class's row says it
|
|
@@ -120,7 +168,7 @@ export function computeExemptions<Brand>(
|
|
|
120
168
|
continue;
|
|
121
169
|
}
|
|
122
170
|
const allowed = typescriptVisibilityOf(exemptionClass);
|
|
123
|
-
for (const evidence of detect
|
|
171
|
+
for (const evidence of detected(exemptionClass, detect, input)) {
|
|
124
172
|
if (!retainable(allowed, visibility.get(evidence.id))) {
|
|
125
173
|
continue;
|
|
126
174
|
}
|
package/src/finding.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface FindingDetails {
|
|
|
55
55
|
readonly narrowerVisibility?: "file" | "package";
|
|
56
56
|
/** On `DS1201` and `DS1203`: the classes implementing the interface, in site order. */
|
|
57
57
|
readonly implementations?: readonly PositionedSymbol[];
|
|
58
|
-
/** On `DS1301`: every position the subject is written at, in site order. */
|
|
58
|
+
/** On `DS1301` and `DS1807`: every position the subject is written at, in site order. */
|
|
59
59
|
readonly writePositions?: readonly FindingPosition[];
|
|
60
60
|
/** On `DS1601`: the manifest section that declares the dependency. */
|
|
61
61
|
readonly dependencyClass?: DependencySection;
|
|
@@ -63,6 +63,8 @@ export interface FindingDetails {
|
|
|
63
63
|
readonly mechanism?: Mechanism;
|
|
64
64
|
/** On `DS1701` and `DS1702`: the refused suppression as written, a member absent where it lacks one. */
|
|
65
65
|
readonly entry?: SuppressionEntry;
|
|
66
|
+
/** On the intra-function kinds: the external rules that report the same kind, as the vocabulary lists them. */
|
|
67
|
+
readonly overlap?: readonly string[];
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/** One suppression in the four-member form the ignore file and the baseline share. */
|
|
@@ -91,6 +93,8 @@ export interface FindingComponent {
|
|
|
91
93
|
/** The number of the component's members. */
|
|
92
94
|
readonly symbolCount: number;
|
|
93
95
|
readonly deletableLines: number;
|
|
96
|
+
/** Every member, the subject included, where the run lists a component in full. */
|
|
97
|
+
readonly members?: readonly PositionedSymbol[];
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
/** One finding with every member the finding schema requires. */
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import type { RunSweep } from "../analysis.ts";
|
|
8
8
|
import { componentId, type Component } from "../components.ts";
|
|
9
|
-
import type { Confidence, Severity } from "../config.ts";
|
|
10
|
-
import type { CompletedFinding, Finding, FindingComponent } from "../finding.ts";
|
|
9
|
+
import type { Cascade, Confidence, Severity } from "../config.ts";
|
|
10
|
+
import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
|
|
11
11
|
import { OUTSIDE } from "../graph.ts";
|
|
12
12
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
13
|
+
import { byPosition } from "../position.ts";
|
|
13
14
|
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
14
|
-
import { isRowSubject, type Dials } from "../suppress.ts";
|
|
15
|
+
import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
|
|
15
16
|
import type { Boundary } from "./boundary.ts";
|
|
16
17
|
import type { EmitterInput } from "./emitter.ts";
|
|
17
18
|
|
|
@@ -83,14 +84,42 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
83
84
|
return (id) => (published.has(id) ? outside : "certain");
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
89
|
+
* its members listed in the canonical order where the run lists a component in full.
|
|
90
|
+
*/
|
|
91
|
+
function findingComponents(
|
|
92
|
+
swept: RunSweep,
|
|
93
|
+
cascade: Cascade,
|
|
94
|
+
): (id: string) => FindingComponent | undefined {
|
|
88
95
|
const held = new Map<string, Component>();
|
|
89
96
|
for (const component of swept.sweep.components) {
|
|
90
97
|
for (const member of component.members) {
|
|
91
98
|
held.set(member, component);
|
|
92
99
|
}
|
|
93
100
|
}
|
|
101
|
+
const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
102
|
+
const listed = (component: Component): readonly PositionedSymbol[] =>
|
|
103
|
+
component.members
|
|
104
|
+
.flatMap((member) => {
|
|
105
|
+
const symbol = symbols.get(member);
|
|
106
|
+
return symbol === undefined
|
|
107
|
+
? []
|
|
108
|
+
: [
|
|
109
|
+
{
|
|
110
|
+
ref: symbol.ref,
|
|
111
|
+
name: symbol.name,
|
|
112
|
+
position: {
|
|
113
|
+
...symbol.position,
|
|
114
|
+
endLine: Math.max(symbol.position.line, symbol.endLine),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
})
|
|
119
|
+
.sort(
|
|
120
|
+
(a, b) =>
|
|
121
|
+
byPosition(a.position, b.position) || (a.ref === b.ref ? 0 : a.ref < b.ref ? -1 : 1),
|
|
122
|
+
);
|
|
94
123
|
return (id) => {
|
|
95
124
|
const component = held.get(id);
|
|
96
125
|
return component === undefined
|
|
@@ -100,6 +129,7 @@ function findingComponents(swept: RunSweep): (id: string) => FindingComponent |
|
|
|
100
129
|
root: component.roots.includes(id),
|
|
101
130
|
symbolCount: component.members.length,
|
|
102
131
|
deletableLines: component.deletableLines,
|
|
132
|
+
...(cascade === "full" ? { members: listed(component) } : {}),
|
|
103
133
|
};
|
|
104
134
|
};
|
|
105
135
|
}
|
|
@@ -123,7 +153,7 @@ export function completed(
|
|
|
123
153
|
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
124
154
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
125
155
|
const classOf = reachabilityClasses(input);
|
|
126
|
-
const componentOf = findingComponents(swept);
|
|
156
|
+
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
127
157
|
let minted = swept.sweep.components.length;
|
|
128
158
|
|
|
129
159
|
return findings.map((finding): CompletedFinding => {
|
|
@@ -136,11 +166,15 @@ export function completed(
|
|
|
136
166
|
const at = isRowSubject(finding.symbol.kind)
|
|
137
167
|
? OUTSIDE
|
|
138
168
|
: (positions.get(finding.symbol.ref) ?? OUTSIDE);
|
|
169
|
+
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
170
|
+
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
171
|
+
const part = isPartSubject(finding.symbol.kind);
|
|
139
172
|
const symbol = union.symbols[at];
|
|
140
|
-
const judged = union.subject[at] === true ? symbol : undefined;
|
|
173
|
+
const judged = !part && union.subject[at] === true ? symbol : undefined;
|
|
141
174
|
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
142
|
-
const found = symbol === undefined ? undefined : componentOf(symbol.id);
|
|
143
|
-
|
|
175
|
+
const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
|
|
176
|
+
// A test file's declaration is a component member only as a test of dead code.
|
|
177
|
+
if (candidate !== undefined && found === undefined && union.test[at] !== true) {
|
|
144
178
|
throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
|
|
145
179
|
}
|
|
146
180
|
let component = found;
|
|
@@ -173,7 +207,9 @@ export function completed(
|
|
|
173
207
|
retainedBy: [],
|
|
174
208
|
configurations:
|
|
175
209
|
candidate?.configurations ??
|
|
176
|
-
(judged === undefined
|
|
210
|
+
(judged === undefined && !part
|
|
211
|
+
? swept.matrix.configurations
|
|
212
|
+
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
177
213
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
178
214
|
fixability: row.fixability,
|
|
179
215
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
package/src/findings/emitter.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Dependencies } from "../dependencies.ts";
|
|
|
4
4
|
import type { FileFacts } from "../file-facts.ts";
|
|
5
5
|
import type { Finding } from "../finding.ts";
|
|
6
6
|
import type { Implementations } from "../implementations.ts";
|
|
7
|
+
import type { IntraFunctionFacts } from "../intra-function-parts.ts";
|
|
7
8
|
import type { Stores } from "../stores.ts";
|
|
8
9
|
import type { Boundary } from "./boundary.ts";
|
|
9
10
|
import type { SelfCheckFacts } from "./self-check.ts";
|
|
@@ -30,6 +31,8 @@ export interface EmitterInput {
|
|
|
30
31
|
readonly boundary: Boundary;
|
|
31
32
|
/** The run's suppression outcomes and configured roots, which the self-check family reports. */
|
|
32
33
|
readonly selfCheck: SelfCheckFacts;
|
|
34
|
+
/** The parts of the run's declarations, which the intra-function family reports. */
|
|
35
|
+
readonly intraFunction: IntraFunctionFacts;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
package/src/findings/emitters.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { intraFunction } from "./intra-function.ts";
|
|
|
9
9
|
import { nonCodeArtifacts } from "./non-code-artifacts.ts";
|
|
10
10
|
import { readsAndWrites } from "./reads-and-writes.ts";
|
|
11
11
|
import { selfCheck } from "./self-check.ts";
|
|
12
|
-
import { unusedDeclarations } from "./unused-declarations.ts";
|
|
12
|
+
import { fallenMembers, unusedDeclarations } from "./unused-declarations.ts";
|
|
13
13
|
import { visibilityNarrowing } from "./visibility-narrowing.ts";
|
|
14
14
|
|
|
15
15
|
/** Every kind family this analyzer reports, each by its emitter, in code order. */
|
|
@@ -24,6 +24,22 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
|
|
|
24
24
|
["intra-function", intraFunction],
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The references of every declaration an unanswered question could have kept live, which
|
|
29
|
+
* the run reports nothing about: a finding about one would be computed from the gap.
|
|
30
|
+
*/
|
|
31
|
+
function heldRefs(input: EmitterInput): ReadonlySet<string> {
|
|
32
|
+
const held = new Set(input.swept.heldByUnanswered);
|
|
33
|
+
if (held.size === 0) {
|
|
34
|
+
return held;
|
|
35
|
+
}
|
|
36
|
+
return new Set(
|
|
37
|
+
input.swept.matrix.union.symbols
|
|
38
|
+
.filter((symbol) => held.has(symbol.id))
|
|
39
|
+
.map((symbol) => symbol.ref),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
/** What the suppressions of a run swept under their marks are decided against. */
|
|
28
44
|
export interface Suppressed {
|
|
29
45
|
/** The same run swept with no mark, which is where a kind about liveness reports a marked declaration. */
|
|
@@ -44,11 +60,14 @@ export interface DecidedFindings {
|
|
|
44
60
|
readonly would: readonly CompletedFinding[];
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
/** Every family's findings over one sweep, completed by one step. */
|
|
48
|
-
function completedOver(
|
|
63
|
+
/** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
|
|
64
|
+
function completedOver(
|
|
65
|
+
input: EmitterInput,
|
|
66
|
+
...more: readonly Emitter[]
|
|
67
|
+
): readonly CompletedFinding[] {
|
|
49
68
|
return completed(
|
|
50
69
|
input,
|
|
51
|
-
[...EMITTERS.values()].flatMap((emit) => emit(input)),
|
|
70
|
+
[...EMITTERS.values(), ...more].flatMap((emit) => emit(input)),
|
|
52
71
|
);
|
|
53
72
|
}
|
|
54
73
|
|
|
@@ -85,12 +104,14 @@ export function decidedFindings(
|
|
|
85
104
|
suppressed: Suppressed = { unmarked: input, records: [] },
|
|
86
105
|
): DecidedFindings {
|
|
87
106
|
const found = completedOver(input);
|
|
88
|
-
const unmarked =
|
|
107
|
+
const unmarked =
|
|
108
|
+
suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
|
|
89
109
|
const would = [...unmarked, ...found];
|
|
90
110
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
111
|
+
const unanswered = heldRefs(input);
|
|
91
112
|
const kept = reportable(
|
|
92
113
|
input,
|
|
93
|
-
found.filter((finding) => !ledger.withheld(finding)),
|
|
114
|
+
found.filter((finding) => !ledger.withheld(finding) && !unanswered.has(finding.symbol.ref)),
|
|
94
115
|
);
|
|
95
116
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
96
117
|
return {
|
|
@@ -131,8 +131,14 @@ function unusedMessage(candidate: Candidate): string {
|
|
|
131
131
|
: UNUSED_MESSAGE;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Whether one interface method has implementations and every one has an empty body, or
|
|
136
|
+
* is one whose implementations the run could not read in full.
|
|
137
|
+
*/
|
|
135
138
|
function marker(input: EmitterInput, method: string): boolean {
|
|
139
|
+
if (input.implementations.unknown.has(method)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
136
142
|
const bodies = [...(input.implementations.bodies.get(method)?.values() ?? [])];
|
|
137
143
|
return bodies.length > 0 && bodies.every((empty) => empty);
|
|
138
144
|
}
|
|
@@ -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
|
];
|