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.
Files changed (100) hide show
  1. package/index.js +14 -0
  2. package/package.json +7 -3
  3. package/readme.md +183 -151
  4. package/rules/better-dom-traversing.js +291 -0
  5. package/rules/consistent-compound-words.js +452 -0
  6. package/rules/consistent-destructuring.js +97 -9
  7. package/rules/consistent-function-scoping.js +20 -0
  8. package/rules/consistent-json-file-read.js +245 -0
  9. package/rules/custom-error-definition.js +89 -4
  10. package/rules/dom-node-dataset.js +431 -0
  11. package/rules/escape-case.js +1 -1
  12. package/rules/expiring-todo-comments.js +42 -81
  13. package/rules/explicit-length-check.js +139 -6
  14. package/rules/filename-case.js +95 -21
  15. package/rules/import-style.js +21 -1
  16. package/rules/index.js +33 -3
  17. package/rules/isolated-functions.js +7 -5
  18. package/rules/no-anonymous-default-export.js +6 -6
  19. package/rules/no-array-callback-reference.js +89 -23
  20. package/rules/no-array-fill-with-reference-type.js +127 -0
  21. package/rules/no-array-for-each.js +17 -1
  22. package/rules/no-array-from-fill.js +76 -0
  23. package/rules/no-array-reduce.js +508 -2
  24. package/rules/no-blob-to-file.js +299 -0
  25. package/rules/no-canvas-to-image.js +152 -0
  26. package/rules/no-confusing-array-splice.js +164 -0
  27. package/rules/no-duplicate-set-values.js +57 -0
  28. package/rules/no-empty-file.js +31 -2
  29. package/rules/no-exports-in-scripts.js +34 -0
  30. package/rules/no-hex-escape.js +1 -1
  31. package/rules/no-incorrect-query-selector.js +347 -0
  32. package/rules/no-invalid-file-input-accept.js +475 -0
  33. package/rules/no-keyword-prefix.js +20 -5
  34. package/rules/no-late-current-target-access.js +166 -0
  35. package/rules/no-lonely-if.js +151 -50
  36. package/rules/no-manually-wrapped-comments.js +151 -0
  37. package/rules/no-null.js +14 -3
  38. package/rules/no-this-outside-of-class.js +75 -0
  39. package/rules/no-typeof-undefined.js +6 -4
  40. package/rules/no-unnecessary-nested-ternary.js +132 -0
  41. package/rules/no-unnecessary-polyfills.js +66 -20
  42. package/rules/no-unreadable-iife.js +33 -3
  43. package/rules/no-unused-array-method-return.js +301 -0
  44. package/rules/no-useless-promise-resolve-reject.js +5 -5
  45. package/rules/no-useless-spread.js +53 -4
  46. package/rules/no-useless-undefined.js +52 -45
  47. package/rules/numeric-separators-style.js +13 -4
  48. package/rules/prefer-array-last-methods.js +93 -0
  49. package/rules/prefer-array-some.js +66 -5
  50. package/rules/prefer-at.js +114 -55
  51. package/rules/prefer-dom-node-append.js +2 -2
  52. package/rules/prefer-dom-node-remove.js +24 -18
  53. package/rules/prefer-export-from.js +6 -6
  54. package/rules/prefer-get-or-insert-computed.js +159 -0
  55. package/rules/prefer-global-this.js +7 -46
  56. package/rules/prefer-https.js +83 -0
  57. package/rules/prefer-includes-over-repeated-comparisons.js +199 -0
  58. package/rules/prefer-iterator-concat.js +174 -0
  59. package/rules/prefer-iterator-to-array-at-end.js +123 -0
  60. package/rules/prefer-keyboard-event-key.js +202 -76
  61. package/rules/prefer-math-abs.js +397 -0
  62. package/rules/prefer-math-min-max.js +5 -3
  63. package/rules/prefer-node-protocol.js +6 -4
  64. package/rules/prefer-query-selector.js +119 -7
  65. package/rules/prefer-queue-microtask.js +204 -0
  66. package/rules/prefer-regexp-test.js +164 -18
  67. package/rules/prefer-set-has.js +411 -22
  68. package/rules/prefer-single-call.js +128 -38
  69. package/rules/prefer-split-limit.js +107 -0
  70. package/rules/prefer-spread.js +34 -4
  71. package/rules/prefer-string-match-all.js +352 -0
  72. package/rules/prefer-string-pad-start-end.js +380 -0
  73. package/rules/prefer-string-raw.js +90 -19
  74. package/rules/prefer-string-repeat.js +173 -0
  75. package/rules/prefer-string-replace-all.js +180 -12
  76. package/rules/prefer-string-slice.js +54 -3
  77. package/rules/prefer-top-level-await.js +129 -1
  78. package/rules/prevent-abbreviations.js +126 -134
  79. package/rules/require-css-escape.js +141 -0
  80. package/rules/require-module-specifiers.js +62 -0
  81. package/rules/require-passive-events.js +292 -0
  82. package/rules/shared/dom-events.js +7 -7
  83. package/rules/shared/identifier-checks.js +205 -0
  84. package/rules/shared/package-json.js +1 -1
  85. package/rules/string-content.js +34 -5
  86. package/rules/template-indent.js +52 -3
  87. package/rules/throw-new-error.js +4 -0
  88. package/rules/try-complexity.js +184 -0
  89. package/rules/utils/boolean.js +12 -12
  90. package/rules/utils/get-duplicate-array-elements.js +77 -0
  91. package/rules/utils/index.js +1 -0
  92. package/rules/utils/is-same-reference.js +6 -4
  93. package/rules/utils/should-add-parentheses-to-await-expression-argument.js +9 -9
  94. package/rules/utils/should-add-parentheses-to-call-expression-callee.js +12 -10
  95. package/rules/utils/should-add-parentheses-to-conditional-expression-child.js +9 -7
  96. package/rules/utils/should-add-parentheses-to-logical-expression-child.js +12 -8
  97. package/rules/utils/should-add-parentheses-to-unary-expression.js +10 -10
  98. package/rules/better-regex.js +0 -155
  99. package/rules/prefer-dom-node-dataset.js +0 -135
  100. 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;