eslint 8.57.0 → 9.2.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 (156) hide show
  1. package/README.md +31 -28
  2. package/bin/eslint.js +4 -3
  3. package/conf/ecma-version.js +16 -0
  4. package/conf/globals.js +1 -0
  5. package/conf/rule-type-list.json +3 -1
  6. package/lib/api.js +7 -11
  7. package/lib/cli-engine/cli-engine.js +14 -3
  8. package/lib/cli-engine/formatters/formatters-meta.json +1 -29
  9. package/lib/cli-engine/lint-result-cache.js +2 -2
  10. package/lib/cli.js +115 -36
  11. package/lib/config/default-config.js +3 -0
  12. package/lib/config/flat-config-array.js +110 -24
  13. package/lib/config/flat-config-helpers.js +41 -20
  14. package/lib/config/flat-config-schema.js +1 -7
  15. package/lib/config/rule-validator.js +42 -6
  16. package/lib/eslint/eslint-helpers.js +116 -58
  17. package/lib/eslint/eslint.js +892 -377
  18. package/lib/eslint/index.js +2 -2
  19. package/lib/eslint/legacy-eslint.js +728 -0
  20. package/lib/linter/apply-disable-directives.js +59 -31
  21. package/lib/linter/code-path-analysis/code-path-analyzer.js +0 -1
  22. package/lib/linter/code-path-analysis/code-path.js +32 -30
  23. package/lib/linter/code-path-analysis/fork-context.js +1 -1
  24. package/lib/linter/config-comment-parser.js +8 -11
  25. package/lib/linter/index.js +1 -3
  26. package/lib/linter/interpolate.js +24 -2
  27. package/lib/linter/linter.js +428 -207
  28. package/lib/linter/report-translator.js +3 -3
  29. package/lib/linter/rules.js +6 -15
  30. package/lib/linter/source-code-fixer.js +1 -1
  31. package/lib/linter/timing.js +16 -8
  32. package/lib/options.js +35 -3
  33. package/lib/rule-tester/index.js +3 -1
  34. package/lib/rule-tester/rule-tester.js +424 -347
  35. package/lib/rules/array-bracket-newline.js +1 -1
  36. package/lib/rules/array-bracket-spacing.js +1 -1
  37. package/lib/rules/block-scoped-var.js +1 -1
  38. package/lib/rules/callback-return.js +2 -2
  39. package/lib/rules/camelcase.js +3 -5
  40. package/lib/rules/capitalized-comments.js +10 -7
  41. package/lib/rules/comma-dangle.js +1 -1
  42. package/lib/rules/comma-style.js +2 -2
  43. package/lib/rules/complexity.js +14 -1
  44. package/lib/rules/constructor-super.js +99 -100
  45. package/lib/rules/default-case.js +1 -1
  46. package/lib/rules/eol-last.js +2 -2
  47. package/lib/rules/function-paren-newline.js +2 -2
  48. package/lib/rules/indent-legacy.js +5 -5
  49. package/lib/rules/indent.js +5 -5
  50. package/lib/rules/index.js +1 -2
  51. package/lib/rules/key-spacing.js +2 -2
  52. package/lib/rules/line-comment-position.js +1 -1
  53. package/lib/rules/lines-around-directive.js +2 -2
  54. package/lib/rules/max-depth.js +1 -1
  55. package/lib/rules/max-len.js +3 -3
  56. package/lib/rules/max-lines.js +3 -3
  57. package/lib/rules/max-nested-callbacks.js +1 -1
  58. package/lib/rules/max-params.js +1 -1
  59. package/lib/rules/max-statements.js +1 -1
  60. package/lib/rules/multiline-comment-style.js +7 -7
  61. package/lib/rules/new-cap.js +1 -1
  62. package/lib/rules/newline-after-var.js +1 -1
  63. package/lib/rules/newline-before-return.js +1 -1
  64. package/lib/rules/no-case-declarations.js +13 -1
  65. package/lib/rules/no-constant-binary-expression.js +7 -8
  66. package/lib/rules/no-constant-condition.js +18 -7
  67. package/lib/rules/no-constructor-return.js +2 -2
  68. package/lib/rules/no-dupe-class-members.js +2 -2
  69. package/lib/rules/no-else-return.js +1 -1
  70. package/lib/rules/no-empty-function.js +2 -2
  71. package/lib/rules/no-empty-static-block.js +1 -1
  72. package/lib/rules/no-extend-native.js +1 -2
  73. package/lib/rules/no-extra-semi.js +1 -1
  74. package/lib/rules/no-fallthrough.js +41 -16
  75. package/lib/rules/no-implicit-coercion.js +66 -24
  76. package/lib/rules/no-inner-declarations.js +23 -2
  77. package/lib/rules/no-invalid-regexp.js +1 -1
  78. package/lib/rules/no-invalid-this.js +1 -1
  79. package/lib/rules/no-lone-blocks.js +3 -3
  80. package/lib/rules/no-loss-of-precision.js +1 -1
  81. package/lib/rules/no-misleading-character-class.js +225 -69
  82. package/lib/rules/no-mixed-spaces-and-tabs.js +1 -1
  83. package/lib/rules/no-multiple-empty-lines.js +1 -1
  84. package/lib/rules/no-new-native-nonconstructor.js +1 -1
  85. package/lib/rules/no-new-symbol.js +8 -1
  86. package/lib/rules/no-restricted-globals.js +1 -1
  87. package/lib/rules/no-restricted-imports.js +186 -40
  88. package/lib/rules/no-restricted-modules.js +2 -2
  89. package/lib/rules/no-return-await.js +1 -1
  90. package/lib/rules/no-sequences.js +1 -0
  91. package/lib/rules/no-this-before-super.js +45 -13
  92. package/lib/rules/no-trailing-spaces.js +2 -3
  93. package/lib/rules/no-unneeded-ternary.js +1 -1
  94. package/lib/rules/no-unsafe-optional-chaining.js +1 -1
  95. package/lib/rules/no-unused-private-class-members.js +1 -1
  96. package/lib/rules/no-unused-vars.js +197 -36
  97. package/lib/rules/no-useless-assignment.js +566 -0
  98. package/lib/rules/no-useless-backreference.js +1 -1
  99. package/lib/rules/no-useless-computed-key.js +2 -2
  100. package/lib/rules/no-useless-return.js +7 -2
  101. package/lib/rules/object-curly-spacing.js +3 -3
  102. package/lib/rules/object-property-newline.js +1 -1
  103. package/lib/rules/one-var.js +5 -5
  104. package/lib/rules/padded-blocks.js +7 -7
  105. package/lib/rules/prefer-arrow-callback.js +3 -3
  106. package/lib/rules/prefer-reflect.js +1 -1
  107. package/lib/rules/prefer-regex-literals.js +1 -1
  108. package/lib/rules/prefer-template.js +1 -1
  109. package/lib/rules/radix.js +2 -2
  110. package/lib/rules/semi-style.js +1 -1
  111. package/lib/rules/sort-imports.js +1 -1
  112. package/lib/rules/sort-keys.js +1 -1
  113. package/lib/rules/sort-vars.js +1 -1
  114. package/lib/rules/space-unary-ops.js +1 -1
  115. package/lib/rules/strict.js +1 -1
  116. package/lib/rules/use-isnan.js +101 -7
  117. package/lib/rules/utils/ast-utils.js +16 -7
  118. package/lib/rules/utils/char-source.js +240 -0
  119. package/lib/rules/utils/lazy-loading-rule-map.js +1 -1
  120. package/lib/rules/utils/unicode/index.js +9 -4
  121. package/lib/rules/yield-star-spacing.js +1 -1
  122. package/lib/shared/runtime-info.js +1 -0
  123. package/lib/shared/serialization.js +55 -0
  124. package/lib/shared/stats.js +30 -0
  125. package/lib/shared/string-utils.js +9 -11
  126. package/lib/shared/types.js +35 -1
  127. package/lib/source-code/index.js +3 -1
  128. package/lib/source-code/source-code.js +299 -85
  129. package/lib/source-code/token-store/backward-token-cursor.js +3 -3
  130. package/lib/source-code/token-store/cursors.js +4 -2
  131. package/lib/source-code/token-store/forward-token-comment-cursor.js +3 -3
  132. package/lib/source-code/token-store/forward-token-cursor.js +3 -3
  133. package/lib/source-code/token-store/index.js +2 -2
  134. package/lib/unsupported-api.js +3 -5
  135. package/messages/no-config-found.js +1 -1
  136. package/messages/plugin-conflict.js +1 -1
  137. package/messages/plugin-invalid.js +1 -1
  138. package/messages/plugin-missing.js +1 -1
  139. package/package.json +32 -29
  140. package/conf/config-schema.js +0 -93
  141. package/lib/cli-engine/formatters/checkstyle.js +0 -60
  142. package/lib/cli-engine/formatters/compact.js +0 -60
  143. package/lib/cli-engine/formatters/jslint-xml.js +0 -41
  144. package/lib/cli-engine/formatters/junit.js +0 -82
  145. package/lib/cli-engine/formatters/tap.js +0 -95
  146. package/lib/cli-engine/formatters/unix.js +0 -58
  147. package/lib/cli-engine/formatters/visualstudio.js +0 -63
  148. package/lib/cli-engine/xml-escape.js +0 -34
  149. package/lib/eslint/flat-eslint.js +0 -1155
  150. package/lib/rule-tester/flat-rule-tester.js +0 -1131
  151. package/lib/rules/require-jsdoc.js +0 -122
  152. package/lib/rules/utils/patterns/letters.js +0 -36
  153. package/lib/rules/valid-jsdoc.js +0 -516
  154. package/lib/shared/config-validator.js +0 -347
  155. package/lib/shared/deprecation-warnings.js +0 -58
  156. package/lib/shared/relative-module-resolver.js +0 -50
