slop-police 0.3.7 → 0.3.9
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 +133 -96
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -3,79 +3,70 @@
|
|
|
3
3
|
<p align="center"><b>Stop LLM crimes in your codebase</b></p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
|
|
6
|
+
<picture>
|
|
7
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/readme-dark.webp">
|
|
8
|
+
<img src="assets/readme-light.webp" alt="slop-police flags an export only used in its own file, a function that just forwards, a variable that is just a rename, and a method that is never called">
|
|
9
|
+
</picture>
|
|
7
10
|
</p>
|
|
8
11
|
|
|
9
12
|
```sh
|
|
10
|
-
pnpm add slop-police
|
|
13
|
+
pnpm add -D slop-police
|
|
11
14
|
# or
|
|
12
|
-
npm i slop-police
|
|
15
|
+
npm i -D slop-police
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
Add a [`slop-police.json`](#configuration) and run `npx slop-police` (or `pnpm exec slop-police`).
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
## Comparison with other tools
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
| | slop-police | knip | jscpd |
|
|
23
|
+
| ------------------------------------------ | :---------: | :--: | :---: |
|
|
24
|
+
| Unused exports and files | ✓ | ✓ | |
|
|
25
|
+
| Unused enum members | ✓ | ✓ | |
|
|
26
|
+
| Unused class methods | ✓ | | |
|
|
27
|
+
| Unused object keys | ✓ | | |
|
|
28
|
+
| Copy-pasted code | ✓ | | ✓ |
|
|
29
|
+
| Helpers rewritten instead of reused | ✓ | | |
|
|
30
|
+
| Pass-through wrappers, getters and aliases | ✓ | | |
|
|
31
|
+
| Flattened errors | ✓ | | |
|
|
32
|
+
|
|
33
|
+
## CI
|
|
34
|
+
|
|
35
|
+
Make CI fail when something slips through
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
```yaml
|
|
38
|
+
- run: npx slop-police
|
|
24
39
|
```
|
|
25
40
|
|
|
26
41
|
## Configuration
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
Put a `slop-police.json` in the directory you run it from:
|
|
29
44
|
|
|
30
45
|
```json
|
|
31
46
|
{
|
|
32
47
|
"projects": ["packages/api/tsconfig.json", "packages/frontend/tsconfig.app.json"],
|
|
33
|
-
"
|
|
34
|
-
"testFiles": ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"],
|
|
35
|
-
"rules": {
|
|
36
|
-
"unused-methods": ["error", { "includePrivate": true }],
|
|
37
|
-
"no-identity-type-alias": "warn",
|
|
38
|
-
"no-single-string-cn": "off"
|
|
39
|
-
}
|
|
48
|
+
"testFiles": ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"]
|
|
40
49
|
}
|
|
41
50
|
```
|
|
42
51
|
|
|
43
52
|
- `projects`: tsconfig files to analyze. Usages are counted across all of them, so a method used only by another package still counts as used.
|
|
44
53
|
- `ignore`: glob patterns (relative to the config) for files to skip entirely. Files that `.gitignore` ignores are skipped too.
|
|
45
54
|
- `testFiles`: glob patterns for tests. Usages inside tests count, but declarations inside tests are not reported.
|
|
46
|
-
- `rules`: every built-in rule
|
|
55
|
+
- `rules`: you usually don't need it, since every built-in rule is already on as an error. It's for rule options, like the stricter `unused-exports` setup below, and for turning on plugin rules. A rule takes `"error"`, `"warn"`, `"off"` or `[severity, options]`.
|
|
47
56
|
- `plugins`: modules with [custom rules](#custom-rules), as paths relative to the config or package names.
|
|
48
57
|
|
|
49
58
|
Exit code is `1` when there are errors, `2` for an invalid config, `0` otherwise.
|
|
50
59
|
|
|
51
|
-
slop-police needs your dependencies installed: without their types it can't tell where a method or key is used. When imports in the analyzed files don't resolve (and no `declare module` covers them), it lists them and exits with `1` without linting. Side-effect imports like `import "./styles.css"` don't count.
|
|
52
|
-
|
|
53
60
|
## Rules
|
|
54
61
|
|
|
55
|
-
###
|
|
62
|
+
### no-pass-through-wrapper
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
A function or method that only returns a call to another function.
|
|
58
65
|
|
|
59
66
|
```ts
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
reset() {} // Method Store.reset is never used
|
|
67
|
+
function loadUser(id: string) {
|
|
68
|
+
return api.loadUser(id);
|
|
63
69
|
}
|
|
64
|
-
new Store().load();
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
A member also counts as used when it implements an interface member that is used or that is declared outside the analyzed code (a library calls it), when it's named in `Pick<Class, "name">`, or when an instance is passed to a JSX prop whose type requires it. Options: `includePrivate` and `includeProtected` (both `false` by default).
|
|
68
|
-
|
|
69
|
-
### unused-object-keys
|
|
70
|
-
|
|
71
|
-
Keys of a top-level `const` object literal that are never read, as long as every use of the object reads statically known keys (`links.a`, `links["a"]`, `const { a } = links`). Objects that escape (passed to a function, spread, accessed dynamically) are skipped.
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
export const links = {
|
|
75
|
-
docs: "https://example.com/docs",
|
|
76
|
-
legacy: "https://example.com/old", // links.legacy is never read
|
|
77
|
-
};
|
|
78
|
-
open(links.docs);
|
|
79
70
|
```
|
|
80
71
|
|
|
81
72
|
### no-identity-type-alias
|
|
@@ -84,113 +75,161 @@ A type alias that only renames another type.
|
|
|
84
75
|
|
|
85
76
|
```ts
|
|
86
77
|
type User = { id: string };
|
|
87
|
-
type Member = User;
|
|
78
|
+
type Member = User;
|
|
88
79
|
```
|
|
89
80
|
|
|
90
|
-
### no-
|
|
81
|
+
### no-duplicate-code
|
|
91
82
|
|
|
92
|
-
|
|
83
|
+
Copy-pasted blocks of code. Options: `minLines` (default `11`) and `minTokens` (default `50`).
|
|
93
84
|
|
|
94
85
|
```ts
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
// stripe-webhook.ts
|
|
87
|
+
const mailer = await Mailer.connect({
|
|
88
|
+
host: "smtp.example.com",
|
|
89
|
+
port: 465,
|
|
90
|
+
secure: true,
|
|
91
|
+
user: env.SMTP_USER,
|
|
92
|
+
password: env.SMTP_PASSWORD,
|
|
93
|
+
});
|
|
94
|
+
await mailer.send({ to: customer.email, subject: "Your license" });
|
|
95
|
+
|
|
96
|
+
// reset-password.ts
|
|
97
|
+
const mailer = await Mailer.connect({
|
|
98
|
+
host: "smtp.example.com",
|
|
99
|
+
port: 465,
|
|
100
|
+
secure: true,
|
|
101
|
+
user: env.SMTP_USER,
|
|
102
|
+
password: env.SMTP_PASSWORD,
|
|
103
|
+
});
|
|
104
|
+
await mailer.send({ to: user.email, subject: "Reset your password" });
|
|
99
105
|
```
|
|
100
106
|
|
|
101
|
-
### no-
|
|
107
|
+
### no-duplicate-function
|
|
102
108
|
|
|
103
|
-
|
|
109
|
+
The same function written again in several files, with different variable names. Option: `minFiles` (default `3`).
|
|
104
110
|
|
|
105
111
|
```ts
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
// users.ts
|
|
113
|
+
export async function loadUsers(id: string) {
|
|
114
|
+
const response = await fetchJson("/users/" + id);
|
|
115
|
+
if (!response.ok) throw new Error("failed to load users");
|
|
116
|
+
return response.data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// posts.ts
|
|
120
|
+
export async function loadPosts(postId: string) {
|
|
121
|
+
const res = await fetchJson("/posts/" + postId);
|
|
122
|
+
if (!res.ok) throw new Error("failed to load posts");
|
|
123
|
+
return res.data;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// teams.ts
|
|
127
|
+
export async function loadTeams(teamId: string) {
|
|
128
|
+
const result = await fetchJson("/teams/" + teamId);
|
|
129
|
+
if (!result.ok) throw new Error("failed to load teams");
|
|
130
|
+
return result.data;
|
|
108
131
|
}
|
|
109
132
|
```
|
|
110
133
|
|
|
111
|
-
|
|
134
|
+
### no-pass-through-getter
|
|
112
135
|
|
|
113
|
-
|
|
114
|
-
- the wrapper adds types: type parameters, overloads, or parameter and return types declared differently from the callee's, like `t(key: TranslationKey)` around `translate(key: string)`.
|
|
115
|
-
- it delegates to a private or protected member, like `get(key) { return this.items.get(key); }`.
|
|
136
|
+
A getter that only returns another property.
|
|
116
137
|
|
|
117
|
-
|
|
138
|
+
```ts
|
|
139
|
+
class Store {
|
|
140
|
+
private items: Item[] = [];
|
|
141
|
+
get list() { return this.items; }
|
|
142
|
+
}
|
|
143
|
+
```
|
|
118
144
|
|
|
119
145
|
### no-redundant-value-alias
|
|
120
146
|
|
|
121
|
-
A
|
|
147
|
+
A variable that only renames another variable.
|
|
122
148
|
|
|
123
149
|
```ts
|
|
124
150
|
function render() {
|
|
125
151
|
const user = getUser();
|
|
126
|
-
const current = user;
|
|
152
|
+
const current = user;
|
|
127
153
|
return current.name;
|
|
128
154
|
}
|
|
129
155
|
```
|
|
130
156
|
|
|
131
157
|
### no-single-string-cn
|
|
132
158
|
|
|
133
|
-
`cn()`
|
|
159
|
+
`cn()` with a single class string.
|
|
134
160
|
|
|
135
161
|
```tsx
|
|
136
|
-
<div className={cn("flex gap-2")} />
|
|
162
|
+
<div className={cn("flex gap-2")} />
|
|
137
163
|
```
|
|
138
164
|
|
|
139
|
-
### no-
|
|
165
|
+
### no-stringified-unknown-error
|
|
140
166
|
|
|
141
|
-
|
|
167
|
+
Turning a caught error into a string with `error instanceof Error ? error.message : String(error)`.
|
|
142
168
|
|
|
143
169
|
```ts
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
170
|
+
try {
|
|
171
|
+
await sendMessage(chatId, text);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
147
174
|
}
|
|
148
175
|
```
|
|
149
176
|
|
|
150
|
-
###
|
|
177
|
+
### unused-object-keys
|
|
151
178
|
|
|
152
|
-
|
|
179
|
+
Object keys that are never read.
|
|
153
180
|
|
|
154
181
|
```ts
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
182
|
+
export const links = {
|
|
183
|
+
docs: "https://example.com/docs",
|
|
184
|
+
legacy: "https://example.com/old", // links.legacy is never read
|
|
185
|
+
};
|
|
186
|
+
open(links.docs);
|
|
187
|
+
```
|
|
158
188
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
189
|
+
An index with a literal type reads only the keys in that type, so `labels[status]` with `status: "active" | "expired"` leaves a `trial` key unused. An index typed as `string` or `any`, or an object passed elsewhere, could read any key, so the object is skipped.
|
|
190
|
+
|
|
191
|
+
### unused-enum-members
|
|
192
|
+
|
|
193
|
+
Enum members that are never used.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
enum Status {
|
|
197
|
+
Active,
|
|
198
|
+
Archived, // Enum member Status.Archived is never used
|
|
166
199
|
}
|
|
167
|
-
|
|
200
|
+
const isActive = (status: Status) => status === Status.Active;
|
|
168
201
|
```
|
|
169
202
|
|
|
170
|
-
###
|
|
203
|
+
### unused-exports
|
|
171
204
|
|
|
172
|
-
|
|
205
|
+
Exports that nothing imports, or that are only used in their own file.
|
|
173
206
|
|
|
174
207
|
```ts
|
|
175
|
-
//
|
|
176
|
-
export
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
208
|
+
// lib.ts
|
|
209
|
+
export type Options = { strict: boolean }; // Export Options is only used in its own file
|
|
210
|
+
export function parse(text: string, options?: Options) {}
|
|
211
|
+
export function format() {} // Export format is never used
|
|
212
|
+
|
|
213
|
+
// main.ts
|
|
214
|
+
import { parse } from "./lib";
|
|
182
215
|
```
|
|
183
216
|
|
|
184
|
-
|
|
217
|
+
If you want to be extra strict, list your entry points in the `entry` option. slop-police then also finds whole files that nothing imports from them or from tests. Code used only by such dead files counts as unused too.
|
|
185
218
|
|
|
186
|
-
|
|
219
|
+
```json
|
|
220
|
+
"unused-exports": ["error", { "entry": ["src/main.tsx", "src/routes/**/*.tsx", "vite.config.ts"] }]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### unused-methods
|
|
224
|
+
|
|
225
|
+
Class methods that are never called. Here `reset` is unused.
|
|
187
226
|
|
|
188
227
|
```ts
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error) }; // Caught value error is flattened into a string
|
|
228
|
+
class Store {
|
|
229
|
+
load() {}
|
|
230
|
+
reset() {}
|
|
193
231
|
}
|
|
232
|
+
new Store().load();
|
|
194
233
|
```
|
|
195
234
|
|
|
196
235
|
## Custom rules
|
|
@@ -286,7 +325,7 @@ test("no-direct-fetch", () => {
|
|
|
286
325
|
});
|
|
287
326
|
```
|
|
288
327
|
|
|
289
|
-
Plugins
|
|
328
|
+
Plugins work only through the npm launcher (`npx slop-police`).
|
|
290
329
|
|
|
291
330
|
## Ignoring findings
|
|
292
331
|
|
|
@@ -302,8 +341,6 @@ class Store {
|
|
|
302
341
|
export const legacyLinks = { a: "/a", b: "/b" };
|
|
303
342
|
```
|
|
304
343
|
|
|
305
|
-
For `no-placeholder-comment`, put the ignore comment on the line directly above the reported comment.
|
|
306
|
-
|
|
307
344
|
## Requirements
|
|
308
345
|
|
|
309
346
|
macOS or Linux on arm64 or x64 (on Windows, use WSL), and Node.js 22.18 or later. The linter bundles its own TypeScript 7 compiler, so your tsconfigs need to work with TypeScript 7.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slop-police",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Type-aware TypeScript linter for the dead code and needless indirection AI agents leave behind",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"node": ">=22.18"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"slop-police-darwin-arm64": "0.3.
|
|
45
|
-
"slop-police-darwin-x64": "0.3.
|
|
46
|
-
"slop-police-linux-arm64": "0.3.
|
|
47
|
-
"slop-police-linux-x64": "0.3.
|
|
44
|
+
"slop-police-darwin-arm64": "0.3.9",
|
|
45
|
+
"slop-police-darwin-x64": "0.3.9",
|
|
46
|
+
"slop-police-linux-arm64": "0.3.9",
|
|
47
|
+
"slop-police-linux-x64": "0.3.9"
|
|
48
48
|
}
|
|
49
49
|
}
|