eslint-plugin-unicorn 64.0.0 → 65.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 +14 -0
- package/package.json +7 -3
- package/readme.md +183 -151
- package/rules/better-dom-traversing.js +291 -0
- package/rules/consistent-compound-words.js +452 -0
- package/rules/consistent-destructuring.js +97 -9
- package/rules/consistent-function-scoping.js +20 -0
- package/rules/consistent-json-file-read.js +245 -0
- package/rules/custom-error-definition.js +89 -4
- package/rules/dom-node-dataset.js +431 -0
- package/rules/escape-case.js +1 -1
- package/rules/expiring-todo-comments.js +42 -81
- package/rules/explicit-length-check.js +139 -6
- package/rules/filename-case.js +95 -21
- package/rules/import-style.js +21 -1
- package/rules/index.js +33 -3
- package/rules/isolated-functions.js +7 -5
- package/rules/no-anonymous-default-export.js +6 -6
- package/rules/no-array-callback-reference.js +89 -23
- package/rules/no-array-fill-with-reference-type.js +127 -0
- package/rules/no-array-for-each.js +17 -1
- package/rules/no-array-from-fill.js +76 -0
- package/rules/no-array-reduce.js +508 -2
- package/rules/no-blob-to-file.js +299 -0
- package/rules/no-canvas-to-image.js +152 -0
- package/rules/no-confusing-array-splice.js +164 -0
- package/rules/no-duplicate-set-values.js +57 -0
- package/rules/no-empty-file.js +31 -2
- package/rules/no-exports-in-scripts.js +34 -0
- package/rules/no-hex-escape.js +1 -1
- package/rules/no-incorrect-query-selector.js +347 -0
- package/rules/no-invalid-file-input-accept.js +475 -0
- package/rules/no-keyword-prefix.js +20 -5
- package/rules/no-late-current-target-access.js +166 -0
- package/rules/no-lonely-if.js +151 -50
- package/rules/no-manually-wrapped-comments.js +151 -0
- package/rules/no-null.js +14 -3
- package/rules/no-this-outside-of-class.js +75 -0
- package/rules/no-typeof-undefined.js +6 -4
- package/rules/no-unnecessary-nested-ternary.js +132 -0
- package/rules/no-unnecessary-polyfills.js +66 -20
- package/rules/no-unreadable-iife.js +33 -3
- package/rules/no-unused-array-method-return.js +301 -0
- package/rules/no-useless-promise-resolve-reject.js +5 -5
- package/rules/no-useless-spread.js +53 -4
- package/rules/no-useless-undefined.js +52 -45
- package/rules/numeric-separators-style.js +13 -4
- package/rules/prefer-array-last-methods.js +93 -0
- package/rules/prefer-array-some.js +66 -5
- package/rules/prefer-at.js +114 -55
- package/rules/prefer-dom-node-append.js +2 -2
- package/rules/prefer-dom-node-remove.js +24 -18
- package/rules/prefer-export-from.js +6 -6
- package/rules/prefer-get-or-insert-computed.js +159 -0
- package/rules/prefer-global-this.js +7 -46
- package/rules/prefer-https.js +83 -0
- package/rules/prefer-includes-over-repeated-comparisons.js +199 -0
- package/rules/prefer-iterator-concat.js +174 -0
- package/rules/prefer-iterator-to-array-at-end.js +123 -0
- package/rules/prefer-keyboard-event-key.js +202 -76
- package/rules/prefer-math-abs.js +397 -0
- package/rules/prefer-math-min-max.js +5 -3
- package/rules/prefer-node-protocol.js +6 -4
- package/rules/prefer-query-selector.js +119 -7
- package/rules/prefer-queue-microtask.js +204 -0
- package/rules/prefer-regexp-test.js +164 -18
- package/rules/prefer-set-has.js +411 -22
- package/rules/prefer-single-call.js +128 -38
- package/rules/prefer-split-limit.js +107 -0
- package/rules/prefer-spread.js +34 -4
- package/rules/prefer-string-match-all.js +352 -0
- package/rules/prefer-string-pad-start-end.js +380 -0
- package/rules/prefer-string-raw.js +90 -19
- package/rules/prefer-string-repeat.js +173 -0
- package/rules/prefer-string-replace-all.js +180 -12
- package/rules/prefer-string-slice.js +54 -3
- package/rules/prefer-top-level-await.js +129 -1
- package/rules/prevent-abbreviations.js +126 -134
- package/rules/require-css-escape.js +141 -0
- package/rules/require-module-specifiers.js +62 -0
- package/rules/require-passive-events.js +292 -0
- package/rules/shared/dom-events.js +7 -7
- package/rules/shared/identifier-checks.js +205 -0
- package/rules/shared/package-json.js +1 -1
- package/rules/string-content.js +34 -5
- package/rules/template-indent.js +52 -3
- package/rules/throw-new-error.js +4 -0
- package/rules/try-complexity.js +184 -0
- package/rules/utils/boolean.js +12 -12
- package/rules/utils/get-duplicate-array-elements.js +77 -0
- package/rules/utils/index.js +1 -0
- package/rules/utils/is-same-reference.js +6 -4
- package/rules/utils/should-add-parentheses-to-await-expression-argument.js +9 -9
- package/rules/utils/should-add-parentheses-to-call-expression-callee.js +12 -10
- package/rules/utils/should-add-parentheses-to-conditional-expression-child.js +9 -7
- package/rules/utils/should-add-parentheses-to-logical-expression-child.js +12 -8
- package/rules/utils/should-add-parentheses-to-unary-expression.js +10 -10
- package/rules/better-regex.js +0 -155
- package/rules/prefer-dom-node-dataset.js +0 -135
- package/rules/prefer-json-parse-buffer.js +0 -161
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import {isMethodCall, isMemberExpression, isNumericLiteral} from './ast/index.js';
|
|
2
|
+
import {escapeString, getParenthesizedText, isNodeValueNotDomNode} from './utils/index.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID_FIRST_CHILD = 'first-child';
|
|
5
|
+
const MESSAGE_ID_FIRST_ELEMENT_CHILD = 'first-element-child';
|
|
6
|
+
const MESSAGE_ID_QUERY_SELECTOR = 'query-selector';
|
|
7
|
+
const MESSAGE_ID_CLOSEST = 'closest';
|
|
8
|
+
const MESSAGE_ID_MERGE_QUERY_SELECTOR = 'merge-query-selector';
|
|
9
|
+
const SUGGESTION_ID_FIRST_CHILD = 'suggestion-first-child';
|
|
10
|
+
const SUGGESTION_ID_FIRST_ELEMENT_CHILD = 'suggestion-first-element-child';
|
|
11
|
+
const SUGGESTION_ID_MERGE_QUERY_SELECTOR = 'suggestion-merge-query-selector';
|
|
12
|
+
const QUERY_SELECTOR_CALL = {
|
|
13
|
+
method: 'querySelector',
|
|
14
|
+
argumentsLength: 1,
|
|
15
|
+
optionalCall: false,
|
|
16
|
+
optionalMember: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const messages = {
|
|
20
|
+
[MESSAGE_ID_FIRST_CHILD]: 'Prefer `.firstChild` over `.childNodes[0]`.',
|
|
21
|
+
[MESSAGE_ID_FIRST_ELEMENT_CHILD]: 'Prefer `.firstElementChild` over `.children[0]`.',
|
|
22
|
+
[MESSAGE_ID_QUERY_SELECTOR]: 'Consider `.querySelector()` over positional child traversal.',
|
|
23
|
+
[MESSAGE_ID_CLOSEST]: 'Consider `.closest()` over chaining `.parentElement`.',
|
|
24
|
+
[MESSAGE_ID_MERGE_QUERY_SELECTOR]: 'Consider merging chained `.querySelector()` calls.',
|
|
25
|
+
[SUGGESTION_ID_FIRST_CHILD]: 'Switch to `.firstChild`.',
|
|
26
|
+
[SUGGESTION_ID_FIRST_ELEMENT_CHILD]: 'Switch to `.firstElementChild`.',
|
|
27
|
+
[SUGGESTION_ID_MERGE_QUERY_SELECTOR]: 'Merge the `.querySelector()` calls.',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const isNonOptionalMemberExpression = node =>
|
|
31
|
+
node.type === 'MemberExpression'
|
|
32
|
+
&& !node.optional;
|
|
33
|
+
|
|
34
|
+
const isDomCollectionMemberExpression = (node, property) =>
|
|
35
|
+
isMemberExpression(node, {
|
|
36
|
+
property,
|
|
37
|
+
optional: false,
|
|
38
|
+
computed: false,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const isNumericIndexMemberExpression = node =>
|
|
42
|
+
isNonOptionalMemberExpression(node)
|
|
43
|
+
&& node.computed
|
|
44
|
+
&& isNumericLiteral(node.property)
|
|
45
|
+
&& Number.isInteger(node.property.value)
|
|
46
|
+
&& node.property.value >= 0;
|
|
47
|
+
|
|
48
|
+
const getIndexedDomCollectionName = node => {
|
|
49
|
+
if (!isNumericIndexMemberExpression(node)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isDomCollectionMemberExpression(node.object, 'childNodes')) {
|
|
54
|
+
return 'childNodes';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isDomCollectionMemberExpression(node.object, 'children')) {
|
|
58
|
+
return 'children';
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isNestedIndexedDomCollection = node =>
|
|
63
|
+
node.parent?.type === 'MemberExpression'
|
|
64
|
+
&& node.parent.object === node
|
|
65
|
+
&& isDomCollectionMemberExpression(node.parent, 'children')
|
|
66
|
+
&& isNumericIndexMemberExpression(node.parent.parent);
|
|
67
|
+
|
|
68
|
+
const hasCommentsInside = (node, sourceCode) =>
|
|
69
|
+
sourceCode.getCommentsInside(node).length > 0;
|
|
70
|
+
|
|
71
|
+
const getFirstChildSuggestion = (node, replacement, messageId, sourceCode) => {
|
|
72
|
+
if (hasCommentsInside(node, sourceCode)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const [, end] = sourceCode.getRange(node);
|
|
77
|
+
const [start] = sourceCode.getRange(node.object.property);
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
messageId,
|
|
82
|
+
fix: fixer => fixer.replaceTextRange([start, end], replacement),
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const isParentElementMemberExpression = node =>
|
|
88
|
+
isMemberExpression(node, {
|
|
89
|
+
property: 'parentElement',
|
|
90
|
+
optional: false,
|
|
91
|
+
computed: false,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const getParentElementChainRoot = node => {
|
|
95
|
+
while (isParentElementMemberExpression(node)) {
|
|
96
|
+
node = node.object;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return node;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const isOutermostParentElementChain = node =>
|
|
103
|
+
!isParentElementMemberExpression(node.parent)
|
|
104
|
+
|| node.parent.object !== node;
|
|
105
|
+
|
|
106
|
+
const isStaticSelector = node =>
|
|
107
|
+
(node.type === 'Literal' && typeof node.value === 'string')
|
|
108
|
+
|| (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis[0].value.cooked !== undefined);
|
|
109
|
+
|
|
110
|
+
const getStaticSelectorValue = node => {
|
|
111
|
+
if (node.type === 'Literal') {
|
|
112
|
+
return node.value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return node.quasis[0].value.cooked;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getSelectorQuote = node =>
|
|
119
|
+
node.type === 'Literal' && node.raw.startsWith('"') ? '"' : '\'';
|
|
120
|
+
|
|
121
|
+
const canMergeSelectorValues = selectors =>
|
|
122
|
+
selectors.every(selector => !selector.includes(','));
|
|
123
|
+
|
|
124
|
+
const isQuerySelectorCall = node =>
|
|
125
|
+
isMethodCall(node, QUERY_SELECTOR_CALL);
|
|
126
|
+
|
|
127
|
+
const isFollowedByStaticQuerySelectorCall = node =>
|
|
128
|
+
node.parent?.type === 'MemberExpression'
|
|
129
|
+
&& node.parent.object === node
|
|
130
|
+
&& node.parent.parent?.callee === node.parent
|
|
131
|
+
&& isQuerySelectorCall(node.parent.parent)
|
|
132
|
+
&& isStaticSelector(node.parent.parent.arguments[0]);
|
|
133
|
+
|
|
134
|
+
const isPartOfChainExpression = node => {
|
|
135
|
+
while (
|
|
136
|
+
(node.parent?.type === 'MemberExpression' && node.parent.object === node)
|
|
137
|
+
|| (node.parent?.type === 'CallExpression' && node.parent.callee === node)
|
|
138
|
+
) {
|
|
139
|
+
node = node.parent;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return node.parent?.type === 'ChainExpression';
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const getQuerySelectorChain = node => {
|
|
146
|
+
const calls = [];
|
|
147
|
+
|
|
148
|
+
while (isQuerySelectorCall(node)) {
|
|
149
|
+
const [selector] = node.arguments;
|
|
150
|
+
if (!isStaticSelector(selector)) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
calls.push(node);
|
|
155
|
+
node = node.callee.object;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
calls.length < 2
|
|
160
|
+
|| node.type === 'ChainExpression'
|
|
161
|
+
|| isNodeValueNotDomNode(node)
|
|
162
|
+
) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const selectors = [];
|
|
167
|
+
for (const call of calls) {
|
|
168
|
+
selectors.unshift(getStaticSelectorValue(call.arguments[0]));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
root: node,
|
|
173
|
+
selectors,
|
|
174
|
+
quoteNode: calls.at(-1).arguments[0],
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const getMergeQuerySelectorSuggestion = (node, querySelectorChain, context) => {
|
|
179
|
+
const {sourceCode} = context;
|
|
180
|
+
if (hasCommentsInside(node, sourceCode)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!canMergeSelectorValues(querySelectorChain.selectors)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const root = getParenthesizedText(querySelectorChain.root, context);
|
|
189
|
+
const selector = querySelectorChain.selectors.join(' ');
|
|
190
|
+
const replacement = `${root}.querySelector(${escapeString(selector, getSelectorQuote(querySelectorChain.quoteNode))})`;
|
|
191
|
+
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
messageId: SUGGESTION_ID_MERGE_QUERY_SELECTOR,
|
|
195
|
+
fix: fixer => fixer.replaceText(node, replacement),
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
201
|
+
const create = context => {
|
|
202
|
+
const {sourceCode} = context;
|
|
203
|
+
|
|
204
|
+
context.on('MemberExpression', node => {
|
|
205
|
+
const collectionName = getIndexedDomCollectionName(node);
|
|
206
|
+
if (
|
|
207
|
+
!collectionName
|
|
208
|
+
|| isNodeValueNotDomNode(node.object.object)
|
|
209
|
+
|| isNestedIndexedDomCollection(node)
|
|
210
|
+
) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const index = node.property.value;
|
|
215
|
+
if (collectionName === 'childNodes' && index === 0) {
|
|
216
|
+
return {
|
|
217
|
+
node,
|
|
218
|
+
messageId: MESSAGE_ID_FIRST_CHILD,
|
|
219
|
+
suggest: getFirstChildSuggestion(node, 'firstChild', SUGGESTION_ID_FIRST_CHILD, sourceCode),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (collectionName === 'children' && index === 0) {
|
|
224
|
+
return {
|
|
225
|
+
node,
|
|
226
|
+
messageId: MESSAGE_ID_FIRST_ELEMENT_CHILD,
|
|
227
|
+
suggest: getFirstChildSuggestion(node, 'firstElementChild', SUGGESTION_ID_FIRST_ELEMENT_CHILD, sourceCode),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (collectionName === 'children') {
|
|
232
|
+
return {
|
|
233
|
+
node,
|
|
234
|
+
messageId: MESSAGE_ID_QUERY_SELECTOR,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
context.on('MemberExpression', node => {
|
|
240
|
+
if (
|
|
241
|
+
!isParentElementMemberExpression(node)
|
|
242
|
+
|| !isParentElementMemberExpression(node.object)
|
|
243
|
+
|| !isOutermostParentElementChain(node)
|
|
244
|
+
|| isNodeValueNotDomNode(getParentElementChainRoot(node))
|
|
245
|
+
) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
node,
|
|
251
|
+
messageId: MESSAGE_ID_CLOSEST,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
context.on('CallExpression', node => {
|
|
256
|
+
if (
|
|
257
|
+
!isQuerySelectorCall(node)
|
|
258
|
+
|| isFollowedByStaticQuerySelectorCall(node)
|
|
259
|
+
|| isPartOfChainExpression(node)
|
|
260
|
+
) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const querySelectorChain = getQuerySelectorChain(node);
|
|
265
|
+
if (!querySelectorChain) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
node,
|
|
271
|
+
messageId: MESSAGE_ID_MERGE_QUERY_SELECTOR,
|
|
272
|
+
suggest: getMergeQuerySelectorSuggestion(node, querySelectorChain, context),
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
278
|
+
const config = {
|
|
279
|
+
create,
|
|
280
|
+
meta: {
|
|
281
|
+
type: 'suggestion',
|
|
282
|
+
docs: {
|
|
283
|
+
description: 'Prefer better DOM traversal APIs.',
|
|
284
|
+
recommended: 'unopinionated',
|
|
285
|
+
},
|
|
286
|
+
hasSuggestions: true,
|
|
287
|
+
messages,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export default config;
|