eslint-plugin-constructors 1.6.0 → 1.7.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 +4 -4
- package/package.json +3 -1
- package/src/index.d.ts +1 -0
- package/src/rules/no-constructor-bind.js +17 -13
- package/src/rules/no-constructor-state.js +12 -7
- package/src/rules/require-error-subclass-name.js +9 -7
- package/src/rules/require-new.js +7 -4
- package/src/utils/init.js +4 -3
- package/src/utils/{ast.js → node.js} +6 -15
- package/src/utils/source.js +16 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
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
|
-
<img src="https://img.shields.io/npm/dm/eslint-plugin-constructors?style=for-the-badge&logo=npm&color=black"></a>
|
|
10
|
+
<img alt="[Monthly npm downloads]" src="https://img.shields.io/npm/dm/eslint-plugin-constructors?style=for-the-badge&logo=npm&color=black"></a>
|
|
11
11
|
<a href="#usage">
|
|
12
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>
|
|
@@ -44,7 +44,7 @@ import constructors from 'eslint-plugin-constructors'
|
|
|
44
44
|
export default [
|
|
45
45
|
// other configs
|
|
46
46
|
{
|
|
47
|
-
files: ['**/*.js'],
|
|
47
|
+
files: ['**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}'], ...constructors.configs.recommended,
|
|
48
48
|
rules: {
|
|
49
49
|
...constructors.configs.recommended.rules,
|
|
50
50
|
'constructors/no-explicit-return': 'off', // selectively disable rule
|
|
@@ -60,7 +60,7 @@ export default [
|
|
|
60
60
|
|
|
61
61
|
### <img height=15 width=auto src="https://www.google.com/s2/favicons?domain=oxc.rs&sz=64"> Oxlint
|
|
62
62
|
|
|
63
|
-
Add a bundled config to your `oxlint.config
|
|
63
|
+
Add a bundled config to your `oxlint.config.*ts`:
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
66
|
...
|
|
@@ -70,7 +70,7 @@ export default defineConfig({
|
|
|
70
70
|
overrides: [
|
|
71
71
|
// other overrides
|
|
72
72
|
{
|
|
73
|
-
files: ['**/*.js'],
|
|
73
|
+
files: ['**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}'],
|
|
74
74
|
jsPlugins: ['eslint-plugin-constructors'],
|
|
75
75
|
rules: {
|
|
76
76
|
...constructors.configs.recommended.rules,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-constructors",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "ESLint rules for constructor usage",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-quality",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@eslint/json": "^2.1.1",
|
|
68
68
|
"@eslint/markdown": "^7.5.1",
|
|
69
69
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
70
|
+
"@typescript-eslint/parser": "^8.72.1",
|
|
70
71
|
"eslint": "^10.13.0",
|
|
71
72
|
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
72
73
|
"eslint-plugin-import-x": "^4.17.1",
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
"oxlint": "^1.89.0"
|
|
83
84
|
},
|
|
84
85
|
"peerDependencies": {
|
|
86
|
+
"@typescript-eslint/parser": ">=8.0.0",
|
|
85
87
|
"eslint": ">=9.0.0"
|
|
86
88
|
},
|
|
87
89
|
"engines": {
|
package/src/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import nodeUtils from '../utils/node.js'
|
|
2
|
+
import source from '../utils/source.js'
|
|
2
3
|
|
|
3
4
|
const re = { superOrArguments: /\bsuper\b|\barguments\b/u }
|
|
4
5
|
|
|
@@ -7,7 +8,7 @@ export default {
|
|
|
7
8
|
type: 'suggestion',
|
|
8
9
|
docs: {
|
|
9
10
|
description: 'Disallow binding methods to `this` in class constructors',
|
|
10
|
-
category: '
|
|
11
|
+
category: 'Stylistic Issues',
|
|
11
12
|
recommended: true
|
|
12
13
|
},
|
|
13
14
|
fixable: 'code',
|
|
@@ -20,37 +21,40 @@ export default {
|
|
|
20
21
|
return {
|
|
21
22
|
AssignmentExpression(node) {
|
|
22
23
|
if (node.operator != '=') return
|
|
23
|
-
const
|
|
24
|
+
const left = node.left,
|
|
25
|
+
right = nodeUtils.unwrapTS(node.right)
|
|
24
26
|
if (left.type != 'MemberExpression' || left.computed || left.object.type != 'ThisExpression') return
|
|
25
27
|
const methodName = left.property.name ; if (!methodName) return
|
|
26
28
|
if (right.type != 'CallExpression'
|
|
27
29
|
|| right.arguments.length != 1
|
|
28
|
-
|| right.arguments[0].type != 'ThisExpression'
|
|
30
|
+
|| nodeUtils.unwrapTS(right.arguments[0]).type != 'ThisExpression'
|
|
29
31
|
) return
|
|
30
|
-
const bindCallee = right.callee
|
|
32
|
+
const bindCallee = nodeUtils.unwrapTS(right.callee)
|
|
31
33
|
if (bindCallee.type != 'MemberExpression'
|
|
32
34
|
|| bindCallee.computed
|
|
33
35
|
|| bindCallee.property.name != 'bind'
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
||
|
|
38
|
-
||
|
|
36
|
+
) return
|
|
37
|
+
const bindTarget = nodeUtils.unwrapTS(bindCallee.object)
|
|
38
|
+
if (bindTarget.type != 'MemberExpression'
|
|
39
|
+
|| bindTarget.computed
|
|
40
|
+
|| bindTarget.property.name != methodName
|
|
41
|
+
|| nodeUtils.unwrapTS(bindTarget.object).type != 'ThisExpression'
|
|
42
|
+
|| !nodeUtils.isInsideConstructor(node)
|
|
39
43
|
) return
|
|
40
44
|
context.report({
|
|
41
45
|
node, messageId: 'unexpectedBind', data: { name: methodName },
|
|
42
46
|
fix(fixer) {
|
|
43
47
|
const stmt = node.parent ; if (stmt?.type != 'ExpressionStatement') return
|
|
44
|
-
const classBody =
|
|
48
|
+
const classBody = nodeUtils.getEnclosingClassBody(node)
|
|
45
49
|
if (!classBody || classBody.loc.start.line == classBody.loc.end.line) return
|
|
46
|
-
const method =
|
|
50
|
+
const method = nodeUtils.findUniqueMethod(classBody, methodName)
|
|
47
51
|
if (!method || re.superOrArguments.test(sourceCode.getText(method.value))) return
|
|
48
52
|
const params = method.value.params.map(param => sourceCode.getText(param)).join(', ')
|
|
49
53
|
return [
|
|
50
54
|
fixer.replaceText(method,
|
|
51
55
|
`${sourceCode.getText(method.key)} = (${params}) => ${
|
|
52
56
|
sourceCode.getText(method.value.body)}`),
|
|
53
|
-
fixer.removeRange(
|
|
57
|
+
fixer.removeRange(source.getLineAwareRemovalRange(sourceCode, stmt))
|
|
54
58
|
]
|
|
55
59
|
}
|
|
56
60
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import nodeUtils from '../utils/node.js'
|
|
2
|
+
import source from '../utils/source.js'
|
|
2
3
|
|
|
3
4
|
const simpleIdentifierValues = new Set(['Infinity', 'NaN', 'undefined']),
|
|
4
5
|
simpleUnaryOperators = new Set(['!', '+', '-', '~', 'typeof', 'void'])
|
|
@@ -7,9 +8,13 @@ function getStateAssignment(stmt) {
|
|
|
7
8
|
if (stmt.type != 'ExpressionStatement') return
|
|
8
9
|
const assignment = stmt.expression
|
|
9
10
|
if (assignment.type != 'AssignmentExpression' || assignment.operator != '=') return
|
|
10
|
-
const { left
|
|
11
|
-
if (left.type != 'MemberExpression'
|
|
12
|
-
|
|
11
|
+
const { left } = assignment
|
|
12
|
+
if (left.type != 'MemberExpression'
|
|
13
|
+
|| left.computed
|
|
14
|
+
|| left.object.type != 'ThisExpression'
|
|
15
|
+
|| !left.property.name
|
|
16
|
+
|| !isSimpleValue(nodeUtils.unwrapTS(assignment.right))
|
|
17
|
+
) return
|
|
13
18
|
return assignment
|
|
14
19
|
}
|
|
15
20
|
|
|
@@ -46,7 +51,7 @@ export default {
|
|
|
46
51
|
type: 'suggestion',
|
|
47
52
|
docs: {
|
|
48
53
|
description: 'Disallow assigning simple initial state to `this` in class constructors',
|
|
49
|
-
category: '
|
|
54
|
+
category: 'Stylistic Issues',
|
|
50
55
|
recommended: true
|
|
51
56
|
},
|
|
52
57
|
fixable: 'code',
|
|
@@ -74,9 +79,9 @@ export default {
|
|
|
74
79
|
if (!safePrefix) return
|
|
75
80
|
const closeBrace = sourceCode.getLastToken(classBody)
|
|
76
81
|
return [
|
|
77
|
-
fixer.insertTextBefore(closeBrace, `${
|
|
82
|
+
fixer.insertTextBefore(closeBrace, `${source.getLineIndent(sourceCode, node)}${name} = ${
|
|
78
83
|
sourceCode.getText(assignment.right)}\n`),
|
|
79
|
-
fixer.removeRange(
|
|
84
|
+
fixer.removeRange(source.getLineAwareRemovalRange(sourceCode, stmt))
|
|
80
85
|
]
|
|
81
86
|
}
|
|
82
87
|
})
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import nodeUtils from '../utils/node.js'
|
|
2
|
+
|
|
1
3
|
export default {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'suggestion',
|
|
4
6
|
docs: {
|
|
5
7
|
description: 'Require that classes extending Error have names ending in "Error"',
|
|
6
|
-
category: '
|
|
8
|
+
category: 'Stylistic Issues',
|
|
7
9
|
recommended: true
|
|
8
10
|
},
|
|
9
11
|
schema: [],
|
|
@@ -12,7 +14,8 @@ export default {
|
|
|
12
14
|
|
|
13
15
|
create(context) {
|
|
14
16
|
function checkClassNode(node) {
|
|
15
|
-
|
|
17
|
+
const superClass = nodeUtils.unwrapTS(node.superClass)
|
|
18
|
+
if (!node.id || !superClass?.name?.endsWith('Error')) return
|
|
16
19
|
checkName(node.id.name, node.id)
|
|
17
20
|
}
|
|
18
21
|
function checkName(name, node) {
|
|
@@ -20,11 +23,10 @@ export default {
|
|
|
20
23
|
context.report({ node, messageId: 'endWithError' })
|
|
21
24
|
}
|
|
22
25
|
function checkVariableDeclarator(node) {
|
|
23
|
-
const classExpr = node.init
|
|
24
|
-
if (classExpr?.type != 'ClassExpression'
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
) return
|
|
26
|
+
const classExpr = nodeUtils.unwrapTS(node.init)
|
|
27
|
+
if (classExpr?.type != 'ClassExpression' || classExpr.id) return
|
|
28
|
+
const superClass = nodeUtils.unwrapTS(classExpr.superClass)
|
|
29
|
+
if (!superClass?.name?.endsWith('Error')) return
|
|
28
30
|
checkName(node.id.name, node.id)
|
|
29
31
|
}
|
|
30
32
|
return {
|
package/src/rules/require-new.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import nodeUtils from '../utils/node.js'
|
|
1
2
|
import globals from 'globals'
|
|
2
3
|
|
|
4
|
+
const defaultOptions = { mode: 'throwing' }
|
|
5
|
+
|
|
3
6
|
const constructors = {
|
|
4
7
|
errors: new Set(Object.keys(globals.builtin).filter(name => name.endsWith('Error'))),
|
|
5
8
|
lenient: new Set(['Array', 'Date', 'Function', 'Object', 'RegExp']),
|
|
@@ -25,17 +28,17 @@ export default {
|
|
|
25
28
|
mode: {
|
|
26
29
|
type: 'string',
|
|
27
30
|
enum: ['errors', 'throwing', 'all'],
|
|
28
|
-
description: 'Constructor category to check
|
|
31
|
+
description: 'Constructor category to check'
|
|
29
32
|
}
|
|
30
33
|
},
|
|
31
34
|
additionalProperties: false
|
|
32
35
|
}],
|
|
33
|
-
defaultOptions: [
|
|
36
|
+
defaultOptions: [defaultOptions],
|
|
34
37
|
messages: { requireNew: 'Use `new {{name}}()` instead of `{{name}}()`.' }
|
|
35
38
|
},
|
|
36
39
|
|
|
37
40
|
create(context) {
|
|
38
|
-
const mode = context.options[0]
|
|
41
|
+
const { mode } = { ...defaultOptions, ...context.options[0] }
|
|
39
42
|
function shouldReportName(name) {
|
|
40
43
|
return mode == 'errors' ? constructors.errors.has(name)
|
|
41
44
|
: mode == 'throwing' ? constructors.errors.has(name) || constructors.throwing.has(name)
|
|
@@ -46,7 +49,7 @@ export default {
|
|
|
46
49
|
return {
|
|
47
50
|
CallExpression(node) {
|
|
48
51
|
if (node.new) return
|
|
49
|
-
const calleeName = node.callee
|
|
52
|
+
const calleeName = nodeUtils.unwrapTS(node.callee)?.name ; if (!calleeName) return
|
|
50
53
|
if (shouldReportName(calleeName))
|
|
51
54
|
context.report({
|
|
52
55
|
node, messageId: 'requireNew', data: { name: calleeName },
|
package/src/utils/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import deprecationsUtils from './deprecations.js'
|
|
2
2
|
import pkg from './pkg.js'
|
|
3
3
|
import rulesUtils from './rules.js'
|
|
4
|
+
import tsParser from '@typescript-eslint/parser'
|
|
4
5
|
|
|
5
6
|
import noConstructorBind from '../rules/no-constructor-bind.js'
|
|
6
7
|
import noConstructorState from '../rules/no-constructor-state.js'
|
|
@@ -17,9 +18,9 @@ export default {
|
|
|
17
18
|
configs(plugin) {
|
|
18
19
|
const plugins = { [namespace]: plugin },
|
|
19
20
|
rules = plugin.rules,
|
|
20
|
-
createConfig =
|
|
21
|
-
plugins
|
|
22
|
-
|
|
21
|
+
createConfig = predicate => ({
|
|
22
|
+
plugins,
|
|
23
|
+
languageOptions: { parser: tsParser },
|
|
23
24
|
rules: rulesUtils.bundle(rules, predicate, namespace)
|
|
24
25
|
})
|
|
25
26
|
plugin.configs = {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
const funcTypes = new Set(['ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression'])
|
|
2
|
-
re = { wsOnly: /^\s*$/ }
|
|
1
|
+
const funcTypes = new Set(['ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression'])
|
|
3
2
|
|
|
4
3
|
export default {
|
|
5
4
|
findUniqueMethod(classBody, methodName) {
|
|
@@ -20,19 +19,6 @@ export default {
|
|
|
20
19
|
}
|
|
21
20
|
},
|
|
22
21
|
|
|
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
22
|
isInsideConstructor(node) {
|
|
37
23
|
let current = node.parent
|
|
38
24
|
while (current) {
|
|
@@ -45,5 +31,10 @@ export default {
|
|
|
45
31
|
current = current.parent
|
|
46
32
|
}
|
|
47
33
|
return false
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
unwrapTS(node) {
|
|
37
|
+
while (node?.type?.startsWith('TS') && node.expression) node = node.expression
|
|
38
|
+
return node
|
|
48
39
|
}
|
|
49
40
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const re = { wsOnly: /^\s*$/ }
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
getLineAwareRemovalRange(sourceCode, node) {
|
|
5
|
+
const text = sourceCode.text,
|
|
6
|
+
start = node.range[0],
|
|
7
|
+
lineStart = text.lastIndexOf('\n', start - 1) + 1
|
|
8
|
+
if (!re.wsOnly.test(text.slice(lineStart, start))) return node.range
|
|
9
|
+
return [lineStart > 0 ? lineStart - 1 : 0, node.range[1]]
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
getLineIndent(sourceCode, node) {
|
|
13
|
+
const line = sourceCode.lines[node.loc.start.line - 1] ?? ''
|
|
14
|
+
return line.slice(0, line.length - line.trimStart().length)
|
|
15
|
+
}
|
|
16
|
+
}
|