eslint 8.22.0 → 8.33.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 (80) hide show
  1. package/README.md +51 -45
  2. package/bin/eslint.js +2 -4
  3. package/conf/globals.js +6 -1
  4. package/conf/rule-type-list.json +2 -2
  5. package/lib/cli-engine/file-enumerator.js +4 -2
  6. package/lib/cli-engine/formatters/formatters-meta.json +46 -0
  7. package/lib/cli-engine/formatters/html.js +76 -51
  8. package/lib/cli.js +163 -40
  9. package/lib/config/default-config.js +2 -2
  10. package/lib/config/flat-config-array.js +1 -1
  11. package/lib/eslint/eslint-helpers.js +409 -87
  12. package/lib/eslint/eslint.js +5 -2
  13. package/lib/eslint/flat-eslint.js +113 -110
  14. package/lib/linter/code-path-analysis/code-path-segment.js +2 -2
  15. package/lib/linter/code-path-analysis/code-path-state.js +7 -7
  16. package/lib/linter/code-path-analysis/debug-helpers.js +3 -3
  17. package/lib/linter/code-path-analysis/id-generator.js +2 -2
  18. package/lib/linter/config-comment-parser.js +1 -2
  19. package/lib/linter/linter.js +17 -7
  20. package/lib/linter/timing.js +4 -4
  21. package/lib/options.js +293 -239
  22. package/lib/rule-tester/flat-rule-tester.js +13 -11
  23. package/lib/rule-tester/rule-tester.js +15 -11
  24. package/lib/rules/array-callback-return.js +2 -2
  25. package/lib/rules/comma-dangle.js +3 -3
  26. package/lib/rules/for-direction.js +1 -1
  27. package/lib/rules/func-name-matching.js +2 -2
  28. package/lib/rules/getter-return.js +14 -8
  29. package/lib/rules/global-require.js +2 -1
  30. package/lib/rules/id-length.js +43 -2
  31. package/lib/rules/indent-legacy.js +4 -4
  32. package/lib/rules/indent.js +23 -15
  33. package/lib/rules/index.js +3 -0
  34. package/lib/rules/key-spacing.js +50 -38
  35. package/lib/rules/lines-around-comment.js +2 -2
  36. package/lib/rules/logical-assignment-operators.js +474 -0
  37. package/lib/rules/multiline-ternary.js +2 -2
  38. package/lib/rules/new-cap.js +2 -2
  39. package/lib/rules/no-else-return.js +1 -1
  40. package/lib/rules/no-empty-static-block.js +47 -0
  41. package/lib/rules/no-empty.js +19 -2
  42. package/lib/rules/no-extra-boolean-cast.js +1 -1
  43. package/lib/rules/no-extra-parens.js +18 -3
  44. package/lib/rules/no-fallthrough.js +26 -5
  45. package/lib/rules/no-implicit-coercion.js +20 -1
  46. package/lib/rules/no-implicit-globals.js +5 -0
  47. package/lib/rules/no-invalid-regexp.js +40 -18
  48. package/lib/rules/no-labels.js +1 -1
  49. package/lib/rules/no-lone-blocks.js +1 -1
  50. package/lib/rules/no-loss-of-precision.js +2 -2
  51. package/lib/rules/no-magic-numbers.js +18 -1
  52. package/lib/rules/no-misleading-character-class.js +4 -4
  53. package/lib/rules/no-new-native-nonconstructor.js +64 -0
  54. package/lib/rules/no-obj-calls.js +1 -1
  55. package/lib/rules/no-restricted-exports.js +106 -10
  56. package/lib/rules/no-return-await.js +28 -1
  57. package/lib/rules/no-underscore-dangle.js +36 -11
  58. package/lib/rules/no-unneeded-ternary.js +1 -1
  59. package/lib/rules/no-use-before-define.js +1 -1
  60. package/lib/rules/no-useless-computed-key.js +1 -1
  61. package/lib/rules/no-var.js +2 -2
  62. package/lib/rules/no-warning-comments.js +24 -5
  63. package/lib/rules/padded-blocks.js +1 -1
  64. package/lib/rules/prefer-arrow-callback.js +4 -3
  65. package/lib/rules/prefer-const.js +13 -1
  66. package/lib/rules/prefer-named-capture-group.js +71 -6
  67. package/lib/rules/prefer-object-spread.js +1 -1
  68. package/lib/rules/prefer-regex-literals.js +147 -32
  69. package/lib/rules/prefer-rest-params.js +1 -1
  70. package/lib/rules/require-yield.js +0 -1
  71. package/lib/rules/strict.js +1 -1
  72. package/lib/rules/utils/ast-utils.js +10 -4
  73. package/lib/shared/directives.js +15 -0
  74. package/lib/shared/logging.js +1 -1
  75. package/lib/shared/runtime-info.js +1 -1
  76. package/lib/shared/traverser.js +1 -1
  77. package/lib/shared/types.js +15 -2
  78. package/lib/source-code/token-store/cursor.js +1 -1
  79. package/messages/print-config-with-directory-path.js +1 -1
  80. package/package.json +27 -27
