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 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'], plugins: { constructors },
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.ts`:
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.6.0",
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
@@ -8,6 +8,7 @@ type ConfigName =
8
8
 
9
9
  interface PluginConfig {
10
10
  plugins: Record<string, unknown>
11
+ languageOptions: { parser?: unknown, [key: string]: unknown }
11
12
  rules: Record<string, RuleSetting>
12
13
  }
13
14
 
@@ -1,4 +1,5 @@
1
- import ast from '../utils/ast.js'
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: 'Best Practices',
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 { left, right } = node
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
- || bindCallee.object.type != 'MemberExpression'
35
- || bindCallee.object.computed
36
- || bindCallee.object.object.type != 'ThisExpression'
37
- || bindCallee.object.property.name != methodName
38
- || !ast.isInsideConstructor(node)
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 = ast.getEnclosingClassBody(node)
48
+ const classBody = nodeUtils.getEnclosingClassBody(node)
45
49
  if (!classBody || classBody.loc.start.line == classBody.loc.end.line) return
46
- const method = ast.findUniqueMethod(classBody, methodName)
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(ast.getLineAwareRemovalRange(sourceCode, stmt))
57
+ fixer.removeRange(source.getLineAwareRemovalRange(sourceCode, stmt))
54
58
  ]
55
59
  }
56
60
  })
@@ -1,4 +1,5 @@
1
- import ast from '../utils/ast.js'
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, right } = assignment
11
- if (left.type != 'MemberExpression' || left.computed || left.object.type != 'ThisExpression') return
12
- if (!left.property.name || !isSimpleValue(right)) return
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: 'Best Practices',
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, `${ast.getLineIndent(sourceCode, node)}${name} = ${
82
+ fixer.insertTextBefore(closeBrace, `${source.getLineIndent(sourceCode, node)}${name} = ${
78
83
  sourceCode.getText(assignment.right)}\n`),
79
- fixer.removeRange(ast.getLineAwareRemovalRange(sourceCode, stmt))
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: 'Best Practices',
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
- if (!node.id || !node.superClass?.name?.endsWith('Error')) return
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
- || !classExpr.superClass?.name?.endsWith('Error')
26
- || classExpr.id // named class expression, already checked
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 {
@@ -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 (default: errors)'
31
+ description: 'Constructor category to check'
29
32
  }
30
33
  },
31
34
  additionalProperties: false
32
35
  }],
33
- defaultOptions: [{ mode: 'throwing' }],
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]?.mode ?? 'throwing'
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.name ; if (!calleeName) return
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 = (predicate, { language, plugins: extraPlugins = {} } = {}) => ({
21
- plugins: { ...plugins, ...extraPlugins },
22
- ...(language ? { language } : {}),
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
+ }