Skip to content

Commit 556ba2e

Browse files
authored
Add option to allow only partial use to the allowTop option of regexp/no-useless-non-capturing-group rule. (#215)
1 parent e2c338f commit 556ba2e

File tree

3 files changed

+234
-79
lines changed

3 files changed

+234
-79
lines changed

docs/rules/no-useless-non-capturing-group.md

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ This rule reports unnecessary non-capturing group
2121
/* eslint regexp/no-useless-non-capturing-group: "error" */
2222

2323
/* ✓ GOOD */
24-
var foo = /(?:abcd)?/
25-
var foo = /a(?:ab|cd)/
24+
var foo = /(?:abcd)?/.test(str)
25+
var foo = /a(?:ab|cd)/.test(str)
2626

2727
/* ✗ BAD */
28-
var foo = /(?:ab|cd)/
29-
var foo = /(?:abcd)/
30-
var foo = /(?:[a-d])/
31-
var foo = /(?:[a-d])|e/
32-
var foo = /(?:a|(?:b|c)|d)/
28+
var foo = /(?:ab|cd)/.test(str)
29+
var foo = /(?:abcd)/.test(str)
30+
var foo = /(?:[a-d])/.test(str)
31+
var foo = /(?:[a-d])|e/.test(str)
32+
var foo = /(?:a|(?:b|c)|d)/.test(str)
3333
```
3434

3535
</eslint-code-block>
@@ -39,33 +39,78 @@ var foo = /(?:a|(?:b|c)|d)/
3939
```json5
4040
{
4141
"regexp/no-useless-non-capturing-group": ["error", {
42-
"allowTop": true
42+
"allowTop": "partial" // or "always" or "never"
4343
}]
4444
}
4545
```
4646

4747
- `"allowTop"`:
48-
Whether a top-level non-capturing group is allowed. Defaults to `false`.
48+
Whether a top-level non-capturing group is allowed. Defaults to `"partial"`.
4949

5050
Sometimes it's useful to wrap a whole pattern into a non-capturing group (e.g. when the pattern is used as a building block to construct more complex patterns). Use this option to allow top-level non-capturing groups.
51+
- `"partial"`:
52+
Allows top-level non-capturing groups of patterns used as strings via `.source`.
5153

52-
<eslint-code-block fix>
54+
<eslint-code-block fix>
5355

54-
```js
55-
/* eslint regexp/no-useless-non-capturing-group: ["error", { allowTop: true }] */
56+
```js
57+
/* eslint regexp/no-useless-non-capturing-group: ["error", { allowTop: "partial" }] */
5658

57-
/* ✓ GOOD */
58-
var foo = /(?:abcd)/
59-
var foo = /(?:ab|cd)/
60-
var foo = /(?:abcd)/
61-
var foo = /(?:[a-d])/
59+
/* ✓ GOOD */
60+
var foo = /(?:ab|cd)/;
61+
var bar = /(?:ab|cd)/; // We still don't know how it will be used.
6262

63-
/* ✗ BAD */
64-
var foo = /(?:[a-d])|e/
65-
var foo = /(?:a|(?:b|c)|d)/
66-
```
63+
/* ✗ BAD */
64+
/(?:ab|cd)/.test(str);
6765

68-
</eslint-code-block>
66+
/*-------*/
67+
var baz = new RexExp(foo.source + 'e');
68+
baz.test(str);
69+
```
70+
71+
</eslint-code-block>
72+
73+
- `"always"`:
74+
Always allow top-level non-capturing groups.
75+
76+
<eslint-code-block fix>
77+
78+
```js
79+
/* eslint regexp/no-useless-non-capturing-group: ["error", { allowTop: "always" }] */
80+
81+
/* ✓ GOOD */
82+
var foo = /(?:abcd)/.test(str)
83+
var foo = /(?:ab|cd)/.test(str)
84+
var foo = /(?:abcd)/.test(str)
85+
var foo = /(?:[a-d])/.test(str)
86+
87+
/* ✗ BAD */
88+
var foo = /(?:[a-d])|e/.test(str)
89+
var foo = /(?:a|(?:b|c)|d)/.test(str)
90+
```
91+
92+
</eslint-code-block>
93+
94+
- `"never"`:
95+
Never allow top-level non-capturing groups.
96+
97+
<eslint-code-block fix>
98+
99+
```js
100+
/* eslint regexp/no-useless-non-capturing-group: ["error", { allowTop: "never" }] */
101+
102+
/* ✗ BAD */
103+
var foo = /(?:ab|cd)/;
104+
var bar = /(?:ab|cd)/;
105+
106+
/(?:ab|cd)/.test(str);
107+
108+
/*-------*/
109+
var baz = new RexExp(foo.source + 'e');
110+
baz.test(str);
111+
```
112+
113+
</eslint-code-block>
69114

70115
## :rocket: Version
71116

lib/rules/no-useless-non-capturing-group.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Group } from "regexpp/ast"
22
import type { RegExpVisitor } from "regexpp/visitor"
33
import type { RegExpContext } from "../utils"
44
import { canUnwrapped, createRule, defineRegexpVisitor } from "../utils"
5+
import { UsageOfPattern } from "../utils/get-usage-of-pattern"
56

67
/**
78
* Returns whether the given group is the top-level group of its pattern.
@@ -36,7 +37,15 @@ export default createRule("no-useless-non-capturing-group", {
3637
{
3738
type: "object",
3839
properties: {
39-
allowTop: { type: "boolean" },
40+
allowTop: {
41+
anyOf: [
42+
{
43+
// backward compatibility
44+
type: "boolean",
45+
},
46+
{ enum: ["always", "never", "partial"] },
47+
],
48+
},
4049
},
4150
additionalProperties: false,
4251
},
@@ -47,7 +56,12 @@ export default createRule("no-useless-non-capturing-group", {
4756
type: "suggestion", // "problem",
4857
},
4958
create(context) {
50-
const allowTop = context.options[0]?.allowTop ?? false
59+
const allowTop: "always" | "never" | "partial" =
60+
context.options[0]?.allowTop === true
61+
? "always"
62+
: context.options[0]?.allowTop === false
63+
? "never"
64+
: context.options[0]?.allowTop ?? "partial"
5165

5266
/**
5367
* Create visitor
@@ -56,10 +70,25 @@ export default createRule("no-useless-non-capturing-group", {
5670
node,
5771
getRegexpLocation,
5872
fixReplaceNode,
73+
getUsageOfPattern,
5974
}: RegExpContext): RegExpVisitor.Handlers {
75+
let isIgnored: (gNode: Group) => boolean
76+
if (allowTop === "always") {
77+
isIgnored = isTopLevel
78+
} else if (allowTop === "partial") {
79+
if (getUsageOfPattern() !== UsageOfPattern.whole) {
80+
isIgnored = isTopLevel
81+
} else {
82+
isIgnored = () => false
83+
}
84+
} else {
85+
// allowTop === "never"
86+
isIgnored = () => false
87+
}
88+
6089
return {
6190
onGroupEnter(gNode) {
62-
if (allowTop && isTopLevel(gNode)) {
91+
if (isIgnored(gNode)) {
6392
return
6493
}
6594

@@ -98,7 +127,18 @@ export default createRule("no-useless-non-capturing-group", {
98127
node,
99128
loc: getRegexpLocation(gNode),
100129
messageId: "unexpected",
101-
fix: fixReplaceNode(gNode, gNode.raw.slice(3, -1)),
130+
fix: fixReplaceNode(gNode, () => {
131+
if (
132+
allowTop === "never" &&
133+
isTopLevel(gNode) &&
134+
getUsageOfPattern() !== UsageOfPattern.whole
135+
) {
136+
// Top-level group and potentially partially used patterns
137+
// do not autofix because they can cause side effects.
138+
return null
139+
}
140+
return gNode.raw.slice(3, -1)
141+
}),
102142
})
103143
},
104144
}

0 commit comments

Comments
 (0)