diff --git a/lib/rules/prefer-named-capture-group.js b/lib/rules/prefer-named-capture-group.js index cff2d8f45d27..c5e662acfa9b 100644 --- a/lib/rules/prefer-named-capture-group.js +++ b/lib/rules/prefer-named-capture-group.js @@ -38,23 +38,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 {ASTNode} node AST node which contains regular expression or a call/new expression. + * @param {ASTNode} regexNode AST node which contains 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 +74,35 @@ module.exports = { regexpp.visitRegExpAST(ast, { onCapturingGroupEnter(group) { if (!group.name) { + const rawText = sourceCode.getText(regexNode); + const start = regexNode.range[0] + rawText.indexOf("(", group.start) + 1; + context.report({ node, messageId: "required", data: { group: group.raw - } + }, + suggest: [ + { + fix(fixer) { + return fixer.insertTextBeforeRange( + [start, start], + `?` + ); + }, + messageId: "addGroupName" + }, + { + fix(fixer) { + return fixer.insertTextBeforeRange( + [start, start], + "?:" + ); + }, + messageId: "addNonCapture" + } + ] }); } } @@ -83,7 +112,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 +130,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 0faf1d6be65e..0fdbc89354e7 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,17 @@ 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`)" + } + ] }] }, { @@ -124,7 +164,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 +182,63 @@ 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})-(\\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: "/(?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 +247,17 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "NewExpression", - data: { group: "(a)" } + data: { group: "(a)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "new RegExp('(?' + 'a)')" + }, + { + messageId: "addNonCapture", + output: "new RegExp('(?:' + 'a)')" + } + ] }] }, { @@ -149,7 +265,17 @@ 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' + 'e')" + }, + { + messageId: "addNonCapture", + output: "new RegExp('a(?:bc)d' + 'e')" + } + ] }] }, { @@ -157,7 +283,17 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "CallExpression", - data: { group: "(a)" } + data: { group: "(a)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "RegExp('(?a)'+'')" + }, + { + messageId: "addNonCapture", + output: "RegExp('(?:a)'+'')" + } + ] }] }, { @@ -165,7 +301,17 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "CallExpression", - data: { group: "(ab)" } + data: { group: "(ab)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "RegExp( '' + '(?ab)')" + }, + { + messageId: "addNonCapture", + output: "RegExp( '' + '(?:ab)')" + } + ] }] }, { @@ -173,7 +319,17 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "NewExpression", - data: { group: "(ab)" } + data: { group: "(ab)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "new RegExp(`(?ab)${''}`)" + }, + { + messageId: "addNonCapture", + output: "new RegExp(`(?:ab)${''}`)" + } + ] }] }, { @@ -185,7 +341,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 +359,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 +377,17 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "NewExpression", - data: { group: "(b)" } + data: { group: "(b)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "new RegExp('a(?b)\\'')" + }, + { + messageId: "addNonCapture", + output: "new RegExp('a(?:b)\\'')" + } + ] }] }, { @@ -209,7 +395,17 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "CallExpression", - data: { group: "(a)" } + data: { group: "(a)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "RegExp('(?a)\\\\d')" + }, + { + messageId: "addNonCapture", + output: "RegExp('(?:a)\\\\d')" + } + ] }] }, { @@ -217,7 +413,17 @@ ruleTester.run("prefer-named-capture-group", rule, { errors: [{ messageId: "required", type: "CallExpression", - data: { group: "(b)" } + data: { group: "(b)" }, + suggestions: [ + { + messageId: "addGroupName", + output: "RegExp(`\\a(?b)`)" + }, + { + messageId: "addNonCapture", + output: "RegExp(`\\a(?:b)`)" + } + ] }] }, { @@ -229,7 +435,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 +457,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 +482,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})'); + ` + } + ] }] } ]