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 +213 -22
- package/docs/LICENSE.md +1 -1
- package/package.json +48 -42
- package/src/data/deprecations.js +7 -8
- package/src/index.d.ts +28 -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 +49 -0
- package/src/utils/pkg.js +21 -0
- package/src/utils/rules.js +7 -0
- package/src/utils.js +0 -80
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="
|
|
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
|
|
12
|
-
<img alt="[
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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"> <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"> <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"> <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
|
-
|
|
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"> <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="#
|
|
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
|
-
|
|
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"> <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
|
-
|
|
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"> <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"> <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
|
|
254
|
-
<img height=
|
|
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
package/package.json
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-constructors",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
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,58 +30,61 @@
|
|
|
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
|
-
"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
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|
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,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
|
-
|
|
3
|
-
|
|
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
|
|
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
|
+
// 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
|
+
}
|
package/src/utils/pkg.js
ADDED
|
@@ -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
|
+
}
|
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 */
|