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,