eslint-plugin-unicorn 28.0.2 → 29.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/index.js +3 -0
- package/package.json +24 -17
- package/readme.md +5 -1
- package/rules/filename-case.js +1 -3
- package/rules/no-array-callback-reference.js +1 -1
- package/rules/no-array-for-each.js +46 -12
- package/rules/no-array-push-push.js +3 -5
- package/rules/no-static-only-class.js +233 -0
- package/rules/prefer-array-flat.js +274 -0
- package/rules/prefer-default-parameters.js +10 -1
- package/rules/prefer-optional-catch-binding.js +12 -5
- package/rules/prefer-spread.js +43 -8
- package/rules/prefer-ternary.js +11 -8
- package/rules/utils/assert-token.js +32 -0
- package/rules/utils/get-class-head-location.js +22 -0
- package/rules/utils/is-node-matches.js +15 -2
- package/rules/utils/is-same-reference.js +152 -0
- package/rules/utils/method-selector.js +33 -10
package/index.js
CHANGED
|
@@ -71,6 +71,7 @@ module.exports = {
|
|
|
71
71
|
'unicorn/no-null': 'error',
|
|
72
72
|
'unicorn/no-object-as-default-parameter': 'error',
|
|
73
73
|
'unicorn/no-process-exit': 'error',
|
|
74
|
+
'unicorn/no-static-only-class': 'error',
|
|
74
75
|
'unicorn/no-this-assignment': 'error',
|
|
75
76
|
'unicorn/no-unreadable-array-destructuring': 'error',
|
|
76
77
|
'unicorn/no-unsafe-regex': 'off',
|
|
@@ -83,6 +84,8 @@ module.exports = {
|
|
|
83
84
|
'unicorn/prefer-add-event-listener': 'error',
|
|
84
85
|
'unicorn/prefer-array-find': 'error',
|
|
85
86
|
// TODO: Enable this by default when targeting Node.js 12.
|
|
87
|
+
'unicorn/prefer-array-flat': 'off',
|
|
88
|
+
// TODO: Enable this by default when targeting Node.js 12.
|
|
86
89
|
'unicorn/prefer-array-flat-map': 'off',
|
|
87
90
|
'unicorn/prefer-array-index-of': 'error',
|
|
88
91
|
'unicorn/prefer-array-some': 'error',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-unicorn",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "29.0.0",
|
|
4
4
|
"description": "Various awesome ESLint rules",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "sindresorhus/eslint-plugin-unicorn",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "xo && nyc ava",
|
|
18
18
|
"create-rule": "node ./scripts/create-rule.js",
|
|
19
|
-
"run-rules-on-codebase": "node ./test/run-rules-on-codebase/lint.
|
|
19
|
+
"run-rules-on-codebase": "node ./test/run-rules-on-codebase/lint.mjs",
|
|
20
20
|
"integration": "node ./test/integration/test.js",
|
|
21
21
|
"smoke": "eslint-remote-tester --config ./test/smoke/eslint-remote-tester.config.js"
|
|
22
22
|
},
|
|
@@ -35,52 +35,52 @@
|
|
|
35
35
|
"xo"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"ci-info": "^
|
|
38
|
+
"ci-info": "^3.1.1",
|
|
39
39
|
"clean-regexp": "^1.0.0",
|
|
40
|
-
"eslint-template-visitor": "^2.
|
|
40
|
+
"eslint-template-visitor": "^2.3.2",
|
|
41
41
|
"eslint-utils": "^2.1.0",
|
|
42
42
|
"eslint-visitor-keys": "^2.0.0",
|
|
43
43
|
"import-modules": "^2.1.0",
|
|
44
44
|
"lodash": "^4.17.20",
|
|
45
45
|
"pluralize": "^8.0.0",
|
|
46
46
|
"read-pkg-up": "^7.0.1",
|
|
47
|
-
"regexp-tree": "^0.1.
|
|
47
|
+
"regexp-tree": "^0.1.23",
|
|
48
48
|
"reserved-words": "^0.1.2",
|
|
49
49
|
"safe-regex": "^2.1.1",
|
|
50
50
|
"semver": "^7.3.4"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@ava/babel": "^1.0.1",
|
|
54
53
|
"@babel/code-frame": "7.12.13",
|
|
55
|
-
"@babel/core": "7.12.
|
|
56
|
-
"@babel/eslint-parser": "7.12.
|
|
54
|
+
"@babel/core": "7.12.17",
|
|
55
|
+
"@babel/eslint-parser": "7.12.17",
|
|
57
56
|
"@lubien/fixture-beta-package": "^1.0.0-beta.1",
|
|
58
|
-
"@typescript-eslint/parser": "^4.
|
|
57
|
+
"@typescript-eslint/parser": "^4.15.1",
|
|
59
58
|
"ava": "^3.15.0",
|
|
60
59
|
"babel-eslint": "^10.1.0",
|
|
61
60
|
"chalk": "^4.1.0",
|
|
62
61
|
"enquirer": "2.3.6",
|
|
63
|
-
"eslint": "^7.
|
|
62
|
+
"eslint": "^7.20.0",
|
|
64
63
|
"eslint-ava-rule-tester": "^4.0.0",
|
|
65
64
|
"eslint-plugin-eslint-plugin": "^2.3.0",
|
|
66
65
|
"eslint-remote-tester": "^1.1.0",
|
|
67
66
|
"execa": "^5.0.0",
|
|
68
67
|
"listr": "^0.14.3",
|
|
68
|
+
"lodash-es": "4.17.20",
|
|
69
69
|
"mem": "8.0.0",
|
|
70
70
|
"nyc": "^15.1.0",
|
|
71
71
|
"outdent": "^0.8.0",
|
|
72
72
|
"pify": "^5.0.0",
|
|
73
|
-
"typescript": "^4.1.
|
|
74
|
-
"vue-eslint-parser": "^7.
|
|
75
|
-
"xo": "^0.
|
|
73
|
+
"typescript": "^4.1.5",
|
|
74
|
+
"vue-eslint-parser": "^7.5.0",
|
|
75
|
+
"xo": "^0.38.1"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"eslint": ">=7.
|
|
78
|
+
"eslint": ">=7.20.0"
|
|
79
79
|
},
|
|
80
80
|
"ava": {
|
|
81
|
-
"babel": true,
|
|
82
81
|
"files": [
|
|
83
|
-
"test/*.
|
|
82
|
+
"test/*.mjs",
|
|
83
|
+
"test/unit/*.mjs"
|
|
84
84
|
]
|
|
85
85
|
},
|
|
86
86
|
"nyc": {
|
|
@@ -114,7 +114,6 @@
|
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
"files": [
|
|
117
|
-
"test/*.js",
|
|
118
117
|
"**/*.mjs"
|
|
119
118
|
],
|
|
120
119
|
"parserOptions": {
|
|
@@ -124,6 +123,14 @@
|
|
|
124
123
|
],
|
|
125
124
|
"rules": {
|
|
126
125
|
"strict": "error",
|
|
126
|
+
"node/no-unsupported-features/node-builtins": [
|
|
127
|
+
"off",
|
|
128
|
+
{
|
|
129
|
+
"ignores": [
|
|
130
|
+
"module.createRequire"
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
],
|
|
127
134
|
"unicorn/no-null": "error",
|
|
128
135
|
"unicorn/prevent-abbreviations": [
|
|
129
136
|
"error",
|
package/readme.md
CHANGED
|
@@ -65,6 +65,7 @@ Configure it in `package.json`.
|
|
|
65
65
|
"unicorn/no-null": "error",
|
|
66
66
|
"unicorn/no-object-as-default-parameter": "error",
|
|
67
67
|
"unicorn/no-process-exit": "error",
|
|
68
|
+
"unicorn/no-static-only-class": "error",
|
|
68
69
|
"unicorn/no-this-assignment": "error",
|
|
69
70
|
"unicorn/no-unreadable-array-destructuring": "error",
|
|
70
71
|
"unicorn/no-unsafe-regex": "off",
|
|
@@ -75,6 +76,7 @@ Configure it in `package.json`.
|
|
|
75
76
|
"unicorn/numeric-separators-style": "off",
|
|
76
77
|
"unicorn/prefer-add-event-listener": "error",
|
|
77
78
|
"unicorn/prefer-array-find": "error",
|
|
79
|
+
"unicorn/prefer-array-flat": "error",
|
|
78
80
|
"unicorn/prefer-array-flat-map": "error",
|
|
79
81
|
"unicorn/prefer-array-index-of": "error",
|
|
80
82
|
"unicorn/prefer-array-some": "error",
|
|
@@ -143,6 +145,7 @@ Configure it in `package.json`.
|
|
|
143
145
|
- [no-null](docs/rules/no-null.md) - Disallow the use of the `null` literal.
|
|
144
146
|
- [no-object-as-default-parameter](docs/rules/no-object-as-default-parameter.md) - Disallow the use of objects as default parameters.
|
|
145
147
|
- [no-process-exit](docs/rules/no-process-exit.md) - Disallow `process.exit()`.
|
|
148
|
+
- [no-static-only-class](docs/rules/no-static-only-class.md) - Forbid classes that only have static members. *(partly fixable)*
|
|
146
149
|
- [no-this-assignment](docs/rules/no-this-assignment.md) - Disallow assigning `this` to a variable.
|
|
147
150
|
- [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) - Disallow unreadable array destructuring. *(partly fixable)*
|
|
148
151
|
- [no-unsafe-regex](docs/rules/no-unsafe-regex.md) - Disallow unsafe regular expressions.
|
|
@@ -153,6 +156,7 @@ Configure it in `package.json`.
|
|
|
153
156
|
- [numeric-separators-style](docs/rules/numeric-separators-style.md) - Enforce the style of numeric separators by correctly grouping digits. *(fixable)*
|
|
154
157
|
- [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions. *(partly fixable)*
|
|
155
158
|
- [prefer-array-find](docs/rules/prefer-array-find.md) - Prefer `.find(…)` over the first element from `.filter(…)`. *(partly fixable)*
|
|
159
|
+
- [prefer-array-flat](docs/rules/prefer-array-flat.md) - Prefer `Array#flat()` over legacy techniques to flatten arrays. *(fixable)*
|
|
156
160
|
- [prefer-array-flat-map](docs/rules/prefer-array-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
|
|
157
161
|
- [prefer-array-index-of](docs/rules/prefer-array-index-of.md) - Prefer `Array#indexOf()` over `Array#findIndex()` when looking for the index of an item. *(partly fixable)*
|
|
158
162
|
- [prefer-array-some](docs/rules/prefer-array-some.md) - Prefer `.some(…)` over `.find(…)`.
|
|
@@ -173,7 +177,7 @@ Configure it in `package.json`.
|
|
|
173
177
|
- [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) - Prefer `Reflect.apply()` over `Function#apply()`. *(fixable)*
|
|
174
178
|
- [prefer-regexp-test](docs/rules/prefer-regexp-test.md) - Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`. *(fixable)*
|
|
175
179
|
- [prefer-set-has](docs/rules/prefer-set-has.md) - Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. *(fixable)*
|
|
176
|
-
- [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from()` and `Array#
|
|
180
|
+
- [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from(…)`, `Array#concat(…)` and `Array#slice()`. *(partly fixable)*
|
|
177
181
|
- [prefer-string-replace-all](docs/rules/prefer-string-replace-all.md) - Prefer `String#replaceAll()` over regex searches with the global flag. *(fixable)*
|
|
178
182
|
- [prefer-string-slice](docs/rules/prefer-string-slice.md) - Prefer `String#slice()` over `String#substr()` and `String#substring()`. *(partly fixable)*
|
|
179
183
|
- [prefer-string-starts-ends-with](docs/rules/prefer-string-starts-ends-with.md) - Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`. *(fixable)*
|
package/rules/filename-case.js
CHANGED
|
@@ -137,9 +137,7 @@ function englishishJoinWords(words) {
|
|
|
137
137
|
return `${words[0]} or ${words[1]}`;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
const last = words.pop();
|
|
142
|
-
return `${words.join(', ')}, or ${last}`;
|
|
140
|
+
return `${words.slice(0, -1).join(', ')}, or ${words[words.length - 1]}`;
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
const create = context => {
|
|
@@ -3,7 +3,7 @@ const {isParenthesized} = require('eslint-utils');
|
|
|
3
3
|
const getDocumentationUrl = require('./utils/get-documentation-url');
|
|
4
4
|
const methodSelector = require('./utils/method-selector');
|
|
5
5
|
const {notFunctionSelector} = require('./utils/not-function');
|
|
6
|
-
const isNodeMatches = require('./utils/is-node-matches');
|
|
6
|
+
const {isNodeMatches} = require('./utils/is-node-matches');
|
|
7
7
|
|
|
8
8
|
const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
|
|
9
9
|
const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
|
|
@@ -14,7 +14,8 @@ const shouldAddParenthesesToExpressionStatementExpression = require('./utils/sho
|
|
|
14
14
|
const getParenthesizedTimes = require('./utils/get-parenthesized-times');
|
|
15
15
|
const extendFixRange = require('./utils/extend-fix-range');
|
|
16
16
|
const isFunctionSelfUsedInside = require('./utils/is-function-self-used-inside');
|
|
17
|
-
const isNodeMatches = require('./utils/is-node-matches');
|
|
17
|
+
const {isNodeMatches} = require('./utils/is-node-matches');
|
|
18
|
+
const assertToken = require('./utils/assert-token');
|
|
18
19
|
|
|
19
20
|
const MESSAGE_ID = 'no-array-for-each';
|
|
20
21
|
const messages = {
|
|
@@ -23,7 +24,8 @@ const messages = {
|
|
|
23
24
|
|
|
24
25
|
const arrayForEachCallSelector = methodSelector({
|
|
25
26
|
name: 'forEach',
|
|
26
|
-
|
|
27
|
+
includeOptionalCall: true,
|
|
28
|
+
includeOptionalMember: true
|
|
27
29
|
});
|
|
28
30
|
|
|
29
31
|
const continueAbleNodeTypes = new Set([
|
|
@@ -48,15 +50,16 @@ function getFixFunction(callExpression, sourceCode, functionInfo) {
|
|
|
48
50
|
const [callback] = callExpression.arguments;
|
|
49
51
|
const parameters = callback.params;
|
|
50
52
|
const array = callExpression.callee.object;
|
|
51
|
-
const {returnStatements} = functionInfo.get(callback);
|
|
53
|
+
const {returnStatements, scope} = functionInfo.get(callback);
|
|
52
54
|
|
|
53
55
|
const getForOfLoopHeadText = () => {
|
|
54
56
|
const [elementText, indexText] = parameters.map(parameter => sourceCode.getText(parameter));
|
|
55
57
|
const useEntries = parameters.length === 2;
|
|
56
58
|
|
|
57
|
-
let text = 'for (
|
|
59
|
+
let text = 'for (';
|
|
60
|
+
text += parameters.some(parameter => isParameterReassigned(parameter, scope)) ? 'let' : 'const';
|
|
61
|
+
text += ' ';
|
|
58
62
|
text += useEntries ? `[${indexText}, ${elementText}]` : elementText;
|
|
59
|
-
|
|
60
63
|
text += ' of ';
|
|
61
64
|
|
|
62
65
|
let arrayText = sourceCode.getText(array);
|
|
@@ -81,6 +84,9 @@ function getFixFunction(callExpression, sourceCode, functionInfo) {
|
|
|
81
84
|
if (callback.body.type === 'BlockStatement') {
|
|
82
85
|
end = callback.body.range[0];
|
|
83
86
|
} else {
|
|
87
|
+
// In this case, parentheses are not included in body location, so we look for `=>` token
|
|
88
|
+
// foo.forEach(bar => ({bar}))
|
|
89
|
+
// ^
|
|
84
90
|
const arrowToken = sourceCode.getTokenBefore(callback.body, isArrowToken);
|
|
85
91
|
end = arrowToken.range[1];
|
|
86
92
|
}
|
|
@@ -90,11 +96,10 @@ function getFixFunction(callExpression, sourceCode, functionInfo) {
|
|
|
90
96
|
|
|
91
97
|
function * replaceReturnStatement(returnStatement, fixer) {
|
|
92
98
|
const returnToken = sourceCode.getFirstToken(returnStatement);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
99
|
+
assertToken(returnToken, {
|
|
100
|
+
expected: 'return',
|
|
101
|
+
ruleId: 'no-array-for-each'
|
|
102
|
+
});
|
|
98
103
|
|
|
99
104
|
if (!returnStatement.argument) {
|
|
100
105
|
yield fixer.replaceText(returnToken, 'continue');
|
|
@@ -166,20 +171,35 @@ function getFixFunction(callExpression, sourceCode, functionInfo) {
|
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
return function * (fixer) {
|
|
174
|
+
// Replace these with `for (const … of …) `
|
|
175
|
+
// foo.forEach(bar => bar)
|
|
176
|
+
// ^^^^^^^^^^^^^^^^^^ (space after `=>` didn't included)
|
|
177
|
+
// foo.forEach(bar => {})
|
|
178
|
+
// ^^^^^^^^^^^^^^^^^^^^^^
|
|
179
|
+
// foo.forEach(function(bar) {})
|
|
180
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
169
181
|
yield fixer.replaceTextRange(getForOfLoopHeadRange(), getForOfLoopHeadText());
|
|
170
182
|
|
|
183
|
+
// Parenthesized callback function
|
|
184
|
+
// foo.forEach( ((bar => {})) )
|
|
185
|
+
// ^^
|
|
171
186
|
yield * removeCallbackParentheses(fixer);
|
|
172
187
|
|
|
173
|
-
// Remove call expression trailing comma
|
|
174
188
|
const [
|
|
175
189
|
penultimateToken,
|
|
176
190
|
lastToken
|
|
177
191
|
] = sourceCode.getLastTokens(callExpression, 2);
|
|
178
192
|
|
|
193
|
+
// The possible trailing comma token of `Array#forEach()` CallExpression
|
|
194
|
+
// foo.forEach(bar => {},)
|
|
195
|
+
// ^
|
|
179
196
|
if (isCommaToken(penultimateToken)) {
|
|
180
197
|
yield fixer.remove(penultimateToken);
|
|
181
198
|
}
|
|
182
199
|
|
|
200
|
+
// The closing parenthesis token of `Array#forEach()` CallExpression
|
|
201
|
+
// foo.forEach(bar => {})
|
|
202
|
+
// ^
|
|
183
203
|
yield fixer.remove(lastToken);
|
|
184
204
|
|
|
185
205
|
for (const returnStatement of returnStatements) {
|
|
@@ -187,11 +207,14 @@ function getFixFunction(callExpression, sourceCode, functionInfo) {
|
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
const expressionStatementLastToken = sourceCode.getLastToken(callExpression.parent);
|
|
210
|
+
// Remove semicolon if it's not needed anymore
|
|
211
|
+
// foo.forEach(bar => {});
|
|
212
|
+
// ^
|
|
190
213
|
if (shouldRemoveExpressionStatementLastToken(expressionStatementLastToken)) {
|
|
191
214
|
yield fixer.remove(expressionStatementLastToken, fixer);
|
|
192
215
|
}
|
|
193
216
|
|
|
194
|
-
// Prevent possible conflicts
|
|
217
|
+
// Prevent possible variable conflicts
|
|
195
218
|
yield * extendFixRange(fixer, callExpression.parent.range);
|
|
196
219
|
};
|
|
197
220
|
}
|
|
@@ -257,6 +280,17 @@ function isParameterSafeToFix(parameter, {scope, array, allIdentifiers}) {
|
|
|
257
280
|
return true;
|
|
258
281
|
}
|
|
259
282
|
|
|
283
|
+
function isParameterReassigned(parameter, scope) {
|
|
284
|
+
const variable = findVariable(scope, parameter);
|
|
285
|
+
const {references} = variable;
|
|
286
|
+
return references.some(reference => {
|
|
287
|
+
const node = reference.identifier;
|
|
288
|
+
const {parent} = node;
|
|
289
|
+
return parent.type === 'UpdateExpression' ||
|
|
290
|
+
(parent.type === 'AssignmentExpression' && parent.left === node);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
260
294
|
function isFixable(callExpression, sourceCode, {scope, functionInfo, allIdentifiers}) {
|
|
261
295
|
// Check `CallExpression`
|
|
262
296
|
if (
|
|
@@ -3,6 +3,7 @@ const {hasSideEffect, isCommaToken, isSemicolonToken} = require('eslint-utils');
|
|
|
3
3
|
const getDocumentationUrl = require('./utils/get-documentation-url');
|
|
4
4
|
const methodSelector = require('./utils/method-selector');
|
|
5
5
|
const getCallExpressionArgumentsText = require('./utils/get-call-expression-arguments-text');
|
|
6
|
+
const isSameReference = require('./utils/is-same-reference');
|
|
6
7
|
|
|
7
8
|
const ERROR = 'error';
|
|
8
9
|
const SUGGESTION = 'suggestion';
|
|
@@ -53,7 +54,7 @@ function create(context) {
|
|
|
53
54
|
const secondCallArray = secondCall.callee.object;
|
|
54
55
|
|
|
55
56
|
// Not same array
|
|
56
|
-
if (
|
|
57
|
+
if (!isSameReference(firstCallArray, secondCallArray)) {
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -83,10 +84,7 @@ function create(context) {
|
|
|
83
84
|
);
|
|
84
85
|
};
|
|
85
86
|
|
|
86
|
-
if (
|
|
87
|
-
hasSideEffect(firstCallArray, sourceCode) ||
|
|
88
|
-
secondCallArguments.some(element => hasSideEffect(element, sourceCode))
|
|
89
|
-
) {
|
|
87
|
+
if (secondCallArguments.some(element => hasSideEffect(element, sourceCode))) {
|
|
90
88
|
problem.suggest = [
|
|
91
89
|
{
|
|
92
90
|
messageId: SUGGESTION,
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {isSemicolonToken} = require('eslint-utils');
|
|
3
|
+
const getDocumentationUrl = require('./utils/get-documentation-url');
|
|
4
|
+
const getClassHeadLocation = require('./utils/get-class-head-location');
|
|
5
|
+
const removeSpacesAfter = require('./utils/remove-spaces-after');
|
|
6
|
+
const assertToken = require('./utils/assert-token');
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'no-static-only-class';
|
|
9
|
+
const messages = {
|
|
10
|
+
[MESSAGE_ID]: 'Use an object instead of a class with only static members.'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const selector = [
|
|
14
|
+
':matches(ClassDeclaration, ClassExpression)',
|
|
15
|
+
':not([superClass], [decorators.length>0])',
|
|
16
|
+
'[body.type="ClassBody"]',
|
|
17
|
+
'[body.body.length>0]'
|
|
18
|
+
].join('');
|
|
19
|
+
|
|
20
|
+
const isEqualToken = ({type, value}) => type === 'Punctuator' && value === '=';
|
|
21
|
+
const isDeclarationOfExportDefaultDeclaration = node =>
|
|
22
|
+
node.type === 'ClassDeclaration' &&
|
|
23
|
+
node.parent.type === 'ExportDefaultDeclaration' &&
|
|
24
|
+
node.parent.declaration === node;
|
|
25
|
+
|
|
26
|
+
function isStaticMember(node) {
|
|
27
|
+
const {
|
|
28
|
+
type,
|
|
29
|
+
private: isPrivate,
|
|
30
|
+
static: isStatic,
|
|
31
|
+
declare: isDeclare,
|
|
32
|
+
readonly: isReadonly,
|
|
33
|
+
accessibility,
|
|
34
|
+
decorators,
|
|
35
|
+
key
|
|
36
|
+
} = node;
|
|
37
|
+
|
|
38
|
+
// Avoid matching unexpected node. For example: https://github.com/tc39/proposal-class-static-block
|
|
39
|
+
/* istanbul ignore next */
|
|
40
|
+
if (type !== 'ClassProperty' && type !== 'MethodDefinition') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!isStatic || isPrivate) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// TypeScript class
|
|
49
|
+
if (
|
|
50
|
+
isDeclare ||
|
|
51
|
+
isReadonly ||
|
|
52
|
+
typeof accessibility !== 'undefined' ||
|
|
53
|
+
(Array.isArray(decorators) && decorators.length > 0) ||
|
|
54
|
+
key.type === 'TSPrivateIdentifier'
|
|
55
|
+
) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function * switchClassMemberToObjectProperty(node, sourceCode, fixer) {
|
|
63
|
+
const {type} = node;
|
|
64
|
+
|
|
65
|
+
const staticToken = sourceCode.getFirstToken(node);
|
|
66
|
+
assertToken(staticToken, {
|
|
67
|
+
expected: [
|
|
68
|
+
{type: 'Keyword', value: 'static'},
|
|
69
|
+
// `babel-eslint` and `@babel/eslint-parser` use `{type: 'Identifier', value: 'static'}`
|
|
70
|
+
{type: 'Identifier', value: 'static'}
|
|
71
|
+
],
|
|
72
|
+
ruleId: 'no-static-only-class'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
yield fixer.remove(staticToken);
|
|
76
|
+
yield removeSpacesAfter(staticToken, sourceCode, fixer);
|
|
77
|
+
|
|
78
|
+
const maybeSemicolonToken = type === 'ClassProperty' ?
|
|
79
|
+
sourceCode.getLastToken(node) :
|
|
80
|
+
sourceCode.getTokenAfter(node);
|
|
81
|
+
const hasSemicolonToken = isSemicolonToken(maybeSemicolonToken);
|
|
82
|
+
|
|
83
|
+
if (type === 'ClassProperty') {
|
|
84
|
+
const {key, value} = node;
|
|
85
|
+
|
|
86
|
+
if (value) {
|
|
87
|
+
// Computed key may have `]` after `key`
|
|
88
|
+
const equalToken = sourceCode.getTokenAfter(key, isEqualToken);
|
|
89
|
+
yield fixer.replaceText(equalToken, ':');
|
|
90
|
+
} else if (hasSemicolonToken) {
|
|
91
|
+
yield fixer.insertTextBefore(maybeSemicolonToken, ': undefined');
|
|
92
|
+
} else {
|
|
93
|
+
yield fixer.insertTextAfter(node, ': undefined');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
yield (
|
|
98
|
+
hasSemicolonToken ?
|
|
99
|
+
fixer.replaceText(maybeSemicolonToken, ',') :
|
|
100
|
+
fixer.insertTextAfter(node, ',')
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function switchClassToObject(node, sourceCode) {
|
|
105
|
+
const {
|
|
106
|
+
type,
|
|
107
|
+
id,
|
|
108
|
+
body,
|
|
109
|
+
declare: isDeclare,
|
|
110
|
+
abstract: isAbstract,
|
|
111
|
+
implements: classImplements,
|
|
112
|
+
parent
|
|
113
|
+
} = node;
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
isDeclare ||
|
|
117
|
+
isAbstract ||
|
|
118
|
+
(Array.isArray(classImplements) && classImplements.length > 0)
|
|
119
|
+
) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (type === 'ClassExpression' && id) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const isExportDefault = isDeclarationOfExportDefaultDeclaration(node);
|
|
128
|
+
|
|
129
|
+
if (isExportDefault && id) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const node of body.body) {
|
|
134
|
+
if (
|
|
135
|
+
node.type === 'ClassProperty' &&
|
|
136
|
+
(
|
|
137
|
+
node.typeAnnotation ||
|
|
138
|
+
// This is a stupid way to check if `value` of `ClassProperty` uses `this`
|
|
139
|
+
(node.value && sourceCode.getText(node).includes('this'))
|
|
140
|
+
)
|
|
141
|
+
) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return function * (fixer) {
|
|
147
|
+
const classToken = sourceCode.getFirstToken(node);
|
|
148
|
+
/* istanbul ignore next */
|
|
149
|
+
assertToken(classToken, {
|
|
150
|
+
expected: {type: 'Keyword', value: 'class'},
|
|
151
|
+
ruleId: 'no-static-only-class'
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (isExportDefault || type === 'ClassExpression') {
|
|
155
|
+
/*
|
|
156
|
+
There are comments after return, and `{` is not on same line
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
function a() {
|
|
160
|
+
return class // comment
|
|
161
|
+
{
|
|
162
|
+
static a() {}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
*/
|
|
167
|
+
if (
|
|
168
|
+
type === 'ClassExpression' &&
|
|
169
|
+
parent.type === 'ReturnStatement' &&
|
|
170
|
+
body.loc.start.line !== parent.loc.start.line &&
|
|
171
|
+
sourceCode.text.slice(classToken.range[1], body.range[0]).trim()
|
|
172
|
+
) {
|
|
173
|
+
yield fixer.replaceText(classToken, '{');
|
|
174
|
+
|
|
175
|
+
const openingBraceToken = sourceCode.getFirstToken(body);
|
|
176
|
+
yield fixer.remove(openingBraceToken);
|
|
177
|
+
} else {
|
|
178
|
+
yield fixer.replaceText(classToken, '');
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
Avoid breaking case like
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
return class
|
|
185
|
+
{};
|
|
186
|
+
```
|
|
187
|
+
*/
|
|
188
|
+
yield removeSpacesAfter(classToken, sourceCode, fixer);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// There should not be ASI problem
|
|
192
|
+
} else {
|
|
193
|
+
yield fixer.replaceText(classToken, 'const');
|
|
194
|
+
yield fixer.insertTextBefore(body, '= ');
|
|
195
|
+
yield fixer.insertTextAfter(body, ';');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const node of body.body) {
|
|
199
|
+
yield * switchClassMemberToObjectProperty(node, sourceCode, fixer);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function create(context) {
|
|
205
|
+
const sourceCode = context.getSourceCode();
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
[selector](node) {
|
|
209
|
+
if (node.body.body.some(node => !isStaticMember(node))) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
context.report({
|
|
214
|
+
node,
|
|
215
|
+
loc: getClassHeadLocation(node, sourceCode),
|
|
216
|
+
messageId: MESSAGE_ID,
|
|
217
|
+
fix: switchClassToObject(node, sourceCode)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
create,
|
|
225
|
+
meta: {
|
|
226
|
+
type: 'suggestion',
|
|
227
|
+
docs: {
|
|
228
|
+
url: getDocumentationUrl(__filename)
|
|
229
|
+
},
|
|
230
|
+
fixable: 'code',
|
|
231
|
+
messages
|
|
232
|
+
}
|
|
233
|
+
};
|