eslint-plugin-unicorn-ts 0.0.1-security → 50.0.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.
Potentially problematic release.
This version of eslint-plugin-unicorn-ts might be problematic. Click here for more details.
- package/configs/all.js +6 -0
- package/configs/flat-config-base.js +10 -0
- package/configs/legacy-config-base.js +10 -0
- package/configs/recommended.js +117 -0
- package/index.js +91 -0
- package/license +9 -0
- package/package.json +186 -4
- package/readme.md +356 -0
- package/rules/ast/call-or-new-expression.js +127 -0
- package/rules/ast/function-types.js +5 -0
- package/rules/ast/index.js +39 -0
- package/rules/ast/is-arrow-function-body.js +7 -0
- package/rules/ast/is-empty-node.js +20 -0
- package/rules/ast/is-expression-statement.js +11 -0
- package/rules/ast/is-function.js +8 -0
- package/rules/ast/is-member-expression.js +101 -0
- package/rules/ast/is-method-call.js +65 -0
- package/rules/ast/is-reference-identifier.js +156 -0
- package/rules/ast/is-static-require.js +14 -0
- package/rules/ast/is-undefined.js +7 -0
- package/rules/ast/literal.js +29 -0
- package/rules/better-regex.js +144 -0
- package/rules/catch-error-name.js +136 -0
- package/rules/consistent-destructuring.js +168 -0
- package/rules/consistent-function-scoping.js +223 -0
- package/rules/custom-error-definition.js +215 -0
- package/rules/empty-brace-spaces.js +72 -0
- package/rules/error-message.js +104 -0
- package/rules/escape-case.js +63 -0
- package/rules/expiring-todo-comments.js +580 -0
- package/rules/explicit-length-check.js +229 -0
- package/rules/filename-case.js +258 -0
- package/rules/fix/add-parenthesizes-to-return-or-throw-expression.js +21 -0
- package/rules/fix/append-argument.js +20 -0
- package/rules/fix/extend-fix-range.js +15 -0
- package/rules/fix/fix-space-around-keywords.js +35 -0
- package/rules/fix/index.js +23 -0
- package/rules/fix/remove-argument.js +32 -0
- package/rules/fix/remove-member-expression-property.js +11 -0
- package/rules/fix/remove-method-call.js +20 -0
- package/rules/fix/remove-parentheses.js +11 -0
- package/rules/fix/remove-spaces-after.js +14 -0
- package/rules/fix/rename-variable.js +9 -0
- package/rules/fix/replace-argument.js +8 -0
- package/rules/fix/replace-node-or-token-and-spaces-before.js +21 -0
- package/rules/fix/replace-reference-identifier.js +35 -0
- package/rules/fix/replace-string-literal.js +11 -0
- package/rules/fix/replace-string-raw.js +14 -0
- package/rules/fix/replace-template-element.js +11 -0
- package/rules/fix/switch-call-expression-to-new-expression.js +18 -0
- package/rules/fix/switch-new-expression-to-call-expression.js +34 -0
- package/rules/import-style.js +364 -0
- package/rules/new-for-builtins.js +85 -0
- package/rules/no-abusive-eslint-disable.js +48 -0
- package/rules/no-array-callback-reference.js +256 -0
- package/rules/no-array-for-each.js +473 -0
- package/rules/no-array-method-this-argument.js +188 -0
- package/rules/no-array-push-push.js +144 -0
- package/rules/no-array-reduce.js +126 -0
- package/rules/no-await-expression-member.js +90 -0
- package/rules/no-console-spaces.js +86 -0
- package/rules/no-document-cookie.js +25 -0
- package/rules/no-empty-file.js +57 -0
- package/rules/no-for-loop.js +427 -0
- package/rules/no-hex-escape.js +46 -0
- package/rules/no-instanceof-array.js +65 -0
- package/rules/no-invalid-remove-event-listener.js +60 -0
- package/rules/no-keyword-prefix.js +199 -0
- package/rules/no-lonely-if.js +151 -0
- package/rules/no-negated-condition.js +144 -0
- package/rules/no-nested-ternary.js +58 -0
- package/rules/no-new-array.js +104 -0
- package/rules/no-new-buffer.js +98 -0
- package/rules/no-null.js +153 -0
- package/rules/no-object-as-default-parameter.js +50 -0
- package/rules/no-process-exit.js +104 -0
- package/rules/no-static-only-class.js +224 -0
- package/rules/no-thenable.js +198 -0
- package/rules/no-this-assignment.js +38 -0
- package/rules/no-typeof-undefined.js +143 -0
- package/rules/no-unnecessary-await.js +107 -0
- package/rules/no-unnecessary-polyfills.js +176 -0
- package/rules/no-unreadable-array-destructuring.js +83 -0
- package/rules/no-unreadable-iife.js +45 -0
- package/rules/no-unused-properties.js +238 -0
- package/rules/no-useless-fallback-in-spread.js +68 -0
- package/rules/no-useless-length-check.js +152 -0
- package/rules/no-useless-promise-resolve-reject.js +212 -0
- package/rules/no-useless-spread.js +381 -0
- package/rules/no-useless-switch-case.js +71 -0
- package/rules/no-useless-undefined.js +301 -0
- package/rules/no-zero-fractions.js +79 -0
- package/rules/number-literal-case.js +52 -0
- package/rules/numeric-separators-style.js +181 -0
- package/rules/prefer-add-event-listener.js +188 -0
- package/rules/prefer-array-find.js +423 -0
- package/rules/prefer-array-flat-map.js +82 -0
- package/rules/prefer-array-flat.js +279 -0
- package/rules/prefer-array-index-of.js +32 -0
- package/rules/prefer-array-some.js +157 -0
- package/rules/prefer-at.js +374 -0
- package/rules/prefer-blob-reading-methods.js +45 -0
- package/rules/prefer-code-point.js +67 -0
- package/rules/prefer-date-now.js +135 -0
- package/rules/prefer-default-parameters.js +219 -0
- package/rules/prefer-dom-node-append.js +48 -0
- package/rules/prefer-dom-node-dataset.js +120 -0
- package/rules/prefer-dom-node-remove.js +122 -0
- package/rules/prefer-dom-node-text-content.js +75 -0
- package/rules/prefer-event-target.js +117 -0
- package/rules/prefer-export-from.js +413 -0
- package/rules/prefer-includes.js +98 -0
- package/rules/prefer-json-parse-buffer.js +159 -0
- package/rules/prefer-keyboard-event-key.js +186 -0
- package/rules/prefer-logical-operator-over-ternary.js +159 -0
- package/rules/prefer-math-trunc.js +109 -0
- package/rules/prefer-modern-dom-apis.js +141 -0
- package/rules/prefer-modern-math-apis.js +212 -0
- package/rules/prefer-module.js +349 -0
- package/rules/prefer-native-coercion-functions.js +185 -0
- package/rules/prefer-negative-index.js +213 -0
- package/rules/prefer-node-protocol.js +61 -0
- package/rules/prefer-number-properties.js +126 -0
- package/rules/prefer-object-from-entries.js +252 -0
- package/rules/prefer-optional-catch-binding.js +75 -0
- package/rules/prefer-prototype-methods.js +88 -0
- package/rules/prefer-query-selector.js +135 -0
- package/rules/prefer-reflect-apply.js +97 -0
- package/rules/prefer-regexp-test.js +156 -0
- package/rules/prefer-set-has.js +186 -0
- package/rules/prefer-set-size.js +103 -0
- package/rules/prefer-spread.js +529 -0
- package/rules/prefer-string-replace-all.js +145 -0
- package/rules/prefer-string-slice.js +182 -0
- package/rules/prefer-string-starts-ends-with.js +199 -0
- package/rules/prefer-string-trim-start-end.js +44 -0
- package/rules/prefer-switch.js +344 -0
- package/rules/prefer-ternary.js +282 -0
- package/rules/prefer-top-level-await.js +152 -0
- package/rules/prefer-type-error.js +151 -0
- package/rules/prevent-abbreviations.js +645 -0
- package/rules/relative-url-style.js +168 -0
- package/rules/require-array-join-separator.js +63 -0
- package/rules/require-number-to-fixed-digits-argument.js +54 -0
- package/rules/require-post-message-target-origin.js +71 -0
- package/rules/shared/abbreviations.js +262 -0
- package/rules/shared/dom-events.js +275 -0
- package/rules/shared/event-keys.js +52 -0
- package/rules/shared/negative-index.js +46 -0
- package/rules/shared/simple-array-search-rule.js +128 -0
- package/rules/shared/typed-array.js +16 -0
- package/rules/string-content.js +187 -0
- package/rules/switch-case-braces.js +109 -0
- package/rules/template-indent.js +219 -0
- package/rules/text-encoding-identifier-case.js +108 -0
- package/rules/throw-new-error.js +53 -0
- package/rules/utils/array-or-object-prototype-property.js +63 -0
- package/rules/utils/assert-token.js +32 -0
- package/rules/utils/avoid-capture.js +146 -0
- package/rules/utils/boolean.js +92 -0
- package/rules/utils/builtins.js +36 -0
- package/rules/utils/cartesian-product-samples.js +24 -0
- package/rules/utils/create-deprecated-rules.js +25 -0
- package/rules/utils/escape-string.js +26 -0
- package/rules/utils/escape-template-element-raw.js +6 -0
- package/rules/utils/get-ancestor.js +20 -0
- package/rules/utils/get-builtin-rule.js +7 -0
- package/rules/utils/get-call-expression-arguments-text.js +21 -0
- package/rules/utils/get-class-head-location.js +22 -0
- package/rules/utils/get-documentation-url.js +10 -0
- package/rules/utils/get-indent-string.js +11 -0
- package/rules/utils/get-previous-node.js +24 -0
- package/rules/utils/get-references.js +9 -0
- package/rules/utils/get-scopes.js +14 -0
- package/rules/utils/get-switch-case-head-location.js +21 -0
- package/rules/utils/get-variable-identifiers.js +7 -0
- package/rules/utils/global-reference-tracker.js +72 -0
- package/rules/utils/has-optional-chain-element.js +21 -0
- package/rules/utils/has-same-range.js +7 -0
- package/rules/utils/index.js +53 -0
- package/rules/utils/is-function-self-used-inside.js +43 -0
- package/rules/utils/is-left-hand-side.js +22 -0
- package/rules/utils/is-logical-expression.js +16 -0
- package/rules/utils/is-method-named.js +9 -0
- package/rules/utils/is-new-expression-with-parentheses.js +26 -0
- package/rules/utils/is-node-matches.js +53 -0
- package/rules/utils/is-node-value-not-dom-node.js +21 -0
- package/rules/utils/is-node-value-not-function.js +42 -0
- package/rules/utils/is-number.js +224 -0
- package/rules/utils/is-object-method.js +11 -0
- package/rules/utils/is-on-same-line.js +7 -0
- package/rules/utils/is-same-identifier.js +8 -0
- package/rules/utils/is-same-reference.js +173 -0
- package/rules/utils/is-shadowed.js +33 -0
- package/rules/utils/is-shorthand-export-local.js +9 -0
- package/rules/utils/is-shorthand-import-local.js +9 -0
- package/rules/utils/is-shorthand-property-assignment-pattern-left.js +10 -0
- package/rules/utils/is-shorthand-property-value.js +8 -0
- package/rules/utils/is-value-not-usable.js +5 -0
- package/rules/utils/lodash.js +1589 -0
- package/rules/utils/needs-semicolon.js +114 -0
- package/rules/utils/numeric.js +53 -0
- package/rules/utils/parentheses.js +73 -0
- package/rules/utils/resolve-variable-name.js +20 -0
- package/rules/utils/rule.js +190 -0
- package/rules/utils/should-add-parentheses-to-conditional-expression-child.js +17 -0
- package/rules/utils/should-add-parentheses-to-expression-statement-expression.js +26 -0
- package/rules/utils/should-add-parentheses-to-logical-expression-child.js +47 -0
- package/rules/utils/should-add-parentheses-to-member-expression-object.js +47 -0
- package/rules/utils/should-add-parentheses-to-new-expression-callee.js +32 -0
- package/rules/utils/should-add-parentheses-to-spread-element-argument.js +22 -0
- package/rules/utils/singular.js +18 -0
- package/rules/utils/to-location.js +21 -0
- package/README.md +0 -5
@@ -0,0 +1,529 @@
|
|
1
|
+
'use strict';
|
2
|
+
const {isParenthesized, getStaticValue, isCommaToken, hasSideEffect} = require('@eslint-community/eslint-utils');
|
3
|
+
const {
|
4
|
+
getParenthesizedRange,
|
5
|
+
getParenthesizedText,
|
6
|
+
needsSemicolon,
|
7
|
+
shouldAddParenthesesToSpreadElementArgument,
|
8
|
+
isNodeMatches,
|
9
|
+
isMethodNamed,
|
10
|
+
} = require('./utils/index.js');
|
11
|
+
const {removeMethodCall} = require('./fix/index.js');
|
12
|
+
const {isLiteral, isMethodCall} = require('./ast/index.js');
|
13
|
+
|
14
|
+
const ERROR_ARRAY_FROM = 'array-from';
|
15
|
+
const ERROR_ARRAY_CONCAT = 'array-concat';
|
16
|
+
const ERROR_ARRAY_SLICE = 'array-slice';
|
17
|
+
const ERROR_ARRAY_TO_SPLICED = 'array-to-spliced';
|
18
|
+
const ERROR_STRING_SPLIT = 'string-split';
|
19
|
+
const SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE = 'argument-is-spreadable';
|
20
|
+
const SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE = 'argument-is-not-spreadable';
|
21
|
+
const SUGGESTION_CONCAT_TEST_ARGUMENT = 'test-argument';
|
22
|
+
const SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS = 'spread-all-arguments';
|
23
|
+
const SUGGESTION_USE_SPREAD = 'use-spread';
|
24
|
+
const messages = {
|
25
|
+
[ERROR_ARRAY_FROM]: 'Prefer the spread operator over `Array.from(…)`.',
|
26
|
+
[ERROR_ARRAY_CONCAT]: 'Prefer the spread operator over `Array#concat(…)`.',
|
27
|
+
[ERROR_ARRAY_SLICE]: 'Prefer the spread operator over `Array#slice()`.',
|
28
|
+
[ERROR_ARRAY_TO_SPLICED]: 'Prefer the spread operator over `Array#toSpliced()`.',
|
29
|
+
[ERROR_STRING_SPLIT]: 'Prefer the spread operator over `String#split(\'\')`.',
|
30
|
+
[SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE]: 'First argument is an `array`.',
|
31
|
+
[SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE]: 'First argument is not an `array`.',
|
32
|
+
[SUGGESTION_CONCAT_TEST_ARGUMENT]: 'Test first argument with `Array.isArray(…)`.',
|
33
|
+
[SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS]: 'Spread all unknown arguments`.',
|
34
|
+
[SUGGESTION_USE_SPREAD]: 'Use `...` operator.',
|
35
|
+
};
|
36
|
+
|
37
|
+
const ignoredSliceCallee = [
|
38
|
+
'arrayBuffer',
|
39
|
+
'blob',
|
40
|
+
'buffer',
|
41
|
+
'file',
|
42
|
+
'this',
|
43
|
+
];
|
44
|
+
|
45
|
+
const isArrayLiteral = node => node.type === 'ArrayExpression';
|
46
|
+
const isArrayLiteralHasTrailingComma = (node, sourceCode) => {
|
47
|
+
if (node.elements.length === 0) {
|
48
|
+
return false;
|
49
|
+
}
|
50
|
+
|
51
|
+
return isCommaToken(sourceCode.getLastToken(node, 1));
|
52
|
+
};
|
53
|
+
|
54
|
+
function fixConcat(node, sourceCode, fixableArguments) {
|
55
|
+
const array = node.callee.object;
|
56
|
+
const concatCallArguments = node.arguments;
|
57
|
+
const arrayParenthesizedRange = getParenthesizedRange(array, sourceCode);
|
58
|
+
const arrayIsArrayLiteral = isArrayLiteral(array);
|
59
|
+
const arrayHasTrailingComma = arrayIsArrayLiteral && isArrayLiteralHasTrailingComma(array, sourceCode);
|
60
|
+
|
61
|
+
const getArrayLiteralElementsText = (node, keepTrailingComma) => {
|
62
|
+
if (
|
63
|
+
!keepTrailingComma
|
64
|
+
&& isArrayLiteralHasTrailingComma(node, sourceCode)
|
65
|
+
) {
|
66
|
+
const start = node.range[0] + 1;
|
67
|
+
const end = sourceCode.getLastToken(node, 1).range[0];
|
68
|
+
return sourceCode.text.slice(start, end);
|
69
|
+
}
|
70
|
+
|
71
|
+
return sourceCode.getText(node, -1, -1);
|
72
|
+
};
|
73
|
+
|
74
|
+
const getFixedText = () => {
|
75
|
+
const nonEmptyArguments = fixableArguments
|
76
|
+
.filter(({node, isArrayLiteral}) => (!isArrayLiteral || node.elements.length > 0));
|
77
|
+
const lastArgument = nonEmptyArguments.at(-1);
|
78
|
+
|
79
|
+
let text = nonEmptyArguments
|
80
|
+
.map(({node, isArrayLiteral, isSpreadable, testArgument}) => {
|
81
|
+
if (isArrayLiteral) {
|
82
|
+
return getArrayLiteralElementsText(node, node === lastArgument.node);
|
83
|
+
}
|
84
|
+
|
85
|
+
let text = getParenthesizedText(node, sourceCode);
|
86
|
+
|
87
|
+
if (testArgument) {
|
88
|
+
return `...(Array.isArray(${text}) ? ${text} : [${text}])`;
|
89
|
+
}
|
90
|
+
|
91
|
+
if (isSpreadable) {
|
92
|
+
if (
|
93
|
+
!isParenthesized(node, sourceCode)
|
94
|
+
&& shouldAddParenthesesToSpreadElementArgument(node)
|
95
|
+
) {
|
96
|
+
text = `(${text})`;
|
97
|
+
}
|
98
|
+
|
99
|
+
text = `...${text}`;
|
100
|
+
}
|
101
|
+
|
102
|
+
return text || ' ';
|
103
|
+
})
|
104
|
+
.join(', ');
|
105
|
+
|
106
|
+
if (!text) {
|
107
|
+
return '';
|
108
|
+
}
|
109
|
+
|
110
|
+
if (arrayIsArrayLiteral) {
|
111
|
+
if (array.elements.length > 0) {
|
112
|
+
text = ` ${text}`;
|
113
|
+
|
114
|
+
if (!arrayHasTrailingComma) {
|
115
|
+
text = `,${text}`;
|
116
|
+
}
|
117
|
+
|
118
|
+
if (
|
119
|
+
arrayHasTrailingComma
|
120
|
+
&& (!lastArgument.isArrayLiteral || !isArrayLiteralHasTrailingComma(lastArgument.node, sourceCode))
|
121
|
+
) {
|
122
|
+
text = `${text},`;
|
123
|
+
}
|
124
|
+
}
|
125
|
+
} else {
|
126
|
+
text = `, ${text}`;
|
127
|
+
}
|
128
|
+
|
129
|
+
return text;
|
130
|
+
};
|
131
|
+
|
132
|
+
function removeArguments(fixer) {
|
133
|
+
const [firstArgument] = concatCallArguments;
|
134
|
+
const lastArgument = concatCallArguments[fixableArguments.length - 1];
|
135
|
+
|
136
|
+
const [start] = getParenthesizedRange(firstArgument, sourceCode);
|
137
|
+
let [, end] = sourceCode.getTokenAfter(lastArgument, isCommaToken).range;
|
138
|
+
|
139
|
+
const textAfter = sourceCode.text.slice(end);
|
140
|
+
const [leadingSpaces] = textAfter.match(/^\s*/);
|
141
|
+
end += leadingSpaces.length;
|
142
|
+
|
143
|
+
return fixer.replaceTextRange([start, end], '');
|
144
|
+
}
|
145
|
+
|
146
|
+
return function * (fixer) {
|
147
|
+
// Fixed code always starts with `[`
|
148
|
+
if (
|
149
|
+
!arrayIsArrayLiteral
|
150
|
+
&& needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[')
|
151
|
+
) {
|
152
|
+
yield fixer.insertTextBefore(node, ';');
|
153
|
+
}
|
154
|
+
|
155
|
+
if (concatCallArguments.length - fixableArguments.length === 0) {
|
156
|
+
yield * removeMethodCall(fixer, node, sourceCode);
|
157
|
+
} else {
|
158
|
+
yield removeArguments(fixer);
|
159
|
+
}
|
160
|
+
|
161
|
+
const text = getFixedText();
|
162
|
+
|
163
|
+
if (arrayIsArrayLiteral) {
|
164
|
+
const closingBracketToken = sourceCode.getLastToken(array);
|
165
|
+
yield fixer.insertTextBefore(closingBracketToken, text);
|
166
|
+
} else {
|
167
|
+
// The array is already accessing `.concat`, there should not any case need add extra `()`
|
168
|
+
yield fixer.insertTextBeforeRange(arrayParenthesizedRange, '[...');
|
169
|
+
yield fixer.insertTextAfterRange(arrayParenthesizedRange, text);
|
170
|
+
yield fixer.insertTextAfterRange(arrayParenthesizedRange, ']');
|
171
|
+
}
|
172
|
+
};
|
173
|
+
}
|
174
|
+
|
175
|
+
const getConcatArgumentSpreadable = (node, scope) => {
|
176
|
+
if (node.type === 'SpreadElement') {
|
177
|
+
return;
|
178
|
+
}
|
179
|
+
|
180
|
+
if (isArrayLiteral(node)) {
|
181
|
+
return {node, isArrayLiteral: true};
|
182
|
+
}
|
183
|
+
|
184
|
+
const result = getStaticValue(node, scope);
|
185
|
+
|
186
|
+
if (!result) {
|
187
|
+
return;
|
188
|
+
}
|
189
|
+
|
190
|
+
const isSpreadable = Array.isArray(result.value);
|
191
|
+
|
192
|
+
return {node, isSpreadable};
|
193
|
+
};
|
194
|
+
|
195
|
+
function getConcatFixableArguments(argumentsList, scope) {
|
196
|
+
const fixableArguments = [];
|
197
|
+
|
198
|
+
for (const node of argumentsList) {
|
199
|
+
const result = getConcatArgumentSpreadable(node, scope);
|
200
|
+
|
201
|
+
if (result) {
|
202
|
+
fixableArguments.push(result);
|
203
|
+
} else {
|
204
|
+
break;
|
205
|
+
}
|
206
|
+
}
|
207
|
+
|
208
|
+
return fixableArguments;
|
209
|
+
}
|
210
|
+
|
211
|
+
function fixArrayFrom(node, sourceCode) {
|
212
|
+
const [object] = node.arguments;
|
213
|
+
|
214
|
+
function getObjectText() {
|
215
|
+
if (isArrayLiteral(object)) {
|
216
|
+
return sourceCode.getText(object);
|
217
|
+
}
|
218
|
+
|
219
|
+
const [start, end] = getParenthesizedRange(object, sourceCode);
|
220
|
+
let text = sourceCode.text.slice(start, end);
|
221
|
+
|
222
|
+
if (
|
223
|
+
!isParenthesized(object, sourceCode)
|
224
|
+
&& shouldAddParenthesesToSpreadElementArgument(object)
|
225
|
+
) {
|
226
|
+
text = `(${text})`;
|
227
|
+
}
|
228
|
+
|
229
|
+
return `[...${text}]`;
|
230
|
+
}
|
231
|
+
|
232
|
+
return function * (fixer) {
|
233
|
+
// Fixed code always starts with `[`
|
234
|
+
if (needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[')) {
|
235
|
+
yield fixer.insertTextBefore(node, ';');
|
236
|
+
}
|
237
|
+
|
238
|
+
const objectText = getObjectText();
|
239
|
+
|
240
|
+
yield fixer.replaceText(node, objectText);
|
241
|
+
};
|
242
|
+
}
|
243
|
+
|
244
|
+
function methodCallToSpread(node, sourceCode) {
|
245
|
+
return function * (fixer) {
|
246
|
+
// Fixed code always starts with `[`
|
247
|
+
if (needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[')) {
|
248
|
+
yield fixer.insertTextBefore(node, ';');
|
249
|
+
}
|
250
|
+
|
251
|
+
yield fixer.insertTextBefore(node, '[...');
|
252
|
+
yield fixer.insertTextAfter(node, ']');
|
253
|
+
|
254
|
+
// The array is already accessing `.slice` or `.split`, there should not any case need add extra `()`
|
255
|
+
|
256
|
+
yield * removeMethodCall(fixer, node, sourceCode);
|
257
|
+
};
|
258
|
+
}
|
259
|
+
|
260
|
+
function isClassName(node) {
|
261
|
+
if (node.type === 'MemberExpression') {
|
262
|
+
node = node.property;
|
263
|
+
}
|
264
|
+
|
265
|
+
if (node.type !== 'Identifier') {
|
266
|
+
return false;
|
267
|
+
}
|
268
|
+
|
269
|
+
const {name} = node;
|
270
|
+
|
271
|
+
return /^[A-Z]./.test(name) && name.toUpperCase() !== name;
|
272
|
+
}
|
273
|
+
|
274
|
+
function isNotArray(node, scope) {
|
275
|
+
if (
|
276
|
+
node.type === 'TemplateLiteral'
|
277
|
+
|| node.type === 'Literal'
|
278
|
+
|| node.type === 'BinaryExpression'
|
279
|
+
|| isClassName(node)
|
280
|
+
// `foo.join()`
|
281
|
+
|| (isMethodNamed(node, 'join') && node.arguments.length <= 1)
|
282
|
+
) {
|
283
|
+
return true;
|
284
|
+
}
|
285
|
+
|
286
|
+
const staticValue = getStaticValue(node, scope);
|
287
|
+
if (staticValue && !Array.isArray(staticValue.value)) {
|
288
|
+
return true;
|
289
|
+
}
|
290
|
+
|
291
|
+
return false;
|
292
|
+
}
|
293
|
+
|
294
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
295
|
+
const create = context => {
|
296
|
+
const {sourceCode} = context;
|
297
|
+
|
298
|
+
// `Array.from()`
|
299
|
+
context.on('CallExpression', node => {
|
300
|
+
if (
|
301
|
+
isMethodCall(node, {
|
302
|
+
object: 'Array',
|
303
|
+
method: 'from',
|
304
|
+
argumentsLength: 1,
|
305
|
+
optionalCall: false,
|
306
|
+
optionalMember: false,
|
307
|
+
})
|
308
|
+
// Allow `Array.from({length})`
|
309
|
+
&& node.arguments[0].type !== 'ObjectExpression'
|
310
|
+
) {
|
311
|
+
return {
|
312
|
+
node,
|
313
|
+
messageId: ERROR_ARRAY_FROM,
|
314
|
+
fix: fixArrayFrom(node, sourceCode),
|
315
|
+
};
|
316
|
+
}
|
317
|
+
});
|
318
|
+
|
319
|
+
// `array.concat()`
|
320
|
+
context.on('CallExpression', node => {
|
321
|
+
if (!isMethodCall(node, {
|
322
|
+
method: 'concat',
|
323
|
+
optionalCall: false,
|
324
|
+
optionalMember: false,
|
325
|
+
})) {
|
326
|
+
return;
|
327
|
+
}
|
328
|
+
|
329
|
+
const {object} = node.callee;
|
330
|
+
const scope = sourceCode.getScope(object);
|
331
|
+
|
332
|
+
if (isNotArray(object, scope)) {
|
333
|
+
return;
|
334
|
+
}
|
335
|
+
|
336
|
+
const staticResult = getStaticValue(object, scope);
|
337
|
+
if (staticResult && !Array.isArray(staticResult.value)) {
|
338
|
+
return;
|
339
|
+
}
|
340
|
+
|
341
|
+
const problem = {
|
342
|
+
node: node.callee.property,
|
343
|
+
messageId: ERROR_ARRAY_CONCAT,
|
344
|
+
};
|
345
|
+
|
346
|
+
const fixableArguments = getConcatFixableArguments(node.arguments, scope);
|
347
|
+
|
348
|
+
if (fixableArguments.length > 0 || node.arguments.length === 0) {
|
349
|
+
problem.fix = fixConcat(node, sourceCode, fixableArguments);
|
350
|
+
return problem;
|
351
|
+
}
|
352
|
+
|
353
|
+
const [firstArgument, ...restArguments] = node.arguments;
|
354
|
+
if (firstArgument.type === 'SpreadElement') {
|
355
|
+
return problem;
|
356
|
+
}
|
357
|
+
|
358
|
+
const fixableArgumentsAfterFirstArgument = getConcatFixableArguments(restArguments, scope);
|
359
|
+
const suggestions = [
|
360
|
+
{
|
361
|
+
messageId: SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE,
|
362
|
+
isSpreadable: true,
|
363
|
+
},
|
364
|
+
{
|
365
|
+
messageId: SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE,
|
366
|
+
isSpreadable: false,
|
367
|
+
},
|
368
|
+
];
|
369
|
+
|
370
|
+
if (!hasSideEffect(firstArgument, sourceCode)) {
|
371
|
+
suggestions.push({
|
372
|
+
messageId: SUGGESTION_CONCAT_TEST_ARGUMENT,
|
373
|
+
testArgument: true,
|
374
|
+
});
|
375
|
+
}
|
376
|
+
|
377
|
+
problem.suggest = suggestions.map(({messageId, isSpreadable, testArgument}) => ({
|
378
|
+
messageId,
|
379
|
+
fix: fixConcat(
|
380
|
+
node,
|
381
|
+
sourceCode,
|
382
|
+
// When apply suggestion, we also merge fixable arguments after the first one
|
383
|
+
[
|
384
|
+
{
|
385
|
+
node: firstArgument,
|
386
|
+
isSpreadable,
|
387
|
+
testArgument,
|
388
|
+
},
|
389
|
+
...fixableArgumentsAfterFirstArgument,
|
390
|
+
],
|
391
|
+
),
|
392
|
+
}));
|
393
|
+
|
394
|
+
if (
|
395
|
+
fixableArgumentsAfterFirstArgument.length < restArguments.length
|
396
|
+
&& restArguments.every(({type}) => type !== 'SpreadElement')
|
397
|
+
) {
|
398
|
+
problem.suggest.push({
|
399
|
+
messageId: SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS,
|
400
|
+
fix: fixConcat(
|
401
|
+
node,
|
402
|
+
sourceCode,
|
403
|
+
node.arguments.map(node => getConcatArgumentSpreadable(node, scope) || {node, isSpreadable: true}),
|
404
|
+
),
|
405
|
+
});
|
406
|
+
}
|
407
|
+
|
408
|
+
return problem;
|
409
|
+
});
|
410
|
+
|
411
|
+
// `array.slice()`
|
412
|
+
context.on('CallExpression', node => {
|
413
|
+
if (!(
|
414
|
+
isMethodCall(node, {
|
415
|
+
method: 'slice',
|
416
|
+
minimumArguments: 0,
|
417
|
+
maximumArguments: 1,
|
418
|
+
optionalCall: false,
|
419
|
+
optionalMember: false,
|
420
|
+
})
|
421
|
+
&& node.callee.object.type !== 'ArrayExpression'
|
422
|
+
)) {
|
423
|
+
return;
|
424
|
+
}
|
425
|
+
|
426
|
+
if (isNodeMatches(node.callee.object, ignoredSliceCallee)) {
|
427
|
+
return;
|
428
|
+
}
|
429
|
+
|
430
|
+
const [firstArgument] = node.arguments;
|
431
|
+
if (firstArgument && !isLiteral(firstArgument, 0)) {
|
432
|
+
return;
|
433
|
+
}
|
434
|
+
|
435
|
+
return {
|
436
|
+
node: node.callee.property,
|
437
|
+
messageId: ERROR_ARRAY_SLICE,
|
438
|
+
fix: methodCallToSpread(node, sourceCode),
|
439
|
+
};
|
440
|
+
});
|
441
|
+
|
442
|
+
// `array.toSpliced()`
|
443
|
+
context.on('CallExpression', node => {
|
444
|
+
if (!(
|
445
|
+
isMethodCall(node, {
|
446
|
+
method: 'toSpliced',
|
447
|
+
argumentsLength: 0,
|
448
|
+
optionalCall: false,
|
449
|
+
optionalMember: false,
|
450
|
+
})
|
451
|
+
&& node.callee.object.type !== 'ArrayExpression'
|
452
|
+
)) {
|
453
|
+
return;
|
454
|
+
}
|
455
|
+
|
456
|
+
return {
|
457
|
+
node: node.callee.property,
|
458
|
+
messageId: ERROR_ARRAY_TO_SPLICED,
|
459
|
+
fix: methodCallToSpread(node, sourceCode),
|
460
|
+
};
|
461
|
+
});
|
462
|
+
|
463
|
+
// `string.split()`
|
464
|
+
context.on('CallExpression', node => {
|
465
|
+
if (!isMethodCall(node, {
|
466
|
+
method: 'split',
|
467
|
+
argumentsLength: 1,
|
468
|
+
optionalCall: false,
|
469
|
+
optionalMember: false,
|
470
|
+
})) {
|
471
|
+
return;
|
472
|
+
}
|
473
|
+
|
474
|
+
const [separator] = node.arguments;
|
475
|
+
if (!isLiteral(separator, '')) {
|
476
|
+
return;
|
477
|
+
}
|
478
|
+
|
479
|
+
const string = node.callee.object;
|
480
|
+
const staticValue = getStaticValue(string, sourceCode.getScope(string));
|
481
|
+
let hasSameResult = false;
|
482
|
+
if (staticValue) {
|
483
|
+
const {value} = staticValue;
|
484
|
+
|
485
|
+
if (typeof value !== 'string') {
|
486
|
+
return;
|
487
|
+
}
|
488
|
+
|
489
|
+
// eslint-disable-next-line unicorn/prefer-spread
|
490
|
+
const resultBySplit = value.split('');
|
491
|
+
const resultBySpread = [...value];
|
492
|
+
|
493
|
+
hasSameResult = resultBySplit.length === resultBySpread.length
|
494
|
+
&& resultBySplit.every((character, index) => character === resultBySpread[index]);
|
495
|
+
}
|
496
|
+
|
497
|
+
const problem = {
|
498
|
+
node: node.callee.property,
|
499
|
+
messageId: ERROR_STRING_SPLIT,
|
500
|
+
};
|
501
|
+
|
502
|
+
if (hasSameResult) {
|
503
|
+
problem.fix = methodCallToSpread(node, sourceCode);
|
504
|
+
} else {
|
505
|
+
problem.suggest = [
|
506
|
+
{
|
507
|
+
messageId: SUGGESTION_USE_SPREAD,
|
508
|
+
fix: methodCallToSpread(node, sourceCode),
|
509
|
+
},
|
510
|
+
];
|
511
|
+
}
|
512
|
+
|
513
|
+
return problem;
|
514
|
+
});
|
515
|
+
};
|
516
|
+
|
517
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
518
|
+
module.exports = {
|
519
|
+
create,
|
520
|
+
meta: {
|
521
|
+
type: 'suggestion',
|
522
|
+
docs: {
|
523
|
+
description: 'Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#{slice,toSpliced}()` and `String#split(\'\')`.',
|
524
|
+
},
|
525
|
+
fixable: 'code',
|
526
|
+
hasSuggestions: true,
|
527
|
+
messages,
|
528
|
+
},
|
529
|
+
};
|
@@ -0,0 +1,145 @@
|
|
1
|
+
'use strict';
|
2
|
+
const {getStaticValue} = require('@eslint-community/eslint-utils');
|
3
|
+
const {parse: parseRegExp} = require('regjsparser');
|
4
|
+
const escapeString = require('./utils/escape-string.js');
|
5
|
+
const {isRegexLiteral, isNewExpression, isMethodCall} = require('./ast/index.js');
|
6
|
+
|
7
|
+
const MESSAGE_ID_USE_REPLACE_ALL = 'method';
|
8
|
+
const MESSAGE_ID_USE_STRING = 'pattern';
|
9
|
+
const messages = {
|
10
|
+
[MESSAGE_ID_USE_REPLACE_ALL]: 'Prefer `String#replaceAll()` over `String#replace()`.',
|
11
|
+
[MESSAGE_ID_USE_STRING]: 'This pattern can be replaced with {{replacement}}.',
|
12
|
+
};
|
13
|
+
|
14
|
+
function getPatternReplacement(node) {
|
15
|
+
if (!isRegexLiteral(node)) {
|
16
|
+
return;
|
17
|
+
}
|
18
|
+
|
19
|
+
const {pattern, flags} = node.regex;
|
20
|
+
if (flags.replace('u', '').replace('v', '') !== 'g') {
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
|
24
|
+
let tree;
|
25
|
+
|
26
|
+
try {
|
27
|
+
tree = parseRegExp(pattern, flags, {
|
28
|
+
unicodePropertyEscape: flags.includes('u'),
|
29
|
+
unicodeSet: flags.includes('v'),
|
30
|
+
namedGroups: true,
|
31
|
+
lookbehind: true,
|
32
|
+
});
|
33
|
+
} catch {
|
34
|
+
return;
|
35
|
+
}
|
36
|
+
|
37
|
+
const parts = tree.type === 'alternative' ? tree.body : [tree];
|
38
|
+
if (parts.some(part => part.type !== 'value')) {
|
39
|
+
return;
|
40
|
+
}
|
41
|
+
|
42
|
+
// TODO: Preserve escape
|
43
|
+
const string = String.fromCodePoint(...parts.map(part => part.codePoint));
|
44
|
+
|
45
|
+
return escapeString(string);
|
46
|
+
}
|
47
|
+
|
48
|
+
const isRegExpWithGlobalFlag = (node, scope) => {
|
49
|
+
if (isRegexLiteral(node)) {
|
50
|
+
return node.regex.flags.includes('g');
|
51
|
+
}
|
52
|
+
|
53
|
+
if (
|
54
|
+
isNewExpression(node, {name: 'RegExp'})
|
55
|
+
&& node.arguments[0]?.type !== 'SpreadElement'
|
56
|
+
&& node.arguments[1]?.type === 'Literal'
|
57
|
+
&& typeof node.arguments[1].value === 'string'
|
58
|
+
) {
|
59
|
+
return node.arguments[1].value.includes('g');
|
60
|
+
}
|
61
|
+
|
62
|
+
const staticResult = getStaticValue(node, scope);
|
63
|
+
|
64
|
+
// Don't know if there is `g` flag
|
65
|
+
if (!staticResult) {
|
66
|
+
return false;
|
67
|
+
}
|
68
|
+
|
69
|
+
const {value} = staticResult;
|
70
|
+
return (
|
71
|
+
Object.prototype.toString.call(value) === '[object RegExp]'
|
72
|
+
&& value.global
|
73
|
+
);
|
74
|
+
};
|
75
|
+
|
76
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
77
|
+
const create = context => ({
|
78
|
+
CallExpression(node) {
|
79
|
+
if (!isMethodCall(node, {
|
80
|
+
methods: ['replace', 'replaceAll'],
|
81
|
+
argumentsLength: 2,
|
82
|
+
optionalCall: false,
|
83
|
+
optionalMember: false,
|
84
|
+
})) {
|
85
|
+
return;
|
86
|
+
}
|
87
|
+
|
88
|
+
const {
|
89
|
+
arguments: [pattern],
|
90
|
+
callee: {property},
|
91
|
+
} = node;
|
92
|
+
|
93
|
+
if (!isRegExpWithGlobalFlag(pattern, context.sourceCode.getScope(pattern))) {
|
94
|
+
return;
|
95
|
+
}
|
96
|
+
|
97
|
+
const methodName = property.name;
|
98
|
+
const patternReplacement = getPatternReplacement(pattern);
|
99
|
+
|
100
|
+
if (methodName === 'replaceAll') {
|
101
|
+
if (!patternReplacement) {
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
|
105
|
+
return {
|
106
|
+
node: pattern,
|
107
|
+
messageId: MESSAGE_ID_USE_STRING,
|
108
|
+
data: {
|
109
|
+
// Show `This pattern can be replaced with a string literal.` for long strings
|
110
|
+
replacement: patternReplacement.length < 20 ? patternReplacement : 'a string literal',
|
111
|
+
},
|
112
|
+
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
113
|
+
fix: fixer => fixer.replaceText(pattern, patternReplacement),
|
114
|
+
};
|
115
|
+
}
|
116
|
+
|
117
|
+
return {
|
118
|
+
node: property,
|
119
|
+
messageId: MESSAGE_ID_USE_REPLACE_ALL,
|
120
|
+
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
121
|
+
* fix(fixer) {
|
122
|
+
yield fixer.insertTextAfter(property, 'All');
|
123
|
+
|
124
|
+
if (!patternReplacement) {
|
125
|
+
return;
|
126
|
+
}
|
127
|
+
|
128
|
+
yield fixer.replaceText(pattern, patternReplacement);
|
129
|
+
},
|
130
|
+
};
|
131
|
+
},
|
132
|
+
});
|
133
|
+
|
134
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
135
|
+
module.exports = {
|
136
|
+
create,
|
137
|
+
meta: {
|
138
|
+
type: 'suggestion',
|
139
|
+
docs: {
|
140
|
+
description: 'Prefer `String#replaceAll()` over regex searches with the global flag.',
|
141
|
+
},
|
142
|
+
fixable: 'code',
|
143
|
+
messages,
|
144
|
+
},
|
145
|
+
};
|