diff --git a/README.md b/README.md index 55485267d..bd480fd4a 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco | [regexp/no-useless-assertions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-assertions.html) | disallow assertions that are known to always accept (or reject) | | | [regexp/no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | :star: | | [regexp/no-useless-dollar-replacements](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-dollar-replacements.html) | disallow useless `$` replacements in replacement string | | -| [regexp/strict](https://ota-meshi.github.io/eslint-plugin-regexp/rules/strict.html) | disallow not strictly valid regular expressions | | +| [regexp/strict](https://ota-meshi.github.io/eslint-plugin-regexp/rules/strict.html) | disallow not strictly valid regular expressions | :wrench: | ### Best Practices diff --git a/docs/rules/README.md b/docs/rules/README.md index b1599c2d6..18f07ff7c 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -25,7 +25,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco | [regexp/no-useless-assertions](./no-useless-assertions.md) | disallow assertions that are known to always accept (or reject) | | | [regexp/no-useless-backreference](./no-useless-backreference.md) | disallow useless backreferences in regular expressions | :star: | | [regexp/no-useless-dollar-replacements](./no-useless-dollar-replacements.md) | disallow useless `$` replacements in replacement string | | -| [regexp/strict](./strict.md) | disallow not strictly valid regular expressions | | +| [regexp/strict](./strict.md) | disallow not strictly valid regular expressions | :wrench: | ### Best Practices diff --git a/docs/rules/strict.md b/docs/rules/strict.md index edaab300c..6f96c2034 100644 --- a/docs/rules/strict.md +++ b/docs/rules/strict.md @@ -9,6 +9,7 @@ description: "disallow not strictly valid regular expressions" > disallow not strictly valid regular expressions - :exclamation: ***This rule has not been released yet.*** +- :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 @@ -20,7 +21,7 @@ Depending on the syntax defined in [Annex B] of the ECMAScript specification, so [Annex B]: https://tc39.es/ecma262/#sec-regular-expressions-patterns - + ```js /* eslint regexp/strict: "error" */ diff --git a/lib/rules/strict.ts b/lib/rules/strict.ts index e9c8b425b..0e7cc4c9f 100644 --- a/lib/rules/strict.ts +++ b/lib/rules/strict.ts @@ -1,15 +1,24 @@ import { RegExpValidator } from "regexpp" +import type { CharacterClassElement, Element } from "regexpp/ast" import type { RegExpVisitor } from "regexpp/visitor" import type { RegExpContext } from "../utils" -import { createRule, defineRegexpVisitor } from "../utils" +import { + isOctalEscape, + createRule, + defineRegexpVisitor, + isEscapeSequence, +} from "../utils" -const validator = new RegExpValidator({ strict: true }) +const validator = new RegExpValidator({ strict: true, ecmaVersion: 2020 }) /** * Check syntax error in a given pattern. - * @returns {string|null} The syntax error. + * @returns The syntax error. */ -function validateRegExpPattern(pattern: string, uFlag?: boolean) { +function validateRegExpPattern( + pattern: string, + uFlag?: boolean, +): string | null { try { validator.validatePattern(pattern, undefined, undefined, uFlag) return null @@ -18,6 +27,9 @@ function validateRegExpPattern(pattern: string, uFlag?: boolean) { } } +const CHARACTER_CLASS_SYNTAX_CHARACTERS = new Set("\\/()[]{}^$.|-+*?".split("")) +const SYNTAX_CHARACTERS = new Set("\\/()[]{}^$.|+*?".split("")) + export default createRule("strict", { meta: { docs: { @@ -27,8 +39,33 @@ export default createRule("strict", { // recommended: true, recommended: false, }, + fixable: "code", schema: [], messages: { + // character escape + invalidControlEscape: + "Invalid or incomplete control escape sequence. Either use a valid control escape sequence or escaping the standalone backslash.", + incompleteEscapeSequence: + "Incomplete escape sequence '{{expr}}'. Either use a valid escape sequence or remove the useless escaping.", + invalidPropertyEscape: + "Invalid property escape sequence '{{expr}}'. Either use a valid property escape sequence or remove the useless escaping.", + incompleteBackreference: + "Incomplete backreference '{{expr}}'. Either use a valid backreference or remove the useless escaping.", + unescapedSourceCharacter: "Unescaped source character '{{expr}}'.", + octalEscape: + "Invalid legacy octal escape sequence '{{expr}}'. Use a hexadecimal escape instead.", + uselessEscape: + "Useless identity escapes with non-syntax characters are forbidden.", + + // character class + invalidRange: + "Invalid character class range. A character set cannot be the minimum or maximum of a character class range. Either escape the `-` or fix the character class range.", + + // assertion + quantifiedAssertion: + "Assertion are not allowed to be quantified directly.", + + // validator regexMessage: "{{message}}.", }, type: "suggestion", @@ -40,21 +77,195 @@ export default createRule("strict", { function createVisitor( regexpContext: RegExpContext, ): RegExpVisitor.Handlers { - const { node, flags, pattern } = regexpContext + const { + node, + flags, + pattern, + getRegexpLocation, + fixReplaceNode, + } = regexpContext + + if (flags.unicode) { + // the Unicode flag enables strict parsing mode automatically + return {} + } - const message = validateRegExpPattern(pattern, flags.unicode) + let reported = false + let hasNamedBackreference = false + + /** Report */ + function report( + messageId: string, + element: Element, + fix?: string | null, + ): void { + reported = true - if (message) { context.report({ node, - messageId: "regexMessage", + loc: getRegexpLocation(element), + messageId, data: { - message, + expr: element.raw, }, + fix: fix ? fixReplaceNode(element, fix) : null, }) } - return {} + return { + // eslint-disable-next-line complexity -- x + onCharacterEnter(cNode) { + if (cNode.raw === "\\") { + // e.g. \c5 or \c + report("invalidControlEscape", cNode) + return + } + if (cNode.raw === "\\u" || cNode.raw === "\\x") { + // e.g. \u000; + report("incompleteEscapeSequence", cNode) + return + } + if (cNode.raw === "\\p" || cNode.raw === "\\P") { + // e.g. \p{H} or \p + report("invalidPropertyEscape", cNode) + return + } + if (cNode.value !== 0 && isOctalEscape(cNode.raw)) { + // e.g. \023 + report( + "octalEscape", + cNode, + `\\x${cNode.value.toString(16).padStart(2, "0")}`, + ) + return + } + + const insideCharClass = + cNode.parent.type === "CharacterClass" || + cNode.parent.type === "CharacterClassRange" + + if (!insideCharClass) { + if (cNode.raw === "\\k") { + // e.g. \k)\k/`, + String.raw`/\p{L}/u`, + String.raw`/ \( \) \[ \] \{ \} \| \* \+ \? \^ \$ \\ \/ \./`, + String.raw`/[\( \) \[ \] \{ \} \| \* \+ \? \^ \$ \\ \/ \. \-]/`, + "/\\u000f/", + "/\\x000f/", + ], invalid: [ + // source characters + { + code: String.raw`/]/`, + output: String.raw`/\]/`, + errors: [ + { + message: "Unescaped source character ']'.", + column: 2, + }, + ], + }, + { + code: String.raw`/{/`, + output: String.raw`/\{/`, + errors: [ + { + message: "Unescaped source character '{'.", + column: 2, + }, + ], + }, { - code: `/]/`, + code: String.raw`/}/`, + output: String.raw`/\}/`, + errors: [ + { + message: "Unescaped source character '}'.", + column: 2, + }, + ], + }, + + // invalid or incomplete escape sequences + { + code: String.raw`/\u{42}/`, + output: null, errors: [ { message: - "Invalid regular expression: /]/: Lone quantifier brackets.", - line: 1, - column: 1, + "Incomplete escape sequence '\\u'. Either use a valid escape sequence or remove the useless escaping.", + column: 2, }, ], }, { - code: `/{/`, + code: "/\\u000;/", + output: null, errors: [ { message: - "Invalid regular expression: /{/: Lone quantifier brackets.", - line: 1, - column: 1, + "Incomplete escape sequence '\\u'. Either use a valid escape sequence or remove the useless escaping.", + column: 2, }, ], }, { - code: `/}/`, + code: "/\\x4/", + output: null, errors: [ { message: - "Invalid regular expression: /}/: Lone quantifier brackets.", - line: 1, - column: 1, + "Incomplete escape sequence '\\x'. Either use a valid escape sequence or remove the useless escaping.", + column: 2, }, ], }, { - code: String.raw`/\u{42}/`, + code: "/\\c;/", + output: null, + errors: [ + { + message: + "Invalid or incomplete control escape sequence. Either use a valid control escape sequence or escaping the standalone backslash.", + column: 2, + }, + ], + }, + { + code: "/\\p/", + output: null, + errors: [ + { + message: + "Invalid property escape sequence '\\p'. Either use a valid property escape sequence or remove the useless escaping.", + column: 2, + }, + ], + }, + { + code: "/\\p{H}/", + output: "/\\p\\{H\\}/", + errors: [ + { + message: + "Invalid property escape sequence '\\p'. Either use a valid property escape sequence or remove the useless escaping.", + column: 2, + }, + { + message: "Unescaped source character '{'.", + column: 4, + }, + { + message: "Unescaped source character '}'.", + column: 6, + }, + ], + }, + { + code: "/\\012/", + output: "/\\x0a/", + errors: [ + { + message: + "Invalid legacy octal escape sequence '\\012'. Use a hexadecimal escape instead.", + column: 2, + }, + ], + }, + + // incomplete backreference + { + code: "/\\k/", + output: null, + errors: [ + { + message: + "Incomplete backreference '\\k'. Either use a valid backreference or remove the useless escaping.", + column: 2, + }, + ], + }, + + // useless escape + { + code: "/\\; \\_ \\a \\- \\'/", + output: "/; _ a - '/", + errors: [ + { + message: + "Useless identity escapes with non-syntax characters are forbidden.", + column: 2, + }, + { + message: + "Useless identity escapes with non-syntax characters are forbidden.", + column: 5, + }, + { + message: + "Useless identity escapes with non-syntax characters are forbidden.", + column: 8, + }, + { + message: + "Useless identity escapes with non-syntax characters are forbidden.", + column: 11, + }, + { + message: + "Useless identity escapes with non-syntax characters are forbidden.", + column: 14, + }, + ], + }, + { + code: "/[\\; \\_ \\a \\']/", + output: "/[; _ a ']/", + errors: [ + { + message: + "Useless identity escapes with non-syntax characters are forbidden.", + column: 3, + }, + { + message: + "Useless identity escapes with non-syntax characters are forbidden.", + column: 6, + }, + { + message: + "Useless identity escapes with non-syntax characters are forbidden.", + column: 9, + }, + { + message: + "Useless identity escapes with non-syntax characters are forbidden.", + column: 12, + }, + ], + }, + + // invalid ranges + { + code: String.raw`/[\w-a]/`, + output: null, + errors: [ + { + message: + "Invalid character class range. A character set cannot be the minimum or maximum of a character class range. Either escape the `-` or fix the character class range.", + column: 3, + }, + ], + }, + { + code: String.raw`/[a-\w]/`, + output: null, + errors: [ + { + message: + "Invalid character class range. A character set cannot be the minimum or maximum of a character class range. Either escape the `-` or fix the character class range.", + column: 5, + }, + ], + }, + + // quantified assertions + { + code: String.raw`/(?!a)+/`, + output: String.raw`/(?:(?!a))+/`, errors: [ { message: - "Invalid regular expression: /\\u{42}/: Invalid unicode escape.", - line: 1, - column: 1, + "Assertion are not allowed to be quantified directly.", + column: 2, }, ], },