@@ -12,7 +12,7 @@ const astUtils = require("./utils/ast-utils");
12
12
  //------------------------------------------------------------------------------
13
13
 
14
14
  const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u;
15
- const ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"];
15
+ const ALLOWABLE_OPERATORS = ["~", "!!", "+", "- -", "-", "*"];
16
16
 
17
17
  /**
18
18
  * Parses and normalizes an option object.
@@ -188,6 +188,7 @@ function getNonEmptyOperand(node) {
188
188
  /** @type {import('../shared/types').Rule} */
189
189
  module.exports = {
190
190
  meta: {
191
+ hasSuggestions: true,
191
192
  type: "suggestion",
192
193
 
193
194
  docs: {
@@ -229,7 +230,8 @@ module.exports = {
229
230
  }],
230
231
 
231
232
  messages: {
232
- useRecommendation: "use `{{recommendation}}` instead."
233
+ implicitCoercion: "Unexpected implicit coercion encountered. Use `{{recommendation}}` instead.",
234
+ useRecommendation: "Use `{{recommendation}}` instead."
233
235
  }
234
236
  },
235
237
 
@@ -241,32 +243,54 @@ module.exports = {
241
243
  * Reports an error and autofixes the node
242
244
  * @param {ASTNode} node An ast node to report the error on.
243
245
  * @param {string} recommendation The recommended code for the issue
246
+ * @param {bool} shouldSuggest Whether this report should offer a suggestion
244
247
  * @param {bool} shouldFix Whether this report should fix the node
245
248
  * @returns {void}
246
249
  */
247
- function report(node, recommendation, shouldFix) {
250
+ function report(node, recommendation, shouldSuggest, shouldFix) {
251
+
252
+ /**
253
+ * Fix function
254
+ * @param {RuleFixer} fixer The fixer to fix.
255
+ * @returns {Fix} The fix object.
256
+ */
257
+ function fix(fixer) {
258
+ const tokenBefore = sourceCode.getTokenBefore(node);
259
+
260
+ if (
261
+ tokenBefore?.range[1] === node.range[0] &&
262
+ !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
263
+ ) {
264
+ return fixer.replaceText(node, ` ${recommendation}`);
265
+ }
266
+
267
+ return fixer.replaceText(node, recommendation);
268
+ }
269
+
248
270
  context.report({
249
271
  node,
250
- messageId: "useRecommendation",
251
- data: {
252
- recommendation
253
- },
272
+ messageId: "implicitCoercion",
273
+ data: { recommendation },
254
274
  fix(fixer) {
255
275
  if (!shouldFix) {
256
276
  return null;
257
277
  }
258
278
 
259
- const tokenBefore = sourceCode.getTokenBefore(node);
260
-
261
- if (
262
- tokenBefore &&
263
- tokenBefore.range[1] === node.range[0] &&
264
- !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
265
- ) {
266
- return fixer.replaceText(node, ` ${recommendation}`);
279
+ return fix(fixer);
280
+ },
281
+ suggest: [
282
+ {
283
+ messageId: "useRecommendation",
284
+ data: { recommendation },
285
+ fix(fixer) {
286
+ if (shouldFix || !shouldSuggest) {
287
+ return null;
288
+ }
289
+
290
+ return fix(fixer);
291
+ }
267
292
  }
268
- return fixer.replaceText(node, recommendation);
269
- }
293
+ ]
270
294
  });
271
295
  }
272
296
 
@@ -278,8 +302,10 @@ module.exports = {
278
302
  operatorAllowed = options.allow.includes("!!");
279
303
  if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
280
304
  const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
305
+ const variable = astUtils.getVariableByName(sourceCode.getScope(node), "Boolean");
306
+ const booleanExists = variable?.identifiers.length === 0;
281
307
 
282
- report(node, recommendation, true);
308
+ report(node, recommendation, true, booleanExists);
283
309
  }
284
310
 
285
311
  // ~foo.indexOf(bar)
@@ -290,7 +316,7 @@ module.exports = {
290
316
  const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
291
317
  const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
292
318
 
293
- report(node, recommendation, false);
319
+ report(node, recommendation, false, false);
294
320
  }
295
321
 
296
322
  // +foo
@@ -298,7 +324,15 @@ module.exports = {
298
324
  if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
299
325
  const recommendation = `Number(${sourceCode.getText(node.argument)})`;
300
326
 
301
- report(node, recommendation, true);
327
+ report(node, recommendation, true, false);
328
+ }
329
+
330
+ // -(-foo)
331
+ operatorAllowed = options.allow.includes("- -");
332
+ if (!operatorAllowed && options.number && node.operator === "-" && node.argument.type === "UnaryExpression" && node.argument.operator === "-" && !isNumeric(node.argument.argument)) {
333
+ const recommendation = `Number(${sourceCode.getText(node.argument.argument)})`;
334
+
335
+ report(node, recommendation, true, false);
302
336
  }
303
337
  },
304
338
 
@@ -314,7 +348,15 @@ module.exports = {
314
348
  if (nonNumericOperand) {
315
349
  const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
316
350
 
317
- report(node, recommendation, true);
351
+ report(node, recommendation, true, false);
352
+ }
353
+
354
+ // foo - 0
355
+ operatorAllowed = options.allow.includes("-");
356
+ if (!operatorAllowed && options.number && node.operator === "-" && node.right.type === "Literal" && node.right.value === 0 && !isNumeric(node.left)) {
357
+ const recommendation = `Number(${sourceCode.getText(node.left)})`;
358
+
359
+ report(node, recommendation, true, false);
318
360
  }
319
361
 
320
362
  // "" + foo
@@ -322,7 +364,7 @@ module.exports = {
322
364
  if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
323
365
  const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
324
366
 
325
- report(node, recommendation, true);
367
+ report(node, recommendation, true, false);
326
368
  }
327
369
  },
328
370
 
@@ -335,7 +377,7 @@ module.exports = {
335
377
  const code = sourceCode.getText(getNonEmptyOperand(node));
336
378
  const recommendation = `${code} = String(${code})`;
337
379
 
338
- report(node, recommendation, true);
380
+ report(node, recommendation, true, false);
339
381
  }
340
382
  },
341
383
 
@@ -373,7 +415,7 @@ module.exports = {
373
415
  const code = sourceCode.getText(node.expressions[0]);
374
416
  const recommendation = `String(${code})`;
375
417
 
376
- report(node, recommendation, true);
418
+ report(node, recommendation, true, false);
377
419
  }
378
420
  };
