eslint-plugin-unslop 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergei Khoroshavin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # eslint-plugin-unslop
2
+
3
+ ESLint plugin that catches common LLM-generated code smells — the kind of subtle junk that sneaks in when your LLM is feeling creative. Smart quotes, invisible unicode, spaghetti imports, dead "shared" code that nobody shares, and declarations ordered for machines instead of humans.
4
+
5
+ Requires ESLint 9+ (flat config). TypeScript optional but recommended.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install --save-dev eslint-plugin-unslop
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ The recommended config enables the three most universal rules out of the box:
16
+
17
+ ```js
18
+ // eslint.config.mjs
19
+ import unslop from 'eslint-plugin-unslop'
20
+
21
+ export default [unslop.configs.recommended]
22
+ ```
23
+
24
+ This turns on:
25
+
26
+ | Rule | Severity | What it does |
27
+ | --------------------------- | -------- | ------------------------------------------------------------------- |
28
+ | `unslop/no-special-unicode` | error | Catches smart quotes, invisible spaces, and other unicode impostors |
29
+ | `unslop/no-unicode-escape` | error | Prefers `"©"` over `"\u00A9"` |
30
+ | `unslop/no-deep-imports` | error | Prevents importing too deep within the same top-level folder |
31
+
32
+ The remaining rules need explicit configuration:
33
+
34
+ ```js
35
+ // eslint.config.mjs
36
+ import unslop from 'eslint-plugin-unslop'
37
+
38
+ export default [
39
+ unslop.configs.recommended,
40
+ {
41
+ rules: {
42
+ 'unslop/no-false-sharing': ['error', { dirs: ['shared', 'utils'] }],
43
+ 'unslop/read-friendly-order': 'warn',
44
+ },
45
+ },
46
+ ]
47
+ ```
48
+
49
+ ## Rules
50
+
51
+ ### `unslop/no-special-unicode`
52
+
53
+ **Recommended**
54
+
55
+ Disallows special unicode punctuation and whitespace characters in string literals and template literals. LLMs love to sprinkle in smart quotes (`“like this”`), non-breaking spaces, and other invisible gremlins that look fine in a PR review but cause fun bugs at runtime.
56
+
57
+ Caught characters include: left/right smart quotes (`“” ‘’`), non-breaking space, en/em dash, horizontal ellipsis, zero-width space, and various other exotic whitespace.
58
+
59
+ ```js
60
+ // Bad — these contain invisible special characters that look normal
61
+ const greeting = 'Hello World' // a non-breaking space (U+00A0) is hiding between the words
62
+ const quote = 'He said “hello”' // smart double quotes (U+201C, U+201D)
63
+
64
+ // Good
65
+ const greeting = 'Hello World' // regular ASCII space
66
+ const quote = 'He said "hello"' // plain ASCII quotes
67
+ ```
68
+
69
+ Note: the bad examples above contain actual unicode characters that may be
70
+ indistinguishable from their ASCII counterparts in your font — that's exactly
71
+ the problem this rule catches.
72
+
73
+ ### `unslop/no-unicode-escape`
74
+
75
+ **Recommended**
76
+
77
+ Prefers actual characters over `\uXXXX` escape sequences. If your string says `\u00A9`, just write `©` — your coworkers will thank you. LLM-generated code sometimes encodes characters as escape sequences for no good reason.
78
+
79
+ ```js
80
+ // Bad
81
+ const copyright = '\u00A9 2025'
82
+ const arrow = '\u2192'
83
+
84
+ // Good
85
+ const copyright = '© 2025'
86
+ const arrow = '→'
87
+ ```
88
+
89
+ ### `unslop/no-deep-imports`
90
+
91
+ **Recommended**
92
+
93
+ Forbids importing more than one level deeper than the current file within the same top-level folder. If `features/auth/login.ts` imports from `features/auth/validators/internal/format.ts`, that's reaching too deep into implementation details. This rule nudges you toward flatter structures and proper module boundaries.
94
+
95
+ Only triggers for imports within the same top-level folder. External packages and imports into other top-level folders are ignored.
96
+
97
+ #### Options
98
+
99
+ ```js
100
+ ;['error', { sourceRoot: 'src' }]
101
+ ```
102
+
103
+ | Option | Type | Default | Description |
104
+ | ------------ | -------- | ------------- | ----------------------------------------- |
105
+ | `sourceRoot` | `string` | auto-detected | Source directory relative to project root |
106
+
107
+ #### Examples
108
+
109
+ Given a project with `sourceRoot: 'src'`:
110
+
111
+ ```js
112
+ // File: src/features/auth/login.ts
113
+
114
+ // OK — one level deep, same folder
115
+ import { validate } from './validators/email.js'
116
+
117
+ // Bad — two levels deep into same top-level folder
118
+ import { format } from './validators/internal/format.js'
119
+ ```
120
+
121
+ ### `unslop/no-false-sharing`
122
+
123
+ **Requires TypeScript parser with program**
124
+
125
+ The "shared" folder anti-pattern detector. LLMs love creating shared utilities that are only used by one consumer — or worse, by nobody at all. This rule requires that modules inside your designated shared directories are actually imported by at least two separate entities. If it's only used in one place, it's not shared — it's misplaced.
126
+
127
+ #### Options
128
+
129
+ ```js
130
+ ;[
131
+ 'error',
132
+ {
133
+ dirs: [{ path: 'shared' }, { path: 'utils', mode: 'file' }],
134
+ mode: 'dir',
135
+ sourceRoot: 'src',
136
+ },
137
+ ]
138
+ ```
139
+
140
+ | Option | Type | Required | Description |
141
+ | ------------- | ----------------- | -------- | -------------------------------------------------------------------------------------------- |
142
+ | `dirs` | `array` | yes | Directories to enforce sharing rules on |
143
+ | `dirs[].path` | `string` | yes | Directory path relative to `sourceRoot` |
144
+ | `dirs[].mode` | `'file' \| 'dir'` | no | How to count consumers for this dir (overrides global `mode`) |
145
+ | `mode` | `'file' \| 'dir'` | no | Global consumer counting mode — `'file'` counts individual files, `'dir'` counts directories |
146
+ | `sourceRoot` | `string` | no | Source directory relative to project root |
147
+
148
+ #### Setup
149
+
150
+ This rule requires `typescript-eslint` with type information:
151
+
152
+ ```js
153
+ // eslint.config.mjs
154
+ import unslop from 'eslint-plugin-unslop'
155
+ import tseslint from 'typescript-eslint'
156
+
157
+ export default tseslint.config(
158
+ {
159
+ languageOptions: {
160
+ parserOptions: {
161
+ projectService: true,
162
+ },
163
+ },
164
+ },
165
+ unslop.configs.recommended,
166
+ {
167
+ rules: {
168
+ 'unslop/no-false-sharing': [
169
+ 'error',
170
+ {
171
+ dirs: ['shared', 'utils'],
172
+ sourceRoot: 'src',
173
+ },
174
+ ],
175
+ },
176
+ },
177
+ )
178
+ ```
179
+
180
+ #### What it catches
181
+
182
+ ```
183
+ src/shared/format-date.ts
184
+ → only imported by src/features/calendar/view.ts
185
+ → error: must be used by 2+ entities
186
+
187
+ src/utils/old-helper.ts
188
+ → not imported by anyone
189
+ → error: must be used by 2+ entities
190
+ ```
191
+
192
+ ### `unslop/read-friendly-order`
193
+
194
+ Enforces a top-down reading order for your code. The idea: when someone opens a file, they should see the important stuff first and the helpers below. LLM-generated code often scatters declarations in random order, making files harder to follow.
195
+
196
+ This rule covers three areas:
197
+
198
+ **Top-level ordering** — Public/exported symbols should come before the private helpers they use. Read the API first, implementation details second.
199
+
200
+ ```js
201
+ // Bad — helper defined before its consumer
202
+ function formatName(name) {
203
+ return name.trim().toLowerCase()
204
+ }
205
+
206
+ export function createUser(name) {
207
+ return { name: formatName(name) }
208
+ }
209
+
210
+ // Good — consumer first, helper below
211
+ export function createUser(name) {
212
+ return { name: formatName(name) }
213
+ }
214
+
215
+ function formatName(name) {
216
+ return name.trim().toLowerCase()
217
+ }
218
+ ```
219
+
220
+ **Class member ordering** — Constructor first, public fields next, then other members ordered by dependency.
221
+
222
+ ```js
223
+ // Bad
224
+ class UserService {
225
+ private format() { /* ... */ }
226
+ name = 'default'
227
+ constructor() { /* ... */ }
228
+ }
229
+
230
+ // Good
231
+ class UserService {
232
+ constructor() { /* ... */ }
233
+ name = 'default'
234
+ private format() { /* ... */ }
235
+ }
236
+ ```
237
+
238
+ **Test file ordering** — Setup hooks (`beforeEach`, `beforeAll`) before teardown hooks (`afterEach`, `afterAll`), and both before test cases.
239
+
240
+ ```js
241
+ // Bad — setup and tests buried between helpers
242
+ function buildFixture(overrides) {
243
+ return { id: 1, ...overrides }
244
+ }
245
+ it('works', () => {
246
+ /* ... */
247
+ })
248
+ function assertCorrect(value) {
249
+ expect(value).toBe(1)
250
+ }
251
+ beforeEach(() => {
252
+ buildFixture()
253
+ })
254
+
255
+ // Good — setup first, then tests, helpers at the bottom
256
+ beforeEach(() => {
257
+ buildFixture()
258
+ })
259
+ it('works', () => {
260
+ /* ... */
261
+ })
262
+ function buildFixture(overrides) {
263
+ return { id: 1, ...overrides }
264
+ }
265
+ function assertCorrect(value) {
266
+ expect(value).toBe(1)
267
+ }
268
+ ```
269
+
270
+ ## A Note on Provenance
271
+
272
+ Yes, a fair amount of this was vibe-coded with LLM assistance — which is fitting, since that's exactly the context this plugin is designed for. That said, the ideas behind these rules, the decisions about what to catch and how to catch it, and the overall design are mine. Every piece of code went through human review, and the test cases in particular were written and verified with deliberate care.
273
+
274
+ The project also dogfoods itself: `eslint-plugin-unslop` is linted using `eslint-plugin-unslop`.
275
+
276
+ ## Contributing
277
+
278
+ See [AGENTS.md](./AGENTS.md) for development setup and guidelines.
279
+
280
+ ## License
281
+
282
+ [MIT](./LICENSE)