eslint 8.51.0 → 8.52.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.
package/README.md CHANGED
@@ -254,6 +254,11 @@ Francesco Trotta
254
254
  <img src="https://github.com/ota-meshi.png?s=75" width="75" height="75"><br />
255
255
  Yosuke Ota
256
256
  </a>
257
+ </td><td align="center" valign="top" width="11%">
258
+ <a href="https://github.com/Tanujkanti4441">
259
+ <img src="https://github.com/Tanujkanti4441.png?s=75" width="75" height="75"><br />
260
+ Tanuj Kanti
261
+ </a>
257
262
  </td></tr></tbody></table>
258
263
 
259
264
  ### Website Team
@@ -288,7 +293,7 @@ The following companies, organizations, and individuals support ESLint's ongoing
288
293
  <h3>Platinum Sponsors</h3>
289
294
  <p><a href="#"><img src="https://images.opencollective.com/2021-frameworks-fund/logo.png" alt="Chrome Frameworks Fund" height="undefined"></a> <a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
290
295
  <p><a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3>
291
- <p><a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://opensource.siemens.com"><img src="https://avatars.githubusercontent.com/u/624020?v=4" alt="Siemens" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a></p><h3>Bronze Sponsors</h3>
296
+ <p><a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a></p><h3>Bronze Sponsors</h3>
292
297
  <p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a> <a href="https://quickbookstoolhub.com"><img src="https://avatars.githubusercontent.com/u/95090305?u=e5bc398ef775c9ed19f955c675cdc1fb6abf01df&v=4" alt="QuickBooks Tool hub" height="32"></a></p>
293
298
  <!--sponsorsend-->
294
299
 
