import * as R from 'ramda' ; import { EmbeddedActionsParser , IToken , Lexer , TokenType , createToken } from 'chevrotain' ; import { PrereqTree } from '../../types/modules' ; import { Logger } from '../logger' ; import { AND_OR_REGEX , MODULE_REGEX , OPERATORS } from './constants' ; /**
 * Parses the string to build a tree of requirements for the module.
 * First it goes through a lexer to generate tokens, then a parser to build
 * the tree.
 *
 * Library used for lexing/parsing is Chevrotain:
 * https://github.com/SAP/chevrotain
 */ const Module = createToken ( { name : 'Module' , pattern : MODULE_REGEX , } ) ; const And = createToken ( { name : 'And' , pattern : 'and' , } ) ; const Or = createToken ( { name : 'Or' , pattern : 'or' , } ) ; const LeftBracket = createToken ( { name : 'LeftBracket' , pattern : /\(/ , } ) ; const RightBracket = createToken ( { name : 'RightBracket' , pattern : /\)/ , } ) ; const WhiteSpace = createToken ( { name : 'Whitespace' , pattern : /\s+/ , group : Lexer . SKIPPED , line_breaks : true , } ) ; const IrrelevantWord = createToken ( { name : 'IrrelevantWord' , pattern : /[^\s()]+/ , group : Lexer . SKIPPED , } ) ; const allTokens = [ WhiteSpace , Module , And , Or , LeftBracket , RightBracket , IrrelevantWord ] ; const ReqTreeLexer = new Lexer ( allTokens ) ; function generateAndBranch ( modules ) { const children = R . uniq ( modules ) ; return { and : children } ; } function generateOrBranch ( modules ) { const children = R . uniq ( modules ) ; return { or : children } ; } see https class ReqTreeParser extends EmbeddedActionsParser { parse : ( ) => PrereqTree ; orExpression : ( ) => PrereqTree ; andExpression : ( ) => PrereqTree ; parenthesisExpression : ( ) => PrereqTree ; atomicExpression : ( ) => PrereqTree ; constructor ( ) { super ( allTokens , { recoveryEnabled : true } ) ; this . parse = this . RULE ( 'parse' , ( ) => this . SUBRULE ( this . andExpression ) ) ; this . andExpression = this . RULE ( 'andExpression' , ( ) => { const value = [ ] ; value . push ( this . SUBRULE ( this . orExpression ) ) ; this . MANY ( ( ) => { this . CONSUME ( And ) ; value . push ( this . SUBRULE2 ( this . orExpression ) ) ; } ) ; return value . length === 1 ? value [ 0 ] : generateAndBranch ( value ) ; } ) ; this . orExpression = this . RULE ( 'orExpression' , ( ) => { const value = [ ] ; value . push ( this . SUBRULE ( this . atomicExpression ) ) ; this . MANY ( ( ) => { this . CONSUME ( Or ) ; value . push ( this . SUBRULE2 ( this . atomicExpression ) ) ; } ) ; return value . length === 1 ? value [ 0 ] : generateOrBranch ( value ) ; } ) ; this . atomicExpression = this . RULE ( 'atomicExpression' , ( ) => this . OR ( { DEF : [ { ALT : ( ) => this . SUBRULE ( this . parenthesisExpression ) } , { ALT : ( ) => this . CONSUME ( Module ) . image } , ] , ERR_MSG : 'a module or parenthesis expression' , } ) ) ; this . parenthesisExpression = this . RULE ( 'parenthesisExpression' , ( ) => { this . CONSUME ( LeftBracket ) ; const expValue = this . SUBRULE ( this . parse ) ; this . CONSUME ( RightBracket ) ; return expValue ; } ) ; this . performSelfAnalysis ( ) ; } canTokenTypeBeInsertedInRecovery ( tokClass ) { return tokClass !== Module ; } } export function cleanOperators ( tokens ) { const output = [ ] ; let temp = [ ] ; let bracketsCount = 0 ; tokens . forEach ( ( token ) => { const { image } = token ; if ( bracketsCount === 0 && image !== '(' && image !== ')' ) { output . push ( token ) ; return ; } temp . push ( token ) ; if ( image === '(' ) { bracketsCount += 1 ; } else if ( image === ')' ) { bracketsCount -= 1 ; if ( bracketsCount === 0 ) { const cleaned = cleanOperators ( temp . slice ( 1 , - 1 ) ) ; if ( cleaned . length ) { output . push ( R . head ( temp ) ! , ... cleaned , R . last ( temp ) ! ) ; } temp = [ ] ; } } } ) ; const findFirstRelevant = R . findIndex ( ( token ) => MODULE_REGEX . test ( token . image ) || token . image === '(' ) ; const findLastRelevant = R . findLastIndex ( ( token ) => MODULE_REGEX . test ( token . image ) || token . image === ')' ) ; const processedTokens = output . slice ( findFirstRelevant ( output ) , findLastRelevant ( output ) + 1 ) ; const removedDuplicates = processedTokens . filter ( ( item , pos , arr ) => { if ( pos === 0 || pos === arr . length - 1 ) return true ; return ! ( AND_OR_REGEX . test ( item . image ) && AND_OR_REGEX . test ( arr [ pos + 1 ] . image ) ) ; } ) ; const moduleTokens = [ ] ; const singularConjunction = removedDuplicates . reduce ( ( acc , token ) => { const { image } = token ; if ( image === 'and' || image === 'or' ) { if ( ! acc ) return token ; if ( acc . image !== image ) return false ; } return acc ; } , null ) ; for ( let i = 0 ; i < removedDuplicates . length ; i ++ ) { const token = removedDuplicates [ i ] ; const nextToken = removedDuplicates [ i + 1 ] ; const isModule = MODULE_REGEX . test ( token . image ) ; const isAnotherModule = nextToken && MODULE_REGEX . test ( nextToken . image ) ; moduleTokens . push ( token ) ; if ( singularConjunction && isModule && isAnotherModule ) { moduleTokens . push ( singularConjunction ) ; } } return moduleTokens ; } see __tests__ export default function parseString ( prerequisite , logger ) { const findModules = R . match ( new RegExp ( MODULE_REGEX , 'g' ) ) ; const moduleMatches = findModules ( prerequisite ) ; if ( moduleMatches . length === 0 ) { return null ; } if ( moduleMatches . length === 1 ) { return moduleMatches [ 0 ] ; } if ( ! prerequisite . includes ( OPERATORS . or ) && ! prerequisite . includes ( OPERATORS . and ) ) { return generateOrBranch ( moduleMatches ) ; } if ( ! prerequisite . includes ( OPERATORS . or ) ) { return generateAndBranch ( moduleMatches ) ; } if ( ! prerequisite . includes ( OPERATORS . and ) ) { return generateOrBranch ( moduleMatches ) ; } if ( R . match ( /\(/g , prerequisite ) . length !== R . match ( /\)/g , prerequisite ) . length ) { logger . info ( { prerequisite } , 'Brackets do not self enclose' ) ; } const lexingResult = ReqTreeLexer . tokenize ( prerequisite ) ; const tokens = cleanOperators ( lexingResult . tokens ) ; const parser = new ReqTreeParser ( ) ; parser . input = tokens ; const result = parser . parse ( ) ; if ( parser . errors . length > 0 ) { logger . info ( { prerequisite , errors : parser . errors } , 'Encountered parsing errors' ) ; } return result ; }