Skip to content

Commit 155f1f4

Browse files
authored
Add regexp/prefer-result-array-groups rule (#351)
1 parent 9c3e37a commit 155f1f4

File tree

12 files changed

+845
-81
lines changed

12 files changed

+845
-81
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
178178
| [regexp/prefer-named-replacement](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-replacement.html) | enforce using named replacement | :wrench: |
179179
| [regexp/prefer-plus-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html) | enforce using `+` quantifier | :star::wrench: |
180180
| [regexp/prefer-question-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-question-quantifier.html) | enforce using `?` quantifier | :star::wrench: |
181+
| [regexp/prefer-result-array-groups](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-result-array-groups.html) | enforce using result array `groups` | :wrench: |
181182
| [regexp/prefer-star-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-star-quantifier.html) | enforce using `*` quantifier | :star::wrench: |
182183
| [regexp/prefer-unicode-codepoint-escapes](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-unicode-codepoint-escapes.html) | enforce use of unicode codepoint escapes | :star::wrench: |
183184
| [regexp/prefer-w](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-w.html) | enforce using `\w` | :star::wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
8787
| [regexp/prefer-named-replacement](./prefer-named-replacement.md) | enforce using named replacement | :wrench: |
8888
| [regexp/prefer-plus-quantifier](./prefer-plus-quantifier.md) | enforce using `+` quantifier | :star::wrench: |
8989
| [regexp/prefer-question-quantifier](./prefer-question-quantifier.md) | enforce using `?` quantifier | :star::wrench: |
90+
| [regexp/prefer-result-array-groups](./prefer-result-array-groups.md) | enforce using result array `groups` | :wrench: |
9091
| [regexp/prefer-star-quantifier](./prefer-star-quantifier.md) | enforce using `*` quantifier | :star::wrench: |
9192
| [regexp/prefer-unicode-codepoint-escapes](./prefer-unicode-codepoint-escapes.md) | enforce use of unicode codepoint escapes | :star::wrench: |
9293
| [regexp/prefer-w](./prefer-w.md) | enforce using `\w` | :star::wrench: |

docs/rules/prefer-named-backreference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ Nothing.
3838

3939
- [regexp/prefer-named-capture-group]
4040
- [regexp/prefer-named-replacement]
41+
- [regexp/prefer-result-array-groups]
4142

4243
[regexp/prefer-named-capture-group]: ./prefer-named-capture-group.md
4344
[regexp/prefer-named-replacement]: ./prefer-named-replacement.md
45+
[regexp/prefer-result-array-groups]: ./prefer-result-array-groups.md
4446

4547
## :rocket: Version
4648

docs/rules/prefer-named-capture-group.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ Nothing.
3838

3939
- [regexp/prefer-named-backreference]
4040
- [regexp/prefer-named-replacement]
41+
- [regexp/prefer-result-array-groups]
4142

4243
[regexp/prefer-named-backreference]: ./prefer-named-backreference.md
4344
[regexp/prefer-named-replacement]: ./prefer-named-replacement.md
45+
[regexp/prefer-result-array-groups]: ./prefer-result-array-groups.md
4446

4547
## :books: Further reading
4648

docs/rules/prefer-named-replacement.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ This rule reports and fixes `$n` parameter in replacement string that do not use
4747

4848
- [regexp/prefer-named-backreference]
4949
- [regexp/prefer-named-capture-group]
50+
- [regexp/prefer-result-array-groups]
5051

5152
[regexp/prefer-named-backreference]: ./prefer-named-backreference.md
5253
[regexp/prefer-named-capture-group]: ./prefer-named-capture-group.md
54+
[regexp/prefer-result-array-groups]: ./prefer-result-array-groups.md
5355

5456
## :mag: Implementation
5557

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/prefer-result-array-groups"
5+
description: "enforce using result array `groups`"
6+
---
7+
# regexp/prefer-result-array-groups
8+
9+
> enforce using result array `groups`
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
13+
14+
## :book: Rule Details
15+
16+
This rule reports and fixes regexp result arrays where named capturing groups are accessed by index instead of using [`groups`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges#using_named_groups).
17+
18+
<eslint-code-block fix>
19+
20+
```js
21+
/* eslint regexp/prefer-result-array-groups: "error" */
22+
23+
const regex = /(?<foo>a)(b)c/g
24+
let match
25+
while (match = regex.exec(str)) {
26+
/* ✓ GOOD */
27+
var p1 = match.groups.foo
28+
var p2 = match[2]
29+
30+
/* ✗ BAD */
31+
var p1 = match[1]
32+
}
33+
```
34+
35+
</eslint-code-block>
36+
37+
## :wrench: Options
38+
39+
```json
40+
{
41+
"regexp/prefer-result-array-groups": ["error", {
42+
"strictTypes": true
43+
}]
44+
}
45+
```
46+
47+
- `strictTypes` ... If `true`, strictly check the type of object to determine if the string instance was used in `match()` and `matchAll()`. Default is `true`.
48+
This option is always on when using TypeScript.
49+
50+
## :couple: Related rules
51+
52+
- [regexp/prefer-named-backreference]
53+
- [regexp/prefer-named-capture-group]
54+
- [regexp/prefer-named-replacement]
55+
56+
[regexp/prefer-named-backreference]: ./prefer-named-backreference.md
57+
[regexp/prefer-named-capture-group]: ./prefer-named-capture-group.md
58+
[regexp/prefer-named-replacement]: ./prefer-named-replacement.md
59+
60+
## :mag: Implementation
61+
62+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/prefer-result-array-groups.ts)
63+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/prefer-result-array-groups.ts)
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import type { RegExpVisitor } from "regexpp/visitor"
2+
import { isOpeningBracketToken } from "eslint-utils"
3+
import type { RegExpContext } from "../utils"
4+
import { createRule, defineRegexpVisitor } from "../utils"
5+
import {
6+
getTypeScriptTools,
7+
isAny,
8+
isClassOrInterface,
9+
} from "../utils/ts-utils"
10+
import type { Expression, Super } from "estree"
11+
12+
export default createRule("prefer-result-array-groups", {
13+
meta: {
14+
docs: {
15+
description: "enforce using result array `groups`",
16+
category: "Stylistic Issues",
17+
recommended: false,
18+
},
19+
fixable: "code",
20+
schema: [
21+
{
22+
type: "object",
23+
properties: {
24+
strictTypes: { type: "boolean" },
25+
},
26+
additionalProperties: false,
27+
},
28+
],
29+
messages: {
30+
unexpected:
31+
"Unexpected indexed access for the named capturing group '{{ name }}' from regexp result array.",
32+
},
33+
type: "suggestion",
34+
},
35+
create(context) {
36+
const strictTypes = context.options[0]?.strictTypes ?? true
37+
const sourceCode = context.getSourceCode()
38+
39+
/**
40+
* Create visitor
41+
*/
42+
function createVisitor(
43+
regexpContext: RegExpContext,
44+
): RegExpVisitor.Handlers {
45+
const {
46+
getAllCapturingGroups,
47+
getCapturingGroupReferences,
48+
} = regexpContext
49+
50+
const capturingGroups = getAllCapturingGroups()
51+
if (!capturingGroups.length) {
52+
return {}
53+
}
54+
55+
for (const ref of getCapturingGroupReferences({ strictTypes })) {
56+
if (
57+
ref.type === "ArrayRef" &&
58+
ref.kind === "index" &&
59+
ref.ref != null
60+
) {
61+
const cgNode = capturingGroups[ref.ref - 1]
62+
if (cgNode && cgNode.name) {
63+
const memberNode =
64+
ref.prop.type === "member" ? ref.prop.node : null
65+
context.report({
66+
node: ref.prop.node,
67+
messageId: "unexpected",
68+
data: {
69+
name: cgNode.name,
70+
},
71+
fix:
72+
memberNode && memberNode.computed
73+
? (fixer) => {
74+
const tokens = sourceCode.getTokensBetween(
75+
memberNode.object,
76+
memberNode.property,
77+
)
78+
let openingBracket = tokens.pop()
79+
while (
80+
openingBracket &&
81+
!isOpeningBracketToken(
82+
openingBracket,
83+
)
84+
) {
85+
openingBracket = tokens.pop()
86+
}
87+
if (!openingBracket) {
88+
// unknown ast
89+
return null
90+
}
91+
92+
const kind = getRegExpArrayTypeKind(
93+
memberNode.object,
94+
)
95+
if (kind === "unknown") {
96+
// Using TypeScript but I can't identify the type or it's not a RegExpXArray type.
97+
return null
98+
}
99+
const needNonNull =
100+
kind === "RegExpXArray"
101+
102+
return fixer.replaceTextRange(
103+
[
104+
openingBracket.range![0],
105+
memberNode.range![1],
106+
],
107+
`${
108+
memberNode.optional ? "" : "."
109+
}groups${
110+
needNonNull ? "!" : ""
111+
}.${cgNode.name}`,
112+
)
113+
}
114+
: null,
115+
})
116+
}
117+
}
118+
}
119+
120+
return {}
121+
}
122+
123+
return defineRegexpVisitor(context, {
124+
createVisitor,
125+
})
126+
127+
type RegExpArrayTypeKind =
128+
| "RegExpXArray" // RegExpMatchArray or RegExpExecArray
129+
| "any"
130+
| "unknown" // It's cannot autofix
131+
132+
/** Gets the type kind of the given node. */
133+
function getRegExpArrayTypeKind(
134+
node: Expression | Super,
135+
): RegExpArrayTypeKind | null {
136+
const {
137+
tsNodeMap,
138+
checker,
139+
usedTS,
140+
hasFullTypeInformation,
141+
} = getTypeScriptTools(context)
142+
if (!usedTS) {
143+
// Not using TypeScript.
144+
return null
145+
}
146+
if (!hasFullTypeInformation) {
147+
// The user has not given the type information to ESLint. So we don't know if this can be autofix.
148+
return "unknown"
149+
}
150+
const tsNode = tsNodeMap.get(node)
151+
const tsType = (tsNode && checker.getTypeAtLocation(tsNode)) || null
152+
if (!tsType) {
153+
// The node type cannot be determined.
154+
return "unknown"
155+
}
156+
157+
if (isClassOrInterface(tsType)) {
158+
const name = tsType.symbol.escapedName
159+
return name === "RegExpMatchArray" || name === "RegExpExecArray"
160+
? "RegExpXArray"
161+
: "unknown"
162+
}
163+
if (isAny(tsType)) {
164+
return "any"
165+
}
166+
return "unknown"
167+
}
168+
},
169+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import preferQuestionQuantifier from "../rules/prefer-question-quantifier"
6363
import preferRange from "../rules/prefer-range"
6464
import preferRegexpExec from "../rules/prefer-regexp-exec"
6565
import preferRegexpTest from "../rules/prefer-regexp-test"
66+
import preferResultArrayGroups from "../rules/prefer-result-array-groups"
6667
import preferStarQuantifier from "../rules/prefer-star-quantifier"
6768
import preferT from "../rules/prefer-t"
6869
import preferUnicodeCodepointEscapes from "../rules/prefer-unicode-codepoint-escapes"
@@ -140,6 +141,7 @@ export const rules = [
140141
preferRange,
141142
preferRegexpExec,
142143
preferRegexpTest,
144+
preferResultArrayGroups,
143145
preferStarQuantifier,
144146
preferT,
145147
preferUnicodeCodepointEscapes,

0 commit comments

Comments
 (0)