eslint-plugin-unicorn 69.0.0 → 70.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 (44) hide show
  1. package/package.json +1 -1
  2. package/readme.md +10 -0
  3. package/rules/consistent-boolean-name.js +637 -36
  4. package/rules/expiring-todo-comments.js +23 -0
  5. package/rules/fix/{add-parenthesizes-to-return-or-throw-expression.js → add-parentheses-to-return-or-throw-expression.js} +1 -1
  6. package/rules/fix/index.js +1 -1
  7. package/rules/fix/switch-new-expression-to-call-expression.js +2 -2
  8. package/rules/index.js +10 -0
  9. package/rules/isolated-functions.js +156 -12
  10. package/rules/no-async-promise-finally.js +111 -0
  11. package/rules/no-collection-bracket-access.js +27 -4
  12. package/rules/no-manually-wrapped-comments.js +5 -1
  13. package/rules/no-negated-array-predicate.js +2 -2
  14. package/rules/no-negated-comparison.js +24 -61
  15. package/rules/no-negated-condition.js +2 -2
  16. package/rules/no-negation-in-equality-check.js +2 -2
  17. package/rules/no-non-function-verb-prefix.js +25 -2
  18. package/rules/no-return-array-push.js +9 -2
  19. package/rules/no-typeof-undefined.js +2 -2
  20. package/rules/no-unnecessary-array-flat-map.js +307 -0
  21. package/rules/no-unnecessary-await.js +2 -2
  22. package/rules/no-unnecessary-fetch-options.js +624 -0
  23. package/rules/no-unsafe-promise-all-settled-values.js +865 -0
  24. package/rules/no-useless-else.js +32 -1
  25. package/rules/no-useless-iterator-to-array.js +42 -16
  26. package/rules/no-useless-spread.js +2 -2
  27. package/rules/prefer-abort-signal-any.js +1391 -0
  28. package/rules/prefer-array-flat-map.js +6 -11
  29. package/rules/prefer-array-from-range.js +98 -0
  30. package/rules/prefer-block-statement-over-iife.js +159 -0
  31. package/rules/prefer-continue.js +6 -2
  32. package/rules/prefer-early-return.js +3 -1
  33. package/rules/prefer-group-by.js +576 -0
  34. package/rules/prefer-iterator-helpers.js +196 -0
  35. package/rules/prefer-number-coercion.js +1 -1
  36. package/rules/prefer-object-iterable-methods.js +57 -14
  37. package/rules/prefer-simplified-conditions.js +584 -0
  38. package/rules/prefer-spread.js +3 -0
  39. package/rules/prefer-string-raw.js +2 -2
  40. package/rules/shared/array-range.js +66 -0
  41. package/rules/shared/iterator-helpers.js +43 -1
  42. package/rules/utils/comparison.js +65 -0
  43. package/rules/utils/is-boolean.js +21 -6
  44. package/rules/utils/track-branch-exits.js +8 -7
@@ -1,13 +1,21 @@
1
1
  import {
2
2
  removeParentheses,
3
3
  fixSpaceAroundKeyword,
4
- addParenthesizesToReturnOrThrowExpression,
4
+ addParenthesesToReturnOrThrowExpression,
5
5
  } from './fix/index.js';
6
6
  import {
7
7
  isParenthesized,
8
8
  isOnSameLine,
9
9
  needsSemicolon,
10
10
  } from './utils/index.js';
11
+ import {
12
+ getBinaryExpressionWithReplacedOperatorText,
13
+ getPunctuatorBinaryExpressionOperatorToken,
14
+ hasLowerLogicalOperatorPrecedence,
15
+ negatedComparisonOperators,
16
+ negatedEqualityOperators,
17
+ negatedLogicalOperators,
18
+ } from './utils/comparison.js';
11
19
 
12
20
  const MESSAGE_ID_ERROR = 'no-negated-comparison/error';
13
21
  const MESSAGE_ID_LOGICAL_ERROR = 'no-negated-comparison/logical-error';
@@ -20,29 +28,6 @@ const messages = {
20
28
  [MESSAGE_ID_LOGICAL_SUGGESTION]: 'Switch to opposite comparisons.',
21
29
  };
22
30
 
