Skip to content

Commit d689253

Browse files
Avoid double reporting + flags API improvements (#306)
* Avoid double reporting for owned RegExp literals * Added test * Adjusted `no-useless-flag`
1 parent 2046aab commit d689253

File tree

5 files changed

+369
-130
lines changed

5 files changed

+369
-130
lines changed

lib/rules/no-useless-flag.ts

Lines changed: 98 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import type { RegExpContext } from "../utils"
2-
import { compositingVisitors, createRule, defineRegexpVisitor } from "../utils"
1+
import type { RegExpContext, RegExpContextForSource } from "../utils"
2+
import {
3+
getFlagsRange,
4+
compositingVisitors,
5+
createRule,
6+
defineRegexpVisitor,
7+
getFlagsLocation,
8+
} from "../utils"
39
import type {
410
CallExpression,
511
Expression,
@@ -45,9 +51,11 @@ type RegExpMethodKind =
4551
| "replaceAll"
4652

4753
class RegExpReference {
48-
public regExpContext: RegExpContext
54+
public readonly regExpContext: RegExpContext
4955

50-
public readonly defineNode: RegExpExpression
56+
public get defineNode(): RegExpExpression {
57+
return this.regExpContext.regexpNode
58+
}
5159

5260
public defineId?: CodePathId
5361

@@ -69,12 +77,8 @@ class RegExpReference {
6977
track: true,
7078
}
7179

72-
public constructor(
73-
regExpContext: RegExpContext,
74-
defineNode: RegExpExpression,
75-
) {
80+
public constructor(regExpContext: RegExpContext) {
7681
this.regExpContext = regExpContext
77-
this.defineNode = defineNode
7882
}
7983

8084
public addReadNode(node: Expression) {
@@ -165,31 +169,6 @@ class RegExpReference {
165169
}
166170
}
167171

168-
/**
169-
* Gets the location for reporting the flag.
170-
*/
171-
function getFlagLocation(
172-
context: Rule.RuleContext,
173-
node: RegExpExpression,
174-
flag: "i" | "m" | "s" | "g" | "y",
175-
) {
176-
const sourceCode = context.getSourceCode()
177-
if (node.type === "Literal") {
178-
const flagIndex =
179-
node.range![1] -
180-
node.regex.flags.length +
181-
node.regex.flags.indexOf(flag)
182-
return {
183-
start: sourceCode.getLocFromIndex(flagIndex),
184-
end: sourceCode.getLocFromIndex(flagIndex + 1),
185-
}
186-
}
187-
if (node.arguments.length >= 2) {
188-
return node.arguments[1].loc!
189-
}
190-
return context.getSourceCode().getTokenAfter(node.arguments[0])!.loc!
191-
}
192-
193172
/**
194173
* Returns a fixer that removes the given flag.
195174
*/
@@ -209,7 +188,13 @@ function fixRemoveFlag(
209188
function createUselessIgnoreCaseFlagVisitor(context: Rule.RuleContext) {
210189
return defineRegexpVisitor(context, {
211190
createVisitor(regExpContext: RegExpContext) {
212-
const { flags, regexpNode, toCharSet, ownsFlags } = regExpContext
191+
const {
192+
flags,
193+
regexpNode,
194+
toCharSet,
195+
ownsFlags,
196+
getFlagLocation,
197+
} = regExpContext
213198

214199
if (!flags.ignoreCase || !ownsFlags) {
215200
return {}
@@ -261,7 +246,7 @@ function createUselessIgnoreCaseFlagVisitor(context: Rule.RuleContext) {
261246
if (unnecessary) {
262247
context.report({
263248
node: regexpNode,
264-
loc: getFlagLocation(context, regexpNode, "i"),
249+
loc: getFlagLocation("i"),
265250
messageId: "uselessIgnoreCaseFlag",
266251
fix: fixRemoveFlag(regExpContext, "i"),
267252
})
@@ -278,7 +263,12 @@ function createUselessIgnoreCaseFlagVisitor(context: Rule.RuleContext) {
278263
function createUselessMultilineFlagVisitor(context: Rule.RuleContext) {
279264
return defineRegexpVisitor(context, {
280265
createVisitor(regExpContext: RegExpContext) {
281-
const { flags, regexpNode, ownsFlags } = regExpContext
266+
const {
267+
flags,
268+
regexpNode,
269+
ownsFlags,
270+
getFlagLocation,
271+
} = regExpContext
282272

283273
if (!flags.multiline || !ownsFlags) {
284274
return {}
@@ -294,7 +284,7 @@ function createUselessMultilineFlagVisitor(context: Rule.RuleContext) {
294284
if (unnecessary) {
295285
context.report({
296286
node: regexpNode,
297-
loc: getFlagLocation(context, regexpNode, "m"),
287+
loc: getFlagLocation("m"),
298288
messageId: "uselessMultilineFlag",
299289
fix: fixRemoveFlag(regExpContext, "m"),
300290
})
@@ -311,7 +301,12 @@ function createUselessMultilineFlagVisitor(context: Rule.RuleContext) {
311301
function createUselessDotAllFlagVisitor(context: Rule.RuleContext) {
312302
return defineRegexpVisitor(context, {
313303
createVisitor(regExpContext: RegExpContext) {
314-
const { flags, regexpNode, ownsFlags } = regExpContext
304+
const {
305+
flags,
306+
regexpNode,
307+
ownsFlags,
308+
getFlagLocation,
309+
} = regExpContext
315310

316311
if (!flags.dotAll || !ownsFlags) {
317312
return {}
@@ -327,7 +322,7 @@ function createUselessDotAllFlagVisitor(context: Rule.RuleContext) {
327322
if (unnecessary) {
328323
context.report({
329324
node: regexpNode,
330-
loc: getFlagLocation(context, regexpNode, "s"),
325+
loc: getFlagLocation("s"),
331326
messageId: "uselessDotAllFlag",
332327
fix: fixRemoveFlag(regExpContext, "s"),
333328
})
@@ -367,10 +362,12 @@ function createUselessGlobalFlagVisitor(
367362
regExpReference: RegExpReference,
368363
data: ReportData,
369364
) {
365+
const { getFlagLocation } = regExpReference.regExpContext
370366
const node = regExpReference.defineNode
367+
371368
context.report({
372369
node,
373-
loc: getFlagLocation(context, node, "g"),
370+
loc: getFlagLocation("g"),
374371
messageId:
375372
data.kind === ReportKind.usedOnlyInSplit
376373
? "uselessGlobalFlagForSplit"
@@ -492,10 +489,12 @@ function createUselessStickyFlagVisitor(
492489
regExpReference: RegExpReference,
493490
data: ReportData,
494491
) {
492+
const { getFlagLocation } = regExpReference.regExpContext
495493
const node = regExpReference.defineNode
494+
496495
context.report({
497496
node,
498-
loc: getFlagLocation(context, node, "y"),
497+
loc: getFlagLocation("y"),
499498
messageId: "uselessStickyFlag",
500499
fix: data.fixable
501500
? fixRemoveFlag(regExpReference.regExpContext, "y")
@@ -640,10 +639,7 @@ function createRegExpReferenceExtractVisitor(
640639
createVisitor(regExpContext: RegExpContext) {
641640
const { flags, regexpNode } = regExpContext
642641
if (flags[flag]) {
643-
const regExpReference = new RegExpReference(
644-
regExpContext,
645-
regexpNode,
646-
)
642+
const regExpReference = new RegExpReference(regExpContext)
647643
regExpReferenceList.push(regExpReference)
648644
regExpReferenceMap.set(regexpNode, regExpReference)
649645
for (const ref of extractExpressionReferences(
@@ -772,6 +768,56 @@ function createRegExpReferenceExtractVisitor(
772768
)
773769
}
774770

771+
/**
772+
* Create visitor for verify unnecessary flags of owned RegExp literals
773+
*/
774+
function createOwnedRegExpFlagsVisitor(context: Rule.RuleContext) {
775+
const sourceCode = context.getSourceCode()
776+
777+
/** Remove the flags of the given literal */
778+
function removeFlags(node: RegExpLiteral): void {
779+
// The u flag is relevant for parsing the literal, so
780+
// we can't just remove it and potentially create
781+
// invalid source code.
782+
const newFlags = node.regex.flags.replace(/[^u]+/g, "")
783+
if (newFlags === node.regex.flags) {
784+
return
785+
}
786+
787+
context.report({
788+
node,
789+
loc: getFlagsLocation(sourceCode, node, node),
790+
messageId: "uselessFlagsOwned",
791+
fix(fixer) {
792+
const range = getFlagsRange(node)
793+
return fixer.replaceTextRange(range, newFlags)
794+
},
795+
})
796+
}
797+
798+
return defineRegexpVisitor(context, {
799+
createSourceVisitor(regExpContext: RegExpContextForSource) {
800+
const { patternSource, regexpNode } = regExpContext
801+
802+
if (patternSource.isStringValue()) {
803+
// all regexp literals are used via `.source`
804+
patternSource.getOwnedRegExpLiterals().forEach(removeFlags)
805+
} else {
806+
// The source is copied from some other regex
807+
if (regexpNode.arguments.length >= 2) {
808+
// and the flags are given using the second parameter
809+
const ownedNode = patternSource.regexpValue?.ownedNode
810+
if (ownedNode) {
811+
removeFlags(ownedNode)
812+
}
813+
}
814+
}
815+
816+
return {}
817+
},
818+
})
819+
}
820+
775821
/**
776822
* Parse option
777823
*/
@@ -844,6 +890,8 @@ export default createRule("no-useless-flag", {
844890
"The 'g' flag is unnecessary because 'String.prototype.search' ignores the 'g' flag.",
845891
uselessStickyFlag:
846892
"The 'y' flag is unnecessary because 'String.prototype.split' ignores the 'y' flag.",
893+
uselessFlagsOwned:
894+
"The flags of this RegExp literal are useless because only the source of the regex is used.",
847895
},
848896
type: "suggestion", // "problem",
849897
},
@@ -881,6 +929,10 @@ export default createRule("no-useless-flag", {
881929
createUselessStickyFlagVisitor(context, strictTypes),
882930
)
883931
}
932+
visitor = compositingVisitors(
933+
visitor,
934+
createOwnedRegExpFlagsVisitor(context),
935+
)
884936
return visitor
885937
},
886938
})

lib/utils/ast-utils/pattern-source.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,30 +139,43 @@ class PatternSegment implements PatternRange {
139139
return this.start <= range.start && range.end <= this.end
140140
}
141141

142+
public getOwnedRegExpLiteral(): RegExpLiteral | null {
143+
if (isRegexpLiteral(this.node)) {
144+
return this.node
145+
}
146+
147+
// e.g. /foo/.source
148+
if (
149+
this.node.type === "MemberExpression" &&
150+
this.node.object.type !== "Super" &&
151+
isRegexpLiteral(this.node.object) &&
152+
getPropertyName(this.node) === "source"
153+
) {
154+
return this.node.object
155+
}
156+
157+
return null
158+
}
159+
142160
public getReplaceRange(range: PatternRange): PatternReplaceRange | null {
143161
if (!this.contains(range)) {
144162
return null
145163
}
146164

147-
if (this.node.type === "Literal") {
148-
// This will cover string literals and RegExp literals
165+
const regexp = this.getOwnedRegExpLiteral()
166+
if (regexp) {
149167
return PatternReplaceRange.fromLiteral(
150-
this.node,
168+
regexp,
151169
this.sourceCode,
152170
this,
153171
range,
154172
)
155173
}
156174

157-
// e.g. /foo/.source
158-
if (
159-
this.node.type === "MemberExpression" &&
160-
this.node.object.type !== "Super" &&
161-
isRegexpLiteral(this.node.object) &&
162-
getPropertyName(this.node) === "source"
163-
) {
175+
if (this.node.type === "Literal") {
176+
// This will cover string literals
164177
return PatternReplaceRange.fromLiteral(
165-
this.node.object,
178+
this.node,
166179
this.sourceCode,
167180
this,
168181
range,
@@ -394,6 +407,25 @@ export class PatternSource {
394407
public getAstLocation(range: PatternRange): AST.SourceLocation {
395408
return astRangeToLocation(this.sourceCode, this.getAstRange(range))
396409
}
410+
411+
/**
412+
* Returns all RegExp literals nodes that are owned by this pattern.
413+
*
414+
* This means that the returned RegExp literals are only used to create
415+
* this pattern and for nothing else.
416+
*/
417+
public getOwnedRegExpLiterals(): readonly RegExpLiteral[] {
418+
const literals: RegExpLiteral[] = []
419+
420+
for (const segment of this.segments) {
421+
const regexp = segment.getOwnedRegExpLiteral()
422+
if (regexp) {
423+
literals.push(regexp)
424+
}
425+
}
426+
427+
return literals
428+
}
397429
}
398430

399431
/**

0 commit comments

Comments
 (0)