eslint-plugin-unslop 0.2.2 → 0.3.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # eslint-plugin-unslop
2
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.
3
+ ESLint plugin for architecture enforcement and code quality. Define module boundaries, control imports and exports, catch false sharing, and fix common LLM-generated code smells - all from a single shared configuration.
4
4
 
5
5
  Requires ESLint 9+ (flat config). TypeScript optional but recommended.
6
6
 
@@ -12,87 +12,67 @@ npm install --save-dev eslint-plugin-unslop
12
12
 
13
13
  ## Quick Start
14
14
 
15
- The recommended config enables the two 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
-
31
- The remaining rules need explicit configuration:
15
+ The full config enables the complete rule suite - architecture enforcement plus symbol fixers:
32
16
 
33
17
  ```js
34
18
  // eslint.config.mjs
35
19
  import unslop from 'eslint-plugin-unslop'
36
20
 
37
21
  export default [
38
- unslop.configs.recommended,
22
+ unslop.configs.full,
39
23
  {
40
- rules: {
41
- 'unslop/no-false-sharing': ['error', { dirs: ['shared', 'utils'] }],
42
- 'unslop/read-friendly-order': 'warn',
24
+ settings: {
25
+ unslop: {
26
+ sourceRoot: 'src',
27
+ architecture: {
28
+ utils: { shared: true },
29
+ 'repository/*': {
30
+ imports: ['utils', 'models/*'],
31
+ exports: ['^create\\w+Repo$', '^Repository[A-Z]\\w+$'],
32
+ },
33
+ 'models/*': {
34
+ imports: ['utils'],
35
+ },
36
+ app: {
37
+ imports: ['*'],
38
+ },
39
+ },
40
+ },
43
41
  },
44
42
  },
45
43
  ]
46
44
  ```
47
45
 
48
- ## Rules
49
-
50
- ### `unslop/no-special-unicode`
51
-
52
- **Recommended**
46
+ This turns on:
53
47
 
54
- 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.
48
+ | Rule | Severity | What it does |
49
+ | ---------------------------- | -------- | ------------------------------------------------------------------- |
50
+ | `unslop/import-control` | error | Enforces declared module import boundaries |
51
+ | `unslop/export-control` | error | Restricts public exports to declared patterns |
52
+ | `unslop/no-false-sharing` | error | Flags shared modules only used by one consumer |
53
+ | `unslop/no-special-unicode` | error | Catches smart quotes, invisible spaces, and other unicode impostors |
54
+ | `unslop/no-unicode-escape` | error | Prefers `"(c)"` over `"\u00A9"` |
55
+ | `unslop/read-friendly-order` | error | Enforces top-down, dependency-friendly declaration order |
55
56
 
56
- Caught characters include: left/right smart quotes (`“” ‘’`), non-breaking space, en/em dash, horizontal ellipsis, zero-width space, and various other exotic whitespace.
57
+ The `configs.minimal` config contains only the zero-config symbol fixers (`no-special-unicode` and `no-unicode-escape`). It is included automatically within `configs.full`, or can be used standalone for projects that don't need architecture enforcement:
57
58
 
58
59
  ```js
59
- // Bad — these contain invisible special characters that look normal
60
- const greeting = 'Hello World' // a non-breaking space (U+00A0) is hiding between the words
61
- const quote = 'He said “hello”' // smart double quotes (U+201C, U+201D)
60
+ // eslint.config.mjs
61
+ import unslop from 'eslint-plugin-unslop'
62
62
 
63
- // Good
64
- const greeting = 'Hello World' // regular ASCII space
65
- const quote = 'He said "hello"' // plain ASCII quotes
63
+ export default [unslop.configs.minimal]
66
64
  ```
67
65
 
68
- Note: the bad examples above contain actual unicode characters that may be
69
- indistinguishable from their ASCII counterparts in your font — that's exactly
70
- the problem this rule catches.
71
-
72
- ### `unslop/no-unicode-escape`
73
-
74
- **Recommended**
75
-
76
- 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.
77
-
78
- ```js
79
- // Bad
80
- const copyright = '\u00A9 2025'
81
- const arrow = '\u2192'
82
-
83
- // Good
84
- const copyright = '© 2025'
85
- const arrow = '→'
86
- ```
66
+ ## Rules
87
67
 
88
68
  ### `unslop/import-control`
89
69
 
90
- Think of this as customs control for your modules you declare which modules are allowed to import from which, and anything undeclared gets turned away at the border.
70
+ Think of this as customs control for your modules - you declare which modules are allowed to import from which, and anything undeclared gets turned away at the border.
91
71
 
92
72
  The rule reads from a shared policy in `settings.unslop.architecture`. It's deny-by-default for cross-module imports, which means forgetting to declare a dependency is a loud error rather than a silent free-for-all. It also enforces:
93
73
 
94
74
  - cross-module imports must arrive through the public gate (`index.ts` or `types.ts`)
