eslint-plugin-unicorn 63.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 (146) hide show
  1. package/index.js +26 -18
  2. package/package.json +32 -30
  3. package/readme.md +183 -147
  4. package/rules/ast/call-or-new-expression.js +3 -5
  5. package/rules/ast/literal.js +1 -6
  6. package/rules/better-dom-traversing.js +291 -0
  7. package/rules/catch-error-name.js +3 -3
  8. package/rules/consistent-compound-words.js +452 -0
  9. package/rules/consistent-destructuring.js +234 -22
  10. package/rules/consistent-function-scoping.js +35 -11
  11. package/rules/consistent-json-file-read.js +245 -0
  12. package/rules/consistent-template-literal-escape.js +52 -0
  13. package/rules/custom-error-definition.js +176 -71
  14. package/rules/dom-node-dataset.js +431 -0
  15. package/rules/escape-case.js +2 -1
  16. package/rules/expiring-todo-comments.js +56 -102
  17. package/rules/explicit-length-check.js +145 -33
  18. package/rules/filename-case.js +97 -13
  19. package/rules/fix/append-argument.js +1 -1
  20. package/rules/fix/remove-expression-statement.js +1 -0
  21. package/rules/import-style.js +30 -10
  22. package/rules/index.js +37 -3
  23. package/rules/isolated-functions.js +11 -5
  24. package/rules/no-accessor-recursion.js +1 -2
  25. package/rules/no-anonymous-default-export.js +9 -11
  26. package/rules/no-array-callback-reference.js +148 -22
  27. package/rules/no-array-fill-with-reference-type.js +127 -0
  28. package/rules/no-array-for-each.js +18 -3
  29. package/rules/no-array-from-fill.js +76 -0
  30. package/rules/no-array-method-this-argument.js +1 -1
  31. package/rules/no-array-reduce.js +509 -2
  32. package/rules/no-blob-to-file.js +299 -0
  33. package/rules/no-canvas-to-image.js +152 -0
  34. package/rules/no-confusing-array-splice.js +164 -0
  35. package/rules/no-duplicate-set-values.js +57 -0
  36. package/rules/no-empty-file.js +31 -2
  37. package/rules/no-exports-in-scripts.js +34 -0
  38. package/rules/no-for-loop.js +1 -5
  39. package/rules/no-hex-escape.js +1 -1
  40. package/rules/no-incorrect-query-selector.js +347 -0
  41. package/rules/no-instanceof-builtins.js +4 -6
  42. package/rules/no-invalid-fetch-options.js +1 -1
  43. package/rules/no-invalid-file-input-accept.js +475 -0
  44. package/rules/no-keyword-prefix.js +23 -5
  45. package/rules/no-late-current-target-access.js +166 -0
  46. package/rules/no-lonely-if.js +152 -52
  47. package/rules/no-manually-wrapped-comments.js +151 -0
  48. package/rules/no-null.js +16 -5
  49. package/rules/no-process-exit.js +1 -1
  50. package/rules/no-this-outside-of-class.js +75 -0
  51. package/rules/no-typeof-undefined.js +7 -4
  52. package/rules/no-unnecessary-nested-ternary.js +132 -0
  53. package/rules/no-unnecessary-polyfills.js +253 -24
  54. package/rules/no-unreadable-iife.js +33 -3
  55. package/rules/no-unused-array-method-return.js +301 -0
  56. package/rules/no-unused-properties.js +13 -19
  57. package/rules/no-useless-iterator-to-array.js +249 -0
  58. package/rules/no-useless-length-check.js +5 -11
  59. package/rules/no-useless-promise-resolve-reject.js +5 -5
  60. package/rules/no-useless-spread.js +53 -4
  61. package/rules/no-useless-undefined.js +62 -46
  62. package/rules/numeric-separators-style.js +17 -6
  63. package/rules/prefer-add-event-listener.js +1 -0
  64. package/rules/prefer-array-find.js +1 -0
  65. package/rules/prefer-array-flat.js +76 -8
  66. package/rules/prefer-array-last-methods.js +93 -0
  67. package/rules/prefer-array-some.js +74 -8
  68. package/rules/prefer-at.js +118 -57
  69. package/rules/prefer-class-fields.js +2 -4
  70. package/rules/prefer-default-parameters.js +1 -2
  71. package/rules/prefer-dom-node-append.js +2 -2
  72. package/rules/prefer-dom-node-remove.js +24 -18
  73. package/rules/prefer-export-from.js +8 -7
  74. package/rules/prefer-get-or-insert-computed.js +159 -0
  75. package/rules/prefer-global-this.js +33 -29
  76. package/rules/prefer-https.js +83 -0
  77. package/rules/prefer-includes-over-repeated-comparisons.js +199 -0
  78. package/rules/prefer-iterator-concat.js +174 -0
  79. package/rules/prefer-iterator-to-array-at-end.js +123 -0
  80. package/rules/prefer-keyboard-event-key.js +202 -76
  81. package/rules/prefer-logical-operator-over-ternary.js +1 -1
  82. package/rules/prefer-math-abs.js +397 -0
  83. package/rules/prefer-math-min-max.js +54 -26
  84. package/rules/prefer-module.js +5 -7
  85. package/rules/prefer-native-coercion-functions.js +8 -1
  86. package/rules/prefer-node-protocol.js +7 -4
  87. package/rules/prefer-number-properties.js +2 -0
  88. package/rules/prefer-object-from-entries.js +12 -7
  89. package/rules/prefer-prototype-methods.js +1 -3
  90. package/rules/prefer-query-selector.js +119 -7
  91. package/rules/prefer-queue-microtask.js +204 -0
  92. package/rules/prefer-regexp-test.js +166 -20
  93. package/rules/prefer-set-has.js +474 -47
  94. package/rules/prefer-simple-condition-first.js +282 -0
  95. package/rules/prefer-single-call.js +129 -38
  96. package/rules/prefer-split-limit.js +107 -0
  97. package/rules/prefer-spread.js +150 -19
  98. package/rules/prefer-string-match-all.js +352 -0
  99. package/rules/prefer-string-pad-start-end.js +380 -0
  100. package/rules/prefer-string-raw.js +90 -19
  101. package/rules/prefer-string-repeat.js +173 -0
  102. package/rules/prefer-string-replace-all.js +180 -12
  103. package/rules/prefer-string-slice.js +55 -4
  104. package/rules/prefer-string-starts-ends-with.js +2 -2
  105. package/rules/prefer-structured-clone.js +1 -0
  106. package/rules/prefer-switch.js +2 -0
  107. package/rules/prefer-ternary.js +3 -2
  108. package/rules/prefer-top-level-await.js +168 -6
  109. package/rules/prevent-abbreviations.js +144 -158
  110. package/rules/relative-url-style.js +1 -0
  111. package/rules/require-css-escape.js +141 -0
  112. package/rules/require-module-attributes.js +16 -18
  113. package/rules/require-module-specifiers.js +62 -0
  114. package/rules/require-passive-events.js +292 -0
  115. package/rules/rule/to-eslint-rule.js +1 -1
  116. package/rules/rule/to-eslint-rules.js +4 -6
  117. package/rules/shared/abbreviations.js +1 -0
  118. package/rules/shared/dom-events.js +7 -7
  119. package/rules/shared/identifier-checks.js +205 -0
  120. package/rules/shared/package-json.js +1 -1
  121. package/rules/string-content.js +35 -5
  122. package/rules/switch-case-braces.js +2 -3
  123. package/rules/switch-case-break-position.js +138 -0
  124. package/rules/template-indent.js +59 -3
  125. package/rules/text-encoding-identifier-case.js +23 -5
  126. package/rules/throw-new-error.js +4 -0
  127. package/rules/try-complexity.js +184 -0
  128. package/rules/utils/assert-token.js +3 -5
  129. package/rules/utils/boolean.js +37 -28
  130. package/rules/utils/create-deprecated-rules.js +25 -27
  131. package/rules/utils/get-call-expression-arguments-text.js +3 -5
  132. package/rules/utils/get-duplicate-array-elements.js +77 -0
  133. package/rules/utils/index.js +4 -1
  134. package/rules/utils/is-node-contains-lexical-this.js +65 -0
  135. package/rules/utils/is-number.js +2 -2
  136. package/rules/utils/is-same-reference.js +25 -17
  137. package/rules/utils/should-add-parentheses-to-await-expression-argument.js +9 -9
  138. package/rules/utils/should-add-parentheses-to-call-expression-callee.js +12 -10
  139. package/rules/utils/should-add-parentheses-to-conditional-expression-child.js +9 -5
  140. package/rules/utils/should-add-parentheses-to-expression-statement-expression.js +1 -1
  141. package/rules/utils/should-add-parentheses-to-logical-expression-child.js +12 -8
  142. package/rules/utils/should-add-parentheses-to-member-expression-object.js +1 -1
  143. package/rules/utils/should-add-parentheses-to-unary-expression.js +10 -10
  144. package/rules/better-regex.js +0 -145
  145. package/rules/prefer-dom-node-dataset.js +0 -135
  146. package/rules/prefer-json-parse-buffer.js +0 -161
