diff --git a/README.md b/README.md
index 2de0b5b7b..7790ddcfb 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,8 @@ module.exports = {
This plugin provides one config:
-- `plugin:regexp/recommended` ... This is the recommended configuration for this plugin.
+- `plugin:regexp/recommended` ... This is the recommended configuration for this plugin.
+ See [lib/configs/recommended.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/recommended.ts) for details.
@@ -84,8 +85,10 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-empty-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-group.html) | disallow empty group | :star: |
| [regexp/no-empty-lookarounds-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-lookarounds-assertion.html) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
| [regexp/no-escape-backspace](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-escape-backspace.html) | disallow escape backspace (`[\b]`) | :star: |
+| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
| [regexp/no-useless-exactly-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-exactly-quantifier.html) | disallow unnecessary exactly quantifier | :star: |
+| [regexp/no-useless-two-nums-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-two-nums-quantifier.html) | disallow unnecessary `{n,m}` quantifier | :star: |
| [regexp/prefer-d](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html) | enforce using `\d` | :star::wrench: |
| [regexp/prefer-plus-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html) | enforce using `+` quantifier | :star::wrench: |
| [regexp/prefer-question-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-question-quantifier.html) | enforce using `?` quantifier | :star::wrench: |
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 88af4dba5..413e15b75 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -17,8 +17,10 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-empty-group](./no-empty-group.md) | disallow empty group | :star: |
| [regexp/no-empty-lookarounds-assertion](./no-empty-lookarounds-assertion.md) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
| [regexp/no-escape-backspace](./no-escape-backspace.md) | disallow escape backspace (`[\b]`) | :star: |
+| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
| [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
+| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star: |
| [regexp/prefer-d](./prefer-d.md) | enforce using `\d` | :star::wrench: |
| [regexp/prefer-plus-quantifier](./prefer-plus-quantifier.md) | enforce using `+` quantifier | :star::wrench: |
| [regexp/prefer-question-quantifier](./prefer-question-quantifier.md) | enforce using `?` quantifier | :star::wrench: |
diff --git a/docs/rules/match-any.md b/docs/rules/match-any.md
index b1fa9d75f..8aa3fbdca 100644
--- a/docs/rules/match-any.md
+++ b/docs/rules/match-any.md
@@ -22,14 +22,14 @@ e.g. `[\s\S]`, `[^]`, `/./s` (dotAll) and more.
/* eslint regexp/match-any: "error" */
/* ✓ GOOD */
-var foo = /[\s\S]/
-var foo = /./s
+var foo = /[\s\S]/;
+var foo = /./s;
/* ✗ BAD */
-var foo = /[\S\s]/
-var foo = /[^]/
-var foo = /[\d\D]/
-var foo = /[\w\W]/
+var foo = /[\S\s]/;
+var foo = /[^]/;
+var foo = /[\d\D]/;
+var foo = /[\w\W]/;
```
@@ -55,14 +55,14 @@ var foo = /[\w\W]/
/* eslint regexp/match-any: ["error", { "allows": ["[^]"] }] */
/* ✓ GOOD */
-var foo = /[^]/
+var foo = /[^]/;
/* ✗ BAD */
-var foo = /[\s\S]/
-var foo = /[\S\s]/
-var foo = /./s
-var foo = /[\d\D]/
-var foo = /[\w\W]/
+var foo = /[\s\S]/;
+var foo = /[\S\s]/;
+var foo = /./s;
+var foo = /[\d\D]/;
+var foo = /[\w\W]/;
```
diff --git a/docs/rules/no-assertion-capturing-group.md b/docs/rules/no-assertion-capturing-group.md
index ca495dc17..7e44f6066 100644
--- a/docs/rules/no-assertion-capturing-group.md
+++ b/docs/rules/no-assertion-capturing-group.md
@@ -20,15 +20,15 @@ This rule reports capturing group that captures assertions.
/* eslint regexp/no-assertion-capturing-group: "error" */
/* ✓ GOOD */
-var foo = /(a)/
-var foo = /a(?:\b)/
-var foo = /a(?:$)/
-var foo = /(?:^)a/
+var foo = /(a)/;
+var foo = /a(?:\b)/;
+var foo = /a(?:$)/;
+var foo = /(?:^)a/;
/* ✗ BAD */
-var foo = /a(\b)/
-var foo = /a($)/
-var foo = /(^)a/
+var foo = /a(\b)/;
+var foo = /a($)/;
+var foo = /(^)a/;
```
diff --git a/docs/rules/no-dupe-characters-character-class.md b/docs/rules/no-dupe-characters-character-class.md
index 8d46db8aa..d73ddf42d 100644
--- a/docs/rules/no-dupe-characters-character-class.md
+++ b/docs/rules/no-dupe-characters-character-class.md
@@ -13,7 +13,7 @@ description: "disallow duplicate characters in the RegExp character class"
Because multiple same character classes in regular expressions only one is useful, they might be typing mistakes.
```js
-var foo = /\\(\\)/
+var foo = /\\(\\)/;
```
## :book: Rule Details
@@ -26,18 +26,18 @@ This rule disallows duplicate characters in the RegExp character class.
/* eslint regexp/no-dupe-characters-character-class: "error" */
/* ✓ GOOD */
-var foo = /[\(\)]/
+var foo = /[\(\)]/;
-var foo = /[a-z\s]/
+var foo = /[a-z\s]/;
-var foo = /[\w]/
+var foo = /[\w]/;
/* ✗ BAD */
-var foo = /[\\(\\)]/
+var foo = /[\\(\\)]/;
// ^^ ^^ "\\" are duplicated
-var foo = /[a-z\\s]/
+var foo = /[a-z\\s]/;
// ^^^ ^ "s" are duplicated
-var foo = /[\w0-9]/
+var foo = /[\w0-9]/;
// ^^^^^ "0-9" are duplicated
```
diff --git a/docs/rules/no-empty-group.md b/docs/rules/no-empty-group.md
index e49f1594b..3ff2cdb3b 100644
--- a/docs/rules/no-empty-group.md
+++ b/docs/rules/no-empty-group.md
@@ -20,16 +20,16 @@ This rule reports empty groups.
/* eslint regexp/no-empty-group: "error" */
/* ✓ GOOD */
-var foo = /(a)/
-var foo = /(?:a)/
+var foo = /(a)/;
+var foo = /(?:a)/;
/* ✗ BAD */
// capturing group
-var foo = /()/
-var foo = /(|)/
+var foo = /()/;
+var foo = /(|)/;
// non-capturing group
-var foo = /(?:)/
-var foo = /(?:|)/
+var foo = /(?:)/;
+var foo = /(?:|)/;
```
diff --git a/docs/rules/no-empty-lookarounds-assertion.md b/docs/rules/no-empty-lookarounds-assertion.md
index 06c04a401..c4daec743 100644
--- a/docs/rules/no-empty-lookarounds-assertion.md
+++ b/docs/rules/no-empty-lookarounds-assertion.md
@@ -20,16 +20,16 @@ This rule reports empty lookahead assertion or empty lookbehind assertion.
/* eslint regexp/no-empty-lookarounds-assertion: "error" */
/* ✓ GOOD */
-var foo = /x(?=y)/
-var foo = /x(?!y)/
-var foo = /(?<=y)x/
-var foo = /(?
diff --git a/docs/rules/no-escape-backspace.md b/docs/rules/no-escape-backspace.md
index f32b8bc8e..ac551c970 100644
--- a/docs/rules/no-escape-backspace.md
+++ b/docs/rules/no-escape-backspace.md
@@ -21,13 +21,13 @@ The word boundaries (`\b`) and the escape backspace (`[\b]`) are indistinguishab
/* eslint regexp/no-escape-backspace: "error" */
/* ✓ GOOD */
-var foo = /\b/
-var foo = /\u0008/
-var foo = /\cH/
-var foo = /\x08/
+var foo = /\b/;
+var foo = /\u0008/;
+var foo = /\cH/;
+var foo = /\x08/;
/* ✗ BAD */
-var foo = /[\b]/
+var foo = /[\b]/;
```
diff --git a/docs/rules/no-invisible-character.md b/docs/rules/no-invisible-character.md
new file mode 100644
index 000000000..e34d2c89a
--- /dev/null
+++ b/docs/rules/no-invisible-character.md
@@ -0,0 +1,46 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/no-invisible-character"
+description: "disallow invisible raw character"
+---
+# regexp/no-invisible-character
+
+> disallow invisible raw character
+
+- :gear: This rule is included in `"plugin:regexp/recommended"`.
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule disallows using invisible characters other than SPACE (`U+0020`) without using escapes.
+
+
+
+```js
+/* eslint regexp/no-invisible-character: "error" */
+
+/* ✓ GOOD */
+var foo = /\t/;
+var foo = /\v/;
+var foo = /\f/;
+var foo = /\u3000/;
+var foo = / /; // SPACE (`U+0020`)
+
+/* ✗ BAD */
+var foo = / /;
+var foo = //;
+var foo = //;
+var foo = / /;
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-invisible-character.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-invisible-character.js)
diff --git a/docs/rules/no-octal.md b/docs/rules/no-octal.md
index b31df28e6..cf5f6ff0d 100644
--- a/docs/rules/no-octal.md
+++ b/docs/rules/no-octal.md
@@ -22,11 +22,11 @@ This rule reports octal escape.
/* eslint regexp/no-octal: "error" */
/* ✓ GOOD */
-var foo = /\0/
-var foo = /=/
+var foo = /\0/;
+var foo = /=/;
/* ✗ BAD */
-var foo = /\075/
+var foo = /\075/;
```
diff --git a/docs/rules/no-useless-exactly-quantifier.md b/docs/rules/no-useless-exactly-quantifier.md
index 5b1821939..833ad8c1b 100644
--- a/docs/rules/no-useless-exactly-quantifier.md
+++ b/docs/rules/no-useless-exactly-quantifier.md
@@ -20,11 +20,11 @@ This rule reports `{0}` or `{1}` quantifiers.
/* eslint regexp/no-useless-exactly-quantifier: "error" */
/* ✓ GOOD */
-var foo = /a/
+var foo = /a/;
/* ✗ BAD */
-var foo = /a{1}/
-var foo = /a{0}/
+var foo = /a{1}/;
+var foo = /a{0}/;
```
diff --git a/docs/rules/no-useless-two-nums-quantifier.md b/docs/rules/no-useless-two-nums-quantifier.md
new file mode 100644
index 000000000..e0879c0a7
--- /dev/null
+++ b/docs/rules/no-useless-two-nums-quantifier.md
@@ -0,0 +1,44 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/no-useless-two-nums-quantifier"
+description: "disallow unnecessary `{n,m}` quantifier"
+---
+# regexp/no-useless-two-nums-quantifier
+
+> disallow unnecessary `{n,m}` quantifier
+
+- :gear: This rule is included in `"plugin:regexp/recommended"`.
+
+## :book: Rule Details
+
+This rule reports unnecessary `{n,m}` quantifiers.
+
+
+
+```js
+/* eslint regexp/no-useless-two-nums-quantifier: "error" */
+
+/* ✓ GOOD */
+var foo = /a{0,1}/;
+var foo = /a{1,5}/;
+var foo = /a{1,}/;
+var foo = /a{2}/;
+
+
+/* ✗ BAD */
+var foo = /a{0,0}/;
+var foo = /a{1,1}/;
+var foo = /a{2,2}/;
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-two-nums-quantifier.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-two-nums-quantifier.js)
diff --git a/docs/rules/prefer-d.md b/docs/rules/prefer-d.md
index 4225ffc3d..ebd22b95d 100644
--- a/docs/rules/prefer-d.md
+++ b/docs/rules/prefer-d.md
@@ -21,12 +21,12 @@ This rule is aimed at using `\d` instead of `[0-9]` in regular expressions.
/* eslint regexp/prefer-d: "error" */
/* ✓ GOOD */
-var foo = /\d/
-var foo = /\D/
+var foo = /\d/;
+var foo = /\D/;
/* ✗ BAD */
-var foo = /[0-9]/
-var foo = /[^0-9]/
+var foo = /[0-9]/;
+var foo = /[^0-9]/;
```
diff --git a/docs/rules/prefer-plus-quantifier.md b/docs/rules/prefer-plus-quantifier.md
index 2e675f86e..50075d147 100644
--- a/docs/rules/prefer-plus-quantifier.md
+++ b/docs/rules/prefer-plus-quantifier.md
@@ -21,10 +21,10 @@ This rule is aimed at using `+` quantifier instead of `{1,}` in regular expressi
/* eslint regexp/prefer-plus-quantifier: "error" */
/* ✓ GOOD */
-var foo = /a+/
+var foo = /a+/;
/* ✗ BAD */
-var foo = /a{1,}/
+var foo = /a{1,}/;
```
diff --git a/docs/rules/prefer-question-quantifier.md b/docs/rules/prefer-question-quantifier.md
index 9765789f4..fe40f0ef4 100644
--- a/docs/rules/prefer-question-quantifier.md
+++ b/docs/rules/prefer-question-quantifier.md
@@ -21,10 +21,10 @@ This rule is aimed at using `?` quantifier instead of `{0,1}` in regular express
/* eslint regexp/prefer-question-quantifier: "error" */
/* ✓ GOOD */
-var foo = /a?/
+var foo = /a?/;
/* ✗ BAD */
-var foo = /a{0,1}/
+var foo = /a{0,1}/;
```
diff --git a/docs/rules/prefer-star-quantifier.md b/docs/rules/prefer-star-quantifier.md
index ff3142c9f..a089ba849 100644
--- a/docs/rules/prefer-star-quantifier.md
+++ b/docs/rules/prefer-star-quantifier.md
@@ -24,7 +24,7 @@ This rule is aimed at using `*` quantifier instead of `{0,}` in regular expressi
var foo = /a*/
/* ✗ BAD */
-var foo = /a{0,}/
+var foo = /a{0,}/;
```
diff --git a/docs/rules/prefer-t.md b/docs/rules/prefer-t.md
index 909cc2824..91fc378a6 100644
--- a/docs/rules/prefer-t.md
+++ b/docs/rules/prefer-t.md
@@ -21,10 +21,10 @@ This rule is aimed at using `\t` in regular expressions.
/* eslint regexp/prefer-t: "error" */
/* ✓ GOOD */
-var foo = /\t/
+var foo = /\t/;
/* ✗ BAD */
-var foo = /\u0009/
+var foo = /\u0009/;
```
diff --git a/docs/rules/prefer-w.md b/docs/rules/prefer-w.md
index 73f482a92..8b37ab3bd 100644
--- a/docs/rules/prefer-w.md
+++ b/docs/rules/prefer-w.md
@@ -21,13 +21,13 @@ This rule is aimed at using `\d` instead of `[0-9]` in regular expressions.
/* eslint regexp/prefer-w: "error" */
/* ✓ GOOD */
-var foo = /\w/
-var foo = /\W/
+var foo = /\w/;
+var foo = /\W/;
/* ✗ BAD */
-var foo = /[0-9a-zA-Z_]/
-var foo = /[^0-9a-zA-Z_]/
-var foo = /[0-9a-z_]/i
+var foo = /[0-9a-zA-Z_]/;
+var foo = /[^0-9a-zA-Z_]/;
+var foo = /[0-9a-z_]/i;
```
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index ce5cf2de4..6189cd9d1 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -35,6 +35,7 @@ module.exports = {
This plugin provides one config:
-- `plugin:regexp/recommended` ... This is the recommended configuration for this plugin.
+- `plugin:regexp/recommended` ... This is the recommended configuration for this plugin.
+ See [lib/configs/recommended.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/recommended.ts) for details.
See [the rule list](../rules/README.md) to get the `rules` that this plugin provides.
diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts
index c8b2de99a..274509f16 100644
--- a/lib/configs/recommended.ts
+++ b/lib/configs/recommended.ts
@@ -1,16 +1,30 @@
-import { recommendedConfig } from "../utils/rules"
-
export default {
plugins: ["regexp"],
rules: {
+ // ESLint core rules
"no-control-regex": "error",
"no-invalid-regexp": "error",
"no-misleading-character-class": "error",
"no-regex-spaces": "error",
"no-useless-backreference": "error",
"prefer-regex-literals": "error",
- // "prefer-named-capture-group": "error", // modern
- // "require-unicode-regexp": "error", // modern
- ...recommendedConfig(),
+
+ // eslint-plugin-regexp rules
+ "regexp/match-any": "error",
+ "regexp/no-assertion-capturing-group": "error",
+ "regexp/no-dupe-characters-character-class": "error",
+ "regexp/no-empty-group": "error",
+ "regexp/no-empty-lookarounds-assertion": "error",
+ "regexp/no-escape-backspace": "error",
+ "regexp/no-invisible-character": "error",
+ "regexp/no-octal": "error",
+ "regexp/no-useless-exactly-quantifier": "error",
+ "regexp/no-useless-two-nums-quantifier": "error",
+ "regexp/prefer-d": "error",
+ "regexp/prefer-plus-quantifier": "error",
+ "regexp/prefer-question-quantifier": "error",
+ "regexp/prefer-star-quantifier": "error",
+ "regexp/prefer-t": "error",
+ "regexp/prefer-w": "error",
},
}
diff --git a/lib/rules/match-any.ts b/lib/rules/match-any.ts
index 1ff52a711..05ebc102d 100644
--- a/lib/rules/match-any.ts
+++ b/lib/rules/match-any.ts
@@ -14,6 +14,7 @@ import {
FLAG_DOTALL,
getRegexpLocation,
getRegexpRange,
+ fixerApplyEscape,
} from "../utils"
const OPTION_SS1 = "[\\s\\S]" as const
@@ -110,11 +111,11 @@ export default createRule("match-any", {
) {
return fixer.replaceTextRange(
[range[0] + 1, range[1] - 1],
- prefer.slice(1, -1),
+ fixerApplyEscape(prefer.slice(1, -1), node),
)
}
- return fixer.replaceTextRange(range, prefer)
+ return fixer.replaceTextRange(range, fixerApplyEscape(prefer, node))
}
/**
diff --git a/lib/rules/no-dupe-characters-character-class.ts b/lib/rules/no-dupe-characters-character-class.ts
index b1ceb341e..ef42d84f1 100644
--- a/lib/rules/no-dupe-characters-character-class.ts
+++ b/lib/rules/no-dupe-characters-character-class.ts
@@ -12,95 +12,16 @@ import {
createRule,
defineRegexpVisitor,
getRegexpLocation,
- CP_DIGIT_ZERO,
- CP_DIGIT_NINE,
- CP_SMALL_A,
- CP_SMALL_Z,
- CP_CAPITAL_A,
- CP_CAPITAL_Z,
CP_LOW_LINE,
-} from "../utils"
-
-const CP_SPACE = " ".codePointAt(0)!
-const CPS_SINGLE_SPACES = new Set(
- Array.from(
- " \f\n\r\t\v\u00a0\u1680\u180e\u2028\u2029\u202f\u205f\u3000\ufeff",
- ).map((s) => s.codePointAt(0)!),
-)
-const CP_EN_QUAD = "\u2000".codePointAt(0)!
-const CP_HAIR_SPACE = "\u200a".codePointAt(0)!
-
-const CP_RANGE_DIGIT = [CP_DIGIT_ZERO, CP_DIGIT_NINE] as const
-const CP_RANGE_SMALL_LETTER = [CP_SMALL_A, CP_SMALL_Z] as const
-const CP_RANGE_CAPITAL_LETTER = [CP_CAPITAL_A, CP_CAPITAL_Z] as const
-const CP_RANGE_SPACES = [CP_EN_QUAD, CP_HAIR_SPACE] as const
-
-const CP_RANGES_WORDS = [
- CP_RANGE_SMALL_LETTER,
- CP_RANGE_CAPITAL_LETTER,
CP_RANGE_DIGIT,
-]
-
-/**
- * Checks if the given code point is within the code point range.
- * @param codePoint The code point to check.
- * @param range The range of code points of the range.
- * @returns {boolean} `true` if the given character is within the character class range.
- */
-function isCodePointInRange(
- codePoint: number,
- [start, end]: readonly [number, number],
-) {
- return start <= codePoint && codePoint <= end
-}
-
-/**
- * Checks if the given code point is digit.
- * @param codePoint The code point to check
- * @returns {boolean} `true` if the given code point is digit.
- */
-function isDigit(codePoint: number) {
- return isCodePointInRange(codePoint, CP_RANGE_DIGIT)
-}
-
-/**
- * Checks if the given code point is space.
- * @param codePoint The code point to check
- * @returns {boolean} `true` if the given code point is space.
- */
-function isSpace(codePoint: number) {
- return (
- CPS_SINGLE_SPACES.has(codePoint) ||
- isCodePointInRange(codePoint, CP_RANGE_SPACES)
- )
-}
-
-/**
- * Checks if the given code point is word.
- * @param codePoint The code point to check
- * @returns {boolean} `true` if the given code point is word.
- */
-function isWord(codePoint: number) {
- return (
- CP_RANGES_WORDS.some((range) => isCodePointInRange(codePoint, range)) ||
- CP_LOW_LINE === codePoint
- )
-}
-
-/**
- * Code point to display string.
- * @param codePoint The code point to convert.
- * @returns {string} the string.
- */
-function codePointToDispString(codePoint: number) {
- if (isSpace(codePoint)) {
- if (codePoint === CP_SPACE) {
- return " "
- }
- return `\\u${codePoint.toString(16).padStart(4, "0")}`
- }
- return String.fromCodePoint(codePoint)
-}
+ CP_RANGE_SPACES,
+ CPS_SINGLE_SPACES,
+ CP_RANGES_WORDS,
+ isDigit,
+ isSpace,
+ isWord,
+ invisibleEscape,
+} from "../utils"
/**
* Checks if the given character is within the character class range.
@@ -405,12 +326,12 @@ export default createRule("no-dupe-characters-character-class", {
) {
const intersectionText =
typeof intersection === "number"
- ? codePointToDispString(intersection)
+ ? invisibleEscape(intersection)
: intersection[0] === intersection[1]
- ? codePointToDispString(intersection[0])
- : `${codePointToDispString(
- intersection[0],
- )}-${codePointToDispString(intersection[1])}`
+ ? invisibleEscape(intersection[0])
+ : `${invisibleEscape(intersection[0])}-${invisibleEscape(
+ intersection[1],
+ )}`
for (const element of elements) {
context.report({
node,
diff --git a/lib/rules/no-invisible-character.ts b/lib/rules/no-invisible-character.ts
new file mode 100644
index 000000000..fd2c9dedb
--- /dev/null
+++ b/lib/rules/no-invisible-character.ts
@@ -0,0 +1,113 @@
+import type { RegExpLiteral, Literal } from "estree"
+import type { RegExpVisitor } from "regexpp/visitor"
+import type { AST } from "eslint"
+import {
+ createRule,
+ defineRegexpVisitor,
+ getRegexpLocation,
+ invisibleEscape,
+ getRegexpRange,
+ isInvisible,
+} from "../utils"
+
+export default createRule("no-invisible-character", {
+ meta: {
+ docs: {
+ description: "disallow invisible raw character",
+ recommended: true,
+ },
+ fixable: "code",
+ schema: [],
+ messages: {
+ unexpected:
+ 'Unexpected invisible character. Use "{{instead}}" instead.',
+ },
+ type: "suggestion", // "problem",
+ },
+ create(context) {
+ const sourceCode = context.getSourceCode()
+
+ /**
+ * Create visitor
+ * @param node
+ */
+ function createLiteralVisitor(
+ node: RegExpLiteral,
+ ): RegExpVisitor.Handlers {
+ return {
+ onCharacterEnter(cNode) {
+ if (cNode.raw === " ") {
+ return
+ }
+ if (cNode.raw.length === 1 && isInvisible(cNode.value)) {
+ const instead = invisibleEscape(
+ String.fromCodePoint(cNode.value),
+ )
+ context.report({
+ node,
+ loc: getRegexpLocation(sourceCode, node, cNode),
+ messageId: "unexpected",
+ data: {
+ instead,
+ },
+ fix(fixer) {
+ const range = getRegexpRange(
+ sourceCode,
+ node,
+ cNode,
+ )
+
+ return fixer.replaceTextRange(range, instead)
+ },
+ })
+ }
+ },
+ }
+ }
+
+ /**
+ * Verify a given string literal.
+ * @param node
+ */
+ function verifyString(node: Literal): void {
+ const text = sourceCode.getText(node)
+
+ let index = 0
+ for (const c of text) {
+ const cp = c.codePointAt(0)!
+ if (isInvisible(cp)) {
+ const instead = invisibleEscape(cp)
+ const range: AST.Range = [
+ node.range![0] + index,
+ node.range![0] + index + c.length,
+ ]
+ context.report({
+ node,
+ loc: {
+ start: sourceCode.getLocFromIndex(range[0]),
+ end: sourceCode.getLocFromIndex(range[1]),
+ },
+ messageId: "unexpected",
+ data: {
+ instead,
+ },
+ fix(fixer) {
+ return fixer.replaceTextRange(range, instead)
+ },
+ })
+ }
+ index += c.length
+ }
+ }
+
+ return defineRegexpVisitor(context, {
+ createLiteralVisitor,
+ createSourceVisitor(node) {
+ if (node.type === "Literal") {
+ verifyString(node)
+ }
+ return {} // no visit
+ },
+ })
+ },
+})
diff --git a/lib/rules/no-useless-two-nums-quantifier.ts b/lib/rules/no-useless-two-nums-quantifier.ts
new file mode 100644
index 000000000..744e58f0c
--- /dev/null
+++ b/lib/rules/no-useless-two-nums-quantifier.ts
@@ -0,0 +1,60 @@
+import type { Expression } from "estree"
+import type { RegExpVisitor } from "regexpp/visitor"
+import {
+ createRule,
+ defineRegexpVisitor,
+ getRegexpLocation,
+ getQuantifierOffsets,
+} from "../utils"
+
+export default createRule("no-useless-two-nums-quantifier", {
+ meta: {
+ docs: {
+ description: "disallow unnecessary `{n,m}` quantifier",
+ recommended: true,
+ },
+ schema: [],
+ messages: {
+ unexpected: 'Unexpected quantifier "{{expr}}".',
+ },
+ type: "suggestion", // "problem",
+ },
+ create(context) {
+ const sourceCode = context.getSourceCode()
+
+ /**
+ * Create visitor
+ * @param node
+ */
+ function createVisitor(node: Expression): RegExpVisitor.Handlers {
+ return {
+ onQuantifierEnter(qNode) {
+ if (qNode.min === qNode.max) {
+ const [startOffset, endOffset] = getQuantifierOffsets(
+ qNode,
+ )
+ const text = qNode.raw.slice(startOffset, endOffset)
+ if (!/^\{\d+,\d+\}$/u.test(text)) {
+ return
+ }
+ context.report({
+ node,
+ loc: getRegexpLocation(sourceCode, node, qNode, [
+ startOffset,
+ endOffset,
+ ]),
+ messageId: "unexpected",
+ data: {
+ expr: text,
+ },
+ })
+ }
+ },
+ }
+ }
+
+ return defineRegexpVisitor(context, {
+ createVisitor,
+ })
+ },
+})
diff --git a/lib/rules/prefer-d.ts b/lib/rules/prefer-d.ts
index 6b5b905ed..f377e8423 100644
--- a/lib/rules/prefer-d.ts
+++ b/lib/rules/prefer-d.ts
@@ -8,6 +8,7 @@ import {
CP_DIGIT_NINE,
getRegexpLocation,
getRegexpRange,
+ fixerApplyEscape,
} from "../utils"
export default createRule("prefer-d", {
@@ -69,7 +70,10 @@ export default createRule("prefer-d", {
if (range == null) {
return null
}
- return fixer.replaceTextRange(range, instead)
+ return fixer.replaceTextRange(
+ range,
+ fixerApplyEscape(instead, node),
+ )
},
})
}
diff --git a/lib/rules/prefer-question-quantifier.ts b/lib/rules/prefer-question-quantifier.ts
index 7ee9a8524..996d08a19 100644
--- a/lib/rules/prefer-question-quantifier.ts
+++ b/lib/rules/prefer-question-quantifier.ts
@@ -6,6 +6,7 @@ import {
getRegexpLocation,
getQuantifierOffsets,
getRegexpRange,
+ fixerApplyEscape,
} from "../utils"
export default createRule("prefer-question-quantifier", {
@@ -103,7 +104,10 @@ export default createRule("prefer-question-quantifier", {
if (range == null) {
return null
}
- return fixer.replaceTextRange(range, instead)
+ return fixer.replaceTextRange(
+ range,
+ fixerApplyEscape(instead, node),
+ )
},
})
}
diff --git a/lib/rules/prefer-t.ts b/lib/rules/prefer-t.ts
index fc98a29ec..1a7b23c69 100644
--- a/lib/rules/prefer-t.ts
+++ b/lib/rules/prefer-t.ts
@@ -5,6 +5,7 @@ import {
defineRegexpVisitor,
getRegexpLocation,
getRegexpRange,
+ CP_TAB,
} from "../utils"
export default createRule("prefer-t", {
@@ -27,10 +28,17 @@ export default createRule("prefer-t", {
* Create visitor
* @param node
*/
- function createVisitor(node: Expression): RegExpVisitor.Handlers {
+ function createVisitor(
+ node: Expression,
+ arrows: string[],
+ ): RegExpVisitor.Handlers {
return {
onCharacterEnter(cNode) {
- if (cNode.value === 9 && cNode.raw !== "\\t") {
+ if (
+ cNode.value === CP_TAB &&
+ cNode.raw !== "\\t" &&
+ !arrows.includes(cNode.raw)
+ ) {
context.report({
node,
loc: getRegexpLocation(sourceCode, node, cNode),
@@ -56,7 +64,12 @@ export default createRule("prefer-t", {
}
return defineRegexpVisitor(context, {
- createVisitor,
+ createLiteralVisitor(node) {
+ return createVisitor(node, [])
+ },
+ createSourceVisitor(node) {
+ return createVisitor(node, ["\t"])
+ },
})
},
})
diff --git a/lib/rules/prefer-w.ts b/lib/rules/prefer-w.ts
index daba098d8..a48228f87 100644
--- a/lib/rules/prefer-w.ts
+++ b/lib/rules/prefer-w.ts
@@ -15,6 +15,7 @@ import {
CP_DIGIT_NINE,
CP_LOW_LINE,
FLAG_IGNORECASE,
+ fixerApplyEscape,
} from "../utils"
/**
@@ -155,7 +156,7 @@ export default createRule("prefer-w", {
}
return fixer.replaceTextRange(
range,
- instead,
+ fixerApplyEscape(instead, node),
)
},
})
@@ -189,7 +190,7 @@ export default createRule("prefer-w", {
node,
unexpectedElements.shift()!,
)!,
- "\\w",
+ fixerApplyEscape("\\w", node),
)
for (const element of unexpectedElements) {
yield fixer.removeRange(
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
index 689be7e45..5e6ac68b3 100644
--- a/lib/utils/index.ts
+++ b/lib/utils/index.ts
@@ -8,8 +8,10 @@ import {
CONSTRUCT,
ReferenceTracker,
getStringIfConstant,
+ findVariable,
} from "eslint-utils"
import type { Rule, AST, SourceCode } from "eslint"
+export * from "./unicode"
export const FLAG_GLOBAL = "g"
export const FLAG_DOTALL = "s"
@@ -18,14 +20,6 @@ export const FLAG_MULTILINE = "m"
export const FLAG_STICKY = "y"
export const FLAG_UNICODE = "u"
-export const CP_DIGIT_ZERO = "0".codePointAt(0)!
-export const CP_DIGIT_NINE = "9".codePointAt(0)!
-export const CP_SMALL_A = "a".codePointAt(0)!
-export const CP_SMALL_Z = "z".codePointAt(0)!
-export const CP_CAPITAL_A = "A".codePointAt(0)!
-export const CP_CAPITAL_Z = "Z".codePointAt(0)!
-export const CP_LOW_LINE = "_".codePointAt(0)!
-
/**
* Define the rule.
* @param ruleName ruleName
@@ -107,7 +101,12 @@ export function defineRegexpVisitor(
return
}
- visitRegExpAST(patternNode, createVisitor(node, pattern, flags))
+ const visitor = createVisitor(node, pattern, flags)
+ if (Object.keys(visitor).length === 0) {
+ return
+ }
+
+ visitRegExpAST(patternNode, visitor)
}
const createLiteralVisitor =
@@ -165,8 +164,26 @@ export function defineRegexpVisitor(
const flags = getStringIfConstant(flagsNode, scope)
if (typeof pattern === "string") {
+ let verifyPatternNode = patternNode
+ if (patternNode.type === "Identifier") {
+ const variable = findVariable(
+ context.getScope(),
+ patternNode,
+ )
+ if (variable && variable.defs.length === 1) {
+ const def = variable.defs[0]
+ if (
+ def.type === "Variable" &&
+ def.parent.kind === "const" &&
+ def.node.init &&
+ def.node.init.type === "Literal"
+ ) {
+ verifyPatternNode = def.node.init
+ }
+ }
+ }
verify(
- patternNode,
+ verifyPatternNode,
pattern,
flags || "",
createSourceVisitor,
@@ -179,6 +196,16 @@ export function defineRegexpVisitor(
}
}
+export function getRegexpRange(
+ sourceCode: SourceCode,
+ node: ESTree.RegExpLiteral,
+ regexpNode: RegExpNode,
+): AST.Range
+export function getRegexpRange(
+ sourceCode: SourceCode,
+ node: ESTree.Expression,
+ regexpNode: RegExpNode,
+): AST.Range | null
/**
* Creates source range from the given regexp node
* @param sourceCode The ESLint source code instance.
@@ -255,6 +282,22 @@ export function availableRegexpLocation(
return true
}
+/**
+ * Escape depending on which node the string applied to fixer is applied.
+ */
+export function fixerApplyEscape(
+ text: string,
+ node: ESTree.Expression,
+): string {
+ if (node.type !== "Literal") {
+ throw new Error(`illegal node type:${node.type}`)
+ }
+ if (!(node as ESTree.RegExpLiteral).regex) {
+ return text.replace(/\\/gu, "\\\\")
+ }
+ return text
+}
+
/**
* Get the offsets of the given quantifier
*/
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index c57c0bae9..45c791d82 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -5,8 +5,10 @@ import noDupeCharactersCharacterClass from "../rules/no-dupe-characters-characte
import noEmptyGroup from "../rules/no-empty-group"
import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion"
import noEscapeBackspace from "../rules/no-escape-backspace"
+import noInvisibleCharacter from "../rules/no-invisible-character"
import noOctal from "../rules/no-octal"
import noUselessExactlyQuantifier from "../rules/no-useless-exactly-quantifier"
+import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
import preferD from "../rules/prefer-d"
import preferPlusQuantifier from "../rules/prefer-plus-quantifier"
import preferQuestionQuantifier from "../rules/prefer-question-quantifier"
@@ -21,8 +23,10 @@ export const rules = [
noEmptyGroup,
noEmptyLookaroundsAssertion,
noEscapeBackspace,
+ noInvisibleCharacter,
noOctal,
noUselessExactlyQuantifier,
+ noUselessTwoNumsQuantifier,
preferD,
preferPlusQuantifier,
preferQuestionQuantifier,
@@ -30,15 +34,3 @@ export const rules = [
preferT,
preferW,
] as RuleModule[]
-
-/**
- * Get recommended config
- */
-export function recommendedConfig(): { [key: string]: "error" | "warn" } {
- return rules.reduce((obj, rule) => {
- if (rule.meta.docs.recommended && !rule.meta.deprecated) {
- obj[rule.meta.docs.ruleId] = rule.meta.docs.default || "error"
- }
- return obj
- }, {} as { [key: string]: "error" | "warn" })
-}
diff --git a/lib/utils/unicode.ts b/lib/utils/unicode.ts
new file mode 100644
index 000000000..b477a351d
--- /dev/null
+++ b/lib/utils/unicode.ts
@@ -0,0 +1,167 @@
+export const CP_TAB = 9
+export const CP_LF = 10
+export const CP_VT = 11
+export const CP_FF = 12
+export const CP_CR = 13
+export const CP_SPACE = " ".codePointAt(0)!
+export const CP_NEL = "\u0085".codePointAt(0)!
+export const CP_NBSP = "\u00a0".codePointAt(0)!
+export const CP_OGHAM_SPACE_MARK = "\u1680".codePointAt(0)!
+export const CP_MONGOLIAN_VOWEL_SEPARATOR = "\u180e".codePointAt(0)!
+export const CP_EN_QUAD = "\u2000".codePointAt(0)!
+export const CP_HAIR_SPACE = "\u200a".codePointAt(0)!
+export const CP_ZWSP = "\u200b".codePointAt(0)!
+export const CP_ZWNJ = "\u200c".codePointAt(0)!
+export const CP_ZWJ = "\u200d".codePointAt(0)!
+export const CP_LRM = "\u200e".codePointAt(0)!
+export const CP_RLM = "\u200f".codePointAt(0)!
+export const CP_LINE_SEPARATOR = "\u2028".codePointAt(0)!
+export const CP_PARAGRAPH_SEPARATOR = "\u2029".codePointAt(0)!
+export const CP_NNBSP = "\u202f".codePointAt(0)!
+export const CP_MMSP = "\u205f".codePointAt(0)!
+export const CP_BRAILLE_PATTERN_BLANK = "\u2800".codePointAt(0)!
+export const CP_IDEOGRAPHIC_SPACE = "\u3000".codePointAt(0)!
+export const CP_BOM = "\ufeff".codePointAt(0)!
+export const CP_DIGIT_ZERO = "0".codePointAt(0)!
+export const CP_DIGIT_NINE = "9".codePointAt(0)!
+export const CP_SMALL_A = "a".codePointAt(0)!
+export const CP_SMALL_Z = "z".codePointAt(0)!
+export const CP_CAPITAL_A = "A".codePointAt(0)!
+export const CP_CAPITAL_Z = "Z".codePointAt(0)!
+export const CP_LOW_LINE = "_".codePointAt(0)!
+
+export const CPS_SINGLE_SPACES = new Set([
+ CP_SPACE,
+ CP_TAB,
+ CP_CR,
+ CP_LF,
+ CP_VT,
+ CP_FF,
+ CP_NBSP,
+ CP_OGHAM_SPACE_MARK,
+ CP_MONGOLIAN_VOWEL_SEPARATOR,
+ CP_LINE_SEPARATOR,
+ CP_PARAGRAPH_SEPARATOR,
+ CP_NNBSP,
+ CP_MMSP,
+ CP_IDEOGRAPHIC_SPACE,
+ CP_BOM,
+])
+
+export const CP_RANGE_DIGIT = [CP_DIGIT_ZERO, CP_DIGIT_NINE] as const
+export const CP_RANGE_SMALL_LETTER = [CP_SMALL_A, CP_SMALL_Z] as const
+export const CP_RANGE_CAPITAL_LETTER = [CP_CAPITAL_A, CP_CAPITAL_Z] as const
+export const CP_RANGE_SPACES = [CP_EN_QUAD, CP_HAIR_SPACE] as const
+
+export const CP_RANGES_WORDS = [
+ CP_RANGE_SMALL_LETTER,
+ CP_RANGE_CAPITAL_LETTER,
+ CP_RANGE_DIGIT,
+]
+
+/**
+ * Checks if the given code point is within the code point range.
+ * @param codePoint The code point to check.
+ * @param range The range of code points of the range.
+ * @returns {boolean} `true` if the given character is within the character class range.
+ */
+function isCodePointInRange(
+ codePoint: number,
+ [start, end]: readonly [number, number],
+) {
+ return start <= codePoint && codePoint <= end
+}
+
+/**
+ * Checks if the given code point is digit.
+ * @param codePoint The code point to check
+ * @returns {boolean} `true` if the given code point is digit.
+ */
+export function isDigit(codePoint: number) {
+ return isCodePointInRange(codePoint, CP_RANGE_DIGIT)
+}
+
+/**
+ * Checks if the given code point is space.
+ * @param codePoint The code point to check
+ * @returns {boolean} `true` if the given code point is space.
+ */
+export function isSpace(codePoint: number) {
+ return (
+ CPS_SINGLE_SPACES.has(codePoint) ||
+ isCodePointInRange(codePoint, CP_RANGE_SPACES)
+ )
+}
+
+/**
+ * Checks if the given code point is word.
+ * @param codePoint The code point to check
+ * @returns {boolean} `true` if the given code point is word.
+ */
+export function isWord(codePoint: number) {
+ return (
+ CP_RANGES_WORDS.some((range) => isCodePointInRange(codePoint, range)) ||
+ CP_LOW_LINE === codePoint
+ )
+}
+
+/**
+ * Checks if the given code point is invisible character.
+ * @param codePoint The code point to check
+ * @returns {boolean} `true` if the given code point is invisible character.
+ */
+export function isInvisible(codePoint: number): boolean {
+ if (isSpace(codePoint)) {
+ return true
+ }
+ return (
+ codePoint === CP_NEL ||
+ codePoint === CP_ZWSP ||
+ codePoint === CP_ZWNJ ||
+ codePoint === CP_ZWJ ||
+ codePoint === CP_LRM ||
+ codePoint === CP_RLM ||
+ codePoint === CP_BRAILLE_PATTERN_BLANK
+ )
+}
+
+/**
+ * Returns a string with invisible characters converted to escape characters.
+ */
+export function invisibleEscape(val: string | number): string {
+ let result = ""
+
+ for (const cp of typeof val === "number" ? [val] : codePoints(val)) {
+ if (cp !== CP_SPACE && isInvisible(cp)) {
+ if (cp === CP_TAB) {
+ result += "\\t"
+ } else if (cp === CP_LF) {
+ result += "\\r"
+ } else if (cp === CP_CR) {
+ result += "\\n"
+ } else if (cp === CP_VT) {
+ result += "\\v"
+ } else if (cp === CP_FF) {
+ result += "\\f"
+ } else {
+ result += `\\u${`${cp.toString(16)}`.padStart(4, "0")}`
+ }
+ } else {
+ result += String.fromCodePoint(cp)
+ }
+ }
+ return result
+}
+
+/**
+ * String to code points
+ */
+function* codePoints(s: string) {
+ for (let i = 0; i < s.length; i += 1) {
+ const cp = s.codePointAt(i)!
+ yield cp
+ if (cp >= 0x10000) {
+ i += 1
+ }
+ }
+}
diff --git a/package.json b/package.json
index ae2856107..1af4d41c0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-regexp",
- "version": "0.0.0",
+ "version": "0.1.0",
"description": "ESLint plugin for finding RegExp mistakes and RegExp style guide violations.",
"main": "dist/index.js",
"files": [
diff --git a/tests/lib/rules/no-invisible-character.ts b/tests/lib/rules/no-invisible-character.ts
new file mode 100644
index 000000000..43ee8f0e7
--- /dev/null
+++ b/tests/lib/rules/no-invisible-character.ts
@@ -0,0 +1,111 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/no-invisible-character"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run("no-invisible-character", rule as any, {
+ valid: [
+ "/a/",
+ "/ /",
+ "/[a]/",
+ "/[ ]/",
+ "/\\t/",
+ "new RegExp('\\t')",
+ `
+ const a = '' + '\t';
+ new RegExp(a)`,
+ ],
+ invalid: [
+ {
+ code: "/\u00a0/",
+ output: "/\\u00a0/",
+ errors: ['Unexpected invisible character. Use "\\u00a0" instead.'],
+ },
+ {
+ code: "/[\t]/",
+ output: "/[\\t]/",
+ errors: ['Unexpected invisible character. Use "\\t" instead.'],
+ },
+ {
+ code:
+ "/[\t\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff\u0085\u200b]/",
+ output:
+ "/[\\t\u00a0\\u1680\u180e\\u2000\u2001\\u2002\u2003\\u2004\u2005\\u2006\u2007\\u2008\u2009\\u200a\u202f\\u205f\u3000\\ufeff\u0085\\u200b]/",
+ errors: [
+ 'Unexpected invisible character. Use "\\t" instead.',
+ 'Unexpected invisible character. Use "\\u00a0" instead.',
+ 'Unexpected invisible character. Use "\\u1680" instead.',
+ 'Unexpected invisible character. Use "\\u180e" instead.',
+ 'Unexpected invisible character. Use "\\u2000" instead.',
+ 'Unexpected invisible character. Use "\\u2001" instead.',
+ 'Unexpected invisible character. Use "\\u2002" instead.',
+ 'Unexpected invisible character. Use "\\u2003" instead.',
+ 'Unexpected invisible character. Use "\\u2004" instead.',
+ 'Unexpected invisible character. Use "\\u2005" instead.',
+ 'Unexpected invisible character. Use "\\u2006" instead.',
+ 'Unexpected invisible character. Use "\\u2007" instead.',
+ 'Unexpected invisible character. Use "\\u2008" instead.',
+ 'Unexpected invisible character. Use "\\u2009" instead.',
+ 'Unexpected invisible character. Use "\\u200a" instead.',
+ 'Unexpected invisible character. Use "\\u202f" instead.',
+ 'Unexpected invisible character. Use "\\u205f" instead.',
+ 'Unexpected invisible character. Use "\\u3000" instead.',
+ 'Unexpected invisible character. Use "\\ufeff" instead.',
+ 'Unexpected invisible character. Use "\\u0085" instead.',
+ 'Unexpected invisible character. Use "\\u200b" instead.',
+ ],
+ },
+ {
+ code:
+ "/[\\t\u00a0\\u1680\u180e\\u2000\u2001\\u2002\u2003\\u2004\u2005\\u2006\u2007\\u2008\u2009\\u200a\u202f\\u205f\u3000\\ufeff\u0085\\u200b]/",
+ output:
+ "/[\\t\\u00a0\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\ufeff\\u0085\\u200b]/",
+ errors: [
+ 'Unexpected invisible character. Use "\\u00a0" instead.',
+ 'Unexpected invisible character. Use "\\u180e" instead.',
+ 'Unexpected invisible character. Use "\\u2001" instead.',
+ 'Unexpected invisible character. Use "\\u2003" instead.',
+ 'Unexpected invisible character. Use "\\u2005" instead.',
+ 'Unexpected invisible character. Use "\\u2007" instead.',
+ 'Unexpected invisible character. Use "\\u2009" instead.',
+ 'Unexpected invisible character. Use "\\u202f" instead.',
+ 'Unexpected invisible character. Use "\\u3000" instead.',
+ 'Unexpected invisible character. Use "\\u0085" instead.',
+ ],
+ },
+ {
+ code:
+ "new RegExp('\t\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff\u0085\u200b')",
+ output:
+ "new RegExp('\\t\u00a0\\u1680\u180e\\u2000\u2001\\u2002\u2003\\u2004\u2005\\u2006\u2007\\u2008\u2009\\u200a\u202f\\u205f\u3000\\ufeff\u0085\\u200b')",
+ errors: [
+ 'Unexpected invisible character. Use "\\t" instead.',
+ 'Unexpected invisible character. Use "\\u00a0" instead.',
+ 'Unexpected invisible character. Use "\\u1680" instead.',
+ 'Unexpected invisible character. Use "\\u180e" instead.',
+ 'Unexpected invisible character. Use "\\u2000" instead.',
+ 'Unexpected invisible character. Use "\\u2001" instead.',
+ 'Unexpected invisible character. Use "\\u2002" instead.',
+ 'Unexpected invisible character. Use "\\u2003" instead.',
+ 'Unexpected invisible character. Use "\\u2004" instead.',
+ 'Unexpected invisible character. Use "\\u2005" instead.',
+ 'Unexpected invisible character. Use "\\u2006" instead.',
+ 'Unexpected invisible character. Use "\\u2007" instead.',
+ 'Unexpected invisible character. Use "\\u2008" instead.',
+ 'Unexpected invisible character. Use "\\u2009" instead.',
+ 'Unexpected invisible character. Use "\\u200a" instead.',
+ 'Unexpected invisible character. Use "\\u202f" instead.',
+ 'Unexpected invisible character. Use "\\u205f" instead.',
+ 'Unexpected invisible character. Use "\\u3000" instead.',
+ 'Unexpected invisible character. Use "\\ufeff" instead.',
+ 'Unexpected invisible character. Use "\\u0085" instead.',
+ 'Unexpected invisible character. Use "\\u200b" instead.',
+ ],
+ },
+ ],
+})
diff --git a/tests/lib/rules/no-useless-two-nums-quantifier.ts b/tests/lib/rules/no-useless-two-nums-quantifier.ts
new file mode 100644
index 000000000..5da8aaecf
--- /dev/null
+++ b/tests/lib/rules/no-useless-two-nums-quantifier.ts
@@ -0,0 +1,33 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/no-useless-two-nums-quantifier"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run("no-useless-two-nums-quantifier", rule as any, {
+ valid: ["/a{1,2}/", "/a{1,}/", "/a{1}/", "/a?/"],
+ invalid: [
+ {
+ code: "/a{1,1}/",
+ errors: [
+ {
+ message: 'Unexpected quantifier "{1,1}".',
+ column: 3,
+ endColumn: 8,
+ },
+ ],
+ },
+ {
+ code: "/a{42,42}/",
+ errors: ['Unexpected quantifier "{42,42}".'],
+ },
+ {
+ code: "/a{042,42}/",
+ errors: ['Unexpected quantifier "{042,42}".'],
+ },
+ ],
+})
diff --git a/tests/lib/rules/prefer-d.ts b/tests/lib/rules/prefer-d.ts
index 672d0eee3..b16e9f2de 100644
--- a/tests/lib/rules/prefer-d.ts
+++ b/tests/lib/rules/prefer-d.ts
@@ -52,6 +52,17 @@ tester.run("prefer-d", rule as any, {
const s = "[0-9]"
new RegExp(s)
`,
+ output: `
+ const s = "\\\\d"
+ new RegExp(s)
+ `,
+ errors: ['Unexpected character set "[0-9]". Use "\\d" instead.'],
+ },
+ {
+ code: `
+ const s = "[0-"+"9]"
+ new RegExp(s)
+ `,
output: null,
errors: ['Unexpected character set "[0-9]". Use "\\d" instead.'],
},
diff --git a/tests/lib/rules/prefer-plus-quantifier.ts b/tests/lib/rules/prefer-plus-quantifier.ts
index 7b32206f5..875d53d77 100644
--- a/tests/lib/rules/prefer-plus-quantifier.ts
+++ b/tests/lib/rules/prefer-plus-quantifier.ts
@@ -60,6 +60,17 @@ tester.run("prefer-plus-quantifier", rule as any, {
const s = "a{1,}"
new RegExp(s)
`,
+ output: `
+ const s = "a+"
+ new RegExp(s)
+ `,
+ errors: ['Unexpected quantifier "{1,}". Use "+" instead.'],
+ },
+ {
+ code: `
+ const s = "a{1"+",}"
+ new RegExp(s)
+ `,
output: null,
errors: ['Unexpected quantifier "{1,}". Use "+" instead.'],
},
diff --git a/tests/lib/rules/prefer-question-quantifier.ts b/tests/lib/rules/prefer-question-quantifier.ts
index 73494d066..e565bef2d 100644
--- a/tests/lib/rules/prefer-question-quantifier.ts
+++ b/tests/lib/rules/prefer-question-quantifier.ts
@@ -92,6 +92,17 @@ tester.run("prefer-question-quantifier", rule as any, {
const s = "a{0,1}"
new RegExp(s)
`,
+ output: `
+ const s = "a?"
+ new RegExp(s)
+ `,
+ errors: ['Unexpected quantifier "{0,1}". Use "?" instead.'],
+ },
+ {
+ code: `
+ const s = "a{0,"+"1}"
+ new RegExp(s)
+ `,
output: null,
errors: ['Unexpected quantifier "{0,1}". Use "?" instead.'],
},
@@ -100,6 +111,19 @@ tester.run("prefer-question-quantifier", rule as any, {
const s = "(?:abc||def)"
new RegExp(s)
`,
+ output: `
+ const s = "(?:abc|def)?"
+ new RegExp(s)
+ `,
+ errors: [
+ 'Unexpected group "(?:abc||def)". Use "(?:abc|def)?" instead.',
+ ],
+ },
+ {
+ code: `
+ const s = "(?:abc|"+"|def)"
+ new RegExp(s)
+ `,
output: null,
errors: [
'Unexpected group "(?:abc||def)". Use "(?:abc|def)?" instead.',
diff --git a/tests/lib/rules/prefer-star-quantifier.ts b/tests/lib/rules/prefer-star-quantifier.ts
index f83132871..db501cbcc 100644
--- a/tests/lib/rules/prefer-star-quantifier.ts
+++ b/tests/lib/rules/prefer-star-quantifier.ts
@@ -60,6 +60,17 @@ tester.run("prefer-star-quantifier", rule as any, {
const s = "a{0,}"
new RegExp(s)
`,
+ output: `
+ const s = "a*"
+ new RegExp(s)
+ `,
+ errors: ['Unexpected quantifier "{0,}". Use "*" instead.'],
+ },
+ {
+ code: `
+ const s = "a{0"+",}"
+ new RegExp(s)
+ `,
output: null,
errors: ['Unexpected quantifier "{0,}". Use "*" instead.'],
},
diff --git a/tests/lib/rules/prefer-t.ts b/tests/lib/rules/prefer-t.ts
index 73b954276..c9bda135a 100644
--- a/tests/lib/rules/prefer-t.ts
+++ b/tests/lib/rules/prefer-t.ts
@@ -9,7 +9,7 @@ const tester = new RuleTester({
})
tester.run("prefer-t", rule as any, {
- valid: ["/\\t/"],
+ valid: ["/\\t/", "new RegExp('\t')"],
invalid: [
{
code: "/\\u0009/",
@@ -36,11 +36,11 @@ tester.run("prefer-t", rule as any, {
},
{
code: `
- const s = "\t"
+ const s = "\\\\u0009"
new RegExp(s)
`,
output: null,
- errors: ['Unexpected character "\t". Use "\\t" instead.'],
+ errors: ['Unexpected character "\\u0009". Use "\\t" instead.'],
},
],
})
diff --git a/tests/lib/rules/prefer-w.ts b/tests/lib/rules/prefer-w.ts
index 891d2fd6a..e1f028e43 100644
--- a/tests/lib/rules/prefer-w.ts
+++ b/tests/lib/rules/prefer-w.ts
@@ -61,6 +61,19 @@ tester.run("prefer-w", rule as any, {
const s = "[0-9A-Z_]"
new RegExp(s, 'i')
`,
+ output: `
+ const s = "\\\\w"
+ new RegExp(s, 'i')
+ `,
+ errors: [
+ 'Unexpected character set "[0-9A-Z_]". Use "\\w" instead.',
+ ],
+ },
+ {
+ code: `
+ const s = "[0-9"+"A-Z_]"
+ new RegExp(s, 'i')
+ `,
output: null,
errors: [
'Unexpected character set "[0-9A-Z_]". Use "\\w" instead.',
@@ -71,6 +84,19 @@ tester.run("prefer-w", rule as any, {
const s = "[0-9A-Z_c]"
new RegExp(s, 'i')
`,
+ output: `
+ const s = "[\\\\wc]"
+ new RegExp(s, 'i')
+ `,
+ errors: [
+ 'Unexpected character set "[0-9A-Z_]". Use "\\w" instead.',
+ ],
+ },
+ {
+ code: `
+ const s = "[0-9"+"A-Z_c]"
+ new RegExp(s, 'i')
+ `,
output: null,
errors: [
'Unexpected character set "[0-9A-Z_]". Use "\\w" instead.',
diff --git a/tests/lib/utils/unicode.ts b/tests/lib/utils/unicode.ts
new file mode 100644
index 000000000..055e2ca10
--- /dev/null
+++ b/tests/lib/utils/unicode.ts
@@ -0,0 +1,35 @@
+import assert from "assert"
+import {
+ isSpace,
+ isInvisible,
+ invisibleEscape,
+ CP_NEL,
+ CP_ZWSP,
+} from "../../../lib/utils/unicode"
+
+const SPACES =
+ " \f\n\r\t\v\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
+
+describe("isSpace", () => {
+ for (const c of SPACES) {
+ it(`${invisibleEscape(c)} is space`, () => {
+ assert.ok(isSpace(c.codePointAt(0)!))
+ })
+ }
+
+ for (const c of [CP_NEL, CP_ZWSP]) {
+ it(`${invisibleEscape(c)} is not space`, () => {
+ assert.ok(!isSpace(c))
+ })
+ }
+})
+
+describe("isInvisible", () => {
+ const str = `${SPACES}\u0085\u200b\u200c\u200d\u200e\u200f\u2800`
+
+ for (const c of str) {
+ it(`${invisibleEscape(c)} is invisible`, () => {
+ assert.ok(isInvisible(c.codePointAt(0)!))
+ })
+ }
+})
diff --git a/tools/update-rules.ts b/tools/update-rules.ts
index c3448cc7c..c170f3a5a 100644
--- a/tools/update-rules.ts
+++ b/tools/update-rules.ts
@@ -26,18 +26,6 @@ ${rules
export const rules = [
${rules.map((rule) => camelCase(rule.meta.docs.ruleName)).join(",")}
] as RuleModule[]
-
-/**
- * Get recommended config
- */
-export function recommendedConfig(): { [key: string]: "error" | "warn" } {
- return rules.reduce((obj, rule) => {
- if (rule.meta.docs.recommended && !rule.meta.deprecated) {
- obj[rule.meta.docs.ruleId] = rule.meta.docs.default || "error"
- }
- return obj
- }, {} as { [key: string]: "error" | "warn" })
-}
`
const filePath = path.resolve(__dirname, "../lib/utils/rules.ts")
diff --git a/tools/update-rulesets.ts b/tools/update-rulesets.ts
new file mode 100644
index 000000000..5f41b17fe
--- /dev/null
+++ b/tools/update-rulesets.ts
@@ -0,0 +1,55 @@
+import path from "path"
+import fs from "fs"
+import os from "os"
+// import eslint from "eslint"
+import { rules } from "./lib/load-rules"
+const isWin = os.platform().startsWith("win")
+
+const coreRules = [
+ "no-control-regex",
+ "no-invalid-regexp",
+ "no-misleading-character-class",
+ "no-regex-spaces",
+ "no-useless-backreference",
+ "prefer-regex-literals",
+ // "prefer-named-capture-group", // modern
+ // "require-unicode-regexp", // modern
+]
+
+let content = `
+export default {
+ plugins: ["regexp"],
+ rules: {
+ // ESLint core rules
+ ${coreRules.map((ruleName) => `"${ruleName}": "error"`).join(",\n")},
+
+ // eslint-plugin-regexp rules
+ ${rules
+ .filter(
+ (rule) => rule.meta.docs.recommended && !rule.meta.deprecated,
+ )
+ .map((rule) => {
+ const conf = rule.meta.docs.default || "error"
+ return `"${rule.meta.docs.ruleId}": "${conf}"`
+ })
+ .join(",\n")}
+ },
+}
+`
+
+const filePath = path.resolve(__dirname, "../lib/configs/recommended.ts")
+
+if (isWin) {
+ content = content
+ .replace(/\r?\n/gu, "\n")
+ .replace(/\r/gu, "\n")
+ .replace(/\n/gu, "\r\n")
+}
+
+// Update file.
+fs.writeFileSync(filePath, content)
+
+// Format files.
+// const linter = new eslint.CLIEngine({ fix: true })
+// const report = linter.executeOnFiles([filePath])
+// eslint.CLIEngine.outputFixes(report)
diff --git a/tools/update.ts b/tools/update.ts
index 9aa003798..5a7422bb6 100644
--- a/tools/update.ts
+++ b/tools/update.ts
@@ -1,4 +1,5 @@
import "./update-rules"
+import "./update-rulesets"
import "./update-docs"
import "./update-readme"
import "./update-docs-rules-index"