@cplieger/deadset-ts 8.0.0-dev.21 → 8.0.0-dev.22
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/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +1 -1
- package/dist/src/findings/intra-function.js +50 -37
- package/dist/src/intra-function-parts.js +1149 -222
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +1 -1
- package/src/findings/intra-function.ts +59 -37
- package/src/intra-function-parts.ts +1435 -247
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "8.0.0-dev.
|
|
3
|
+
"version": "8.0.0-dev.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"lint:fix": "eslint . --fix && prettier --write ."
|
|
33
33
|
},
|
|
34
34
|
"cplieger": {
|
|
35
|
-
"vitestShards":
|
|
35
|
+
"vitestShards": 3
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@typescript/native": "npm:typescript@7.1.0-dev.20261103.1"
|
|
@@ -4,7 +4,7 @@ export const RECORD = {
|
|
|
4
4
|
conformance: {
|
|
5
5
|
corpusVersion: "6.1.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:fcfdf88a7d45e1ac96545ef10c8427462260f4e81641d4d808b80086256bb6b7",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|
|
@@ -313,7 +313,7 @@ export const KIND_ROWS = [
|
|
|
313
313
|
holdsLive: false,
|
|
314
314
|
rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
|
|
315
315
|
precondition:
|
|
316
|
-
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, is not used as a value, is not a go:linkname or cgo target, is not a go-fuzz entry point, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. In Go a function literal that is the callee of the call it is written in, the call of a `go` or a `defer` statement included, is not used as a value, wherever the literal is written. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported for deletion when its body reads no parameter after it, and an unread parameter before the last read one is reported for a rename to its name with a leading underscore, which keeps every argument in place and which the compilers' unused-parameter rules read as declared unread, unless its name already starts with one. A parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type, and that carries a type annotation, is not reported for deletion where the inference of the call holding the function may read the annotation, being where the callee is generic and its parameter type at the function's place names a type parameter at the parameter's place, or where the function is written inside an object literal such a call is passed, and it is reported for deletion at `probable` where it carries an annotation no such inference reads, because nothing in the program shows that an outside reader ignores it. A name an object binding pattern of a parameter of such a function binds, annotated or not, that the body never reads is reported for deletion at `probable`, because the pattern reads the property it names from the argument and the code that hands the argument may act on that read, which nothing in the program shows. Any other TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. A TypeScript constructor is used as a value where its class is used as a value other than as the callee of a `new` expression or the class an `extends` clause names, and its signature is not free where its class carries a decorator or a parameter of the constructor carries one, because the code that decorator or a container runs may construct the class. A parameter property is read by the write to its member the constructor makes, so it is never reported under this kind, and its member is judged under DS1301. A TypeScript name a parameter binds, whole or through a binding pattern, that starts with an underscore is never reported, because a leading underscore is the spelling unused-parameter rules read as declared unread, and no other name exempts a parameter. A Go parameter named by the blank identifier declares no name a body could read and is never reported.",
|
|
316
|
+
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, is not used as a value, is not a go:linkname or cgo target, is not a go-fuzz entry point, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. In Go a function literal that is the callee of the call it is written in, the call of a `go` or a `defer` statement included, is not used as a value, wherever the literal is written. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported for deletion when its body reads no parameter after it, and an unread parameter before the last read one is reported for a rename to its name with a leading underscore, which keeps every argument in place and which the compilers' unused-parameter rules read as declared unread, unless its name already starts with one. A function's `length` counts each parameter before the first one with a default or a rest, and code holding the function may read it, so in TypeScript an unread parameter that `length` counts is reported at `probable` where the function is used as a value or is a root, which a library's published API is and which code outside the program calls, and a function whose `length` the program reads through its own name reports no parameter. A parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type, and that carries a type annotation, is not reported for deletion where the inference of the call holding the function may read the annotation, being where the callee is generic and its parameter type at the function's place names a type parameter at the parameter's place, or where the function is written inside an object literal such a call is passed, and it is reported for deletion at `probable` where it carries an annotation no such inference reads, because nothing in the program shows that an outside reader ignores it. A name an object binding pattern of a parameter of such a function binds, annotated or not, that the body never reads is reported for deletion at `probable`, because the pattern reads the property it names from the argument and the code that hands the argument may act on that read, which nothing in the program shows. Any other TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. A TypeScript constructor is used as a value where its class is used as a value other than as the callee of a `new` expression or the class an `extends` clause names, and its signature is not free where its class carries a decorator or a parameter of the constructor carries one, because the code that decorator or a container runs may construct the class. A parameter property is read by the write to its member the constructor makes, so it is never reported under this kind, and its member is judged under DS1301. A TypeScript name a parameter binds, whole or through a binding pattern, that starts with an underscore is never reported, because a leading underscore is the spelling unused-parameter rules read as declared unread, and no other name exempts a parameter. A Go parameter named by the blank identifier declares no name a body could read and is never reported.",
|
|
317
317
|
details: { overlap: "required" },
|
|
318
318
|
defaultEnabled: true,
|
|
319
319
|
defaultSeverity: "warn",
|
|
@@ -2,10 +2,25 @@ import { CODE, type Confidence } from "../contract/contract.generated.ts";
|
|
|
2
2
|
import type { Finding } from "../finding.ts";
|
|
3
3
|
import { KINDS } from "../kinds.ts";
|
|
4
4
|
import { parameterRef } from "../ref.ts";
|
|
5
|
-
import {
|
|
5
|
+
import type { PartFact, PartKind } from "../intra-function-parts.ts";
|
|
6
6
|
import { reachedWithTheTests } from "../liveness/sweep.ts";
|
|
7
7
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
8
8
|
|
|
9
|
+
/** The code each part kind is reported under, and what its finding says before the overlap clause. */
|
|
10
|
+
const PART_KINDS: Readonly<Record<PartKind, { code: string; says: (name: string) => string }>> = {
|
|
11
|
+
parameter: {
|
|
12
|
+
code: CODE.unusedParameter,
|
|
13
|
+
says: (name) => `parameter ${name} is never read in the body`,
|
|
14
|
+
},
|
|
15
|
+
result: { code: CODE.unusedResult, says: () => "result is discarded at every call site" },
|
|
16
|
+
store: { code: CODE.deadStore, says: (name) => `value written to ${name} is never read` },
|
|
17
|
+
statement: { code: CODE.unreachableStatement, says: () => "statement is unreachable" },
|
|
18
|
+
case: {
|
|
19
|
+
code: CODE.unreachableCase,
|
|
20
|
+
says: (name) => `case ${name} is covered by an earlier case of the same switch`,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
9
24
|
/** A list of rules as a sentence names them. */
|
|
10
25
|
function spelled(rules: readonly string[]): string {
|
|
11
26
|
if (rules.length < 2) {
|
|
@@ -38,21 +53,29 @@ type ParameterVerdict =
|
|
|
38
53
|
| undefined;
|
|
39
54
|
|
|
40
55
|
/**
|
|
41
|
-
* Whether one unread parameter is reported, and how. Of a free
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
56
|
+
* Whether one unread parameter is reported, and how (DS1401's precondition). Of a free
|
|
57
|
+
* signature it is deleted. Of a function used as a value, one a read parameter follows is
|
|
58
|
+
* renamed to an underscore name, and any other is deleted, kept where the call's inference
|
|
59
|
+
* may read its annotation. A deletion is `probable` where an unread annotation or a
|
|
60
|
+
* contextual pattern may matter to the caller, or where `length` counts the parameter of
|
|
61
|
+
* a function used as a value or of a root, whose holder may count it.
|
|
47
62
|
*/
|
|
48
63
|
function parameterVerdict(
|
|
49
64
|
part: PartFact,
|
|
50
65
|
free: ReadonlySet<string>,
|
|
51
66
|
freeButValued: ReadonlySet<string>,
|
|
67
|
+
rooted: ReadonlySet<string>,
|
|
52
68
|
): ParameterVerdict {
|
|
53
69
|
const place = part.parameter;
|
|
70
|
+
const counted = place?.counted === true;
|
|
54
71
|
if (place?.inValue !== true && free.has(part.declaration)) {
|
|
55
|
-
|
|
72
|
+
if (place?.annotated !== undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
fix: "delete",
|
|
77
|
+
confidence: counted && rooted.has(part.declaration) ? "probable" : "certain",
|
|
78
|
+
};
|
|
56
79
|
}
|
|
57
80
|
const valued = place?.inValue === true || freeButValued.has(part.declaration);
|
|
58
81
|
if (!valued) {
|
|
@@ -64,7 +87,7 @@ function parameterVerdict(
|
|
|
64
87
|
if (place?.annotated === "steers") {
|
|
65
88
|
return undefined;
|
|
66
89
|
}
|
|
67
|
-
const unseen = place?.annotated === "free" || place?.contextualPattern === true;
|
|
90
|
+
const unseen = place?.annotated === "free" || place?.contextualPattern === true || counted;
|
|
68
91
|
return { fix: "delete", confidence: unseen ? "probable" : "certain" };
|
|
69
92
|
}
|
|
70
93
|
|
|
@@ -78,16 +101,14 @@ function parameterVerdict(
|
|
|
78
101
|
function reported(
|
|
79
102
|
input: EmitterInput,
|
|
80
103
|
part: PartFact,
|
|
104
|
+
rooted: ReadonlySet<string>,
|
|
81
105
|
callersUnknown: ReadonlySet<string>,
|
|
82
106
|
): boolean {
|
|
83
|
-
if (part.used) {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
107
|
const { free, freeButValued, discardedEverywhere } = input.intraFunction;
|
|
87
|
-
switch (part.
|
|
88
|
-
case
|
|
89
|
-
return parameterVerdict(part, free, freeButValued) !== undefined;
|
|
90
|
-
case
|
|
108
|
+
switch (part.kind) {
|
|
109
|
+
case "parameter":
|
|
110
|
+
return parameterVerdict(part, free, freeButValued, rooted) !== undefined;
|
|
111
|
+
case "result":
|
|
91
112
|
return (
|
|
92
113
|
free.has(part.declaration) &&
|
|
93
114
|
discardedEverywhere.has(part.declaration) &&
|
|
@@ -120,49 +141,50 @@ export const intraFunction: Emitter = (input) => {
|
|
|
120
141
|
const testCode = union.test[at] === true || union.support[at] === true;
|
|
121
142
|
return !testCode || (reached[at] === true && !testsOfDeadCode.has(id));
|
|
122
143
|
};
|
|
123
|
-
const
|
|
144
|
+
const rooted: ReadonlySet<string> = new Set(
|
|
124
145
|
union.rooted.flatMap((root) => {
|
|
125
146
|
const symbol = union.symbols[root.at];
|
|
126
147
|
return symbol === undefined ? [] : [symbol.id];
|
|
127
148
|
}),
|
|
128
149
|
);
|
|
150
|
+
const unknown = new Set(rooted);
|
|
129
151
|
const { declared, loaded } = input.boundary.consumers;
|
|
130
152
|
const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
|
|
131
|
-
const published = (id: string): boolean => {
|
|
132
|
-
const symbol = byId.get(id);
|
|
133
|
-
return (
|
|
134
|
-
symbol !== undefined &&
|
|
135
|
-
symbol.kind !== "file" &&
|
|
136
|
-
(symbol.exported || published(symbol.parent))
|
|
137
|
-
);
|
|
138
|
-
};
|
|
139
153
|
if (!closed) {
|
|
140
|
-
for (const
|
|
141
|
-
|
|
142
|
-
unknown.add(part.declaration);
|
|
143
|
-
}
|
|
154
|
+
for (const id of input.intraFunction.published) {
|
|
155
|
+
unknown.add(id);
|
|
144
156
|
}
|
|
145
157
|
}
|
|
146
158
|
const found: Finding[] = [];
|
|
147
159
|
for (const part of input.intraFunction.parts) {
|
|
148
160
|
const declaration = byId.get(part.declaration);
|
|
149
|
-
const
|
|
150
|
-
|
|
161
|
+
const { code, says } = PART_KINDS[part.kind];
|
|
162
|
+
const overlap = KINDS.get(code)?.overlap?.ts ?? [];
|
|
163
|
+
if (
|
|
164
|
+
declaration === undefined ||
|
|
165
|
+
!judged(declaration.id) ||
|
|
166
|
+
!reported(input, part, rooted, unknown)
|
|
167
|
+
) {
|
|
151
168
|
continue;
|
|
152
169
|
}
|
|
153
170
|
const verdict =
|
|
154
|
-
part.
|
|
155
|
-
? parameterVerdict(
|
|
171
|
+
part.kind === "parameter"
|
|
172
|
+
? parameterVerdict(
|
|
173
|
+
part,
|
|
174
|
+
input.intraFunction.free,
|
|
175
|
+
input.intraFunction.freeButValued,
|
|
176
|
+
rooted,
|
|
177
|
+
)
|
|
156
178
|
: undefined;
|
|
157
179
|
const message =
|
|
158
180
|
verdict?.fix === "rename"
|
|
159
|
-
? `${part.
|
|
160
|
-
: part.
|
|
181
|
+
? `${says(part.name)}, and a read parameter follows it, so rename it to _${part.name}`
|
|
182
|
+
: says(part.name);
|
|
161
183
|
found.push({
|
|
162
|
-
code
|
|
184
|
+
code,
|
|
163
185
|
position: part.position,
|
|
164
186
|
symbol: {
|
|
165
|
-
...(part.
|
|
187
|
+
...(part.kind === "parameter"
|
|
166
188
|
? { ref: parameterRef(declaration.ref, part.name), holder: declaration.ref }
|
|
167
189
|
: { ref: declaration.ref }),
|
|
168
190
|
kind: part.kind,
|