eslint 8.45.0 → 8.46.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
@@ -284,7 +284,7 @@ The following companies, organizations, and individuals support ESLint's ongoing
284
284
  <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>
285
285
  <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>
286
286
  <p><a href="https://sentry.io"><img src="https://avatars.githubusercontent.com/u/1396951?v=4" alt="Sentry" height="64"></a> <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>
287
- <p><a href="https://iboysoft.com/"><img src="https://images.opencollective.com/iboysoft-software/7f9d60e/avatar.png" alt="iBoysoft" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" 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: free icons, photos, illustrations, and music" 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://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465?v=4" alt="Mercedes-Benz Group" 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>
287
+ <p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" 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: free icons, photos, illustrations, and music" 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://iboysoft.com/"><img src="https://images.opencollective.com/iboysoft-software/7f9d60e/avatar.png" alt="iBoysoft" height="32"></a> <a href="https://www.bairesdev.com/sponsoring-open-source-projects/"><img src="https://images.opencollective.com/bairesdev/48bb773/logo.png" alt="BairesDev" height="32"></a> <a href="https://github.com/about"><img src="https://avatars.githubusercontent.com/u/9919?v=4" alt="GitHub" 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>
288
288
  <!--sponsorsend-->
289
289
 
290
290
  ## Technology Sponsors
@@ -212,6 +212,38 @@ function assertIsObject(value) {
212
212
  }
213
213
  }
214
214
 
215
+ /**
216
+ * The error type when there's an eslintrc-style options in a flat config.
217
+ */
218
+ class IncompatibleKeyError extends Error {
219
+
220
+ /**
221
+ * @param {string} key The invalid key.
222
+ */
223
+ constructor(key) {
224
+ super("This appears to be in eslintrc format rather than flat config format.");
225
+ this.messageTemplate = "eslintrc-incompat";
226
+ this.messageData = { key };
227
+ }
228
+ }
229
+
230
+ /**
231
+ * The error type when there's an eslintrc-style plugins array found.
232
+ */
233
+ class IncompatiblePluginsError extends Error {
234
+
235
+ /**
236
+ * Creates a new instance.
237
+ * @param {Array<string>} plugins The plugins array.
238
+ */
239
+ constructor(plugins) {
240
+ super("This appears to be in eslintrc format (array of strings) rather than flat config format (object).");
241
+ this.messageTemplate = "eslintrc-plugins";
242
+ this.messageData = { plugins };
243
+ }
244
+ }
245
+
246
+
215
247
  //-----------------------------------------------------------------------------
216
248
  // Low-Level Schemas
217
249
  //-----------------------------------------------------------------------------
@@ -303,6 +335,11 @@ const pluginsSchema = {
303
335
  throw new TypeError("Expected an object.");
304
336
  }
305
337
 
338
+ // make sure it's not an array, which would mean eslintrc-style is used
339
+ if (Array.isArray(value)) {
340
+ throw new IncompatiblePluginsError(value);
341
+ }
342
+
306
343
  // second check the keys to make sure they are objects
307
344
  for (const key of Object.keys(value)) {
308
345
 
@@ -438,11 +475,44 @@ const sourceTypeSchema = {
438
475
  }
439
476
  };
440
477
 
478
+ /**
479
+ * Creates a schema that always throws an error. Useful for warning
480
+ * about eslintrc-style keys.
481
+ * @param {string} key The eslintrc key to create a schema for.
482
+ * @returns {ObjectPropertySchema} The schema.
483
+ */
484
+ function createEslintrcErrorSchema(key) {
485
+ return {
486
+ merge: "replace",
487
+ validate() {
488
+ throw new IncompatibleKeyError(key);
489
+ }
490
+ };
491
+ }
492
+
493
+ const eslintrcKeys = [
494
+ "env",
495
+ "extends",
496
+ "globals",
497
+ "ignorePatterns",
498
+ "noInlineConfig",
499
+ "overrides",
500
+ "parser",
501
+ "parserOptions",
502
+ "reportUnusedDisableDirectives",
503
+ "root"
504
+ ];
505
+
441
506
  //-----------------------------------------------------------------------------
442
507
  // Full schema
443
508
  //-----------------------------------------------------------------------------
444
509
 
