@cplieger/deadset-ts 4.0.0 → 4.0.1
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 +67 -49
- package/dist/src/analysis.js +1 -1
- package/dist/src/discover.js +1 -1
- package/dist/src/entry-point-gaps.js +2 -2
- package/dist/src/reporters/template.js +3 -3
- package/dist/src/schema.js +2 -2
- package/dist/src/verbs/print-projects.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +1 -1
- package/src/discover.ts +1 -1
- package/src/entry-point-gaps.ts +2 -2
- package/src/reporters/template.ts +3 -3
- package/src/schema.ts +2 -2
- package/src/verbs/print-projects.ts +1 -1
package/README.md
CHANGED
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@cplieger/deadset-ts) [](https://jsr.io/@cplieger/deadset-ts)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
deadset-ts finds dead code in TypeScript projects, down to class members, type members and dead stores inside functions, and writes a report your CI can fail on.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
It reads your `tsconfig.json` projects, `allowJs` JavaScript included, with the type information of TypeScript 7.1.0-dev.20261003.1, its one dependency. Your project keeps its own TypeScript version and must also compile under that version. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Why use it
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
deadset-ts is built for a CI gate on dead code. Each finding names code to delete, an `export` to drop or a setting to fix.
|
|
12
|
+
|
|
13
|
+
- It reports unused exports, files and dependencies, and unused members of classes, interfaces, type aliases, enums and namespaces, private and static included.
|
|
14
|
+
- Inside a function, it reports unused parameters and results, unreachable statements and cases, and dead stores.
|
|
15
|
+
- It reports an `export` only its own file uses, and declarations that only reference each other.
|
|
16
|
+
- It keeps what decorators, reflective lookups, `JSON.stringify` and the frameworks, containers, serializers and templates you name in `deadset.json` use.
|
|
17
|
+
- The same tree gives the same report, and a baseline keyed on code and symbol survives moved lines.
|
|
18
|
+
- It writes text, JSON, GitHub annotations, SARIF 2.1.0 or a custom template.
|
|
19
|
+
|
|
20
|
+
Consider [Knip](https://knip.dev) if you want issues fixed with `--fix`, plugins for over 100 tools, or Astro, MDX, Svelte and Vue files checked.
|
|
12
21
|
|
|
13
22
|
## Install
|
|
14
23
|
|
|
@@ -20,70 +29,79 @@ npx jsr add -D @cplieger/deadset-ts
|
|
|
20
29
|
npx @cplieger/deadset-ts version
|
|
21
30
|
```
|
|
22
31
|
|
|
23
|
-
The npm package carries the command and the library as JavaScript that the compiler emits at publish, so `npx deadset-ts` and `import("@cplieger/deadset-ts")` both run on plain Node.js 24 or later, with no loader and no flag. It carries the TypeScript source as well, which is what a TypeScript consumer's own compiler resolves. That consumer sets `allowImportingTsExtensions`, because the source names each import with its `.ts` extension. The JSR package ships source only, and Deno compiles it: a Deno program imports and type-checks the library, but the compiler's synchronous session does not open under Deno, so running an analysis needs Node.
|
|
24
|
-
|
|
25
32
|
## Usage
|
|
26
33
|
|
|
34
|
+
Create a `deadset.json` at the project root that says whether the project is an `application` or a `library`, then run `analyze`. A run reads every `tsconfig` project under that folder, project references included, and reports a declaration only when it is dead in all of them.
|
|
35
|
+
|
|
27
36
|
```sh
|
|
28
|
-
|
|
37
|
+
echo '{ "target": { "kind": "application" } }' > deadset.json
|
|
38
|
+
npx @cplieger/deadset-ts analyze --report=deadset-report.json
|
|
29
39
|
```
|
|
30
40
|
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
`analyze` writes the JSON report to `deadset-report.json` and a text rendering beside it as `deadset-report.json.txt`. Take this `src/greet.ts`, where the rest of the project calls only `greet` and `Counter.increment`:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export function greet(name: string): string {
|
|
45
|
+
return `hello ${name}`;
|
|
46
|
+
}
|
|
47
|
+
export function farewell(name: string): string {
|
|
48
|
+
return `bye ${name}`;
|
|
49
|
+
}
|
|
50
|
+
export class Counter {
|
|
51
|
+
private count = 0;
|
|
52
|
+
increment(): void {
|
|
53
|
+
this.count += 1;
|
|
54
|
+
}
|
|
55
|
+
reset(): void {
|
|
56
|
+
this.count = 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
The text rendering lists three findings:
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
```text
|
|
64
|
+
src/greet.ts:4:17: function farewell: exported function has no reference in the target and none from any loaded consumer [certain] (DS1001)
|
|
65
|
+
src/greet.ts:8:11: class-member Counter.count: member Counter.count is written at 2 positions and never read [certain] (DS1301)
|
|
66
|
+
src/greet.ts:12:3: method Counter.reset: method has no reference in the target [certain] (DS1003)
|
|
67
|
+
summary: 3 findings (0 allow, 0 warn, 3 deny), 6 deletable lines, 0 suppressions in effect, 0 reasons recorded, 0 stale suppressions, 0 pending, 0 omitted
|
|
68
|
+
```
|
|
53
69
|
|
|
54
|
-
The
|
|
70
|
+
The run exits 1 because the report holds a finding at `deny` severity, and 0 when it holds none. Three common next steps:
|
|
55
71
|
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
-
|
|
72
|
+
- Add `--format=sarif` or `--format=github` for a SARIF file or GitHub workflow annotations. Naming any format turns off the default text rendering, so add `--format=text` as well to keep it.
|
|
73
|
+
- Run `npx @cplieger/deadset-ts explain --why=farewell` to see why one declaration is reported, retained, held by an unanswered question, live or dead.
|
|
74
|
+
- Add `--baseline-write=deadset-baseline.json` to record today's findings. Later runs fail on a new finding and on a baseline row whose finding has gone. To keep one declaration, put `// deadset:ignore DS1001 -- <reason>` on the line above it.
|
|
59
75
|
|
|
60
|
-
|
|
76
|
+
[Commands and exit codes](docs/commands.md) covers every verb and option, and [Configuration](docs/configuration.md) covers every setting.
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
- `DirectoryEntry`, `PathKind`: one entry of a directory read, and what one path names.
|
|
78
|
+
## API
|
|
64
79
|
|
|
65
|
-
|
|
80
|
+
The package exports the command line as a function, and the helpers that spell the positions and symbol references in a report.
|
|
66
81
|
|
|
67
|
-
- `
|
|
68
|
-
- `
|
|
69
|
-
- `
|
|
70
|
-
- `
|
|
82
|
+
- `run` runs the command line and returns its exit code, without exiting the process. `Writer` is its output stream, and `SETTING_OPTIONS` maps each option to the setting it sets.
|
|
83
|
+
- `Host`, `DirectoryEntry` and `PathKind` describe the filesystem, working directory and version a run reads.
|
|
84
|
+
- `renderPosition`, `positionKey`, `byPosition`, `PositionError` and `Position` render and order positions.
|
|
85
|
+
- `renderRef`, `nameComponent`, `computedComponent`, `isRef`, `REF_EXPRESSIONS`, `Module`, `Component`, `Fragment` and `DependencySection` spell stable symbol references.
|
|
86
|
+
- `DECLINED_CONVENTIONS` and `DeclinedConvention` list the entry-point conventions of other tools the analysis does not read.
|
|
87
|
+
- `CONTRACT_VERSION` is the version of the [deadset Contract](https://github.com/cplieger/deadset-spec) this analyzer implements.
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
A TypeScript caller that imports the source sets `allowImportingTsExtensions`, because each import in it ends in `.ts`. [Using deadset-ts as a library](docs/library.md) covers the packages and in-process runs, and [JSR](https://jsr.io/@cplieger/deadset-ts/doc) has the full reference.
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
- `nameComponent(name)`, `computedComponent(text)`: the canonical spelling of one component of that reference, from a declared name or from a computed key.
|
|
76
|
-
- `isRef(value)`: whether one string is a reference of this language's grammar.
|
|
77
|
-
- `REF_EXPRESSIONS`: one regular expression per form the grammar defines.
|
|
78
|
-
- `Module`, `Component`, `Fragment`, `DependencySection`: the module half of a reference, one component of its fragment, what the fragment names, and the manifest section a dependency was declared in.
|
|
91
|
+
## Related projects
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset-spec), which fixes the issue codes, the report schema and the exit codes. It passes the Contract's conformance corpus except one reflective-lookup fixture, the gap [`conformance.json`](conformance.json) declares.
|
|
81
94
|
|
|
82
|
-
-
|
|
95
|
+
- [deadset-go](https://github.com/cplieger/deadset-go) is the same analysis for Go modules.
|
|
96
|
+
- [deadset](https://github.com/cplieger/deadset) runs both analyzers as one command and merges their reports, resolving the edges between Go and TypeScript code.
|
|
83
97
|
|
|
84
|
-
|
|
98
|
+
## Documentation
|
|
85
99
|
|
|
86
|
-
-
|
|
100
|
+
- [Commands and exit codes](docs/commands.md) lists every verb, option, rendering and exit code.
|
|
101
|
+
- [Configuration](docs/configuration.md) lists every setting, the suppression files and the scope document.
|
|
102
|
+
- [How deadset-ts works](docs/how-it-works.md) explains the projects it loads, the roots it starts from and what it holds back.
|
|
103
|
+
- [Using deadset-ts as a library](docs/library.md) covers the npm and JSR packages and running the analyzer in-process.
|
|
104
|
+
- [Batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size of the reference pass.
|
|
87
105
|
|
|
88
106
|
## Contributing
|
|
89
107
|
|
package/dist/src/analysis.js
CHANGED
|
@@ -129,7 +129,7 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
129
129
|
dropped(configFile, "the compiler opened no project for the configuration");
|
|
130
130
|
}
|
|
131
131
|
if (failures.length > 0) {
|
|
132
|
-
throw new DiscoveryError(`${String(failures.length)} error(s)
|
|
132
|
+
throw new DiscoveryError(`${String(failures.length)} error(s), so no answer was produced`, failures);
|
|
133
133
|
}
|
|
134
134
|
const answers = session.projects.filter((answer) => answer !== undefined);
|
|
135
135
|
const [first] = notBuilt;
|
package/dist/src/discover.js
CHANGED
|
@@ -291,7 +291,7 @@ function declaredProjects(engine, host, targetRoot, entries) {
|
|
|
291
291
|
const unnamed = referencesOf(host, project.configFile).find((path) => !seen.has(path));
|
|
292
292
|
if (unnamed !== undefined) {
|
|
293
293
|
throw new DiscoveryError(`the project ${JSON.stringify(project.id)} (${project.configFile}) references ` +
|
|
294
|
-
`${unnamed}, which the build matrix does not name
|
|
294
|
+
`${unnamed}, which the build matrix does not name. A referenced project is built ` +
|
|
295
295
|
"into the program that references it, so the matrix names it too", []);
|
|
296
296
|
}
|
|
297
297
|
}
|
|
@@ -230,7 +230,7 @@ const PARTLY_ENTERED = [
|
|
|
230
230
|
{
|
|
231
231
|
tool: "vite",
|
|
232
232
|
convention: "its configuration and every entry point it names",
|
|
233
|
-
reason: "Only a worker a call addresses by a literal is entered
|
|
233
|
+
reason: "Only a worker a call addresses by a literal is entered. No rule reads the " +
|
|
234
234
|
"configuration, so a file only it names is reported as never imported.",
|
|
235
235
|
},
|
|
236
236
|
{
|
|
@@ -260,7 +260,7 @@ const PARTLY_ENTERED = [
|
|
|
260
260
|
{
|
|
261
261
|
tool: "webpack",
|
|
262
262
|
convention: "its configuration and every entry point it names",
|
|
263
|
-
reason: "Only a worker a call addresses by a literal is entered
|
|
263
|
+
reason: "Only a worker a call addresses by a literal is entered. No rule reads the " +
|
|
264
264
|
"configuration, so a file only it names is reported as never imported.",
|
|
265
265
|
},
|
|
266
266
|
];
|
|
@@ -775,7 +775,7 @@ class Execution {
|
|
|
775
775
|
let held = value;
|
|
776
776
|
for (const name of names) {
|
|
777
777
|
if (!isObject(held)) {
|
|
778
|
-
throw new RenderError(`
|
|
778
|
+
throw new RenderError(`cannot evaluate field ${name} in a ${kindOf(held)}`);
|
|
779
779
|
}
|
|
780
780
|
if (!Object.hasOwn(held, name)) {
|
|
781
781
|
throw new RenderError(`the document has no member ${JSON.stringify(name)} here`);
|
|
@@ -807,7 +807,7 @@ class Execution {
|
|
|
807
807
|
}
|
|
808
808
|
if (head.k !== "func") {
|
|
809
809
|
if (rest.length > 0 || piped !== undefined) {
|
|
810
|
-
throw new RenderError("
|
|
810
|
+
throw new RenderError("cannot give an argument to a non-function");
|
|
811
811
|
}
|
|
812
812
|
return this.arg(head, dot);
|
|
813
813
|
}
|
|
@@ -903,7 +903,7 @@ class Execution {
|
|
|
903
903
|
.map((key) => [key, value[key] ?? null]);
|
|
904
904
|
}
|
|
905
905
|
else {
|
|
906
|
-
throw new RenderError(`range
|
|
906
|
+
throw new RenderError(`range cannot iterate over a ${kindOf(value)}`);
|
|
907
907
|
}
|
|
908
908
|
if (entries.length === 0) {
|
|
909
909
|
this.run(node.otherwise, dot);
|
package/dist/src/schema.js
CHANGED
|
@@ -218,10 +218,10 @@ export function unimplementedKey(label, path) {
|
|
|
218
218
|
const nearest = nearestKey(path);
|
|
219
219
|
let hint = "";
|
|
220
220
|
if (nearest === path) {
|
|
221
|
-
hint =
|
|
221
|
+
hint = `. ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
|
|
222
222
|
}
|
|
223
223
|
else if (nearest !== "") {
|
|
224
|
-
hint =
|
|
224
|
+
hint = `. The nearest implemented key is ${JSON.stringify(nearest)}`;
|
|
225
225
|
}
|
|
226
226
|
return new ConfigError("unimplemented-key", path, `${label}: key ${JSON.stringify(path)} is not implemented${hint}`);
|
|
227
227
|
}
|
|
@@ -46,7 +46,7 @@ export const printProjectsVerb = ({ out, err, host, inputs, scope, openClient })
|
|
|
46
46
|
for (const line of failures) {
|
|
47
47
|
err.write(`${line}\n`);
|
|
48
48
|
}
|
|
49
|
-
err.write(`deadset-ts: ${String(failures.length)} error(s)
|
|
49
|
+
err.write(`deadset-ts: ${String(failures.length)} error(s), so no answer was produced\n`);
|
|
50
50
|
return EXIT_FAILURE;
|
|
51
51
|
}
|
|
52
52
|
const projects = session.projects.filter((id) => id !== undefined);
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/analysis.ts
CHANGED
|
@@ -270,7 +270,7 @@ function readProjects<Answer>(
|
|
|
270
270
|
}
|
|
271
271
|
if (failures.length > 0) {
|
|
272
272
|
throw new DiscoveryError(
|
|
273
|
-
`${String(failures.length)} error(s)
|
|
273
|
+
`${String(failures.length)} error(s), so no answer was produced`,
|
|
274
274
|
failures,
|
|
275
275
|
);
|
|
276
276
|
}
|
package/src/discover.ts
CHANGED
|
@@ -415,7 +415,7 @@ function declaredProjects(
|
|
|
415
415
|
if (unnamed !== undefined) {
|
|
416
416
|
throw new DiscoveryError(
|
|
417
417
|
`the project ${JSON.stringify(project.id)} (${project.configFile}) references ` +
|
|
418
|
-
`${unnamed}, which the build matrix does not name
|
|
418
|
+
`${unnamed}, which the build matrix does not name. A referenced project is built ` +
|
|
419
419
|
"into the program that references it, so the matrix names it too",
|
|
420
420
|
[],
|
|
421
421
|
);
|
package/src/entry-point-gaps.ts
CHANGED
|
@@ -249,7 +249,7 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
|
|
|
249
249
|
tool: "vite",
|
|
250
250
|
convention: "its configuration and every entry point it names",
|
|
251
251
|
reason:
|
|
252
|
-
"Only a worker a call addresses by a literal is entered
|
|
252
|
+
"Only a worker a call addresses by a literal is entered. No rule reads the " +
|
|
253
253
|
"configuration, so a file only it names is reported as never imported.",
|
|
254
254
|
},
|
|
255
255
|
{
|
|
@@ -284,7 +284,7 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
|
|
|
284
284
|
tool: "webpack",
|
|
285
285
|
convention: "its configuration and every entry point it names",
|
|
286
286
|
reason:
|
|
287
|
-
"Only a worker a call addresses by a literal is entered
|
|
287
|
+
"Only a worker a call addresses by a literal is entered. No rule reads the " +
|
|
288
288
|
"configuration, so a file only it names is reported as never imported.",
|
|
289
289
|
},
|
|
290
290
|
];
|
|
@@ -893,7 +893,7 @@ class Execution {
|
|
|
893
893
|
let held = value;
|
|
894
894
|
for (const name of names) {
|
|
895
895
|
if (!isObject(held)) {
|
|
896
|
-
throw new RenderError(`
|
|
896
|
+
throw new RenderError(`cannot evaluate field ${name} in a ${kindOf(held)}`);
|
|
897
897
|
}
|
|
898
898
|
if (!Object.hasOwn(held, name)) {
|
|
899
899
|
throw new RenderError(`the document has no member ${JSON.stringify(name)} here`);
|
|
@@ -927,7 +927,7 @@ class Execution {
|
|
|
927
927
|
}
|
|
928
928
|
if (head.k !== "func") {
|
|
929
929
|
if (rest.length > 0 || piped !== undefined) {
|
|
930
|
-
throw new RenderError("
|
|
930
|
+
throw new RenderError("cannot give an argument to a non-function");
|
|
931
931
|
}
|
|
932
932
|
return this.arg(head, dot);
|
|
933
933
|
}
|
|
@@ -1021,7 +1021,7 @@ class Execution {
|
|
|
1021
1021
|
.sort()
|
|
1022
1022
|
.map((key) => [key, value[key] ?? null]);
|
|
1023
1023
|
} else {
|
|
1024
|
-
throw new RenderError(`range
|
|
1024
|
+
throw new RenderError(`range cannot iterate over a ${kindOf(value)}`);
|
|
1025
1025
|
}
|
|
1026
1026
|
if (entries.length === 0) {
|
|
1027
1027
|
this.run(node.otherwise, dot);
|
package/src/schema.ts
CHANGED
|
@@ -261,9 +261,9 @@ export function unimplementedKey(label: string, path: string): ConfigError {
|
|
|
261
261
|
const nearest = nearestKey(path);
|
|
262
262
|
let hint = "";
|
|
263
263
|
if (nearest === path) {
|
|
264
|
-
hint =
|
|
264
|
+
hint = `. ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
|
|
265
265
|
} else if (nearest !== "") {
|
|
266
|
-
hint =
|
|
266
|
+
hint = `. The nearest implemented key is ${JSON.stringify(nearest)}`;
|
|
267
267
|
}
|
|
268
268
|
return new ConfigError(
|
|
269
269
|
"unimplemented-key",
|
|
@@ -60,7 +60,7 @@ export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openCli
|
|
|
60
60
|
for (const line of failures) {
|
|
61
61
|
err.write(`${line}\n`);
|
|
62
62
|
}
|
|
63
|
-
err.write(`deadset-ts: ${String(failures.length)} error(s)
|
|
63
|
+
err.write(`deadset-ts: ${String(failures.length)} error(s), so no answer was produced\n`);
|
|
64
64
|
return EXIT_FAILURE;
|
|
65
65
|
}
|
|
66
66
|
const projects = session.projects.filter((id): id is string => id !== undefined);
|