slop-police 0.3.8 → 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 +107 -141
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -3,46 +3,12 @@
|
|
|
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
|
-
```ts
|
|
10
|
-
import { api } from "./api";
|
|
11
|
-
|
|
12
|
-
export type User = { id: string; name: string };
|
|
13
|
-
|
|
14
|
-
export function fetchUser(id: string) {
|
|
15
|
-
return api.getUser(id);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class UserCache {
|
|
19
|
-
private users = new Map<string, User>();
|
|
20
|
-
|
|
21
|
-
async get(id: string) {
|
|
22
|
-
const user = this.users.get(id) ?? (await fetchUser(id));
|
|
23
|
-
const cached = user;
|
|
24
|
-
this.users.set(id, cached);
|
|
25
|
-
return cached;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
has(id: string) {
|
|
29
|
-
return this.users.has(id);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
slop-police isn't:
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
user-cache.ts:3:13 error unused-exports Export User is only used in its own file
|
|
38
|
-
user-cache.ts:5:17 error no-pass-through-wrapper Function fetchUser only forwards to api.getUser
|
|
39
|
-
user-cache.ts:5:17 error unused-exports Export fetchUser is only used in its own file
|
|
40
|
-
user-cache.ts:14:11 error no-redundant-value-alias Local alias cached only renames user; use the original binding directly
|
|
41
|
-
user-cache.ts:19:3 error unused-methods Method UserCache.has is never used
|
|
42
|
-
|
|
43
|
-
5 errors, 0 warnings
|
|
44
|
-
```
|
|
45
|
-
|
|
46
12
|
```sh
|
|
47
13
|
pnpm add -D slop-police
|
|
48
14
|
# or
|
|
@@ -51,7 +17,7 @@ npm i -D slop-police
|
|
|
51
17
|
|
|
52
18
|
Add a [`slop-police.json`](#configuration) and run `npx slop-police` (or `pnpm exec slop-police`).
|
|
53
19
|
|
|
54
|
-
##
|
|
20
|
+
## Comparison with other tools
|
|
55
21
|
|
|
56
22
|
| | slop-police | knip | jscpd |
|
|
57
23
|
| ------------------------------------------ | :---------: | :--: | :---: |
|
|
@@ -62,7 +28,7 @@ Add a [`slop-police.json`](#configuration) and run `npx slop-police` (or `pnpm e
|
|
|
62
28
|
| Copy-pasted code | ✓ | | ✓ |
|
|
63
29
|
| Helpers rewritten instead of reused | ✓ | | |
|
|
64
30
|
| Pass-through wrappers, getters and aliases | ✓ | | |
|
|
65
|
-
|
|
|
31
|
+
| Flattened errors | ✓ | | |
|
|
66
32
|
|
|
67
33
|
## CI
|
|
68
34
|
|
|
@@ -79,7 +45,6 @@ Put a `slop-police.json` in the directory you run it from:
|
|
|
79
45
|
```json
|
|
80
46
|
{
|
|
81
47
|
"projects": ["packages/api/tsconfig.json", "packages/frontend/tsconfig.app.json"],
|
|
82
|
-
"ignore": ["**/.next/**"],
|
|
83
48
|
"testFiles": ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"]
|
|
84
49
|
}
|
|
85
50
|
```
|
|
@@ -94,174 +59,177 @@ Exit code is `1` when there are errors, `2` for an invalid config, `0` otherwise
|
|
|
94
59
|
|
|
95
60
|
## Rules
|
|
96
61
|
|
|
97
|
-
###
|
|
62
|
+
### no-pass-through-wrapper
|
|
98
63
|
|
|
99
|
-
|
|
64
|
+
A function or method that only returns a call to another function.
|
|
100
65
|
|
|
101
66
|
```ts
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
reset() {} // Method Store.reset is never used
|
|
67
|
+
function loadUser(id: string) {
|
|
68
|
+
return api.loadUser(id);
|
|
105
69
|
}
|
|
106
|
-
new Store().load();
|
|
107
70
|
```
|
|
108
71
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
### unused-object-keys
|
|
72
|
+
### no-identity-type-alias
|
|
112
73
|
|
|
113
|
-
|
|
74
|
+
A type alias that only renames another type.
|
|
114
75
|
|
|
115
76
|
```ts
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
legacy: "https://example.com/old", // links.legacy is never read
|
|
119
|
-
};
|
|
120
|
-
open(links.docs);
|
|
77
|
+
type User = { id: string };
|
|
78
|
+
type Member = User;
|
|
121
79
|
```
|
|
122
80
|
|
|
123
|
-
###
|
|
81
|
+
### no-duplicate-code
|
|
124
82
|
|
|
125
|
-
|
|
83
|
+
Copy-pasted blocks of code. Options: `minLines` (default `11`) and `minTokens` (default `50`).
|
|
126
84
|
|
|
127
85
|
```ts
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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" });
|
|
138
105
|
```
|
|
139
106
|
|
|
140
|
-
###
|
|
107
|
+
### no-duplicate-function
|
|
141
108
|
|
|
142
|
-
|
|
109
|
+
The same function written again in several files, with different variable names. Option: `minFiles` (default `3`).
|
|
143
110
|
|
|
144
111
|
```ts
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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;
|
|
148
117
|
}
|
|
149
|
-
const isActive = (status: Status) => status === Status.Active;
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### no-identity-type-alias
|
|
153
118
|
|
|
154
|
-
|
|
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
|
+
}
|
|
155
125
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
131
|
+
}
|
|
159
132
|
```
|
|
160
133
|
|
|
161
134
|
### no-pass-through-getter
|
|
162
135
|
|
|
163
|
-
A getter that only returns another property
|
|
136
|
+
A getter that only returns another property.
|
|
164
137
|
|
|
165
138
|
```ts
|
|
166
139
|
class Store {
|
|
167
140
|
private items: Item[] = [];
|
|
168
|
-
get list() { return this.items; }
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### no-pass-through-wrapper
|
|
173
|
-
|
|
174
|
-
A function or method whose body only returns a call (awaited or not) to a named function, passing its own parameters on as they are.
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
function loadUser(id: string) { // Function loadUser only forwards to api.loadUser
|
|
178
|
-
return api.loadUser(id);
|
|
141
|
+
get list() { return this.items; }
|
|
179
142
|
}
|
|
180
143
|
```
|
|
181
144
|
|
|
182
|
-
It uses types to skip wrappers that do something:
|
|
183
|
-
|
|
184
|
-
- the shape is required: a method of a typed object literal (a `Proxy` handler, a Worker's `fetch`), a method that implements or overrides an inherited member, or a function assigned to a variable with a declared type. An override that only calls `super`'s method of the same name is still reported.
|
|
185
|
-
- 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)`.
|
|
186
|
-
- it delegates to a private or protected member, like `get(key) { return this.items.get(key); }`.
|
|
187
|
-
|
|
188
|
-
Calls on computed values (`getDay(date).getTime()`, `/^\s*$/.test(text)`) aren't forwarding, so they don't count.
|
|
189
|
-
|
|
190
145
|
### no-redundant-value-alias
|
|
191
146
|
|
|
192
|
-
A
|
|
147
|
+
A variable that only renames another variable.
|
|
193
148
|
|
|
194
149
|
```ts
|
|
195
150
|
function render() {
|
|
196
151
|
const user = getUser();
|
|
197
|
-
const current = user;
|
|
152
|
+
const current = user;
|
|
198
153
|
return current.name;
|
|
199
154
|
}
|
|
200
155
|
```
|
|
201
156
|
|
|
202
157
|
### no-single-string-cn
|
|
203
158
|
|
|
204
|
-
`cn()`
|
|
159
|
+
`cn()` with a single class string.
|
|
205
160
|
|
|
206
161
|
```tsx
|
|
207
|
-
<div className={cn("flex gap-2")} />
|
|
162
|
+
<div className={cn("flex gap-2")} />
|
|
208
163
|
```
|
|
209
164
|
|
|
210
|
-
### no-
|
|
165
|
+
### no-stringified-unknown-error
|
|
211
166
|
|
|
212
|
-
|
|
167
|
+
Turning a caught error into a string with `error instanceof Error ? error.message : String(error)`.
|
|
213
168
|
|
|
214
169
|
```ts
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
170
|
+
try {
|
|
171
|
+
await sendMessage(chatId, text);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
218
174
|
}
|
|
219
175
|
```
|
|
220
176
|
|
|
221
|
-
###
|
|
177
|
+
### unused-object-keys
|
|
222
178
|
|
|
223
|
-
|
|
179
|
+
Object keys that are never read.
|
|
224
180
|
|
|
225
181
|
```ts
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
```
|
|
229
188
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
237
199
|
}
|
|
238
|
-
|
|
200
|
+
const isActive = (status: Status) => status === Status.Active;
|
|
239
201
|
```
|
|
240
202
|
|
|
241
|
-
###
|
|
203
|
+
### unused-exports
|
|
242
204
|
|
|
243
|
-
|
|
205
|
+
Exports that nothing imports, or that are only used in their own file.
|
|
244
206
|
|
|
245
207
|
```ts
|
|
246
|
-
//
|
|
247
|
-
export
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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";
|
|
253
215
|
```
|
|
254
216
|
|
|
255
|
-
|
|
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.
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
"unused-exports": ["error", { "entry": ["src/main.tsx", "src/routes/**/*.tsx", "vite.config.ts"] }]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### unused-methods
|
|
256
224
|
|
|
257
|
-
|
|
225
|
+
Class methods that are never called. Here `reset` is unused.
|
|
258
226
|
|
|
259
227
|
```ts
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
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() {}
|
|
264
231
|
}
|
|
232
|
+
new Store().load();
|
|
265
233
|
```
|
|
266
234
|
|
|
267
235
|
## Custom rules
|
|
@@ -357,7 +325,7 @@ test("no-direct-fetch", () => {
|
|
|
357
325
|
});
|
|
358
326
|
```
|
|
359
327
|
|
|
360
|
-
Plugins
|
|
328
|
+
Plugins work only through the npm launcher (`npx slop-police`).
|
|
361
329
|
|
|
362
330
|
## Ignoring findings
|
|
363
331
|
|
|
@@ -373,8 +341,6 @@ class Store {
|
|
|
373
341
|
export const legacyLinks = { a: "/a", b: "/b" };
|
|
374
342
|
```
|
|
375
343
|
|
|
376
|
-
For `no-placeholder-comment`, put the ignore comment on the line directly above the reported comment.
|
|
377
|
-
|
|
378
344
|
## Requirements
|
|
379
345
|
|
|
380
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
|
}
|