Skip to content

Commit

Permalink
Add regexp/prefer-result-array-groups rule (#351)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Oct 4, 2021
1 parent 9c3e37a commit 155f1f4
Show file tree
Hide file tree
Showing 12 changed files with 845 additions and 81 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/prefer-named-replacement](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-replacement.html) | enforce using named replacement | :wrench: |
| [regexp/prefer-plus-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html) | enforce using `+` quantifier | :star::wrench: |
| [regexp/prefer-question-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-question-quantifier.html) | enforce using `?` quantifier | :star::wrench: |
| [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: |
| [regexp/prefer-star-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-star-quantifier.html) | enforce using `*` quantifier | :star::wrench: |
| [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: |
| [regexp/prefer-w](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-w.html) | enforce using `\w` | :star::wrench: |
Expand Down
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/prefer-named-replacement](./prefer-named-replacement.md) | enforce using named replacement | :wrench: |
| [regexp/prefer-plus-quantifier](./prefer-plus-quantifier.md) | enforce using `+` quantifier | :star::wrench: |
| [regexp/prefer-question-quantifier](./prefer-question-quantifier.md) | enforce using `?` quantifier | :star::wrench: |
| [regexp/prefer-result-array-groups](./prefer-result-array-groups.md) | enforce using result array `groups` | :wrench: |
| [regexp/prefer-star-quantifier](./prefer-star-quantifier.md) | enforce using `*` quantifier | :star::wrench: |
| [regexp/prefer-unicode-codepoint-escapes](./prefer-unicode-codepoint-escapes.md) | enforce use of unicode codepoint escapes | :star::wrench: |
| [regexp/prefer-w](./prefer-w.md) | enforce using `\w` | :star::wrench: |
Expand Down
2 changes: 2 additions & 0 deletions docs/rules/prefer-named-backreference.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ Nothing.

- [regexp/prefer-named-capture-group]
- [regexp/prefer-named-replacement]
- [regexp/prefer-result-array-groups]

[regexp/prefer-named-capture-group]: ./prefer-named-capture-group.md
[regexp/prefer-named-replacement]: ./prefer-named-replacement.md
[regexp/prefer-result-array-groups]: ./prefer-result-array-groups.md

## :rocket: Version

Expand Down
2 changes: 2 additions & 0 deletions docs/rules/prefer-named-capture-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ Nothing.

- [regexp/prefer-named-backreference]
- [regexp/prefer-named-replacement]
- [regexp/prefer-result-array-groups]

[regexp/prefer-named-backreference]: ./prefer-named-backreference.md
[regexp/prefer-named-replacement]: ./prefer-named-replacement.md
[regexp/prefer-result-array-groups]: ./prefer-result-array-groups.md

## :books: Further reading

Expand Down
2 changes: 2 additions & 0 deletions docs/rules/prefer-named-replacement.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ This rule reports and fixes `$n` parameter in replacement string that do not use

- [regexp/prefer-named-backreference]
- [regexp/prefer-named-capture-group]
- [regexp/prefer-result-array-groups]

[regexp/prefer-named-backreference]: ./prefer-named-backreference.md
[regexp/prefer-named-capture-group]: ./prefer-named-capture-group.md
[regexp/prefer-result-array-groups]: ./prefer-result-array-groups.md

## :mag: Implementation

Expand Down
63 changes: 63 additions & 0 deletions docs/rules/prefer-result-array-groups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "regexp/prefer-result-array-groups"
description: "enforce using result array `groups`"
---
# regexp/prefer-result-array-groups

> enforce using result array `groups`
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :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.

## :book: Rule Details

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).

<eslint-code-block fix>

```js
/* eslint regexp/prefer-result-array-groups: "error" */

const regex = /(?<foo>a)(b)c/g
let match
while (match = regex.exec(str)) {
/* ✓ GOOD */
var p1 = match.groups.foo
var p2 = match[2]

/* ✗ BAD */
var p1 = match[1]
}
```

</eslint-code-block>

## :wrench: Options

```json
{
"regexp/prefer-result-array-groups": ["error", {
"strictTypes": true
}]
}
```

- `strictTypes` ... If `true`, strictly check the type of object to determine if the string instance was used in `match()` and `matchAll()`. Default is `true`.
This option is always on when using TypeScript.

## :couple: Related rules

- [regexp/prefer-named-backreference]
- [regexp/prefer-named-capture-group]
- [regexp/prefer-named-replacement]

