eslint-plugin-unicorn 67.0.0 → 69.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 (192) hide show
  1. package/index.js +4 -0
  2. package/package.json +8 -3
  3. package/readme.md +165 -16
  4. package/rules/ast/is-empty-node.js +1 -5
  5. package/rules/ast/is-empty-object-expression.js +2 -2
  6. package/rules/better-dom-traversing.js +17 -8
  7. package/rules/class-reference-in-static-methods.js +14 -2
  8. package/rules/comment-content.js +136 -18
  9. package/rules/consistent-assert.js +1 -1
  10. package/rules/consistent-boolean-name.js +44 -5
  11. package/rules/consistent-compound-words.js +7 -5
  12. package/rules/consistent-conditional-object-spread.js +360 -0
  13. package/rules/consistent-destructuring.js +5 -0
  14. package/rules/consistent-existence-index-check.js +1 -1
  15. package/rules/consistent-json-file-read.js +4 -16
  16. package/rules/consistent-optional-chaining.js +3 -20
  17. package/rules/consistent-tuple-labels.js +49 -0
  18. package/rules/custom-error-definition.js +26 -7
  19. package/rules/default-export-style.js +569 -0
  20. package/rules/expiring-todo-comments.js +1 -1
  21. package/rules/explicit-length-check.js +7 -32
  22. package/rules/import-style.js +8 -6
  23. package/rules/index.js +50 -1
  24. package/rules/logical-assignment-operators.js +17 -4
  25. package/rules/{prevent-abbreviations.js → name-replacements.js} +11 -9
  26. package/rules/new-for-builtins.js +39 -5
  27. package/rules/no-accessor-recursion.js +7 -0
  28. package/rules/no-accidental-bitwise-operator.js +97 -0
  29. package/rules/no-array-callback-reference.js +1 -7
  30. package/rules/no-array-concat-in-loop.js +45 -0
  31. package/rules/no-array-front-mutation.js +64 -0
  32. package/rules/no-array-sort-for-min-max.js +225 -0
  33. package/rules/no-array-splice.js +78 -64
  34. package/rules/no-boolean-sort-comparator.js +215 -0
  35. package/rules/no-break-in-nested-loop.js +33 -0
  36. package/rules/no-chained-comparison.js +116 -0
  37. package/rules/no-collection-bracket-access.js +165 -0
  38. package/rules/no-computed-property-existence-check.js +6 -0
  39. package/rules/no-confusing-array-splice.js +2 -21
  40. package/rules/no-confusing-array-with.js +2 -20
  41. package/rules/no-constant-zero-expression.js +91 -0
  42. package/rules/no-declarations-before-early-exit.js +29 -36
  43. package/rules/no-double-comparison.js +117 -0
  44. package/rules/no-duplicate-if-branches.js +143 -0
  45. package/rules/no-duplicate-logical-operands.js +151 -0
  46. package/rules/no-for-each.js +11 -12
  47. package/rules/no-for-loop.js +3 -15
  48. package/rules/no-impossible-length-comparison.js +166 -0
  49. package/rules/no-incorrect-query-selector.js +8 -4
  50. package/rules/no-incorrect-template-string-interpolation.js +106 -43
  51. package/rules/no-instanceof-builtins.js +73 -14
  52. package/rules/no-invalid-argument-count.js +110 -54
  53. package/rules/no-invalid-character-comparison.js +121 -0
  54. package/rules/no-invalid-fetch-options.js +3 -1
  55. package/rules/no-invalid-well-known-symbol-methods.js +205 -0
  56. package/rules/no-keyword-prefix.js +14 -4
  57. package/rules/no-late-current-target-access.js +13 -107
  58. package/rules/no-late-event-control.js +127 -0
  59. package/rules/no-loop-iterable-mutation.js +541 -0
  60. package/rules/no-manually-wrapped-comments.js +10 -2
  61. package/rules/no-misrefactored-assignment.js +102 -0
  62. package/rules/no-named-default.js +35 -2
  63. package/rules/no-nonstandard-builtin-properties.js +1383 -0
  64. package/rules/no-object-methods-with-collections.js +4 -289
  65. package/rules/no-process-exit.js +16 -12
  66. package/rules/no-redundant-comparison.js +5 -12
  67. package/rules/no-return-array-push.js +20 -5
  68. package/rules/no-selector-as-dom-name.js +150 -0
  69. package/rules/no-thenable.js +27 -11
  70. package/rules/no-top-level-side-effects.js +8 -1
  71. package/rules/no-uncalled-method.js +2 -8
  72. package/rules/no-undeclared-class-members.js +8 -6
  73. package/rules/no-unnecessary-boolean-comparison.js +131 -0
  74. package/rules/no-unnecessary-global-this.js +36 -1
  75. package/rules/no-unnecessary-splice.js +15 -20
  76. package/rules/no-unreadable-for-of-expression.js +2 -1
  77. package/rules/no-unreadable-object-destructuring.js +15 -0
  78. package/rules/no-unsafe-property-key.js +10 -15
  79. package/rules/no-unsafe-string-replacement.js +31 -0
  80. package/rules/no-unused-array-method-return.js +11 -2
  81. package/rules/no-unused-properties.js +4 -3
  82. package/rules/no-useless-boolean-cast.js +58 -1
  83. package/rules/no-useless-coercion.js +71 -15
  84. package/rules/no-useless-compound-assignment.js +110 -0
  85. package/rules/no-useless-concat.js +27 -3
  86. package/rules/no-useless-delete-check.js +353 -0
  87. package/rules/no-useless-else.js +27 -62
  88. package/rules/no-useless-error-capture-stack-trace.js +1 -1
  89. package/rules/no-useless-fallback-in-spread.js +7 -118
  90. package/rules/no-useless-iterator-to-array.js +165 -115
  91. package/rules/no-useless-logical-operand.js +248 -0
  92. package/rules/no-useless-undefined.js +11 -28
  93. package/rules/no-xor-as-exponentiation.js +59 -0
  94. package/rules/prefer-abort-signal-timeout.js +395 -0
  95. package/rules/prefer-aggregate-error.js +478 -0
  96. package/rules/prefer-array-find.js +95 -5
  97. package/rules/prefer-array-flat-map.js +9 -21
  98. package/rules/prefer-array-flat.js +1 -24
  99. package/rules/prefer-array-from-async.js +273 -0
  100. package/rules/prefer-array-iterable-methods.js +157 -0
  101. package/rules/prefer-array-some.js +56 -31
  102. package/rules/prefer-at.js +6 -30
  103. package/rules/prefer-await.js +29 -0
  104. package/rules/prefer-boolean-return.js +224 -0
  105. package/rules/prefer-code-point.js +58 -8
  106. package/rules/prefer-continue.js +377 -0
  107. package/rules/prefer-default-parameters.js +10 -6
  108. package/rules/prefer-dispose.js +1 -5
  109. package/rules/prefer-dom-node-replace-children.js +353 -0
  110. package/rules/prefer-early-return.js +160 -30
  111. package/rules/prefer-else-if.js +50 -53
  112. package/rules/prefer-error-is-error.js +181 -0
  113. package/rules/prefer-event-target.js +3 -13
  114. package/rules/prefer-flat-math-min-max.js +103 -0
  115. package/rules/prefer-global-this.js +1 -1
  116. package/rules/prefer-has-check.js +1 -4
  117. package/rules/prefer-hoisting-branch-code.js +424 -0
  118. package/rules/prefer-identifier-import-export-specifiers.js +6 -11
  119. package/rules/prefer-includes-over-repeated-comparisons.js +5 -1
  120. package/rules/prefer-includes.js +6 -0
  121. package/rules/prefer-iterator-concat.js +21 -1
  122. package/rules/prefer-logical-operator-over-ternary.js +12 -20
  123. package/rules/prefer-math-constants.js +103 -0
  124. package/rules/prefer-math-min-max.js +1 -5
  125. package/rules/prefer-minimal-ternary.js +41 -13
  126. package/rules/prefer-modern-dom-apis.js +1 -45
  127. package/rules/prefer-node-protocol.js +1 -0
  128. package/rules/prefer-number-properties.js +2 -5
  129. package/rules/prefer-object-from-entries.js +6 -1
  130. package/rules/prefer-object-iterable-methods.js +36 -11
  131. package/rules/prefer-observer-apis.js +498 -0
  132. package/rules/prefer-optional-catch-binding.js +30 -24
  133. package/rules/prefer-private-class-fields.js +2 -8
  134. package/rules/prefer-promise-try.js +238 -0
  135. package/rules/prefer-promise-with-resolvers.js +248 -0
  136. package/rules/prefer-query-selector.js +79 -17
  137. package/rules/prefer-queue-microtask.js +7 -9
  138. package/rules/prefer-regexp-escape.js +263 -0
  139. package/rules/prefer-set-methods.js +337 -0
  140. package/rules/prefer-set-size.js +10 -2
  141. package/rules/prefer-short-arrow-method.js +3 -3
  142. package/rules/prefer-single-replace.js +218 -0
  143. package/rules/prefer-spread.js +14 -21
  144. package/rules/prefer-string-replace-all.js +5 -0
  145. package/rules/prefer-structured-clone.js +2 -2
  146. package/rules/prefer-switch.js +1 -0
  147. package/rules/prefer-ternary.js +4 -2
  148. package/rules/prefer-toggle-attribute.js +293 -0
  149. package/rules/prefer-type-literal-last.js +22 -54
  150. package/rules/prefer-unary-minus.js +103 -0
  151. package/rules/prefer-url-can-parse.js +308 -0
  152. package/rules/prefer-url-search-parameters.js +411 -0
  153. package/rules/prefer-while-loop-condition.js +252 -0
  154. package/rules/require-passive-events.js +11 -4
  155. package/rules/require-proxy-trap-boolean-return.js +40 -126
  156. package/rules/rule/to-eslint-listener.js +5 -3
  157. package/rules/shared/array-concat-in-loop.js +151 -0
  158. package/rules/shared/identifier-checks.js +4 -16
  159. package/rules/shared/late-event-handler.js +82 -0
  160. package/rules/shared/{abbreviations.js → name-replacements.js} +63 -0
  161. package/rules/shared/no-array-mutate-rule.js +19 -2
  162. package/rules/shared/regexp-escape.js +81 -0
  163. package/rules/shared/splice-replacements.js +1 -18
  164. package/rules/string-content.js +11 -4
  165. package/rules/template-indent.js +2 -8
  166. package/rules/throw-new-error.js +7 -0
  167. package/rules/utils/block-scope.js +42 -0
  168. package/rules/utils/builtin-collection-type.js +267 -0
  169. package/rules/utils/builtins.js +50 -1
  170. package/rules/utils/comments.js +16 -0
  171. package/rules/utils/comparison.js +17 -0
  172. package/rules/utils/contains-suspension-point.js +35 -0
  173. package/rules/utils/get-const-variable-initializer.js +31 -0
  174. package/rules/utils/global-reference-tracker.js +14 -1
  175. package/rules/utils/has-optional-chain-element.js +36 -4
  176. package/rules/utils/index.js +38 -2
  177. package/rules/utils/is-boolean.js +26 -1
  178. package/rules/utils/is-event.js +127 -0
  179. package/rules/utils/is-function-self-used-inside.js +1 -5
  180. package/rules/utils/is-weak-map.js +21 -0
  181. package/rules/utils/is-weak-set.js +21 -0
  182. package/rules/utils/length-or-size.js +53 -0
  183. package/rules/utils/member-expression.js +18 -0
  184. package/rules/utils/needs-semicolon.js +1 -5
  185. package/rules/utils/numeric.js +20 -0
  186. package/rules/utils/should-add-parentheses-to-logical-expression-child.js +8 -14
  187. package/rules/utils/should-add-parentheses-to-member-expression-object.js +1 -5
  188. package/rules/utils/should-add-parentheses-to-unary-expression.js +8 -2
  189. package/rules/utils/should-report-replace-children-receiver.js +210 -0
  190. package/rules/utils/track-branch-exits.js +79 -0
  191. package/rules/utils/type-helpers.js +166 -8
  192. package/rules/utils/types.js +22 -0
