@@ -16,6 +16,7 @@ import {
1616 findVariable ,
1717} from "eslint-utils"
1818import type { Rule , AST , SourceCode } from "eslint"
19+ import { parseStringTokens } from "./string-literal-parser"
1920export * from "./unicode"
2021
2122type RegexpRule = {
@@ -184,13 +185,21 @@ function buildRegexpVisitor(
184185 }
185186 } )
186187 } ,
188+ // eslint-disable-next-line complexity -- X(
187189 Program ( ) {
188190 const scope = context . getScope ( )
189191 const tracker = new ReferenceTracker ( scope )
190192
191193 // Iterate calls of RegExp.
192194 // E.g., `new RegExp()`, `RegExp()`, `new window.RegExp()`,
193195 // `const {RegExp: a} = window; new a()`, etc...
196+ const regexpDataList : {
197+ newOrCall : ESTree . NewExpression | ESTree . CallExpression
198+ patternNode : ESTree . Expression
199+ pattern : string | null
200+ flagsNode : ESTree . Expression | ESTree . SpreadElement | undefined
201+ flags : string | null
202+ } [ ] = [ ]
194203 for ( const { node } of tracker . iterateGlobalReferences ( {
195204 RegExp : { [ CALL ] : true , [ CONSTRUCT ] : true } ,
196205 } ) ) {
@@ -202,8 +211,19 @@ function buildRegexpVisitor(
202211 continue
203212 }
204213 const pattern = getStringIfConstant ( patternNode , scope )
205- const flags = getStringIfConstant ( flagsNode , scope )
214+ const flags = flagsNode
215+ ? getStringIfConstant ( flagsNode , scope )
216+ : null
206217
218+ regexpDataList . push ( {
219+ newOrCall,
220+ patternNode,
221+ pattern,
222+ flagsNode,
223+ flags,
224+ } )
225+ }
226+ for ( const { patternNode, pattern, flags } of regexpDataList ) {
207227 if ( typeof pattern === "string" ) {
208228 let verifyPatternNode = patternNode
209229 if ( patternNode . type === "Identifier" ) {
@@ -219,7 +239,30 @@ function buildRegexpVisitor(
219239 def . node . init &&
220240 def . node . init . type === "Literal"
221241 ) {
222- verifyPatternNode = def . node . init
242+ let useInit = false
243+ if ( variable . references . length > 2 ) {
244+ if (
245+ variable . references . every ( ( ref ) => {
246+ if ( ref . isWriteOnly ( ) ) {
247+ return true
248+ }
249+ return regexpDataList . some (
250+ ( r ) =>
251+ r . patternNode ===
252+ ref . identifier &&
253+ r . flags === flags ,
254+ )
255+ } )
256+ ) {
257+ useInit = true
258+ }
259+ } else {
260+ useInit = true
261+ }
262+
263+ if ( useInit ) {
264+ verifyPatternNode = def . node . init
265+ }
223266 }
224267 }
225268 }
@@ -261,12 +304,53 @@ export function getRegexpRange(
261304 sourceCode : SourceCode ,
262305 node : ESTree . Expression ,
263306 regexpNode : RegExpNode ,
307+ offsets ?: [ number , number ] ,
264308) : AST . Range | null {
265- if ( ! availableRegexpLocation ( sourceCode , node ) ) {
266- return null
309+ const startOffset = regexpNode . start + ( offsets ?. [ 0 ] ?? 0 )
310+ const endOffset = regexpNode . end + ( offsets ?. [ 1 ] ?? 0 )
311+ if ( isRegexpLiteral ( node ) ) {
312+ const nodeStart = node . range ! [ 0 ] + 1
313+ return [ nodeStart + startOffset , nodeStart + endOffset ]
267314 }
268- const nodeStart = node . range ! [ 0 ] + 1
269- return [ nodeStart + regexpNode . start , nodeStart + regexpNode . end ]
315+ if ( isStringLiteral ( node ) ) {
316+ let start : number | null = null
317+ let end : number | null = null
318+ try {
319+ const sourceText = sourceCode . text . slice (
320+ node . range ! [ 0 ] + 1 ,
321+ node . range ! [ 1 ] - 1 ,
322+ )
323+ let startIndex = 0
324+ for ( const t of parseStringTokens ( sourceText ) ) {
325+ const endIndex = startIndex + t . value . length
326+
327+ if (
328+ start == null &&
329+ startIndex <= startOffset &&
330+ startOffset < endIndex
331+ ) {
332+ start = t . range [ 0 ]
333+ }
334+ if (
335+ start != null &&
336+ end == null &&
337+ startIndex < endOffset &&
338+ endOffset <= endIndex
339+ ) {
340+ end = t . range [ 1 ]
341+ break
342+ }
343+ startIndex = endIndex
344+ }
345+ if ( start != null && end != null ) {
346+ const nodeStart = node . range ! [ 0 ] + 1
347+ return [ nodeStart + start , nodeStart + end ]
348+ }
349+ } catch {
350+ // ignore
351+ }
352+ }
353+ return null
270354}
271355
272356/**
@@ -300,28 +384,31 @@ export function getRegexpLocation(
300384}
301385
302386/**
303- * Check if the location of the regular expression node is available.
304- * @param sourceCode The ESLint source code instance.
305- * @param node The node to check.
306- * @returns `true` if the location of the regular expression node is available.
387+ * Check if the given expression node is regexp literal.
307388 */
308- export function availableRegexpLocation (
309- sourceCode : SourceCode ,
389+ function isRegexpLiteral (
310390 node : ESTree . Expression ,
311- ) : boolean {
391+ ) : node is ESTree . RegExpLiteral {
312392 if ( node . type !== "Literal" ) {
313393 return false
314394 }
315395 if ( ! ( node as ESTree . RegExpLiteral ) . regex ) {
316- if ( typeof node . value !== "string" ) {
317- return false
318- }
319- if (
320- sourceCode . text . slice ( node . range ! [ 0 ] + 1 , node . range ! [ 1 ] - 1 ) !==
321- node . value
322- ) {
323- return false
324- }
396+ return false
397+ }
398+ return true
399+ }
400+
401+ /**
402+ * Check if the given expression node is string literal.
403+ */
404+ function isStringLiteral (
405+ node : ESTree . Expression ,
406+ ) : node is ESTree . Literal & { value : string } {
407+ if ( node . type !== "Literal" ) {
408+ return false
409+ }
410+ if ( typeof node . value !== "string" ) {
411+ return false
325412 }
326413 return true
327414}
0 commit comments