diff --git a/lib/rules/prefer-named-capture-group.js b/lib/rules/prefer-named-capture-group.js index 1a13ffa8582..66259fc7bed 100644 --- a/lib/rules/prefer-named-capture-group.js +++ b/lib/rules/prefer-named-capture-group.js @@ -23,6 +23,61 @@ const regexpp = require("regexpp"); const parser = new regexpp.RegExpParser(); +/** + * Creates fixer suggestions for the regex, if statically determinable. + * @param {number} groupStart Starting index of the regex group. + * @param {string} pattern The regular expression pattern to be checked. + * @param {string} rawText Source text of the regexNode. + * @param {ASTNode} regexNode AST node which contains the regular expression. + * @returns {Array} Fixer suggestions for the regex, if statically determinable. + */ +function suggestIfPossible(groupStart, pattern, rawText, regexNode) { + switch (regexNode.type) { + case "Literal": + if (typeof regexNode.value === "string" && rawText.includes("\\")) { + return null; + } + break; + case "TemplateLiteral": + if (regexNode.expressions.length || rawText.slice(1, -1) !== pattern) { + return null; + } + break; + default: + return null; + } + + const start = regexNode.range[0] + groupStart + 2; + + return [ + { + fix(fixer) { + const existingTemps = pattern.match(/temp\d+/gu) || []; + const highestTempCount = existingTemps.reduce( + (previous, next) => + Math.max(previous, Number(next.slice("temp".length))), + 0 + ); + + return fixer.insertTextBeforeRange( + [start, start], + `?` + ); + }, + messageId: "addGroupName" + }, + { + fix(fixer) { + return fixer.insertTextBeforeRange( + [start, start], + "?:" + ); + }, + messageId: "addNonCapture" + } + ]; +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -38,23 +93,29 @@ module.exports = { url: "https://eslint.org/docs/rules/prefer-named-capture-group" }, + hasSuggestions: true, + schema: [], messages: { + addGroupName: "Add name to capture group.", + addNonCapture: "Convert group to non-capturing.", required: "Capture group '{{group}}' should be converted to a named or non-capturing group." } }, create(context) { + const sourceCode = context.getSourceCode(); /** * Function to check regular expression. - * @param {string} pattern The regular expression pattern to be check. - * @param {ASTNode} node AST node which contains regular expression. + * @param {string} pattern The regular expression pattern to be checked. + * @param {ASTNode} node AST node which contains the regular expression or a call/new expression. + * @param {ASTNode} regexNode AST node which contains the regular expression. * @param {boolean} uFlag Flag indicates whether unicode mode is enabled or not. * @returns {void} */ - function checkRegex(pattern, node, uFlag) { + function checkRegex(pattern, node, regexNode, uFlag) { let ast; try { @@ -68,12 +129,16 @@ module.exports = { regexpp.visitRegExpAST(ast, { onCapturingGroupEnter(group) { if (!group.name) { + const rawText = sourceCode.getText(regexNode); + const suggest = suggestIfPossible(group.start, pattern, rawText, regexNode); + context.report({ node, messageId: "required", data: { group: group.raw - } + }, + suggest }); } } @@ -83,7 +148,7 @@ module.exports = { return { Literal(node) { if (node.regex) { - checkRegex(node.regex.pattern, node, node.regex.flags.includes("u")); + checkRegex(node.regex.pattern, node, node, node.regex.flags.includes("u")); } }, Program() { @@ -101,7 +166,7 @@ module.exports = { const flags = getStringIfConstant(node.arguments[1]); if (regex) { - checkRegex(regex, node, flags && flags.includes("u")); + checkRegex(regex, node, node.arguments[0], flags && flags.includes("u")); } } } diff --git a/tests/lib/rules/prefer-named-capture-group.js b/tests/lib/rules/prefer-named-capture-group.js index 0faf1d6be65..dad3d7c0290 100644 --- a/tests/lib/rules/prefer-named-capture-group.js +++ b/tests/lib/rules/prefer-named-capture-group.js @@ -82,7 +82,17 @@ ruleTester.run("prefer-named-capture-group", rule, { data: { group: "([0-9]{4})" }, line: 1, column: 1, - endColumn: 13 + endColumn: 13, + suggestions: [ + { + messageId: "addGroupName", + output: "/(?[0-9]{4})/" + }, + { + messageId: "addNonCapture", + output: "/(?:[0-9]{4})/" + } + ] }] }, { @@ -93,7 +103,17 @@ ruleTester.run("prefer-named-capture-group", rule, { data: { group: "([0-9]{4})" }, line: 1, column: 1, - endColumn: 25 + endColumn: 25, + suggestions: [ + { + messageId: "addGroupName", + output: "new RegExp('(?[0-9]{4})')" + }, + { + messageId: "addNonCapture", + output: "new RegExp('(?:[0-9]{4})')" + } + ] }] }, { @@ -104,7 +124,17 @@ ruleTester.run("prefer-named-capture-group", rule, { data: { group: "([0-9]{4})" }, line: 1, column: 1, - endColumn: 21 + endColumn: 21, + suggestions: [ + { + messageId: "addGroupName", + output: "RegExp('(?[0-9]{4})')" + }, + { + messageId: "addNonCapture", + output: "RegExp('(?:[0-9]{4})')" + } + ] }] }, { @@ -112,7 +142,44 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "NewExpression", - data: { group: "(bc)" } + data: { group: "(bc)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "new RegExp(`a(?bc)d`)" + }, + { + messageId: "addNonCapture", + output: "new RegExp(`a(?:bc)d`)" + } + ] + }] + }, + { + code: "new RegExp('\u1234\u5678(?:a)(b)');", + errors: [{ + messageId: "required", + type: "NewExpression", + data: { group: "(b)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "new RegExp('\u1234\u5678(?:a)(?b)');" + }, + { + messageId: "addNonCapture", + output: "new RegExp('\u1234\u5678(?:a)(?:b)');" + } + ] + }] + }, + { + code: "new RegExp('\\u1234\\u5678(?:a)(b)');", + errors: [{ + messageId: "required", + type: "NewExpression", + data: { group: "(b)" }, + suggestions: null }] }, { @@ -124,7 +191,17 @@ ruleTester.run("prefer-named-capture-group", rule, { data: { group: "([0-9]{4})" }, line: 1, column: 1, - endColumn: 21 + endColumn: 21, + suggestions: [ + { + messageId: "addGroupName", + output: "/(?[0-9]{4})-(\\w{5})/" + }, + { + messageId: "addNonCapture", + output: "/(?:[0-9]{4})-(\\w{5})/" + } + ] }, { messageId: "required", @@ -132,7 +209,173 @@ ruleTester.run("prefer-named-capture-group", rule, { data: { group: "(\\w{5})" }, line: 1, column: 1, - endColumn: 21 + endColumn: 21, + suggestions: [ + { + messageId: "addGroupName", + output: "/([0-9]{4})-(?\\w{5})/" + }, + { + messageId: "addNonCapture", + output: "/([0-9]{4})-(?:\\w{5})/" + } + ] + } + ] + }, + { + code: "/([0-9]{4})-(5)/", + errors: [ + { + messageId: "required", + type: "Literal", + data: { group: "([0-9]{4})" }, + line: 1, + column: 1, + endColumn: 17, + suggestions: [ + { + messageId: "addGroupName", + output: "/(?[0-9]{4})-(5)/" + }, + { + messageId: "addNonCapture", + output: "/(?:[0-9]{4})-(5)/" + } + ] + }, + { + messageId: "required", + type: "Literal", + data: { group: "(5)" }, + line: 1, + column: 1, + endColumn: 17, + suggestions: [ + { + messageId: "addGroupName", + output: "/([0-9]{4})-(?5)/" + }, + { + messageId: "addNonCapture", + output: "/([0-9]{4})-(?:5)/" + } + ] + } + ] + }, + { + code: "/(?(a))/", + errors: [ + { + messageId: "required", + type: "Literal", + data: { group: "(a)" }, + line: 1, + column: 1, + endColumn: 16, + suggestions: [ + { + messageId: "addGroupName", + output: "/(?(?a))/" + }, + { + messageId: "addNonCapture", + output: "/(?(?:a))/" + } + ] + } + ] + }, + { + code: "/(?(a)(?b))/", + errors: [ + { + messageId: "required", + type: "Literal", + data: { group: "(a)" }, + line: 1, + column: 1, + endColumn: 27, + suggestions: [ + { + messageId: "addGroupName", + output: "/(?(?a)(?b))/" + }, + { + messageId: "addNonCapture", + output: "/(?(?:a)(?b))/" + } + ] + } + ] + }, + { + code: "/(?[0-9]{4})-(\\w{5})/", + errors: [ + { + messageId: "required", + type: "Literal", + data: { group: "(\\w{5})" }, + line: 1, + column: 1, + endColumn: 29, + suggestions: [ + { + messageId: "addGroupName", + output: "/(?[0-9]{4})-(?\\w{5})/" + }, + { + messageId: "addNonCapture", + output: "/(?[0-9]{4})-(?:\\w{5})/" + } + ] + } + ] + }, + { + code: "/(?[0-9]{4})-(5)/", + errors: [ + { + messageId: "required", + type: "Literal", + data: { group: "(5)" }, + line: 1, + column: 1, + endColumn: 25, + suggestions: [ + { + messageId: "addGroupName", + output: "/(?[0-9]{4})-(?5)/" + }, + { + messageId: "addNonCapture", + output: "/(?[0-9]{4})-(?:5)/" + } + ] + } + ] + }, + { + code: "/(?a)(?a)(a)(?a)/", + errors: [ + { + messageId: "required", + type: "Literal", + data: { group: "(a)" }, + line: 1, + column: 1, + endColumn: 39, + suggestions: [ + { + messageId: "addGroupName", + output: "/(?a)(?a)(?a)(?a)/" + }, + { + messageId: "addNonCapture", + output: "/(?a)(?a)(?:a)(?a)/" + } + ] } ] }, @@ -141,7 +384,8 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "NewExpression", - data: { group: "(a)" } + data: { group: "(a)" }, + suggestions: null }] }, { @@ -149,7 +393,34 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "NewExpression", - data: { group: "(bc)" } + data: { group: "(bc)" }, + suggestions: null + }] + }, + { + code: "new RegExp(\"foo\" + \"(a)\" + \"(b)\");", + errors: [ + { + messageId: "required", + type: "NewExpression", + data: { group: "(a)" }, + suggestions: null + }, + { + messageId: "required", + type: "NewExpression", + data: { group: "(b)" }, + suggestions: null + } + ] + }, + { + code: "new RegExp(\"foo\" + \"(?:a)\" + \"(b)\");", + errors: [{ + messageId: "required", + type: "NewExpression", + data: { group: "(b)" }, + suggestions: null }] }, { @@ -157,7 +428,8 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "CallExpression", - data: { group: "(a)" } + data: { group: "(a)" }, + suggestions: null }] }, { @@ -165,7 +437,8 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "CallExpression", - data: { group: "(ab)" } + data: { group: "(ab)" }, + suggestions: null }] }, { @@ -173,7 +446,8 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "NewExpression", - data: { group: "(ab)" } + data: { group: "(ab)" }, + suggestions: null }] }, { @@ -185,7 +459,17 @@ ruleTester.run("prefer-named-capture-group", rule, { line: 1, column: 1, endLine: 2, - endColumn: 3 + endColumn: 3, + suggestions: [ + { + messageId: "addGroupName", + output: "new RegExp(`(?a)\n`)" + }, + { + messageId: "addNonCapture", + output: "new RegExp(`(?:a)\n`)" + } + ] }] }, { @@ -193,7 +477,17 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "CallExpression", - data: { group: "(b\nc)" } + data: { group: "(b\nc)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "RegExp(`a(?b\nc)d`)" + }, + { + messageId: "addNonCapture", + output: "RegExp(`a(?:b\nc)d`)" + } + ] }] }, { @@ -201,7 +495,8 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "NewExpression", - data: { group: "(b)" } + data: { group: "(b)" }, + suggestions: null }] }, { @@ -209,7 +504,8 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "CallExpression", - data: { group: "(a)" } + data: { group: "(a)" }, + suggestions: null }] }, { @@ -217,7 +513,8 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "CallExpression", - data: { group: "(b)" } + data: { group: "(b)" }, + suggestions: null }] }, { @@ -229,7 +526,17 @@ ruleTester.run("prefer-named-capture-group", rule, { data: { group: "([0-9]{4})" }, line: 1, column: 1, - endColumn: 36 + endColumn: 36, + suggestions: [ + { + messageId: "addGroupName", + output: "new globalThis.RegExp('(?[0-9]{4})')" + }, + { + messageId: "addNonCapture", + output: "new globalThis.RegExp('(?:[0-9]{4})')" + } + ] }] }, { @@ -241,7 +548,17 @@ ruleTester.run("prefer-named-capture-group", rule, { data: { group: "([0-9]{4})" }, line: 1, column: 1, - endColumn: 32 + endColumn: 32, + suggestions: [ + { + messageId: "addGroupName", + output: "globalThis.RegExp('(?[0-9]{4})')" + }, + { + messageId: "addNonCapture", + output: "globalThis.RegExp('(?:[0-9]{4})')" + } + ] }] }, { @@ -256,7 +573,23 @@ ruleTester.run("prefer-named-capture-group", rule, { data: { group: "([0-9]{4})" }, line: 3, column: 17, - endColumn: 52 + endColumn: 52, + suggestions: [ + { + messageId: "addGroupName", + output: ` + function foo() { var globalThis = bar; } + new globalThis.RegExp('(?[0-9]{4})'); + ` + }, + { + messageId: "addNonCapture", + output: ` + function foo() { var globalThis = bar; } + new globalThis.RegExp('(?:[0-9]{4})'); + ` + } + ] }] } ]