95
- - same-module relative imports can only go one level deeper no tunnelling into internals
75
+ - same-module relative imports can only go one level deeper - no tunnelling into internals
96
76
  - files that don't match any declared module are denied (fail-closed, not fail-silently)
97
77
 
98
78
  #### Configuration
@@ -103,6 +83,7 @@ import unslop from 'eslint-plugin-unslop'
103
83
 
104
84
  export default [
105
85
  {
86
+ plugins: { unslop },
106
87
  settings: {
107
88
  unslop: {
108
89
  sourceRoot: 'src',
@@ -124,6 +105,7 @@ export default [
124
105
  rules: {
125
106
  'unslop/import-control': 'error',
126
107
  'unslop/export-control': 'error',
108
+ 'unslop/no-false-sharing': 'error',
127
109
  },
128
110
  },
129
111
  ]
@@ -133,77 +115,51 @@ export default [
133
115
 
134
116
  The customs declaration form for the other direction: what are you actually exporting from your module's public entrypoints?
135
117
 
136
- When a module defines `exports` regex patterns in `settings.unslop.architecture`, every symbol exported from that module's `index.ts` or `types.ts` must match at least one pattern otherwise it's stopped at the gate with an error at the export site. Modules without `exports` are waved through by default, so you can adopt this gradually.
118
+ When a module defines `exports` regex patterns in `settings.unslop.architecture`, every symbol exported from that module's `index.ts` or `types.ts` must match at least one pattern - otherwise it's stopped at the gate with an error at the export site. Modules without `exports` are waved through by default, so you can adopt this gradually.
137
119
 
138
120
  ### `unslop/no-false-sharing`
139
121
 
140
- **Requires TypeScript parser with program**
141
-
142
- 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.
143
-
144
- #### Options
145
-
146
- ```js
147
- ;[
148
- 'error',
149
- {
150
- dirs: [{ path: 'shared' }, { path: 'utils', mode: 'file' }],
151
- mode: 'dir',
152
- sourceRoot: 'src',
153
- },
154
- ]
155
- ```
156
-
157
- | Option | Type | Required | Description |
158
- | ------------- | ----------------- | -------- | -------------------------------------------------------------------------------------------- |
159
- | `dirs` | `array` | yes | Directories to enforce sharing rules on |
160
- | `dirs[].path` | `string` | yes | Directory path relative to `sourceRoot` |
161
- | `dirs[].mode` | `'file' \| 'dir'` | no | How to count consumers for this dir (overrides global `mode`) |
162
- | `mode` | `'file' \| 'dir'` | no | Global consumer counting mode — `'file'` counts individual files, `'dir'` counts directories |
163
- | `sourceRoot` | `string` | no | Source directory relative to project root |
122
+ The "shared" folder anti-pattern detector. LLMs (and some humans also) love creating shared utilities that are only used by one consumer - or worse, by nobody at all. This rule requires that modules marked as `shared` in your architecture settings are actually imported by at least two separate directory-level consumers. If it's only used in one place, it's not shared - it's misplaced.
164
123
 
165
- #### Setup
124
+ #### Configuration
166
125
 
167
- This rule requires `typescript-eslint` with type information:
126
+ Shared modules are declared via `shared: true` on module policies in
127
+ `settings.unslop.architecture`:
168
128
 
169
129
  ```js
170
130
  // eslint.config.mjs
171
131
  import unslop from 'eslint-plugin-unslop'
172
- import tseslint from 'typescript-eslint'
173
132
 
174
- export default tseslint.config(
175
- {
176
- languageOptions: {
177
- parserOptions: {
178
- projectService: true,
179
- },
180
- },
181
- },
182
- unslop.configs.recommended,
133
+ export default [
134
+ unslop.configs.full,
183
135
  {
184
- rules: {
185
- 'unslop/no-false-sharing': [
186
- 'error',
187
- {
188
- dirs: ['shared', 'utils'],
189
- sourceRoot: 'src',
136
+ settings: {
137
+ unslop: {
138
+ sourceRoot: 'src',
139
+ architecture: {
140
+ utils: { shared: true },
141
+ 'shared/*': { shared: true },
190
142
  },
191
- ],
143
+ },
192
144
  },
193
145
  },
194
- )
146
+ ]
195
147
  ```
196
148
 
149
+ The rule takes no options - all configuration comes from the shared architecture settings, consistent with `import-control` and `export-control`.
150
+
151
+ Consumer counting is always at the directory level: the first path segment relative to the source root. A file in `src/shared/format-date.ts` must be imported by files in at least two distinct top-level directories (e.g., `featureA` and `featureB`).
152
+
197
153
  #### What it catches
198
154
 
199
155
  ```
200
156
  src/shared/format-date.ts
201
- only imported by src/features/calendar/view.ts
202
- error: must be used by 2+ entities
157
+ -> only imported by src/features/calendar/view.ts
158
+ -> error: must be used by 2+ entities
203
159
 
204
160
  src/utils/old-helper.ts
205
- not imported by anyone
206
- error: must be used by 2+ entities
161
+ -> not imported by anyone
162
+ -> error: must be used by 2+ entities
207
163
  ```
208
164
 
209
165
  ### `unslop/read-friendly-order`
@@ -212,10 +168,10 @@ Enforces a top-down reading order for your code. The idea: when someone opens a
212
168
 
213
169
  This rule covers three areas:
214
170
 
215
- **Top-level ordering** Public/exported symbols should come before the private helpers they use. Read the API first, implementation details second.
171
+ **Top-level ordering** - Public/exported symbols should come before the private helpers they use. Read the API first, implementation details second.
216
172
 
217
173
  ```js
218
- // Bad helper defined before its consumer
174
+ // Bad - helper defined before its consumer
219
175
  function formatName(name) {
220
176
  return name.trim().toLowerCase()
221
177
  }
@@ -224,7 +180,7 @@ export function createUser(name) {
224
180
  return { name: formatName(name) }
225
181
  }
226
182
 
227
- // Good consumer first, helper below
183
+ // Good - consumer first, helper below
228
184
  export function createUser(name) {
229
185
  return { name: formatName(name) }
230
186
  }
@@ -234,7 +190,7 @@ function formatName(name) {
234
190
  }
235
191
  ```
236
192
 
237
- **Class member ordering** Constructor first, public fields next, then other members ordered by dependency.
193
+ **Class member ordering** - Constructor first, public fields next, then other members ordered by dependency.
238
194
 
239
195
  ```js
240
196
  // Bad
@@ -252,10 +208,10 @@ class UserService {
252
208
  }
253
209
  ```
254
210
 
255
- **Test file ordering** Setup hooks (`beforeEach`, `beforeAll`) before teardown hooks (`afterEach`, `afterAll`), and both before test cases.
211
+ **Test file ordering** - Setup hooks (`beforeEach`, `beforeAll`) before teardown hooks (`afterEach`, `afterAll`), and both before test cases.
256
212
 
257
213
  ```js
258
- // Bad setup and tests buried between helpers
214
+ // Bad - setup and tests buried between helpers
259
215
  function buildFixture(overrides) {
260
216
  return { id: 1, ...overrides }
261
217
  }
@@ -269,7 +225,7 @@ beforeEach(() => {
269
225
  buildFixture()
270
226
  })
271
227
 
272
- // Good setup first, then tests, helpers at the bottom
228
+ // Good - setup first, then tests, helpers at the bottom
273
229
  beforeEach(() => {
274
230
  buildFixture()
275
231
  })
@@ -284,9 +240,43 @@ function assertCorrect(value) {
284
240
  }
285
241
  ```
286
242
 
243
+ ### `unslop/no-special-unicode`
244
+
245
+ 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.
246
+
247
+ Caught characters include: left/right smart quotes (`“” ‘’`), non-breaking space, en/em dash, horizontal ellipsis, zero-width space, and various other exotic whitespace.
248
+
249
+ ```js
250
+ // Bad - these contain invisible special characters that look normal
251
+ const greeting = 'Hello World' // a non-breaking space (U+00A0) is hiding between the words
252
+ const quote = 'He said “hello”' // smart double quotes (U+201C, U+201D)
253
+
254
+ // Good
255
+ const greeting = 'Hello World' // regular ASCII space
256
+ const quote = 'He said "hello"' // plain ASCII quotes
257
+ ```
258
+
259
+ Note: the bad examples above contain actual unicode characters that may be
260
+ indistinguishable from their ASCII counterparts in your font - that's exactly
261
+ the problem this rule catches.
262
+
263
+ ### `unslop/no-unicode-escape`
264
+
265
+ Prefers actual characters over `\uXXXX` escape sequences. If your string says `\u00A9`, just write `(c)` - your coworkers will thank you. LLM-generated code sometimes encodes characters as escape sequences for no good reason.
266
+
267
+ ```js
268
+ // Bad
269
+ const copyright = '\u00A9 2025'
270
+ const arrow = '\u2192'
271
+
272
+ // Good
273
+ const copyright = '(c) 2025'
274
+ const arrow = '->'
275
+ ```
276
+
287
277
  ## A Note on Provenance
288
278
 
289
- 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.
279
+ 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.
290
280
 
291
281
  The project also dogfoods itself: `eslint-plugin-unslop` is linted using `eslint-plugin-unslop`.
292
282
 
@@ -332,12 +322,12 @@ Run these checks periodically:
332
322
 
333
323
  ```bash
334
324
  gh api repos/skhoroshavin/eslint-plugin-unslop/branches/main/protection
335
- gh run list --workflow "PR Gate" --limit 5
325
+ gh run list -workflow "Test" -limit 5
336
326
  ```
337
327
 
338
328
  Expected audit outcomes:
339
329
 
340
- - `required_status_checks.contexts` includes `PR Gate`
330
+ - `required_status_checks.contexts` includes `Test`
341
331
  - `required_pull_request_reviews.required_approving_review_count` is `1` or greater
342
332
  - `required_pull_request_reviews.dismiss_stale_reviews` is `true`
343
333
  - `enforce_admins.enabled` is `true`