diff --git a/docs/rules/require-named-capture-group.md b/docs/rules/require-named-capture-group.md new file mode 100644 index 00000000000..571825e3c1e --- /dev/null +++ b/docs/rules/require-named-capture-group.md @@ -0,0 +1,43 @@ +# Suggest using named capture group in regular expression (require-named-capture-group) + +With the landing of ECMAScript 2018, named capture group can be used in regular expression, which can improve the readability. + +```js +const regex = /(?[0-9]{4})/; +``` + +## Rule Details + +This rule is aimed at using named capture group instead of numbered capture group in regular expression. + +Examples of **incorrect** code for this rule: + +```js +/*eslint require-named-capture-group: "error"*/ + +const foo = /(ba[rz])/; +const bar = new RegExp('(ba[rz])'); +const baz = RegExp('(ba[rz])'); + +foo.exec('bar')[1]; // Retrieve the group result. +``` + +Examples of **correct** code for this rule: + +```js +/*eslint require-named-capture-group: "error"*/ + +const foo = /(?ba[rz])/; +const bar = new RegExp('(?ba[rz])'); +const baz = RegExp('(?ba[rz])'); + +foo.exec('bar').groups.id; // Retrieve the group result. +``` + +## When Not To Use It + +If you are targeting ECMAScript 2017 and/or older environments, you can disable this rule, because this ECMAScript feature is only supported in ECMAScript 2018 and/or newer environments. + +## Related Rules + +* [no-invalid-regexp](./no-invalid-regexp.md) diff --git a/lib/rules/require-named-capture-group.js b/lib/rules/require-named-capture-group.js new file mode 100644 index 00000000000..3e3d07227d8 --- /dev/null +++ b/lib/rules/require-named-capture-group.js @@ -0,0 +1,87 @@ +/** + * @fileoverview Rule to enforce requiring named capture groups in regular expression. + * @author Pig Fang + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const regexpp = require("regexpp"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const parser = new regexpp.RegExpParser(); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "suggestion", + + docs: { + description: "enforce using named capture group in regular expression", + category: "Best Practices", + recommended: false, + url: "https://eslint.org/docs/rules/require-named-capture-group" + }, + + schema: [], + + messages: { + required: "Capture group '{{group}}' in regular expression should be named." + } + }, + + create(context) { + /** + * Function to check regular expression. + * + * @param {string} regex The regular expression to be check. + * @param {ASTNode} node AST node which contains regular expression. + * @returns {void} + */ + function checkRegex(regex, node) { + const ast = parser.parsePattern(regex); + regexpp.visitRegExpAST(ast, { + onCapturingGroupEnter(group) { + if (!group.name) { + context.report({ + node, + messageId: "required", + data: { + group: group.raw + } + }); + } + } + }); + } + + return { + Literal(node) { + if (node.regex) { + checkRegex(node.regex.pattern, node); + } + }, + "NewExpression, CallExpression"(node) { + const { callee, arguments: [firstArg] } = node; + if ( + callee.type === "Identifier" && + callee.name === "RegExp" && + firstArg && + firstArg.type === "Literal" && + typeof firstArg.value === "string" + ) { + checkRegex(firstArg.value, node); + } + } + } + } +} diff --git a/tests/lib/rules/require-named-capture-group.js b/tests/lib/rules/require-named-capture-group.js new file mode 100644 index 00000000000..7dc91918b4c --- /dev/null +++ b/tests/lib/rules/require-named-capture-group.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Tests for require-named-capture-group rule. + * @author Pig Fang + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/require-named-capture-group"), + RuleTester = require("../../../lib/testers/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); + +ruleTester.run("require-named-capture-group", rule, { + valid: [ + "/normal_regex/", + "/(?:[0-9]{4})/", + "/(?[0-9]{4})/", + "new RegExp()", + "new RegExp(foo)", + "new RegExp('')", + "new RegExp('(?[0-9]{4})')", + "RegExp()", + "RegExp(foo)", + "RegExp('')", + "RegExp('(?[0-9]{4})')" + ], + + invalid: [ + { + code: "/([0-9]{4})/", + errors: [{ messageId: "required", type: "Literal", data: { group: '([0-9]{4})' } }] + }, + { + code: "new RegExp('([0-9]{4})')", + errors: [{ messageId: "required", type: "NewExpression", data: { group: '([0-9]{4})' } }] + }, + { + code: "RegExp('([0-9]{4})')", + errors: [{ messageId: "required", type: "CallExpression", data: { group: '([0-9]{4})' } }] + }, + { + code: "/([0-9]{4})-(\\w{5})/", + errors: [ + { messageId: "required", type: "Literal", data: { group: '([0-9]{4})' } }, + { messageId: "required", type: "Literal", data: { group: '(\\w{5})' } }, + ] + } + ] +});