import { CleanRegexRule , createRuleListener , getDocUrl } from "../rules-util" ; import { CharSet } from "refa" ; import { CharacterClass , CharacterSet , Element , LookaroundAssertion } from "regexpp/ast" ; import { assertionKindToMatchingDirection , getFirstCharAfter , invertMatchingDirection } from "../ast-util" ; import { isMatchAll , wordCharSet } from "../char-util" ; function getCharacters ( lookaround ) { if ( lookaround . alternatives . length === 1 ) { const alt = lookaround . alternatives [ 0 ] ; if ( alt . elements . length === 1 ) { const first = alt . elements [ 0 ] ; if ( first . type === "CharacterSet" || first . type === "CharacterClass" ) { return first ; } } } return null ; } export default { meta : { type : "suggestion" , docs : { description : "Prefer predefined assertions over equivalent lookarounds." , url : getDocUrl ( "prefer-predefined-assertion" ) , } , fixable : "code" , } , create ( context ) { return createRuleListener ( ( { visitAST , flags , replaceElement } ) => { const isWord = ( chars ) => chars . isSubsetOf ( wordCharSet ( flags ) ) ; const isNonWord = ( chars ) => chars . isDisjointWith ( wordCharSet ( flags ) ) ; function replaceWordAssertion ( node , wordNegated ) { const direction = assertionKindToMatchingDirection ( node . kind ) ; const dependsOn = [ ] ; /**
                 * Whether the lookaround is equivalent to (?!\w) / (?<!\w) or (?=\w) / (?<=\w)
                 */ let lookaroundNegated = node . negate ; if ( wordNegated ) { const after = getFirstCharAfter ( node , direction , flags ) ; dependsOn . push ( ... after . elements ) ; const hasNextCharacter = ! after . char . edge ; if ( hasNextCharacter ) { lookaroundNegated = ! lookaroundNegated ; } else { return ; } } const before = getFirstCharAfter ( node , invertMatchingDirection ( direction ) , flags ) ; dependsOn . push ( ... before . elements ) ; if ( before . char . edge ) { return ; } let otherNegated ; if ( isWord ( before . char . char ) ) { otherNegated = false ; } else if ( isNonWord ( before . char . char ) ) { otherNegated = true ; } else { return ; } if ( lookaroundNegated === otherNegated ) { context . report ( { message : "This assertion can be replaced with a negated word boundary assertion (\\B)." , ... replaceElement ( node , "\\B" , { dependsOn } ) , } ) ; } else { context . report ( { message : "This assertion can be replaced with a word boundary assertion (\\b)." , ... replaceElement ( node , "\\b" , { dependsOn } ) , } ) ; } } function replaceAllAssertion ( node , multiline = false ) { if ( ! node . negate ) { return ; } if ( flags . multiline === multiline ) { const replacement = node . kind === "lookahead" ? "$" : "^" ; context . report ( { message : `This assertion can be replaced with ${ replacement }.` , ... replaceElement ( node , replacement , { dependsOnFlags : true } ) , } ) ; } } visitAST ( { onAssertionEnter ( node ) { if ( node . kind !== "lookahead" && node . kind !== "lookbehind" ) { return ; } const chars = getCharacters ( node ) ; if ( chars === null ) { return ; } if ( chars . type === "CharacterSet" ) { if ( chars . kind === "word" ) { replaceWordAssertion ( node , chars . negate ) ; } else if ( chars . kind === "any" ) { if ( flags . dotAll ) { replaceAllAssertion ( node ) ; } else { replaceAllAssertion ( node , true ) ; } } else if ( chars . kind === "property" ) { if ( isMatchAll ( chars , flags ) ) { replaceAllAssertion ( node ) ; } } } else { if ( isMatchAll ( chars , flags ) ) { replaceAllAssertion ( node ) ; } } } , } ) ; } ) ; } , } as CleanRegexRule ;