Skip to content

Commit 9a780be

Browse files
authored
Fix false positives for matchAll in regexp/no-unused-capturing-group rule (#403)
1 parent 85d03f2 commit 9a780be

File tree

3 files changed

+234
-49
lines changed

3 files changed

+234
-49
lines changed

lib/rules/no-unused-capturing-group.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export default createRule("no-unused-capturing-group", {
114114
ref.type === "ReplacerFunctionRef"
115115
) {
116116
if (ref.kind === "index") {
117-
if (ref.ref) {
117+
if (ref.ref != null) {
118118
indexRefs.push(ref.ref)
119119
} else {
120120
return null

lib/utils/extract-capturing-group-references.ts

Lines changed: 172 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,56 @@ type ExtractCapturingGroupReferencesContext = {
136136
isString: (node: Expression) => boolean
137137
}
138138

139+
type ArrayMethodName = Exclude<keyof unknown[], "length" | symbol | number>
140+
const WELL_KNOWN_ARRAY_METHODS: {
141+
[key in ArrayMethodName]: {
142+
// If specified, the method receives a function that iterates the element.
143+
// Specify an array with the index of the argument that receives the element.
144+
elementParameters?: number[]
145+
result?:
146+
| "element" // The method returns the element.
147+
| "array" // The method returns an array with some of the elements.]
148+
| "iterator" // The method returns an iterator with some of the elements.
149+
}
150+
} = {
151+
toString: {},
152+
toLocaleString: {},
153+
pop: { result: "element" },
154+
push: {},
155+
concat: { result: "array" },
156+
join: {},
157+
reverse: { result: "array" },
158+
shift: { result: "element" },
159+
slice: { result: "array" },
160+
sort: { elementParameters: [0, 1], result: "array" },
161+
splice: { result: "array" },
162+
unshift: {},
163+
indexOf: {},
164+
lastIndexOf: {},
165+
every: { elementParameters: [0] },
166+
some: { elementParameters: [0] },
167+
forEach: { elementParameters: [0] },
168+
map: { elementParameters: [0] },
169+
filter: { elementParameters: [0], result: "array" },
170+
reduce: { elementParameters: [1] },
171+
reduceRight: { elementParameters: [1] },
172+
// ES2015
173+
find: { elementParameters: [0], result: "element" },
174+
findIndex: { elementParameters: [0] },
175+
fill: {},
176+
copyWithin: { result: "array" },
177+
entries: {},
178+
keys: {},
179+
values: { result: "iterator" },
180+
// ES2016
181+
includes: {},
182+
// ES2019
183+
flatMap: { elementParameters: [0] },
184+
flat: {},
185+
// ES2022
186+
at: { result: "element" },
187+
}
188+
139189
/**
140190
* Extracts the usage of the capturing group.
141191
*/
@@ -234,11 +284,9 @@ function* iterateForMember(
234284
object: Expression,
235285
ctx: ExtractCapturingGroupReferencesContext,
236286
): Iterable<CapturingGroupReference> {
237-
const parent = getParent(memberExpression)
287+
const parent = getCallExpressionFromCalleeExpression(memberExpression)
238288
if (
239289
!parent ||
240-
parent.type !== "CallExpression" ||
241-
parent.callee !== memberExpression ||
242290
!isKnownMethodCall(parent, {
243291
test: 1,
244292
exec: 1,
@@ -335,7 +383,7 @@ function* iterateForStringReplace(
335383

336384
/** Iterate the capturing group references for String.prototype.matchAll(). */
337385
function* iterateForStringMatchAll(
338-
node: KnownMethodCall,
386+
node: CallExpression,
339387
argument: Expression,
340388
ctx: ExtractCapturingGroupReferencesContext,
341389
): Iterable<CapturingGroupReference> {
@@ -351,37 +399,41 @@ function* iterateForStringMatchAll(
351399
return
352400
}
353401
if (hasNameRef(iterationRef)) {
402+
if (
403+
iterationRef.type === "member" &&
404+
isWellKnownArrayMethodName(iterationRef.name)
405+
) {
406+
const call = getCallExpressionFromCalleeExpression(
407+
iterationRef.node,
408+
)
409+
if (call) {
410+
for (const cgRef of iterateForArrayMethodOfStringMatchAll(
411+
call,
412+
iterationRef.name,
413+
argument,
414+
ctx,
415+
)) {
416+
useRet = true
417+
yield cgRef
418+
if (cgRef.type === "UnknownRef") {
419+
return
420+
}
421+
}
422+
}
423+
continue
424+
}
354425
if (Number.isNaN(Number(iterationRef.name))) {
355426
// Not aimed to iteration.
356427
continue
357428
}
358429
}
359430
for (const ref of iterationRef.extractPropertyReferences()) {
360-
if (hasNameRef(ref)) {
361-
if (ref.name === "groups") {
362-
for (const namedRef of ref.extractPropertyReferences()) {
363-
useRet = true
364-
yield getNamedArrayRef(namedRef)
365-
}
366-
} else {
367-
if (
368-
ref.name === "input" ||
369-
ref.name === "index" ||
370-
ref.name === "indices"
371-
) {
372-
continue
373-
}
374-
useRet = true
375-
yield getIndexArrayRef(ref)
376-
}
377-
} else {
431+
for (const cgRef of iterateForRegExpMatchArrayReference(ref)) {
378432
useRet = true
379-
yield {
380-
type: "UnknownRef",
381-
kind: "array",
382-
prop: ref,
433+
yield cgRef
434+
if (cgRef.type === "UnknownRef") {
435+
return
383436
}
384-
return
385437
}
386438
}
387439
}
@@ -420,28 +472,11 @@ function* iterateForExecResult(
420472
ctx: ExtractCapturingGroupReferencesContext,
421473
): Iterable<CapturingGroupReference> {
422474
for (const ref of extractPropertyReferences(node, ctx.context)) {
423-
if (hasNameRef(ref)) {
424-
if (ref.name === "groups") {
425-
for (const namedRef of ref.extractPropertyReferences()) {
426-
yield getNamedArrayRef(namedRef)
427-
}
428-
} else {
429-
if (
430-
ref.name === "input" ||
431-
ref.name === "index" ||
432-
ref.name === "indices"
433-
) {
434-
continue
435-
}
436-
yield getIndexArrayRef(ref)
437-
}
438-
} else {
439-
yield {
440-
type: "UnknownRef",
441-
kind: "array",
442-
prop: ref,
475+
for (const cgRef of iterateForRegExpMatchArrayReference(ref)) {
476+
yield cgRef
477+
if (cgRef.type === "UnknownRef") {
478+
return
443479
}
444-
return
445480
}
446481
}
447482
}
@@ -590,6 +625,75 @@ function* iterateForReplacerFunction(
590625
}
591626
}
592627

628+
/** Iterate the capturing group references for RegExpMatchArray reference. */
629+
function* iterateForRegExpMatchArrayReference(
630+
ref: PropertyReference,
631+
): Iterable<CapturingGroupReference> {
632+
if (hasNameRef(ref)) {
633+
if (ref.name === "groups") {
634+
for (const namedRef of ref.extractPropertyReferences()) {
635+
yield getNamedArrayRef(namedRef)
636+
}
637+
} else {
638+
if (
639+
ref.name === "input" ||
640+
ref.name === "index" ||
641+
ref.name === "indices"
642+
) {
643+
return
644+
}
645+
yield getIndexArrayRef(ref)
646+
}
647+
} else {
648+
yield {
649+
type: "UnknownRef",
650+
kind: "array",
651+
prop: ref,
652+
}
653+
}
654+
}
655+
656+
/** Iterate the capturing group references for Array method of String.prototype.matchAll(). */
657+
function* iterateForArrayMethodOfStringMatchAll(
658+
node: CallExpression,
659+
methodsName: ArrayMethodName,
660+
argument: Expression,
661+
ctx: ExtractCapturingGroupReferencesContext,
662+
): Iterable<CapturingGroupReference> {
663+
const arrayMethod = WELL_KNOWN_ARRAY_METHODS[methodsName]
664+
if (
665+
arrayMethod.elementParameters &&
666+
node.arguments[0] &&
667+
(node.arguments[0].type === "FunctionExpression" ||
668+
node.arguments[0].type === "ArrowFunctionExpression")
669+
) {
670+
const fnNode = node.arguments[0]
671+
for (const index of arrayMethod.elementParameters) {
672+
const param = fnNode.params[index]
673+
if (param) {
674+
for (const ref of extractPropertyReferencesForPattern(
675+
param,
676+
ctx.context,
677+
)) {
678+
yield* iterateForRegExpMatchArrayReference(ref)
679+
}
680+
}
681+
}
682+
}
683+
if (arrayMethod.result) {
684+
if (arrayMethod.result === "element") {
685+
for (const ref of extractPropertyReferences(node, ctx.context)) {
686+
yield* iterateForRegExpMatchArrayReference(ref)
687+
}
688+
} else if (
689+
arrayMethod.result === "array" ||
690+
arrayMethod.result === "iterator"
691+
) {
692+
yield* iterateForStringMatchAll(node, argument, ctx)
693+
}
694+
}
695+
}
696+
593697
/** Checks whether the given reference is a named reference. */
594698
function hasNameRef(
595699
ref: PropertyReference,
@@ -638,3 +742,23 @@ function getNamedArrayRef(
638742
prop: namedRef,
639743
}
640744
}
745+
746+
/** Gets the CallExpression from the given callee node. */
747+
function getCallExpressionFromCalleeExpression(
748+
expression: Expression,
749+
): CallExpression | null {
750+
const parent = getParent(expression)
751+
if (
752+
!parent ||
753+
parent.type !== "CallExpression" ||
754+
parent.callee !== expression
755+
) {
756+
return null
757+
}
758+
return parent
759+
}
760+
761+
/** Checks whether the given name is a well known array method name. */
762+
function isWellKnownArrayMethodName(name: string): name is ArrayMethodName {
763+
return Boolean(WELL_KNOWN_ARRAY_METHODS[name as ArrayMethodName])
764+
}

tests/lib/rules/no-unused-capturing-group.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,20 @@ tester.run("no-unused-capturing-group", rule as any, {
134134
String.raw`
135135
const matches = [...('2000-12-31 2000-12-31'.matchAll(/(\d{4})-(\d{2})-(\d{2})/g))]
136136
`,
137+
String.raw`
138+
const bs = [...'abc_abc'.matchAll(/a(b)/g)].map(m => m[1])
139+
`,
140+
String.raw`
141+
;[...'abc_abc'.matchAll(/a(b)(c)/g)].forEach(m => console.log(m[1], m[2]))
142+
`,
143+
String.raw`
144+
const filtered = [...'abc_abc_abb_acc'.matchAll(/(?<a>.)(?<b>.)(?<c>.)/g)].filter(m => m.groups.b === m.groups.c);
145+
console.log(filtered[0].groups.a);
146+
`,
147+
String.raw`
148+
const element = [...'abc_abc_abb_acc'.matchAll(/(?<a>.)(?<b>.)(?<c>.)/g)].find(m => m.groups.b === m.groups.c);
149+
console.log(element.groups.a);
150+
`,
137151
],
138152
invalid: [
139153
{
@@ -469,5 +483,52 @@ tester.run("no-unused-capturing-group", rule as any, {
469483
code: `'str'.replace(/(?<foo>\\w+)/, () => {});`,
470484
errors: ["Capturing group 'foo' is defined but never used."],
471485
},
486+
{
487+
code: String.raw`
488+
const bs = [...'abc_abc'.matchAll(/a(b)/g)].map(m => m[0])
489+
`,
490+
output: String.raw`
491+
const bs = [...'abc_abc'.matchAll(/a(?:b)/g)].map(m => m[0])
492+
`,
493+
options: [{ fixable: true }],
494+
errors: ["Capturing group number 1 is defined but never used."],
495+
},
496+
{
497+
code: String.raw`
498+
;[...'abc_abc'.matchAll(/a(b)(c)/g)].forEach(m => console.log(m[1]))
499+
`,
500+
output: String.raw`
501+
;[...'abc_abc'.matchAll(/a(b)(?:c)/g)].forEach(m => console.log(m[1]))
502+
`,
503+
options: [{ fixable: true }],
504+
errors: ["Capturing group number 2 is defined but never used."],
505+
},
506+
{
507+
code: String.raw`
508+
const filtered = [...'abc_abc_abb_acc'.matchAll(/(?<a>.)(?<b>.)(?<c>.)/g)].filter(m => m.groups.b === m.groups.c);
509+
console.log(filtered[0].groups.b);
510+
`,
511+
output: null,
512+
options: [{ fixable: true }],
513+
errors: ["Capturing group 'a' is defined but never used."],
514+
},
515+
{
516+
code: String.raw`
517+
const filtered = [...'abc_abc_abb_acc'.matchAll(/(?<a>.)(?<b>.)(?<c>.)/g)].filter(m => m.groups.a === m.groups.c);
518+
console.log(filtered[0].groups.a);
519+
`,
520+
output: null,
521+
options: [{ fixable: true }],
522+
errors: ["Capturing group 'b' is defined but never used."],
523+
},
524+
{
525+
code: String.raw`
526+
const element = [...'abc_abc_abb_acc'.matchAll(/(?<a>.)(?<b>.)(?<c>.)/g)].find(m => m.groups.a === m.groups.c);
527+
console.log(element.groups.a);
528+
`,
529+
output: null,
530+
options: [{ fixable: true }],
531+
errors: ["Capturing group 'b' is defined but never used."],
532+
},
472533
],
473534
})

0 commit comments

Comments
 (0)