@@ -71,11 +71,9 @@ function create(node, options, types) {
71
71
  const maximumArgumentsLength = Number.isFinite(maximumArguments) ? maximumArguments : argumentsLength;
72
72
  if (
73
73
  typeof maximumArgumentsLength === 'number'
74
- && node.arguments.some(
75
- (node, index) =>
76
- node.type === 'SpreadElement'
77
- && index < maximumArgumentsLength,
78
- )
74
+ && node.arguments.some((node, index) =>
75
+ node.type === 'SpreadElement'
76
+ && index < maximumArgumentsLength)
79
77
  ) {
80
78
  return false;
81
79
  }
@@ -3,10 +3,6 @@ export function isLiteral(node, value) {
3
3
  return false;
4
4
  }
5
5
 
6
- if (value === null) {
7
- return node.raw === 'null';
8
- }
9
-
10
6
  return node.value === value;
11
7
  }
12
8
 
@@ -16,8 +12,7 @@ export const isNumericLiteral = node => node.type === 'Literal' && typeof node.v
16
12
 
17
13
  export const isRegexLiteral = node => node.type === 'Literal' && Boolean(node.regex);
18
14
 
19
- // eslint-disable-next-line unicorn/no-null
20
- export const isNullLiteral = node => isLiteral(node, null);
15
+ export const isNullLiteral = node => node?.type === 'Literal' && node.raw === 'null';
21
16
 
22
17
  export const isBigIntLiteral = node => node.type === 'Literal' && Boolean(node.bigint);
23
18
 
@@ -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;
@@ -34,9 +34,7 @@ const isPromiseCatchParameter = node =>
34
34
  const create = context => {
35
35
  const options = context.options[0];
36
36
  const {name: expectedName} = options;
37
- const ignore = options.ignore.map(
38
- pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u'),
39
- );
37
+ const ignore = options.ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u'));
40
38
  const isNameAllowed = name =>
41
39
  name === expectedName
42
40
  || ignore.some(regexp => regexp.test(name))
@@ -104,10 +102,12 @@ const schema = [
104
102
  properties: {
105
103
  name: {
106
104
  type: 'string',
105
+ description: 'The expected name for the error variable.',
107
106
  },
108
107
  ignore: {
109
108
  type: 'array',
110
109
  uniqueItems: true,
110
+ description: 'Patterns to ignore.',
111
111
  },
112
112
  },
113
113
  },