eslint-plugin-constructors 1.4.2 → 1.5.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 +176 -19
- package/docs/LICENSE.md +1 -1
- package/package.json +48 -44
- package/src/data/deprecations.js +7 -8
- package/src/index.d.ts +27 -3
- package/src/index.js +5 -30
- package/src/rules/no-constructor-bind.js +60 -0
- package/src/rules/no-constructor-state.js +87 -0
- package/src/rules/require-error-subclass-name.js +0 -3
- package/src/rules/require-new.js +13 -13
- package/src/utils/ast.js +49 -0
- package/src/utils/deprecations.js +51 -0
- package/src/utils/init.js +47 -0
- package/src/utils/rules.js +7 -0
- package/src/utils.js +0 -80
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
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="
|
|
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
|
-
<a>
|
|
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
11
|
<a target="_blank" href="https://eslint.org/">
|
|
12
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>
|
|
@@ -46,7 +46,7 @@ export default [
|
|
|
46
46
|
rules: {
|
|
47
47
|
...constructors.configs.recommended.rules,
|
|
48
48
|
'constructors/no-explicit-return': 'off', // selectively disable rule
|
|
49
|
-
'constructors/require-new': ['error', { mode: 'throwing' }] // selectively
|
|
49
|
+
'constructors/require-new': ['error', { mode: 'throwing' }] // selectively tweak rule
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
]
|
|
@@ -63,24 +63,146 @@ Available bundles: `recommended`, `problem` (deterministic bugs only)
|
|
|
63
63
|
Supported rules
|
|
64
64
|
</h2>
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
<h3 id="no-constructor-bind">
|
|
67
|
+
<code>no-constructor-bind</code>
|
|
68
|
+
<a title="Introduced in recent release" target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.5.0">
|
|
69
|
+
<img height=16 align="middle" src="https://api.iconify.design/mdi/sprout.svg?color=%234a4a4a"></a>
|
|
70
|
+
<a title="Included in recommended config" href="#usage">
|
|
71
|
+
<img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
|
|
72
|
+
<a title="Accepts eslint --fix" target="_blank" href="https://eslint.org/docs/latest/use/command-line-interface#--fix">
|
|
73
|
+
<img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
|
|
74
|
+
<a title="Doesn't prevent bugs" href="#no-constructor-bind">
|
|
75
|
+
<img height=18 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
|
|
76
|
+
<a title="Copy rule URL" target="_blank"
|
|
77
|
+
href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23no-constructor-bind">
|
|
78
|
+
<img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
|
|
79
|
+
</h3>
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
|
|
83
|
+
<summary>Disallows binding methods to <code>this</code> inside class constructors (use class arrow functions instead).</summary>
|
|
84
|
+
|
|
85
|
+
#### <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
|
+
|
|
87
|
+
```diff
|
|
88
|
+
class User {
|
|
89
|
+
constructor() {
|
|
90
|
+
! this.greet = this.greet.bind(this)
|
|
91
|
+
}
|
|
92
|
+
greet() { return 'Hello' }
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### <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
|
|
97
|
+
|
|
98
|
+
```diff
|
|
99
|
+
class User {
|
|
100
|
+
- constructor() {
|
|
101
|
+
- this.greet = this.greet.bind(this)
|
|
102
|
+
- }
|
|
103
|
+
+ greet = () => 'Hello'
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### When not to use it
|
|
108
|
+
|
|
109
|
+
If you target environments that do not support class fields.
|
|
110
|
+
|
|
111
|
+
#### Introduced in
|
|
112
|
+
|
|
113
|
+
<a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.5.0">v1.5.0</a> (October 19, 2026)
|
|
114
|
+
|
|
115
|
+
#### See also
|
|
116
|
+
|
|
117
|
+
- <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64"> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind">MDN — `Function.prototype.bind()`</a>
|
|
118
|
+
- <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64"> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields">MDN — Public class fields</a>
|
|
119
|
+
|
|
120
|
+
<div align="center">
|
|
121
|
+
|
|
122
|
+
[Back to index ↑][epc-rules]
|
|
123
|
+
|
|
124
|
+
</div>
|
|
71
125
|
|
|
72
126
|
---
|
|
73
127
|
|
|
128
|
+
</details>
|
|
129
|
+
|
|
130
|
+
<h3 id="no-constructor-state">
|
|
131
|
+
<code>no-constructor-state</code>
|
|
132
|
+
<a title="Introduced in recent release" target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.5.0">
|
|
133
|
+
<img height=16 align="middle" src="https://api.iconify.design/mdi/sprout.svg?color=%234a4a4a"></a>
|
|
134
|
+
<a title="Included in recommended config" href="#usage">
|
|
135
|
+
<img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
|
|
136
|
+
<a title="Accepts eslint --fix" target="_blank" href="https://eslint.org/docs/latest/use/command-line-interface#--fix">
|
|
137
|
+
<img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
|
|
138
|
+
<a title="Doesn't prevent bugs" href="#no-constructor-state">
|
|
139
|
+
<img height=18 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
|
|
140
|
+
<a title="Copy rule URL" target="_blank"
|
|
141
|
+
href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23no-constructor-state">
|
|
142
|
+
<img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
|
|
143
|
+
</h3>
|
|
144
|
+
|
|
145
|
+
<details>
|
|
146
|
+
|
|
147
|
+
<summary>Disallows assigning simple initial state to <code>this</code> inside class constructors (use class fields instead).</summary>
|
|
148
|
+
|
|
149
|
+
#### <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
|
|
150
|
+
|
|
151
|
+
```diff
|
|
152
|
+
class Component {
|
|
153
|
+
constructor() {
|
|
154
|
+
! this.state = { ready: false }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### <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
|
|
160
|
+
|
|
161
|
+
```diff
|
|
162
|
+
class Component {
|
|
163
|
+
- constructor() {
|
|
164
|
+
- this.state = { ready: false }
|
|
165
|
+
- }
|
|
166
|
+
+ state = { ready: false }
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Dynamic initialization (e.g. `this.state = createInitialState()`) is **allowed** by default, because the value is not a simple literal.
|
|
171
|
+
|
|
172
|
+
#### When not to use it
|
|
173
|
+
|
|
174
|
+
If you target environments that do not support class fields.
|
|
175
|
+
|
|
176
|
+
#### Introduced in
|
|
177
|
+
|
|
178
|
+
<a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.5.0">v1.5.0</a> (October 19, 2026)
|
|
179
|
+
|
|
180
|
+
#### See also
|
|
181
|
+
|
|
182
|
+
- <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64"> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields">MDN — Public class fields</a>
|
|
183
|
+
|
|
184
|
+
<div align="center">
|
|
185
|
+
|
|
186
|
+
[Back to index ↑][epc-rules]
|
|
187
|
+
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
</details>
|
|
193
|
+
|
|
74
194
|
<h3 id="no-explicit-return">
|
|
75
195
|
<code>no-explicit-return</code>
|
|
76
196
|
<a title="Included in recommended config" href="#usage">
|
|
77
197
|
<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"
|
|
198
|
+
<a title="Copy rule URL" target="_blank"
|
|
79
199
|
href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23no-explicit-return">
|
|
80
200
|
<img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
|
|
81
201
|
</h3>
|
|
82
202
|
|
|
83
|
-
|
|
203
|
+
<details>
|
|
204
|
+
|
|
205
|
+
<summary>Disallows explicit <code>return</code> statements inside class constructors.</summary>
|
|
84
206
|
|
|
85
207
|
#### <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
208
|
|
|
@@ -106,7 +228,7 @@ Disallows explicit `return` statements inside class constructors.
|
|
|
106
228
|
- return
|
|
107
229
|
+ this.x = 1
|
|
108
230
|
}
|
|
109
|
-
|
|
231
|
+
|
|
110
232
|
class B {
|
|
111
233
|
constructor() {
|
|
112
234
|
- return this
|
|
@@ -119,6 +241,14 @@ Disallows explicit `return` statements inside class constructors.
|
|
|
119
241
|
|
|
120
242
|
If you need to return a different object from a constructor.
|
|
121
243
|
|
|
244
|
+
#### Introduced in
|
|
245
|
+
|
|
246
|
+
<a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.0.0">v1.0.0</a> (September 9, 2026)
|
|
247
|
+
|
|
248
|
+
#### See also
|
|
249
|
+
|
|
250
|
+
- <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64"> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor">MDN — Class `constructor`</a>
|
|
251
|
+
|
|
122
252
|
<div align="center">
|
|
123
253
|
|
|
124
254
|
[Back to index ↑][epc-rules]
|
|
@@ -127,18 +257,22 @@ If you need to return a different object from a constructor.
|
|
|
127
257
|
|
|
128
258
|
---
|
|
129
259
|
|
|
260
|
+
</details>
|
|
261
|
+
|
|
130
262
|
<h3 id="require-error-subclass-name">
|
|
131
263
|
<code>require-error-subclass-name</code>
|
|
132
264
|
<a title="Included in recommended config" href="#usage">
|
|
133
265
|
<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="#
|
|
266
|
+
<a title="Doesn't prevent bugs" href="#require-error-subclass-name">
|
|
135
267
|
<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"
|
|
268
|
+
<a title="Copy rule URL" target="_blank"
|
|
137
269
|
href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23require-error-subclass-name">
|
|
138
270
|
<img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
|
|
139
271
|
</h3>
|
|
140
272
|
|
|
141
|
-
|
|
273
|
+
<details>
|
|
274
|
+
|
|
275
|
+
<summary>Requires classes extending <code>Error</code> have a name ending in <code>Error</code>.</summary>
|
|
142
276
|
|
|
143
277
|
#### <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
278
|
|
|
@@ -160,6 +294,14 @@ Requires classes extending `Error` have a name ending in `Error`.
|
|
|
160
294
|
|
|
161
295
|
If you don't follow the convention of suffixing custom error classes w/ `Error`.
|
|
162
296
|
|
|
297
|
+
#### Introduced in
|
|
298
|
+
|
|
299
|
+
<a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.0.0">v1.0.0</a> (September 9, 2026)
|
|
300
|
+
|
|
301
|
+
#### See also
|
|
302
|
+
|
|
303
|
+
- <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64"> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error">MDN — `Error`</a>
|
|
304
|
+
|
|
163
305
|
<div align="center">
|
|
164
306
|
|
|
165
307
|
[Back to index ↑][epc-rules]
|
|
@@ -168,6 +310,8 @@ If you don't follow the convention of suffixing custom error classes w/ `Error`.
|
|
|
168
310
|
|
|
169
311
|
---
|
|
170
312
|
|
|
313
|
+
</details>
|
|
314
|
+
|
|
171
315
|
<h3 id="require-new">
|
|
172
316
|
<code>require-new</code>
|
|
173
317
|
<a title="Included in recommended config" href="#usage">
|
|
@@ -176,16 +320,18 @@ If you don't follow the convention of suffixing custom error classes w/ `Error`.
|
|
|
176
320
|
<img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
|
|
177
321
|
<a title="Accepts array w/ options" href="#usage">
|
|
178
322
|
<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"
|
|
323
|
+
<a title="Copy rule URL" target="_blank"
|
|
180
324
|
href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-constructors%2F%23require-new">
|
|
181
325
|
<img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
|
|
182
326
|
</h3>
|
|
183
327
|
|
|
184
|
-
|
|
328
|
+
<details>
|
|
329
|
+
|
|
330
|
+
<summary>Requires <code>new</code> when calling error constructors (and optionally other built-ins).</summary>
|
|
185
331
|
|
|
186
332
|
#### Options
|
|
187
333
|
|
|
188
|
-
- `mode` (default `'errors'`) – constructor category to enforce `new` on (`'errors'`, `'throwing'`, `'all'`)
|
|
334
|
+
- `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
335
|
|
|
190
336
|
#### <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
337
|
|
|
@@ -220,12 +366,23 @@ Requires `new` when calling error constructors (and optionally other built-ins).
|
|
|
220
366
|
|
|
221
367
|
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
368
|
|
|
369
|
+
#### Introduced in
|
|
370
|
+
|
|
371
|
+
<a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-constructors/releases/tag/v1.0.0">v1.0.0</a> (September 9, 2026)
|
|
372
|
+
|
|
373
|
+
#### See also
|
|
374
|
+
|
|
375
|
+
- <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64"> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new">MDN — `new` operator</a>
|
|
376
|
+
- <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64"> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error">MDN — `Error` objects</a>
|
|
377
|
+
|
|
223
378
|
<div align="center">
|
|
224
379
|
|
|
225
380
|
[Back to index ↑][epc-rules]
|
|
226
381
|
|
|
227
382
|
</div>
|
|
228
383
|
|
|
384
|
+
</details>
|
|
385
|
+
|
|
229
386
|
---
|
|
230
387
|
|
|
231
388
|
## <img width=30 align="top" src="https://api.iconify.design/boxicons/code-alt-filled.svg?color=%232b2b2a"> Shield embed
|
|
@@ -250,8 +407,8 @@ If you prefer calling built-ins w/o `new` (which the runtime allows for some of
|
|
|
250
407
|
|
|
251
408
|
## <img height=30 width=27 align="top" src="https://api.iconify.design/ic/baseline-account-balance.svg?color=%232b2b2a"> License
|
|
252
409
|
|
|
253
|
-
<a
|
|
254
|
-
<img height=
|
|
410
|
+
<a href="https://codeberg.org/adamlui/eslint-plugin-constructors/src/branch/main/docs/LICENSE.md">
|
|
411
|
+
<img height=88 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-chrome-extensions/d79e1db7e2/assets/images/banners/mit-license/banner.png"></a>
|
|
255
412
|
<br><br>
|
|
256
413
|
|
|
257
414
|
<details>
|
package/docs/LICENSE.md
CHANGED
package/package.json
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-constructors",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "ESLint rules for constructor usage",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
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
|
-
"
|
|
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,49 +30,43 @@
|
|
|
27
30
|
"url": "https://paypal.me/adamlui"
|
|
28
31
|
}
|
|
29
32
|
],
|
|
30
|
-
"
|
|
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
|
-
"prepare": "husky",
|
|
38
|
-
"lint": "eslint . --cache && yamllint .",
|
|
39
|
-
"lint:all": "eslint . && yamllint .",
|
|
40
|
-
"lint:fix": "eslint . --fix --cache && yamllint .",
|
|
41
|
-
"lint:fix-all": "eslint . --fix && yamllint .",
|
|
42
|
-
"test": "node tests/run.js",
|
|
43
|
-
"demo": "node demo/run.js",
|
|
44
|
-
"bump:patch": "bash utils/bump.sh patch",
|
|
45
|
-
"bump:minor": "bash utils/bump.sh minor",
|
|
46
53
|
"bump:feat": "npm run bump:minor",
|
|
47
|
-
"bump:major": "bash utils/bump.sh major"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
"
|
|
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.js"
|
|
52
61
|
},
|
|
53
|
-
"keywords": [
|
|
54
|
-
"code-quality",
|
|
55
|
-
"constructor",
|
|
56
|
-
"error",
|
|
57
|
-
"eslint",
|
|
58
|
-
"eslint-plugin",
|
|
59
|
-
"eslintplugin",
|
|
60
|
-
"security"
|
|
61
|
-
],
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"globals": "^17.
|
|
64
|
-
},
|
|
65
|
-
"peerDependencies": {
|
|
66
|
-
"eslint": ">=9.0.0"
|
|
63
|
+
"globals": "^17.13.0"
|
|
67
64
|
},
|
|
68
65
|
"devDependencies": {
|
|
69
66
|
"@e18e/eslint-plugin": "^0.8.1",
|
|
70
67
|
"@eslint/js": "^10.0.1",
|
|
71
|
-
"@eslint/json": "^2.1.
|
|
72
|
-
"@eslint/markdown": "^
|
|
68
|
+
"@eslint/json": "^2.1.1",
|
|
69
|
+
"@eslint/markdown": "^7.5.1",
|
|
73
70
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
74
71
|
"eslint": "^10.12.0",
|
|
75
72
|
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
@@ -77,10 +74,17 @@
|
|
|
77
74
|
"eslint-plugin-n": "^18.4.1",
|
|
78
75
|
"eslint-plugin-no-secrets": "^2.3.3",
|
|
79
76
|
"eslint-plugin-no-unsanitized": "^4.1.5",
|
|
77
|
+
"eslint-plugin-package-json": "^1.10.1",
|
|
80
78
|
"eslint-plugin-promise": "^7.3.0",
|
|
81
79
|
"eslint-plugin-regexp": "^3.3.1",
|
|
82
80
|
"eslint-plugin-security": "^4.2.0",
|
|
83
|
-
"
|
|
84
|
-
"
|
|
81
|
+
"eslint-plugin-sonarjs": "^4.2.2",
|
|
82
|
+
"eslint-plugin-woodpecker": "^1.0.1"
|
|
83
|
+
},
|
|
84
|
+
"peerDependencies": {
|
|
85
|
+
"eslint": ">=9.0.0"
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=18.18.0"
|
|
85
89
|
}
|
|
86
90
|
}
|
package/src/data/deprecations.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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,30 @@
|
|
|
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,
|
|
3
|
-
configs:
|
|
26
|
+
rules: Record<string, PluginRule>
|
|
27
|
+
configs: Record<ConfigName, PluginConfig>
|
|
4
28
|
}
|
|
5
29
|
|
|
6
|
-
export default plugin
|
|
30
|
+
export { plugin as default, plugin as constructors }
|
package/src/index.js
CHANGED
|
@@ -1,34 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import init from './utils/init.js'
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
package/src/rules/require-new.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import globals from 'globals'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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' ?
|
|
41
|
-
: mode == 'throwing' ?
|
|
42
|
-
: mode == 'all' ?
|
|
43
|
-
||
|
|
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 {
|
package/src/utils/ast.js
ADDED
|
@@ -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
|
+
/* eslint-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,47 @@
|
|
|
1
|
+
import deprecationsUtils from './deprecations.js'
|
|
2
|
+
import rulesUtils from './rules.js'
|
|
3
|
+
|
|
4
|
+
import noConstructorBind from '../rules/no-constructor-bind.js'
|
|
5
|
+
import noConstructorState from '../rules/no-constructor-state.js'
|
|
6
|
+
import noExplicitReturn from '../rules/no-explicit-return.js'
|
|
7
|
+
import requireErrorSubclassName from '../rules/require-error-subclass-name.js'
|
|
8
|
+
import requireNew from '../rules/require-new.js'
|
|
9
|
+
|
|
10
|
+
import deprecations from '../data/deprecations.js'
|
|
11
|
+
|
|
12
|
+
const namespace = 'constructors'
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
configs(plugin) {
|
|
16
|
+
const plugins = { [namespace]: plugin },
|
|
17
|
+
rules = plugin.rules,
|
|
18
|
+
createConfig = (predicate, { language, plugins: extraPlugins = {} } = {}) => ({
|
|
19
|
+
plugins: { ...plugins, ...extraPlugins },
|
|
20
|
+
...(language ? { language } : {}),
|
|
21
|
+
rules: rulesUtils.bundle(rules, predicate, namespace)
|
|
22
|
+
})
|
|
23
|
+
plugin.configs = {
|
|
24
|
+
recommended: createConfig(rule => rule.meta.docs.recommended),
|
|
25
|
+
problem: createConfig(rule => rule.meta.type == 'problem')
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
deprecations(plugin) {
|
|
30
|
+
for (const entry of Object.entries(deprecations))
|
|
31
|
+
deprecationsUtils.initAlias(plugin, entry)
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
plugin() { return { rules: {}, configs: {} }},
|
|
35
|
+
|
|
36
|
+
rules(plugin) {
|
|
37
|
+
plugin.rules = {
|
|
38
|
+
'no-constructor-bind': noConstructorBind,
|
|
39
|
+
'no-constructor-state': noConstructorState,
|
|
40
|
+
'no-explicit-return': noExplicitReturn,
|
|
41
|
+
'require-error-subclass-name': requireErrorSubclassName,
|
|
42
|
+
'require-new': requireNew
|
|
43
|
+
}
|
|
44
|
+
for (const [ruleId, rule] of Object.entries(plugin.rules))
|
|
45
|
+
rule.meta.docs.url = `https://codeberg.org/adamlui/eslint-plugin-${namespace}/#${ruleId}`
|
|
46
|
+
}
|
|
47
|
+
}
|
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 */
|