@@ -53,6 +53,14 @@ module.exports = {
53
53
  enforceInClassFields: {
54
54
  type: "boolean",
55
55
  default: false
56
+ },
57
+ allowInArrayDestructuring: {
58
+ type: "boolean",
59
+ default: true
60
+ },
61
+ allowInObjectDestructuring: {
62
+ type: "boolean",
63
+ default: true
56
64
  }
57
65
  },
58
66
  additionalProperties: false
@@ -74,6 +82,8 @@ module.exports = {
74
82
  const enforceInMethodNames = typeof options.enforceInMethodNames !== "undefined" ? options.enforceInMethodNames : false;
75
83
  const enforceInClassFields = typeof options.enforceInClassFields !== "undefined" ? options.enforceInClassFields : false;
76
84
  const allowFunctionParams = typeof options.allowFunctionParams !== "undefined" ? options.allowFunctionParams : true;
85
+ const allowInArrayDestructuring = typeof options.allowInArrayDestructuring !== "undefined" ? options.allowInArrayDestructuring : true;
86
+ const allowInObjectDestructuring = typeof options.allowInObjectDestructuring !== "undefined" ? options.allowInObjectDestructuring : true;
77
87
 
78
88
  //-------------------------------------------------------------------------
79
89
  // Helpers
@@ -195,6 +205,7 @@ module.exports = {
195
205
  checkForDanglingUnderscoreInFunctionParameters(node);
196
206
  }
197
207
 
208
+
198
209
  /**
199
210
  * Check if variable expression has a dangling underscore
200
211
  * @param {ASTNode} node node to evaluate
@@ -202,18 +213,32 @@ module.exports = {
202
213
  * @private
203
214
  */
204
215
  function checkForDanglingUnderscoreInVariableExpression(node) {
205
- const identifier = node.id.name;
216
+ context.getDeclaredVariables(node).forEach(variable => {
217
+ const definition = variable.defs.find(def => def.node === node);
218
+ const identifierNode = definition.name;
219
+ const identifier = identifierNode.name;
220
+ let parent = identifierNode.parent;
221
+
222
+ while (!["VariableDeclarator", "ArrayPattern", "ObjectPattern"].includes(parent.type)) {
223
+ parent = parent.parent;
224
+ }
206
225
 
207
- if (typeof identifier !== "undefined" && hasDanglingUnderscore(identifier) &&
208
- !isSpecialCaseIdentifierInVariableExpression(identifier) && !isAllowed(identifier)) {
209
- context.report({
210
- node,
211
- messageId: "unexpectedUnderscore",
212
- data: {
213
- identifier
214
- }
215
- });
216
- }
226
+ if (
227
+ hasDanglingUnderscore(identifier) &&
228
+ !isSpecialCaseIdentifierInVariableExpression(identifier) &&
229
+ !isAllowed(identifier) &&
230
+ !(allowInArrayDestructuring && parent.type === "ArrayPattern") &&
231
+ !(allowInObjectDestructuring && parent.type === "ObjectPattern")
232
+ ) {
233
+ context.report({
234
+ node,
235
+ messageId: "unexpectedUnderscore",
236
+ data: {
237
+ identifier
238
+ }
239
+ });
240
+ }
241
+ });
217
242
  }
218
243
 
219
244
  /**
@@ -144,7 +144,7 @@ module.exports = {
144
144
  context.report({
145
145
  node,
146
146
  messageId: "unnecessaryConditionalAssignment",
147
- fix: fixer => {
147
+ fix(fixer) {
148
148
  const shouldParenthesizeAlternate =
149
149
  (
150
150
  astUtils.getPrecedence(node.alternate) < OR_PRECEDENCE ||
@@ -68,7 +68,7 @@ function isInClassStaticInitializerRange(node, location) {
68
68
  }
69
69
 
70
70
  /**
71
- * Checks whether a given scope is the scope of a a class static initializer.
71
+ * Checks whether a given scope is the scope of a class static initializer.
72
72
  * Static initializers are static blocks and initializers of static fields.
73
73
  * @param {eslint-scope.Scope} scope A scope to check.
74
74
  * @returns {boolean} `true` if the scope is a class static initializer scope.
@@ -74,7 +74,7 @@ function hasUselessComputedKey(node) {
74
74
 
75
75
  return value !== "constructor";
76
76
 
77
- /* istanbul ignore next */
77
+ /* c8 ignore next */
78
78
  default:
79
79
  throw new Error(`Unexpected node type: ${node.type}`);
80
80
  }
