diff --git a/.changeset/rare-spiders-drop.md b/.changeset/rare-spiders-drop.md new file mode 100644 index 000000000..1abe3529e --- /dev/null +++ b/.changeset/rare-spiders-drop.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-regexp": minor +--- + +Add suggestions for `regexp/no-lazy-ends` diff --git a/README.md b/README.md index 561ec723e..7fccedf32 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo | [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 | ✅ | | | | | [no-escape-backspace](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-escape-backspace.html) | disallow escape backspace (`[\b]`) | ✅ | | | | | [no-invalid-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invalid-regexp.html) | disallow invalid regular expression strings in `RegExp` constructors | ✅ | | | | -| [no-lazy-ends](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-lazy-ends.html) | disallow lazy quantifiers at the end of an expression | | ✅ | | | +| [no-lazy-ends](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-lazy-ends.html) | disallow lazy quantifiers at the end of an expression | | ✅ | | 💡 | | [no-misleading-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-misleading-capturing-group.html) | disallow capturing groups that do not behave as one would expect | ✅ | | | 💡 | | [no-misleading-unicode-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-misleading-unicode-character.html) | disallow multi-code-point characters in character classes and quantifiers | ✅ | | 🔧 | 💡 | | [no-missing-g-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-missing-g-flag.html) | disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll` | ✅ | | 🔧 | | diff --git a/docs/rules/index.md b/docs/rules/index.md index 31e251aac..4e2b1ede2 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -26,7 +26,7 @@ sidebarDepth: 0 | [no-empty-lookarounds-assertion](no-empty-lookarounds-assertion.md) | disallow empty lookahead assertion or empty lookbehind assertion | ✅ | | | | | [no-escape-backspace](no-escape-backspace.md) | disallow escape backspace (`[\b]`) | ✅ | | | | | [no-invalid-regexp](no-invalid-regexp.md) | disallow invalid regular expression strings in `RegExp` constructors | ✅ | | | | -| [no-lazy-ends](no-lazy-ends.md) | disallow lazy quantifiers at the end of an expression | | ✅ | | | +| [no-lazy-ends](no-lazy-ends.md) | disallow lazy quantifiers at the end of an expression | | ✅ | | 💡 | | [no-misleading-capturing-group](no-misleading-capturing-group.md) | disallow capturing groups that do not behave as one would expect | ✅ | | | 💡 | | [no-misleading-unicode-character](no-misleading-unicode-character.md) | disallow multi-code-point characters in character classes and quantifiers | ✅ | | 🔧 | 💡 | | [no-missing-g-flag](no-missing-g-flag.md) | disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll` | ✅ | | 🔧 | | diff --git a/docs/rules/no-lazy-ends.md b/docs/rules/no-lazy-ends.md index 993a3542c..7cd630bdf 100644 --- a/docs/rules/no-lazy-ends.md +++ b/docs/rules/no-lazy-ends.md @@ -9,6 +9,8 @@ since: "v0.8.0" ⚠️ This rule _warns_ in the ✅ `plugin:regexp/recommended` config. +💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). + > disallow lazy quantifiers at the end of an expression diff --git a/lib/rules/no-lazy-ends.ts b/lib/rules/no-lazy-ends.ts index 2145ec292..7f3cb60ff 100644 --- a/lib/rules/no-lazy-ends.ts +++ b/lib/rules/no-lazy-ends.ts @@ -1,6 +1,7 @@ import type { RegExpVisitor } from "@eslint-community/regexpp/visitor" import type { Alternative, Quantifier } from "@eslint-community/regexpp/ast" import type { RegExpContext } from "../utils" +import type { Rule } from "eslint" import { createRule, defineRegexpVisitor } from "../utils" import { UsageOfPattern } from "../utils/get-usage-of-pattern" @@ -57,6 +58,7 @@ export default createRule("no-lazy-ends", { additionalProperties: false, }, ], + hasSuggestions: true, messages: { uselessElement: "The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.", @@ -64,6 +66,15 @@ export default createRule("no-lazy-ends", { "The quantifier can be removed because the quantifier is lazy and has a minimum of 1.", uselessRange: "The quantifier can be replaced with '{{{min}}}' because the quantifier is lazy and has a minimum of {{min}}.", + + suggestMakeGreedy: + "Make the quantifier greedy. (This changes the behavior of the regex.)", + suggestRemoveElement: + "Remove the quantified element. (This does not changes the behavior of the regex.)", + suggestRemoveQuantifier: + "Remove the quantifier. (This does not changes the behavior of the regex.)", + suggestRange: + "Replace the quantifier with '{{{min}}}'. (This does not changes the behavior of the regex.)", }, type: "problem", }, @@ -74,6 +85,7 @@ export default createRule("no-lazy-ends", { node, getRegexpLocation, getUsageOfPattern, + fixReplaceNode, }: RegExpContext): RegExpVisitor.Handlers { if (ignorePartial) { const usageOfPattern = getUsageOfPattern() @@ -88,17 +100,48 @@ export default createRule("no-lazy-ends", { for (const lazy of extractLazyEndQuantifiers( pNode.alternatives, )) { + const makeGreedy: Rule.SuggestionReportDescriptor = { + messageId: "suggestMakeGreedy", + fix: fixReplaceNode(lazy, lazy.raw.slice(0, -1)), + } + if (lazy.min === 0) { + // we have to replace the quantifier with (?:) to + // avoid creating invalid patterns. E.g. /a??/ -> /(?:)/ + const replacement = + pNode.alternatives.length === 1 && + pNode.alternatives[0].elements.length === 1 && + pNode.alternatives[0].elements[0] === lazy + ? "(?:)" + : "" + context.report({ node, loc: getRegexpLocation(lazy), messageId: "uselessElement", + suggest: [ + { + messageId: "suggestRemoveElement", + fix: fixReplaceNode(lazy, replacement), + }, + makeGreedy, + ], }) } else if (lazy.min === 1) { context.report({ node, loc: getRegexpLocation(lazy), messageId: "uselessQuantifier", + suggest: [ + { + messageId: "suggestRemoveQuantifier", + fix: fixReplaceNode( + lazy, + lazy.element.raw, + ), + }, + makeGreedy, + ], }) } else { context.report({ @@ -108,6 +151,19 @@ export default createRule("no-lazy-ends", { data: { min: String(lazy.min), }, + suggest: [ + { + messageId: "suggestRange", + data: { + min: String(lazy.min), + }, + fix: fixReplaceNode( + lazy, + `${lazy.element.raw}{${lazy.min}}`, + ), + }, + makeGreedy, + ], }) } } diff --git a/tests/lib/rules/no-lazy-ends.ts b/tests/lib/rules/no-lazy-ends.ts index 3a6eadbb7..2c5f52811 100644 --- a/tests/lib/rules/no-lazy-ends.ts +++ b/tests/lib/rules/no-lazy-ends.ts @@ -38,6 +38,10 @@ tester.run("no-lazy-ends", rule as any, { "The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.", line: 1, column: 2, + suggestions: [ + { output: "/(?:)/.test(str)" }, + { output: "/a?/.test(str)" }, + ], }, ], }, @@ -49,6 +53,10 @@ tester.run("no-lazy-ends", rule as any, { "The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.", line: 1, column: 2, + suggestions: [ + { output: "/(?:)/.test(str)" }, + { output: "/a*/.test(str)" }, + ], }, ], }, @@ -60,6 +68,10 @@ tester.run("no-lazy-ends", rule as any, { "The quantifier can be removed because the quantifier is lazy and has a minimum of 1.", line: 1, column: 2, + suggestions: [ + { output: "/a/.test(str)" }, + { output: "/a+/.test(str)" }, + ], }, ], }, @@ -71,6 +83,10 @@ tester.run("no-lazy-ends", rule as any, { "The quantifier can be replaced with '{3}' because the quantifier is lazy and has a minimum of 3.", line: 1, column: 2, + suggestions: [ + { output: "/a{3}/.test(str)" }, + { output: "/a{3,7}/.test(str)" }, + ], }, ], }, @@ -82,6 +98,10 @@ tester.run("no-lazy-ends", rule as any, { "The quantifier can be replaced with '{3}' because the quantifier is lazy and has a minimum of 3.", line: 1, column: 2, + suggestions: [ + { output: "/a{3}/.test(str)" }, + { output: "/a{3,}/.test(str)" }, + ], }, ], }, @@ -94,6 +114,10 @@ tester.run("no-lazy-ends", rule as any, { "The quantifier can be removed because the quantifier is lazy and has a minimum of 1.", line: 1, column: 9, + suggestions: [ + { output: `/(?:a|b(c))/.test(str)` }, + { output: `/(?:a|b(c+))/.test(str)` }, + ], }, ], }, @@ -105,6 +129,10 @@ tester.run("no-lazy-ends", rule as any, { "The quantifier can be removed because the quantifier is lazy and has a minimum of 1.", line: 1, column: 9, + suggestions: [ + { output: `/a(?:c|ab)?/.test(str)` }, + { output: `/a(?:c|ab+)?/.test(str)` }, + ], }, ], }, diff --git a/tests/lib/utils/rules.ts b/tests/lib/utils/rules.ts index ccef5064c..b88da1afa 100644 --- a/tests/lib/utils/rules.ts +++ b/tests/lib/utils/rules.ts @@ -63,9 +63,7 @@ describe("Check if the strict of all rules is correct", () => { it(messageId, () => { const message = rule.meta.messages[messageId] assert.ok( - message.endsWith(".") || - message.endsWith("?") || - message.endsWith("}}"), + /(?:[.?]|\}\})\)?$/u.test(message), "Doesn't end with a dot.", ) })