eslint-plugin-constructors 1.4.3 → 1.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
@@ -4,12 +4,12 @@
4
4
 
5
5
  <a target="_blank" href="https://www.npmjs.com/package/eslint-plugin-constructors">
6
6
  <img alt="[Latest Build]" src="https://img.shields.io/npm/v/eslint-plugin-constructors?logo=icinga&style=for-the-badge&label=Latest%20Build&color=32fcee"></a>
7
- <a href="#license">
7
+ <a href="https://codeberg.org/adamlui/eslint-plugin-constructors/src/branch/main/docs/LICENSE.md">
8
8
  <img alt="[License: MIT]" src="https://img.shields.io/badge/License-MIT-f1f20a.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
9
9
  <a target="_blank" href="https://getnpmstats.com/?package=eslint-plugin-constructors&range=last-year&interval=monthly">
10
10
  <img src="https://img.shields.io/npm/dm/eslint-plugin-constructors?style=for-the-badge&logo=npm&color=black"></a>
11
- <a target="_blank" href="https://eslint.org/">
12
- <img alt="[Requires ESLint >=9.0.0]" src="https://img.shields.io/badge/Requires-ESLint_%3E%3D9.0.0-4B32C3?style=for-the-badge&logo=eslint"></a>
11
+ <a href="#usage">
12
+ <img alt="[Supports ESLint >=9.0.0 / Oxlint >=1.65.0]" src="https://img.shields.io/badge/Supports-ESLint_%3E%3D9.0.0_%2F_Oxlint_%3E%3D1.65.0-4B32C3?style=for-the-badge&logo=eslint"></a>
13
13
  <br><br>
14
14
 
15
15
  ESLint rules to enforce best practices for constructor usage.
@@ -33,6 +33,8 @@ npm i -D eslint-plugin-constructors
33
33
  Usage
34
34
  </h2>
35
35
 
36
+ ### <img height=15 width=auto src="https://www.google.com/s2/favicons?domain=eslint.org&sz=64"> ESLint
37
+
36
38
  Add a bundled config to your `eslint.config.*js`:
37
39
 
38
40
  ```js
@@ -46,41 +48,195 @@ export default [
46
48
  rules: {
47
49
  ...constructors.configs.recommended.rules,
48
50
  'constructors/no-explicit-return': 'off', // selectively disable rule
49
- 'constructors/require-new': ['error', { mode: 'throwing' }] // selectively tewak rule
51
+ 'constructors/require-new': ['error', { mode: 'throwing' }] // selectively tweak rule
50
52
  }
51
53
  }
52
54
  ]
53
55
  ```
54
56
 
55
- Available bundles: `recommended`, `problem` (deterministic bugs only)
56
-
57
57
  **Note:** Rule config is either a severity string (`'off'`, `'warn'`, `'error'`) or an array `[severity, options]` when the rule accepts options.
58
58
 
59
59
  ---
60
60
 
61
+ ### <img height=15 width=auto src="https://www.google.com/s2/favicons?domain=oxc.rs&sz=64"> Oxlint
62
+
63
+ Add a bundled config to your `oxlint.config.ts`:
64
+
65
+ ```ts
66
+ ...
67
+ import constructors from 'eslint-plugin-constructors'
68
+
69
+ export default defineConfig({
70
+ overrides: [
71
+ // other overrides
72
+ {
73
+ files: ['**/*.js'],
74
+ jsPlugins: ['eslint-plugin-constructors'],
75
+ rules: {
76
+ ...constructors.configs.recommended.rules,
77
+ 'constructors/no-explicit-return': 'allow', // selectively disable rule
78
+ 'constructors/require-new': ['deny', { mode: 'throwing' }] // selectively tweak rule
79
+ }
80
+ }
81
+ ]
82
+ })
83
+ ```
84
+
85
+ **Note:** Rule config is either a severity string (`'allow'`, `'warn'`, `'deny'`) or an array `[severity, options]` when the rule accepts options.
86
+
87
+ **Note:** Oxlint is typically **6-20× faster than ESLint** w/ this plugin (due to its Rust core and parallel file processing).
88
+
89
+ ---
90
+
91
+ *Available bundles:* `recommended`, `problem` (deterministic bugs only)
92
+
93
+ ---
94
+
61
95
  <h2 id="supported-rules">
62
96
  <img height=30 width=27 align="top" src="https://api.iconify.design/mdi/format-list-checks.svg?color=%232b2b2a">
63
97
  Supported rules
64
98
  </h2>
65
99
 