@@ -90,7 +90,7 @@ function getScopeNode(node) {
90
90
  }
91
91
  }
92
92
 
93
- /* istanbul ignore next : unreachable */
93
+ /* c8 ignore next */
94
94
  return null;
95
95
  }
96
96
 
@@ -159,7 +159,7 @@ function hasReferenceInTDZ(node) {
159
159
  return !reference.init && (
160
160
  start < idStart ||
161
161
  (defaultValue !== null && start >= defaultStart && end <= defaultEnd) ||
162
- (start >= initStart && end <= initEnd)
162
+ (!astUtils.isFunction(node) && start >= initStart && end <= initEnd)
163
163
  );
164
164
  });
165
165
  };
@@ -37,6 +37,15 @@ module.exports = {
37
37
  },
38
38
  location: {
39
39
  enum: ["start", "anywhere"]
40
+ },
41
+ decoration: {
42
+ type: "array",
43
+ items: {
44
+ type: "string",
45
+ pattern: "^\\S$"
46
+ },
47
+ minItems: 1,
48
+ uniqueItems: true
40
49
  }
41
50
  },
42
51
  additionalProperties: false
@@ -53,6 +62,7 @@ module.exports = {
53
62
  configuration = context.options[0] || {},
54
63
  warningTerms = configuration.terms || ["todo", "fixme", "xxx"],
55
64
  location = configuration.location || "start",
65
+ decoration = [...configuration.decoration || []].join(""),
56
66
  selfConfigRegEx = /\bno-warning-comments\b/u;
57
67
 
58
68
  /**
@@ -64,6 +74,7 @@ module.exports = {
64
74
  */
65
75
  function convertToRegExp(term) {
66
76
  const escaped = escapeRegExp(term);
77
+ const escapedDecoration = escapeRegExp(decoration);
67
78
 
68
79
  /*
69
80
  * When matching at the start, ignore leading whitespace, and
@@ -74,18 +85,23 @@ module.exports = {
74
85
  * e.g. terms ["TODO"] matches `//TODO something`
75
86
  * $ handles any terms at the end of a comment
76
87
  * e.g. terms ["TODO"] matches `// something TODO`
77
- * \s* handles optional leading spaces (for "start" location only)
78
- * e.g. terms ["TODO"] matches `// TODO something`
79
88
  * \b handles terms preceded/followed by word boundary
80
89
  * e.g. terms: ["!FIX", "FIX!"] matches `// FIX!something` or `// something!FIX`
81
90
  * terms: ["FIX"] matches `// FIX!` or `// !FIX`, but not `// fixed or affix`
91
+ *
92
+ * For location start:
93
+ * [\s]* handles optional leading spaces
94
+ * e.g. terms ["TODO"] matches `// TODO something`
95
+ * [\s\*]* (where "\*" is the escaped string of decoration)
96
+ * handles optional leading spaces or decoration characters (for "start" location only)
97
+ * e.g. terms ["TODO"] matches `/**** TODO something ... `
82
98
  */
83
99
  const wordBoundary = "\\b";
84
100
 
85
101
  let prefix = "";
86
102
 
87
103
  if (location === "start") {
88
- prefix = "^\\s*";
104
+ prefix = `^[\\s${escapedDecoration}]*`;
89
105
  } else if (/^\w/u.test(term)) {
90
106
  prefix = wordBoundary;
91
107
  }
@@ -95,12 +111,15 @@ module.exports = {
95
111
 
96
112
  /*
97
113
  * For location "start", the typical regex is:
98
- * /^\s*ESCAPED_TERM\b/iu.
114
+ * /^[\s]*ESCAPED_TERM\b/iu.
115
+ * Or if decoration characters are specified (e.g. "*"), then any of
116
+ * those characters may appear in any order at the start:
117
+ * /^[\s\*]*ESCAPED_TERM\b/iu.
99
118
  *
100
119
  * For location "anywhere" the typical regex is
101
120
  * /\bESCAPED_TERM\b/iu
102
121
  *
103
- * If it starts or ends with non-word character, the prefix and suffix empty, respectively.
122
+ * If it starts or ends with non-word character, the prefix and suffix are empty, respectively.
104
123
  */
105
124
  return new RegExp(`${prefix}${escaped}${suffix}`, flags);
106
125
  }
@@ -186,7 +186,7 @@ module.exports = {
186
186
  case "ClassBody":
187
187
  return options.classes;
188
188
 
189
- /* istanbul ignore next */
189
+ /* c8 ignore next */
190
190
  default:
191
191
  throw new Error("unreachable");
192
192
  }
@@ -53,7 +53,7 @@ function getVariableOfArguments(scope) {
53
53
  }
54
54
  }
55
55
 
56
- /* istanbul ignore next */
56
+ /* c8 ignore next */
57
57
  return null;
58
58
  }
