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"