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 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 - architecture enforcement plus symbol fixers:
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
- This turns on:
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
- | Rule | Severity | What it does |
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
- 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.
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
- 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:
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 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. Regardless of module policy, `export * from ...` is rejected in `index.ts` and `types.ts` so symbol provenance stays explicit.
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 also) 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 (`index.ts` and `types.ts`) and requires each exported symbol 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.
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
- Shared modules are declared via `shared: true` on module policies in
132
- `settings.unslop.architecture`:
105
+ Mark a module as shared via `shared: true`:
133
106
 
134
107
  ```js
135
- // eslint.config.mjs
136
- import unslop from 'eslint-plugin-unslop'
137
-
138
- export default [
139
- unslop.configs.full,
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
- The rule takes no options - all configuration comes from the shared architecture settings, consistent with `import-control` and `export-control`.
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/read-friendly-order`
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
- 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.
151
+ ### `unslop/read-friendly-order`
174
152
 
175
- This rule covers three areas:
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 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.
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: left/right smart quotes (`“” ‘’`), non-breaking space, en/em dash, horizontal ellipsis, zero-width space, and various other exotic whitespace.
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 World' // a non-breaking space (U+00A0) is hiding between the words
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. LLM-generated code sometimes encodes characters as escape sequences for no good reason.
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