diff --git a/README.md b/README.md
index 24f580bbd..fa3483784 100644
--- a/README.md
+++ b/README.md
@@ -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: |
diff --git a/docs/rules/README.md b/docs/rules/README.md
index f9930888c..bf50b78f4 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -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: |
diff --git a/docs/rules/prefer-named-backreference.md b/docs/rules/prefer-named-backreference.md
index 8960956ea..0bb71410c 100644
--- a/docs/rules/prefer-named-backreference.md
+++ b/docs/rules/prefer-named-backreference.md
@@ -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
diff --git a/docs/rules/prefer-named-capture-group.md b/docs/rules/prefer-named-capture-group.md
index f59a36994..e5e8684c8 100644
--- a/docs/rules/prefer-named-capture-group.md
+++ b/docs/rules/prefer-named-capture-group.md
@@ -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
diff --git a/docs/rules/prefer-named-replacement.md b/docs/rules/prefer-named-replacement.md
index 8d59b529c..d431fbc74 100644
--- a/docs/rules/prefer-named-replacement.md
+++ b/docs/rules/prefer-named-replacement.md
@@ -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
diff --git a/docs/rules/prefer-result-array-groups.md b/docs/rules/prefer-result-array-groups.md
new file mode 100644
index 000000000..c5ea55116
--- /dev/null
+++ b/docs/rules/prefer-result-array-groups.md
@@ -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: ***This rule has not been released yet.***
+- :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).
+
+
+
+```js
+/* eslint regexp/prefer-result-array-groups: "error" */
+
+const regex = /(?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]
+}
+```
+
+
+
+## :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)
diff --git a/lib/rules/prefer-result-array-groups.ts b/lib/rules/prefer-result-array-groups.ts
new file mode 100644
index 000000000..1f4876a8a
--- /dev/null
+++ b/lib/rules/prefer-result-array-groups.ts
@@ -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"
+ }
+ },
+})
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index fb15707de..c5c23486d 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -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"
@@ -140,6 +141,7 @@ export const rules = [
preferRange,
preferRegexpExec,
preferRegexpTest,
+ preferResultArrayGroups,
preferStarQuantifier,
preferT,
preferUnicodeCodepointEscapes,
diff --git a/lib/utils/ts-utils/index.ts b/lib/utils/ts-utils/index.ts
new file mode 100644
index 000000000..8d4712511
--- /dev/null
+++ b/lib/utils/ts-utils/index.ts
@@ -0,0 +1,159 @@
+import type { Rule } from "eslint"
+import type * as TS from "typescript"
+// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- ignore
+type TypeScript = typeof import("typescript")
+
+/**
+ * Get TypeScript tools
+ */
+export function getTypeScriptTools(
+ context: Rule.RuleContext,
+): {
+ tsNodeMap: ReadonlyMap
+ checker: TS.TypeChecker
+ usedTS: boolean
+ hasFullTypeInformation: boolean
+} {
+ const ts = getTypeScript()
+ const tsNodeMap: ReadonlyMap =
+ context.parserServices.esTreeNodeToTSNodeMap
+ const checker: TS.TypeChecker =
+ context.parserServices.program &&
+ context.parserServices.program.getTypeChecker()
+ const usedTS = Boolean(ts && tsNodeMap && checker)
+ const hasFullTypeInformation =
+ usedTS && context.parserServices.hasFullTypeInformation !== false
+
+ return {
+ tsNodeMap,
+ checker,
+ usedTS,
+ hasFullTypeInformation,
+ }
+}
+
+let cacheTypeScript: TypeScript | undefined
+/**
+ * Get TypeScript tools
+ */
+export function getTypeScript(): TypeScript | undefined {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore
+ return (cacheTypeScript ??= require("typescript"))
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
+ } catch (e: any) {
+ if (e.code === "MODULE_NOT_FOUND") {
+ return undefined
+ }
+ throw e
+ }
+}
+
+/**
+ * Check if a given type is an array-like type or not.
+ */
+export function isArrayLikeObject(tsType: TS.Type): boolean {
+ const ts = getTypeScript()!
+ return (
+ isObject(tsType) &&
+ (tsType.objectFlags &
+ (ts.ObjectFlags.ArrayLiteral |
+ ts.ObjectFlags.EvolvingArray |
+ ts.ObjectFlags.Tuple)) !==
+ 0
+ )
+}
+
+/**
+ * Check if a given type is an interface type or not.
+ */
+export function isClassOrInterface(
+ tsType: TS.Type,
+): tsType is TS.InterfaceType {
+ const ts = getTypeScript()!
+ return (
+ isObject(tsType) &&
+ (tsType.objectFlags & ts.ObjectFlags.ClassOrInterface) !== 0
+ )
+}
+
+/**
+ * Check if a given type is an object type or not.
+ */
+export function isObject(tsType: TS.Type): tsType is TS.ObjectType {
+ const ts = getTypeScript()!
+ return (tsType.flags & ts.TypeFlags.Object) !== 0
+}
+
+/**
+ * Check if a given type is a reference type or not.
+ */
+export function isReferenceObject(tsType: TS.Type): tsType is TS.TypeReference {
+ const ts = getTypeScript()!
+ return (
+ isObject(tsType) &&
+ (tsType.objectFlags & ts.ObjectFlags.Reference) !== 0
+ )
+}
+
+/**
+ * Check if a given type is a union-or-intersection type or not.
+ */
+export function isUnionOrIntersection(
+ tsType: TS.Type,
+): tsType is TS.UnionOrIntersectionType {
+ const ts = getTypeScript()!
+ return (tsType.flags & ts.TypeFlags.UnionOrIntersection) !== 0
+}
+/**
+ * Check if a given type is a type-parameter type or not.
+ */
+export function isTypeParameter(
+ tsType: TS.Type,
+): tsType is TS.UnionOrIntersectionType {
+ const ts = getTypeScript()!
+ return (tsType.flags & ts.TypeFlags.TypeParameter) !== 0
+}
+
+/**
+ * Check if a given type is an any type or not.
+ */
+export function isAny(tsType: TS.Type): boolean {
+ const ts = getTypeScript()!
+ return (tsType.flags & ts.TypeFlags.Any) !== 0
+}
+/**
+ * Check if a given type is an unknown type or not.
+ */
+export function isUnknown(tsType: TS.Type): boolean {
+ const ts = getTypeScript()!
+ return (tsType.flags & ts.TypeFlags.Unknown) !== 0
+}
+/**
+ * Check if a given type is an string-like type or not.
+ */
+export function isStringLine(tsType: TS.Type): boolean {
+ const ts = getTypeScript()!
+ return (tsType.flags & ts.TypeFlags.StringLike) !== 0
+}
+/**
+ * Check if a given type is an number-like type or not.
+ */
+export function isNumberLike(tsType: TS.Type): boolean {
+ const ts = getTypeScript()!
+ return (tsType.flags & ts.TypeFlags.NumberLike) !== 0
+}
+/**
+ * Check if a given type is an boolean-like type or not.
+ */
+export function isBooleanLike(tsType: TS.Type): boolean {
+ const ts = getTypeScript()!
+ return (tsType.flags & ts.TypeFlags.BooleanLike) !== 0
+}
+/**
+ * Check if a given type is an bigint-like type or not.
+ */
+export function isBigIntLike(tsType: TS.Type): boolean {
+ const ts = getTypeScript()!
+ return (tsType.flags & ts.TypeFlags.BigIntLike) !== 0
+}
diff --git a/lib/utils/type-tracker/index.ts b/lib/utils/type-tracker/index.ts
index 19f2daeb6..359271f16 100644
--- a/lib/utils/type-tracker/index.ts
+++ b/lib/utils/type-tracker/index.ts
@@ -30,19 +30,24 @@ import { getJSDoc, parseTypeText } from "./jsdoc"
import type { JSDocTypeNode } from "./jsdoc/jsdoctypeparser-ast"
import { TypeIterable, UNKNOWN_ITERABLE } from "./type-data/iterable"
import { getParent } from "../ast-utils"
-
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- ignore
-const ts: typeof import("typescript") = (() => {
- try {
- // eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore
- return require("typescript")
- } catch (e) {
- if (e.code === "MODULE_NOT_FOUND") {
- return undefined
- }
- throw e
- }
-})()
+import {
+ getTypeScript,
+ getTypeScriptTools,
+ isAny,
+ isArrayLikeObject,
+ isBigIntLike,
+ isBooleanLike,
+ isClassOrInterface,
+ isNumberLike,
+ isObject,
+ isReferenceObject,
+ isStringLine,
+ isTypeParameter,
+ isUnionOrIntersection,
+ isUnknown,
+} from "../ts-utils"
+
+const ts = getTypeScript()!
export type TypeTracker = {
isString: (node: ES.Expression) => boolean
@@ -63,12 +68,7 @@ export function createTypeTracker(context: Rule.RuleContext): TypeTracker {
return cache
}
- const tsNodeMap: ReadonlyMap =
- context.parserServices.esTreeNodeToTSNodeMap
- const checker: TS.TypeChecker =
- context.parserServices.program &&
- context.parserServices.program.getTypeChecker()
- const availableTS = Boolean(ts && tsNodeMap && checker)
+ const { tsNodeMap, checker, usedTS } = getTypeScriptTools(context)
const cacheTypeInfo = new WeakMap()
@@ -95,7 +95,7 @@ export function createTypeTracker(context: Rule.RuleContext): TypeTracker {
if (isString(node)) {
return true
}
- if (availableTS) {
+ if (usedTS) {
return false
}
return getType(node) == null
@@ -171,7 +171,7 @@ export function createTypeTracker(context: Rule.RuleContext): TypeTracker {
return STRING
}
- if (availableTS) {
+ if (usedTS) {
return getTypeByTs(node)
}
@@ -505,7 +505,7 @@ export function createTypeTracker(context: Rule.RuleContext): TypeTracker {
}
}
- return availableTS ? getTypeByTs(node) : null
+ return usedTS ? getTypeByTs(node) : null
}
/**
@@ -525,22 +525,19 @@ export function createTypeTracker(context: Rule.RuleContext): TypeTracker {
/* eslint-enable complexity -- X( */
tsType: TS.Type,
): TypeInfo | null {
- if ((tsType.flags & ts.TypeFlags.StringLike) !== 0) {
+ if (isStringLine(tsType)) {
return STRING
}
- if ((tsType.flags & ts.TypeFlags.NumberLike) !== 0) {
+ if (isNumberLike(tsType)) {
return NUMBER
}
- if ((tsType.flags & ts.TypeFlags.BooleanLike) !== 0) {
+ if (isBooleanLike(tsType)) {
return BOOLEAN
}
- if ((tsType.flags & ts.TypeFlags.BigIntLike) !== 0) {
+ if (isBigIntLike(tsType)) {
return BIGINT
}
- if (
- (tsType.flags & ts.TypeFlags.Any) !== 0 ||
- (tsType.flags & ts.TypeFlags.Unknown) !== 0
- ) {
+ if (isAny(tsType) || isUnknown(tsType)) {
return null
}
if (isArrayLikeObject(tsType)) {
@@ -550,7 +547,7 @@ export function createTypeTracker(context: Rule.RuleContext): TypeTracker {
if (isReferenceObject(tsType) && tsType.target !== tsType) {
return getTypeFromTsType(tsType.target)
}
- if ((tsType.flags & ts.TypeFlags.TypeParameter) !== 0) {
+ if (isTypeParameter(tsType)) {
const constraintType = getConstraintType(tsType)
if (constraintType) {
return getTypeFromTsType(constraintType)
@@ -596,56 +593,6 @@ export function createTypeTracker(context: Rule.RuleContext): TypeTracker {
}
}
-/**
- * Check if a given type is an array-like type or not.
- */
-function isArrayLikeObject(tsType: TS.Type) {
- return (
- isObject(tsType) &&
- (tsType.objectFlags &
- (ts.ObjectFlags.ArrayLiteral |
- ts.ObjectFlags.EvolvingArray |
- ts.ObjectFlags.Tuple)) !==
- 0
- )
-}
-
-/**
- * Check if a given type is an interface type or not.
- */
-function isClassOrInterface(tsType: TS.Type): tsType is TS.InterfaceType {
- return (
- isObject(tsType) &&
- (tsType.objectFlags & ts.ObjectFlags.ClassOrInterface) !== 0
- )
-}
-
-/**
- * Check if a given type is an object type or not.
- */
-function isObject(tsType: TS.Type): tsType is TS.ObjectType {
- return (tsType.flags & ts.TypeFlags.Object) !== 0
-}
-
-/**
- * Check if a given type is a reference type or not.
- */
-function isReferenceObject(tsType: TS.Type): tsType is TS.TypeReference {
- return (
- isObject(tsType) &&
- (tsType.objectFlags & ts.ObjectFlags.Reference) !== 0
- )
-}
-
-/**
- * Check if a given type is a union-or-intersection type or not.
- */
-function isUnionOrIntersection(
- tsType: TS.Type,
-): tsType is TS.UnionOrIntersectionType {
- return (tsType.flags & ts.TypeFlags.UnionOrIntersection) !== 0
-}
-
/** Get type from jsdoc type text */
function typeTextToTypeInfo(typeText?: string): TypeInfo | null {
if (typeText == null) {
diff --git a/tests/lib/rules/prefer-result-array-groups.ts b/tests/lib/rules/prefer-result-array-groups.ts
new file mode 100644
index 000000000..12a92facd
--- /dev/null
+++ b/tests/lib/rules/prefer-result-array-groups.ts
@@ -0,0 +1,413 @@
+import { RuleTester } from "eslint"
+import path from "path"
+import rule from "../../../lib/rules/prefer-result-array-groups"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run("prefer-result-array-groups", rule as any, {
+ valid: [
+ `
+ const regex = /regexp/
+ let match
+ while (match = regex.exec(foo)) {
+ const match = match[0]
+ // ...
+ }
+ `,
+ `
+ const regex = /a(b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match[1]
+ // ...
+ }
+ `,
+ `
+ const regex = /a(b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match?.[1]
+ // ...
+ }
+ `,
+ `
+ const regex = /a(b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match.unknown
+ // ...
+ }
+ `,
+ `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match.unknown
+ // ...
+ }
+ `,
+ // String.prototype.match()
+ `
+ const arr = "str".match(/regexp/)
+ const p1 = arr[1]
+ `,
+ `
+ const arr = "str".match(/a(b)c/)
+ const p1 = arr[1]
+ `,
+ `
+ const arr = "str".match(/a(?b)c/)
+ const p1 = arr.groups.foo
+ `,
+ `
+ const arr = "str".match(/a(?b)c/)
+ const p1 = arr.unknown
+ `,
+ `
+ const arr = unknown.match(/a(?b)c/)
+ const p1 = arr[1]
+ `,
+ `
+ const arr = "str".match(/a(?b)c/g)
+ const p1 = arr[1]
+ `,
+ // String.prototype.matchAll()
+ `
+ const matches = "str".matchAll(/a(?b)c/);
+ for (const match of matches) {
+ const p1 = match.groups.foo
+ // ..
+ }
+ `,
+ `
+ const matches = "str".matchAll(/a(b)c/);
+ for (const match of matches) {
+ const p1 = match[1]
+ // ..
+ }
+ `,
+ `
+ const matches = unknown.matchAll(/a(?b)c/);
+ for (const match of matches) {
+ const p1 = match.groups.foo
+ // ..
+ }
+ `,
+ ],
+ invalid: [
+ {
+ code: `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match[1]
+ // ...
+ }
+ `,
+ output: `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match.groups.foo
+ // ...
+ }
+ `,
+ errors: [
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ line: 5,
+ column: 28,
+ },
+ ],
+ },
+ {
+ code: `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match?.[1]
+ // ...
+ }
+ `,
+ output: `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match?.groups.foo
+ // ...
+ }
+ `,
+ errors: [
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ line: 5,
+ column: 28,
+ },
+ ],
+ },
+ {
+ code: `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match?.[(1)]
+ // ...
+ }
+ `,
+ output: `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match?.groups.foo
+ // ...
+ }
+ `,
+ errors: [
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ line: 5,
+ column: 28,
+ },
+ ],
+ },
+ {
+ code: `
+ const regex = /(a)(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match[1]
+ const p2 = match[2]
+ // ...
+ }
+ `,
+ output: `
+ const regex = /(a)(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match[1]
+ const p2 = match.groups.foo
+ // ...
+ }
+ `,
+ errors: [
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ line: 6,
+ column: 28,
+ },
+ ],
+ },
+ {
+ code: `
+ const regex = /(?a)(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const [,p1,p2] = match
+ // ...
+ }
+ `,
+ output: null,
+ errors: [
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ line: 5,
+ column: 25,
+ },
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'bar' from regexp result array.",
+ line: 5,
+ column: 28,
+ },
+ ],
+ },
+ // String.prototype.match()
+ {
+ code: `
+ const arr = "str".match(/a(?b)c/)
+ const p1 = arr[1]
+ `,
+ output: `
+ const arr = "str".match(/a(?b)c/)
+ const p1 = arr.groups.foo
+ `,
+ errors: [
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ line: 3,
+ column: 24,
+ },
+ ],
+ },
+ {
+ code: `
+ const arr = unknown.match(/a(?b)c/)
+ const p1 = arr[1]
+ `,
+ output: `
+ const arr = unknown.match(/a(?b)c/)
+ const p1 = arr.groups.foo
+ `,
+ options: [{ strictTypes: false }],
+ errors: [
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ line: 3,
+ column: 24,
+ },
+ ],
+ },
+ // String.prototype.matchAll()
+ {
+ code: `
+ const matches = "str".matchAll(/a(?b)c/);
+ for (const match of matches) {
+ const p1 = match[1]
+ // ..
+ }
+ `,
+ output: `
+ const matches = "str".matchAll(/a(?b)c/);
+ for (const match of matches) {
+ const p1 = match.groups.foo
+ // ..
+ }
+ `,
+ errors: [
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ line: 4,
+ column: 28,
+ },
+ ],
+ },
+ {
+ code: `
+ const matches = unknown.matchAll(/a(?b)c/);
+ for (const match of matches) {
+ const p1 = match[1]
+ // ..
+ }
+ `,
+ output: `
+ const matches = unknown.matchAll(/a(?b)c/);
+ for (const match of matches) {
+ const p1 = match.groups.foo
+ // ..
+ }
+ `,
+ options: [{ strictTypes: false }],
+ errors: [
+ {
+ message:
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ line: 4,
+ column: 28,
+ },
+ ],
+ },
+ // with TypeScript
+ {
+ filename: path.join(__dirname, "prefer-result-array-groups.ts"),
+ code: `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match[1]
+ // ...
+ }
+ `,
+ output: `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match.groups!.foo
+ // ...
+ }
+ `,
+ parser: require.resolve("@typescript-eslint/parser"),
+ parserOptions: {
+ project: require.resolve("../../../tsconfig.json"),
+ },
+ errors: [
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ ],
+ },
+ {
+ // If don't give type information.
+ code: `
+ const regex = /a(?b)c/
+ let match
+ while (match = regex.exec(foo)) {
+ const p1 = match[1]
+ // ...
+ }
+ `,
+ output: null,
+ parser: require.resolve("@typescript-eslint/parser"),
+ errors: [
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ ],
+ },
+ {
+ // Not using RegExpExecArray
+ filename: path.join(__dirname, "prefer-result-array-groups.ts"),
+ code: `
+ const regex = /a(?b)c/
+ let match: any[] | null
+ while (match = regex.exec(foo)) {
+ const p1 = match[1]
+ // ...
+ }
+ `,
+ output: null,
+ parser: require.resolve("@typescript-eslint/parser"),
+ parserOptions: {
+ project: require.resolve("../../../tsconfig.json"),
+ },
+ errors: [
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ ],
+ },
+ {
+ // Using `any` type.
+ filename: path.join(__dirname, "prefer-result-array-groups.ts"),
+ code: `
+ const regex = /a(?b)c/
+ let match: any
+ while (match = regex.exec(foo)) {
+ const p1 = match[1]
+ // ...
+ }
+ `,
+ output: `
+ const regex = /a(?b)c/
+ let match: any
+ while (match = regex.exec(foo)) {
+ const p1 = match.groups.foo
+ // ...
+ }
+ `,
+ parser: require.resolve("@typescript-eslint/parser"),
+ parserOptions: {
+ project: require.resolve("../../../tsconfig.json"),
+ },
+ errors: [
+ "Unexpected indexed access for the named capturing group 'foo' from regexp result array.",
+ ],
+ },
+ ],
+})
diff --git a/typings/eslint-utils/index.d.ts b/typings/eslint-utils/index.d.ts
index 1587b2e46..469b432dd 100644
--- a/typings/eslint-utils/index.d.ts
+++ b/typings/eslint-utils/index.d.ts
@@ -37,6 +37,9 @@ export function isCommentToken(
export function isOpeningParenToken(
token: eslint.AST.Token | ESTree.Comment,
): boolean
+export function isOpeningBracketToken(
+ token: eslint.AST.Token | ESTree.Comment,
+): boolean
export function hasSideEffect(
node: ESTree.Node,
sourceCode: SourceCode,