@@ -146,6 +146,8 @@ module.exports = {
146146 messages : {
147147 unexpectedRegExp : "Use a regular expression literal instead of the 'RegExp' constructor." ,
148148 replaceWithLiteral : "Replace with an equivalent regular expression literal." ,
149+ replaceWithLiteralAndFlags : "Replace with an equivalent regular expression literal with flags '{{ flags }}'." ,
150+ replaceWithIntendedLiteralAndFlags : "Replace with a regular expression literal with flags '{{ flags }}'." ,
149151 unexpectedRedundantRegExp : "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor." ,
150152 unexpectedRedundantRegExpWithFlags : "Use regular expression literal with flags instead of the 'RegExp' constructor."
151153 }
@@ -258,6 +260,8 @@ module.exports = {
258260 return Math . min ( ecmaVersion , REGEXPP_LATEST_ECMA_VERSION ) ;
259261 }
260262
263+ const regexppEcmaVersion = getRegexppEcmaVersion ( context . languageOptions . ecmaVersion ) ;
264+
261265 /**
262266 * Makes a character escaped or else returns null.
263267 * @param {string } character The character to escape.
@@ -293,6 +297,83 @@ module.exports = {
293297 }
294298 }
295299
300+ /**
301+ * Checks whether the given regex and flags are valid for the ecma version or not.
302+ * @param {string } pattern The regex pattern to check.
303+ * @param {string | undefined } flags The regex flags to check.
304+ * @returns {boolean } True if the given regex pattern and flags are valid for the ecma version.
305+ */
306+ function isValidRegexForEcmaVersion ( pattern , flags ) {
307+ const validator = new RegExpValidator ( { ecmaVersion : regexppEcmaVersion } ) ;
308+
309+ try {
310+ validator . validatePattern ( pattern , 0 , pattern . length , flags ? flags . includes ( "u" ) : false ) ;
311+ if ( flags ) {
312+ validator . validateFlags ( flags ) ;
313+ }
314+ return true ;
315+ } catch {
316+ return false ;
317+ }
318+ }
319+
320+ /**
321+ * Checks whether two given regex flags contain the same flags or not.
322+ * @param {string } flagsA The regex flags.
323+ * @param {string } flagsB The regex flags.
324+ * @returns {boolean } True if two regex flags contain same flags.
325+ */
326+ function areFlagsEqual ( flagsA , flagsB ) {
327+ return [ ...flagsA ] . sort ( ) . join ( "" ) === [ ...flagsB ] . sort ( ) . join ( "" ) ;
328+ }
329+
330+
331+ /**
332+ * Merges two regex flags.
333+ * @param {string } flagsA The regex flags.
334+ * @param {string } flagsB The regex flags.
335+ * @returns {string } The merged regex flags.
336+ */
337+ function mergeRegexFlags ( flagsA , flagsB ) {
338+ const flagsSet = new Set ( [
339+ ...flagsA ,
340+ ...flagsB
341+ ] ) ;
342+
343+ return [ ...flagsSet ] . join ( "" ) ;
344+ }
345+
346+ /**
347+ * Checks whether a give node can be fixed to the given regex pattern and flags.
348+ * @param {ASTNode } node The node to check.
349+ * @param {string } pattern The regex pattern to check.
350+ * @param {string } flags The regex flags
351+ * @returns {boolean } True if a node can be fixed to the given regex pattern and flags.
352+ */
353+ function canFixTo ( node , pattern , flags ) {
354+ const tokenBefore = sourceCode . getTokenBefore ( node ) ;
355+
356+ return sourceCode . getCommentsInside ( node ) . length === 0 &&
357+ ( ! tokenBefore || validPrecedingTokens . has ( tokenBefore . value ) ) &&
358+ isValidRegexForEcmaVersion ( pattern , flags ) ;
359+ }
360+
361+ /**
362+ * Returns a safe output code considering the before and after tokens.
363+ * @param {ASTNode } node The regex node.
364+ * @param {string } newRegExpValue The new regex expression value.
365+ * @returns {string } The output code.
366+ */
367+ function getSafeOutput ( node , newRegExpValue ) {
368+ const tokenBefore = sourceCode . getTokenBefore ( node ) ;
369+ const tokenAfter = sourceCode . getTokenAfter ( node ) ;
370+
371+ return ( tokenBefore && ! canTokensBeAdjacent ( tokenBefore , newRegExpValue ) && tokenBefore . range [ 1 ] === node . range [ 0 ] ? " " : "" ) +
372+ newRegExpValue +
373+ ( tokenAfter && ! canTokensBeAdjacent ( newRegExpValue , tokenAfter ) && node . range [ 1 ] === tokenAfter . range [ 0 ] ? " " : "" ) ;
374+
375+ }
376+
296377 return {
297378 Program ( ) {
298379 const scope = context . getScope ( ) ;
@@ -306,10 +387,69 @@ module.exports = {
306387
307388 for ( const { node } of tracker . iterateGlobalReferences ( traceMap ) ) {
308389 if ( disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral ( node ) ) {
390+ const regexNode = node . arguments [ 0 ] ;
391+
309392 if ( node . arguments . length === 2 ) {
310- context . report ( { node, messageId : "unexpectedRedundantRegExpWithFlags" } ) ;
393+ const suggests = [ ] ;
394+
395+ const argFlags = getStringValue ( node . arguments [ 1 ] ) || "" ;
396+
397+ if ( canFixTo ( node , regexNode . regex . pattern , argFlags ) ) {
398+ suggests . push ( {
399+ messageId : "replaceWithLiteralAndFlags" ,
400+ pattern : regexNode . regex . pattern ,
401+ flags : argFlags
402+ } ) ;
403+ }
404+
405+ const literalFlags = regexNode . regex . flags || "" ;
406+ const mergedFlags = mergeRegexFlags ( literalFlags , argFlags ) ;
407+
408+ if (
409+ ! areFlagsEqual ( mergedFlags , argFlags ) &&
410+ canFixTo ( node , regexNode . regex . pattern , mergedFlags )
411+ ) {
412+ suggests . push ( {
413+ messageId : "replaceWithIntendedLiteralAndFlags" ,
414+ pattern : regexNode . regex . pattern ,
415+ flags : mergedFlags
416+ } ) ;
417+ }
418+
419+ context . report ( {
420+ node,
421+ messageId : "unexpectedRedundantRegExpWithFlags" ,
422+ suggest : suggests . map ( ( { flags, pattern, messageId } ) => ( {
423+ messageId,
424+ data : {
425+ flags
426+ } ,
427+ fix ( fixer ) {
428+ return fixer . replaceText ( node , getSafeOutput ( node , `/${ pattern } /${ flags } ` ) ) ;
429+ }
430+ } ) )
431+ } ) ;
311432 } else {
312- context . report ( { node, messageId : "unexpectedRedundantRegExp" } ) ;
433+ const outputs = [ ] ;
434+
435+ if ( canFixTo ( node , regexNode . regex . pattern , regexNode . regex . flags ) ) {
436+ outputs . push ( sourceCode . getText ( regexNode ) ) ;
437+ }
438+
439+
440+ context . report ( {
441+ node,
442+ messageId : "unexpectedRedundantRegExp" ,
443+ suggest : outputs . map ( output => ( {
444+ messageId : "replaceWithLiteral" ,
445+ fix ( fixer ) {
446+ return fixer . replaceText (
447+ node ,
448+ getSafeOutput ( node , output )
449+ ) ;
450+ }
451+ } ) )
452+ } ) ;
313453 }
314454 } else if ( hasOnlyStaticStringArguments ( node ) ) {
315455 let regexContent = getStringValue ( node . arguments [ 0 ] ) ;
@@ -320,32 +460,14 @@ module.exports = {
320460 flags = getStringValue ( node . arguments [ 1 ] ) ;
321461 }
322462
323- const regexppEcmaVersion = getRegexppEcmaVersion ( context . languageOptions . ecmaVersion ) ;
324- const RegExpValidatorInstance = new RegExpValidator ( { ecmaVersion : regexppEcmaVersion } ) ;
325-
326- try {
327- RegExpValidatorInstance . validatePattern ( regexContent , 0 , regexContent . length , flags ? flags . includes ( "u" ) : false ) ;
328- if ( flags ) {
329- RegExpValidatorInstance . validateFlags ( flags ) ;
330- }
331- } catch {
332- noFix = true ;
333- }
334-
335- const tokenBefore = sourceCode . getTokenBefore ( node ) ;
336-
337- if ( tokenBefore && ! validPrecedingTokens . has ( tokenBefore . value ) ) {
463+ if ( ! canFixTo ( node , regexContent , flags ) ) {
338464 noFix = true ;
339465 }
340466
341467 if ( ! / ^ [ - a - z A - Z 0 - 9 \\ [ \] ( ) { } \t \r \n \v \f ! @ # $ % ^ & * + ^ _ = / ~ ` . > < ? , ' " | : ; ] * $ / u. test ( regexContent ) ) {
342468 noFix = true ;
343469 }
344470
345- if ( sourceCode . getCommentsInside ( node ) . length > 0 ) {
346- noFix = true ;
347- }
348-
349471 if ( regexContent && ! noFix ) {
350472 let charIncrease = 0 ;
351473
@@ -377,14 +499,7 @@ module.exports = {
377499 suggest : noFix ? [ ] : [ {
378500 messageId : "replaceWithLiteral" ,
379501 fix ( fixer ) {
380- const tokenAfter = sourceCode . getTokenAfter ( node ) ;
381-
382- return fixer . replaceText (
383- node ,
384- ( tokenBefore && ! canTokensBeAdjacent ( tokenBefore , newRegExpValue ) && tokenBefore . range [ 1 ] === node . range [ 0 ] ? " " : "" ) +
385- newRegExpValue +
386- ( tokenAfter && ! canTokensBeAdjacent ( newRegExpValue , tokenAfter ) && node . range [ 1 ] === tokenAfter . range [ 0 ] ? " " : "" )
387- ) ;
502+ return fixer . replaceText ( node , getSafeOutput ( node , newRegExpValue ) ) ;
388503 }
389504 } ]
390505 } ) ;
0 commit comments