59
59
 
@@ -126,7 +126,7 @@ function getCallbackInfo(node) {
126
126
  parent = parent.parent;
127
127
  }
128
128
 
129
- /* istanbul ignore next */
129
+ /* c8 ignore next */
130
130
  throw new Error("unreachable");
131
131
  }
132
132
 
@@ -335,6 +335,7 @@ module.exports = {
335
335
  // Convert the function expression to an arrow function.
336
336
  const functionToken = sourceCode.getFirstToken(node, node.async ? 1 : 0);
337
337
  const leftParenToken = sourceCode.getTokenAfter(functionToken, astUtils.isOpeningParenToken);
338
+ const tokenBeforeBody = sourceCode.getTokenBefore(node.body);
338
339
 
339
340
  if (sourceCode.commentsExistBetween(functionToken, leftParenToken)) {
340
341
 
@@ -348,7 +349,7 @@ module.exports = {
348
349
  // Remove extra tokens and spaces.
349
350
  yield fixer.removeRange([functionToken.range[0], leftParenToken.range[0]]);
350
351
  }
351
- yield fixer.insertTextBefore(node.body, "=> ");
352
+ yield fixer.insertTextAfter(tokenBeforeBody, " =>");
352
353
 
353
354
  // Get the node that will become the new arrow function.
354
355
  let replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
@@ -446,7 +446,19 @@ module.exports = {
446
446
 
447
447
  reportCount += nodesToReport.length;
448
448
 
449
- shouldFix = shouldFix && (reportCount === varDeclParent.declarations.length);
449
+ let totalDeclarationsCount = 0;
450
+
451
+ varDeclParent.declarations.forEach(declaration => {
452
+ if (declaration.id.type === "ObjectPattern") {
453
+ totalDeclarationsCount += declaration.id.properties.length;
454
+ } else if (declaration.id.type === "ArrayPattern") {
455
+ totalDeclarationsCount += declaration.id.elements.length;
456
+ } else {
457
+ totalDeclarationsCount += 1;
458
+ }
459
+ });
460
+
461
+ shouldFix = shouldFix && (reportCount === totalDeclarationsCount);
450
462
  }
451
463
  }
452
464
 
@@ -23,6 +23,61 @@ const regexpp = require("regexpp");
23
23
 
24
24
  const parser = new regexpp.RegExpParser();
25
25
 
26
+ /**
27
+ * Creates fixer suggestions for the regex, if statically determinable.
28
+ * @param {number} groupStart Starting index of the regex group.
29
+ * @param {string} pattern The regular expression pattern to be checked.
30
+ * @param {string} rawText Source text of the regexNode.
31
+ * @param {ASTNode} regexNode AST node which contains the regular expression.
32
+ * @returns {Array<SuggestionResult>} Fixer suggestions for the regex, if statically determinable.
33
+ */
34
+ function suggestIfPossible(groupStart, pattern, rawText, regexNode) {
35
+ switch (regexNode.type) {
36
+ case "Literal":
37
+ if (typeof regexNode.value === "string" && rawText.includes("\\")) {
38
+ return null;
39
+ }
40
+ break;
41
+ case "TemplateLiteral":
42
+ if (regexNode.expressions.length || rawText.slice(1, -1) !== pattern) {
43
+ return null;
44
+ }
45
+ break;
46
+ default:
47
+ return null;
48
+ }
49
+
50
+ const start = regexNode.range[0] + groupStart + 2;
51
+
52
+ return [
53
+ {
54
+ fix(fixer) {
55
+ const existingTemps = pattern.match(/temp\d+/gu) || [];
56
+ const highestTempCount = existingTemps.reduce(
57
+ (previous, next) =>
58
+ Math.max(previous, Number(next.slice("temp".length))),
59
+ 0
60
+ );
61
+
62
+ return fixer.insertTextBeforeRange(
63
+ [start, start],
64
+ `?<temp${highestTempCount + 1}>`
65
+ );
66
+ },
67
+ messageId: "addGroupName"
68
+ },
69
+ {
70
+ fix(fixer) {
71
+ return fixer.insertTextBeforeRange(
72
+ [start, start],
73
+ "?:"
74
+ );
75
+ },
76
+ messageId: "addNonCapture"
77
+ }
78
+ ];
79
+ }
80
+
26
81
  //------------------------------------------------------------------------------
27
82
  // Rule Definition
28
83
  //------------------------------------------------------------------------------
@@ -38,23 +93,29 @@ module.exports = {
38
93
  url: "https://eslint.org/docs/rules/prefer-named-capture-group"
39
94
  },
40
95
 
96
+ hasSuggestions: true,
97
+
41
98
  schema: [],
42
99
 
43
100
  messages: {
101
+ addGroupName: "Add name to capture group.",
102
+ addNonCapture: "Convert group to non-capturing.",
44
103
  required: "Capture group '{{group}}' should be converted to a named or non-capturing group."
45
104
  }
46
105
  },