23
- const operatorReplacements = new Map([
24
- ['===', '!=='],
25
- ['!==', '==='],
26
- ['==', '!='],
27
- ['!=', '=='],
28
- ['>', '<='],
29
- ['>=', '<'],
30
- ['<', '>='],
31
- ['<=', '>'],
32
- ]);
33
-
34
- const safelyFixableOperators = new Set([
35
- '===',
36
- '!==',
37
- '==',
38
- '!=',
39
- ]);
40
-
41
- const logicalOperatorReplacements = new Map([
42
- ['&&', '||'],
43
- ['||', '&&'],
44
- ]);
45
-
46
31
  const defaultOptions = {
47
32
  checkLogicalExpressions: false,
48
33
  };
@@ -72,20 +57,20 @@ const isNegation = node =>
72
57
 
73
58
  const isComparison = node =>
74
59
  node.type === 'BinaryExpression'
75
- && operatorReplacements.has(node.operator);
60
+ && negatedComparisonOperators.has(node.operator);
76
61
 
77
62
  const isLogicalExpressionWithOnlyComparisons = node =>
78
63
  isComparison(node)
79
64
  || (
80
65
  node.type === 'LogicalExpression'
81
- && logicalOperatorReplacements.has(node.operator)
66
+ && negatedLogicalOperators.has(node.operator)
82
67
  && isLogicalExpressionWithOnlyComparisons(node.left)
83
68
  && isLogicalExpressionWithOnlyComparisons(node.right)
84
69
  );
85
70
 
86
71
  const isSafelyFixableLogicalExpression = node =>
87
72
  isComparison(node)
88
- ? safelyFixableOperators.has(node.operator)
73
+ ? negatedEqualityOperators.has(node.operator)
89
74
  : isSafelyFixableLogicalExpression(node.left) && isSafelyFixableLogicalExpression(node.right);
90
75
 
