eslint-plugin-unslop 0.5.0 → 0.5.1
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 +64 -94
- package/dist/index.cjs +266 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +266 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# eslint-plugin-unslop
|
|
2
2
|
|
|
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.
|
|
3
|
+
ESLint plugin for architecture enforcement and code quality. Define module boundaries, control imports and exports, catch false sharing and single-use constants, 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,7 +12,7 @@ npm install --save-dev eslint-plugin-unslop
|
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
15
|
-
The full config enables the complete rule suite
|
|
15
|
+
The `full` config enables the complete rule suite:
|
|
16
16
|
|
|
17
17
|
```js
|
|
18
18
|
// eslint.config.mjs
|
|
@@ -42,22 +42,19 @@ export default [
|
|
|
42
42
|
]
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Architecture rules (`import-control`, `export-control`, `no-false-sharing`) require a reachable
|
|
46
|
-
`tsconfig.json`. Set `compilerOptions.rootDir`, and if you use aliases, configure
|
|
47
|
-
`compilerOptions.paths`.
|
|
45
|
+
Architecture rules (`import-control`, `export-control`, `no-false-sharing`, `no-single-use-constants`) require a reachable `tsconfig.json`. Set `compilerOptions.rootDir`, and if you use aliases, configure `compilerOptions.paths`.
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
| Rule | What it does |
|
|
48
|
+
| -------------------------------- | ---------------------------------------------------------------------- |
|
|
49
|
+
| `unslop/import-control` | Enforces module boundaries and forbids local namespace imports |
|
|
50
|
+
| `unslop/export-control` | Restricts export patterns and forbids `export *` in module entrypoints |
|
|
51
|
+
| `unslop/no-false-sharing` | Flags shared entrypoint symbols with fewer than two consumer groups |
|
|
52
|
+
| `unslop/no-single-use-constants` | Flags module-scope constants used once or never across the project |
|
|
53
|
+
| `unslop/no-special-unicode` | Catches smart quotes, invisible spaces, and other unicode impostors |
|
|
54
|
+
| `unslop/no-unicode-escape` | Prefers `"©"` over `"\u00A9"` |
|
|
55
|
+
| `unslop/read-friendly-order` | Enforces top-down, dependency-friendly declaration order |
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
| ---------------------------- | -------- | ---------------------------------------------------------------------- |
|
|
53
|
-
| `unslop/import-control` | error | Enforces boundaries and forbids local namespace imports |
|
|
54
|
-
| `unslop/export-control` | error | Restricts export patterns and forbids `export *` in module entrypoints |
|
|
55
|
-
| `unslop/no-false-sharing` | error | Flags shared entrypoint symbols with fewer than two consumer groups |
|
|
56
|
-
| `unslop/no-special-unicode` | error | Catches smart quotes, invisible spaces, and other unicode impostors |
|
|
57
|
-
| `unslop/no-unicode-escape` | error | Prefers `"©"` over `"\u00A9"` |
|
|
58
|
-
| `unslop/read-friendly-order` | error | Enforces top-down, dependency-friendly declaration order |
|
|
59
|
-
|
|
60
|
-
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
|
+
The `minimal` config contains only the zero-config symbol fixers (`no-special-unicode` and `no-unicode-escape`) for projects that don't need architecture enforcement:
|
|
61
58
|
|
|
62
59
|
```js
|
|
63
60
|
// eslint.config.mjs
|
|
@@ -66,13 +63,27 @@ import unslop from 'eslint-plugin-unslop'
|
|
|
66
63
|
export default [unslop.configs.minimal]
|
|
67
64
|
```
|
|
68
65
|
|
|
66
|
+
## Architecture Settings
|
|
67
|
+
|
|
68
|
+
All architecture rules read from `settings.unslop.architecture`. Each key is a module matcher (path segments, `*` per segment), and each value is a policy object:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
{
|
|
72
|
+
imports?: string[] // module matchers this module may import from; '*' allows all
|
|
73
|
+
exports?: string[] // regex patterns symbols exported from index.ts/types.ts must match
|
|
74
|
+
shared?: boolean // marks module as shared; enables no-false-sharing
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Best match wins by fewest wildcards, then longest matcher, then declaration order. All architecture rules take no options - policy comes entirely from this shared settings block.
|
|
79
|
+
|
|
69
80
|
## Rules
|
|
70
81
|
|
|
71
82
|
### `unslop/import-control`
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
Customs control for your modules: you declare which modules are allowed to import from which, and anything undeclared gets turned away at the border.
|
|
74
85
|
|
|
75
|
-
|
|
86
|
+
Deny-by-default for cross-module imports, so forgetting to declare a dependency is a loud error rather than a silent free-for-all. It also enforces:
|
|
76
87
|
|
|
77
88
|
- cross-module imports must arrive through the public gate (`index.ts` or `types.ts`)
|
|
78
89
|
- local cross-module namespace imports are forbidden (`import * as X from '<local-module>'`)
|
|
@@ -81,80 +92,30 @@ The rule reads from a shared policy in `settings.unslop.architecture`. It's deny
|
|
|
81
92
|
|
|
82
93
|
Alias imports are resolved via `compilerOptions.paths` from `tsconfig.json`.
|
|
83
94
|
|
|
84
|
-
#### Configuration
|
|
85
|
-
|
|
86
|
-
```js
|
|
87
|
-
// eslint.config.mjs
|
|
88
|
-
import unslop from 'eslint-plugin-unslop'
|
|
89
|
-
|
|
90
|
-
export default [
|
|
91
|
-
{
|
|
92
|
-
plugins: { unslop },
|
|
93
|
-
settings: {
|
|
94
|
-
unslop: {
|
|
95
|
-
architecture: {
|
|
96
|
-
utils: { shared: true },
|
|
97
|
-
'repository/*': {
|
|
98
|
-
imports: ['utils', 'models/*'],
|
|
99
|
-
exports: ['^create\\w+Repo$', '^Repository[A-Z]\\w+$'],
|
|
100
|
-
},
|
|
101
|
-
'models/*': {
|
|
102
|
-
imports: ['utils'],
|
|
103
|
-
},
|
|
104
|
-
app: {
|
|
105
|
-
imports: ['*'],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
rules: {
|
|
111
|
-
'unslop/import-control': 'error',
|
|
112
|
-
'unslop/export-control': 'error',
|
|
113
|
-
'unslop/no-false-sharing': 'error',
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
]
|
|
117
|
-
```
|
|
118
|
-
|
|
119
95
|
### `unslop/export-control`
|
|
120
96
|
|
|
121
97
|
The customs declaration form for the other direction: what are you actually exporting from your module's public entrypoints?
|
|
122
98
|
|
|
123
|
-
When a module defines `exports` regex patterns
|
|
99
|
+
When a module defines `exports` regex patterns, every symbol exported from its `index.ts` or `types.ts` must match at least one pattern - otherwise it's stopped at the gate. Modules without `exports` are waved through by default, so you can adopt this gradually. Regardless of module policy, `export * from ...` is rejected in public entrypoints so symbol provenance stays explicit.
|
|
124
100
|
|
|
125
101
|
### `unslop/no-false-sharing`
|
|
126
102
|
|
|
127
|
-
The "shared" folder anti-pattern detector. LLMs (and some humans
|
|
128
|
-
|
|
129
|
-
#### Configuration
|
|
103
|
+
The "shared" folder anti-pattern detector. LLMs (and some humans) love creating shared APIs that are only used by one consumer - or worse, by nobody at all. This rule evaluates symbols exported from shared module entrypoints and requires each to be imported by at least two separate directory-level consumer groups. If a symbol is used in only one place, it's not shared - it's misplaced.
|
|
130
104
|
|
|
131
|
-
|
|
132
|
-
`settings.unslop.architecture`:
|
|
105
|
+
Mark a module as shared via `shared: true`:
|
|
133
106
|
|
|
134
107
|
```js
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
settings: {
|
|
142
|
-
unslop: {
|
|
143
|
-
architecture: {
|
|
144
|
-
utils: { shared: true },
|
|
145
|
-
'shared/*': { shared: true },
|
|
146
|
-
},
|
|
147
|
-
},
|
|
108
|
+
settings: {
|
|
109
|
+
unslop: {
|
|
110
|
+
architecture: {
|
|
111
|
+
utils: { shared: true },
|
|
112
|
+
'shared/*': { shared: true },
|
|
148
113
|
},
|
|
149
114
|
},
|
|
150
|
-
|
|
115
|
+
}
|
|
151
116
|
```
|
|
152
117
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
Consumer counting is always at the directory level: the importer file path relative to the source root derived from `tsconfig.json`, minus filename. Both value imports and `import type` imports count as consumers, and alias imports configured in `compilerOptions.paths` are resolved the same as relative imports.
|
|
156
|
-
|
|
157
|
-
#### What it catches
|
|
118
|
+
Consumer counting is at the directory level: the importer file path relative to the source root derived from `tsconfig.json`, minus filename. Both value imports and `import type` imports count, and alias imports from `compilerOptions.paths` are resolved the same as relative imports.
|
|
158
119
|
|
|
159
120
|
```
|
|
160
121
|
src/shared/index.ts
|
|
@@ -168,11 +129,28 @@ src/shared/types.ts
|
|
|
168
129
|
-> error: symbol "LegacyOptions" has 0 consumer group(s) (no consumers found)
|
|
169
130
|
```
|
|
170
131
|
|
|
171
|
-
### `unslop/
|
|
132
|
+
### `unslop/no-single-use-constants`
|
|
133
|
+
|
|
134
|
+
Flags module-scope `const` declarations that are used once or never across the entire project. LLM-generated code loves to extract magic values into constants that are then referenced a single time - adding indirection without improving clarity. If a constant isn't actually reused, inline it or delete it.
|
|
135
|
+
|
|
136
|
+
Non-exported constants are counted locally via scope analysis. Exported constants are counted project-wide using the TypeScript program, so cross-file usage is detected. Function and class-expression initializers are ignored - only real value constants are checked.
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
// Bad - defined once, used once
|
|
140
|
+
const MAX_RETRIES = 3
|
|
141
|
+
function fetchWithRetry() {
|
|
142
|
+
return retry(MAX_RETRIES)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Good - inline it
|
|
146
|
+
function fetchWithRetry() {
|
|
147
|
+
return retry(3)
|
|
148
|
+
}
|
|
149
|
+
```
|
|
172
150
|
|
|
173
|
-
|
|
151
|
+
### `unslop/read-friendly-order`
|
|
174
152
|
|
|
175
|
-
|
|
153
|
+
Enforces a top-down reading order. 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.
|
|
176
154
|
|
|
177
155
|
**Top-level ordering** - Public/exported symbols should come before the private helpers they use. Read the API first, implementation details second.
|
|
178
156
|
|
|
@@ -224,9 +202,6 @@ function buildFixture(overrides) {
|
|
|
224
202
|
it('works', () => {
|
|
225
203
|
/* ... */
|
|
226
204
|
})
|
|
227
|
-
function assertCorrect(value) {
|
|
228
|
-
expect(value).toBe(1)
|
|
229
|
-
}
|
|
230
205
|
beforeEach(() => {
|
|
231
206
|
buildFixture()
|
|
232
207
|
})
|
|
@@ -241,20 +216,17 @@ it('works', () => {
|
|
|
241
216
|
function buildFixture(overrides) {
|
|
242
217
|
return { id: 1, ...overrides }
|
|
243
218
|
}
|
|
244
|
-
function assertCorrect(value) {
|
|
245
|
-
expect(value).toBe(1)
|
|
246
|
-
}
|
|
247
219
|
```
|
|
248
220
|
|
|
249
221
|
### `unslop/no-special-unicode`
|
|
250
222
|
|
|
251
|
-
Disallows special unicode punctuation and whitespace characters in string
|
|
223
|
+
Disallows special unicode punctuation and whitespace characters in string 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.
|
|
252
224
|
|
|
253
|
-
Caught characters include:
|
|
225
|
+
Caught characters include: smart quotes (`“” ‘’`), non-breaking space, en/em dash, horizontal ellipsis, zero-width space, and various other exotic whitespace.
|
|
254
226
|
|
|
255
227
|
```js
|
|
256
228
|
// Bad - these contain invisible special characters that look normal
|
|
257
|
-
const greeting = 'Hello
|
|
229
|
+
const greeting = 'Hello World' // a non-breaking space (U+00A0) is hiding between the words
|
|
258
230
|
const quote = 'He said “hello”' // smart double quotes (U+201C, U+201D)
|
|
259
231
|
|
|
260
232
|
// Good
|
|
@@ -262,13 +234,11 @@ const greeting = 'Hello World' // regular ASCII space
|
|
|
262
234
|
const quote = 'He said "hello"' // plain ASCII quotes
|
|
263
235
|
```
|
|
264
236
|
|
|
265
|
-
Note: the bad examples above contain actual unicode characters that may be
|
|
266
|
-
indistinguishable from their ASCII counterparts in your font - that's exactly
|
|
267
|
-
the problem this rule catches.
|
|
237
|
+
Note: the bad examples above contain actual unicode characters that may be indistinguishable from their ASCII counterparts in your font - that's exactly the problem this rule catches.
|
|
268
238
|
|
|
269
239
|
### `unslop/no-unicode-escape`
|
|
270
240
|
|
|
271
|
-
Prefers actual characters over `\uXXXX` escape sequences. If your string says `\u00A9`, just write `©` - your coworkers will thank you.
|
|
241
|
+
Prefers actual characters over `\uXXXX` escape sequences. If your string says `\u00A9`, just write `©` - your coworkers will thank you.
|
|
272
242
|
|
|
273
243
|
```js
|
|
274
244
|
// Bad
|