@@ -0,0 +1,411 @@
1
+ import {findVariable, getStaticValue} from '@eslint-community/eslint-utils';
2
+ import {
3
+ isMethodCall,
4
+ isNewExpression,
5
+ getStaticStringValue,
6
+ } from './ast/index.js';
7
+ import {
8
+ getParenthesizedRange,
9
+ isKnownNonString,
10
+ isSameIdentifier,
11
+ isTypeImportSpecifier,
12
+ isTypeScriptExpressionWrapper,
13
+ unwrapTypeScriptExpression,
14
+ wouldRemoveComments,
15
+ } from './utils/index.js';
16
+
17
+ const MESSAGE_ID_ERROR = 'prefer-url-search-parameters/error';
18
+ const MESSAGE_ID_SUGGESTION = 'prefer-url-search-parameters/suggestion';
19
+ const messages = {
20
+ [MESSAGE_ID_ERROR]: 'Prefer `URLSearchParams` over manually splitting query strings.',
21
+ [MESSAGE_ID_SUGGESTION]: 'Use `{{replacement}}`.',
22
+ };
23
+
24
+ const urlImportSources = new Set([
25
+ 'node:url',
26
+ 'url',
27
+ ]);
28
+
29
+ const isTypeOnlyImport = definition =>
30
+ definition.type === 'ImportBinding'
31
+ && isTypeImportSpecifier(definition.node);
32
+
33
+ const isErasedDefinition = definition =>
34
+ definition.type === 'Type'
35
+ || isTypeOnlyImport(definition);
36
+
37
+ const isUrlSearchParametersImport = definition => {
38
+ if (definition.type !== 'ImportBinding') {
39
+ return false;
40
+ }
41
+
42
+ const {node, parent} = definition;
43
+ return urlImportSources.has(parent.source.value)
44
+ && parent.importKind !== 'type'
45
+ && node.type === 'ImportSpecifier'
46
+ && node.importKind !== 'type'
47
+ && node.imported.type === 'Identifier'
48
+ && node.imported.name === 'URLSearchParams';
49
+ };
50
+
51
+ const isGlobalNameAvailable = (name, node, context) => {
52
+ const variable = findVariable(context.sourceCode.getScope(node), name);
53
+ return !variable || variable.defs.every(definition => isErasedDefinition(definition));
54
+ };
55
+
56
+ const isUrlSearchParametersAvailable = (node, context) => {
57
+ const variable = findVariable(context.sourceCode.getScope(node), 'URLSearchParams');
58
+ return !variable
59
+ || variable.defs.every(definition => isErasedDefinition(definition))
60
+ || variable.defs.some(definition => isUrlSearchParametersImport(definition));
61
+ };
62
+
63
+ const getStaticNumberValue = (node, context) =>
64
+ getStaticValue(node, context.sourceCode.getScope(node))?.value;
65
+
66
+ const isStaticString = (node, value) =>
67
+ getStaticStringValue(unwrapTypeScriptExpression(node)) === value;
68
+
69
+ const isStaticNonString = (node, context) => {
70
+ const staticValue = getStaticValue(node, context.sourceCode.getScope(node));
71
+ return Boolean(staticValue) && typeof staticValue.value !== 'string';
72
+ };
73
+
74
+ const getArgumentText = (node, context) => {
75
+ const text = context.sourceCode.getText(node);
76
+ return node.type === 'SequenceExpression' ? `(${text})` : text;
77
+ };
78
+
79
+ const getAmpersandSplitCall = (node, context) => {
80
+ node = unwrapTypeScriptExpression(node);
81
+ if (!isMethodCall(node, {
82
+ method: 'split',
83
+ argumentsLength: 1,
84
+ computed: false,
85
+ optionalCall: false,
86
+ optionalMember: false,
87
+ })) {
88
+ return;
89
+ }
90
+
91
+ if (
92
+ !isStaticString(node.arguments[0], '&')
93
+ || isKnownNonString(node.callee.object, context)
94
+ || isStaticNonString(node.callee.object, context)
95
+ ) {
96
+ return;
97
+ }
98
+
99
+ return node;
100
+ };
101
+
102
+ const isEqualsSplitCall = (node, parameter, context) => {
103
+ node = unwrapTypeScriptExpression(node);
104
+
105
+ if (!isMethodCall(node, {
106
+ method: 'split',
107
+ minimumArguments: 1,
108
+ maximumArguments: 2,
109
+ computed: false,
110
+ optionalCall: false,
111
+ optionalMember: false,
112
+ })) {
113
+ return false;
114
+ }
115
+
116
+ if (!isSameIdentifier(unwrapTypeScriptExpression(node.callee.object), parameter)) {
117
+ return false;
118
+ }
119
+
120
+ if (!isStaticString(node.arguments[0], '=')) {
121
+ return false;
122
+ }
123
+
124
+ return node.arguments.length === 1 || getStaticNumberValue(node.arguments[1], context) === 2;
125
+ };
126
+
127
+ const getCallbackReturnExpression = callback => {
128
+ if (
129
+ (callback.type !== 'ArrowFunctionExpression' && callback.type !== 'FunctionExpression')
130
+ || callback.async
131
+ || callback.generator
132
+ || callback.params.length !== 1
133
+ || callback.params[0].type !== 'Identifier'
134
+ ) {
135
+ return;
136
+ }
137
+
138
+ if (callback.body.type !== 'BlockStatement') {
139
+ return {
140
+ parameter: callback.params[0],
141
+ returnExpression: callback.body,
142
+ };
143
+ }
144
+
145
+ if (
146
+ callback.body.body.length !== 1
147
+ || callback.body.body[0].type !== 'ReturnStatement'
148
+ || !callback.body.body[0].argument
149
+ ) {
150
+ return;
151
+ }
152
+
153
+ return {
154
+ parameter: callback.params[0],
155
+ returnExpression: callback.body.body[0].argument,
156
+ };
157
+ };
158
+
159
+ const getManualSearchParametersPipeline = (node, context) => {
160
+ node = unwrapTypeScriptExpression(node);
161
+
162
+ if (
163
+ !isMethodCall(node, {
164
+ method: 'map',
165
+ argumentsLength: 1,
166
+ computed: false,
167
+ optionalCall: false,
168
+ optionalMember: false,
169
+ })
170
+ || node.typeArguments
171
+ || node.typeParameters
172
+ ) {
173
+ return;
174
+ }
175
+
176
+ const splitCall = getAmpersandSplitCall(node.callee.object, context);
177
+ if (!splitCall) {
178
+ return;
179
+ }
180
+
181
+ const [callback] = node.arguments;
182
+ const result = getCallbackReturnExpression(callback);
183
+ if (!result || !isEqualsSplitCall(result.returnExpression, result.parameter, context)) {
184
+ return;
185
+ }
186
+
187
+ return {
188
+ node,
189
+ query: splitCall.callee.object,
190
+ };
191
+ };
192
+
193
+ const getUrlSearchParametersText = (query, context) =>
194
+ `new URLSearchParams(${getArgumentText(query, context)})`;
195
+
196
+ const getReplacementWithArgument = (node, argument, replacementArgument, context) => {
197
+ const {sourceCode} = context;
198
+ const [nodeStart, nodeEnd] = sourceCode.getRange(node);
199
+ const [argumentStart, argumentEnd] = getParenthesizedRange(argument, context);
200
+
201
+ return sourceCode.text.slice(nodeStart, argumentStart)
202
+ + replacementArgument
203
+ + sourceCode.text.slice(argumentEnd, nodeEnd);
204
+ };
205
+
206
+ const getPreservedWrapperRanges = (node, argument, context) => {
207
+ const [nodeStart, nodeEnd] = context.sourceCode.getRange(node);
208
+ const [argumentStart, argumentEnd] = getParenthesizedRange(argument, context);
209
+
210
+ return [
211
+ [nodeStart, argumentStart],
212
+ [argumentEnd, nodeEnd],
213
+ ];
214
+ };
215
+
216
+ const getSuggestion = ({node, replacement, preservedNodes}, context) => {
217
+ if (
218
+ !isUrlSearchParametersAvailable(node, context)
219
+ || wouldRemoveComments(context, node, preservedNodes)
220
+ ) {
221
+ return;
222
+ }
223
+
224
+ return [
225
+ {
226
+ messageId: MESSAGE_ID_SUGGESTION,
227
+ data: {replacement},
228
+ fix: fixer => fixer.replaceText(node, replacement),
229
+ },
230
+ ];
231
+ };
232
+
233
+ const createProblem = ({node, query, replacement, preservedNodes = [query]}, context) => {
234
+ const suggest = getSuggestion({
235
+ node,
236
+ replacement,
237
+ preservedNodes,
238
+ }, context);
239
+ if (!suggest) {
240
+ return;
241
+ }
242
+
243
+ return {
244
+ node,
245
+ messageId: MESSAGE_ID_ERROR,
246
+ suggest,
247
+ };
248
+ };
249
+
250
+ const isObjectFromEntriesCall = (node, context) =>
251
+ isMethodCall(node, {
252
+ object: 'Object',
253
+ method: 'fromEntries',
254
+ argumentsLength: 1,
255
+ computed: false,
256
+ optionalCall: false,
257
+ optionalMember: false,
258
+ })
259
+ && isGlobalNameAvailable('Object', node, context);
260
+
261
+ const isMapConstructor = (node, context) =>
262
+ isNewExpression(node, {
263
+ name: 'Map',
264
+ argumentsLength: 1,
265
+ })
266
+ && isGlobalNameAvailable('Map', node, context);
267
+
268
+ const isUrlSearchParametersConstructor = node =>
269
+ isNewExpression(node, {
270
+ name: 'URLSearchParams',
271
+ argumentsLength: 1,
272
+ });
273
+
274
+ const isPropertyNamed = (node, name, {computed}) =>
275
+ computed
276
+ ? getStaticStringValue(node) === name
277
+ : node.type === 'Identifier' && node.name === name;
278
+
279
+ const isPotentialObjectFromEntriesCall = node =>
280
+ node.type === 'CallExpression'
281
+ && node.arguments.length === 1
282
+ && node.callee.type === 'MemberExpression'
283
+ && node.callee.object.type === 'Identifier'
284
+ && node.callee.object.name === 'Object'
285
+ && isPropertyNamed(node.callee.property, 'fromEntries', {computed: node.callee.computed});
286
+
287
+ const isPotentialWrapper = node =>
288
+ isPotentialObjectFromEntriesCall(node)
289
+ || isNewExpression(node, {
290
+ names: [
291
+ 'Map',
292
+ 'URLSearchParams',
293
+ ],
294
+ argumentsLength: 1,
295
+ });
296
+
297
+ const getFirstArgumentParent = node => {
298
+ let expression = node;
299
+ let {parent} = node;
300
+ while (
301
+ isTypeScriptExpressionWrapper(parent)
302
+ && parent.expression === expression
303
+ ) {
304
+ expression = parent;
305
+ parent = parent.parent;
306
+ }
307
+
308
+ return parent?.arguments?.[0] === expression ? parent : undefined;
309
+ };
310
+
311
+ const isWrappedPipeline = node => {
312
+ const parent = getFirstArgumentParent(node);
313
+ return parent && isPotentialWrapper(parent);
314
+ };
315
+
316
+ const getObjectFromEntriesProblem = (node, context) => {
317
+ if (!isObjectFromEntriesCall(node, context)) {
318
+ return;
319
+ }
320
+
321
+ const [argument] = node.arguments;
322
+ const pipeline = getManualSearchParametersPipeline(argument, context);
323
+ if (!pipeline) {
324
+ return;
325
+ }
326
+
327
+ const replacement = getReplacementWithArgument(node, argument, getUrlSearchParametersText(pipeline.query, context), context);
328
+ return createProblem({
329
+ node,
330
+ query: pipeline.query,
331
+ replacement,
332
+ preservedNodes: [
333
+ ...getPreservedWrapperRanges(node, argument, context),
334
+ pipeline.query,
335
+ ],
336
+ }, context);
337
+ };
338
+
339
+ const getNewExpressionProblem = (node, context) => {
340
+ const isMap = isMapConstructor(node, context);
341
+ if (!isMap && !isUrlSearchParametersConstructor(node)) {
342
+ return;
343
+ }
344
+
345
+ const [argument] = node.arguments;
346
+ const pipeline = getManualSearchParametersPipeline(argument, context);
347
+ if (!pipeline) {
348
+ return;
349
+ }
350
+
351
+ const queryText = getArgumentText(pipeline.query, context);
352
+ const urlSearchParametersText = getUrlSearchParametersText(pipeline.query, context);
353
+ const replacementArgument = isMap ? urlSearchParametersText : queryText;
354
+ const replacement = getReplacementWithArgument(node, argument, replacementArgument, context);
355
+
356
+ return createProblem({
357
+ node,
358
+ query: pipeline.query,
359
+ replacement,
360
+ preservedNodes: [
361
+ ...getPreservedWrapperRanges(node, argument, context),
362
+ pipeline.query,
363
+ ],
364
+ }, context);
365
+ };
366
+
367
+ /** @param {import('eslint').Rule.RuleContext} context */
368
+ const create = context => {
369
+ context.on('CallExpression', node => {
370
+ const objectFromEntriesProblem = getObjectFromEntriesProblem(node, context);
371
+ if (objectFromEntriesProblem) {
372
+ return objectFromEntriesProblem;
373
+ }
374
+
375
+ if (isWrappedPipeline(node)) {
376
+ return;
377
+ }
378
+
379
+ const pipeline = getManualSearchParametersPipeline(node, context);
380
+ if (!pipeline) {
381
+ return;
382
+ }
383
+
384
+ return createProblem({
385
+ node,
386
+ query: pipeline.query,
387
+ replacement: getUrlSearchParametersText(pipeline.query, context),
388
+ }, context);
389
+ });
390
+
391
+ context.on('NewExpression', node => getNewExpressionProblem(node, context));
392
+ };
393
+
394
+ /** @type {import('eslint').Rule.RuleModule} */
395
+ const config = {
396
+ create,
397
+ meta: {
398
+ type: 'suggestion',
399
+ docs: {
400
+ description: 'Prefer `URLSearchParams` over manually splitting query strings.',
401
+ recommended: 'unopinionated',
402
+ },
403
+ hasSuggestions: true,
404
+ messages,
405
+ languages: [
406
+ 'js/js',
407
+ ],
408
+ },
409
+ };
410
+
411
+ export default config;
@@ -0,0 +1,252 @@
1
+ import {
2
+ isBooleanLiteral,
3
+ isFunction,
4
+ isLoop,
5
+ } from './ast/index.js';
6
+ import {removeStatement} from './fix/index.js';
7
+ import {
8
+ hasCommentInRange,
9
+ getLastTrailingCommentOnSameLine,
10
+ getReferences,
11
+ shouldAddParenthesesToUnaryExpressionArgument,
12
+ } from './utils/index.js';
13
+
14
+ /**
15
+ @import * as ESLint from 'eslint';
16
+ */
17
+
18
+ const MESSAGE_ID = 'prefer-while-loop-condition';
19
+ const messages = {
20
+ [MESSAGE_ID]: 'Prefer putting the condition in the `while` statement.',
21
+ };
22
+
23
+ const isUnlabeledBreakStatement = node => node?.type === 'BreakStatement' && !node.label;
24
+ const isSwitchOrLoop = node => node.type === 'SwitchStatement' || isLoop(node);
25
+
26
+ const isNodeInsideRange = (node, [start, end], sourceCode) => {
27
+ const [nodeStart, nodeEnd] = sourceCode.getRange(node);
28
+ return nodeStart >= start && nodeEnd <= end;
29
+ };
30
+
31
+ const getOnlyUnlabeledBreakStatement = node => {
32
+ if (isUnlabeledBreakStatement(node)) {
33
+ return node;
34
+ }
35
+
36
+ if (node.type !== 'BlockStatement' || node.body.length !== 1) {
37
+ return;
38
+ }
39
+
40
+ const [statement] = node.body;
41
+ return isUnlabeledBreakStatement(statement) ? statement : undefined;
42
+ };
43
+
44
+ function hasUnlabeledBreakStatement(node, sourceCode) {
45
+ if (isUnlabeledBreakStatement(node)) {
46
+ return true;
47
+ }
48
+
49
+ if (isFunction(node) || isSwitchOrLoop(node)) {
50
+ return false;
51
+ }
52
+
53
+ for (const key of sourceCode.visitorKeys[node.type] ?? []) {
54
+ const value = node[key];
55
+
56
+ if (Array.isArray(value)) {
57
+ for (const element of value) {
58
+ if (
59
+ element
60
+ && hasUnlabeledBreakStatement(element, sourceCode)
61
+ ) {
62
+ return true;
63
+ }
64
+ }
65
+
66
+ continue;
67
+ }
68
+
69
+ if (
70
+ value
71
+ && hasUnlabeledBreakStatement(value, sourceCode)
72
+ ) {
73
+ return true;
74
+ }
75
+ }
76
+
77
+ return false;
78
+ }
79
+
80
+ const hasOtherBreakForSameLoop = (loop, sourceCode) =>
81
+ loop.body.body.slice(1).some(node => hasUnlabeledBreakStatement(node, sourceCode));
82
+
83
+ const isLabeledStatementBody = node => node.parent.type === 'LabeledStatement' && node.parent.body === node;
84
+
85
+ const isVariableDefinition = definition => definition.type === 'Variable' && definition.parent.kind === 'var';
86
+
87
+ const hasLoopBodyLexicalDefinition = (variable, loop, sourceCode) => {
88
+ const loopBodyRange = sourceCode.getRange(loop.body);
89
+ return variable.defs.some(definition =>
90
+ !isVariableDefinition(definition)
91
+ && isNodeInsideRange(definition.name, loopBodyRange, sourceCode));
92
+ };
93
+
94
+ const hasUnsafeLiftReference = (loop, firstStatement, sourceCode) => {
95
+ const testRange = sourceCode.getRange(firstStatement.test);
96
+ return getReferences(sourceCode.getScope(firstStatement.test)).some(reference =>
97
+ isNodeInsideRange(reference.identifier, testRange, sourceCode)
98
+ && reference.resolved
99
+ && hasLoopBodyLexicalDefinition(reference.resolved, loop, sourceCode));
100
+ };
101
+
102
+ const isInfiniteLoop = node => {
103
+ switch (node.type) {
104
+ case 'WhileStatement':
105
+ case 'DoWhileStatement': {
106
+ return isBooleanLiteral(node.test, true);
107
+ }
108
+
109
+ case 'ForStatement': {
110
+ return !node.init
111
+ && !node.update
112
+ && (!node.test || isBooleanLiteral(node.test, true));
113
+ }
114
+
115
+ default: {
116
+ return false;
117
+ }
118
+ }
119
+ };
120
+
121
+ const getLoopConditionText = (test, sourceCode) => {
122
+ if (
123
+ test.type === 'UnaryExpression'
124
+ && test.operator === '!'
125
+ ) {
126
+ return sourceCode.getText(test.argument);
127
+ }
128
+
129
+ const text = sourceCode.getText(test);
130
+ return shouldAddParenthesesToUnaryExpressionArgument(test, '!')
131
+ ? `!(${text})`
132
+ : `!${text}`;
133
+ };
134
+
135
+ const getLoopHeadRange = (node, sourceCode) => [
136
+ sourceCode.getRange(node)[0],
137
+ sourceCode.getRange(node.body)[0],
138
+ ];
139
+
140
+ const getDoWhileTailRange = (node, sourceCode) => [
141
+ sourceCode.getRange(node.body)[1],
142
+ sourceCode.getRange(node)[1],
143
+ ];
144
+
145
+ const getLoopBodyHeadRange = (loop, firstStatement, sourceCode) => [
146
+ sourceCode.getRange(loop.body)[0],
147
+ sourceCode.getRange(firstStatement)[0],
148
+ ];
149
+
150
+ function * fixLoop(fixer, {
151
+ loop,
152
+ firstStatement,
153
+ condition,
154
+ sourceCode,
155
+ context,
156
+ }) {
157
+ if (loop.type === 'WhileStatement') {
158
+ yield fixer.replaceText(loop.test, condition);
159
+ } else if (loop.type === 'ForStatement') {
160
+ yield fixer.replaceTextRange(getLoopHeadRange(loop, sourceCode), `while (${condition}) `);
161
+ } else {
162
+ yield fixer.replaceText(sourceCode.getFirstToken(loop), `while (${condition})`);
163
+ yield fixer.removeRange(getDoWhileTailRange(loop, sourceCode));
164
+ }
165
+
166
+ yield removeStatement(firstStatement, context, fixer);
167
+ }
168
+
169
+ /** @param {ESLint.Rule.RuleContext} context */
170
+ const create = context => {
171
+ const {sourceCode} = context;
172
+
173
+ context.on([
174
+ 'DoWhileStatement',
175
+ 'ForStatement',
176
+ 'WhileStatement',
177
+ ], node => {
178
+ const loopSyntaxCommentRanges = [
179
+ getLoopHeadRange(node, sourceCode),
180
+ ...(node.type === 'DoWhileStatement' ? [getDoWhileTailRange(node, sourceCode)] : []),
181
+ ];
182
+
183
+ if (
184
+ !isInfiniteLoop(node)
185
+ || node.body.type !== 'BlockStatement'
186
+ || isLabeledStatementBody(node)
187
+ || loopSyntaxCommentRanges.some(range => hasCommentInRange(context, range))
188
+ || (node.type === 'DoWhileStatement' && getLastTrailingCommentOnSameLine(context, node))
189
+ ) {
190
+ return;
191
+ }
192
+
193
+ const [firstStatement] = node.body.body;
194
+ if (
195
+ !firstStatement
196
+ || firstStatement.type !== 'IfStatement'
197
+ || firstStatement.alternate
198
+ || hasCommentInRange(context, getLoopBodyHeadRange(node, firstStatement, sourceCode))
199
+ || sourceCode.getCommentsInside(firstStatement).length > 0
200
+ || getLastTrailingCommentOnSameLine(context, firstStatement)
201
+ ) {
202
+ return;
203
+ }
204
+
205
+ if (
206
+ !getOnlyUnlabeledBreakStatement(firstStatement.consequent)
207
+ || hasOtherBreakForSameLoop(node, sourceCode)
208
+ || hasUnsafeLiftReference(node, firstStatement, sourceCode)
209
+ ) {
210
+ return;
211
+ }
212
+
213
+ return {
214
+ node: firstStatement,
215
+ messageId: MESSAGE_ID,
216
+ /** @param {ESLint.Rule.RuleFixer} fixer */
217
+ * fix(fixer) {
218
+ const fixes = fixLoop(fixer, {
219
+ loop: node,
220
+ firstStatement,
221
+ condition: getLoopConditionText(firstStatement.test, sourceCode),
222
+ sourceCode,
223
+ context,
224
+ });
225
+
226
+ for (const fix of fixes) {
227
+ yield fix;
228
+ }
229
+ },
230
+ };
231
+ });
232
+ };
233
+
234
+ /** @type {ESLint.Rule.RuleModule} */
235
+ const config = {
236
+ create,
237
+ meta: {
238
+ type: 'suggestion',
239
+ docs: {
240
+ description: 'Prefer putting the condition in the while statement.',
241
+ recommended: 'unopinionated',
242
+ },
243
+ fixable: 'code',
244
+ schema: [],
245
+ messages,
246
+ languages: [
247
+ 'js/js',
248
+ ],
249
+ },
250
+ };
251
+
252
+ export default config;
@@ -1,5 +1,10 @@
1
1
  import {isBooleanLiteral, isMethodCall, isStringLiteral} from './ast/index.js';