91
76
  const parentNeedsGroupedComparison = parent => [
@@ -111,10 +96,7 @@ function * fix({
111
96
  const bangToken = sourceCode.getFirstToken(unaryExpression);
112
97
  const tokenAfterBang = sourceCode.getTokenAfter(bangToken);
113
98
  const tokenAfterBangIncludingComments = sourceCode.getTokenAfter(bangToken, {includeComments: true});
114
- const operatorToken = sourceCode.getTokenAfter(
115
- comparison.left,
116
- token => token.type === 'Punctuator' && token.value === comparison.operator,
117
- );
99
+ const operatorToken = getPunctuatorBinaryExpressionOperatorToken(comparison, context);
118
100
  const {parent} = unaryExpression;
119
101
  const needsReturnOrThrowParentheses = (
120
102
  (parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
@@ -145,7 +127,7 @@ function * fix({
145
127
  yield fixer.replaceText(operatorToken, replacementOperator);
146
128
 
147
129
  if (needsReturnOrThrowParentheses) {
148
- yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
130
+ yield addParenthesesToReturnOrThrowExpression(fixer, parent, context);
149
131
  return;
150
132
  }
151
133
 
@@ -156,37 +138,18 @@ function * fix({
156
138
  }
157
139
  }
158
140
 
159
- const logicalOperatorPrecedence = {
160
- '||': 1,
161
- '&&': 2,
162
- };
163
-
164
- const getOperatorToken = (comparison, sourceCode) => sourceCode.getTokenAfter(
165
- comparison.left,
166
- token => token.type === 'Punctuator' && token.value === comparison.operator,
141
+ const getFixedComparisonText = (comparison, context) => getBinaryExpressionWithReplacedOperatorText(
142
+ comparison,
143
+ context,
144
+ negatedComparisonOperators.get(comparison.operator),
167
145
  );
168
146
 
169
- const getFixedComparisonText = (comparison, sourceCode) => {
170
- const operatorToken = getOperatorToken(comparison, sourceCode);
171
- const [comparisonStart] = sourceCode.getRange(comparison);
172
- const [operatorStart, operatorEnd] = sourceCode.getRange(operatorToken);
173
- const comparisonText = sourceCode.getText(comparison);
174
-
175
- return [
176
- comparisonText.slice(0, operatorStart - comparisonStart),
177
- operatorReplacements.get(comparison.operator),
178
- comparisonText.slice(operatorEnd - comparisonStart),
179
- ].join('');
180
- };
181
-
182
147
  const getFixedLogicalExpressionText = (node, context, parentOperator) => {
183
- const {sourceCode} = context;
184
-
185
148
  if (isComparison(node)) {
186
- return getFixedComparisonText(node, sourceCode);
149
+ return getFixedComparisonText(node, context);
187
150
  }
188
151
 
189
- const operator = logicalOperatorReplacements.get(node.operator);
152
+ const operator = negatedLogicalOperators.get(node.operator);
190
153
  const text = [
191
154
  getFixedLogicalExpressionText(node.left, context, operator),
192
155
  operator,
@@ -197,7 +160,7 @@ const getFixedLogicalExpressionText = (node, context, parentOperator) => {
197
160
  operator !== parentOperator
198
161
  && isParenthesized(node, context)
199
162
  )
200
- || logicalOperatorPrecedence[operator] < logicalOperatorPrecedence[parentOperator]
163
+ || hasLowerLogicalOperatorPrecedence(operator, parentOperator)
201
164
  );
202
165
 
203
166
  return needsParentheses
@@ -230,7 +193,7 @@ function * fixLogical({
230
193
  yield fixer.replaceText(logicalExpression, getFixedLogicalExpressionText(logicalExpression, context));
231
194
 
232
195
  if (needsReturnOrThrowParentheses) {
233
- yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
196
+ yield addParenthesesToReturnOrThrowExpression(fixer, parent, context);
234
197
  return;
235
198
  }
236
199
 
@@ -253,7 +216,7 @@ const create = context => {
253
216
 
254
217
  if (isComparison(argument)) {
255
218
  const comparison = argument;
256
- const replacementOperator = operatorReplacements.get(comparison.operator);
219
+ const replacementOperator = negatedComparisonOperators.get(comparison.operator);
257
220
  const problem = {
258
221
  node: unaryExpression,
259
222
  messageId: MESSAGE_ID_ERROR,
@@ -267,7 +230,7 @@ const create = context => {
267
230
  replacementOperator,
268
231
  });
269
232
 
270
- if (safelyFixableOperators.has(comparison.operator)) {
233
+ if (negatedEqualityOperators.has(comparison.operator)) {
271
234
  problem.fix = fixFunction;
272
235
  } else {
273
236
  problem.suggest = [
@@ -5,7 +5,7 @@ https://github.com/eslint/eslint/blob/5c39425fc55ecc0b97bbd07ac22654c0eb4f789c/l
5
5
  import {
6
6
  removeParentheses,
7
7
  fixSpaceAroundKeyword,
8
- addParenthesizesToReturnOrThrowExpression,
8
+ addParenthesesToReturnOrThrowExpression,
9
9
  } from './fix/index.js';
10
10
  import {
11
11
  getParenthesizedRange,
@@ -118,7 +118,7 @@ const create = context => {
118
118
  && !isParenthesized(node, context)
119
119
  && !isParenthesized(test, context)
120
120
  ) {
121
- yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
121
+ yield addParenthesesToReturnOrThrowExpression(fixer, parent, context);
122
122
  return;
123
123
  }
124
124
 
@@ -1,4 +1,4 @@
1
- import {fixSpaceAroundKeyword, addParenthesizesToReturnOrThrowExpression} from './fix/index.js';
1
+ import {fixSpaceAroundKeyword, addParenthesesToReturnOrThrowExpression} from './fix/index.js';
2
2
  import {needsSemicolon, isParenthesized, isOnSameLine} from './utils/index.js';
3
3
 
4
4
  const MESSAGE_ID_ERROR = 'no-negation-in-equality-check/error';
@@ -58,7 +58,7 @@ const create = context => {
58
58
  ) {
59
59
  const returnToken = sourceCode.getFirstToken(parent);
60
60
  if (!isOnSameLine(returnToken, tokenAfterBang, context)) {
61
- yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
61
+ yield addParenthesesToReturnOrThrowExpression(fixer, parent, context);
62
62
  }
63
63
  }
64
64
 
@@ -1,3 +1,4 @@
1
+ import {isRegExp} from 'node:util/types';
1
2
  import {
2
3
  isNullishType,
3
4
  isTypeScriptFile,
@@ -31,8 +32,12 @@ const messages = {
31
32
 
32
33
  const isUppercaseAscii = character => character >= 'A' && character <= 'Z';
33
34
 
34
- const prepareOptions = ({verbs}) => ({
35
+ const prepareOptions = ({
36
+ verbs,
37
+ ignore = [],
38
+ }) => ({
35
39
  verbs: verbs.toSorted((first, second) => second.length - first.length),
40
+ ignore: ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u')),
36
41
  });
37
42
 
38
43
  function getVerbPrefix(name, verbs) {
@@ -129,6 +134,15 @@ const getIntersectionTypeCallability = (type, checker, program, visitedTypes) =>
129
134
  const isInDeclaredModule = (node, sourceCode) =>
130
135
  sourceCode.getAncestors(node).some(ancestor => ancestor.type === 'TSModuleDeclaration' && ancestor.declare);
131
136
 
137
+ function isIgnoredName(name, ignore) {
138
+ return ignore.some(regexp => {
139
+ regexp.lastIndex = 0;
140
+ const isIgnored = regexp.test(name);
141
+ regexp.lastIndex = 0;
142
+ return isIgnored;
143
+ });
144
+ }
145
+
132
146
  function getTypeCallability(type, checker, program, visitedTypes = new Set()) {
133
147
  if (isIgnoredType(type)) {
134
148
  return unknown;
@@ -184,6 +198,10 @@ function getProblem(identifier, context, options, typeNode = identifier) {
184
198
  return;
185
199
  }
186
200
 
201
+ if (isIgnoredName(identifier.name, options.ignore)) {
202
+ return;
203
+ }
204
+
187
205
  const {parserServices} = context.sourceCode;
188
206
  let type;
189
207
  try {
@@ -298,6 +316,11 @@ const schema = [
298
316
  uniqueItems: true,
299
317
  description: 'Function-style verb prefixes to check.',
300
318
  },
319
+ ignore: {
320
+ type: 'array',
321
+ uniqueItems: true,
322
+ description: 'Patterns to ignore.',
323
+ },
301
324
  },
302
325
  },
303
326
  ];
@@ -313,7 +336,7 @@ const config = {
313
336
  requiresTypeChecking: true,
314
337
  },
315
338
  schema,
316
- defaultOptions: [{verbs: defaultVerbs}],
339
+ defaultOptions: [{verbs: defaultVerbs, ignore: []}],
317
340
  messages,
318
341
  languages: [
319
342
  'js/js',
@@ -1,5 +1,5 @@
1
1
  import {isMethodCall} from './ast/index.js';
2
- import {isKnownNonArray, needsSemicolon} from './utils/index.js';
2
+ import {isArray, isKnownNonArray, needsSemicolon} from './utils/index.js';
3
3
 
4
4
  const MESSAGE_ID_ERROR = 'no-return-array-push/error';
5
5
  const MESSAGE_ID_SUGGESTION = 'no-return-array-push/suggestion';
@@ -10,7 +10,10 @@ const messages = {
10
10
 
11
11
  const ignoredCallees = [
12
12
  'stream.push',
13
+ 'router.push',
13
14
  'this.push',
15
+ 'this.router.push',
16
+ 'this.$router.push',
14
17
  'this.stream.push',
15
18
  'process.stdin.push',
16
19
  'process.stdout.push',
@@ -53,6 +56,10 @@ function isStaticMemberPath(node, path) {
53
56
 
54
57
  const isIgnoredCallee = callee => ignoredCallees.some(ignoredCallee => isStaticMemberPath(callee, ignoredCallee));
55
58
 
59
+ const isIgnoredPushCallee = (callExpression, context) =>
60
+ isIgnoredCallee(callExpression.callee)
61
+ && !isArray(callExpression.callee.object, context);
62
+
56
63
  function getCallExpressionResultNode(callExpression) {
57
64
  let node = callExpression;
58
65
 
@@ -133,7 +140,7 @@ const create = context => {
133
140
  const {name: method} = property;
134
141
 
135
142
  if (
136
- (method === 'push' && isIgnoredCallee(callExpression.callee))
143
+ (method === 'push' && isIgnoredPushCallee(callExpression, context))
137
144
  || isReturnValueDiscarded(callExpression)
138
145
  || isResultMemberAccessed(callExpression)
139
146
  || isKnownNonArray(callExpression.callee.object, context)
@@ -1,6 +1,6 @@
1
1
  import {isLiteral} from './ast/index.js';
2
2
  import {
3
- addParenthesizesToReturnOrThrowExpression,
3
+ addParenthesesToReturnOrThrowExpression,
4
4
  removeSpacesAfter,
5
5
  } from './fix/index.js';
6
6
  import {
@@ -74,7 +74,7 @@ const create = context => {
74
74
  && !isParenthesized(binaryExpression, context)
75
75
  && !isParenthesized(typeofNode, context)
76
76
  ) {
77
- yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
77
+ yield addParenthesesToReturnOrThrowExpression(fixer, parent, context);
78
78
  return;
79
79
  }
80
80
 
@@ -0,0 +1,307 @@
1
+ import {hasSideEffect} from '@eslint-community/eslint-utils';
2
+ import {isMethodCall} from './ast/index.js';
3
+ import {
4
+ getParenthesizedText,
5
+ isKnownNonArray,
6
+ isParenthesized,
7
+ isSameIdentifier,
8
+ shouldAddParenthesesToMemberExpressionObject,
9
+ wouldRemoveComments,
10
+ } from './utils/index.js';
11
+
12
+ const MESSAGE_ID = 'no-unnecessary-array-flat-map';
13
+ const SUGGESTION_ID_FILTER_MAP = 'no-unnecessary-array-flat-map/filter-map-suggestion';
14
+
15
+ const messages = {
16
+ [MESSAGE_ID]: 'Prefer `.{{method}}(…)` over `.flatMap(…)` for this single-item array callback.',
17
+ [SUGGESTION_ID_FILTER_MAP]: 'Replace `.flatMap(…)` with `.filter(…).map(…)`.',
18
+ };
19
+
20
+ const arrowBodyNeedsParenthesesTypes = new Set([
21
+ 'ObjectExpression',
22
+ 'SequenceExpression',
23
+ ]);
24
+
25
+ const typescriptExpressionWrapperTypes = new Set([
26
+ 'TSAsExpression',
27
+ 'TSNonNullExpression',
28
+ 'TSSatisfiesExpression',
29
+ 'TSTypeAssertion',
30
+ ]);
31
+
32
+ const hasTypeArguments = node => node.typeArguments || node.typeParameters;
33
+
34
+ const isFilterCallExpression = node => isMethodCall(node, {
35
+ method: 'filter',
36
+ argumentsLength: 1,
37
+ optionalCall: false,
38
+ optionalMember: false,
39
+ computed: false,
40
+ });
41
+
42
+ const isSimpleSingleParameterArrowCallback = node =>
43
+ node.type === 'ArrowFunctionExpression'
44
+ && !node.async
45
+ && !node.returnType
46
+ && !node.typeParameters
47
+ && node.params.length === 1
48
+ && node.params[0].type === 'Identifier'
49
+ && !node.params[0].optional
50
+ && node.body.type !== 'BlockStatement';
51
+
52
+ const isEmptyArrayExpression = node =>
53
+ node.type === 'ArrayExpression'
54
+ && node.elements.length === 0;
55
+
56
+ const getSingleArrayElement = node => {
57
+ if (
58
+ node.type !== 'ArrayExpression'
59
+ || node.elements.length !== 1
60
+ || !node.elements[0]
61
+ || node.elements[0].type === 'SpreadElement'
62
+ ) {
63
+ return;
64
+ }
65
+
66
+ return node.elements[0];
67
+ };
68
+
69
+ function shouldParenthesizeArrowBody(node, context) {
70
+ if (isParenthesized(node, context)) {
71
+ return false;
72
+ }
73
+
74
+ if (arrowBodyNeedsParenthesesTypes.has(node.type)) {
75
+ return true;
76
+ }
77
+
78
+ return typescriptExpressionWrapperTypes.has(node.type) && shouldParenthesizeArrowBody(node.expression, context);
79
+ }
80
+
81
+ function getCallbackResult(callback) {
82
+ const directElement = getSingleArrayElement(callback.body);
83
+ if (directElement) {
84
+ return {
85
+ type: 'map',
86
+ element: directElement,
87
+ arrayExpression: callback.body,
88
+ };
89
+ }
90
+
91
+ if (callback.body.type !== 'ConditionalExpression') {
92
+ return;
93
+ }
94
+
95
+ const {test, consequent, alternate} = callback.body;
96
+ const element = getSingleArrayElement(consequent);
97
+ if (!element || !isEmptyArrayExpression(alternate)) {
98
+ return;
99
+ }
100
+
101
+ return {
102
+ type: 'conditional',
103
+ test,
104
+ element,
105
+ arrayExpression: consequent,
106
+ };
107
+ }
108
+
109
+ function getArrowBodyText(node, context) {
110
+ if (isParenthesized(node, context)) {
111
+ return getParenthesizedText(node, context);
112
+ }
113
+
114
+ const text = context.sourceCode.getText(node);
115
+ return shouldParenthesizeArrowBody(node, context)
116
+ ? `(${text})`
117
+ : text;
118
+ }
119
+
120
+ function getMemberExpressionObjectText(node, context) {
121
+ if (node.type === 'Super') {
122
+ return 'super';
123
+ }
124
+
125
+ if (isParenthesized(node, context)) {
126
+ return getParenthesizedText(node, context);
127
+ }
128
+
129
+ const text = context.sourceCode.getText(node);
130
+ return shouldAddParenthesesToMemberExpressionObject(node, context) ? `(${text})` : text;
131
+ }
132
+
133
+ function getArrowParameterText(callback, context) {
134
+ const parameterText = context.sourceCode.getText(callback.params[0]);
135
+ return callback.params[0].typeAnnotation ? `(${parameterText})` : parameterText;
136
+ }
137
+
138
+ function getFilterMapSuggestion(flatMapCallExpression, callback, callbackResult, context) {
139
+ if (
140
+ hasTypeArguments(flatMapCallExpression)
141
+ || callback.params[0].typeAnnotation
142
+ || wouldRemoveComments(context, flatMapCallExpression, [
143
+ flatMapCallExpression.callee.object,
144
+ callbackResult.test,
145
+ callbackResult.element,
146
+ ])
147
+ || hasSideEffect(callbackResult.test, context.sourceCode)
148
+ || hasSideEffect(callbackResult.element, context.sourceCode)
149
+ ) {
150
+ return;
151
+ }
152
+
153
+ const arrayText = getMemberExpressionObjectText(flatMapCallExpression.callee.object, context);
154
+ const parameterText = getArrowParameterText(callback, context);
155
+ const testText = getArrowBodyText(callbackResult.test, context);
156
+ const elementText = getArrowBodyText(callbackResult.element, context);
157
+
158
+ return {
159
+ messageId: SUGGESTION_ID_FILTER_MAP,
160
+ fix: fixer => fixer.replaceText(
161
+ flatMapCallExpression,
162
+ `${arrayText}.filter(${parameterText} => ${testText}).map(${parameterText} => ${elementText})`,
163
+ ),
164
+ };
165
+ }
166
+
167
+ function getProblemForFilterFlatMap(flatMapCallExpression, callbackResult, context) {
168
+ const filterCallExpression = flatMapCallExpression.callee.object;
169
+ if (!isFilterCallExpression(filterCallExpression)) {
170
+ return;
171
+ }
172
+
173
+ const problem = {
174
+ node: flatMapCallExpression.callee.property,
175
+ messageId: MESSAGE_ID,
176
+ data: {method: 'map'},
177
+ };
178
+
179
+ if (
180
+ hasTypeArguments(filterCallExpression)
181
+ || hasTypeArguments(flatMapCallExpression)
182
+ || isKnownNonArray(filterCallExpression.callee.object, context)
183
+ || wouldRemoveComments(context, callbackResult.arrayExpression, [callbackResult.element])
184
+ ) {
185
+ return problem;
186
+ }
187
+
188
+ return {
189
+ ...problem,
190
+ * fix(fixer) {
191
+ yield fixer.replaceText(flatMapCallExpression.callee.property, 'map');
192
+ yield fixer.replaceText(callbackResult.arrayExpression, getArrowBodyText(callbackResult.element, context));
193
+ },
194
+ };
195
+ }
196
+
197
+ function getProblemForConditionalFlatMap(flatMapCallExpression, callback, callbackResult, context) {
198
+ const method = isSameIdentifier(callbackResult.element, callback.params[0]) ? 'filter' : 'filter().map';
199
+ const problem = {
200
+ node: flatMapCallExpression.callee.property,
201
+ messageId: MESSAGE_ID,
202
+ data: {method},
203
+ };
204
+
205
+ if (method === 'filter') {
206
+ if (
207
+ hasTypeArguments(flatMapCallExpression)
208
+ || hasSideEffect(callbackResult.test, context.sourceCode)
209
+ || wouldRemoveComments(context, flatMapCallExpression, [
210
+ flatMapCallExpression.callee.object,
211
+ callback.params[0],
212
+ callbackResult.test,
213
+ ])
214
+ ) {
215
+ return problem;
216
+ }
217
+
218
+ const arrayText = getMemberExpressionObjectText(flatMapCallExpression.callee.object, context);
219
+ const parameterText = getArrowParameterText(callback, context);
220
+ const testText = getArrowBodyText(callbackResult.test, context);
221
+
222
+ return {
223
+ ...problem,
224
+ fix: fixer => fixer.replaceText(
225
+ flatMapCallExpression,
226
+ `${arrayText}.filter(${parameterText} => ${testText})`,
227
+ ),
228
+ };
229
+ }
230
+
231
+ const suggestion = getFilterMapSuggestion(flatMapCallExpression, callback, callbackResult, context);
232
+ return suggestion
233
+ ? {
234
+ ...problem,
235
+ suggest: [suggestion],
236
+ }
237
+ : problem;
238
+ }
239
+
240
+ function getProblem(flatMapCallExpression, context) {
241
+ if (
242
+ !isMethodCall(flatMapCallExpression, {
243
+ method: 'flatMap',
244
+ argumentsLength: 1,
245
+ optionalCall: false,
246
+ optionalMember: false,
247
+ computed: false,
248
+ })
249
+ || isKnownNonArray(flatMapCallExpression.callee.object, context)
250
+ ) {
251
+ return;
252
+ }
253
+
254
+ const filterCallExpression = flatMapCallExpression.callee.object;
255
+ if (
256
+ isFilterCallExpression(filterCallExpression)
257
+ && isKnownNonArray(filterCallExpression.callee.object, context)
258
+ ) {
259
+ return;
260
+ }
261
+
262
+ const [callback] = flatMapCallExpression.arguments;
263
+ if (!isSimpleSingleParameterArrowCallback(callback)) {
264
+ return;
265
+ }
266
+
267
+ const callbackResult = getCallbackResult(callback);
268
+ if (!callbackResult) {
269
+ return;
270
+ }
271
+
272
+ if (callbackResult.type === 'map') {
273
+ return getProblemForFilterFlatMap(flatMapCallExpression, callbackResult, context) ?? {
274
+ node: flatMapCallExpression.callee.property,
275
+ messageId: MESSAGE_ID,
276
+ data: {method: 'map'},
277
+ };
278
+ }
279
+
280
+ return getProblemForConditionalFlatMap(flatMapCallExpression, callback, callbackResult, context);
281
+ }
282
+
283
+ /** @param {import('eslint').Rule.RuleContext} context */
284
+ const create = context => {
285
+ context.on('CallExpression', callExpression => getProblem(callExpression, context));
286
+ };
287
+
288
+ /** @type {import('eslint').Rule.RuleModule} */
289
+ const config = {
290
+ create,
291
+ meta: {
292
+ type: 'suggestion',
293
+ docs: {
294
+ description: 'Disallow `Array#flatMap()` callbacks that only wrap a single item.',
295
+ recommended: true,
296
+ },
297
+ fixable: 'code',
298
+ hasSuggestions: true,
299
+ schema: [],
300
+ messages,
301
+ languages: [
302
+ 'js/js',
303
+ ],
304
+ },
305
+ };
306
+
307
+ export default config;
@@ -1,4 +1,4 @@
1
- import {addParenthesizesToReturnOrThrowExpression, removeSpacesAfter} from './fix/index.js';
1
+ import {addParenthesesToReturnOrThrowExpression, removeSpacesAfter} from './fix/index.js';
2
2
  import {isParenthesized, needsSemicolon, isOnSameLine} from './utils/index.js';
3
3
 
4
4
  const MESSAGE_ID = 'no-unnecessary-await';
@@ -74,7 +74,7 @@ const create = context => {
74
74
  !isOnSameLine(awaitToken, valueNode, context)
75
75
  && !isParenthesized(node, context)
76
76
  ) {
77
- yield addParenthesizesToReturnOrThrowExpression(fixer, node.parent, context);
77
+ yield addParenthesesToReturnOrThrowExpression(fixer, node.parent, context);
78
78
  }
79
79
 
80
80
  yield fixer.remove(awaitToken);