import { CleanRegexRule , createRuleListener , getDocUrl } from "../rules-util" ; import { mention } from "../format" ; import { JS } from "refa" ; import { getFirstCharAfter , getFirstCharConsumedBy , getLengthRange , hasSomeDescendant , isPotentiallyEmpty , assertionKindToMatchingDirection , } from "../ast-util" ; import { EdgeAssertion , LookaroundAssertion , WordBoundaryAssertion } from "regexpp/ast" ; import { assertNever } from "../util" ; import { wordCharSet } from "../char-util" ; export default { meta : { type : "problem" , docs : { description : "Disallow assertions that are known to always accept (or reject)." , url : getDocUrl ( "no-unnecessary-assertions" ) , } , } , create ( context ) { return createRuleListener ( ( { visitAST , flags , reportElement } ) => { function checkStartOrEnd ( node ) { const direction = assertionKindToMatchingDirection ( node . kind ) ; const next = getFirstCharAfter ( node , direction , flags ) ; const followed = node . kind === "end" ? "followed" : "preceded" ; if ( ! next . char . edge ) { if ( ! flags . multiline ) { context . report ( { message : `${ mention ( node ) } will always reject because it is ${ followed } by a character.` , ... reportElement ( node ) , } ) ; } else { const oldDotAll = flags . dotAll ; flags . dotAll = false ; const dot = JS . createCharSet ( [ { kind : "any" } ] , flags ) ; flags . dotAll = oldDotAll ; if ( next . char . char . isSubsetOf ( dot ) ) { context . report ( { message : `${ mention ( node ) } will always reject because it is ${ followed } by a non-line-terminator character.` , ... reportElement ( node ) , } ) ; } else if ( next . char . char . isDisjointWith ( dot ) ) { context . report ( { message : `${ mention ( node ) } will always accept because it is ${ followed } by a line-terminator character.` , ... reportElement ( node ) , } ) ; } } } } function checkWordBoundary ( node ) { const word = wordCharSet ( flags ) ; const next = getFirstCharAfter ( node , "ltr" , flags ) ; const prev = getFirstCharAfter ( node , "rtl" , flags ) ; if ( prev . char . edge || next . char . edge ) { return ; } const nextIsWord = next . char . char . isSubsetOf ( word ) ; const prevIsWord = prev . char . char . isSubsetOf ( word ) ; const nextIsNonWord = next . char . char . isDisjointWith ( word ) ; const prevIsNonWord = prev . char . char . isDisjointWith ( word ) ; const accept = node . negate ? "reject" : "accept" ; const reject = node . negate ? "accept" : "reject" ; if ( prevIsNonWord ) { if ( nextIsWord ) { context . report ( { message : `${ mention ( node ) } will always ${ accept } because it is preceded by a non-word character and followed by a word character.` , ... reportElement ( node ) , } ) ; } if ( nextIsNonWord ) { context . report ( { message : `${ mention ( node ) } will always ${ reject } because it is preceded by a non-word character and followed by a non-word character.` , ... reportElement ( node ) , } ) ; } } if ( prevIsWord ) { if ( nextIsNonWord ) { context . report ( { message : `${ mention ( node ) } will always ${ accept } because it is preceded by a word character and followed by a non-word character.` , ... reportElement ( node ) , } ) ; } if ( nextIsWord ) { context . report ( { message : `${ mention ( node ) } will always ${ reject } because it is preceded by a word character and followed by a word character.` , ... reportElement ( node ) , } ) ; } } } function checkLookaround ( node ) { if ( isPotentiallyEmpty ( node . alternatives ) ) { return ; } const direction = assertionKindToMatchingDirection ( node . kind ) ; const after = getFirstCharAfter ( node , direction , flags ) ; if ( after . char . edge ) { return ; } const firstOf = getFirstCharConsumedBy ( node . alternatives , direction , flags ) ; if ( firstOf . empty ) { return ; } const accept = node . negate ? "reject" : "accept" ; const reject = node . negate ? "accept" : "reject" ; const nodeName = `${ node . negate ? "negative " : "" }${ node . kind } ${ mention ( node ) }` ; if ( after . char . char . isDisjointWith ( firstOf . char ) ) { context . report ( { message : `The ${ nodeName } will always ${ reject }.` , ... reportElement ( node ) , } ) ; } if ( ! hasSomeDescendant ( node , d => d !== node && d . type === "Assertion" ) ) { const range = getLengthRange ( node . alternatives ) ; if ( range && range . max === 1 ) { if ( firstOf . exact && after . char . char . isSubsetOf ( firstOf . char ) ) { context . report ( { message : `The ${ nodeName } will always ${ accept }.` , ... reportElement ( node ) , } ) ; } } } } visitAST ( { onAssertionEnter ( node ) { switch ( node . kind ) { case "start" : case "end" : checkStartOrEnd ( node ) ; break ; case "word" : checkWordBoundary ( node ) ; break ; case "lookahead" : case "lookbehind" : checkLookaround ( node ) ; break ; default : throw assertNever ( node ) ; } } , } ) ; } ) ; } , } as CleanRegexRule ;