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
@@ -23,6 +23,7 @@ const MESSAGE_ID_HAVE_PACKAGE = 'unicorn/havePackage';
23
23
  const MESSAGE_ID_DONT_HAVE_PACKAGE = 'unicorn/dontHavePackage';
24
24
  const MESSAGE_ID_VERSION_MATCHES = 'unicorn/versionMatches';
25
25
  const MESSAGE_ID_PEER_VERSION_MATCHES = 'unicorn/peerVersionMatches';
26
+ const MESSAGE_ID_UNSUPPORTED_CATALOG_PROTOCOL = 'unicorn/unsupportedCatalogProtocol';
26
27
  const MESSAGE_ID_ENGINE_MATCHES = 'unicorn/engineMatches';
27
28
  const MESSAGE_ID_REMOVE_WHITESPACE = 'unicorn/removeWhitespaces';
28
29
  const MESSAGE_ID_MISSING_AT_SYMBOL = 'unicorn/missingAtSymbol';
@@ -46,6 +47,8 @@ const messages = {
46
47
  'Due since package version matched: {{comparison}}. {{message}}',
47
48
  [MESSAGE_ID_PEER_VERSION_MATCHES]:
48
49
  'Due since peer dependency version matched: {{comparison}}. {{message}}',
50
+ [MESSAGE_ID_UNSUPPORTED_CATALOG_PROTOCOL]:
51
+ 'Cannot check {{dependencyType}} version because {{package}} uses the unsupported `catalog:` protocol. {{message}}',
49
52
  [MESSAGE_ID_ENGINE_MATCHES]:
50
53
  'Due since Node.js version matched: {{comparison}}. {{message}}',
51
54
  [MESSAGE_ID_REMOVE_WHITESPACE]:
@@ -284,6 +287,10 @@ function getRangeFloor(range) {
284
287
  return semver.validRange(range) ? semver.minVersion(range) : undefined;
285
288
  }
286
289
 
290
+ function isCatalogProtocol(rawVersion) {
291
+ return typeof rawVersion === 'string' && rawVersion.startsWith('catalog:');
292
+ }
293
+
287
294
  const DEFAULT_OPTIONS = {
288
295
  terms: ['todo', 'fixme', 'xxx'],
289
296
  ignore: [],
@@ -480,6 +487,14 @@ const create = context => {
480
487
  continue;
481
488
  }
482
489
 
490
+ if (isCatalogProtocol(targetPackageRawVersion)) {
491
+ report(MESSAGE_ID_UNSUPPORTED_CATALOG_PROTOCOL, {
492
+ dependencyType: 'dependency',
493
+ package: dependency.name,
494
+ });
495
+ continue;
496
+ }
497
+
483
498
  const targetPackageVersion = tryToCoerceVersion(targetPackageRawVersion);
484
499
 
485
500
  /* c8 ignore start */
@@ -505,6 +520,14 @@ const create = context => {
505
520
  continue;
506
521
  }
507
522
 
523
+ if (isCatalogProtocol(targetPeerRawVersion)) {
524
+ report(MESSAGE_ID_UNSUPPORTED_CATALOG_PROTOCOL, {
525
+ dependencyType: 'peer dependency',
526
+ package: peerDependency.name,
527
+ });
528
+ continue;
529
+ }
530
+
508
531
  const targetPeerVersion = getRangeFloor(targetPeerRawVersion);
509
532
 
510
533
  if (targetPeerVersion && satisfiesRange(targetPeerVersion, peerDependency.condition, peerDependency.version)) {
@@ -11,7 +11,7 @@ import {isSemicolonToken} from '@eslint-community/eslint-utils';
11
11
  @param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
12
12
  @returns {ESLint.Rule.ReportFixer}
13
13
  */
14
- export default function * addParenthesizesToReturnOrThrowExpression(fixer, node, context) {
14
+ export default function * addParenthesesToReturnOrThrowExpression(fixer, node, context) {
15
15
  if (node.type !== 'ReturnStatement' && node.type !== 'ThrowStatement') {
16
16
  return;
17
17
  }
@@ -21,4 +21,4 @@ export {default as replaceReferenceIdentifier} from './replace-reference-identif
21
21
  export {default as replaceNodeOrTokenAndSpacesBefore} from './replace-node-or-token-and-spaces-before.js';
22
22
  export {default as fixSpaceAroundKeyword} from './fix-space-around-keywords.js';
23
23
  export {default as replaceStringRaw} from './replace-string-raw.js';
24
- export {default as addParenthesizesToReturnOrThrowExpression} from './add-parenthesizes-to-return-or-throw-expression.js';
24
+ export {default as addParenthesesToReturnOrThrowExpression} from './add-parentheses-to-return-or-throw-expression.js';
@@ -3,7 +3,7 @@ import {
3
3
  isParenthesized,
4
4
  isOnSameLine,
5
5
  } from '../utils/index.js';
6
- import addParenthesizesToReturnOrThrowExpression from './add-parenthesizes-to-return-or-throw-expression.js';
6
+ import addParenthesesToReturnOrThrowExpression from './add-parentheses-to-return-or-throw-expression.js';
7
7
  import removeSpaceAfter from './remove-spaces-after.js';
8
8
 
9
9
  /**
@@ -39,6 +39,6 @@ export default function * switchNewExpressionToCallExpression(newExpression, con
39
39
  if (!isOnSameLine(newToken, newExpression.callee, context) && !isParenthesized(newExpression, context)) {
40
40
  // Ideally, we should use first parenthesis of the `callee`, and should check spaces after the `new` token
41
41
  // But adding extra parentheses is harmless, no need to be too complicated
42
- yield addParenthesizesToReturnOrThrowExpression(fixer, newExpression.parent, context);
42
+ yield addParenthesesToReturnOrThrowExpression(fixer, newExpression.parent, context);
43
43
  }
44
44
  }
package/rules/index.js CHANGED
@@ -53,6 +53,7 @@ export {default as 'no-array-sort-for-min-max'} from './no-array-sort-for-min-ma
53
53
  export {default as 'no-array-sort'} from './no-array-sort.js';
54
54
  export {default as 'no-array-splice'} from './no-array-splice.js';
55
55
  export {default as 'no-asterisk-prefix-in-documentation-comments'} from './no-asterisk-prefix-in-documentation-comments.js';
56
+ export {default as 'no-async-promise-finally'} from './no-async-promise-finally.js';
56
57
  export {default as 'no-await-expression-member'} from './no-await-expression-member.js';
57
58
  export {default as 'no-await-in-promise-methods'} from './no-await-in-promise-methods.js';
58
59
  export {default as 'no-blob-to-file'} from './no-blob-to-file.js';
@@ -129,9 +130,11 @@ export {default as 'no-typeof-undefined'} from './no-typeof-undefined.js';
129
130
  export {default as 'no-uncalled-method'} from './no-uncalled-method.js';
130
131
  export {default as 'no-undeclared-class-members'} from './no-undeclared-class-members.js';
131
132
  export {default as 'no-unnecessary-array-flat-depth'} from './no-unnecessary-array-flat-depth.js';
133
+ export {default as 'no-unnecessary-array-flat-map'} from './no-unnecessary-array-flat-map.js';
132
134
  export {default as 'no-unnecessary-array-splice-count'} from './no-unnecessary-array-splice-count.js';
133
135
  export {default as 'no-unnecessary-await'} from './no-unnecessary-await.js';
134
136
  export {default as 'no-unnecessary-boolean-comparison'} from './no-unnecessary-boolean-comparison.js';
137
+ export {default as 'no-unnecessary-fetch-options'} from './no-unnecessary-fetch-options.js';
135
138
  export {default as 'no-unnecessary-global-this'} from './no-unnecessary-global-this.js';
136
139
  export {default as 'no-unnecessary-nested-ternary'} from './no-unnecessary-nested-ternary.js';
137
140
  export {default as 'no-unnecessary-polyfills'} from './no-unnecessary-polyfills.js';
@@ -144,6 +147,7 @@ export {default as 'no-unreadable-new-expression'} from './no-unreadable-new-exp
144
147
  export {default as 'no-unreadable-object-destructuring'} from './no-unreadable-object-destructuring.js';
145
148
  export {default as 'no-unsafe-buffer-conversion'} from './no-unsafe-buffer-conversion.js';
146
149
  export {default as 'no-unsafe-dom-html'} from './no-unsafe-dom-html.js';
150
+ export {default as 'no-unsafe-promise-all-settled-values'} from './no-unsafe-promise-all-settled-values.js';
147
151
  export {default as 'no-unsafe-property-key'} from './no-unsafe-property-key.js';
148
152
  export {default as 'no-unsafe-string-replacement'} from './no-unsafe-string-replacement.js';
149
153
  export {default as 'no-unused-array-method-return'} from './no-unused-array-method-return.js';
@@ -173,6 +177,7 @@ export {default as 'no-zero-fractions'} from './no-zero-fractions.js';
173
177
  export {default as 'number-literal-case'} from './number-literal-case.js';
174
178
  export {default as 'numeric-separators-style'} from './numeric-separators-style.js';
175
179
  export {default as 'operator-assignment'} from './operator-assignment.js';
180
+ export {default as 'prefer-abort-signal-any'} from './prefer-abort-signal-any.js';
176
181
  export {default as 'prefer-abort-signal-timeout'} from './prefer-abort-signal-timeout.js';
177
182
  export {default as 'prefer-add-event-listener-options'} from './prefer-add-event-listener-options.js';
178
183
  export {default as 'prefer-add-event-listener'} from './prefer-add-event-listener.js';
@@ -182,6 +187,7 @@ export {default as 'prefer-array-flat-map'} from './prefer-array-flat-map.js';
182
187
  export {default as 'prefer-array-flat'} from './prefer-array-flat.js';
183
188
  export {default as 'prefer-array-from-async'} from './prefer-array-from-async.js';
184
189
  export {default as 'prefer-array-from-map'} from './prefer-array-from-map.js';
190
+ export {default as 'prefer-array-from-range'} from './prefer-array-from-range.js';
185
191
  export {default as 'prefer-array-index-of'} from './prefer-array-index-of.js';
186
192
  export {default as 'prefer-array-iterable-methods'} from './prefer-array-iterable-methods.js';
187
193
  export {default as 'prefer-array-last-methods'} from './prefer-array-last-methods.js';
@@ -191,6 +197,7 @@ export {default as 'prefer-at'} from './prefer-at.js';
191
197
  export {default as 'prefer-await'} from './prefer-await.js';
192
198
  export {default as 'prefer-bigint-literals'} from './prefer-bigint-literals.js';
193
199
  export {default as 'prefer-blob-reading-methods'} from './prefer-blob-reading-methods.js';
200
+ export {default as 'prefer-block-statement-over-iife'} from './prefer-block-statement-over-iife.js';
194
201
  export {default as 'prefer-boolean-return'} from './prefer-boolean-return.js';
195
202
  export {default as 'prefer-class-fields'} from './prefer-class-fields.js';
196
203
  export {default as 'prefer-classlist-toggle'} from './prefer-classlist-toggle.js';
@@ -214,6 +221,7 @@ export {default as 'prefer-flat-math-min-max'} from './prefer-flat-math-min-max.
214
221
  export {default as 'prefer-get-or-insert-computed'} from './prefer-get-or-insert-computed.js';
215
222
  export {default as 'prefer-global-number-constants'} from './prefer-global-number-constants.js';
216
223
  export {default as 'prefer-global-this'} from './prefer-global-this.js';
224
+ export {default as 'prefer-group-by'} from './prefer-group-by.js';
217
225
  export {default as 'prefer-has-check'} from './prefer-has-check.js';
218
226
  export {default as 'prefer-hoisting-branch-code'} from './prefer-hoisting-branch-code.js';
219
227
  export {default as 'prefer-https'} from './prefer-https.js';
@@ -223,6 +231,7 @@ export {default as 'prefer-includes-over-repeated-comparisons'} from './prefer-i
223
231
  export {default as 'prefer-includes'} from './prefer-includes.js';
224
232
  export {default as 'prefer-iterable-in-constructor'} from './prefer-iterable-in-constructor.js';
225
233
  export {default as 'prefer-iterator-concat'} from './prefer-iterator-concat.js';
234
+ export {default as 'prefer-iterator-helpers'} from './prefer-iterator-helpers.js';
226
235
  export {default as 'prefer-iterator-to-array-at-end'} from './prefer-iterator-to-array-at-end.js';
227
236
  export {default as 'prefer-iterator-to-array'} from './prefer-iterator-to-array.js';
228
237
  export {default as 'prefer-keyboard-event-key'} from './prefer-keyboard-event-key.js';
@@ -267,6 +276,7 @@ export {default as 'prefer-set-size'} from './prefer-set-size.js';
267
276
  export {default as 'prefer-short-arrow-method'} from './prefer-short-arrow-method.js';
268
277
  export {default as 'prefer-simple-condition-first'} from './prefer-simple-condition-first.js';
269
278
  export {default as 'prefer-simple-sort-comparator'} from './prefer-simple-sort-comparator.js';
279
+ export {default as 'prefer-simplified-conditions'} from './prefer-simplified-conditions.js';
270
280
  export {default as 'prefer-single-array-predicate'} from './prefer-single-array-predicate.js';
271
281
  export {default as 'prefer-single-call'} from './prefer-single-call.js';
272
282
  export {default as 'prefer-single-object-destructuring'} from './prefer-single-object-destructuring.js';
@@ -1,19 +1,95 @@
1
1
  import globals from 'globals';
2
- import {functionTypes} from './ast/index.js';
2
+ import {functionTypes, isMemberExpression, isMethodCall} from './ast/index.js';
3
3
 
4
4
  const MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE = 'externally-scoped-variable';
5
+ const MESSAGE_ID_SUPER = 'super';
6
+ const MESSAGE_ID_THIS_EXPRESSION = 'this-expression';
7
+ const functionContextReferenceMessageIds = new Map([
8
+ ['Super', MESSAGE_ID_SUPER],
9
+ ['ThisExpression', MESSAGE_ID_THIS_EXPRESSION],
10
+ ]);
11
+
5
12
  const messages = {
6
13
  [MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE]: 'Variable {{name}} not defined in scope of isolated function. Function is isolated because: {{reason}}.',
14
+ [MESSAGE_ID_SUPER]: 'Unexpected `super` in isolated function. Function is isolated because: {{reason}}.',
15
+ [MESSAGE_ID_THIS_EXPRESSION]: 'Unexpected `this` in isolated function. Function is isolated because: {{reason}}.',
7
16
  };
8
17
 
9
18
  /** @type {{functions: string[], selectors: string[], comments: string[], overrideGlobals?: import('eslint').Linter.Globals}} */
10
19
  const defaultOptions = {
11
- functions: ['makeSynchronous'],
20
+ functions: ['makeSynchronous', 'workerize'],
12
21
  selectors: [],
13
22
  comments: ['@isolated'],
14
23
  overrideGlobals: {},
15
24
  };
16
25
 
26
+ const scriptingObjects = ['browser', 'chrome'];
27
+
28
+ const getObjectPropertyName = node => {
29
+ if (node.computed && node.key.type !== 'Literal') {
30
+ return;
31
+ }
32
+
33
+ if (node.key.type === 'Identifier') {
34
+ return node.key.name;
35
+ }
36
+
37
+ if (node.key.type === 'Literal' && typeof node.key.value === 'string') {
38
+ return node.key.value;
39
+ }
40
+ };
41
+
42
+ const getDefaultArgumentCallReason = node => {
43
+ if (
44
+ node.parent.type !== 'CallExpression'
45
+ || node.parent.arguments[0] !== node
46
+ ) {
47
+ return;
48
+ }
49
+
50
+ if (isMethodCall(node.parent, {object: 'browser', method: 'execute'})) {
51
+ return 'callee of method named "browser.execute"';
52
+ }
53
+
54
+ if (isMethodCall(node.parent, {object: 'page', method: 'evaluate'})) {
55
+ return 'callee of method named "page.evaluate"';
56
+ }
57
+ };
58
+
59
+ const getScriptingExecuteScriptObjectName = node => {
60
+ if (
61
+ !isMethodCall(node, {method: 'executeScript'})
62
+ || !isMemberExpression(node.callee.object, {
63
+ objects: scriptingObjects,
64
+ property: 'scripting',
65
+ })
66
+ ) {
67
+ return;
68
+ }
69
+
70
+ return node.callee.object.object.name;
71
+ };
72
+
73
+ const getExecuteScriptPropertyReason = node => {
74
+ if (
75
+ node.parent.type !== 'Property'
76
+ || node.parent.kind !== 'init'
77
+ || getObjectPropertyName(node.parent) !== 'func'
78
+ || node.parent.parent.type !== 'ObjectExpression'
79
+ || node.parent.parent.parent.type !== 'CallExpression'
80
+ || node.parent.parent.parent.arguments[0] !== node.parent.parent
81
+ ) {
82
+ return;
83
+ }
84
+
85
+ const scriptingObjectName = getScriptingExecuteScriptObjectName(node.parent.parent.parent);
86
+ if (!scriptingObjectName) {
87
+ return;
88
+ }
89
+
90
+ return `property "func" passed to "${scriptingObjectName}.scripting.executeScript"`;
91
+ };
92
+
17
93
  /** @param {import('eslint').Rule.RuleContext} context */
18
94
  const create = context => {
19
95
  const {sourceCode} = context;
@@ -29,8 +105,58 @@ const create = context => {
29
105
  };
30
106
  const checked = new WeakSet();
31
107
 
108
+ function * getFunctionContextProblems(node, reason, root = node) {
109
+ const messageId = functionContextReferenceMessageIds.get(node.type);
110
+ if (messageId) {
111
+ yield {
112
+ node,
113
+ messageId,
114
+ data: {reason},
115
+ };
116
+ return;
117
+ }
118
+
119
+ if (node !== root) {
120
+ if (functionTypes.includes(node.type) && node.type !== 'ArrowFunctionExpression') {
121
+ return;
122
+ }
123
+
124
+ if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
125
+ if (node.superClass) {
126
+ yield * getFunctionContextProblems(node.superClass, reason, root);
127
+ }
128
+
129
+ for (const classElement of node.body.body) {
130
+ if (classElement.computed) {
131
+ yield * getFunctionContextProblems(classElement.key, reason, root);
132
+ }
133
+ }
134
+
135
+ return;
136
+ }
137
+ }
138
+
139
+ for (const key of sourceCode.visitorKeys[node.type] ?? []) {
140
+ const value = node[key];
141
+
142
+ if (Array.isArray(value)) {
143
+ for (const childNode of value) {
144
+ if (childNode?.type) {
145
+ yield * getFunctionContextProblems(childNode, reason, root);
146
+ }
147
+ }
148
+
149
+ continue;
150
+ }
151
+
152
+ if (value?.type) {
153
+ yield * getFunctionContextProblems(value, reason, root);
154
+ }
155
+ }
156
+ }
157
+
32
158
  /** @param {import('estree').Node} node */
33
- const reportExternallyScopedVariables = (node, reason) => {
159
+ const reportIsolatedFunctionProblems = (node, reason) => {
34
160
  if (checked.has(node) || !functionTypes.includes(node.type)) {
35
161
  return;
36
162
  }
@@ -69,12 +195,33 @@ const create = context => {
69
195
  data: {name: identifier.name, reason},
70
196
  });
71
197
  }
198
+
199
+ for (const problem of getFunctionContextProblems(node, reason)) {
200
+ context.report(problem);
201
+ }
72
202
  };
73
203
 
74
204
  const isComment = token => token?.type === 'Block' || token?.type === 'Line';
75
205
 
206
+ const canCommentApplyToParent = (node, parent) =>
207
+ parent && (
208
+ [
209
+ 'VariableDeclarator',
210
+ 'VariableDeclaration',
211
+ 'ExportNamedDeclaration',
212
+ 'ExportDefaultDeclaration',
213
+ ].includes(parent.type)
214
+ || (
215
+ (
216
+ parent.type === 'Property'
217
+ || parent.type === 'MethodDefinition'
218
+ )
219
+ && parent.value === node
220
+ )
221
+ );
222
+
76
223
  /**
77
- Find a comment on this node or its parent, in cases where the node passed is part of a variable or export declaration.
224
+ Find a comment on this node or its parent, in cases where the node passed is part of a variable, export, property, or method declaration.
78
225
  @param {import('estree').Node} node
79
226
  */
80
227
  const findComment = node => {
@@ -82,12 +229,7 @@ const create = context => {
82
229
  let commentableNode = node;
83
230
  while (
84
231
  !isComment(previousToken)
85
- && [
86
- 'VariableDeclarator',
87
- 'VariableDeclaration',
88
- 'ExportNamedDeclaration',
89
- 'ExportDefaultDeclaration',
90
- ].includes(commentableNode.parent.type)
232
+ && canCommentApplyToParent(commentableNode, commentableNode.parent)
91
233
  ) {
92
234
  commentableNode = commentableNode.parent;
93
235
  previousToken = sourceCode.getTokenBefore(commentableNode, {includeComments: true});
@@ -127,6 +269,8 @@ const create = context => {
127
269
  ) {
128
270
  return `callee of function named ${JSON.stringify(node.parent.callee.name)}`;
129
271
  }
272
+
273
+ return getDefaultArgumentCallReason(node) ?? getExecuteScriptPropertyReason(node);
130
274
  };
131
275
 
132
276
  context.onExit(
@@ -137,7 +281,7 @@ const create = context => {
137
281
  return;
138
282
  }
139
283
 
140
- return reportExternallyScopedVariables(node, reason);
284
+ return reportIsolatedFunctionProblems(node, reason);
141
285
  },
142
286
  );
143
287
 
@@ -146,7 +290,7 @@ const create = context => {
146
290
  selector,
147
291
  node => {
148
292
  const reason = `matches selector ${JSON.stringify(selector)}`;
149
- return reportExternallyScopedVariables(node, reason);
293
+ return reportIsolatedFunctionProblems(node, reason);
150
294
  },
151
295
  );
152
296
  }
@@ -0,0 +1,111 @@
1
+ import {findVariable, getPropertyName} from '@eslint-community/eslint-utils';
2
+ import {isFunction} from './ast/index.js';
3
+ import {
4
+ getConstVariableInitializer,
5
+ isPromiseType,
6
+ unwrapTypeScriptExpression,
7
+ } from './utils/index.js';
8
+
9
+ const MESSAGE_ID = 'no-async-promise-finally';
10
+ const messages = {
11
+ [MESSAGE_ID]: 'Do not pass an async function to `Promise#finally()`.',
12
+ };
13
+
14
+ const isAsyncNonGeneratorFunction = node =>
15
+ isFunction(node)
16
+ && node.async
17
+ && !node.generator;
18
+
19
+ function isPromiseObject(node, context) {
20
+ const {parserServices} = context.sourceCode;
21
+ if (!parserServices?.program) {
22
+ return;
23
+ }
24
+
25
+ try {
26
+ return isPromiseType(
27
+ parserServices.getTypeAtLocation(node),
28
+ parserServices.program.getTypeChecker(),
29
+ );
30
+ } catch {
31
+ // TypeScript can throw while resolving incomplete projects; keep this rule best-effort.
32
+ }
33
+ }
34
+
35
+ function isAsyncFunctionDeclarationReference(node, context) {
36
+ if (node.type !== 'Identifier') {
37
+ return false;
38
+ }
39
+
40
+ const variable = findVariable(context.sourceCode.getScope(node), node);
41
+ if (variable?.defs.length !== 1) {
42
+ return false;
43
+ }
44
+
45
+ const [definition] = variable.defs;
46
+ return definition.type === 'FunctionName'
47
+ && isAsyncNonGeneratorFunction(definition.node);
48
+ }
49
+
50
+ function isAsyncFinallyCallback(node, context) {
51
+ node = unwrapTypeScriptExpression(node);
52
+
53
+ if (isAsyncNonGeneratorFunction(node)) {
54
+ return true;
55
+ }
56
+
57
+ const initializer = getConstVariableInitializer(node, context);
58
+ if (initializer && isAsyncNonGeneratorFunction(unwrapTypeScriptExpression(initializer))) {
59
+ return true;
60
+ }
61
+
62
+ return isAsyncFunctionDeclarationReference(node, context);
63
+ }
64
+
65
+ /** @param {import('eslint').Rule.RuleContext} context */
66
+ const create = context => {
67
+ context.on('CallExpression', callExpression => {
68
+ const {callee} = callExpression;
69
+ if (callee.type !== 'MemberExpression') {
70
+ return;
71
+ }
72
+
73
+ const method = getPropertyName(callee, context.sourceCode.getScope(callExpression));
74
+ if (method !== 'finally') {
75
+ return;
76
+ }
77
+
78
+ const promiseObject = isPromiseObject(callee.object, context);
79
+ if (promiseObject === false) {
80
+ return;
81
+ }
82
+
83
+ const [callback] = callExpression.arguments;
84
+ if (!callback || !isAsyncFinallyCallback(callback, context)) {
85
+ return;
86
+ }
87
+
88
+ return {
89
+ node: callback,
90
+ messageId: MESSAGE_ID,
91
+ };
92
+ });
93
+ };
94
+
95
+ /** @type {import('eslint').Rule.RuleModule} */
96
+ const config = {
97
+ create,
98
+ meta: {
99
+ type: 'problem',
100
+ docs: {
101
+ description: 'Disallow async functions as `Promise#finally()` callbacks.',
102
+ recommended: 'unopinionated',
103
+ },
104
+ messages,
105
+ languages: [
106
+ 'js/js',
107
+ ],
108
+ },
109
+ };
110
+
111
+ export default config;
@@ -56,6 +56,28 @@ const collectionTypes = [
56
56
  // `.method(…)` without parentheses, so suggestions are limited to them.
57
57
  const simpleObjectTypes = new Set(['Identifier', 'ThisExpression', 'MemberExpression']);
58
58
 
59
+ function getStaticPropertyValues(node, sourceCode) {
60
+ const staticResult = getStaticValue(node, sourceCode.getScope(node));
61
+ if (staticResult) {
62
+ return [staticResult.value];
63
+ }
64
+
65
+ if (node.type !== 'ConditionalExpression') {
66
+ return;
67
+ }
68
+
69
+ const consequentValues = getStaticPropertyValues(node.consequent, sourceCode);
70
+ const alternateValues = getStaticPropertyValues(node.alternate, sourceCode);
71
+ if (!consequentValues || !alternateValues) {
72
+ return;
73
+ }
74
+
75
+ return [
76
+ ...consequentValues,
77
+ ...alternateValues,
78
+ ];
79
+ }
80
+
59
81
  function getProblem(node, collection, context) {
60
82
  const {sourceCode} = context;
61
83
  const {name: type} = collection;
@@ -132,11 +154,12 @@ const create = context => {
132
154
  }
133
155
 
134
156
  // Allow accessing a real member, including `Symbol` ones like `map[Symbol.iterator]`.
135
- const staticResult = getStaticValue(node.property, context.sourceCode.getScope(node.property));
157
+ const staticPropertyValues = getStaticPropertyValues(node.property, context.sourceCode);
136
158
  if (
137
- staticResult
138
- && (typeof staticResult.value === 'string' || typeof staticResult.value === 'symbol')
139
- && Reflect.has(collection.prototype, staticResult.value)
159
+ staticPropertyValues?.every(value =>
160
+ (typeof value === 'string' || typeof value === 'symbol')
161
+ && Reflect.has(collection.prototype, value),
162
+ )
140
163
  ) {
141
164
  return;
142
165
  }
@@ -11,12 +11,14 @@ const messages = {
11
11
  };
12
12
 
13
13
  // Trailing `:` marks a complete line (heading, label, list intro), not a wrapped sentence.
14
- const sentenceEndPattern = /[!.:?]$/v;
14
+ const sentenceEndPattern = /(?:[\p{Extended_Pictographic}!.:?]\p{Variation_Selector}?|\p{RGI_Emoji})$/v;
15
15
  const directiveCommentPattern = /^(?:[#\/@]|eslint(?:$|\s|-)|globals?\b|exported\b|no default$|noinspection\b|(?:c8|istanbul|v8)\s+ignore\b|(?:biome|deno|dprint|oxlint|prettier)-|(?:cspell|spell-checker):)/v;
16
+ const annotationCommentPattern = /^[A-Z]{2,}(?:\([^\)]+\))?:/v;
16
17
  const spdxCommentPattern = /^SPDX-/v;
17
18
  const copyrightCommentPattern = /^(?:©|copyright\b)/iv;
18
19
  // Editor modelines: vim (`vim:`, `vi:`) and Emacs (`-*- … -*-`).
19
20
  const modelineCommentPattern = /^(?:vim?:|-\*-)/v;
21
+ const structuredCommentPattern = /^(?:language=\S+|(?:end)?region(?:$|\s)|<\/?editor-fold\b)/v;
20
22
  const listCommentPattern = /^(?:[*+\-]\s|\d+(?:\.|\))\s)/v;
21
23
  const separatorCommentPattern = /^[#*\-=_~]{3,}$/v;
22
24
  const urlPattern = /\bhttps?:\/\/|www\./v;
@@ -28,9 +30,11 @@ const endsWithSentencePunctuation = comment => sentenceEndPattern.test(getCommen
28
30
 
29
31
  const isIgnoredCommentText = text =>
30
32
  directiveCommentPattern.test(text)
33
+ || annotationCommentPattern.test(text)
31
34
  || spdxCommentPattern.test(text)
32
35
  || copyrightCommentPattern.test(text)
33
36
  || modelineCommentPattern.test(text)
37
+ || structuredCommentPattern.test(text)
34
38
  || listCommentPattern.test(text)
35
39
  || separatorCommentPattern.test(text)
36
40
  || urlPattern.test(text)
@@ -8,7 +8,7 @@ import {
8
8
  shouldAddParenthesesToUnaryExpressionArgument,
9
9
  } from './utils/index.js';
10
10
  import {
11
- addParenthesizesToReturnOrThrowExpression,
11
+ addParenthesesToReturnOrThrowExpression,
12
12
  fixSpaceAroundKeyword,
13
13
  } from './fix/index.js';
14
14
  import {isFunction, isMethodCall} from './ast/index.js';
@@ -177,7 +177,7 @@ const create = context => {
177
177
  }
178
178
 
179
179
  if (needsReturnOrThrowParentheses) {
180
- yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
180
+ yield addParenthesesToReturnOrThrowExpression(fixer, parent, context);
181
181
  return;
182
182
  }
183
183