eslint-plugin-unicorn 68.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 (53) hide show
  1. package/package.json +3 -1
  2. package/readme.md +12 -0
  3. package/rules/ast/is-empty-object-expression.js +2 -2
  4. package/rules/comment-content.js +136 -18
  5. package/rules/consistent-boolean-name.js +31 -4
  6. package/rules/consistent-conditional-object-spread.js +9 -3
  7. package/rules/consistent-tuple-labels.js +49 -0
  8. package/rules/custom-error-definition.js +26 -7
  9. package/rules/index.js +12 -0
  10. package/rules/no-chained-comparison.js +1 -1
  11. package/rules/no-computed-property-existence-check.js +6 -0
  12. package/rules/no-incorrect-template-string-interpolation.js +82 -30
  13. package/rules/no-instanceof-builtins.js +59 -13
  14. package/rules/no-invalid-well-known-symbol-methods.js +205 -0
  15. package/rules/no-late-current-target-access.js +9 -71
  16. package/rules/no-late-event-control.js +127 -0
  17. package/rules/no-nonstandard-builtin-properties.js +2 -0
  18. package/rules/no-object-methods-with-collections.js +4 -289
  19. package/rules/no-return-array-push.js +20 -5
  20. package/rules/no-top-level-side-effects.js +8 -1
  21. package/rules/no-unnecessary-global-this.js +36 -1
  22. package/rules/no-unreadable-object-destructuring.js +15 -0
  23. package/rules/no-unsafe-string-replacement.js +31 -0
  24. package/rules/no-unused-array-method-return.js +11 -2
  25. package/rules/no-useless-boolean-cast.js +58 -1
  26. package/rules/no-useless-concat.js +27 -3
  27. package/rules/no-useless-else.js +16 -2
  28. package/rules/no-useless-iterator-to-array.js +165 -115
  29. package/rules/prefer-abort-signal-timeout.js +395 -0
  30. package/rules/prefer-aggregate-error.js +478 -0
  31. package/rules/prefer-at.js +5 -6
  32. package/rules/prefer-code-point.js +58 -8
  33. package/rules/prefer-continue.js +16 -0
  34. package/rules/prefer-dom-node-replace-children.js +353 -0
  35. package/rules/prefer-error-is-error.js +181 -0
  36. package/rules/prefer-has-check.js +1 -4
  37. package/rules/prefer-hoisting-branch-code.js +9 -7
  38. package/rules/prefer-iterator-concat.js +21 -1
  39. package/rules/prefer-minimal-ternary.js +15 -11
  40. package/rules/prefer-modern-dom-apis.js +1 -45
  41. package/rules/prefer-observer-apis.js +498 -0
  42. package/rules/prefer-promise-try.js +238 -0
  43. package/rules/prefer-set-methods.js +337 -0
  44. package/rules/prefer-toggle-attribute.js +293 -0
  45. package/rules/prefer-url-search-parameters.js +411 -0
  46. package/rules/shared/late-event-handler.js +82 -0
  47. package/rules/shared/name-replacements.js +6 -0
  48. package/rules/utils/builtin-collection-type.js +267 -0
  49. package/rules/utils/index.js +6 -0
  50. package/rules/utils/is-boolean.js +3 -1
  51. package/rules/utils/is-event.js +127 -0
  52. package/rules/utils/should-report-replace-children-receiver.js +210 -0
  53. package/rules/utils/type-helpers.js +166 -8