@@ -1,36 +1,28 @@
1
1
  {
2
- "types": [
3
- { "name": "problem", "displayName": "Possible Problems", "description": "These rules relate to possible logic errors in code:" },
4
- { "name": "suggestion", "displayName": "Suggestions", "description": "These rules suggest alternate ways of doing things:" },
5
- { "name": "layout", "displayName": "Layout & Formatting", "description": "These rules care about how the code looks rather than how it executes:" }
6
- ],
7
- "deprecated": {
8
- "name": "Deprecated",
9
- "description": "These rules have been deprecated in accordance with the <a href=\"{{ '/use/rule-deprecation' | url }}\">deprecation policy</a>, and replaced by newer rules:",
10
- "rules": []
2
+ "types": {
3
+ "problem": [],
4
+ "suggestion": [],
5
+ "layout": []
11
6
  },
12
- "removed": {
13
- "name": "Removed",
14
- "description": "These rules from older versions of ESLint (before the <a href=\"{{ '/use/rule-deprecation' | url }}\">deprecation policy</a> existed) have been replaced by newer rules:",
15
- "rules": [
16
- { "removed": "generator-star", "replacedBy": ["generator-star-spacing"] },
17
- { "removed": "global-strict", "replacedBy": ["strict"] },
18
- { "removed": "no-arrow-condition", "replacedBy": ["no-confusing-arrow", "no-constant-condition"] },
19
- { "removed": "no-comma-dangle", "replacedBy": ["comma-dangle"] },
20
- { "removed": "no-empty-class", "replacedBy": ["no-empty-character-class"] },
21
- { "removed": "no-empty-label", "replacedBy": ["no-labels"] },
22
- { "removed": "no-extra-strict", "replacedBy": ["strict"] },
23
- { "removed": "no-reserved-keys", "replacedBy": ["quote-props"] },
24
- { "removed": "no-space-before-semi", "replacedBy": ["semi-spacing"] },
25
- { "removed": "no-wrap-func", "replacedBy": ["no-extra-parens"] },
26
- { "removed": "space-after-function-name", "replacedBy": ["space-before-function-paren"] },
27
- { "removed": "space-after-keywords", "replacedBy": ["keyword-spacing"] },
28
- { "removed": "space-before-function-parentheses", "replacedBy": ["space-before-function-paren"] },
29
- { "removed": "space-before-keywords", "replacedBy": ["keyword-spacing"] },
30
- { "removed": "space-in-brackets", "replacedBy": ["object-curly-spacing", "array-bracket-spacing"] },
31
- { "removed": "space-return-throw-case", "replacedBy": ["keyword-spacing"] },
32
- { "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] },
33
- { "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] }
34
- ]
35
- }
7
+ "deprecated": [],
8
+ "removed": [
9
+ { "removed": "generator-star", "replacedBy": ["generator-star-spacing"] },
10
+ { "removed": "global-strict", "replacedBy": ["strict"] },
11
+ { "removed": "no-arrow-condition", "replacedBy": ["no-confusing-arrow", "no-constant-condition"] },
12
+ { "removed": "no-comma-dangle", "replacedBy": ["comma-dangle"] },
13
+ { "removed": "no-empty-class", "replacedBy": ["no-empty-character-class"] },
14
+ { "removed": "no-empty-label", "replacedBy": ["no-labels"] },
15
+ { "removed": "no-extra-strict", "replacedBy": ["strict"] },
16
+ { "removed": "no-reserved-keys", "replacedBy": ["quote-props"] },
17
+ { "removed": "no-space-before-semi", "replacedBy": ["semi-spacing"] },
18
+ { "removed": "no-wrap-func", "replacedBy": ["no-extra-parens"] },
19
+ { "removed": "space-after-function-name", "replacedBy": ["space-before-function-paren"] },
20
+ { "removed": "space-after-keywords", "replacedBy": ["keyword-spacing"] },
21
+ { "removed": "space-before-function-parentheses", "replacedBy": ["space-before-function-paren"] },
22
+ { "removed": "space-before-keywords", "replacedBy": ["keyword-spacing"] },
23
+ { "removed": "space-in-brackets", "replacedBy": ["object-curly-spacing", "array-bracket-spacing"] },
24
+ { "removed": "space-return-throw-case", "replacedBy": ["keyword-spacing"] },
25
+ { "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] },
26
+ { "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] }
27
+ ]
36
28
  }
package/lib/cli.js CHANGED
@@ -318,7 +318,14 @@ const cli = {
318
318
  options = CLIOptions.parse(args);
319
319
  } catch (error) {
320
320
  debug("Error parsing CLI options:", error.message);
321
- log.error(error.message);
321
+
322
+ let errorMessage = error.message;
323
+
324
+ if (usingFlatConfig) {
325
+ errorMessage += "\nYou're using eslint.config.js, some command line flags are no longer available. Please see https://eslint.org/docs/latest/use/command-line-interface for details.";
326
+ }
327
+
328
+ log.error(errorMessage);
322
329
  return 2;
323
330
  }
324
331
 
@@ -5,6 +5,16 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ //-----------------------------------------------------------------------------
9
+ // Requirements
10
+ //-----------------------------------------------------------------------------
11
+
12
+ /*
13
+ * Note: This can be removed in ESLint v9 because structuredClone is available globally
14
+ * starting in Node.js v17.
15
+ */
16
+ const structuredClone = require("@ungap/structured-clone").default;
17
+
8
18
  //-----------------------------------------------------------------------------
9
19
  // Type Definitions
10
20
  //-----------------------------------------------------------------------------
@@ -119,7 +129,7 @@ function normalizeRuleOptions(ruleOptions) {
119
129
  : [ruleOptions];
120
130
 
121
131
  finalOptions[0] = ruleSeverities.get(finalOptions[0]);
122
- return finalOptions;
132
+ return structuredClone(finalOptions);
123
133
  }
124
134
 
125
135
  //-----------------------------------------------------------------------------
@@ -378,48 +388,57 @@ const rulesSchema = {
378
388
  ...second
379
389
  };
380
390
 
381
- for (const ruleId of Object.keys(result)) {
382
-
383
- // avoid hairy edge case
384
- if (ruleId === "__proto__") {
385
-
386
- /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
387
- delete result.__proto__;
388
- continue;
389
- }
390
-
391
- result[ruleId] = normalizeRuleOptions(result[ruleId]);
392
-
393
- /*
394
- * If either rule config is missing, then the correct
395
- * config is already present and we just need to normalize
396
- * the severity.
397
- */
398
- if (!(ruleId in first) || !(ruleId in second)) {
399
- continue;
400
- }
401
391
 
402
- const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
403
- const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
392
+ for (const ruleId of Object.keys(result)) {
404
393
 
405
- /*
406
- * If the second rule config only has a severity (length of 1),
407
- * then use that severity and keep the rest of the options from
408
- * the first rule config.
409
- */
410
- if (secondRuleOptions.length === 1) {
411
- result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
412
- continue;
394
+ try {
395
+
396
+ // avoid hairy edge case
397
+ if (ruleId === "__proto__") {
398
+
399
+ /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
400
+ delete result.__proto__;
401
+ continue;
402
+ }
403
+
404
+ result[ruleId] = normalizeRuleOptions(result[ruleId]);
405
+
406
+ /*
407
+ * If either rule config is missing, then the correct
408
+ * config is already present and we just need to normalize
409
+ * the severity.
410
+ */
411
+ if (!(ruleId in first) || !(ruleId in second)) {
412
+ continue;
413
+ }
414
+
415
+ const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
416
+ const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
417
+
418
+ /*
419
+ * If the second rule config only has a severity (length of 1),
420
+ * then use that severity and keep the rest of the options from
421
+ * the first rule config.
422
+ */
423
+ if (secondRuleOptions.length === 1) {
424
+ result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
425
+ continue;
426
+ }
427
+
428
+ /*
429
+ * In any other situation, then the second rule config takes
430
+ * precedence. That means the value at `result[ruleId]` is
431
+ * already correct and no further work is necessary.
432
+ */
433
+ } catch (ex) {
434
+ throw new Error(`Key "${ruleId}": ${ex.message}`, { cause: ex });
413
435
  }
414
436
 
415
- /*
416
- * In any other situation, then the second rule config takes
417
- * precedence. That means the value at `result[ruleId]` is
418
- * already correct and no further work is necessary.
419
- */
420
437
  }
421
438
 
422
439
  return result;
440
+
441
+
423
442
  },
424
443
 
425
444
  validate(value) {
@@ -30,7 +30,7 @@ function compareLocations(itemA, itemB) {
30
30
 
31
31
  /**
32
32
  * Groups a set of directives into sub-arrays by their parent comment.
33
- * @param {Directive[]} directives Unused directives to be removed.
33
+ * @param {Iterable<Directive>} directives Unused directives to be removed.
34
34
  * @returns {Directive[][]} Directives grouped by their parent comment.
35
35
  */
36
36
  function groupByParentComment(directives) {
@@ -177,10 +177,10 @@ function createCommentRemoval(directives, commentToken) {
177
177
 
178
178
  /**
179
179
  * Parses details from directives to create output Problems.
180
- * @param {Directive[]} allDirectives Unused directives to be removed.
180
+ * @param {Iterable<Directive>} allDirectives Unused directives to be removed.
181
181
  * @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
182
182
  */
183
- function processUnusedDisableDirectives(allDirectives) {
183
+ function processUnusedDirectives(allDirectives) {
184
184
  const directiveGroups = groupByParentComment(allDirectives);
185
185
 
186
186
  return directiveGroups.flatMap(
@@ -199,6 +199,95 @@ function processUnusedDisableDirectives(allDirectives) {
199
199
  );
200
200
  }
201
201
 
202
+ /**
203
+ * Collect eslint-enable comments that are removing suppressions by eslint-disable comments.
204
+ * @param {Directive[]} directives The directives to check.
205
+ * @returns {Set<Directive>} The used eslint-enable comments
206
+ */
207
+ function collectUsedEnableDirectives(directives) {
208
+
209
+ /**
210
+ * A Map of `eslint-enable` keyed by ruleIds that may be marked as used.
211
+ * If `eslint-enable` does not have a ruleId, the key will be `null`.
212
+ * @type {Map<string|null, Directive>}
213
+ */
214
+ const enabledRules = new Map();
215
+
216
+ /**
217
+ * A Set of `eslint-enable` marked as used.
218
+ * It is also the return value of `collectUsedEnableDirectives` function.
219
+ * @type {Set<Directive>}
220
+ */
221
+ const usedEnableDirectives = new Set();
222
+
223
+ /*
224
+ * Checks the directives backwards to see if the encountered `eslint-enable` is used by the previous `eslint-disable`,
225
+ * and if so, stores the `eslint-enable` in `usedEnableDirectives`.
226
+ */
227
+ for (let index = directives.length - 1; index >= 0; index--) {
228
+ const directive = directives[index];
229
+
230
+ if (directive.type === "disable") {
231
+ if (enabledRules.size === 0) {
232
+ continue;
233
+ }
234
+ if (directive.ruleId === null) {
235
+
236
+ // If encounter `eslint-disable` without ruleId,
237
+ // mark all `eslint-enable` currently held in enabledRules as used.
238
+ // e.g.
239
+ // /* eslint-disable */ <- current directive
240
+ // /* eslint-enable rule-id1 */ <- used
241
+ // /* eslint-enable rule-id2 */ <- used
242
+ // /* eslint-enable */ <- used
243
+ for (const enableDirective of enabledRules.values()) {
244
+ usedEnableDirectives.add(enableDirective);
245
+ }
246
+ enabledRules.clear();
247
+ } else {
248
+ const enableDirective = enabledRules.get(directive.ruleId);
249
+
250
+ if (enableDirective) {
251
+
252
+ // If encounter `eslint-disable` with ruleId, and there is an `eslint-enable` with the same ruleId in enabledRules,
253
+ // mark `eslint-enable` with ruleId as used.
254
+ // e.g.
255
+ // /* eslint-disable rule-id */ <- current directive
256
+ // /* eslint-enable rule-id */ <- used
257
+ usedEnableDirectives.add(enableDirective);
258
+ } else {
259
+ const enabledDirectiveWithoutRuleId = enabledRules.get(null);
260
+
261
+ if (enabledDirectiveWithoutRuleId) {
262
+
263
+ // If encounter `eslint-disable` with ruleId, and there is no `eslint-enable` with the same ruleId in enabledRules,
264
+ // mark `eslint-enable` without ruleId as used.
265
+ // e.g.
266
+ // /* eslint-disable rule-id */ <- current directive
267
+ // /* eslint-enable */ <- used
268
+ usedEnableDirectives.add(enabledDirectiveWithoutRuleId);
269
+ }
270
+ }
271
+ }
272
+ } else if (directive.type === "enable") {
273
+ if (directive.ruleId === null) {
274
+
275
+ // If encounter `eslint-enable` without ruleId, the `eslint-enable` that follows it are unused.
276
+ // So clear enabledRules.
277
+ // e.g.
278
+ // /* eslint-enable */ <- current directive
279
+ // /* eslint-enable rule-id *// <- unused
280
+ // /* eslint-enable */ <- unused
281
+ enabledRules.clear();
282
+ enabledRules.set(null, directive);
283
+ } else {
284
+ enabledRules.set(directive.ruleId, directive);
285
+ }
286
+ }
287
+ }
288
+ return usedEnableDirectives;
289
+ }
290
+
202
291
  /**
203
292
  * This is the same as the exported function, except that it
204
293
  * doesn't handle disable-line and disable-next-line directives, and it always reports unused
@@ -206,7 +295,7 @@ function processUnusedDisableDirectives(allDirectives) {
206
295
  * @param {Object} options options for applying directives. This is the same as the options
207
296
  * for the exported function, except that `reportUnusedDisableDirectives` is not supported
208
297
  * (this function always reports unused disable directives).
209
- * @returns {{problems: LintMessage[], unusedDisableDirectives: LintMessage[]}} An object with a list
298
+ * @returns {{problems: LintMessage[], unusedDirectives: LintMessage[]}} An object with a list
210
299
  * of problems (including suppressed ones) and unused eslint-disable directives
211
300
  */
212
301
  function applyDirectives(options) {
@@ -258,17 +347,42 @@ function applyDirectives(options) {
258
347
  const unusedDisableDirectivesToReport = options.directives
259
348
  .filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive));
260
349
 
261
- const processed = processUnusedDisableDirectives(unusedDisableDirectivesToReport);
262
350
 
263
- const unusedDisableDirectives = processed
351
+ const unusedEnableDirectivesToReport = new Set(
352
+ options.directives.filter(directive => directive.unprocessedDirective.type === "enable")
353
+ );
354
+
355
+ /*
356
+ * If directives has the eslint-enable directive,
357
+ * check whether the eslint-enable comment is used.
358
+ */
359
+ if (unusedEnableDirectivesToReport.size > 0) {
360
+ for (const directive of collectUsedEnableDirectives(options.directives)) {
361
+ unusedEnableDirectivesToReport.delete(directive);
362
+ }
363
+ }
364
+
365
+ const processed = processUnusedDirectives(unusedDisableDirectivesToReport)
366
+ .concat(processUnusedDirectives(unusedEnableDirectivesToReport));
367
+
368
+ const unusedDirectives = processed
264
369
  .map(({ description, fix, unprocessedDirective }) => {
265
370
  const { parentComment, type, line, column } = unprocessedDirective;
266
371
 
372
+ let message;
373
+
374
+ if (type === "enable") {
375
+ message = description
376
+ ? `Unused eslint-enable directive (no matching eslint-disable directives were found for ${description}).`
377
+ : "Unused eslint-enable directive (no matching eslint-disable directives were found).";
378
+ } else {
379
+ message = description
380
+ ? `Unused eslint-disable directive (no problems were reported from ${description}).`
381
+ : "Unused eslint-disable directive (no problems were reported).";
382
+ }
267
383
  return {
268
384
  ruleId: null,
269
- message: description
270
- ? `Unused eslint-disable directive (no problems were reported from ${description}).`
271
- : "Unused eslint-disable directive (no problems were reported).",
385
+ message,
272
386
  line: type === "disable-next-line" ? parentComment.commentToken.loc.start.line : line,
273
387
  column: type === "disable-next-line" ? parentComment.commentToken.loc.start.column + 1 : column,
274
388
  severity: options.reportUnusedDisableDirectives === "warn" ? 1 : 2,
@@ -277,7 +391,7 @@ function applyDirectives(options) {
277
391
  };
278
392
  });
279
393
 
280
- return { problems, unusedDisableDirectives };
394
+ return { problems, unusedDirectives };
281
395
  }
282
396
 
283
397
  /**
@@ -344,8 +458,8 @@ module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirec
344
458
 
345
459
  return reportUnusedDisableDirectives !== "off"
346
460
  ? lineDirectivesResult.problems
347
- .concat(blockDirectivesResult.unusedDisableDirectives)
348
- .concat(lineDirectivesResult.unusedDisableDirectives)
461
+ .concat(blockDirectivesResult.unusedDirectives)
462
+ .concat(lineDirectivesResult.unusedDirectives)
349
463
  .sort(compareLocations)
350
464
  : lineDirectivesResult.problems;
351
465
  };
package/lib/options.js CHANGED
@@ -47,7 +47,7 @@ const optionator = require("optionator");
47
47
  * @property {Object} [parserOptions] Specify parser options
48
48
  * @property {string[]} [plugin] Specify plugins
49
49
  * @property {string} [printConfig] Print the configuration for the given file
50
- * @property {boolean | undefined} reportUnusedDisableDirectives Adds reported errors for unused eslint-disable directives
50
+ * @property {boolean | undefined} reportUnusedDisableDirectives Adds reported errors for unused eslint-disable and eslint-enable directives
51
51
  * @property {string} [resolvePluginsRelativeTo] A folder where plugins should be resolved from, CWD by default
52
52
  * @property {Object} [rule] Specify rules
53
53
  * @property {string[]} [rulesdir] Load additional rules from this directory. Deprecated: Use rules from plugins
@@ -304,7 +304,7 @@ module.exports = function(usingFlatConfig) {
304
304
  option: "report-unused-disable-directives",
305
305
  type: "Boolean",
306
306
  default: void 0,
307
- description: "Adds reported errors for unused eslint-disable directives"
307
+ description: "Adds reported errors for unused eslint-disable and eslint-enable directives"
308
308
  },
309
309
  {
310
310
  heading: "Caching"
@@ -9,12 +9,51 @@
9
9
  // Requirements
10
10
  //------------------------------------------------------------------------------
11
11
 
12
- const { getVariableByName, isArrowToken } = require("./utils/ast-utils");
12
+ const { getVariableByName, isArrowToken, isClosingBraceToken, isClosingParenToken } = require("./utils/ast-utils");
13
13
 
14
14
  //------------------------------------------------------------------------------
15
15
  // Helpers
16
16
  //------------------------------------------------------------------------------
17
17
 
18
+ const BREAK_OR_CONTINUE = new Set(["BreakStatement", "ContinueStatement"]);
19
+
20
+ // Declaration types that must contain a string Literal node at the end.
21
+ const DECLARATIONS = new Set(["ExportAllDeclaration", "ExportNamedDeclaration", "ImportDeclaration"]);
22
+
23
+ const IDENTIFIER_OR_KEYWORD = new Set(["Identifier", "Keyword"]);
24
+
25
+ // Keywords that can immediately precede an ExpressionStatement node, mapped to the their node types.
26
+ const NODE_TYPES_BY_KEYWORD = {
27
+ __proto__: null,
28
+ break: "BreakStatement",
29
+ continue: "ContinueStatement",
30
+ debugger: "DebuggerStatement",
31
+ do: "DoWhileStatement",
32
+ else: "IfStatement",
33
+ return: "ReturnStatement",
34
+ yield: "YieldExpression"
35
+ };
36
+
37
+ /*
38
+ * Before an opening parenthesis, `>` (for JSX), and postfix `++` and `--` always trigger ASI;
39
+ * the tokens `:`, `;`, `{` and `=>` don't expect a semicolon, as that would count as an empty statement.
40
+ */
41
+ const PUNCTUATORS = new Set([":", ";", ">", "{", "=>", "++", "--"]);
42
+
43
+ /*
44
+ * Statements that can contain an `ExpressionStatement` after a closing parenthesis.
45
+ * DoWhileStatement is an exception in that it always triggers ASI after the closing parenthesis.
46
+ */
47
+ const STATEMENTS = new Set([
48
+ "DoWhileStatement",
49
+ "ForInStatement",
50
+ "ForOfStatement",
51
+ "ForStatement",
52
+ "IfStatement",
53
+ "WhileStatement",
54
+ "WithStatement"
55
+ ]);
56
+
18
57
  /**
19
58
  * Tests if a node appears at the beginning of an ancestor ExpressionStatement node.
20
59
  * @param {ASTNode} node The node to check.
@@ -53,7 +92,8 @@ module.exports = {
53
92
 
54
93
  messages: {
55
94
  preferLiteral: "The object literal notation {} is preferable.",
56
- useLiteral: "Replace with '{{replacement}}'."
95
+ useLiteral: "Replace with '{{replacement}}'.",
96
+ useLiteralAfterSemicolon: "Replace with '{{replacement}}', add preceding semicolon."
57
97
  }
58
98
  },
59
99
 
@@ -80,6 +120,50 @@ module.exports = {
80
120
  return false;
81
121
  }
82
122
 
123
+ /**
124
+ * Determines whether a parenthesized object literal that replaces a specified node needs to be preceded by a semicolon.
125
+ * @param {ASTNode} node The node to be replaced. This node should be at the start of an `ExpressionStatement` or at the start of the body of an `ArrowFunctionExpression`.
126
+ * @returns {boolean} Whether a semicolon is required before the parenthesized object literal.
127
+ */
128
+ function needsSemicolon(node) {
129
+ const prevToken = sourceCode.getTokenBefore(node);
130
+
131
+ if (!prevToken || prevToken.type === "Punctuator" && PUNCTUATORS.has(prevToken.value)) {
132
+ return false;
133
+ }
134
+
135
+ const prevNode = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
136
+
137
+ if (isClosingParenToken(prevToken)) {
138
+ return !STATEMENTS.has(prevNode.type);
139
+ }
140
+
141
+ if (isClosingBraceToken(prevToken)) {
142
+ return (
143
+ prevNode.type === "BlockStatement" && prevNode.parent.type === "FunctionExpression" ||
144
+ prevNode.type === "ClassBody" && prevNode.parent.type === "ClassExpression" ||
145
+ prevNode.type === "ObjectExpression"
146
+ );
147
+ }
148
+
149
+ if (IDENTIFIER_OR_KEYWORD.has(prevToken.type)) {
150
+ if (BREAK_OR_CONTINUE.has(prevNode.parent.type)) {
151
+ return false;
152
+ }
153
+
154
+ const keyword = prevToken.value;
155
+ const nodeType = NODE_TYPES_BY_KEYWORD[keyword];
156
+
157
+ return prevNode.type !== nodeType;
158
+ }
159
+
160
+ if (prevToken.type === "String") {
161
+ return !DECLARATIONS.has(prevNode.parent.type);
162
+ }
163
+
164
+ return true;
165
+ }
166
+
83
167
  /**
84
168
  * Reports on nodes where the `Object` constructor is called without arguments.
85
169
  * @param {ASTNode} node The node to evaluate.
@@ -93,16 +177,30 @@ module.exports = {
93
177
  const variable = getVariableByName(sourceCode.getScope(node), "Object");
94
178
 
95
179
  if (variable && variable.identifiers.length === 0) {
96
- const replacement = needsParentheses(node) ? "({})" : "{}";
180
+ let replacement;
181
+ let fixText;
182
+ let messageId = "useLiteral";
183
+
184
+ if (needsParentheses(node)) {
185
+ replacement = "({})";
186
+ if (needsSemicolon(node)) {
187
+ fixText = ";({})";
188
+ messageId = "useLiteralAfterSemicolon";
189
+ } else {
190
+ fixText = "({})";
191
+ }
192
+ } else {
193
+ replacement = fixText = "{}";
194
+ }
97
195
 
98
196
  context.report({
99
197
  node,
100
198
  messageId: "preferLiteral",
101
199
  suggest: [
102
200
  {
103
- messageId: "useLiteral",
201
+ messageId,
104
202
  data: { replacement },
105
- fix: fixer => fixer.replaceText(node, replacement)
203
+ fix: fixer => fixer.replaceText(node, fixText)
106
204
  }
107
205
  ]
108
206
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "8.51.0",
3
+ "version": "8.52.0",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {
@@ -63,10 +63,11 @@
63
63
  "@eslint-community/eslint-utils": "^4.2.0",
64
64
  "@eslint-community/regexpp": "^4.6.1",
65
65
  "@eslint/eslintrc": "^2.1.2",
66
- "@eslint/js": "8.51.0",
67
- "@humanwhocodes/config-array": "^0.11.11",
66
+ "@eslint/js": "8.52.0",
67
+ "@humanwhocodes/config-array": "^0.11.13",
68
68
  "@humanwhocodes/module-importer": "^1.0.1",
69
69
  "@nodelib/fs.walk": "^1.2.8",
70
+ "@ungap/structured-clone": "^1.2.0",
70
71
  "ajv": "^6.12.4",
71
72
  "chalk": "^4.0.0",
72
73
  "cross-spawn": "^7.0.2",