2
- import {getIndentString, getScopes, isLeftHandSide} from './utils/index.js';
2
+ import {
3
+ getIndentString,
4
+ getParenthesizedRange,
5
+ getScopes,
6
+ isLeftHandSide,
7
+ } from './utils/index.js';
3
8
 
4
9
  const MESSAGE_ID = 'require-passive-events';
5
10
  const messages = {
@@ -168,8 +173,10 @@ const isEventParameterSafe = (listener, context) => {
168
173
  return true;
169
174
  };
170
175
 
171
- const fixMissingOptions = listener => fixer =>
172
- fixer.insertTextAfter(listener, ', {passive: true}');
176
+ const fixMissingOptions = (listener, context) => fixer =>
177
+ // Insert after any parentheses around the listener so the option isn't swallowed
178
+ // into a sequence expression, e.g. `((() => {}, {passive: true}))`.
179
+ fixer.insertTextAfterRange(getParenthesizedRange(listener, context), ', {passive: true}');
173
180
 
174
181
  const fixBooleanOptions = optionsNode => fixer =>
175
182
  fixer.replaceText(
@@ -203,7 +210,7 @@ const fixPassiveFalse = passiveProperty => fixer =>
203
210
  const getOptionsProblem = (context, callExpression, optionsNode) => {
204
211
  if (!optionsNode) {
205
212
  return {
206
- fix: fixMissingOptions(callExpression.arguments[1]),
213
+ fix: fixMissingOptions(callExpression.arguments[1], context),
207
214
  };
208
215
  }
209
216