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.
Files changed (2) hide show
  1. package/README.md +133 -96
  2. 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
- A type-aware TypeScript linter for the dead code and needless indirection AI agents leave behind.
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
- Run it with `pnpm exec slop-police` or `npx slop-police`.
18
+ Add a [`slop-police.json`](#configuration) and run `npx slop-police` (or `pnpm exec slop-police`).
16
19
 
17
- Example output:
20
+ ## Comparison with other tools
18
21
 
19
- ```
20
- deck-store.ts:42:3 error unused-methods Method DeckStore.reset is never used
21
- links.ts:30:3 error unused-object-keys links.privacyPolicy is never read
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
- 2 errors, 0 warnings
37
+ ```yaml
38
+ - run: npx slop-police
24
39
  ```
25
40
 
26
41
  ## Configuration
27
42
 
28
- slop-police reads `slop-police.json` from the current directory:
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
- "ignore": ["**/.next/**"],
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 runs as an error with its default options. List a rule here to change that: `"error"`, `"warn"` or `"off"`, or `[severity, options]`. Plugin rules run only when they're listed.
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
- ### unused-methods
62
+ ### no-pass-through-wrapper
56
63
 
57
- Class methods, getters and setters that are never referenced anywhere in the analyzed projects.
64
+ A function or method that only returns a call to another function.
58
65
 
59
66
  ```ts
60
- class Store {
61
- load() {}
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; // Type alias Member only renames User
78
+ type Member = User;
88
79
  ```
89
80
 
90
- ### no-pass-through-getter
81
+ ### no-duplicate-code
91
82
 
92
- A getter that only returns another property of `this`.
83
+ Copy-pasted blocks of code. Options: `minLines` (default `11`) and `minTokens` (default `50`).
93
84
 
94
85
  ```ts
95
- class Store {
96
- private items: Item[] = [];
97
- get list() { return this.items; } // Getter list only returns this.items
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-pass-through-wrapper
107
+ ### no-duplicate-function
102
108
 
103
- 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.
109
+ The same function written again in several files, with different variable names. Option: `minFiles` (default `3`).
104
110
 
105
111
  ```ts
106
- function loadUser(id: string) { // Function loadUser only forwards to api.loadUser
107
- return api.loadUser(id);
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
- It uses types to skip wrappers that do something:
134
+ ### no-pass-through-getter
112
135
 
113
- - 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.
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
- Calls on computed values (`getDay(date).getTime()`, `/^\s*$/.test(text)`) aren't forwarding, so they don't count.
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 local `const` that only renames another `const`, when using the original directly means the same thing.
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; // Local alias current only renames user; use the original binding directly
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()` called with a single static class string.
159
+ `cn()` with a single class string.
134
160
 
135
161
  ```tsx
136
- <div className={cn("flex gap-2")} /> // Use the class string directly instead of wrapping it in cn()
162
+ <div className={cn("flex gap-2")} />
137
163
  ```
138
164
 
139
- ### no-placeholder-comment
165
+ ### no-stringified-unknown-error
140
166
 
141
- A comment that only promises future work instead of describing the code: "add more validation", "handle more cases", "can be extended in the future", "extend this logic", "customize this behavior", "future enhancement", "implement … here". Ordinary TODOs don't match.
167
+ Turning a caught error into a string with `error instanceof Error ? error.message : String(error)`.
142
168
 
143
169
  ```ts
144
- function parse(input: string) {
145
- // Add more validation here
146
- return JSON.parse(input);
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
- ### no-duplicate-code
177
+ ### unused-object-keys
151
178
 
152
- Copy-pasted code: at least 50 tokens in a row that repeat earlier code (in the same file or one before it by path) and span at least 11 lines. Tokens are compared by their text, types included, so a renamed variable or a different type ends a copy, but comments, formatting and imports are left out. Test files are neither reported nor compared. `// slop-police-ignore no-duplicate-code` above a statement, class member or object property takes it out of the comparison, so it is not reported as a copy or as the original. Options: `minLines` (default `11`, counting every line a copy spans) and `minTokens` (default `50`).
179
+ Object keys that are never read.
153
180
 
154
181
  ```ts
155
- // update-card.ts and delete-card.ts both check the card the same way:
156
- const userId = ctx.userId;
157
- assert(userId);
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
- const cards = await getCardsWithDeck(ctx.db, [input.id]);
160
- if (cards.length === 0) {
161
- throw notFound("Card not found");
162
- }
163
- const card = cards[0];
164
- if (card.deckAuthorId !== userId) {
165
- throw forbidden("You don't own this card");
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
- // delete-card.ts:6:5 Lines 6-16 duplicate update-card.ts:6-16
200
+ const isActive = (status: Status) => status === Status.Active;
168
201
  ```
169
202
 
170
- ### no-duplicate-function
203
+ ### unused-exports
171
204
 
172
- A function whose body has the same shape as functions in at least two other files: a helper that was rewritten instead of reused. Shapes ignore the names of parameters and locals and the values of literals, but not the names of what the body calls or reads. Bodies with fewer than 2 statements or 16 syntax nodes, and functions in test files, don't count. Option: `minFiles` (default `3`, at least `2`), the number of files a shape has to appear in.
205
+ Exports that nothing imports, or that are only used in their own file.
173
206
 
174
207
  ```ts
175
- // users.ts, posts.ts and teams.ts each have:
176
- export async function loadUsers(id: string) {
177
- const response = await fetchJson("/users/" + id);
178
- if (!response.ok) throw new Error("failed to load users");
179
- return response.data;
180
- }
181
- // Function loadUsers has the same body shape as loadPosts (posts.ts:1), loadTeams (teams.ts:1)
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
- ### no-stringified-unknown-error
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
- `error instanceof Error ? error.message : String(error)` (in either order), which flattens a caught value into a string and drops its type, stack and cause. A plain `instanceof Error` check counts when the variable is named `error`, `err`, `e` or `cause`; an `||` of `instanceof` checks counts for any name. A function that does nothing but this conversion, like `getErrorMessage(error)`, is where it belongs and isn't reported; use it instead of repeating the expression.
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
- try {
190
- await sendMessage(chatId, text);
191
- } catch (error) {
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 need the npm launcher (`npx slop-police` or `pnpm exec slop-police`), which runs them in Node while the linter serves ASTs and types. Published plugin packages must ship JavaScript, since Node doesn't run TypeScript from `node_modules`.
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.7",
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.7",
45
- "slop-police-darwin-x64": "0.3.7",
46
- "slop-police-linux-arm64": "0.3.7",
47
- "slop-police-linux-x64": "0.3.7"
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
  }