eslint 8.47.0 → 8.49.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.
@@ -10,6 +10,21 @@
10
10
 
11
11
  const astUtils = require("./utils/ast-utils");
12
12
 
13
+ //------------------------------------------------------------------------------
14
+ // Helpers
15
+ //------------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Types of class members.
19
+ * Those have `test` method to check it matches to the given class member.
20
+ * @private
21
+ */
22
+ const ClassMemberTypes = {
23
+ "*": { test: () => true },
24
+ field: { test: node => node.type === "PropertyDefinition" },
25
+ method: { test: node => node.type === "MethodDefinition" }
26
+ };
27
+
13
28
  //------------------------------------------------------------------------------
14
29
  // Rule Definition
15
30
  //------------------------------------------------------------------------------
@@ -29,7 +44,32 @@ module.exports = {
29
44
 
30
45
  schema: [
31
46
  {
32
- enum: ["always", "never"]
47
+ anyOf: [
48
+ {
49
+ type: "object",
50
+ properties: {
51
+ enforce: {
52
+ type: "array",
53
+ items: {
54
+ type: "object",
55
+ properties: {
56
+ blankLine: { enum: ["always", "never"] },
57
+ prev: { enum: ["method", "field", "*"] },
58
+ next: { enum: ["method", "field", "*"] }
59
+ },
60
+ additionalProperties: false,
61
+ required: ["blankLine", "prev", "next"]
62
+ },
63
+ minItems: 1
64
+ }
65
+ },
66
+ additionalProperties: false,
67
+ required: ["enforce"]
68
+ },
69
+ {
70
+ enum: ["always", "never"]
71
+ }
72
+ ]
33
73
  },
34
74
  {
35
75
  type: "object",
@@ -55,6 +95,7 @@ module.exports = {
55
95
  options[0] = context.options[0] || "always";
56
96
  options[1] = context.options[1] || { exceptAfterSingleLine: false };
57
97
 
98
+ const configureList = typeof options[0] === "object" ? options[0].enforce : [{ blankLine: options[0], prev: "*", next: "*" }];
58
99
  const sourceCode = context.sourceCode;
59
100
 
60
101
  /**
@@ -144,6 +185,38 @@ module.exports = {
144
185
  return sourceCode.getTokensBetween(before, after, { includeComments: true }).length !== 0;
145
186
  }
146
187
 
188
+ /**
189
+ * Checks whether the given node matches the given type.
190
+ * @param {ASTNode} node The class member node to check.
191
+ * @param {string} type The class member type to check.
192
+ * @returns {boolean} `true` if the class member node matched the type.
193
+ * @private
194
+ */
195
+ function match(node, type) {
196
+ return ClassMemberTypes[type].test(node);
197
+ }
198
+
199
+ /**
200
+ * Finds the last matched configuration from the configureList.
201
+ * @param {ASTNode} prevNode The previous node to match.
202
+ * @param {ASTNode} nextNode The current node to match.
203
+ * @returns {string|null} Padding type or `null` if no matches were found.
204
+ * @private
205
+ */
206
+ function getPaddingType(prevNode, nextNode) {
207
+ for (let i = configureList.length - 1; i >= 0; --i) {
208
+ const configure = configureList[i];
209
+ const matched =
210
+ match(prevNode, configure.prev) &&
211
+ match(nextNode, configure.next);
212
+
213
+ if (matched) {
214
+ return configure.blankLine;
215
+ }
216
+ }
217
+ return null;
218
+ }
219
+
147
220
  return {
148
221
  ClassBody(node) {
149
222
  const body = node.body;
@@ -158,22 +231,34 @@ module.exports = {
158
231
  const isPadded = afterPadding.loc.start.line - beforePadding.loc.end.line > 1;
159
232
  const hasTokenInPadding = hasTokenOrCommentBetween(beforePadding, afterPadding);
160
233
  const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0);
234
+ const paddingType = getPaddingType(body[i], body[i + 1]);
235
+
236
+ if (paddingType === "never" && isPadded) {
237
+ context.report({
238
+ node: body[i + 1],
239
+ messageId: "never",
161
240
 
162
- if ((options[0] === "always" && !skip && !isPadded) ||
163
- (options[0] === "never" && isPadded)) {
241
+ fix(fixer) {
242
+ if (hasTokenInPadding) {
243
+ return null;
244
+ }
245
+ return fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n");
246
+ }
247
+ });
248
+ } else if (paddingType === "always" && !skip && !isPadded) {
164
249
  context.report({
165
250
  node: body[i + 1],
166
- messageId: isPadded ? "never" : "always",
251
+ messageId: "always",
252
+
167
253
  fix(fixer) {
168
254
  if (hasTokenInPadding) {
169
255
  return null;
170
256
  }
171
- return isPadded
172
- ? fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n")
173
- : fixer.insertTextAfter(curLineLastToken, "\n");
257
+ return fixer.insertTextAfter(curLineLastToken, "\n");
174
258
  }
175
259
  });
176
260
  }
261
+
177
262
  }
178
263
  }
179
264
  };
@@ -16,6 +16,22 @@ const { directivesPattern } = require("../shared/directives");
16
16
 
17
17
  const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu;
18
18
 
19
+ /**
20
+ * Checks all segments in a set and returns true if any are reachable.
21
+ * @param {Set<CodePathSegment>} segments The segments to check.
22
+ * @returns {boolean} True if any segment is reachable; false otherwise.
23
+ */
24
+ function isAnySegmentReachable(segments) {
25
+
26
+ for (const segment of segments) {
27
+ if (segment.reachable) {
28
+ return true;
29
+ }
30
+ }
31
+
32
+ return false;
33
+ }
34
+
19
35
  /**
20
36
  * Checks whether or not a given comment string is really a fallthrough comment and not an ESLint directive.
21
37
  * @param {string} comment The comment string to check.
@@ -51,15 +67,6 @@ function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, f
51
67
  return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern));
52
68
  }
53
69
 
54
- /**
55
- * Checks whether or not a given code path segment is reachable.
56
- * @param {CodePathSegment} segment A CodePathSegment to check.
57
- * @returns {boolean} `true` if the segment is reachable.
58
- */
59
- function isReachable(segment) {
60
- return segment.reachable;
61
- }
62
-
63
70
  /**
64
71
  * Checks whether a node and a token are separated by blank lines
65
72
  * @param {ASTNode} node The node to check
@@ -109,7 +116,8 @@ module.exports = {
109
116
 
110
117
  create(context) {
111
118
  const options = context.options[0] || {};
112
- let currentCodePath = null;
119
+ const codePathSegments = [];
120
+ let currentCodePathSegments = new Set();
113
121
  const sourceCode = context.sourceCode;
114
122
  const allowEmptyCase = options.allowEmptyCase || false;
115
123
 
@@ -126,13 +134,33 @@ module.exports = {
126
134
  fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT;
127
135
  }
128
136
  return {
129
- onCodePathStart(codePath) {
130
- currentCodePath = codePath;
137
+
138
+ onCodePathStart() {
139
+ codePathSegments.push(currentCodePathSegments);
140
+ currentCodePathSegments = new Set();
131
141
  },
142
+
132
143
  onCodePathEnd() {
133
- currentCodePath = currentCodePath.upper;
144
+ currentCodePathSegments = codePathSegments.pop();
145
+ },
146
+
147
+ onUnreachableCodePathSegmentStart(segment) {
148
+ currentCodePathSegments.add(segment);
149
+ },
150
+
151
+ onUnreachableCodePathSegmentEnd(segment) {
152
+ currentCodePathSegments.delete(segment);
153
+ },
154
+
155
+ onCodePathSegmentStart(segment) {
156
+ currentCodePathSegments.add(segment);
134
157
  },
135
158
 
159
+ onCodePathSegmentEnd(segment) {
160
+ currentCodePathSegments.delete(segment);
161
+ },
162
+
163
+
136
164
  SwitchCase(node) {
137
165
 
138
166
  /*
@@ -157,7 +185,7 @@ module.exports = {
157
185
  * `break`, `return`, or `throw` are unreachable.
158
186
  * And allows empty cases and the last case.
159
187
  */
160
- if (currentCodePath.currentSegments.some(isReachable) &&
188
+ if (isAnySegmentReachable(currentCodePathSegments) &&
161
189
  (node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) &&
162
190
  node.parent.cases[node.parent.cases.length - 1] !== node) {
163
191
  fallthroughCase = node;
@@ -10,6 +10,7 @@
10
10
  //------------------------------------------------------------------------------
11
11
 
12
12
  const { findVariable } = require("@eslint-community/eslint-utils");
13
+ const astUtils = require("./utils/ast-utils");
13
14
 
14
15
  //------------------------------------------------------------------------------
15
16
  // Helpers
@@ -59,6 +60,78 @@ function isPromiseExecutor(node, scope) {
59
60
  isGlobalReference(parent.callee, getOuterScope(scope));
60
61
  }
61
62
 
63
+ /**
64
+ * Checks if the given node is a void expression.
65
+ * @param {ASTNode} node The node to check.
66
+ * @returns {boolean} - `true` if the node is a void expression
67
+ */
68
+ function expressionIsVoid(node) {
69
+ return node.type === "UnaryExpression" && node.operator === "void";
70
+ }
71
+
72
+ /**
73
+ * Fixes the linting error by prepending "void " to the given node
74
+ * @param {Object} sourceCode context given by context.sourceCode
75
+ * @param {ASTNode} node The node to fix.
76
+ * @param {Object} fixer The fixer object provided by ESLint.
77
+ * @returns {Array<Object>} - An array of fix objects to apply to the node.
78
+ */
79
+ function voidPrependFixer(sourceCode, node, fixer) {
80
+
81
+ const requiresParens =
82
+
83
+ // prepending `void ` will fail if the node has a lower precedence than void
84
+ astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) &&
85
+
86
+ // check if there are parentheses around the node to avoid redundant parentheses
87
+ !astUtils.isParenthesised(sourceCode, node);
88
+
89
+ // avoid parentheses issues
90
+ const returnOrArrowToken = sourceCode.getTokenBefore(
91
+ node,
92
+ node.parent.type === "ArrowFunctionExpression"
93
+ ? astUtils.isArrowToken
94
+
95
+ // isReturnToken
96
+ : token => token.type === "Keyword" && token.value === "return"
97
+ );
98
+
99
+ const firstToken = sourceCode.getTokenAfter(returnOrArrowToken);
100
+
101
+ const prependSpace =
102
+
103
+ // is return token, as => allows void to be adjacent
104
+ returnOrArrowToken.value === "return" &&
105
+
106
+ // If two tokens (return and "(") are adjacent
107
+ returnOrArrowToken.range[1] === firstToken.range[0];
108
+
109
+ return [
110
+ fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`),
111
+ fixer.insertTextAfter(node, requiresParens ? ")" : "")
112
+ ];
113
+ }
114
+
115
+ /**
116
+ * Fixes the linting error by `wrapping {}` around the given node's body.
117
+ * @param {Object} sourceCode context given by context.sourceCode
118
+ * @param {ASTNode} node The node to fix.
119
+ * @param {Object} fixer The fixer object provided by ESLint.
120
+ * @returns {Array<Object>} - An array of fix objects to apply to the node.
121
+ */
122
+ function curlyWrapFixer(sourceCode, node, fixer) {
123
+
124
+ // https://github.com/eslint/eslint/pull/17282#issuecomment-1592795923
125
+ const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
126
+ const firstToken = sourceCode.getTokenAfter(arrowToken);
127
+ const lastToken = sourceCode.getLastToken(node);
128
+
129
+ return [
130
+ fixer.insertTextBefore(firstToken, "{"),
131
+ fixer.insertTextAfter(lastToken, "}")
132
+ ];
133
+ }
134
+
62
135
  //------------------------------------------------------------------------------
63
136
  // Rule Definition
64
137
  //------------------------------------------------------------------------------
@@ -74,10 +147,27 @@ module.exports = {
74
147
  url: "https://eslint.org/docs/latest/rules/no-promise-executor-return"
75
148
  },
76
149
 
77
- schema: [],
150
+ hasSuggestions: true,
151
+
152
+ schema: [{
153
+ type: "object",
154
+ properties: {
155
+ allowVoid: {
156
+ type: "boolean",
157
+ default: false
158
+ }
159
+ },
160
+ additionalProperties: false
161
+ }],
78
162
 
79
163
  messages: {
80
- returnsValue: "Return values from promise executor functions cannot be read."
164
+ returnsValue: "Return values from promise executor functions cannot be read.",
165
+
166
+ // arrow and function suggestions
167
+ prependVoid: "Prepend `void` to the expression.",
168
+
169
+ // only arrow suggestions
170
+ wrapBraces: "Wrap the expression in `{}`."
81
171
  }
82
172
  },
83
173
 
@@ -85,26 +175,52 @@ module.exports = {
85
175
 
86
176
  let funcInfo = null;
87
177
  const sourceCode = context.sourceCode;
88
-
89
- /**
90
- * Reports the given node.
91
- * @param {ASTNode} node Node to report.
92
- * @returns {void}
93
- */
94
- function report(node) {
95
- context.report({ node, messageId: "returnsValue" });
96
- }
178
+ const {
179
+ allowVoid = false
180
+ } = context.options[0] || {};
97
181
 
98
182
  return {
99
183
 
100
184
  onCodePathStart(_, node) {
101
185
  funcInfo = {
102
186
  upper: funcInfo,
103
- shouldCheck: functionTypesToCheck.has(node.type) && isPromiseExecutor(node, sourceCode.getScope(node))
187
+ shouldCheck:
188
+ functionTypesToCheck.has(node.type) &&
189
+ isPromiseExecutor(node, sourceCode.getScope(node))
104
190
  };
105
191
 
106
- if (funcInfo.shouldCheck && node.type === "ArrowFunctionExpression" && node.expression) {
107
- report(node.body);
192
+ if (// Is a Promise executor
193
+ funcInfo.shouldCheck &&
194
+ node.type === "ArrowFunctionExpression" &&
195
+ node.expression &&
196
+
197
+ // Except void
198
+ !(allowVoid && expressionIsVoid(node.body))
199
+ ) {
200
+ const suggest = [];
201
+
202
+ // prevent useless refactors
203
+ if (allowVoid) {
204
+ suggest.push({
205
+ messageId: "prependVoid",
206
+ fix(fixer) {
207
+ return voidPrependFixer(sourceCode, node.body, fixer);
208
+ }
209
+ });
210
+ }
211
+
212
+ suggest.push({
213
+ messageId: "wrapBraces",
214
+ fix(fixer) {
215
+ return curlyWrapFixer(sourceCode, node, fixer);
216
+ }
217
+ });
218
+
219
+ context.report({
220
+ node: node.body,
221
+ messageId: "returnsValue",
222
+ suggest
223
+ });
108
224
  }
109
225
  },
110
226
 
@@ -113,9 +229,31 @@ module.exports = {
113
229
  },
114
230
 
115
231
  ReturnStatement(node) {
116
- if (funcInfo.shouldCheck && node.argument) {
117
- report(node);
232
+ if (!(funcInfo.shouldCheck && node.argument)) {
233
+ return;
118
234
  }
235
+
236
+ // node is `return <expression>`
237
+ if (!allowVoid) {
238
+ context.report({ node, messageId: "returnsValue" });
239
+ return;
240
+ }
241
+
242
+ if (expressionIsVoid(node.argument)) {
243
+ return;
244
+ }
245
+
246
+ // allowVoid && !expressionIsVoid
247
+ context.report({
248
+ node,
249
+ messageId: "returnsValue",
250
+ suggest: [{
251
+ messageId: "prependVoid",
252
+ fix(fixer) {
253
+ return voidPrependFixer(sourceCode, node.argument, fixer);
254
+ }
255
+ }]
256
+ });
119
257
  }
120
258
  };
121
259
  }
@@ -90,6 +90,21 @@ module.exports = {
90
90
  return Boolean(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends);
91
91
  }
92
92
 
93
+ /**
94
+ * Determines if every segment in a set has been called.
95
+ * @param {Set<CodePathSegment>} segments The segments to search.
96
+ * @returns {boolean} True if every segment has been called; false otherwise.
97
+ */
98
+ function isEverySegmentCalled(segments) {
99
+ for (const segment of segments) {
100
+ if (!isCalled(segment)) {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ return true;
106
+ }
107
+
93
108
  /**
94
109
  * Checks whether or not this is before `super()` is called.
95
110
  * @returns {boolean} `true` if this is before `super()` is called.
@@ -97,7 +112,7 @@ module.exports = {
97
112
  function isBeforeCallOfSuper() {
98
113
  return (
99
114
  isInConstructorOfDerivedClass() &&
100
- !funcInfo.codePath.currentSegments.every(isCalled)
115
+ !isEverySegmentCalled(funcInfo.currentSegments)
101
116
  );
102
117
  }
103
118
 
@@ -108,11 +123,9 @@ module.exports = {
108
123
  * @returns {void}
109
124
  */
110
125
  function setInvalid(node) {
111
- const segments = funcInfo.codePath.currentSegments;
112
-
113
- for (let i = 0; i < segments.length; ++i) {
114
- const segment = segments[i];
126
+ const segments = funcInfo.currentSegments;
115
127
 
128
+ for (const segment of segments) {
116
129
  if (segment.reachable) {
117
130
  segInfoMap[segment.id].invalidNodes.push(node);
118
131
  }
@@ -124,11 +137,9 @@ module.exports = {
124
137
  * @returns {void}
125
138
  */
126
139
  function setSuperCalled() {
127
- const segments = funcInfo.codePath.currentSegments;
128
-
129
- for (let i = 0; i < segments.length; ++i) {
130
- const segment = segments[i];
140
+ const segments = funcInfo.currentSegments;
131
141
 
142
+ for (const segment of segments) {
132
143
  if (segment.reachable) {
133
144
  segInfoMap[segment.id].superCalled = true;
134
145
  }
@@ -156,14 +167,16 @@ module.exports = {
156
167
  classNode.superClass &&
157
168
  !astUtils.isNullOrUndefined(classNode.superClass)
158
169
  ),
159
- codePath
170
+ codePath,
171
+ currentSegments: new Set()
160
172
  };
161
173
  } else {
162
174
  funcInfo = {
163
175
  upper: funcInfo,
164
176
  isConstructor: false,
165
177
  hasExtends: false,
166
- codePath
178
+ codePath,
179
+ currentSegments: new Set()
167
180
  };
168
181
  }
169
182
  },
@@ -211,6 +224,8 @@ module.exports = {
211
224
  * @returns {void}
212
225
  */
213
226
  onCodePathSegmentStart(segment) {
227
+ funcInfo.currentSegments.add(segment);
228
+
214
229
  if (!isInConstructorOfDerivedClass()) {
215
230
  return;
216
231
  }
@@ -225,6 +240,18 @@ module.exports = {
225
240
  };
226
241
  },
227
242
 
243
+ onUnreachableCodePathSegmentStart(segment) {
244
+ funcInfo.currentSegments.add(segment);
245
+ },
246
+
247
+ onUnreachableCodePathSegmentEnd(segment) {
248
+ funcInfo.currentSegments.delete(segment);
249
+ },
250
+
251
+ onCodePathSegmentEnd(segment) {
252
+ funcInfo.currentSegments.delete(segment);
253
+ },
254
+
228
255
  /**
229
256
  * Update information of the code path segment when a code path was
230
257
  * looped.
@@ -11,6 +11,22 @@
11
11
 
12
12
  const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"];
13
13
 
14
+ /**
15
+ * Checks all segments in a set and returns true if any are reachable.
16
+ * @param {Set<CodePathSegment>} segments The segments to check.
17
+ * @returns {boolean} True if any segment is reachable; false otherwise.
18
+ */
19
+ function isAnySegmentReachable(segments) {
20
+
21
+ for (const segment of segments) {
22
+ if (segment.reachable) {
23
+ return true;
24
+ }
25
+ }
26
+
27
+ return false;
28
+ }
29
+
14
30
  /**
15
31
  * Determines whether the given node is the first node in the code path to which a loop statement
16
32
  * 'loops' for the next iteration.
@@ -90,29 +106,36 @@ module.exports = {
90
106
  loopsByTargetSegments = new Map(),
91
107
  loopsToReport = new Set();
92
108
 
93
- let currentCodePath = null;
109
+ const codePathSegments = [];
110
+ let currentCodePathSegments = new Set();
94
111
 
95
112
  return {
96
- onCodePathStart(codePath) {
97
- currentCodePath = codePath;
113
+
114
+ onCodePathStart() {
115
+ codePathSegments.push(currentCodePathSegments);
116
+ currentCodePathSegments = new Set();
98
117
  },
99
118
 
100
119
  onCodePathEnd() {
101
- currentCodePath = currentCodePath.upper;
120
+ currentCodePathSegments = codePathSegments.pop();
102
121
  },
103
122
 
104
- [loopSelector](node) {
123
+ onUnreachableCodePathSegmentStart(segment) {
124
+ currentCodePathSegments.add(segment);
125
+ },
105
126
 
106
- /**
107
- * Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise.
108
- * For unreachable segments, the code path analysis does not raise events required for this implementation.
109
- */
110
- if (currentCodePath.currentSegments.some(segment => segment.reachable)) {
111
- loopsToReport.add(node);
112
- }
127
+ onUnreachableCodePathSegmentEnd(segment) {
128
+ currentCodePathSegments.delete(segment);
129
+ },
130
+
131
+ onCodePathSegmentEnd(segment) {
132
+ currentCodePathSegments.delete(segment);
113
133
  },
114
134
 
115
135
  onCodePathSegmentStart(segment, node) {
136
+
137
+ currentCodePathSegments.add(segment);
138
+
116
139
  if (isLoopingTarget(node)) {
117
140
  const loop = node.parent;
118
141
 
@@ -140,6 +163,18 @@ module.exports = {
140
163
  }
141
164
  },
142
165
 
166
+ [loopSelector](node) {
167
+
168
+ /**
169
+ * Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise.
170
+ * For unreachable segments, the code path analysis does not raise events required for this implementation.
171
+ */
172
+ if (isAnySegmentReachable(currentCodePathSegments)) {
173
+ loopsToReport.add(node);
174
+ }
175
+ },
176
+
177
+
143
178
  "Program:exit"() {
144
179
  loopsToReport.forEach(
145
180
  node => context.report({ node, messageId: "invalid" })