From 18662edd8823518f400af1d20aa180158edeb88d Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 9 Sep 2023 14:51:00 +0900 Subject: [PATCH 1/3] Add support for v flag to `regexp/no-useless-escape` rule --- lib/rules/no-useless-escape.ts | 144 +++++++-- lib/utils/unicode.ts | 8 + tests/lib/rules/no-useless-escape.ts | 419 ++++++++++++++++++++++++++- 3 files changed, 549 insertions(+), 22 deletions(-) diff --git a/lib/rules/no-useless-escape.ts b/lib/rules/no-useless-escape.ts index 1045eeeac..8a18924fe 100644 --- a/lib/rules/no-useless-escape.ts +++ b/lib/rules/no-useless-escape.ts @@ -1,5 +1,9 @@ import type { RegExpVisitor } from "@eslint-community/regexpp/visitor" -import type { Character } from "@eslint-community/regexpp/ast" +import type { + Character, + CharacterClass, + ExpressionCharacterClass, +} from "@eslint-community/regexpp/ast" import type { RegExpContext } from "../utils" import { createRule, @@ -21,6 +25,19 @@ import { CP_PIPE, CP_MINUS, canUnwrapped, + CP_HASH, + CP_PERCENT, + CP_BAN, + CP_AMP, + CP_COMMA, + CP_COLON, + CP_SEMI, + CP_LT, + CP_EQ, + CP_GT, + CP_AT, + CP_TILDE, + CP_BACKTICK, } from "../utils" const REGEX_CHAR_CLASS_ESCAPES = new Set([ @@ -28,6 +45,18 @@ const REGEX_CHAR_CLASS_ESCAPES = new Set([ CP_CLOSING_BRACKET, // ] CP_MINUS, // - ]) +const REGEX_CLASS_SET_CHAR_CLASS_ESCAPE = new Set([ + CP_BACK_SLASH, // \\ + CP_SLASH, // / + CP_OPENING_BRACKET, // [ + CP_CLOSING_BRACKET, // ] + CP_OPENING_BRACE, // { + CP_CLOSING_BRACE, // } + CP_PIPE, // | + CP_OPENING_PAREN, // ( + CP_CLOSING_PAREN, // ) + CP_MINUS, // -, +]) const REGEX_ESCAPES = new Set([ CP_BACK_SLASH, // \\ CP_SLASH, // / @@ -46,7 +75,30 @@ const REGEX_ESCAPES = new Set([ CP_CLOSING_PAREN, // ) ]) -const POTENTIAL_ESCAPE_SEQUENCE = new Set("uxkpP") +const POTENTIAL_ESCAPE_SEQUENCE = new Set("uxkpPq") +// A single character set of ClassSetReservedDoublePunctuator. +// && !! ## $$ %% ** ++ ,, .. :: ;; << == >> ?? @@ ^^ `` ~~ are ClassSetReservedDoublePunctuator +const REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR = new Set([ + CP_BAN, // ! + CP_HASH, // # + CP_DOLLAR, // $ + CP_PERCENT, // % + CP_AMP, // & + CP_STAR, // * + CP_PLUS, // + + CP_COMMA, // , + CP_DOT, // . + CP_COLON, // : + CP_SEMI, // ; + CP_LT, // < + CP_EQ, // = + CP_GT, // > + CP_QUESTION, // ? + CP_AT, // @ + CP_CARET, // ^ + CP_BACKTICK, // ` + CP_TILDE, // ~ +]) export default createRule("no-useless-escape", { meta: { @@ -68,6 +120,8 @@ export default createRule("no-useless-escape", { */ function createVisitor({ node, + flags, + pattern, getRegexpLocation, fixReplaceNode, }: RegExpContext): RegExpVisitor.Handlers { @@ -89,37 +143,85 @@ export default createRule("no-useless-escape", { }) } - let inCharacterClass = false + const characterClassStack: ( + | CharacterClass + | ExpressionCharacterClass + )[] = [] return { - onCharacterClassEnter() { - inCharacterClass = true - }, - onCharacterClassLeave() { - inCharacterClass = false - }, + onCharacterClassEnter: (characterClassNode) => + characterClassStack.unshift(characterClassNode), + onCharacterClassLeave: () => characterClassStack.shift(), + onExpressionCharacterClassEnter: (characterClassNode) => + characterClassStack.unshift(characterClassNode), + onExpressionCharacterClassLeave: () => + characterClassStack.shift(), onCharacterEnter(cNode) { if (cNode.raw.startsWith("\\")) { // escapes const char = cNode.raw.slice(1) - if (char === String.fromCodePoint(cNode.value)) { - const allowedEscapes = inCharacterClass - ? REGEX_CHAR_CLASS_ESCAPES - : REGEX_ESCAPES + const escapedChar = String.fromCodePoint(cNode.value) + if (char === escapedChar) { + let allowedEscapes: Set + if (characterClassStack.length) { + allowedEscapes = flags.unicodeSets + ? REGEX_CLASS_SET_CHAR_CLASS_ESCAPE + : REGEX_CHAR_CLASS_ESCAPES + } else { + allowedEscapes = REGEX_ESCAPES + } if (allowedEscapes.has(cNode.value)) { return } - if (inCharacterClass && cNode.value === CP_CARET) { - const target = - cNode.parent.type === "CharacterClassRange" - ? cNode.parent - : cNode - const parent = target.parent - if (parent.type === "CharacterClass") { - if (parent.elements.indexOf(target) === 0) { + if (characterClassStack.length) { + const characterClassNode = + characterClassStack[0] + if (cNode.value === CP_CARET) { + if ( + characterClassNode.start + 1 === + cNode.start + ) { // e.g. /[\^]/ return } } + if (flags.unicodeSets) { + if ( + REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR.has( + cNode.value, + ) + ) { + if ( + pattern[cNode.end] === escapedChar + ) { + // Escaping is valid if it is a ClassSetReservedDoublePunctuator. + return + } + const prevIndex = cNode.start - 1 + if ( + pattern[prevIndex] === escapedChar + ) { + if (escapedChar !== "^") { + // e.g. [&\&] + // ^ // If it's the second character, it's a valid escape. + return + } + const elementStartIndex = + characterClassNode.start + + 1 + // opening bracket(`[`) + (characterClassNode.negate + ? 1 // `negate` caret(`^`) + : 0) + if ( + elementStartIndex <= prevIndex + ) { + // [^^\^], [_^\^] + // ^ ^ // If it's the second caret(`^`) character, it's a valid escape. + // But [^\^] is unnecessary escape. + return + } + } + } + } } if (!canUnwrapped(cNode, char)) { return diff --git a/lib/utils/unicode.ts b/lib/utils/unicode.ts index ee1773e9e..f396c1b9b 100644 --- a/lib/utils/unicode.ts +++ b/lib/utils/unicode.ts @@ -8,15 +8,23 @@ export const CP_FF = 12 export const CP_CR = 13 export const CP_SPACE = " ".codePointAt(0)! export const CP_BAN = "!".codePointAt(0)! +export const CP_HASH = "#".codePointAt(0) export const CP_DOLLAR = "$".codePointAt(0)! +export const CP_PERCENT = "%".codePointAt(0)! +export const CP_AMP = "&".codePointAt(0)! export const CP_OPENING_PAREN = "(".codePointAt(0)! export const CP_CLOSING_PAREN = ")".codePointAt(0)! export const CP_STAR = "*".codePointAt(0)! export const CP_PLUS = "+".codePointAt(0)! +export const CP_COMMA = ",".codePointAt(0)! export const CP_MINUS = "-".codePointAt(0)! export const CP_DOT = ".".codePointAt(0)! export const CP_SLASH = "/".codePointAt(0)! export const CP_COLON = ":".codePointAt(0)! +export const CP_SEMI = ";".codePointAt(0)! +export const CP_LT = "<".codePointAt(0)! +export const CP_EQ = "=".codePointAt(0)! +export const CP_GT = ">".codePointAt(0)! export const CP_QUESTION = "?".codePointAt(0)! export const CP_AT = "@".codePointAt(0)! export const CP_OPENING_BRACKET = "[".codePointAt(0)! diff --git a/tests/lib/rules/no-useless-escape.ts b/tests/lib/rules/no-useless-escape.ts index 8328c1b4b..b7d993e0d 100644 --- a/tests/lib/rules/no-useless-escape.ts +++ b/tests/lib/rules/no-useless-escape.ts @@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-useless-escape" const tester = new RuleTester({ parserOptions: { - ecmaVersion: 2020, + ecmaVersion: "latest", sourceType: "module", }, }) @@ -65,6 +65,60 @@ tester.run("no-useless-escape", rule as any, { // String.raw`/()\1\8/; /()\1\9/`, "/[\\^-`]/", + + // ES2024 + String.raw`/[\q{abc}]/v`, + String.raw`/[\(]/v`, + String.raw`/[\)]/v`, + String.raw`/[\{]/v`, + String.raw`/[\]]/v`, + String.raw`/[\}]/v`, + String.raw`/[\/]/v`, + String.raw`/[\-]/v`, + String.raw`/[\|]/v`, + String.raw`/[\$$]/v`, + String.raw`/[\&&]/v`, + String.raw`/[\!!]/v`, + String.raw`/[\##]/v`, + String.raw`/[\%%]/v`, + String.raw`/[\**]/v`, + String.raw`/[\++]/v`, + String.raw`/[\,,]/v`, + String.raw`/[\..]/v`, + String.raw`/[\::]/v`, + String.raw`/[\;;]/v`, + String.raw`/[\<<]/v`, + String.raw`/[\==]/v`, + String.raw`/[\>>]/v`, + String.raw`/[\??]/v`, + String.raw`/[\@@]/v`, + "/[\\``]/v", + String.raw`/[\~~]/v`, + String.raw`/[^\^^]/v`, + String.raw`/[_\^^]/v`, + String.raw`/[$\$]/v`, + String.raw`/[&\&]/v`, + String.raw`/[!\!]/v`, + String.raw`/[#\#]/v`, + String.raw`/[%\%]/v`, + String.raw`/[*\*]/v`, + String.raw`/[+\+]/v`, + String.raw`/[,\,]/v`, + String.raw`/[.\.]/v`, + String.raw`/[:\:]/v`, + String.raw`/[;\;]/v`, + String.raw`/[<\<]/v`, + String.raw`/[=\=]/v`, + String.raw`/[>\>]/v`, + String.raw`/[?\?]/v`, + String.raw`/[@\@]/v`, + "/[`\\`]/v", + String.raw`/[~\~]/v`, + String.raw`/[^^\^]/v`, + String.raw`/[_^\^]/v`, + String.raw`/[\&&&\&]/v`, + String.raw`/[[\-]\-]/v`, + String.raw`/[\^]/v`, ], invalid: [ { @@ -128,5 +182,368 @@ tester.run("no-useless-escape", rule as any, { "Unnecessary escape character: \\P.", ], }, + + // ES2024 + { + code: String.raw`/[\$]/v`, + output: String.raw`/[$]/v`, + errors: [ + { + line: 1, + column: 3, + endColumn: 4, + message: "Unnecessary escape character: \\$.", + }, + ], + }, + { + code: String.raw`/[\&\&]/v`, + output: String.raw`/[&\&]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\&.", + }, + ], + }, + { + code: String.raw`/[\!\!]/v`, + output: String.raw`/[!\!]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\!.", + }, + ], + }, + { + code: String.raw`/[\#\#]/v`, + output: String.raw`/[#\#]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\#.", + }, + ], + }, + { + code: String.raw`/[\%\%]/v`, + output: String.raw`/[%\%]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\%.", + }, + ], + }, + { + code: String.raw`/[\*\*]/v`, + output: String.raw`/[*\*]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\*.", + }, + ], + }, + { + code: String.raw`/[\+\+]/v`, + output: String.raw`/[+\+]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\+.", + }, + ], + }, + { + code: String.raw`/[\,\,]/v`, + output: String.raw`/[,\,]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\,.", + }, + ], + }, + { + code: String.raw`/[\.\.]/v`, + output: String.raw`/[.\.]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + }, + ], + }, + { + code: String.raw`/[\:\:]/v`, + output: String.raw`/[:\:]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\:.", + }, + ], + }, + { + code: String.raw`/[\;\;]/v`, + output: String.raw`/[;\;]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\;.", + }, + ], + }, + { + code: String.raw`/[\<\<]/v`, + output: String.raw`/[<\<]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\<.", + }, + ], + }, + { + code: String.raw`/[\=\=]/v`, + output: String.raw`/[=\=]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\=.", + }, + ], + }, + { + code: String.raw`/[\>\>]/v`, + output: String.raw`/[>\>]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\>.", + }, + ], + }, + { + code: String.raw`/[\?\?]/v`, + output: String.raw`/[?\?]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\?.", + }, + ], + }, + { + code: String.raw`/[\@\@]/v`, + output: String.raw`/[@\@]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\@.", + }, + ], + }, + { + code: "/[\\`\\`]/v", + output: "/[`\\`]/v", + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\`.", + }, + ], + }, + { + code: String.raw`/[\~\~]/v`, + output: String.raw`/[~\~]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\~.", + }, + ], + }, + { + code: String.raw`/[^\^\^]/v`, + output: String.raw`/[^^\^]/v`, + errors: [ + { + line: 1, + column: 4, + message: "Unnecessary escape character: \\^.", + }, + ], + }, + { + code: String.raw`/[_\^\^]/v`, + output: String.raw`/[_^\^]/v`, + errors: [ + { + line: 1, + column: 4, + message: "Unnecessary escape character: \\^.", + }, + ], + }, + { + code: String.raw`/[^\^]/v`, + output: String.raw`/[^^]/v`, + errors: [ + { + line: 1, + column: 4, + message: "Unnecessary escape character: \\^.", + }, + ], + }, + { + code: String.raw`/[\&\&&\&]/v`, + output: String.raw`/[&\&&\&]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\&.", + }, + ], + }, + { + code: String.raw`/[\p{ASCII}--\.]/v`, + output: String.raw`/[\p{ASCII}--.]/v`, + errors: [ + { + line: 1, + column: 14, + message: "Unnecessary escape character: \\..", + }, + ], + }, + { + code: String.raw`/[\p{ASCII}&&\.]/v`, + output: String.raw`/[\p{ASCII}&&.]/v`, + errors: [ + { + line: 1, + column: 14, + message: "Unnecessary escape character: \\..", + }, + ], + }, + { + code: String.raw`/[\.--[.&]]/v`, + output: String.raw`/[.--[.&]]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + }, + ], + }, + { + code: String.raw`/[\.&&[.&]]/v`, + output: String.raw`/[.&&[.&]]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + }, + ], + }, + { + code: String.raw`/[\.--\.--\.]/v`, + output: String.raw`/[.--.--.]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + }, + { + line: 1, + column: 7, + message: "Unnecessary escape character: \\..", + }, + { + line: 1, + column: 11, + message: "Unnecessary escape character: \\..", + }, + ], + }, + { + code: String.raw`/[\.&&\.&&\.]/v`, + output: String.raw`/[.&&.&&.]/v`, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + }, + { + line: 1, + column: 7, + message: "Unnecessary escape character: \\..", + }, + { + line: 1, + column: 11, + message: "Unnecessary escape character: \\..", + }, + ], + }, + { + code: String.raw`/[[\.&]--[\.&]]/v`, + output: String.raw`/[[.&]--[.&]]/v`, + errors: [ + { + line: 1, + column: 4, + message: "Unnecessary escape character: \\..", + }, + { + line: 1, + column: 11, + message: "Unnecessary escape character: \\..", + }, + ], + }, + { + code: String.raw`/[[\.&]&&[\.&]]/v`, + output: String.raw`/[[.&]&&[.&]]/v`, + errors: [ + { + line: 1, + column: 4, + message: "Unnecessary escape character: \\..", + }, + { + line: 1, + column: 11, + message: "Unnecessary escape character: \\..", + }, + ], + }, ], }) From d8d445b936e7eba4500491ee880566d9cfd5efe4 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 9 Sep 2023 14:51:30 +0900 Subject: [PATCH 2/3] Create six-squids-look.md --- .changeset/six-squids-look.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-squids-look.md diff --git a/.changeset/six-squids-look.md b/.changeset/six-squids-look.md new file mode 100644 index 000000000..cbb459911 --- /dev/null +++ b/.changeset/six-squids-look.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-regexp": minor +--- + +Add support for v flag to `regexp/no-useless-escape` rule From ff3bc580d478b37b48a8435342439955cf048458 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 9 Sep 2023 17:48:21 +0900 Subject: [PATCH 3/3] fix for string --- lib/rules/no-useless-escape.ts | 12 ++++++++++-- tests/lib/rules/no-useless-escape.ts | 5 +++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/rules/no-useless-escape.ts b/lib/rules/no-useless-escape.ts index 8a18924fe..9a301c00b 100644 --- a/lib/rules/no-useless-escape.ts +++ b/lib/rules/no-useless-escape.ts @@ -75,7 +75,11 @@ const REGEX_ESCAPES = new Set([ CP_CLOSING_PAREN, // ) ]) -const POTENTIAL_ESCAPE_SEQUENCE = new Set("uxkpPq") +const POTENTIAL_ESCAPE_SEQUENCE = new Set("uxkpP") +const POTENTIAL_ESCAPE_SEQUENCE_FOR_CHAR_CLASS = new Set([ + ...POTENTIAL_ESCAPE_SEQUENCE, + "q", +]) // A single character set of ClassSetReservedDoublePunctuator. // && !! ## $$ %% ** ++ ,, .. :: ;; << == >> ?? @@ ^^ `` ~~ are ClassSetReservedDoublePunctuator const REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR = new Set([ @@ -230,7 +234,11 @@ export default createRule("no-useless-escape", { cNode, 0, char, - !POTENTIAL_ESCAPE_SEQUENCE.has(char), + !( + characterClassStack.length + ? POTENTIAL_ESCAPE_SEQUENCE_FOR_CHAR_CLASS + : POTENTIAL_ESCAPE_SEQUENCE + ).has(char), ) } } diff --git a/tests/lib/rules/no-useless-escape.ts b/tests/lib/rules/no-useless-escape.ts index b7d993e0d..f3696829f 100644 --- a/tests/lib/rules/no-useless-escape.ts +++ b/tests/lib/rules/no-useless-escape.ts @@ -182,6 +182,11 @@ tester.run("no-useless-escape", rule as any, { "Unnecessary escape character: \\P.", ], }, + { + code: String.raw`/[\q{abc}]/;`, // Missing v flag + output: null, + errors: ["Unnecessary escape character: \\q."], + }, // ES2024 {