47
106
 
48
107
  create(context) {
108
+ const sourceCode = context.getSourceCode();
49
109
 
50
110
  /**
51
111
  * Function to check regular expression.
52
- * @param {string} pattern The regular expression pattern to be check.
53
- * @param {ASTNode} node AST node which contains regular expression.
112
+ * @param {string} pattern The regular expression pattern to be checked.
113
+ * @param {ASTNode} node AST node which contains the regular expression or a call/new expression.
114
+ * @param {ASTNode} regexNode AST node which contains the regular expression.
54
115
  * @param {boolean} uFlag Flag indicates whether unicode mode is enabled or not.
55
116
  * @returns {void}
56
117
  */
57
- function checkRegex(pattern, node, uFlag) {
118
+ function checkRegex(pattern, node, regexNode, uFlag) {
58
119
  let ast;
59
120
 
60
121
  try {
@@ -68,12 +129,16 @@ module.exports = {
68
129
  regexpp.visitRegExpAST(ast, {
69
130
  onCapturingGroupEnter(group) {
70
131
  if (!group.name) {
132
+ const rawText = sourceCode.getText(regexNode);
133
+ const suggest = suggestIfPossible(group.start, pattern, rawText, regexNode);
134
+
71
135
  context.report({
72
136
  node,
73
137
  messageId: "required",
74
138
  data: {
75
139
  group: group.raw
76
- }
140
+ },
141
+ suggest
77
142
  });
78
143
  }
79
144
  }
@@ -83,7 +148,7 @@ module.exports = {
83
148
  return {
84
149
  Literal(node) {
85
150
  if (node.regex) {
86
- checkRegex(node.regex.pattern, node, node.regex.flags.includes("u"));
151
+ checkRegex(node.regex.pattern, node, node, node.regex.flags.includes("u"));
87
152
  }
88
153
  },
89
154
  Program() {
@@ -101,7 +166,7 @@ module.exports = {
101
166
  const flags = getStringIfConstant(node.arguments[1]);
102
167
 
103
168
  if (regex) {
104
- checkRegex(regex, node, flags && flags.includes("u"));
169
+ checkRegex(regex, node, node.arguments[0], flags && flags.includes("u"));
105
170
  }
106
171
  }
107
172
  }
@@ -247,7 +247,7 @@ module.exports = {
247
247
 
248
248
  docs: {
249
249
  description:
250
- "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead.",
250
+ "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead",
251
251
  recommended: false,
252
252
  url: "https://eslint.org/docs/rules/prefer-object-spread"
253
253
  },
@@ -146,6 +146,8 @@ module.exports = {
146
146
  messages: {
147
147
  unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.",
148
148
  replaceWithLiteral: "Replace with an equivalent regular expression literal.",
149
+ replaceWithLiteralAndFlags: "Replace with an equivalent regular expression literal with flags '{{ flags }}'.",
150
+ replaceWithIntendedLiteralAndFlags: "Replace with a regular expression literal with flags '{{ flags }}'.",
149
151
  unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
150
152
  unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
151
153
  }
@@ -248,16 +250,18 @@ module.exports = {
248
250
 
249
251
  /**
250
252
  * Returns a ecmaVersion compatible for regexpp.
251
- * @param {any} ecmaVersion The ecmaVersion to convert.
253
+ * @param {number} ecmaVersion The ecmaVersion to convert.
252
254
  * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
253
255
  */
254
256
  function getRegexppEcmaVersion(ecmaVersion) {
255
- if (typeof ecmaVersion !== "number" || ecmaVersion <= 5) {
257
+ if (ecmaVersion <= 5) {
256
258
  return 5;
257
259
  }
258
- return Math.min(ecmaVersion + 2009, REGEXPP_LATEST_ECMA_VERSION);
260
+ return Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION);
259
261
  }
260
262
 
263
+ const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion);
264
+
261
265
  /**
262
266
  * Makes a character escaped or else returns null.
263
267
  * @param {string} character The character to escape.
@@ -293,6 +297,83 @@ module.exports = {
293
297
  }
294
298
  }
295
299
 
300
+ /**
301
+ * Checks whether the given regex and flags are valid for the ecma version or not.
302
+ * @param {string} pattern The regex pattern to check.
303
+ * @param {string | undefined} flags The regex flags to check.
304
+ * @returns {boolean} True if the given regex pattern and flags are valid for the ecma version.
305
+ */
306
+ function isValidRegexForEcmaVersion(pattern, flags) {
307
+ const validator = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
308
+
309
+ try {
310
+ validator.validatePattern(pattern, 0, pattern.length, flags ? flags.includes("u") : false);
311
+ if (flags) {
312
+ validator.validateFlags(flags);
313
+ }
314
+ return true;
315
+ } catch {
316
+ return false;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Checks whether two given regex flags contain the same flags or not.
322
+ * @param {string} flagsA The regex flags.
323
+ * @param {string} flagsB The regex flags.
324
+ * @returns {boolean} True if two regex flags contain same flags.
325
+ */
326
+ function areFlagsEqual(flagsA, flagsB) {
327
+ return [...flagsA].sort().join("") === [...flagsB].sort().join("");
328
+ }
329
+
330
+
331
+ /**
332
+ * Merges two regex flags.
333
+ * @param {string} flagsA The regex flags.
334
+ * @param {string} flagsB The regex flags.
335
+ * @returns {string} The merged regex flags.
336
+ */
337
+ function mergeRegexFlags(flagsA, flagsB) {
338
+ const flagsSet = new Set([
339
+ ...flagsA,
340
+ ...flagsB
341
+ ]);
342
+
343
+ return [...flagsSet].join("");
344
+ }
345
+
346
+ /**
347
+ * Checks whether a give node can be fixed to the given regex pattern and flags.
348
+ * @param {ASTNode} node The node to check.
349
+ * @param {string} pattern The regex pattern to check.
350
+ * @param {string} flags The regex flags
351
+ * @returns {boolean} True if a node can be fixed to the given regex pattern and flags.
352
+ */
353
+ function canFixTo(node, pattern, flags) {
354
+ const tokenBefore = sourceCode.getTokenBefore(node);
355
+
356
+ return sourceCode.getCommentsInside(node).length === 0 &&
357
+ (!tokenBefore || validPrecedingTokens.has(tokenBefore.value)) &&
358
+ isValidRegexForEcmaVersion(pattern, flags);
359
+ }
360
+
361
+ /**
362
+ * Returns a safe output code considering the before and after tokens.
363
+ * @param {ASTNode} node The regex node.
364
+ * @param {string} newRegExpValue The new regex expression value.
365
+ * @returns {string} The output code.
366
+ */
367
+ function getSafeOutput(node, newRegExpValue) {
368
+ const tokenBefore = sourceCode.getTokenBefore(node);
369
+ const tokenAfter = sourceCode.getTokenAfter(node);
370
+
371
+ return (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
372
+ newRegExpValue +
373
+ (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "");
374
+
375
+ }
376
+
296
377
  return {
297
378
  Program() {
298
379
  const scope = context.getScope();
@@ -306,10 +387,69 @@ module.exports = {
306
387
 
307
388
  for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
308
389
  if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) {
390
+ const regexNode = node.arguments[0];
391
+
309
392
  if (node.arguments.length === 2) {
310
- context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" });
393
+ const suggests = [];
394
+
395
+ const argFlags = getStringValue(node.arguments[1]) || "";
396
+
397
+ if (canFixTo(node, regexNode.regex.pattern, argFlags)) {
398
+ suggests.push({
399
+ messageId: "replaceWithLiteralAndFlags",
400
+ pattern: regexNode.regex.pattern,
401
+ flags: argFlags
402
+ });
403
+ }
404
+
405
+ const literalFlags = regexNode.regex.flags || "";
406
+ const mergedFlags = mergeRegexFlags(literalFlags, argFlags);
407
+
408
+ if (
409
+ !areFlagsEqual(mergedFlags, argFlags) &&
410
+ canFixTo(node, regexNode.regex.pattern, mergedFlags)
411
+ ) {
412
+ suggests.push({
413
+ messageId: "replaceWithIntendedLiteralAndFlags",
414
+ pattern: regexNode.regex.pattern,
415
+ flags: mergedFlags
416
+ });
417
+ }
418
+
419
+ context.report({
420
+ node,
421
+ messageId: "unexpectedRedundantRegExpWithFlags",
422
+ suggest: suggests.map(({ flags, pattern, messageId }) => ({
423
+ messageId,
424
+ data: {
425
+ flags
426
+ },
427
+ fix(fixer) {
428
+ return fixer.replaceText(node, getSafeOutput(node, `/${pattern}/${flags}`));
429
+ }
430
+ }))
431
+ });
311
432
  } else {
312
- context.report({ node, messageId: "unexpectedRedundantRegExp" });
433
+ const outputs = [];
434
+
435
+ if (canFixTo(node, regexNode.regex.pattern, regexNode.regex.flags)) {
436
+ outputs.push(sourceCode.getText(regexNode));
437
+ }
438
+
439
+
440
+ context.report({
441
+ node,
442
+ messageId: "unexpectedRedundantRegExp",
443
+ suggest: outputs.map(output => ({
444
+ messageId: "replaceWithLiteral",
445
+ fix(fixer) {
446
+ return fixer.replaceText(
447
+ node,
448
+ getSafeOutput(node, output)
449
+ );
450
+ }
451
+ }))
452
+ });
313
453
  }
314
454
  } else if (hasOnlyStaticStringArguments(node)) {
315
455
  let regexContent = getStringValue(node.arguments[0]);
@@ -320,21 +460,7 @@ module.exports = {
320
460
  flags = getStringValue(node.arguments[1]);
321
461
  }
322
462
 
323
- const regexppEcmaVersion = getRegexppEcmaVersion(context.parserOptions.ecmaVersion);
324
- const RegExpValidatorInstance = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
325
-
326
- try {
327
- RegExpValidatorInstance.validatePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
328
- if (flags) {
329
- RegExpValidatorInstance.validateFlags(flags);
330
- }
331
- } catch {
332
- noFix = true;
333
- }
334
-
335
- const tokenBefore = sourceCode.getTokenBefore(node);
336
-
337
- if (tokenBefore && !validPrecedingTokens.has(tokenBefore.value)) {
463
+ if (!canFixTo(node, regexContent, flags)) {
338
464
  noFix = true;
339
465
  }
340
466
 
@@ -342,10 +468,6 @@ module.exports = {
342
468
  noFix = true;
343
469
  }
344
470
 
345
- if (sourceCode.getCommentsInside(node).length > 0) {
346
- noFix = true;
347
- }
348
-
349
471
  if (regexContent && !noFix) {
350
472
  let charIncrease = 0;
351
473
 
@@ -377,14 +499,7 @@ module.exports = {
377
499
  suggest: noFix ? [] : [{
378
500
  messageId: "replaceWithLiteral",
379
501
  fix(fixer) {
380
- const tokenAfter = sourceCode.getTokenAfter(node);
381
-
382
- return fixer.replaceText(
383
- node,
384
- (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
385
- newRegExpValue +
386
- (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "")
387
- );
502
+ return fixer.replaceText(node, getSafeOutput(node, newRegExpValue));
388
503
  }
389
504
  }]
390
505
  });
@@ -30,7 +30,7 @@ function getVariableOfArguments(scope) {
30
30
  }
31
31
  }
32
32
 
33
- /* istanbul ignore next : unreachable */
33
+ /* c8 ignore next */
34
34
  return null;
35
35
  }
36
36
 
@@ -68,7 +68,6 @@ module.exports = {
68
68
  // Increases the count of `yield` keyword.
69
69
  YieldExpression() {
70
70
 
71
- /* istanbul ignore else */
72
71
  if (stack.length > 0) {
73
72
  stack[stack.length - 1] += 1;
74
73
  }