import { RegExpParser } from "regexpp" ; import { Character , CharacterClass , CharacterSet , Flags } from "regexpp/ast" ; import { mentionCharElement } from "../format" ; import { CleanRegexRule , createRuleListener , getDocUrl } from "../rules-util" ; import { toCharSet } from "../char-util" ; type LiteralEscapeMode = "require" | "disallow" | "allow" ; type LiteralContext = "inside" | "outside" | "all" ; interface LiteralEscape { context : LiteralContext ; matches : ( character : number ) => boolean ; escape : LiteralEscapeMode ; name : string | undefined ; } function processLiteralEscapeOption ( literalOptions ) { const escapes = [ ] ; const parser = new RegExpParser ( { ecmaVersion : 2020 } ) ; const flags = { unicode : true } ; for ( let index = 0 ; index < literalOptions . length ; index ++ ) { const element = literalOptions [ index ] ; const characterClassRaw = element [ "characters" ] ; const errorMessage = `The characters pattern ${ JSON . stringify ( characterClassRaw ) } of ${ element . name } is not a valid character, character class, or character set.` ; let ast ; try { ast = parser . parsePattern ( characterClassRaw , undefined , undefined , true ) ; } catch ( error ) { throw new SyntaxError ( errorMessage ) ; } let char = undefined ; if ( ast . alternatives . length === 1 ) { const alt = ast . alternatives [ 0 ] ; if ( alt . elements . length === 1 ) { const e = alt . elements [ 0 ] ; if ( e . type === "CharacterClass" || e . type === "Character" || e . type === "CharacterSet" ) { char = e ; } } } if ( char === undefined ) { throw new Error ( errorMessage ) ; } const set = toCharSet ( char , flags ) ; let matches ; if ( set . isEmpty ) { matches = ( ) => false ; } else if ( set . isAll ) { matches = ( ) => true ; } else if ( set . ranges . length === 1 ) { const { min , max } = set . ranges [ 0 ] ; matches = c => min <= c && c <= max ; } else { matches = c => set . has ( c ) ; } escapes . push ( { matches , context : element . context || "all" , escape : element . escape , name : element . name || `rules[${ index }]` , } ) ; } return escapes ; } function isFirstInCharClass ( char ) { const parent = char . parent ; switch ( parent . type ) { case "CharacterClass" : return parent . elements [ 0 ] === char ; case "CharacterClassRange" : if ( parent . min !== char ) { return false ; } return parent . parent . elements [ 0 ] === parent ; default : throw new Error ( "The given character is not part of a character class." ) ; } } function isLastInCharClass ( char ) { const parent = char . parent ; switch ( parent . type ) { case "CharacterClass" : return parent . elements [ parent . elements . length - 1 ] === char ; case "CharacterClassRange" : if ( parent . max !== char ) { return false ; } return parent . parent . elements [ parent . parent . elements . length - 1 ] === parent ; default : throw new Error ( "The given character is not part of a character class." ) ; } } const SYNTAX_CHARACTERS = new Set ( [ ... "^$\\.*+?()[]{}|" ] ) ; /**
 * Returns whether the character can be escaped and unescaped at will without changing the meaning of the pattern.
 */ function canToggleLiteralEscape ( char , flags ) { const inside = char . parent . type === "CharacterClass" || char . parent . type === "CharacterClassRange" ; const { unicode } = flags ; if ( char . raw === "\\" ) { return false ; } if ( char . raw . startsWith ( "\\" ) ) { const charString = char . raw . substr ( 1 ) ; if ( charString === "\\" ) { return false ; } if ( charString === String . fromCodePoint ( char . value ) ) { if ( inside ) { if ( char . value === "]" . charCodeAt ( 0 ) ) { return false ; } else if ( char . value === "-" . charCodeAt ( 0 ) ) { return isFirstInCharClass ( char ) || isLastInCharClass ( char ) ; } else if ( char . value === "^" . charCodeAt ( 0 ) ) { return ! isFirstInCharClass ( char ) ; } else { return true ; } } else { if ( unicode ) { return false ; } if ( charString === "/" ) { return false ; } if ( SYNTAX_CHARACTERS . has ( charString ) ) { return false ; } else { return true ; } } } else { return false ; } } else { if ( unicode ) { if ( inside && ( SYNTAX_CHARACTERS . has ( char . raw ) || char . raw === "/" ) ) { return true ; } else { return false ; } } else { if ( /^[a-zA-Z0-9]$/ . test ( char . raw ) ) { return false ; } else { return true ; } } } } function charsOf ( string ) { const chars = [ ... string ] . map ( s => { const cp = s . codePointAt ( 0 ) ; if ( cp === undefined ) { throw new Error ( ) ; } return cp ; } ) ; if ( chars . length === 0 ) { return ( ) => false ; } else if ( chars . length === 1 ) { const char = chars [ 0 ] ; return c => c == char ; } else { const set = new Set ( chars ) ; return c => set . has ( c ) ; } } const standardRules = [ { name : "standard:opening-square-bracket" , escape : "require" , context : "inside" , matches : charsOf ( "[" ) , } , { name : "standard:closing-square-bracket" , escape : "require" , context : "outside" , matches : charsOf ( "]" ) , } , { name : "standard:curly-braces" , escape : "require" , context : "outside" , matches : charsOf ( "{}" ) , } , { name : "standard:all" , escape : "disallow" , context : "all" , matches : ( ) => true , } , ] ; export default { meta : { type : "suggestion" , docs : { description : "How to handle identity escapes." , url : getDocUrl ( "identity-escape" ) , } , fixable : "code" , schema : [ { type : "object" , properties : { rules : { type : "array" , items : { type : "object" , properties : { name : { type : "string" , } , escape : { type : "string" , enum : [ "require" , "disallow" , "allow" ] , } , context : { type : "string" , enum : [ "inside" , "outside" , "all" ] , } , characters : { type : "string" , } , } , required : [ "escape" , "characters" ] , additionalProperties : false , } , } , } , additionalProperties : false , } , ] , } , create ( context ) { const options = context . options [ 0 ] || { } ; const literalEscaped = [ ] ; literalEscaped . push ( ... processLiteralEscapeOption ( options [ "rules" ] || [ ] ) ) ; literalEscaped . push ( ... standardRules ) ; return createRuleListener ( ( { visitAST , flags , replaceElement } ) => { visitAST ( { onCharacterEnter ( node ) { if ( ! canToggleLiteralEscape ( node , flags ) ) { return ; } const inside = node . parent . type === "CharacterClass" || node . parent . type === "CharacterClassRange" ; const isEscaped = node . raw . startsWith ( "\\" ) ; const toggled = isEscaped ? node . raw . substr ( 1 ) : "\\" + node . raw ; for ( const escapeRule of literalEscaped ) { if ( ( escapeRule . context === "inside" && ! inside ) || ( escapeRule . context === "outside" && inside ) ) { continue ; } if ( escapeRule . matches ( node . value ) ) { const causedBy = escapeRule . name ? ` (${ escapeRule . name })` : "" ; switch ( escapeRule . escape ) { case "allow" : return ; case "disallow" : if ( isEscaped ) { context . report ( { message : `The character ${ mentionCharElement ( node ) } should be unescaped${ causedBy }.` , ... replaceElement ( node , toggled ) , } ) ; } return ; case "require" : if ( ! isEscaped ) { context . report ( { message : `The character ${ mentionCharElement ( node ) } should be escaped${ causedBy }.` , ... replaceElement ( node , toggled ) , } ) ; } return ; default : throw new Error ( `Invalid escape option "${ escapeRule . escape }".` ) ; } } } } , } ) ; } ) ; } , } as CleanRegexRule ;