Skip to content

Commit

Permalink
Add prefer-result-array-groups rule
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Oct 2, 2021
1 parent 903cced commit 5bb7dd1
Show file tree
Hide file tree
Showing 9 changed files with 527 additions and 9 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/prefer-named-capture-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-capture-group.html) | enforce using named capture groups | |
| [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 @@ -86,6 +86,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/prefer-named-capture-group](./prefer-named-capture-group.md) | enforce using named capture groups | |
| [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
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 index access in regexp result array that do not use the name of their referenced capturing group.

<eslint-code-block fix>

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

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

/* ✗ BAD */
var p1 = re[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)
104 changes: 104 additions & 0 deletions lib/rules/prefer-result-array-groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { RegExpVisitor } from "regexpp/visitor"
import { isOpeningBracketToken } from "eslint-utils"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"

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 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",
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
}
return fixer.replaceTextRange(
[
openingBracket.range![0],
memberNode.range![1],
],
`${
memberNode.optional ? "" : "."
}groups.${cgNode.name}`,
)
}
: null,
})
}
}
}

return {}
}

return defineRegexpVisitor(context, {
createVisitor,
})
},
})
13 changes: 9 additions & 4 deletions lib/utils/extract-capturing-group-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ export type CapturingGroupReference =

type ExtractCapturingGroupReferencesContext = {
flags: ReadonlyFlags
typeTracer: TypeTracker
countOfCapturingGroup: number
context: Rule.RuleContext
isString: (node: Expression) => boolean
}

/**
Expand All @@ -145,12 +145,17 @@ export function* extractCapturingGroupReferences(
typeTracer: TypeTracker,
countOfCapturingGroup: number,
context: Rule.RuleContext,
options: {
strictTypes: boolean
},
): Iterable<CapturingGroupReference> {
const ctx = {
const ctx: ExtractCapturingGroupReferencesContext = {
flags,
typeTracer,
countOfCapturingGroup,
context,
isString: options.strictTypes
? (n) => typeTracer.isString(n)
: (n) => typeTracer.maybeString(n),
}
for (const ref of extractExpressionReferences(node, context)) {
if (ref.type === "argument") {
Expand Down Expand Up @@ -187,7 +192,7 @@ function* iterateForArgument(
if (callExpression.arguments[0] !== argument) {
return
}
if (!ctx.typeTracer.isString(callExpression.callee.object)) {
if (!ctx.isString(callExpression.callee.object)) {
yield {
type: "UnknownUsage",
node: argument,
Expand Down
27 changes: 22 additions & 5 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ type RegExpContextBase = {
/**
* Returns the capturing group references
*/
getCapturingGroupReferences: () => CapturingGroupReference[]
getCapturingGroupReferences: (options?: {
strictTypes?: boolean // default true
}) => CapturingGroupReference[]

/**
* Returns a list of all capturing groups in the order of their numbers.
Expand Down Expand Up @@ -610,7 +612,10 @@ function buildRegExpContextBase({
const sourceCode = context.getSourceCode()

let cacheUsageOfPattern: UsageOfPattern | null = null
let cacheCapturingGroupReference: CapturingGroupReference[] | null = null
const cacheCapturingGroupReferenceMap = new Map<
boolean /* strictTypes */,
CapturingGroupReference[]
>()
let cacheAllCapturingGroups: CapturingGroup[] | null = null
return {
getRegexpLocation: (range, offsets) => {
Expand Down Expand Up @@ -643,21 +648,33 @@ function buildRegExpContextBase({
},
getUsageOfPattern: () =>
(cacheUsageOfPattern ??= getUsageOfPattern(regexpNode, context)),
getCapturingGroupReferences: () => {
getCapturingGroupReferences: (options?: {
strictTypes?: boolean // default true
}) => {
const strictTypes = Boolean(options?.strictTypes ?? true)
const cacheCapturingGroupReference = cacheCapturingGroupReferenceMap.get(
strictTypes,
)
if (cacheCapturingGroupReference) {
return cacheCapturingGroupReference
}
const countOfCapturingGroup = getAllCapturingGroupsWithCache()
.length
return (cacheCapturingGroupReference = [
const capturingGroupReferences = [
...extractCapturingGroupReferences(
regexpNode,
flags,
createTypeTracker(context),
countOfCapturingGroup,
context,
{ strictTypes },
),
])
]
cacheCapturingGroupReferenceMap.set(
strictTypes,
capturingGroupReferences,
)
return capturingGroupReferences
},
getAllCapturingGroups: getAllCapturingGroupsWithCache,

Expand Down
2 changes: 2 additions & 0 deletions lib/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,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 @@ -138,6 +139,7 @@ export const rules = [
preferRange,
preferRegexpExec,
preferRegexpTest,
preferResultArrayGroups,
preferStarQuantifier,
preferT,
preferUnicodeCodepointEscapes,
Expand Down
Loading

0 comments on commit 5bb7dd1

Please sign in to comment.