445
510
  exports.flatConfigSchema = {
511
+
512
+ // eslintrc-style keys that should always error
513
+ ...Object.fromEntries(eslintrcKeys.map(key => [key, createEslintrcErrorSchema(key)])),
514
+
515
+ // flat config keys
446
516
  settings: deepObjectAssignSchema,
447
517
  linterOptions: {
448
518
  schema: {
@@ -714,12 +714,10 @@ class FlatESLint {
714
714
  }
715
715
  const rule = getRuleFromConfig(ruleId, config);
716
716
 
717
- // ensure the rule exists
718
- if (!rule) {
719
- throw new TypeError(`Could not find the rule "${ruleId}".`);
717
+ // ignore unknown rules
718
+ if (rule) {
719
+ resultRules.set(ruleId, rule);
720
720
  }
721
-
722
- resultRules.set(ruleId, rule);
723
721
  }
724
722
  }
725
723
 
@@ -14,6 +14,16 @@ const collector = new (class {
14
14
  }
15
15
 
16
16
  onPatternEnter() {
17
+
18
+ /*
19
+ * `RegExpValidator` may parse the pattern twice in one `validatePattern`.
20
+ * So `this._controlChars` should be cleared here as well.
21
+ *
22
+ * For example, the `/(?<a>\x1f)/` regex will parse the pattern twice.
23
+ * This is based on the content described in Annex B.
24
+ * If the regex contains a `GroupName` and the `u` flag is not used, `ParseText` will be called twice.
25
+ * See https://tc39.es/ecma262/2023/multipage/additional-ecmascript-features-for-web-browsers.html#sec-parsepattern-annexb
26
+ */
17
27
  this._controlChars = [];
18
28
  }
19
29
 
@@ -32,10 +42,13 @@ const collector = new (class {
32
42
 
33
43
  collectControlChars(regexpStr, flags) {
34
44
  const uFlag = typeof flags === "string" && flags.includes("u");
45
+ const vFlag = typeof flags === "string" && flags.includes("v");
46
+
47
+ this._controlChars = [];
48
+ this._source = regexpStr;
35
49
 
36
50
  try {
37
- this._source = regexpStr;
38
- this._validator.validatePattern(regexpStr, void 0, void 0, uFlag); // Call onCharacter hook
51
+ this._validator.validatePattern(regexpStr, void 0, void 0, { unicode: uFlag, unicodeSets: vFlag }); // Call onCharacter hook
39
52
  } catch {
40
53
 
41
54
  // Ignore syntax errors in RegExp.
@@ -5,20 +5,18 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ //------------------------------------------------------------------------------
9
+ // Requirements
10
+ //------------------------------------------------------------------------------
11
+
12
+ const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
13
+
8
14
  //------------------------------------------------------------------------------
9
15
  // Helpers
10
16
  //------------------------------------------------------------------------------
11
17
 
12
- /*
13
- * plain-English description of the following regexp:
14
- * 0. `^` fix the match at the beginning of the string
15
- * 1. `([^\\[]|\\.|\[([^\\\]]|\\.)+\])*`: regexp contents; 0 or more of the following
16
- * 1.0. `[^\\[]`: any character that's not a `\` or a `[` (anything but escape sequences and character classes)
17
- * 1.1. `\\.`: an escape sequence
18
- * 1.2. `\[([^\\\]]|\\.)+\]`: a character class that isn't empty
19
- * 2. `$`: fix the match at the end of the string
20
- */
21
- const regex = /^([^\\[]|\\.|\[([^\\\]]|\\.)+\])*$/u;
18
+ const parser = new RegExpParser();
19
+ const QUICK_TEST_REGEX = /\[\]/u;
22
20
 
23
21
  //------------------------------------------------------------------------------
24
22
  // Rule Definition
@@ -45,9 +43,32 @@ module.exports = {
45
43
  create(context) {
46
44
  return {
47
45
  "Literal[regex]"(node) {
48
- if (!regex.test(node.regex.pattern)) {
49
- context.report({ node, messageId: "unexpected" });
46
+ const { pattern, flags } = node.regex;
47
+
48
+ if (!QUICK_TEST_REGEX.test(pattern)) {
49
+ return;
50
50
  }
51
+
52
+ let regExpAST;
53
+
54
+ try {
55
+ regExpAST = parser.parsePattern(pattern, 0, pattern.length, {
56
+ unicode: flags.includes("u"),
57
+ unicodeSets: flags.includes("v")
58
+ });
59
+ } catch {
60
+
61
+ // Ignore regular expressions that regexpp cannot parse
62
+ return;
63
+ }
64
+
65
+ visitRegExpAST(regExpAST, {
66
+ onCharacterClassEnter(characterClass) {
67
+ if (!characterClass.negate && characterClass.elements.length === 0) {
68
+ context.report({ node, messageId: "unexpected" });
69
+ }
70
+ }
71
+ });
51
72
  }
52
73
  };
53
74
 
@@ -4,6 +4,8 @@
4
4
  */
5
5
  "use strict";
6
6
 
7
+ const astUtils = require("./utils/ast-utils");
8
+
7
9
  //------------------------------------------------------------------------------
8
10
  // Rule Definition
9
11
  //------------------------------------------------------------------------------
@@ -19,7 +21,18 @@ module.exports = {
19
21
  url: "https://eslint.org/docs/latest/rules/no-empty-pattern"
20
22
  },
21
23
 
22
- schema: [],
24
+ schema: [
25
+ {
26
+ type: "object",
27
+ properties: {
28
+ allowObjectPatternsAsParameters: {
29
+ type: "boolean",
30
+ default: false
31
+ }
32
+ },
33
+ additionalProperties: false
34
+ }
35
+ ],
23
36
 
24
37
  messages: {
25
38
  unexpected: "Unexpected empty {{type}} pattern."
@@ -27,11 +40,33 @@ module.exports = {
27
40
  },
28
41
 
29
42
  create(context) {
43
+ const options = context.options[0] || {},
44
+ allowObjectPatternsAsParameters = options.allowObjectPatternsAsParameters || false;
45
+
30
46
  return {
31
47
  ObjectPattern(node) {
32
- if (node.properties.length === 0) {
33
- context.report({ node, messageId: "unexpected", data: { type: "object" } });
48
+
49
+ if (node.properties.length > 0) {
50
+ return;
34
51
  }
52
+
53
+ // Allow {} and {} = {} empty object patterns as parameters when allowObjectPatternsAsParameters is true
54
+ if (
55
+ allowObjectPatternsAsParameters &&
56
+ (
57
+ astUtils.isFunction(node.parent) ||
58
+ (
59
+ node.parent.type === "AssignmentPattern" &&
60
+ astUtils.isFunction(node.parent.parent) &&
61
+ node.parent.right.type === "ObjectExpression" &&
62
+ node.parent.right.properties.length === 0
63
+ )
64
+ )
65
+ ) {
66
+ return;
67
+ }
68
+
69
+ context.report({ node, messageId: "unexpected", data: { type: "object" } });
35
70
  },
36
71
  ArrayPattern(node) {
37
72
  if (node.elements.length === 0) {
@@ -10,7 +10,7 @@
10
10
 
11
11
  const RegExpValidator = require("@eslint-community/regexpp").RegExpValidator;
12
12
  const validator = new RegExpValidator();
13
- const validFlags = /[dgimsuy]/gu;
13
+ const validFlags = /[dgimsuvy]/gu;
14
14
  const undefined1 = void 0;
15
15
 
16
16
  //------------------------------------------------------------------------------
@@ -108,12 +108,14 @@ module.exports = {
108
108
  /**
109
109
  * Check syntax error in a given pattern.
110
110
  * @param {string} pattern The RegExp pattern to validate.
111
- * @param {boolean} uFlag The Unicode flag.
111
+ * @param {Object} flags The RegExp flags to validate.
112
+ * @param {boolean} [flags.unicode] The Unicode flag.
113
+ * @param {boolean} [flags.unicodeSets] The UnicodeSets flag.
112
114
  * @returns {string|null} The syntax error.
113
115
  */
114
- function validateRegExpPattern(pattern, uFlag) {
116
+ function validateRegExpPattern(pattern, flags) {
115
117
  try {
116
- validator.validatePattern(pattern, undefined1, undefined1, uFlag);
118
+ validator.validatePattern(pattern, undefined1, undefined1, flags);
117
119
  return null;
118
120
  } catch (err) {
119
121
  return err.message;
@@ -131,10 +133,19 @@ module.exports = {
131
133
  }
132
134
  try {
133
135
  validator.validateFlags(flags);
134
- return null;
135
136
  } catch {
136
137
  return `Invalid flags supplied to RegExp constructor '${flags}'`;
137
138
  }
139
+
140
+ /*
141
+ * `regexpp` checks the combination of `u` and `v` flags when parsing `Pattern` according to `ecma262`,
142
+ * but this rule may check only the flag when the pattern is unidentifiable, so check it here.
143
+ * https://tc39.es/ecma262/multipage/text-processing.html#sec-parsepattern
144
+ */
145
+ if (flags.includes("u") && flags.includes("v")) {
146
+ return "Regex 'u' and 'v' flags cannot be used together";
147
+ }
148
+ return null;
138
149
  }
139
150
 
140
151
  return {
@@ -166,8 +177,12 @@ module.exports = {
166
177
 
167
178
  // If flags are unknown, report the regex only if its pattern is invalid both with and without the "u" flag
168
179
  flags === null
169
- ? validateRegExpPattern(pattern, true) && validateRegExpPattern(pattern, false)
170
- : validateRegExpPattern(pattern, flags.includes("u"))
180
+ ? (
181
+ validateRegExpPattern(pattern, { unicode: true, unicodeSets: false }) &&
182
+ validateRegExpPattern(pattern, { unicode: false, unicodeSets: true }) &&
183
+ validateRegExpPattern(pattern, { unicode: false, unicodeSets: false })
184
+ )
185
+ : validateRegExpPattern(pattern, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") })
171
186
  );
172
187
 
173
188
  if (message) {
@@ -186,7 +186,7 @@ module.exports = {
186
186
  }
187
187
 
188
188
  const references = sourceCode.getScope(node).through;
189
- const unsafeRefs = references.filter(r => !isSafe(loopNode, r)).map(r => r.identifier.name);
189
+ const unsafeRefs = references.filter(r => r.resolved && !isSafe(loopNode, r)).map(r => r.identifier.name);
190
190
 
191
191
  if (unsafeRefs.length > 0) {
192
192
  context.report({
@@ -18,7 +18,7 @@ const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");
18
18
  *
19
19
  * CharacterClassRange syntax can steal a part of character sequence,
20
20
  * so this function reverts CharacterClassRange syntax and restore the sequence.
21
- * @param {regexpp.AST.CharacterClassElement[]} nodes The node list to iterate character sequences.
21
+ * @param {import('@eslint-community/regexpp').AST.CharacterClassElement[]} nodes The node list to iterate character sequences.
22
22
  * @returns {IterableIterator<number[]>} The list of character sequences.
23
23
  */
24
24
  function *iterateCharacterSequence(nodes) {
@@ -37,6 +37,9 @@ function *iterateCharacterSequence(nodes) {
37
37
  break;
38
38
 
39
39
  case "CharacterSet":
40
+ case "CharacterClass": // [[]] nesting character class
41
+ case "ClassStringDisjunction": // \q{...}
42
+ case "ExpressionCharacterClass": // [A--B]
40
43
  if (seq.length > 0) {
41
44
  yield seq;
42
45
  seq = [];
@@ -144,7 +147,10 @@ module.exports = {
144
147
  pattern,
145
148
  0,
146
149
  pattern.length,
147
- flags.includes("u")
150
+ {
151
+ unicode: flags.includes("u"),
152
+ unicodeSets: flags.includes("v")
153
+ }
148
154
  );
149
155
  } catch {
150
156
 
@@ -77,7 +77,7 @@ module.exports = {
77
77
  let regExpAST;
78
78
 
79
79
  try {
80
- regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, flags.includes("u"));
80
+ regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") });
81
81
  } catch {
82
82
 
83
83
  // Ignore regular expressions with syntax errors
@@ -155,13 +155,28 @@ module.exports = {
155
155
  const regExpVar = astUtils.getVariableByName(scope, "RegExp");
156
156
  const shadowed = regExpVar && regExpVar.defs.length > 0;
157
157
  const patternNode = node.arguments[0];
158
- const flagsNode = node.arguments[1];
159
158
 
160
159
  if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(patternNode) && !shadowed) {
161
160
  const pattern = patternNode.value;
162
161
  const rawPattern = patternNode.raw.slice(1, -1);
163
162
  const rawPatternStartRange = patternNode.range[0] + 1;
164
- const flags = isString(flagsNode) ? flagsNode.value : "";
163
+ let flags;
164
+
165
+ if (node.arguments.length < 2) {
166
+
167
+ // It has no flags.
168
+ flags = "";
169
+ } else {
170
+ const flagsNode = node.arguments[1];
171
+
172
+ if (isString(flagsNode)) {
173
+ flags = flagsNode.value;
174
+ } else {
175
+
176
+ // The flags cannot be determined.
177
+ return;
178
+ }
179
+ }
165
180
 
166
181
  checkRegex(
167
182
  node,
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Disallows unnecessary `return await`
3
3
  * @author Jordan Harband
4
+ * @deprecated in ESLint v8.46.0
4
5
  */
5
6
  "use strict";
6
7
 
@@ -26,6 +27,10 @@ module.exports = {
26
27
 
27
28
  fixable: null,
28
29
 
30
+ deprecated: true,
31
+
32
+ replacedBy: [],
33
+
29
34
  schema: [
30
35
  ],
31
36
 
@@ -95,7 +95,7 @@ module.exports = {
95
95
  let regExpAST;
96
96
 
97
97
  try {
98
- regExpAST = parser.parsePattern(pattern, 0, pattern.length, flags.includes("u"));
98
+ regExpAST = parser.parsePattern(pattern, 0, pattern.length, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") });
99
99
  } catch {
100
100
 
101
101
  // Ignore regular expressions with syntax errors
@@ -6,7 +6,12 @@
6
6
  "use strict";
7
7
 
8
8
  const astUtils = require("./utils/ast-utils");
9
+ const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
9
10
 
11
+ /**
12
+ * @typedef {import('@eslint-community/regexpp').AST.CharacterClass} CharacterClass
13
+ * @typedef {import('@eslint-community/regexpp').AST.ExpressionCharacterClass} ExpressionCharacterClass
14
+ */
10
15
  //------------------------------------------------------------------------------
11
16
  // Rule Definition
12
17
  //------------------------------------------------------------------------------
@@ -28,55 +33,17 @@ const VALID_STRING_ESCAPES = union(new Set("\\nrvtbfux"), astUtils.LINEBREAKS);
28
33
  const REGEX_GENERAL_ESCAPES = new Set("\\bcdDfnpPrsStvwWxu0123456789]");
29
34
  const REGEX_NON_CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set("^/.$*+?[{}|()Bk"));
30
35
 
31
- /**
32
- * Parses a regular expression into a list of characters with character class info.
33
- * @param {string} regExpText The raw text used to create the regular expression
34
- * @returns {Object[]} A list of characters, each with info on escaping and whether they're in a character class.
35
- * @example
36
- *
37
- * parseRegExp("a\\b[cd-]");
38
- *
39
- * // returns:
40
- * [
41
- * { text: "a", index: 0, escaped: false, inCharClass: false, startsCharClass: false, endsCharClass: false },
42
- * { text: "b", index: 2, escaped: true, inCharClass: false, startsCharClass: false, endsCharClass: false },
43
- * { text: "c", index: 4, escaped: false, inCharClass: true, startsCharClass: true, endsCharClass: false },
44
- * { text: "d", index: 5, escaped: false, inCharClass: true, startsCharClass: false, endsCharClass: false },
45
- * { text: "-", index: 6, escaped: false, inCharClass: true, startsCharClass: false, endsCharClass: false }
46
- * ];
47
- *
36
+ /*
37
+ * Set of characters that require escaping in character classes in `unicodeSets` mode.
38
+ * ( ) [ ] { } / - \ | are ClassSetSyntaxCharacter
48
39
  */
49
- function parseRegExp(regExpText) {
50
- const charList = [];
40
+ const REGEX_CLASSSET_CHARACTER_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set("q/[{}|()-"));
51
41
 
52
- regExpText.split("").reduce((state, char, index) => {
53
- if (!state.escapeNextChar) {
54
- if (char === "\\") {
55
- return Object.assign(state, { escapeNextChar: true });
56
- }
57
- if (char === "[" && !state.inCharClass) {
58
- return Object.assign(state, { inCharClass: true, startingCharClass: true });
59
- }
60
- if (char === "]" && state.inCharClass) {
61
- if (charList.length && charList[charList.length - 1].inCharClass) {
62
- charList[charList.length - 1].endsCharClass = true;
63
- }
64
- return Object.assign(state, { inCharClass: false, startingCharClass: false });
65
- }
66
- }
67
- charList.push({
68
- text: char,
69
- index,
70
- escaped: state.escapeNextChar,
71
- inCharClass: state.inCharClass,
72
- startsCharClass: state.startingCharClass,
73
- endsCharClass: false
74
- });
75
- return Object.assign(state, { escapeNextChar: false, startingCharClass: false });
76
- }, { escapeNextChar: false, inCharClass: false, startingCharClass: false });
77
-
78
- return charList;
79
- }
42
+ /*
43
+ * A single character set of ClassSetReservedDoublePunctuator.
44
+ * && !! ## $$ %% ** ++ ,, .. :: ;; << == >> ?? @@ ^^ `` ~~ are ClassSetReservedDoublePunctuator
45
+ */
46
+ const REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR = new Set("!#$%&*+,.:;<=>?@^`~");
80
47
 
81
48
  /** @type {import('../shared/types').Rule} */
82
49
  module.exports = {
@@ -103,15 +70,17 @@ module.exports = {
103
70
 
104
71
  create(context) {
105
72
  const sourceCode = context.sourceCode;
73
+ const parser = new RegExpParser();
106
74
 
107
75
  /**
108
76
  * Reports a node
109
77
  * @param {ASTNode} node The node to report
110
78
  * @param {number} startOffset The backslash's offset from the start of the node
111
79
  * @param {string} character The uselessly escaped character (not including the backslash)
80
+ * @param {boolean} [disableEscapeBackslashSuggest] `true` if escapeBackslash suggestion should be turned off.
112
81
  * @returns {void}
113
82
  */
114
- function report(node, startOffset, character) {
83
+ function report(node, startOffset, character, disableEscapeBackslashSuggest) {
115
84
  const rangeStart = node.range[0] + startOffset;
116
85
  const range = [rangeStart, rangeStart + 1];
117
86
  const start = sourceCode.getLocFromIndex(rangeStart);
@@ -134,12 +103,16 @@ module.exports = {
134
103
  return fixer.removeRange(range);
135
104
  }
136
105
  },
137
- {
138
- messageId: "escapeBackslash",
139
- fix(fixer) {
140
- return fixer.insertTextBeforeRange(range, "\\");
141
- }
142
- }
106
+ ...disableEscapeBackslashSuggest
107
+ ? []
108
+ : [
109
+ {
110
+ messageId: "escapeBackslash",
111
+ fix(fixer) {
112
+ return fixer.insertTextBeforeRange(range, "\\");
113
+ }
114
+ }
115
+ ]
143
116
  ]
144
117
  });
145
118
  }
@@ -182,6 +155,133 @@ module.exports = {
182
155
  }
183
156
  }
184
157
 
158
+ /**
159
+ * Checks if the escape character in given regexp is unnecessary.
160
+ * @private
161
+ * @param {ASTNode} node node to validate.
162
+ * @returns {void}
163
+ */
164
+ function validateRegExp(node) {
165
+ const { pattern, flags } = node.regex;
166
+ let patternNode;
167
+ const unicode = flags.includes("u");
168
+ const unicodeSets = flags.includes("v");
169
+
170
+ try {
171
+ patternNode = parser.parsePattern(pattern, 0, pattern.length, { unicode, unicodeSets });
172
+ } catch {
173
+
174
+ // Ignore regular expressions with syntax errors
175
+ return;
176
+ }
177
+
178
+ /** @type {(CharacterClass | ExpressionCharacterClass)[]} */
179
+ const characterClassStack = [];
180
+
181
+ visitRegExpAST(patternNode, {
182
+ onCharacterClassEnter: characterClassNode => characterClassStack.unshift(characterClassNode),
183
+ onCharacterClassLeave: () => characterClassStack.shift(),
184
+ onExpressionCharacterClassEnter: characterClassNode => characterClassStack.unshift(characterClassNode),
185
+ onExpressionCharacterClassLeave: () => characterClassStack.shift(),
186
+ onCharacterEnter(characterNode) {
187
+ if (!characterNode.raw.startsWith("\\")) {
188
+
189
+ // It's not an escaped character.
190
+ return;
191
+ }
192
+
193
+ const escapedChar = characterNode.raw.slice(1);
194
+
195
+ if (escapedChar !== String.fromCodePoint(characterNode.value)) {
196
+
197
+ // It's a valid escape.
198
+ return;
199
+ }
200
+ let allowedEscapes;
201
+
202
+ if (characterClassStack.length) {
203
+ allowedEscapes = unicodeSets ? REGEX_CLASSSET_CHARACTER_ESCAPES : REGEX_GENERAL_ESCAPES;
204
+ } else {
205
+ allowedEscapes = REGEX_NON_CHARCLASS_ESCAPES;
206
+ }
207
+ if (allowedEscapes.has(escapedChar)) {
208
+ return;
209
+ }
210
+
211
+ const reportedIndex = characterNode.start + 1;
212
+ let disableEscapeBackslashSuggest = false;
213
+
214
+ if (characterClassStack.length) {
215
+ const characterClassNode = characterClassStack[0];
216
+
217
+ if (escapedChar === "^") {
218
+
219
+ /*
220
+ * The '^' character is also a special case; it must always be escaped outside of character classes, but
221
+ * it only needs to be escaped in character classes if it's at the beginning of the character class. To
222
+ * account for this, consider it to be a valid escape character outside of character classes, and filter
223
+ * out '^' characters that appear at the start of a character class.
224
+ */
225
+ if (characterClassNode.start + 1 === characterNode.start) {
226
+
227
+ return;
228
+ }
229
+ }
230
+ if (!unicodeSets) {
231
+ if (escapedChar === "-") {
232
+
233
+ /*
234
+ * The '-' character is a special case, because it's only valid to escape it if it's in a character
235
+ * class, and is not at either edge of the character class. To account for this, don't consider '-'
236
+ * characters to be valid in general, and filter out '-' characters that appear in the middle of a
237
+ * character class.
238
+ */
239
+ if (characterClassNode.start + 1 !== characterNode.start && characterNode.end !== characterClassNode.end - 1) {
240
+
241
+ return;
242
+ }
243
+ }
244
+ } else { // unicodeSets mode
245
+ if (REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR.has(escapedChar)) {
246
+
247
+ // Escaping is valid if it is a ClassSetReservedDoublePunctuator.
248
+ if (pattern[characterNode.end] === escapedChar) {
249
+ return;
250
+ }
251
+ if (pattern[characterNode.start - 1] === escapedChar) {
252
+ if (escapedChar !== "^") {
253
+ return;
254
+ }
255
+
256
+ // If the previous character is a `negate` caret(`^`), escape to caret is unnecessary.
257
+
258
+ if (!characterClassNode.negate) {
259
+ return;
260
+ }
261
+ const negateCaretIndex = characterClassNode.start + 1;
262
+
263
+ if (negateCaretIndex < characterNode.start - 1) {
264
+ return;
265
+ }
266
+ }
267
+ }
268
+
269
+ if (characterNode.parent.type === "ClassIntersection" || characterNode.parent.type === "ClassSubtraction") {
270
+ disableEscapeBackslashSuggest = true;
271
+ }
272
+ }
273
+ }
274
+
275
+ report(
276
+ node,
277
+ reportedIndex,
278
+ escapedChar,
279
+ disableEscapeBackslashSuggest
280
+ );
281
+ }
282
+ });
283
+ }
284
+
185
285
  /**
186
286
  * Checks if a node has an escape.
187
287
  * @param {ASTNode} node node to check.
@@ -220,32 +320,7 @@ module.exports = {
220
320
  validateString(node, match);
221
321
  }
222
322
  } else if (node.regex) {
223
- parseRegExp(node.regex.pattern)
224
-
225
- /*
226
- * The '-' character is a special case, because it's only valid to escape it if it's in a character
227
- * class, and is not at either edge of the character class. To account for this, don't consider '-'
228
- * characters to be valid in general, and filter out '-' characters that appear in the middle of a
229
- * character class.
230
- */
231
- .filter(charInfo => !(charInfo.text === "-" && charInfo.inCharClass && !charInfo.startsCharClass && !charInfo.endsCharClass))
232
-
233
- /*
234
- * The '^' character is also a special case; it must always be escaped outside of character classes, but
235
- * it only needs to be escaped in character classes if it's at the beginning of the character class. To
236
- * account for this, consider it to be a valid escape character outside of character classes, and filter
237
- * out '^' characters that appear at the start of a character class.
238
- */
239
- .filter(charInfo => !(charInfo.text === "^" && charInfo.startsCharClass))
240
-
241
- // Filter out characters that aren't escaped.
242
- .filter(charInfo => charInfo.escaped)
243
-
244
- // Filter out characters that are valid to escape, based on their position in the regular expression.
245
- .filter(charInfo => !(charInfo.inCharClass ? REGEX_GENERAL_ESCAPES : REGEX_NON_CHARCLASS_ESCAPES).has(charInfo.text))
246
-
247
- // Report all the remaining characters.
248
- .forEach(charInfo => report(node, charInfo.index, charInfo.text));
323
+ validateRegExp(node);
249
324
  }
250
325
 
251
326
  }
@@ -112,14 +112,17 @@ module.exports = {
112
112
  * @param {string} pattern The regular expression pattern to be checked.
113
113
  * @param {ASTNode} node AST node which contains the regular expression or a call/new expression.
114
114
  * @param {ASTNode} regexNode AST node which contains the regular expression.
115
- * @param {boolean} uFlag Flag indicates whether unicode mode is enabled or not.
115
+ * @param {string|null} flags The regular expression flags to be checked.
116
116
  * @returns {void}
117
117
  */
118
- function checkRegex(pattern, node, regexNode, uFlag) {
118
+ function checkRegex(pattern, node, regexNode, flags) {
119
119
  let ast;
120
120
 
121
121
  try {
122
- ast = parser.parsePattern(pattern, 0, pattern.length, uFlag);
122
+ ast = parser.parsePattern(pattern, 0, pattern.length, {
123
+ unicode: Boolean(flags && flags.includes("u")),
124
+ unicodeSets: Boolean(flags && flags.includes("v"))
125
+ });
123
126
  } catch {
124
127
 
125
128
  // ignore regex syntax errors
@@ -148,7 +151,7 @@ module.exports = {
148
151
  return {
149
152
  Literal(node) {
150
153
  if (node.regex) {
151
- checkRegex(node.regex.pattern, node, node, node.regex.flags.includes("u"));
154
+ checkRegex(node.regex.pattern, node, node, node.regex.flags);
152
155
  }
153
156
  },
154
157
  Program(node) {
@@ -166,7 +169,7 @@ module.exports = {
166
169
  const flags = getStringIfConstant(refNode.arguments[1]);
167
170
 
168
171
  if (regex) {
169
- checkRegex(regex, refNode, refNode.arguments[0], flags && flags.includes("u"));
172
+ checkRegex(regex, refNode, refNode.arguments[0], flags);
170
173
  }
171
174
  }
172
175
  }
@@ -241,7 +241,7 @@ module.exports = {
241
241
  /**
242
242
  * Returns a ecmaVersion compatible for regexpp.
243
243
  * @param {number} ecmaVersion The ecmaVersion to convert.
244
- * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
244
+ * @returns {import("@eslint-community/regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
245
245
  */
246
246
  function getRegexppEcmaVersion(ecmaVersion) {
247
247
  if (ecmaVersion <= 5) {
@@ -297,7 +297,10 @@ module.exports = {
297
297
  const validator = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
298
298
 
299
299
  try {
300
- validator.validatePattern(pattern, 0, pattern.length, flags ? flags.includes("u") : false);
300
+ validator.validatePattern(pattern, 0, pattern.length, {
301
+ unicode: flags ? flags.includes("u") : false,
302
+ unicodeSets: flags ? flags.includes("v") : false
303
+ });
301
304
  if (flags) {
302
305
  validator.validateFlags(flags);
303
306
  }
@@ -461,7 +464,10 @@ module.exports = {
461
464
  if (regexContent && !noFix) {
462
465
  let charIncrease = 0;
463
466
 
464
- const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
467
+ const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, {
468
+ unicode: flags ? flags.includes("u") : false,
469
+ unicodeSets: flags ? flags.includes("v") : false
470
+ });
465
471
 
466
472
  visitRegExpAST(ast, {
467
473
  onCharacterEnter(characterNode) {
@@ -28,7 +28,7 @@ module.exports = {
28
28
  type: "suggestion",
29
29
 
30
30
  docs: {
31
- description: "Enforce the use of `u` flag on RegExp",
31
+ description: "Enforce the use of `u` or `v` flag on RegExp",
32
32
  recommended: false,
33
33
  url: "https://eslint.org/docs/latest/rules/require-unicode-regexp"
34
34
  },
@@ -51,7 +51,7 @@ module.exports = {
51
51
  "Literal[regex]"(node) {
52
52
  const flags = node.regex.flags || "";
53
53
 
54
- if (!flags.includes("u")) {
54
+ if (!flags.includes("u") && !flags.includes("v")) {
55
55
  context.report({
56
56
  messageId: "requireUFlag",
57
57
  node,
@@ -85,7 +85,7 @@ module.exports = {
85
85
  const pattern = getStringIfConstant(patternNode, scope);
86
86
  const flags = getStringIfConstant(flagsNode, scope);
87
87
 
88
- if (!flagsNode || (typeof flags === "string" && !flags.includes("u"))) {
88
+ if (!flagsNode || (typeof flags === "string" && !flags.includes("u") && !flags.includes("v"))) {
89
89
  context.report({
90
90
  messageId: "requireUFlag",
91
91
  node: refNode,
@@ -8,7 +8,7 @@
8
8
 
9
9
  const { RegExpValidator } = require("@eslint-community/regexpp");
10
10
 
11
- const REGEXPP_LATEST_ECMA_VERSION = 2022;
11
+ const REGEXPP_LATEST_ECMA_VERSION = 2024;
12
12
 
13
13
  /**
14
14
  * Checks if the given regular expression pattern would be valid with the `u` flag.
@@ -28,7 +28,7 @@ function isValidWithUnicodeFlag(ecmaVersion, pattern) {
28
28
  });
29
29
 
30
30
  try {
31
- validator.validatePattern(pattern, void 0, void 0, /* uFlag = */ true);
31
+ validator.validatePattern(pattern, void 0, void 0, { unicode: /* uFlag = */ true });
32
32
  } catch {
33
33
  return false;
34
34
  }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+
3
+ /* eslint consistent-return: 0 -- no default case */
4
+
5
+ const messages = {
6
+
7
+ env: `
8
+ A config object is using the "env" key, which is not supported in flat config system.
9
+
10
+ Flat config uses "languageOptions.globals" to define global variables for your files.
11
+
12
+ Please see the following page for information on how to convert your config object into the correct format:
13
+ https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options
14
+ `,
15
+
16
+ extends: `
17
+ A config object is using the "extends" key, which is not supported in flat config system.
18
+
19
+ Instead of "extends", you can include config objects that you'd like to extend from directly in the flat config array.
20
+
21
+ Please see the following page for more information:
22
+ https://eslint.org/docs/latest/use/configure/migration-guide#predefined-configs
23
+ `,
24
+
25
+ globals: `
26
+ A config object is using the "globals" key, which is not supported in flat config system.
27
+
28
+ Flat config uses "languageOptions.globals" to define global variables for your files.
29
+
30
+ Please see the following page for information on how to convert your config object into the correct format:
31
+ https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options
32
+ `,
33
+
34
+ ignorePatterns: `
35
+ A config object is using the "ignorePatterns" key, which is not supported in flat config system.
36
+
37
+ Flat config uses "ignores" to specify files to ignore.
38
+
39
+ Please see the following page for information on how to convert your config object into the correct format:
40
+ https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files
41
+ `,
42
+
43
+ noInlineConfig: `
44
+ A config object is using the "noInlineConfig" key, which is not supported in flat config system.
45
+
46
+ Flat config uses "linterOptions.noInlineConfig" to specify files to ignore.
47
+
48
+ Please see the following page for information on how to convert your config object into the correct format:
49
+ https://eslint.org/docs/latest/use/configure/migration-guide#linter-options
50
+ `,
51
+
52
+ overrides: `
53
+ A config object is using the "overrides" key, which is not supported in flat config system.
54
+
55
+ Flat config is an array that acts like the eslintrc "overrides" array.
56
+
57
+ Please see the following page for information on how to convert your config object into the correct format:
58
+ https://eslint.org/docs/latest/use/configure/migration-guide#glob-based-configs
59
+ `,
60
+
61
+ parser: `
62
+ A config object is using the "parser" key, which is not supported in flat config system.
63
+
64
+ Flat config uses "languageOptions.parser" to override the default parser.
65
+
66
+ Please see the following page for information on how to convert your config object into the correct format:
67
+ https://eslint.org/docs/latest/use/configure/migration-guide#custom-parsers
68
+ `,
69
+
70
+ parserOptions: `
71
+ A config object is using the "parserOptions" key, which is not supported in flat config system.
72
+
73
+ Flat config uses "languageOptions.parserOptions" to specify parser options.
74
+
75
+ Please see the following page for information on how to convert your config object into the correct format:
76
+ https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options
77
+ `,
78
+
79
+ reportUnusedDisableDirectives: `
80
+ A config object is using the "reportUnusedDisableDirectives" key, which is not supported in flat config system.
81
+
82
+ Flat config uses "linterOptions.reportUnusedDisableDirectives" to specify files to ignore.
83
+
84
+ Please see the following page for information on how to convert your config object into the correct format:
85
+ https://eslint.org/docs/latest/use/configure/migration-guide#linter-options
86
+ `,
87
+
88
+ root: `
89
+ A config object is using the "root" key, which is not supported in flat config system.
90
+
91
+ Flat configs always act as if they are the root config file, so this key can be safely removed.
92
+ `
93
+ };
94
+
95
+ module.exports = function({ key }) {
96
+
97
+ return messages[key].trim();
98
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ module.exports = function({ plugins }) {
4
+
5
+ const isArrayOfStrings = typeof plugins[0] === "string";
6
+
7
+ return `
8
+ A config object has a "plugins" key defined as an array${isArrayOfStrings ? " of strings" : ""}.
9
+
10
+ Flat config requires "plugins" to be an object in this form:
11
+
12
+ {
13
+ plugins: {
14
+ ${isArrayOfStrings && plugins[0] ? plugins[0] : "namespace"}: pluginObject
15
+ }
16
+ }
17
+
18
+ Please see the following page for information on how to convert your config object into the correct format:
19
+ https://eslint.org/docs/latest/use/configure/migration-guide#importing-plugins-and-custom-parsers
20
+
21
+ If you're using a shareable config that you cannot rewrite in flat config format, then use the compatibility utility:
22
+ https://eslint.org/docs/latest/use/configure/migration-guide#using-eslintrc-configs-in-flat-config
23
+ `;
24
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "8.45.0",
3
+ "version": "8.46.0",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {
@@ -61,21 +61,21 @@
61
61
  "bugs": "https://github.com/eslint/eslint/issues/",
62
62
  "dependencies": {
63
63
  "@eslint-community/eslint-utils": "^4.2.0",
64
- "@eslint-community/regexpp": "^4.4.0",
65
- "@eslint/eslintrc": "^2.1.0",
66
- "@eslint/js": "8.44.0",
64
+ "@eslint-community/regexpp": "^4.6.1",
65
+ "@eslint/eslintrc": "^2.1.1",
66
+ "@eslint/js": "^8.46.0",
67
67
  "@humanwhocodes/config-array": "^0.11.10",
68
68
  "@humanwhocodes/module-importer": "^1.0.1",
69
69
  "@nodelib/fs.walk": "^1.2.8",
70
- "ajv": "^6.10.0",
70
+ "ajv": "^6.12.4",
71
71
  "chalk": "^4.0.0",
72
72
  "cross-spawn": "^7.0.2",
73
73
  "debug": "^4.3.2",
74
74
  "doctrine": "^3.0.0",
75
75
  "escape-string-regexp": "^4.0.0",
76
- "eslint-scope": "^7.2.0",
77
- "eslint-visitor-keys": "^3.4.1",
78
- "espree": "^9.6.0",
76
+ "eslint-scope": "^7.2.2",
77
+ "eslint-visitor-keys": "^3.4.2",
78
+ "espree": "^9.6.1",
79
79
  "esquery": "^1.4.2",
80
80
  "esutils": "^2.0.2",
81
81
  "fast-deep-equal": "^3.1.3",