66
- | ID | Description | Auto-fixable | Recommended |
67
- | --- | --- | :-: | :-: |
68
- | [`no-explicit-return`](#no-explicit-return) | Disallows explicit `return` statements inside class constructors | <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> | <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> |
69
- | [`require-error-subclass-name`](#require-error-subclass-name) | Requires classes extending `Error` have a name ending in `Error` | <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> | <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> |
70
- | [`require-new`](#require-new) | Requires `new` when calling error constructors (and optionally other built-ins) | <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> | <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> |
100
+ <h3 id="no-constructor-bind">
101
+ <code>no-constructor-bind</code>
102
+ <a title="Introduced in latest minor release" target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.5.0">
103
+ <img height=16 align="middle" src="https://api.iconify.design/mdi/sprout.svg?color=%234a4a4a"></a>
104
+ <a title="Included in recommended config" href="#usage">
105
+ <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
106
+ <a title="Accepts eslint --fix" target="_blank" href="https://eslint.org/docs/latest/use/command-line-interface#--fix">
107
+ <img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
108
+ <a title="Doesn't prevent bugs" href="#no-constructor-bind">
109
+ <img height=18 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
110
+ <a title="Copy rule URL" target="_blank"
111
+ href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23no-constructor-bind">
112
+ <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
113
+ </h3>
114
+
115
+ <details>
116
+
117
+ <summary>Disallows binding methods to <code>this</code> inside class constructors (use class arrow functions instead).</summary>
118
+
119
+ #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
120
+
121
+ ```diff
122
+ class User {
123
+ constructor() {
124
+ ! this.greet = this.greet.bind(this)
125
+ }
126
+ greet() { return 'Hello' }
127
+ }
128
+ ```
129
+
130
+ #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> Correct code
131
+
132
+ ```diff
133
+ class User {
134
+ - constructor() {
135
+ - this.greet = this.greet.bind(this)
136
+ - }
137
+ + greet = () => 'Hello'
138
+ }
139
+ ```
140
+
141
+ #### When not to use it
142
+
143
+ If you target environments that do not support class fields.
144
+
145
+ #### Introduced in
146
+
147
+ <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.5.0">v1.5.0</a> (October 19, 2026)
148
+
149
+ #### See also
150
+
151
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64">&ensp;<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind">MDN — `Function.prototype.bind()`</a>
152
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64">&ensp;<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields">MDN — Public class fields</a>
153
+
154
+ <div align="center">
155
+
156
+ [Back to index ↑][epc-rules]
157
+
158
+ </div>
159
+
160
+ ---
161
+
162
+ </details>
163
+
164
+ <h3 id="no-constructor-state">
165
+ <code>no-constructor-state</code>
166
+ <a title="Introduced in latest minor release" target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.5.0">
167
+ <img height=16 align="middle" src="https://api.iconify.design/mdi/sprout.svg?color=%234a4a4a"></a>
168
+ <a title="Included in recommended config" href="#usage">
169
+ <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
170
+ <a title="Accepts eslint --fix" target="_blank" href="https://eslint.org/docs/latest/use/command-line-interface#--fix">
171
+ <img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
172
+ <a title="Doesn't prevent bugs" href="#no-constructor-state">
173
+ <img height=18 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
174
+ <a title="Copy rule URL" target="_blank"
175
+ href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23no-constructor-state">
176
+ <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
177
+ </h3>
178
+
179
+ <details>
180
+
181
+ <summary>Disallows assigning simple initial state to <code>this</code> inside class constructors (use class fields instead).</summary>
182
+
183
+ #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
184
+
185
+ ```diff
186
+ class Component {
187
+ constructor() {
188
+ ! this.state = { ready: false }
189
+ }
190
+ }
191
+ ```
192
+
193
+ #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> Correct code
194
+
195
+ ```diff
196
+ class Component {
197
+ - constructor() {
198
+ - this.state = { ready: false }
199
+ - }
200
+ + state = { ready: false }
201
+ }
202
+ ```
203
+
204
+ Dynamic initialization (e.g. `this.state = createInitialState()`) is **allowed** by default, because the value is not a simple literal.
205
+
206
+ #### When not to use it
207
+
208
+ If you target environments that do not support class fields.
209
+
210
+ #### Introduced in
211
+
212
+ <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.5.0">v1.5.0</a> (October 19, 2026)
213
+
214
+ #### See also
215
+
216
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64">&ensp;<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields">MDN — Public class fields</a>
217
+
218
+ <div align="center">
219
+
220
+ [Back to index ↑][epc-rules]
221
+
222
+ </div>
71
223
 
72
224
  ---
73
225
 
226
+ </details>
227
+
74
228
  <h3 id="no-explicit-return">
75
229
  <code>no-explicit-return</code>
76
230
  <a title="Included in recommended config" href="#usage">
77
231
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
78
- <a title="Copy rule URL" target="_blank"
232
+ <a title="Copy rule URL" target="_blank"
79
233
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23no-explicit-return">
80
234
  <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
81
235
  </h3>
82
236
 
83
- Disallows explicit `return` statements inside class constructors.
237
+ <details>
238
+
239
+ <summary>Disallows explicit <code>return</code> statements inside class constructors.</summary>
84
240
 
85
241
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
86
242
 
@@ -106,7 +262,7 @@ Disallows explicit `return` statements inside class constructors.
106
262
  - return
107
263
  + this.x = 1
108
264
  }
109
-
265
+
110
266
  class B {
111
267
  constructor() {
112
268
  - return this
@@ -119,6 +275,14 @@ Disallows explicit `return` statements inside class constructors.
119
275
 
120
276
  If you need to return a different object from a constructor.
121
277
 
278
+ #### Introduced in
279
+
280
+ <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.0.0">v1.0.0</a> (September 9, 2026)
281
+
282
+ #### See also
283
+
284
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64">&ensp;<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor">MDN — Class `constructor`</a>
285
+
122
286
  <div align="center">
123
287
 
124
288
  [Back to index ↑][epc-rules]
@@ -127,18 +291,22 @@ If you need to return a different object from a constructor.
127
291
 
128
292
  ---
129
293
 
294
+ </details>
295
+
130
296
  <h3 id="require-error-subclass-name">
131
297
  <code>require-error-subclass-name</code>
132
298
  <a title="Included in recommended config" href="#usage">
133
299
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
134
- <a title="Doesn't prevent bugs" href="#meta-spacing">
300
+ <a title="Doesn't prevent bugs" href="#require-error-subclass-name">
135
301
  <img height=18 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
136
- <a title="Copy rule URL" target="_blank"
302
+ <a title="Copy rule URL" target="_blank"
137
303
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23require-error-subclass-name">
138
304
  <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
139
305
  </h3>
140
306
 
141
- Requires classes extending `Error` have a name ending in `Error`.
307
+ <details>
308
+
309
+ <summary>Requires classes extending <code>Error</code> have a name ending in <code>Error</code>.</summary>
142
310
 
143
311
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
144
312
 
@@ -160,6 +328,14 @@ Requires classes extending `Error` have a name ending in `Error`.
160
328
 
161
329
  If you don't follow the convention of suffixing custom error classes w/ `Error`.
162
330
 
331
+ #### Introduced in
332
+
333
+ <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.0.0">v1.0.0</a> (September 9, 2026)
334
+
335
+ #### See also
336
+
337
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64">&ensp;<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error">MDN — `Error`</a>
338
+
163
339
  <div align="center">
164
340
 
165
341
  [Back to index ↑][epc-rules]
@@ -168,6 +344,8 @@ If you don't follow the convention of suffixing custom error classes w/ `Error`.
168
344
 
169
345
  ---
170
346
 
347
+ </details>
348
+
171
349
  <h3 id="require-new">
172
350
  <code>require-new</code>
173
351
  <a title="Included in recommended config" href="#usage">
@@ -176,16 +354,18 @@ If you don't follow the convention of suffixing custom error classes w/ `Error`.
176
354
  <img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
177
355
  <a title="Accepts array w/ options" href="#usage">
178
356
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
179
- <a title="Copy rule URL" target="_blank"
357
+ <a title="Copy rule URL" target="_blank"
180
358
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23require-new">
181
359
  <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
182
360
  </h3>
183
361
 
184
- Requires `new` when calling error constructors (and optionally other built-ins).
362
+ <details>
363
+
364
+ <summary>Requires <code>new</code> when calling error constructors (and optionally other built-ins).</summary>
185
365
 
186
366
  #### Options
187
367
 
188
- - `mode` (default `'errors'`) – constructor category to enforce `new` on (`'errors'`, `'throwing'`, `'all'`)
368
+ - `mode` (default `'errors'`) – constructor category to enforce `new` on (`'errors'`, `'throwing'`, `'all'`) <a title="Introduced in v1.0.0" target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.0.0"><img height=14 alt="[Since v1.0.0]" src="https://img.shields.io/badge/since-v1.0.0-8a9199?style=flat-square" align="middle"></a>
189
369
 
190
370
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
191
371
 
@@ -220,12 +400,23 @@ Requires `new` when calling error constructors (and optionally other built-ins).
220
400
 
221
401
  If you prefer calling built-ins w/o `new` (which the runtime allows for some of them) and don't want to enforce it for consistency.
222
402
 
403
+ #### Introduced in
404
+
405
+ <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.0.0">v1.0.0</a> (September 9, 2026)
406
+
407
+ #### See also
408
+
409
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64">&ensp;<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new">MDN — `new` operator</a>
410
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64">&ensp;<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error">MDN — `Error` objects</a>
411
+
223
412
  <div align="center">
224
413
 
225
414
  [Back to index ↑][epc-rules]
226
415
 
227
416
  </div>
228
417
 
418
+ </details>
419
+
229
420
  ---
230
421
 
231
422
  ## <img width=30 align="top" src="https://api.iconify.design/boxicons/code-alt-filled.svg?color=%232b2b2a"> Shield embed
@@ -250,8 +441,8 @@ If you prefer calling built-ins w/o `new` (which the runtime allows for some of
250
441
 
251
442
  ## <img height=30 width=27 align="top" src="https://api.iconify.design/ic/baseline-account-balance.svg?color=%232b2b2a"> License
252
443
 
253
- <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/src/branch/main/docs/LICENSE.md">
254
- <img height=85 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/9e820e5d51/assets/images/logos/mit-license/red-pixelated.png"></a>
444
+ <a href="https://codeberg.org/adamlui/eslint-plugin-constructors/src/branch/main/docs/LICENSE.md">
445
+ <img height=88 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-chrome-extensions/d79e1db7e2/assets/images/banners/mit-license/banner.png"></a>
255
446
  <br><br>
256
447
 
257
448
  <details>
package/docs/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- # 🏛️ MIT License
1
+ # MIT License
2
2
 
3
3
  **Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui).**
4
4
 
package/package.json CHANGED
@@ -1,22 +1,25 @@
1
1
  {
2
2
  "name": "eslint-plugin-constructors",
3
- "version": "1.4.3",
3
+ "version": "1.5.1",
4
4
  "description": "ESLint rules for constructor usage",
5
- "type": "module",
6
- "engines": {
7
- "node": ">=18.18.0"
8
- },
9
- "author": {
10
- "name": "Adam Lui",
11
- "email": "adam@kudoai.org",
12
- "url": "https://codeberg.org/adamlui"
13
- },
5
+ "keywords": [
6
+ "code-quality",
7
+ "constructor",
8
+ "error",
9
+ "eslint",
10
+ "eslint-plugin",
11
+ "eslintplugin",
12
+ "security"
13
+ ],
14
14
  "homepage": "https://codeberg.org/adamlui/eslint-plugin-constructors/#readme",
15
15
  "bugs": {
16
16
  "url": "https://codeberg.org/adamlui/eslint-plugin-constructors/issues",
17
17
  "email": "support@adamlui.com"
18
18
  },
19
- "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://codeberg.org/adamlui/eslint-plugin-constructors.git"
22
+ },
20
23
  "funding": [
21
24
  {
22
25
  "type": "ko-fi",
@@ -27,58 +30,61 @@
27
30
  "url": "https://paypal.me/adamlui"
28
31
  }
29
32
  ],
30
- "main": "src/index.js",
33
+ "license": "MIT",
34
+ "author": {
35
+ "name": "Adam Lui",
36
+ "email": "adam@kudoai.org",
37
+ "url": "https://codeberg.org/adamlui"
38
+ },
39
+ "type": "module",
40
+ "exports": {
41
+ ".": {
42
+ "types": "./src/index.d.ts",
43
+ "default": "./src/index.js"
44
+ }
45
+ },
46
+ "main": "./src/index.js",
47
+ "types": "./src/index.d.ts",
31
48
  "files": [
32
49
  "docs/",
33
50
  "src/"
34
51
  ],
35
- "types": "src/index.d.ts",
36
52
  "scripts": {
37
- "lint": "eslint . --cache && yamllint .",
38
- "lint:all": "eslint . && yamllint .",
39
- "lint:fix": "eslint . --fix --cache && yamllint .",
40
- "lint:fix-all": "eslint . --fix && yamllint .",
41
- "test": "node tests/run.js",
42
- "demo": "node demo/run.js",
43
- "bump:patch": "bash utils/bump.sh patch",
44
- "bump:minor": "bash utils/bump.sh minor",
45
53
  "bump:feat": "npm run bump:minor",
46
- "bump:major": "bash utils/bump.sh major"
47
- },
48
- "repository": {
49
- "type": "git",
50
- "url": "git+https://codeberg.org/adamlui/eslint-plugin-constructors.git"
54
+ "bump:major": "bash utils/bump.sh major",
55
+ "bump:minor": "bash utils/bump.sh minor",
56
+ "bump:patch": "bash utils/bump.sh patch",
57
+ "demo": "eslint demo",
58
+ "lint": "pre-commit run",
59
+ "lint:all": "pre-commit run --all-files",
60
+ "test": "node tests/run"
51
61
  },
52
- "keywords": [
53
- "code-quality",
54
- "constructor",
55
- "error",
56
- "eslint",
57
- "eslint-plugin",
58
- "eslintplugin",
59
- "security"
60
- ],
61
62
  "dependencies": {
62
63
  "globals": "^17.13.0"
63
64
  },
64
- "peerDependencies": {
65
- "eslint": ">=9.0.0"
66
- },
67
65
  "devDependencies": {
68
66
  "@e18e/eslint-plugin": "^0.8.1",
69
- "@eslint/js": "^10.0.1",
70
67
  "@eslint/json": "^2.1.1",
71
68
  "@eslint/markdown": "^7.5.1",
72
69
  "@stylistic/eslint-plugin": "^5.10.0",
73
- "eslint": "^10.12.0",
70
+ "eslint": "^10.13.0",
74
71
  "eslint-plugin-eslint-plugin": "^7.6.2",
75
72
  "eslint-plugin-import-x": "^4.17.1",
76
- "eslint-plugin-n": "^18.4.1",
73
+ "eslint-plugin-n": "^18.5.0",
77
74
  "eslint-plugin-no-secrets": "^2.3.3",
78
75
  "eslint-plugin-no-unsanitized": "^4.1.5",
76
+ "eslint-plugin-package-json": "^1.10.1",
79
77
  "eslint-plugin-promise": "^7.3.0",
80
78
  "eslint-plugin-regexp": "^3.3.1",
81
79
  "eslint-plugin-security": "^4.2.0",
82
- "yamllint-js": "^0.2.4"
80
+ "eslint-plugin-sonarjs": "^4.2.2",
81
+ "eslint-plugin-woodpecker": "^1.1.0",
82
+ "oxlint": "^1.89.0"
83
+ },
84
+ "peerDependencies": {
85
+ "eslint": ">=9.0.0"
86
+ },
87
+ "engines": {
88
+ "node": ">=18.18.0"
83
89
  }
84
90
  }
@@ -1,10 +1,9 @@
1
- // Delete a line to end support
2
-
3
- export const rules = { // old: new
4
- 'require-new': {
5
- 'require-new': { mode: 'errors', // 2026-9-24 announced certain default switch + possible unnamed syntax removal
6
- builtins: 'all', 'error-only': 'errors' } // 2026-9-13 announced removal @ v2
1
+ export default {
2
+ rules: {
3
+ 'require-new': {
4
+ default: { mode: 'errors' }, // 2026-9-24 announced default switch
5
+ wrapKey: 'mode', // 2026-9-24 announced potential removal
6
+ renames: { builtins: 'all', 'error-only': 'errors' } // 2026-9-13 announced removal @ v2
7
+ }
7
8
  }
8
9
  }
9
-
10
- export default { rules }
package/src/index.d.ts CHANGED
@@ -1,6 +1,31 @@
1
+ type RuleSeverity = 0 | 1 | 2 | 'off' | 'warn' | 'error'
2
+ type RuleSetting = RuleSeverity | [RuleSeverity, ...unknown[]]
3
+
4
+ type ConfigName =
5
+ | 'recommended'
6
+ | 'problem'
7
+
8
+ interface PluginConfig {
9
+ plugins: Record<string, unknown>
10
+ rules: Record<string, RuleSetting>
11
+ }
12
+
13
+ interface PluginRule {
14
+ meta: {
15
+ type?: 'problem' | 'suggestion' | 'layout'
16
+ deprecated?: boolean | {
17
+ message?: string
18
+ replacedBy?: Array<{ rule: { name: string } }>
19
+ }
20
+ docs?: Record<string, unknown>
21
+ }
22
+ create: (context: unknown) => Record<string, (...args: unknown[]) => void>
23
+ }
24
+
1
25
  declare const plugin: {
2
- rules: Record<string, unknown>
3
- configs: { recommended: Record<string, unknown> }
26
+ meta: { name: string }
27
+ rules: Record<string, PluginRule>
28
+ configs: Record<ConfigName, PluginConfig>
4
29
  }
5
30
 
6
- export default plugin
31
+ export { plugin as default, plugin as constructors }
package/src/index.js CHANGED
@@ -1,34 +1,9 @@
1
- import { rules as oldRules } from './data/deprecations.js'
1
+ import init from './utils/init.js'
2
2
 
3
- import noExplicitReturn from './rules/no-explicit-return.js'
4
- import requireErrorSubclassName from './rules/require-error-subclass-name.js'
5
- import requireNew from './rules/require-new.js'
3
+ const plugin = init.plugin()
6
4
 
7
- import { bundleRules, initDeprecatedAlias } from './utils.js'
8
-
9
- const plugin = {
10
- rules: {
11
- 'no-explicit-return': noExplicitReturn,
12
- 'require-error-subclass-name': requireErrorSubclassName,
13
- 'require-new': requireNew
14
- }
15
- }
16
-
17
- for (const [ruleId, rule] of Object.entries(plugin.rules))
18
- rule.meta.docs.url = `https://codeberg.org/adamlui/eslint-plugin-constructors/#${ruleId}`
19
-
20
- for (const entry of Object.entries(oldRules))
21
- initDeprecatedAlias(plugin, entry)
22
-
23
- plugin.configs = {
24
- recommended: {
25
- plugins: { constructors: plugin },
26
- rules: bundleRules(plugin.rules, rule => rule.meta.docs.recommended, 'constructors')
27
- },
28
- problem: {
29
- plugins: { constructors: plugin },
30
- rules: bundleRules(plugin.rules, rule => rule.meta.type == 'problem', 'constructors')
31
- }
32
- }
5
+ init.rules(plugin)
6
+ init.deprecations(plugin)
7
+ init.configs(plugin)
33
8
 
34
9
  export { plugin as default, plugin as constructors }
@@ -0,0 +1,60 @@
1
+ import ast from '../utils/ast.js'
2
+
3
+ const re = { superOrArguments: /\bsuper\b|\barguments\b/u }
4
+
5
+ export default {
6
+ meta: {
7
+ type: 'suggestion',
8
+ docs: {
9
+ description: 'Disallow binding methods to `this` in class constructors',
10
+ category: 'Best Practices',
11
+ recommended: true
12
+ },
13
+ fixable: 'code',
14
+ schema: [],
15
+ messages: { unexpectedBind: 'Use an arrow class field instead of binding `{{name}}` in the constructor.' }
16
+ },
17
+
18
+ create(context) {
19
+ const sourceCode = context.sourceCode
20
+ return {
21
+ AssignmentExpression(node) {
22
+ if (node.operator != '=') return
23
+ const { left, right } = node
24
+ if (left.type != 'MemberExpression' || left.computed || left.object.type != 'ThisExpression') return
25
+ const methodName = left.property.name ; if (!methodName) return
26
+ if (right.type != 'CallExpression'
27
+ || right.arguments.length != 1
28
+ || right.arguments[0].type != 'ThisExpression'
29
+ ) return
30
+ const bindCallee = right.callee
31
+ if (bindCallee.type != 'MemberExpression'
32
+ || bindCallee.computed
33
+ || bindCallee.property.name != 'bind'
34
+ || bindCallee.object.type != 'MemberExpression'
35
+ || bindCallee.object.computed
36
+ || bindCallee.object.object.type != 'ThisExpression'
37
+ || bindCallee.object.property.name != methodName
38
+ || !ast.isInsideConstructor(node)
39
+ ) return
40
+ context.report({
41
+ node, messageId: 'unexpectedBind', data: { name: methodName },
42
+ fix(fixer) {
43
+ const stmt = node.parent ; if (stmt?.type != 'ExpressionStatement') return
44
+ const classBody = ast.getEnclosingClassBody(node)
45
+ if (!classBody || classBody.loc.start.line == classBody.loc.end.line) return
46
+ const method = ast.findUniqueMethod(classBody, methodName)
47
+ if (!method || re.superOrArguments.test(sourceCode.getText(method.value))) return
48
+ const params = method.value.params.map(param => sourceCode.getText(param)).join(', ')
49
+ return [
50
+ fixer.replaceText(method,
51
+ `${sourceCode.getText(method.key)} = (${params}) => ${
52
+ sourceCode.getText(method.value.body)}`),
53
+ fixer.removeRange(ast.getLineAwareRemovalRange(sourceCode, stmt))
54
+ ]
55
+ }
56
+ })
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,87 @@
1
+ import ast from '../utils/ast.js'
2
+
3
+ const simpleIdentifierValues = new Set(['Infinity', 'NaN', 'undefined']),
4
+ simpleUnaryOperators = new Set(['!', '+', '-', '~', 'typeof', 'void'])
5
+
6
+ function getStateAssignment(stmt) {
7
+ if (stmt.type != 'ExpressionStatement') return
8
+ const assignment = stmt.expression
9
+ if (assignment.type != 'AssignmentExpression' || assignment.operator != '=') return
10
+ const { left, right } = assignment
11
+ if (left.type != 'MemberExpression' || left.computed || left.object.type != 'ThisExpression') return
12
+ if (!left.property.name || !isSimpleValue(right)) return
13
+ return assignment
14
+ }
15
+
16
+ function isSimpleValue(node) {
17
+ switch (node?.type) {
18
+ case 'Literal':
19
+ return true
20
+ case 'TemplateLiteral':
21
+ return node.expressions.length == 0
22
+ case 'Identifier':
23
+ return simpleIdentifierValues.has(node.name)
24
+ case 'UnaryExpression':
25
+ return simpleUnaryOperators.has(node.operator)
26
+ && (node.argument.type == 'Literal'
27
+ || (node.argument.type == 'Identifier' && simpleIdentifierValues.has(node.argument.name)))
28
+ case 'ArrayExpression':
29
+ return node.elements.every(element => element == null || isSimpleValue(element))
30
+ case 'ObjectExpression':
31
+ return node.properties.every(property =>
32
+ property.type == 'Property' && property.kind == 'init'
33
+ && !property.computed && !property.method && isSimpleValue(property.value))
34
+ default:
35
+ return false
36
+ }
37
+ }
38
+
39
+ function isSuperCall(stmt) {
40
+ return stmt.type == 'ExpressionStatement' && stmt.expression.type == 'CallExpression'
41
+ && stmt.expression.callee.type == 'Super'
42
+ }
43
+
44
+ export default {
45
+ meta: {
46
+ type: 'suggestion',
47
+ docs: {
48
+ description: 'Disallow assigning simple initial state to `this` in class constructors',
49
+ category: 'Best Practices',
50
+ recommended: true
51
+ },
52
+ fixable: 'code',
53
+ schema: [],
54
+ messages: { useClassField: 'Use a class field instead of assigning `{{name}}` in the constructor.' }
55
+ },
56
+
57
+ create(context) {
58
+ const sourceCode = context.sourceCode
59
+ return {
60
+ MethodDefinition(node) {
61
+ if (node.kind != 'constructor' || node.static) return
62
+ const statements = node.value.body.body
63
+ statements.forEach((stmt, idx) => {
64
+ const assignment = getStateAssignment(stmt) ; if (!assignment) return
65
+ const name = assignment.left.property.name
66
+ context.report({
67
+ node: assignment, messageId: 'useClassField', data: { name },
68
+ fix(fixer) {
69
+ const classBody = node.parent
70
+ if (classBody?.type != 'ClassBody' || classBody.loc.start.line == classBody.loc.end.line)
71
+ return
72
+ const safePrefix = statements.slice(0, idx)
73
+ .every(prev => isSuperCall(prev) || getStateAssignment(prev))
74
+ if (!safePrefix) return
75
+ const closeBrace = sourceCode.getLastToken(classBody)
76
+ return [
77
+ fixer.insertTextBefore(closeBrace, `${ast.getLineIndent(sourceCode, node)}${name} = ${
78
+ sourceCode.getText(assignment.right)}\n`),
79
+ fixer.removeRange(ast.getLineAwareRemovalRange(sourceCode, stmt))
80
+ ]
81
+ }
82
+ })
83
+ })
84
+ }
85
+ }
86
+ }
87
+ }
@@ -15,12 +15,10 @@ export default {
15
15
  if (!node.id || !node.superClass?.name?.endsWith('Error')) return
16
16
  checkName(node.id.name, node.id)
17
17
  }
18
-
19
18
  function checkName(name, node) {
20
19
  if (!name || name.endsWith('Error')) return
21
20
  context.report({ node, messageId: 'endWithError' })
22
21
  }
23
-
24
22
  function checkVariableDeclarator(node) {
25
23
  const classExpr = node.init
26
24
  if (classExpr?.type != 'ClassExpression'
@@ -29,7 +27,6 @@ export default {
29
27
  ) return
30
28
  checkName(node.id.name, node.id)
31
29
  }
32
-
33
30
  return {
34
31
  ClassDeclaration: checkClassNode,
35
32
  ClassExpression: checkClassNode,
@@ -1,14 +1,14 @@
1
1
  import globals from 'globals'
2
2
 
3
- const errorConstructors = new Set(Object.keys(globals.builtin).filter(name => name.endsWith('Error')))
4
-
5
- const throwingConstructors = new Set([
6
- 'ArrayBuffer', 'BigInt64Array', 'BigUint64Array', 'DataView', 'FinalizationRegistry', 'Float32Array',
7
- 'Float64Array', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Promise', 'Proxy', 'Set', 'SharedArrayBuffer',
8
- 'Uint8Array', 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', 'WeakRef', 'WeakSet'
9
- ])
10
-
11
- const lenientConstructors = new Set(['Array', 'Date', 'Function', 'Object', 'RegExp'])
3
+ const constructors = {
4
+ errors: new Set(Object.keys(globals.builtin).filter(name => name.endsWith('Error'))),
5
+ lenient: new Set(['Array', 'Date', 'Function', 'Object', 'RegExp']),
6
+ throwing: new Set([
7
+ 'ArrayBuffer', 'BigInt64Array', 'BigUint64Array', 'DataView', 'FinalizationRegistry', 'Float32Array',
8
+ 'Float64Array', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Promise', 'Proxy', 'Set', 'SharedArrayBuffer',
9
+ 'Uint8Array', 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', 'WeakRef', 'WeakSet'
10
+ ])
11
+ }
12
12
 
13
13
  export default {
14
14
  meta: {
@@ -37,10 +37,10 @@ export default {
37
37
  create(context) {
38
38
  const mode = context.options[0]?.mode ?? 'throwing'
39
39
  function shouldReportName(name) {
40
- return mode == 'errors' ? errorConstructors.has(name)
41
- : mode == 'throwing' ? errorConstructors.has(name) || throwingConstructors.has(name)
42
- : mode == 'all' ? errorConstructors.has(name) || throwingConstructors.has(name)
43
- || lenientConstructors.has(name)
40
+ return mode == 'errors' ? constructors.errors.has(name)
41
+ : mode == 'throwing' ? constructors.errors.has(name) || constructors.throwing.has(name)
42
+ : mode == 'all' ? constructors.errors.has(name) || constructors.throwing.has(name)
43
+ || constructors.lenient.has(name)
44
44
  : false
45
45
  }
46
46
  return {
@@ -0,0 +1,49 @@
1
+ const funcTypes = new Set(['ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression']),
2
+ re = { wsOnly: /^\s*$/ }
3
+
4
+ export default {
5
+ findUniqueMethod(classBody, methodName) {
6
+ const matches = classBody.body.filter(member =>
7
+ member.type == 'MethodDefinition' && !member.static && !member.computed
8
+ && member.key.type == 'Identifier' && member.key.name == methodName)
9
+ if (matches.length != 1) return
10
+ const method = matches[0]
11
+ if (method.kind != 'method' || method.value.async || method.value.generator) return
12
+ return method
13
+ },
14
+
15
+ getEnclosingClassBody(node) {
16
+ let current = node.parent
17
+ while (current) {
18
+ if (current.type == 'ClassBody') return current
19
+ current = current.parent
20
+ }
21
+ },
22
+
23
+ getLineAwareRemovalRange(sourceCode, node) {
24
+ const text = sourceCode.text,
25
+ start = node.range[0],
26
+ lineStart = text.lastIndexOf('\n', start - 1) + 1
27
+ if (!re.wsOnly.test(text.slice(lineStart, start))) return node.range
28
+ return [lineStart > 0 ? lineStart - 1 : 0, node.range[1]]
29
+ },
30
+
31
+ getLineIndent(sourceCode, node) {
32
+ const line = sourceCode.lines[node.loc.start.line - 1] ?? ''
33
+ return line.slice(0, line.length - line.trimStart().length)
34
+ },
35
+
36
+ isInsideConstructor(node) {
37
+ let current = node.parent
38
+ while (current) {
39
+ if (funcTypes.has(current.type))
40
+ return ( // whether this func is constructor's val
41
+ current.parent?.type == 'MethodDefinition'
42
+ && current.parent.kind == 'constructor'
43
+ && !current.parent.static
44
+ )
45
+ current = current.parent
46
+ }
47
+ return false
48
+ }
49
+ }
@@ -0,0 +1,51 @@
1
+ // oxlint-disable security/detect-object-injection
2
+
3
+ export default {
4
+ initAlias(plugin, [ruleId, { default: defaultOpts, wrapKey, renames = {} }]) {
5
+ const source = plugin.rules[ruleId] ; if (!source) return
6
+ plugin.rules[ruleId] = {
7
+ meta: {
8
+ ...source.meta,
9
+ schema: [{ type: ['object', 'string'] }],
10
+ defaultOptions: [],
11
+ messages: {
12
+ ...source.meta.messages,
13
+ deprecatedPositional: 'Passing a bare string is deprecated. '
14
+ + `Use \`['error', { ${wrapKey}: '{{wrapped}}' }]\` instead.`,
15
+ deprecatedValue: `'{{old}}' is deprecated. Use '{{new}}' instead.`
16
+ }
17
+ },
18
+ create(context) {
19
+ const raw = context.options[0]
20
+ let newOpts, warnings = []
21
+ if (raw === undefined) // no opts passed → apply preserved old default
22
+ newOpts = defaultOpts
23
+ else if (typeof raw == 'string') { // old pos syntax → wrap in obj + rename
24
+ const wrapped = renames[raw] ?? raw
25
+ newOpts = { [wrapKey]: wrapped }
26
+ warnings.push({ messageId: 'deprecatedPositional', data: { wrapped } })
27
+ if (renames[raw])
28
+ warnings.push({ messageId: 'deprecatedValue', data: { old: raw, new: wrapped } })
29
+ } else if (raw !== null && typeof raw == 'object') // new obj syntax → pass thru unchanged
30
+ newOpts = raw
31
+ else
32
+ newOpts = defaultOpts
33
+ const aliasContext = Object.create(context)
34
+ Object.defineProperty(aliasContext, 'options', {
35
+ value: newOpts === undefined ? [] : [newOpts],
36
+ writable: true, configurable: true, enumerable: true
37
+ })
38
+ const visitors = { ...source.create(aliasContext) }
39
+ if (warnings.length > 0) {
40
+ const originalProgram = visitors.Program
41
+ visitors.Program = function(node) {
42
+ for (const warning of warnings)
43
+ context.report({ node, messageId: warning.messageId, data: warning.data })
44
+ if (originalProgram) originalProgram.call(this, node)
45
+ }
46
+ }
47
+ return visitors
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,49 @@
1
+ import deprecationsUtils from './deprecations.js'
2
+ import pkg from './pkg.js'
3
+ import rulesUtils from './rules.js'
4
+
5
+ import noConstructorBind from '../rules/no-constructor-bind.js'
6
+ import noConstructorState from '../rules/no-constructor-state.js'
7
+ import noExplicitReturn from '../rules/no-explicit-return.js'
8
+ import requireErrorSubclassName from '../rules/require-error-subclass-name.js'
9
+ import requireNew from '../rules/require-new.js'
10
+
11
+ import deprecations from '../data/deprecations.js'
12
+
13
+ const pkgName = pkg.getName(),
14
+ namespace = pkgName.replace(/^@[^/]+\//, '').replace(/^eslint-plugin-/, '')
15
+
16
+ export default {
17
+ configs(plugin) {
18
+ const plugins = { [namespace]: plugin },
19
+ rules = plugin.rules,
20
+ createConfig = (predicate, { language, plugins: extraPlugins = {} } = {}) => ({
21
+ plugins: { ...plugins, ...extraPlugins },
22
+ ...(language ? { language } : {}),
23
+ rules: rulesUtils.bundle(rules, predicate, namespace)
24
+ })
25
+ plugin.configs = {
26
+ recommended: createConfig(rule => rule.meta.docs.recommended),
27
+ problem: createConfig(rule => rule.meta.type == 'problem')
28
+ }
29
+ },
30
+
31
+ deprecations(plugin) {
32
+ for (const entry of Object.entries(deprecations))
33
+ deprecationsUtils.initAlias(plugin, entry)
34
+ },
35
+
36
+ plugin() { return { meta: { name: pkgName }, rules: {}, configs: {} }},
37
+
38
+ rules(plugin) {
39
+ plugin.rules = {
40
+ 'no-constructor-bind': noConstructorBind,
41
+ 'no-constructor-state': noConstructorState,
42
+ 'no-explicit-return': noExplicitReturn,
43
+ 'require-error-subclass-name': requireErrorSubclassName,
44
+ 'require-new': requireNew
45
+ }
46
+ for (const [ruleId, rule] of Object.entries(plugin.rules))
47
+ rule.meta.docs.url = `https://codeberg.org/adamlui/${pkgName}/#${ruleId}`
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ export default {
6
+ getName() { // oxlint-disable security/detect-non-literal-fs-filename
7
+ let current = path.dirname(fileURLToPath(import.meta.url))
8
+ while (current) {
9
+ const pkgPath = path.join(current, 'package.json')
10
+ if (fs.existsSync(pkgPath))
11
+ try {
12
+ const { name } = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
13
+ if (name) return name
14
+ } catch { /* malformed, keep walking */ }
15
+ const parent = path.dirname(current)
16
+ if (parent === current) return null
17
+ current = parent
18
+ }
19
+ return null
20
+ } // oxlint-enable security/detect-non-literal-fs-filename
21
+ }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ bundle(rules, predicate, pluginName) {
3
+ return Object.fromEntries(Object.entries(rules)
4
+ .filter(([, rule]) => !rule.meta.deprecated && predicate(rule))
5
+ .map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
6
+ }
7
+ }
package/src/utils.js DELETED
@@ -1,80 +0,0 @@
1
- export function bundleRules(rules, predicate, pluginName) {
2
- return Object.fromEntries(Object.entries(rules)
3
- .filter(([, rule]) => predicate(rule))
4
- .map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
5
- }
6
-
7
- export function initDeprecatedAlias(plugin, [oldName, alias]) { /* eslint-disable security/detect-object-injection */
8
- const routes = typeof alias == 'string' ? { [alias]: null } // 'old-rule': 'new-rule'
9
- : Array.isArray(alias) ? Object.fromEntries(alias.map(entry =>
10
- typeof entry == 'string' ? [entry, null] // ['new-rule-1', 'new-rule-2']
11
- : Object.entries(entry)[0])) // [{ 'new-rule': { 'old-opt': 'new-opt' }}]
12
- : alias // { 'new-rule': null | { 'new-opt': default } | { 'old-val': 'new-val' } | { ..., 'new-opt': default }}
13
- const targetNames = Object.keys(routes),
14
- isRename = !targetNames.includes(oldName),
15
- originals = Object.fromEntries(targetNames.map(name => [name, plugin.rules[name]])),
16
- source = originals[oldName] ?? originals[targetNames[0]]
17
- plugin.rules[oldName] = {
18
- meta: {
19
- ...source.meta,
20
- defaultOptions: [],
21
- schema: [{ type: ['object', 'array', 'string', 'number', 'boolean', 'null'] }],
22
- docs: { ...source.meta.docs, recommended: isRename ? false : source.meta.docs.recommended },
23
- messages: {
24
- ...source.meta.messages,
25
- deprecatedOption: `'{{old}}' is deprecated. Use '{{new}}' instead.`
26
- },
27
- ...(isRename ? { deprecated: {
28
- message: `Use ${targetNames.map(name => `\`${name}\``).join(', ')} instead.`,
29
- replacedBy: targetNames.map(name => ({ rule: { name }}))
30
- }} : {})
31
- },
32
- create(context) {
33
- const opts = context.options[0],
34
- isObj = opts?.constructor === Object
35
- return Object.assign({}, ...Object.entries(routes).map(([targetName, mapping]) => {
36
- let newOpts = opts, deprecation
37
- if (mapping) {
38
- const entries = Object.entries(mapping),
39
- namedRemap = entries.every(([, val]) => typeof val == 'string'),
40
- wrapKey = namedRemap && entries.length > 1 ? entries.at(-1)[0] : null,
41
- valMap = wrapKey ? Object.fromEntries(entries.slice(0, -1)) : mapping
42
- if (namedRemap) {
43
- if (typeof opts == 'string') { // bare old val → remap, optionally wrapped
44
- if (valMap[opts]) deprecation = { old: opts, new: valMap[opts] }
45
- const remapped = valMap[opts] ?? opts
46
- newOpts = wrapKey ? { [wrapKey]: remapped } : remapped
47
- } else if (wrapKey) { // object opts (or defaults) → remap inner val, warn if deprecated
48
- const raw = isObj ? (opts[wrapKey] ?? mapping[wrapKey]) : mapping[wrapKey],
49
- remapped = valMap[raw]
50
- if (remapped) deprecation = { old: raw, new: remapped }
51
- newOpts = { [wrapKey]: remapped ?? raw }
52
- } else { // object opts → old key → new key
53
- newOpts = { ...opts }
54
- for (const [oldKey, newKey] of entries)
55
- if (opts?.[oldKey] !== undefined) {
56
- newOpts[newKey] = opts[oldKey] ; delete newOpts[oldKey] }
57
- }
58
- } else { // { new-opt: default } — positional wrap
59
- const [newKey, defaultValue] = entries[0]
60
- if (opts !== undefined && !isObj)
61
- deprecation = { old: JSON.stringify(opts), new: `{ ${newKey}: ${JSON.stringify(opts)} }` }
62
- newOpts = { [newKey]: isObj ? (opts[newKey] ?? defaultValue) : (opts ?? defaultValue) }
63
- }
64
- }
65
- const aliasContext = Object.create(context)
66
- Object.defineProperty(aliasContext, 'options',
67
- { value: [newOpts], writable: true, configurable: true, enumerable: true })
68
- const visitors = { ...originals[targetName].create(aliasContext) }
69
- if (deprecation) {
70
- const ruleProgram = visitors.Program
71
- visitors.Program = function(node) {
72
- context.report({ node, messageId: 'deprecatedOption', data: deprecation })
73
- if (ruleProgram) ruleProgram.call(this, node)
74
- }
75
- }
76
- return visitors
77
- }))
78
- }
79
- }
80
- } /* eslint-enable security/detect-object-injection */