@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/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.21",
3
+ "version": "8.0.0-dev.22",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.21",
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": 2
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:352b870acbc05213d4869bc18611137d0782cc0c9a8379e7a998a3d9cea2d966",
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 { type PartFact } from "../intra-function-parts.ts";
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 signature it is deleted. A
42
- * caller may pass a function used as a value more arguments than it declares, so there one
43
- * a read parameter follows is renamed to an underscore name, which the compilers read as
44
- * declared unread, and one no read parameter follows is deleted: kept where the call's
45
- * inference may read its annotation, at `probable` where nothing reads the annotation or
46
- * where it is a name a contextually typed object pattern binds.
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
- return place?.annotated === undefined ? { fix: "delete", confidence: "certain" } : undefined;
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.code) {
88
- case CODE.unusedParameter:
89
- return parameterVerdict(part, free, freeButValued) !== undefined;
90
- case CODE.unusedResult:
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 unknown = new Set(
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 part of input.intraFunction.parts) {
141
- if (published(part.declaration)) {
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 overlap = KINDS.get(part.code)?.overlap?.ts ?? [];
150
- if (declaration === undefined || !judged(declaration.id) || !reported(input, part, unknown)) {
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.code === CODE.unusedParameter
155
- ? parameterVerdict(part, input.intraFunction.free, input.intraFunction.freeButValued)
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.message}, and a read parameter follows it, so rename it to _${part.name}`
160
- : part.message;
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: part.code,
184
+ code,
163
185
  position: part.position,
164
186
  symbol: {
165
- ...(part.code === CODE.unusedParameter
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,