eslint-plugin-constructors 1.0.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 ADDED
@@ -0,0 +1,148 @@
1
+ # eslint-plugin-constructors
2
+
3
+ [![](https://img.shields.io/npm/v/eslint-plugin-constructors?style=for-the-badge&label=Latest%20Build)](https://www.npmjs.com/package/eslint-plugin-constructors)
4
+ [![](https://img.shields.io/badge/Requires-ESLint_%3E%3D9.0.0-4B32C3?style=for-the-badge&logo=eslint)](https://eslint.org/)
5
+
6
+ ESLint plugin to enforce best practices for constructor usage.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm i -D eslint-plugin-constructors
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ Add `constructors` to your flat config:
17
+
18
+ ```js
19
+ import constructors from 'eslint-plugin-constructors'
20
+
21
+ export default [
22
+ {
23
+ files: ['**/*.js'],
24
+ plugins: { constructors },
25
+ rules: {
26
+ 'constructors/no-explicit-return': 'error',
27
+ 'constructors/require-error-subclass-name': 'error',
28
+ 'constructors/require-new': 'error'
29
+ }
30
+ }
31
+ ]
32
+ ```
33
+
34
+ Or use the bundled recommended config:
35
+
36
+ ```js
37
+ import constructors from 'eslint-plugin-constructors'
38
+
39
+ export default [
40
+ {
41
+ files: ['**/*.js'],
42
+ ...constructors.configs.recommended
43
+ }
44
+ ];
45
+ ```
46
+
47
+ ## Rules
48
+
49
+ The plugin has 3 rules.
50
+
51
+ ### `require-new`
52
+
53
+ Requires `new` when calling error constructors (and optionally other built‑in constructors).
54
+
55
+ #### Options
56
+
57
+ - `"error-only"` (default) – only error constructors (`Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `URIError`, `EvalError`, `AggregateError`)
58
+ - `"builtins"` – also includes `Array`, `Boolean`, `Date`, `Function`, `Number`, `Object`, `RegExp`, `String`, `Symbol`, `BigInt`, `Map`, `Set`, `WeakMap`, `WeakSet`
59
+
60
+ #### Example incorrect code
61
+
62
+ ```js
63
+ throw Error('boom')
64
+ throw TypeError('bad')
65
+
66
+ // w/ builtins:
67
+ const arr = Array(),
68
+ m = Map()
69
+ ```
70
+
71
+ #### Example correct code
72
+
73
+ ```js
74
+ throw new Error('boom')
75
+ throw new TypeError('bad')
76
+
77
+ // w/ builtins:
78
+ const arr = new Array(),
79
+ m = new Map()
80
+ ```
81
+
82
+ #### When Not To Use It
83
+
84
+ If you prefer the convenience of calling built‑ins without `new` (which is allowed by the spec).
85
+
86
+ ---
87
+
88
+ ### `require-error-subclass-name`
89
+
90
+ Requires classes extending `Error` have a name ending in `Error`.
91
+
92
+ #### Example incorrect Code
93
+
94
+ ```js
95
+ class My extends Error {}
96
+ const Custom = class extends Error {}
97
+ ```
98
+
99
+ #### Example correct Code
100
+
101
+ ```js
102
+ class MyError extends Error {}
103
+ const CustomError = class extends Error {}
104
+ ```
105
+
106
+ #### When Not To Use It
107
+
108
+ If you don't follow the common convention of suffixing custom error classes w/ `Error`.
109
+
110
+ ---
111
+
112
+ ### `no-explicit-return`
113
+
114
+ Disallows explicit `return` statements inside class constructors.
115
+
116
+ #### Incorrect Code
117
+
118
+ ```js
119
+ class A {
120
+ constructor() {
121
+ return
122
+ }
123
+ }
124
+
125
+ class B {
126
+ constructor() {
127
+ return this
128
+ }
129
+ }
130
+ ```
131
+
132
+ #### Correct Code
133
+
134
+ ```js
135
+ class A {
136
+ constructor() {
137
+ this.x = 1
138
+ }
139
+ }
140
+ ```
141
+
142
+ #### When Not To Use It
143
+
144
+ If you need to return a different object from a constructor (a rare but valid pattern).
145
+
146
+ ## License
147
+
148
+ Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui).
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "eslint-plugin-constructors",
3
+ "version": "1.0.0",
4
+ "description": "ESLint rules for constructor usage",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18.18.0"
8
+ },
9
+ "author": {
10
+ "name": "Adam Lui",
11
+ "email": "adam@kudoai.org",
12
+ "url": "https://codeberg.org/adamlui"
13
+ },
14
+ "homepage": "https://codeberg.org/adamlui/eslint-plugin-constructors/#readme",
15
+ "license": "MIT",
16
+ "main": "src/index.js",
17
+ "files": [
18
+ "src"
19
+ ],
20
+ "scripts": {
21
+ "prepare": "husky",
22
+ "lint": "eslint . --cache",
23
+ "lint:all": "eslint .",
24
+ "lint:fix": "eslint . --fix --cache",
25
+ "lint:fix-all": "eslint . --fix",
26
+ "test": "node tests/run.js",
27
+ "demo": "eslint --config demo/eslint.config.js demo/buggy-script.js"
28
+ },
29
+ "keywords": [
30
+ "code-quality",
31
+ "constructor",
32
+ "error",
33
+ "eslint",
34
+ "eslint-plugin",
35
+ "eslintplugin",
36
+ "security"
37
+ ],
38
+ "devDependencies": {
39
+ "@e18e/eslint-plugin": "^0.8.0",
40
+ "@eslint/js": "^10.0.1",
41
+ "@eslint/json": "^2.1.0",
42
+ "@eslint/markdown": "^8.0.3",
43
+ "@stylistic/eslint-plugin": "^5.10.0",
44
+ "eslint": "^10.10.0",
45
+ "eslint-plugin-import-x": "^4.17.1",
46
+ "eslint-plugin-no-secrets": "^2.3.3",
47
+ "eslint-plugin-no-unsanitized": "^4.1.5",
48
+ "eslint-plugin-promise": "^7.3.0",
49
+ "eslint-plugin-regexp": "^3.3.0",
50
+ "globals": "^17.12.0",
51
+ "husky": "^9.1.7"
52
+ },
53
+ "peerDependencies": {
54
+ "eslint": ">=9.0.0"
55
+ }
56
+ }
package/src/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import requireNew from './rules/require-new.js'
2
+ import requireErrorSubclassName from './rules/require-error-subclass-name.js'
3
+ import noExplicitReturn from './rules/no-explicit-return.js'
4
+
5
+ const plugin = {
6
+ rules: {
7
+ 'require-new': requireNew,
8
+ 'require-error-subclass-name': requireErrorSubclassName,
9
+ 'no-explicit-return': noExplicitReturn
10
+ }
11
+ }
12
+
13
+ plugin.configs = {
14
+ recommended: [
15
+ {
16
+ plugins: { constructors: plugin },
17
+ rules: {
18
+ 'constructors/require-new': 'error',
19
+ 'constructors/require-error-subclass-name': 'error',
20
+ 'constructors/no-explicit-return': 'error'
21
+ }
22
+ }
23
+ ]
24
+ }
25
+
26
+ export default plugin
@@ -0,0 +1,37 @@
1
+ const reFuncNodeType = /^(?:Arrow)?Function(?:Declaration|Expression)$/
2
+
3
+ export default {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Disallow explicit return statements in class constructors',
8
+ category: 'Best Practices',
9
+ recommended: true
10
+ },
11
+ schema: [],
12
+ messages: { unexpectedReturn: 'Unexpected return statement in constructor' }
13
+ },
14
+
15
+ create(context) {
16
+ function isInsideConstructorFunc(node) {
17
+ let current = node.parent
18
+ while (current) {
19
+ if (reFuncNodeType.test(current.type))
20
+ return ( // whether this func is constructor's val
21
+ current.parent?.type == 'MethodDefinition'
22
+ && current.parent.kind == 'constructor'
23
+ && !current.parent.static
24
+ )
25
+ current = current.parent
26
+ }
27
+ return false
28
+ }
29
+
30
+ return {
31
+ ReturnStatement(node) {
32
+ if (isInsideConstructorFunc(node))
33
+ context.report({ node, messageId: 'unexpectedReturn' })
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,39 @@
1
+ export default {
2
+ meta: {
3
+ type: 'suggestion',
4
+ docs: {
5
+ description: 'Require that classes extending Error have names ending in "Error"',
6
+ category: 'Best Practices',
7
+ recommended: true
8
+ },
9
+ schema: [],
10
+ messages: { endWithError: 'Class extending Error must have a name ending in "Error"' }
11
+ },
12
+
13
+ create(context) {
14
+ function checkName(name, node) {
15
+ if (!name || name.endsWith('Error')) return
16
+ context.report({ node, messageId: 'endWithError' })
17
+ }
18
+
19
+ function checkClassNode(node) {
20
+ if (!node.id || !node.superClass?.name?.endsWith('Error')) return
21
+ checkName(node.id.name, node.id)
22
+ }
23
+
24
+ function checkVariableDeclarator(node) {
25
+ const classExpr = node.init
26
+ if (classExpr?.type != 'ClassExpression'
27
+ || !classExpr.superClass?.name?.endsWith('Error')
28
+ || classExpr.id // named class expression, already checked
29
+ ) return
30
+ checkName(node.id.name, node.id)
31
+ }
32
+
33
+ return {
34
+ ClassDeclaration: checkClassNode,
35
+ ClassExpression: checkClassNode,
36
+ VariableDeclarator: checkVariableDeclarator
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,49 @@
1
+ const ERR_CONSTRUCTORS = new Set([
2
+ 'Error',
3
+ 'TypeError',
4
+ 'RangeError',
5
+ 'SyntaxError',
6
+ 'ReferenceError',
7
+ 'URIError',
8
+ 'EvalError',
9
+ 'AggregateError'
10
+ ])
11
+
12
+ const BUILTIN_CONSTRUCTORS = new Set([
13
+ 'Array', 'Boolean', 'Date', 'Function', 'Number', 'Object',
14
+ 'RegExp', 'String', 'Symbol', 'BigInt', 'Map', 'Set',
15
+ 'WeakMap', 'WeakSet'
16
+ ])
17
+
18
+ function isErrConstructor(name) { return ERR_CONSTRUCTORS.has(name) }
19
+
20
+ export default {
21
+ meta: {
22
+ type: 'problem',
23
+ docs: {
24
+ description: 'Require `new` when calling constructor functions',
25
+ category: 'Best Practices',
26
+ recommended: true
27
+ },
28
+ fixable: 'code',
29
+ schema: [{ enum: ['error-only', 'builtins'] }],
30
+ messages: { requireNew: 'Use `new {{name}}()` instead of `{{name}}()`' }
31
+ },
32
+
33
+ create(context) {
34
+ const mode = context.options[0] || 'error-only'
35
+ return {
36
+ CallExpression(node) {
37
+ if (node.new) return
38
+ const calleeName = node.callee.name ; if (!calleeName) return
39
+ const shouldReport = mode == 'error-only' ? isErrConstructor(calleeName)
40
+ : isErrConstructor(calleeName) || BUILTIN_CONSTRUCTORS.has(calleeName)
41
+ if (shouldReport)
42
+ context.report({
43
+ node, messageId: 'requireNew', data: { name: calleeName },
44
+ fix(fixer) { return fixer.insertTextBefore(node, 'new ') }
45
+ })
46
+ }
47
+ }
48
+ }
49
+ }