[regexp/prefer-named-backreference]: ./prefer-named-backreference.md
[regexp/prefer-named-capture-group]: ./prefer-named-capture-group.md
[regexp/prefer-named-replacement]: ./prefer-named-replacement.md

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/prefer-result-array-groups.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/prefer-result-array-groups.ts)
169 changes: 169 additions & 0 deletions lib/rules/prefer-result-array-groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import type { RegExpVisitor } from "regexpp/visitor"
import { isOpeningBracketToken } from "eslint-utils"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"
import {
getTypeScriptTools,
isAny,
isClassOrInterface,
} from "../utils/ts-utils"
import type { Expression, Super } from "estree"

export default createRule("prefer-result-array-groups", {
meta: {
docs: {
description: "enforce using result array `groups`",
category: "Stylistic Issues",
recommended: false,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
strictTypes: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
unexpected:
"Unexpected indexed access for the named capturing group '{{ name }}' from regexp result array.",
},
type: "suggestion",
},
create(context) {
const strictTypes = context.options[0]?.strictTypes ?? true
const sourceCode = context.getSourceCode()

/**
* Create visitor
*/
function createVisitor(
regexpContext: RegExpContext,
): RegExpVisitor.Handlers {
const {
getAllCapturingGroups,
getCapturingGroupReferences,
} = regexpContext

const capturingGroups = getAllCapturingGroups()
if (!capturingGroups.length) {
return {}
}

for (const ref of getCapturingGroupReferences({ strictTypes })) {
if (
ref.type === "ArrayRef" &&
ref.kind === "index" &&
ref.ref != null
) {
const cgNode = capturingGroups[ref.ref - 1]
if (cgNode && cgNode.name) {
const memberNode =
ref.prop.type === "member" ? ref.prop.node : null
context.report({
node: ref.prop.node,
messageId: "unexpected",
data: {
name: cgNode.name,
},
fix:
memberNode && memberNode.computed
? (fixer) => {
const tokens = sourceCode.getTokensBetween(
memberNode.object,
memberNode.property,
)
let openingBracket = tokens.pop()
while (
openingBracket &&
!isOpeningBracketToken(
openingBracket,
)
) {
openingBracket = tokens.pop()
}
if (!openingBracket) {
// unknown ast
return null
}

const kind = getRegExpArrayTypeKind(
memberNode.object,
)
if (kind === "unknown") {
// Using TypeScript but I can't identify the type or it's not a RegExpXArray type.
return null
}
const needNonNull =
kind === "RegExpXArray"

return fixer.replaceTextRange(
[
openingBracket.range![0],
memberNode.range![1],
],
`${
memberNode.optional ? "" : "."
}groups${
needNonNull ? "!" : ""
}.${cgNode.name}`,
)
}
: null,
})
}
}
}

return {}
}

return defineRegexpVisitor(context, {
createVisitor,
})

type RegExpArrayTypeKind =
| "RegExpXArray" // RegExpMatchArray or RegExpExecArray
| "any"
| "unknown" // It's cannot autofix

/** Gets the type kind of the given node. */
function getRegExpArrayTypeKind(
node: Expression | Super,
): RegExpArrayTypeKind | null {
const {
tsNodeMap,
checker,
usedTS,
hasFullTypeInformation,
} = getTypeScriptTools(context)
if (!usedTS) {
// Not using TypeScript.
return null
}
if (!hasFullTypeInformation) {
// The user has not given the type information to ESLint. So we don't know if this can be autofix.
return "unknown"
}
const tsNode = tsNodeMap.get(node)
const tsType = (tsNode && checker.getTypeAtLocation(tsNode)) || null
if (!tsType) {
// The node type cannot be determined.
return "unknown"
}

if (isClassOrInterface(tsType)) {
const name = tsType.symbol.escapedName
return name === "RegExpMatchArray" || name === "RegExpExecArray"
? "RegExpXArray"
: "unknown"
}
if (isAny(tsType)) {
return "any"
}
return "unknown"
}
},
})
2 changes: 2 additions & 0 deletions lib/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import preferQuestionQuantifier from "../rules/prefer-question-quantifier"
import preferRange from "../rules/prefer-range"
import preferRegexpExec from "../rules/prefer-regexp-exec"
import preferRegexpTest from "../rules/prefer-regexp-test"
import preferResultArrayGroups from "../rules/prefer-result-array-groups"
import preferStarQuantifier from "../rules/prefer-star-quantifier"
import preferT from "../rules/prefer-t"
import preferUnicodeCodepointEscapes from "../rules/prefer-unicode-codepoint-escapes"
Expand Down Expand Up @@ -140,6 +141,7 @@ export const rules = [
preferRange,
preferRegexpExec,
preferRegexpTest,
preferResultArrayGroups,
preferStarQuantifier,
preferT,
preferUnicodeCodepointEscapes,
Expand Down
Loading

0 comments on commit 155f1f4

Please sign in to comment.