379
421
  }
@@ -49,13 +49,22 @@ module.exports = {
49
49
 
50
50
  docs: {
51
51
  description: "Disallow variable or `function` declarations in nested blocks",
52
- recommended: true,
52
+ recommended: false,
53
53
  url: "https://eslint.org/docs/latest/rules/no-inner-declarations"
54
54
  },
55
55
 
56
56
  schema: [
57
57
  {
58
58
  enum: ["functions", "both"]
59
+ },
60
+ {
61
+ type: "object",
62
+ properties: {
63
+ blockScopedFunctions: {
64
+ enum: ["allow", "disallow"]
65
+ }
66
+ },
67
+ additionalProperties: false
59
68
  }
60
69
  ],
61
70
 
@@ -66,6 +75,10 @@ module.exports = {
66
75
 
67
76
  create(context) {
68
77
 
78
+ const sourceCode = context.sourceCode;
79
+ const ecmaVersion = context.languageOptions.ecmaVersion;
80
+ const blockScopedFunctions = context.options[1]?.blockScopedFunctions ?? "allow";
81
+
69
82
  /**
70
83
  * Ensure that a given node is at a program or function body's root.
71
84
  * @param {ASTNode} node Declaration node to check.
@@ -97,7 +110,15 @@ module.exports = {
97
110
 
98
111
  return {
99
112
 
100
- FunctionDeclaration: check,
113
+ FunctionDeclaration(node) {
114
+ const isInStrictCode = sourceCode.getScope(node).upper.isStrict;
115
+
116
+ if (blockScopedFunctions === "allow" && ecmaVersion >= 2015 && isInStrictCode) {
117
+ return;
118
+ }
119
+
120
+ check(node);
121
+ },
101
122
  VariableDeclaration(node) {
102
123
  if (context.options[0] === "both" && node.kind === "var") {
103
124
  check(node);
@@ -55,7 +55,7 @@ module.exports = {
55
55
  const temp = options.allowConstructorFlags.join("").replace(validFlags, "");
56
56
 
57
57
  if (temp) {
58
- allowedFlags = new RegExp(`[${temp}]`, "giu");
58
+ allowedFlags = new RegExp(`[${temp}]`, "gu");
59
59
  }
60
60
  }
61
61
 
@@ -74,7 +74,7 @@ module.exports = {
74
74
  * an object which has a flag that whether or not `this` keyword is valid.
75
75
  */
76
76
  stack.getCurrent = function() {
77
- const current = this[this.length - 1];
77
+ const current = this.at(-1);
78
78
 
79
79
  if (!current.init) {
80
80
  current.init = true;
@@ -78,7 +78,7 @@ module.exports = {
78
78
 
79
79
  const block = node.parent;
80
80
 
81
- if (loneBlocks[loneBlocks.length - 1] === block) {
81
+ if (loneBlocks.at(-1) === block) {
82
82
  loneBlocks.pop();
83
83
  }
84
84
  }
@@ -101,7 +101,7 @@ module.exports = {
101
101
  }
102
102
  },
103
103
  "BlockStatement:exit"(node) {
104
- if (loneBlocks.length > 0 && loneBlocks[loneBlocks.length - 1] === node) {
104
+ if (loneBlocks.length > 0 && loneBlocks.at(-1) === node) {
105
105
  loneBlocks.pop();
106
106
  report(node);
107
107
  } else if (
@@ -117,7 +117,7 @@ module.exports = {
117
117
  };
118
118
 
119
119
  ruleDef.VariableDeclaration = function(node) {
120
- if (node.kind === "let" || node.kind === "const") {
120
+ if (node.kind !== "var") {
121
121
  markLoneBlock(node);
122
122
  }
123
123
  };
@@ -64,7 +64,7 @@ module.exports = {
64
64
  */
65
65
  function notBaseTenLosesPrecision(node) {
66
66
  const rawString = getRaw(node).toUpperCase();
67
- let base = 0;
67
+ let base;
68
68
 
69
69
  if (rawString.startsWith("0B")) {
70
70
  base = 2;
@@ -3,11 +3,18 @@
3
3
  */
4
4
  "use strict";
5
5
 
6
- const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("@eslint-community/eslint-utils");
6
+ const {
7
+ CALL,
8
+ CONSTRUCT,
9
+ ReferenceTracker,
10
+ getStaticValue,
11
+ getStringIfConstant
12
+ } = require("@eslint-community/eslint-utils");
7
13
  const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
8
14
  const { isCombiningCharacter, isEmojiModifier, isRegionalIndicatorSymbol, isSurrogatePair } = require("./utils/unicode");
9
15
  const astUtils = require("./utils/ast-utils.js");
10
16
  const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");
17
+ const { parseStringLiteral, parseTemplateToken } = require("./utils/char-source");
11
18
 
12
19
  //------------------------------------------------------------------------------
13
20
  // Helpers
@@ -62,7 +69,6 @@ function *iterateCharacterSequence(nodes) {
62
69
  }
63
70
  }
64
71
 
65
-
66
72
  /**
67
73
  * Checks whether the given character node is a Unicode code point escape or not.
68
74
  * @param {Character} char the character node to check.
@@ -73,80 +79,153 @@ function isUnicodeCodePointEscape(char) {
73
79
  }
74
80
 
75
81
  /**
76
- * Each function returns `true` if it detects that kind of problem.
77
- * @type {Record<string, (chars: Character[]) => boolean>}
82
+ * Each function returns matched characters if it detects that kind of problem.
83
+ * @type {Record<string, (chars: Character[]) => IterableIterator<Character[]>>}
78
84
  */
79
- const hasCharacterSequence = {
80
- surrogatePairWithoutUFlag(chars) {
81
- return chars.some((c, i) => {
82
- if (i === 0) {
83
- return false;
85
+ const findCharacterSequences = {
86
+ *surrogatePairWithoutUFlag(chars) {
87
+ for (const [index, char] of chars.entries()) {
88
+ if (index === 0) {
89
+ continue;
90
+ }
91
+ const previous = chars[index - 1];
92
+
93
+ if (
94
+ isSurrogatePair(previous.value, char.value) &&
95
+ !isUnicodeCodePointEscape(previous) &&
96
+ !isUnicodeCodePointEscape(char)
97
+ ) {
98
+ yield [previous, char];
84
99
  }
85
- const c1 = chars[i - 1];
86
-
87
- return (
88
- isSurrogatePair(c1.value, c.value) &&
89
- !isUnicodeCodePointEscape(c1) &&
90
- !isUnicodeCodePointEscape(c)
91
- );
92
- });
100
+ }
93
101
  },
94
102
 
95
- surrogatePair(chars) {
96
- return chars.some((c, i) => {
97
- if (i === 0) {
98
- return false;
103
+ *surrogatePair(chars) {
104
+ for (const [index, char] of chars.entries()) {
105
+ if (index === 0) {
106
+ continue;
99
107
  }
100
- const c1 = chars[i - 1];
108
+ const previous = chars[index - 1];
101
109
 
102
- return (
103
- isSurrogatePair(c1.value, c.value) &&
110
+ if (
111
+ isSurrogatePair(previous.value, char.value) &&
104
112
  (
105
- isUnicodeCodePointEscape(c1) ||
106
- isUnicodeCodePointEscape(c)
113
+ isUnicodeCodePointEscape(previous) ||
114
+ isUnicodeCodePointEscape(char)
107
115
  )
108
- );
109
- });
116
+ ) {
117
+ yield [previous, char];
118
+ }
119
+ }
110
120
  },
111
121
 
112
- combiningClass(chars) {
113
- return chars.some((c, i) => (
114
- i !== 0 &&
115
- isCombiningCharacter(c.value) &&
116
- !isCombiningCharacter(chars[i - 1].value)
117
- ));
122
+ *combiningClass(chars) {
123
+ for (const [index, char] of chars.entries()) {
124
+ if (index === 0) {
125
+ continue;
126
+ }
127
+ const previous = chars[index - 1];
128
+
129
+ if (
130
+ isCombiningCharacter(char.value) &&
131
+ !isCombiningCharacter(previous.value)
132
+ ) {
133
+ yield [previous, char];
134
+ }
135
+ }
118
136
  },
119
137
 
120
- emojiModifier(chars) {
121
- return chars.some((c, i) => (
122
- i !== 0 &&
123
- isEmojiModifier(c.value) &&
124
- !isEmojiModifier(chars[i - 1].value)
125
- ));
138
+ *emojiModifier(chars) {
139
+ for (const [index, char] of chars.entries()) {
140
+ if (index === 0) {
141
+ continue;
142
+ }
143
+ const previous = chars[index - 1];
144
+
145
+ if (
146
+ isEmojiModifier(char.value) &&
147
+ !isEmojiModifier(previous.value)
148
+ ) {
149
+ yield [previous, char];
150
+ }
151
+ }
126
152
  },
127
153
 
128
- regionalIndicatorSymbol(chars) {
129
- return chars.some((c, i) => (
130
- i !== 0 &&
131
- isRegionalIndicatorSymbol(c.value) &&
132
- isRegionalIndicatorSymbol(chars[i - 1].value)
133
- ));
154
+ *regionalIndicatorSymbol(chars) {
155
+ for (const [index, char] of chars.entries()) {
156
+ if (index === 0) {
157
+ continue;
158
+ }
159
+ const previous = chars[index - 1];
160
+
161
+ if (
162
+ isRegionalIndicatorSymbol(char.value) &&
163
+ isRegionalIndicatorSymbol(previous.value)
164
+ ) {
165
+ yield [previous, char];
166
+ }
167
+ }
134
168
  },
135
169
 
136
- zwj(chars) {
137
- const lastIndex = chars.length - 1;
170
+ *zwj(chars) {
171
+ let sequence = null;
172
+
173
+ for (const [index, char] of chars.entries()) {
174
+ if (index === 0 || index === chars.length - 1) {
175
+ continue;
176
+ }
177
+ if (
178
+ char.value === 0x200d &&
179
+ chars[index - 1].value !== 0x200d &&
180
+ chars[index + 1].value !== 0x200d
181
+ ) {
182
+ if (sequence) {
183
+ if (sequence.at(-1) === chars[index - 1]) {
184
+ sequence.push(char, chars[index + 1]); // append to the sequence
185
+ } else {
186
+ yield sequence;
187
+ sequence = chars.slice(index - 1, index + 2);
188
+ }
189
+ } else {
190
+ sequence = chars.slice(index - 1, index + 2);
191
+ }
192
+ }
193
+ }
138
194
 
139
- return chars.some((c, i) => (
140
- i !== 0 &&
141
- i !== lastIndex &&
142
- c.value === 0x200d &&
143
- chars[i - 1].value !== 0x200d &&
144
- chars[i + 1].value !== 0x200d
145
- ));
195
+ if (sequence) {
196
+ yield sequence;
197
+ }
146
198
  }
147
199
  };
148
200
 
149
- const kinds = Object.keys(hasCharacterSequence);
201
+ const kinds = Object.keys(findCharacterSequences);
202
+
203
+ /**
204
+ * Gets the value of the given node if it's a static value other than a regular expression object,
205
+ * or the node's `regex` property.
206
+ * The purpose of this method is to provide a replacement for `getStaticValue` in environments where certain regular expressions cannot be evaluated.
207
+ * A known example is Node.js 18 which does not support the `v` flag.
208
+ * Calling `getStaticValue` on a regular expression node with the `v` flag on Node.js 18 always returns `null`.
209
+ * A limitation of this method is that it can only detect a regular expression if the specified node is itself a regular expression literal node.
210
+ * @param {ASTNode | undefined} node The node to be inspected.
211
+ * @param {Scope} initialScope Scope to start finding variables. This function tries to resolve identifier references which are in the given scope.
212
+ * @returns {{ value: any } | { regex: { pattern: string, flags: string } } | null} The static value of the node, or `null`.
213
+ */
214
+ function getStaticValueOrRegex(node, initialScope) {
215
+ if (!node) {
216
+ return null;
217
+ }
218
+ if (node.type === "Literal" && node.regex) {
219
+ return { regex: node.regex };
220
+ }
221
+
222
+ const staticValue = getStaticValue(node, initialScope);
223
+
224
+ if (staticValue?.value instanceof RegExp) {
225
+ return null;
226
+ }
227
+ return staticValue;
228
+ }
150
229
 
151
230
  //------------------------------------------------------------------------------
152
231
  // Rule Definition
@@ -180,6 +259,7 @@ module.exports = {
180
259
  create(context) {
181
260
  const sourceCode = context.sourceCode;
182
261
  const parser = new RegExpParser();
262
+ const checkedPatternNodes = new Set();
183
263
 
184
264
  /**
185
265
  * Verify a given regular expression.
@@ -208,21 +288,70 @@ module.exports = {
208
288
  return;
209
289
  }
210
290
 
211
- const foundKinds = new Set();
291
+ const foundKindMatches = new Map();
212
292
 
213
293
  visitRegExpAST(patternNode, {
214
294
  onCharacterClassEnter(ccNode) {
215
295
  for (const chars of iterateCharacterSequence(ccNode.elements)) {
216
296
  for (const kind of kinds) {
217
- if (hasCharacterSequence[kind](chars)) {
218
- foundKinds.add(kind);
297
+ if (foundKindMatches.has(kind)) {
298
+ foundKindMatches.get(kind).push(...findCharacterSequences[kind](chars));
299
+ } else {
300
+ foundKindMatches.set(kind, [...findCharacterSequences[kind](chars)]);
219
301
  }
220
302
  }
221
303
  }
222
304
  }
223
305
  });
224
306
 
225
- for (const kind of foundKinds) {
307
+ let codeUnits = null;
308
+
309
+ /**
310
+ * Finds the report loc(s) for a range of matches.
311
+ * Only literals and expression-less templates generate granular errors.
312
+ * @param {Character[][]} matches Lists of individual characters being reported on.
313
+ * @returns {Location[]} locs for context.report.
314
+ * @see https://github.com/eslint/eslint/pull/17515
315
+ */
316
+ function getNodeReportLocations(matches) {
317
+ if (!astUtils.isStaticTemplateLiteral(node) && node.type !== "Literal") {
318
+ return matches.length ? [node.loc] : [];
319
+ }
320
+ return matches.map(chars => {
321
+ const firstIndex = chars[0].start;
322
+ const lastIndex = chars.at(-1).end - 1;
323
+ let start;
324
+ let end;
325
+
326
+ if (node.type === "TemplateLiteral") {
327
+ const source = sourceCode.getText(node);
328
+ const offset = node.range[0];
329
+
330
+ codeUnits ??= parseTemplateToken(source);
331
+ start = offset + codeUnits[firstIndex].start;
332
+ end = offset + codeUnits[lastIndex].end;
333
+ } else if (typeof node.value === "string") { // String Literal
334
+ const source = node.raw;
335
+ const offset = node.range[0];
336
+
337
+ codeUnits ??= parseStringLiteral(source);
338
+ start = offset + codeUnits[firstIndex].start;
339
+ end = offset + codeUnits[lastIndex].end;
340
+ } else { // RegExp Literal
341
+ const offset = node.range[0] + 1; // Add 1 to skip the leading slash.
342
+
343
+ start = offset + firstIndex;
344
+ end = offset + lastIndex + 1;
345
+ }
346
+
347
+ return {
348
+ start: sourceCode.getLocFromIndex(start),
349
+ end: sourceCode.getLocFromIndex(end)
350
+ };
351
+ });
352
+ }
353
+
354
+ for (const [kind, matches] of foundKindMatches) {
226
355
  let suggest;
227
356
 
228
357
  if (kind === "surrogatePairWithoutUFlag") {
@@ -232,16 +361,24 @@ module.exports = {
232
361
  }];
233
362
  }
234
363
 
235
- context.report({
236
- node,
237
- messageId: kind,
238
- suggest
239
- });
364
+ const locs = getNodeReportLocations(matches);
365
+
366
+ for (const loc of locs) {
367
+ context.report({
368
+ node,
369
+ loc,
370
+ messageId: kind,
371
+ suggest
372
+ });
373
+ }
240
374
  }
241
375
  }
242
376
 
243
377
  return {
244
378
  "Literal[regex]"(node) {
379
+ if (checkedPatternNodes.has(node)) {
380
+ return;
381
+ }
245
382
  verify(node, node.regex.pattern, node.regex.flags, fixer => {
246
383
  if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern)) {
247
384
  return null;
@@ -262,12 +399,31 @@ module.exports = {
262
399
  for (const { node: refNode } of tracker.iterateGlobalReferences({
263
400
  RegExp: { [CALL]: true, [CONSTRUCT]: true }
264
401
  })) {
402
+ let pattern, flags;
265
403
  const [patternNode, flagsNode] = refNode.arguments;
266
- const pattern = getStringIfConstant(patternNode, scope);
267
- const flags = getStringIfConstant(flagsNode, scope);
404
+ const evaluatedPattern = getStaticValueOrRegex(patternNode, scope);
405
+
406
+ if (!evaluatedPattern) {
407
+ continue;
408
+ }
409
+ if (flagsNode) {
410
+ if (evaluatedPattern.regex) {
411
+ pattern = evaluatedPattern.regex.pattern;
412
+ checkedPatternNodes.add(patternNode);
413
+ } else {
414
+ pattern = String(evaluatedPattern.value);
415
+ }
416
+ flags = getStringIfConstant(flagsNode, scope);
417
+ } else {
418
+ if (evaluatedPattern.regex) {
419
+ continue;
420
+ }
421
+ pattern = String(evaluatedPattern.value);
422
+ flags = "";
423
+ }
268
424
 
269
- if (typeof pattern === "string") {
270
- verify(refNode, pattern, flags || "", fixer => {
425
+ if (typeof flags === "string") {
426
+ verify(patternNode, pattern, flags, fixer => {
271
427
 
272
428
  if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern)) {
273
429
  return null;
@@ -18,7 +18,7 @@ module.exports = {
18
18
 
19
19
  docs: {
20
20
  description: "Disallow mixed spaces and tabs for indentation",
21
- recommended: true,
21
+ recommended: false,
22
22
  url: "https://eslint.org/docs/latest/rules/no-mixed-spaces-and-tabs"
23
23
  },
24
24