@@ -10,7 +10,7 @@ const messages = {
10
10
 
11
11
  const orderingOperators = new Set(['<', '>', '<=', '>=']);
12
12
  const equalityOperators = new Set(['===', '!==', '==', '!=']);
13
- const comparisonOperators = new Set([...orderingOperators, ...equalityOperators]);
13
+ const comparisonOperators = orderingOperators.union(equalityOperators);
14
14
 
15
15
  const isComparison = node => node.type === 'BinaryExpression' && comparisonOperators.has(node.operator);
16
16
  const isBooleanLiteral = node => node.type === 'Literal' && typeof node.value === 'boolean';
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getParenthesizedText,
3
3
  hasOptionalChainElement,
4
+ isBoolean,
4
5
  isBooleanExpression,
5
6
  isControlFlowTest,
6
7
  wouldRemoveComments,
@@ -120,6 +121,11 @@ const create = context => {
120
121
  return;
121
122
  }
122
123
 
124
+ // A property whose value type is a known boolean is a value read, not an existence check (and `no-unnecessary-boolean-comparison` may rewrite a comparison into this form).
125
+ if (isBoolean(node, context)) {
126
+ return;
127
+ }
128
+
123
129
  return {
124
130
  node,
125
131
  messageId: MESSAGE_ID,
@@ -20,6 +20,45 @@ const isBindingDeclaration = (raw, index) => {
20
20
  return /^\s*(?:import|export|const|let|var)\b[^;={}]*$/.test(raw.slice(lineStart, index));
21
21
  };
22
22
 
23
+ // `{type}` and `$expr}` inside an embedded block comment (e.g. JSDoc `@param {string}`) are not template interpolation mistakes (common in code-generation templates). Only closed `/* … */` comments count, so a stray `/*` inside a string doesn't suppress a genuine match.
24
+ const getBlockCommentRanges = raw => raw.matchAll(/\/\*[\s\S]*?\*\//g)
25
+ .map(match => [match.index, match.index + match[0].length])
26
+ .toArray();
27
+
28
+ const isInsideRanges = (ranges, index) => ranges.some(([start, end]) => index >= start && index < end);
29
+
30
+ const getTemplateElementRawRange = (node, sourceCode) => {
31
+ const [start, end] = sourceCode.getRange(node);
32
+
33
+ return [
34
+ start + 1,
35
+ end - (node.tail ? 1 : 2),
36
+ ];
37
+ };
38
+
39
+ const getTemplateRawWithExpressionsMasked = (node, sourceCode) => {
40
+ const [templateStart, templateEnd] = sourceCode.getRange(node);
41
+ const rawStart = templateStart + 1;
42
+ let raw = sourceCode.text.slice(rawStart, templateEnd - 1);
43
+
44
+ for (const [index, quasi] of node.quasis.entries()) {
45
+ const nextQuasi = node.quasis[index + 1];
46
+
47
+ if (!nextQuasi) {
48
+ break;
49
+ }
50
+
51
+ const [, quasiRawEnd] = getTemplateElementRawRange(quasi, sourceCode);
52
+ const [nextQuasiRawStart] = getTemplateElementRawRange(nextQuasi, sourceCode);
53
+ const maskStart = quasiRawEnd - rawStart;
54
+ const maskEnd = nextQuasiRawStart - rawStart;
55
+
56
+ raw = raw.slice(0, maskStart) + ' '.repeat(maskEnd - maskStart) + raw.slice(maskEnd);
57
+ }
58
+
59
+ return raw;
60
+ };
61
+
23
62
  const getMissingDollarReplacements = raw => raw.matchAll(missingDollar)
24
63
  .filter(match => !isBindingDeclaration(raw, match.index))
25
64
  .map(match => ({
@@ -37,49 +76,62 @@ const getMissingOpeningBraceReplacements = raw => raw.matchAll(missingOpeningBra
37
76
  }))
38
77
  .toArray();
39
78
 
40
- const getReplacements = raw => {
79
+ const getReplacements = (raw, blockCommentRanges, rawOffset) => {
41
80
  const replacements = [
42
81
  ...getMissingDollarReplacements(raw),
43
82
  ...getMissingOpeningBraceReplacements(raw),
44
83
  ];
45
84
 
46
- return replacements.toSorted((replacement, nextReplacement) => replacement.index - nextReplacement.index);
85
+ if (replacements.length === 0) {
86
+ return replacements;
87
+ }
88
+
89
+ return replacements
90
+ .filter(replacement => !isInsideRanges(blockCommentRanges, rawOffset + replacement.index))
91
+ .toSorted((replacement, nextReplacement) => replacement.index - nextReplacement.index);
47
92
  };
48
93
 
49
94
  /** @param {import('eslint').Rule.RuleContext} context */
50
95
  const create = context => {
51
- context.on('TemplateElement', function * (node) {
52
- if (isTaggedTemplateLiteral(node.parent)) {
96
+ context.on('TemplateLiteral', function * (node) {
97
+ if (isTaggedTemplateLiteral(node)) {
53
98
  return;
54
99
  }
55
100
 
56
- const rawStart = context.sourceCode.getRange(node)[0] + 1;
57
-
58
- for (const {index, incorrect, correct} of getReplacements(node.value.raw)) {
59
- const start = rawStart + index;
60
- const end = start + incorrect.length;
61
-
62
- yield {
63
- node,
64
- loc: {
65
- start: context.sourceCode.getLocFromIndex(start),
66
- end: context.sourceCode.getLocFromIndex(end),
67
- },
68
- messageId: MESSAGE_ID,
69
- data: {
70
- correct,
71
- },
72
- suggest: [
73
- {
74
- messageId: MESSAGE_ID_SUGGESTION,
75
- data: {
76
- incorrect,
77
- correct,
78
- },
79
- fix: fixer => fixer.replaceTextRange([start, end], correct),
101
+ const templateRawStart = context.sourceCode.getRange(node)[0] + 1;
102
+ const blockCommentRanges = getBlockCommentRanges(getTemplateRawWithExpressionsMasked(node, context.sourceCode));
103
+
104
+ for (const quasi of node.quasis) {
105
+ const [rawStart, rawEnd] = getTemplateElementRawRange(quasi, context.sourceCode);
106
+ const raw = context.sourceCode.text.slice(rawStart, rawEnd);
107
+ const rawOffset = rawStart - templateRawStart;
108
+
109
+ for (const {index, incorrect, correct} of getReplacements(raw, blockCommentRanges, rawOffset)) {
110
+ const start = rawStart + index;
111
+ const end = start + incorrect.length;
112
+
113
+ yield {
114
+ node: quasi,
115
+ loc: {
116
+ start: context.sourceCode.getLocFromIndex(start),
117
+ end: context.sourceCode.getLocFromIndex(end),
80
118
  },
81
- ],
82
- };
119
+ messageId: MESSAGE_ID,
120
+ data: {
121
+ correct,
122
+ },
123
+ suggest: [
124
+ {
125
+ messageId: MESSAGE_ID_SUGGESTION,
126
+ data: {
127
+ incorrect,
128
+ correct,
129
+ },
130
+ fix: fixer => fixer.replaceTextRange([start, end], correct),
131
+ },
132
+ ],
133
+ };
134
+ }
83
135
  }
84
136
  });
85
137
  };
@@ -3,6 +3,7 @@ import {
3
3
  getParenthesizedRange,
4
4
  getTokenStore,
5
5
  isParenthesized,
6
+ isGlobalIdentifier,
6
7
  shouldAddParenthesesToUnaryExpressionArgument,
7
8
  } from './utils/index.js';
8
9
  import {replaceNodeOrTokenAndSpacesBefore, fixSpaceAroundKeyword} from './fix/index.js';
@@ -26,6 +27,13 @@ const primitiveWrappers = new Set([
26
27
  'Symbol',
27
28
  ]);
28
29
 
30
+ const globalObjectNames = new Set([
31
+ 'global',
32
+ 'globalThis',
33
+ 'self',
34
+ 'window',
35
+ ]);
36
+
29
37
  const strictStrategyConstructors = [
30
38
  // Error types
31
39
  ...builtinErrors,
@@ -73,7 +81,7 @@ const replaceWithFunctionCall = (node, context, functionName) => function * (fix
73
81
  yield replaceNodeOrTokenAndSpacesBefore(right, '', fixer, context, tokenStore);
74
82
  };
75
83
 
76
- const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
84
+ const replaceWithTypeOfExpression = (node, context, constructorName) => function * (fixer) {
77
85
  const {left, right} = node;
78
86
  const tokenStore = getTokenStore(context, node);
79
87
  const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
@@ -105,9 +113,35 @@ const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
105
113
 
106
114
  const rightRange = getParenthesizedRange(right, {sourceCode: tokenStore});
107
115
 
108
- yield fixer.replaceTextRange(rightRange, safeQuote + sourceCode.getText(right).toLowerCase() + safeQuote);
116
+ yield fixer.replaceTextRange(rightRange, safeQuote + constructorName.toLowerCase() + safeQuote);
109
117
  };
110
118
 
119
+ function getConstructor(node, context) {
120
+ if (node.type === 'Identifier') {
121
+ return {
122
+ name: node.name,
123
+ referenceText: node.name,
124
+ };
125
+ }
126
+
127
+ if (
128
+ node.type === 'MemberExpression'
129
+ && !node.computed
130
+ && node.property.type === 'Identifier'
131
+ && node.object.type === 'Identifier'
132
+ && globalObjectNames.has(node.object.name)
133
+ && isGlobalIdentifier(node.object, context)
134
+ ) {
135
+ return {
136
+ name: node.property.name,
137
+ referenceText: context.sourceCode.getText(node),
138
+ };
139
+ }
140
+ }
141
+
142
+ const hasCommentsInside = (node, context) =>
143
+ context.sourceCode.getCommentsInside(node).length > 0;
144
+
111
145
  /** @param {import('eslint').Rule.RuleContext} context */
112
146
  const create = context => {
113
147
  const {
@@ -124,11 +158,16 @@ const create = context => {
124
158
  context.on('BinaryExpression', /** @param {import('estree').BinaryExpression} node */ node => {
125
159
  const {right, operator} = node;
126
160
 
127
- if ((operator !== 'instanceof') || (right.type !== 'Identifier') || exclude.includes(right.name)) {
161
+ if (operator !== 'instanceof') {
162
+ return;
163
+ }
164
+
165
+ const constructor = getConstructor(right, context);
166
+ if (!constructor || exclude.includes(constructor.name)) {
128
167
  return;
129
168
  }
130
169
 
131
- const constructorName = right.name;
170
+ const {name: constructorName, referenceText} = constructor;
132
171
 
133
172
  /** @type {import('eslint').Rule.ReportDescriptor} */
134
173
  const problem = {
@@ -140,24 +179,31 @@ const create = context => {
140
179
  constructorName === 'Array'
141
180
  || (constructorName === 'Error' && useErrorIsError)
142
181
  ) {
143
- const functionName = constructorName === 'Array' ? 'Array.isArray' : 'Error.isError';
182
+ const methodName = constructorName === 'Array' ? 'isArray' : 'isError';
183
+ const functionName = `${referenceText}.${methodName}`;
144
184
  problem.fix = replaceWithFunctionCall(node, context, functionName);
145
185
  return problem;
146
186
  }
147
187
 
148
188
  if (constructorName === 'Function') {
149
- problem.fix = replaceWithTypeOfExpression(node, context);
189
+ if (!hasCommentsInside(right, context)) {
190
+ problem.fix = replaceWithTypeOfExpression(node, context, constructorName);
191
+ }
192
+
150
193
  return problem;
151
194
  }
152
195
 
153
196
  if (primitiveWrappers.has(constructorName)) {
154
- problem.suggest = [
155
- {
156
- messageId: MESSAGE_ID_SWITCH_TO_TYPE_OF,
157
- data: {type: constructorName.toLowerCase()},
158
- fix: replaceWithTypeOfExpression(node, context),
159
- },
160
- ];
197
+ if (!hasCommentsInside(right, context)) {
198
+ problem.suggest = [
199
+ {
200
+ messageId: MESSAGE_ID_SWITCH_TO_TYPE_OF,
201
+ data: {type: constructorName.toLowerCase()},
202
+ fix: replaceWithTypeOfExpression(node, context, constructorName),
203
+ },
204
+ ];
205
+ }
206
+
161
207
  return problem;
162
208
  }
163
209
 
@@ -0,0 +1,205 @@
1
+ import {isPromiseType} from './utils/index.js';
2
+
3
+ const MESSAGE_ID_ASYNC_DISPOSE = 'no-invalid-well-known-symbol-methods/async-dispose';
4
+ const MESSAGE_ID_DISPOSE_GENERATOR = 'no-invalid-well-known-symbol-methods/dispose-generator';
5
+ const MESSAGE_ID_SYNC_ITERATOR = 'no-invalid-well-known-symbol-methods/sync-iterator';
6
+ const MESSAGE_ID_ASYNC_ITERATOR = 'no-invalid-well-known-symbol-methods/async-iterator';
7
+ const MESSAGE_ID_SUGGESTION = 'no-invalid-well-known-symbol-methods/suggestion';
8
+
9
+ const messages = {
10
+ [MESSAGE_ID_ASYNC_DISPOSE]: '`Symbol.dispose` must not return a Promise. Use `Symbol.asyncDispose` instead.',
11
+ [MESSAGE_ID_DISPOSE_GENERATOR]: '`Symbol.dispose` must not be a generator. Use a normal method for synchronous disposal or `Symbol.asyncDispose` for asynchronous disposal.',
12
+ [MESSAGE_ID_SYNC_ITERATOR]: '`Symbol.iterator` must return a sync iterator. Use `Symbol.asyncIterator` for async iterators.',
13
+ [MESSAGE_ID_ASYNC_ITERATOR]: '`Symbol.asyncIterator` must return an async iterator directly. Use an async generator or return the iterator synchronously.',
14
+ [MESSAGE_ID_SUGGESTION]: 'Use `Symbol.{{replacement}}` instead.',
15
+ };
16
+
17
+ const promiseTypeNames = new Set(['Promise', 'PromiseLike']);
18
+ const checkedSymbolNames = new Set(['dispose', 'iterator', 'asyncIterator']);
19
+
20
+ function getWellKnownSymbolName(member) {
21
+ if (
22
+ !member.computed
23
+ || member.key.type !== 'MemberExpression'
24
+ || member.key.computed
25
+ || member.key.object.type !== 'Identifier'
26
+ || member.key.object.name !== 'Symbol'
27
+ || member.key.property.type !== 'Identifier'
28
+ ) {
29
+ return;
30
+ }
31
+
32
+ return member.key.property.name;
33
+ }
34
+
35
+ function getFunctionNode(member) {
36
+ if (member.kind === 'get' || member.kind === 'set') {
37
+ return;
38
+ }
39
+
40
+ if (
41
+ member.type === 'MethodDefinition'
42
+ || member.type === 'PropertyDefinition'
43
+ ) {
44
+ return member.value;
45
+ }
46
+
47
+ if (member.type === 'Property') {
48
+ return member.value;
49
+ }
50
+ }
51
+
52
+ function isFunctionNode(node) {
53
+ return node?.type === 'FunctionExpression' || node?.type === 'ArrowFunctionExpression';
54
+ }
55
+
56
+ function isPromiseTypeReference(node) {
57
+ return (
58
+ node.type === 'TSTypeReference'
59
+ && node.typeName.type === 'Identifier'
60
+ && promiseTypeNames.has(node.typeName.name)
61
+ );
62
+ }
63
+
64
+ function hasPromiseReturnTypeAnnotation(functionNode) {
65
+ const typeNode = functionNode.returnType?.typeAnnotation;
66
+ if (!typeNode) {
67
+ return false;
68
+ }
69
+
70
+ if (typeNode.type === 'TSTypeReference') {
71
+ return isPromiseTypeReference(typeNode);
72
+ }
73
+
74
+ if (typeNode.type === 'TSUnionType') {
75
+ return typeNode.types.some(typeNode => isPromiseTypeReference(typeNode));
76
+ }
77
+
78
+ return false;
79
+ }
80
+
81
+ function hasPromiseReturnTypeInformation(functionNode, context) {
82
+ const {parserServices} = context.sourceCode;
83
+ if (!parserServices?.program) {
84
+ return false;
85
+ }
86
+
87
+ try {
88
+ const checker = parserServices.program.getTypeChecker();
89
+ const typeScriptNode = parserServices.esTreeNodeToTSNodeMap.get(functionNode);
90
+ const signature = checker.getSignatureFromDeclaration(typeScriptNode);
91
+ if (signature) {
92
+ return isPromiseType(checker.getReturnTypeOfSignature(signature), checker) === true;
93
+ }
94
+
95
+ const type = parserServices.getTypeAtLocation(functionNode);
96
+ return type.getCallSignatures().some(signature =>
97
+ isPromiseType(checker.getReturnTypeOfSignature(signature), checker) === true,
98
+ );
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ const hasPromiseReturnType = (functionNode, context) =>
105
+ hasPromiseReturnTypeAnnotation(functionNode)
106
+ || hasPromiseReturnTypeInformation(functionNode, context);
107
+
108
+ function getReplacementSuggestion(member, replacement) {
109
+ return {
110
+ messageId: MESSAGE_ID_SUGGESTION,
111
+ data: {replacement},
112
+ fix: fixer => fixer.replaceText(member.key.property, replacement),
113
+ };
114
+ }
115
+
116
+ function getProblem(member, context) {
117
+ const symbolName = getWellKnownSymbolName(member);
118
+ if (!checkedSymbolNames.has(symbolName)) {
119
+ return;
120
+ }
121
+
122
+ const functionNode = getFunctionNode(member);
123
+ if (!isFunctionNode(functionNode)) {
124
+ return;
125
+ }
126
+
127
+ if (
128
+ symbolName === 'dispose'
129
+ && functionNode.generator
130
+ ) {
131
+ return {
132
+ node: member.key,
133
+ messageId: MESSAGE_ID_DISPOSE_GENERATOR,
134
+ };
135
+ }
136
+
137
+ const returnsPromise = !functionNode.async && hasPromiseReturnType(functionNode, context);
138
+
139
+ if (
140
+ symbolName === 'dispose'
141
+ && (functionNode.async || returnsPromise)
142
+ ) {
143
+ return {
144
+ node: member.key,
145
+ messageId: MESSAGE_ID_ASYNC_DISPOSE,
146
+ fix: fixer => fixer.replaceText(member.key.property, 'asyncDispose'),
147
+ };
148
+ }
149
+
150
+ if (
151
+ symbolName === 'iterator'
152
+ && (functionNode.async || returnsPromise)
153
+ ) {
154
+ const problem = {
155
+ node: member.key,
156
+ messageId: MESSAGE_ID_SYNC_ITERATOR,
157
+ };
158
+
159
+ if (functionNode.generator) {
160
+ problem.suggest = [
161
+ getReplacementSuggestion(member, 'asyncIterator'),
162
+ ];
163
+ }
164
+
165
+ return problem;
166
+ }
167
+
168
+ if (
169
+ symbolName === 'asyncIterator'
170
+ && !functionNode.generator
171
+ && (functionNode.async || returnsPromise)
172
+ ) {
173
+ return {
174
+ node: member.key,
175
+ messageId: MESSAGE_ID_ASYNC_ITERATOR,
176
+ };
177
+ }
178
+ }
179
+
180
+ /** @param {import('eslint').Rule.RuleContext} context */
181
+ const create = context => {
182
+ context.on('MethodDefinition', node => getProblem(node, context));
183
+ context.on('Property', node => getProblem(node, context));
184
+ context.on('PropertyDefinition', node => getProblem(node, context));
185
+ };
186
+
187
+ /** @type {import('eslint').Rule.RuleModule} */
188
+ const config = {
189
+ create,
190
+ meta: {
191
+ type: 'problem',
192
+ docs: {
193
+ description: 'Disallow invalid implementations of well-known symbol methods.',
194
+ recommended: 'unopinionated',
195
+ },
196
+ fixable: 'code',
197
+ hasSuggestions: true,
198
+ messages,
199
+ languages: [
200
+ 'js/js',
201
+ ],
202
+ },
203
+ };
204
+
205
+ export default config;
@@ -1,84 +1,22 @@
1
1
  import {findVariable} from '@eslint-community/eslint-utils';
2
- import {isMemberExpression, isFunction} from './ast/index.js';
3
- import {containsSuspensionPoint} from './utils/index.js';
2
+ import {isMemberExpression} from './ast/index.js';
3
+ import {
4
+ createLateEventHandlerTracker,
5
+ eventParameterNamePattern,
6
+ getEnclosingFunction,
7
+ } from './shared/late-event-handler.js';
4
8
 
5
9
  const MESSAGE_ID_AFTER_SUSPENSION = 'after-suspension';
6
10
  const MESSAGE_ID_IN_NESTED_FUNCTION = 'in-nested-function';
7
- const eventParameterNamePattern = /^(?:e|event|evt|[a-z][\dA-Za-z]*Event)$/u;
8
11
  const messages = {
9
12
  [MESSAGE_ID_AFTER_SUSPENSION]: '`{{name}}.currentTarget` is `null` after the handler suspends. It is only set during synchronous event dispatch; save it to a variable beforehand.',
10
13
  [MESSAGE_ID_IN_NESTED_FUNCTION]: '`{{name}}.currentTarget` is `null` inside this nested function. It is only set during synchronous event dispatch; save it to a variable in the outer function.',
11
14
  };
12
15
 
13
- const getEnclosingFunction = node => {
14
- for (let current = node.parent; current; current = current.parent) {
15
- if (isFunction(current)) {
16
- return current;
17
- }
18
- }
19
- };
20
-
21
16
  /** @param {import('eslint').Rule.RuleContext} context */
22
17
  const create = context => {
23
18
  const {sourceCode} = context;
24
- // Functions that have passed a suspension point (`await`/`for await…of`/`yield`) at the current point of the traversal.
25
- const suspendedFunctions = new Set();
26
-
27
- // ESLint traverses in source order and fires `onExit` only after the whole subtree of the
28
- // suspension point is visited, so by the time a later access is reached it has “completed”.
29
- const markSuspended = node => {
30
- const functionNode = getEnclosingFunction(node);
31
- if (functionNode) {
32
- suspendedFunctions.add(functionNode);
33
- }
34
- };
35
-
36
- const isRepeatedLoopPart = (loop, child) => {
37
- switch (loop.type) {
38
- case 'ForStatement': {
39
- return [
40
- loop.test,
41
- loop.update,
42
- loop.body,
43
- ].includes(child);
44
- }
45
-
46
- case 'ForInStatement':
47
- case 'ForOfStatement': {
48
- return child !== loop.right;
49
- }
50
-
51
- case 'DoWhileStatement':
52
- case 'WhileStatement': {
53
- return true;
54
- }
55
-
56
- default: {
57
- return false;
58
- }
59
- }
60
- };
61
-
62
- const isInsideSuspendingLoop = (node, functionNode) => {
63
- for (let child = node, current = node.parent; current && current !== functionNode; child = current, current = current.parent) {
64
- if (
65
- isRepeatedLoopPart(current, child)
66
- && containsSuspensionPoint(current, sourceCode.visitorKeys)
67
- ) {
68
- return true;
69
- }
70
- }
71
-
72
- return false;
73
- };
74
-
75
- context.onExit('AwaitExpression', markSuspended);
76
- context.onExit('YieldExpression', markSuspended);
77
- context.onExit('ForOfStatement', node => {
78
- if (node.await) {
79
- markSuspended(node);
80
- }
81
- });
19
+ const lateEventHandlerTracker = createLateEventHandlerTracker(context);
82
20
 
83
21
  context.on('MemberExpression', node => {
84
22
  if (
@@ -104,8 +42,8 @@ const create = context => {
104
42
 
105
43
  if (
106
44
  !isInNestedFunction
107
- && !suspendedFunctions.has(handlerFunction)
108
- && !isInsideSuspendingLoop(node, handlerFunction)
45
+ && !lateEventHandlerTracker.isFunctionSuspended(handlerFunction)
46
+ && !lateEventHandlerTracker.isInsideSuspendingLoop(node, handlerFunction)
109
47
  ) {
110
48
  return;
111
49
  }