Skip to content

Commit

Permalink
New: add rule "require-named-capture-group" (fixes #11381)
Browse files Browse the repository at this point in the history
  • Loading branch information
g-plane committed Feb 14, 2019
1 parent ad7a380 commit 49d535e
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 0 deletions.
43 changes: 43 additions & 0 deletions 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 = /(?<year>[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 = /(?<id>ba[rz])/;
const bar = new RegExp('(?<id>ba[rz])');
const baz = RegExp('(?<id>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)
87 changes: 87 additions & 0 deletions 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 <https://github.com/g-plane>
*/

"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);
}
}
}
}
}
57 changes: 57 additions & 0 deletions tests/lib/rules/require-named-capture-group.js
@@ -0,0 +1,57 @@
/**
* @fileoverview Tests for require-named-capture-group rule.
* @author Pig Fang <https://github.com/g-plane>
*/

"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})/",
"/(?<year>[0-9]{4})/",
"new RegExp()",
"new RegExp(foo)",
"new RegExp('')",
"new RegExp('(?<year>[0-9]{4})')",
"RegExp()",
"RegExp(foo)",
"RegExp('')",
"RegExp('(?<year>[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})' } },
]
}
]
});

0 comments on commit 49d535e

Please sign in to comment.