cucumber-tag-expressions
Version:
Cucumber Tag Expression parser
245 lines (217 loc) • 5.57 kB
text/typescript
const OPERAND = 'operand'
const OPERATOR = 'operator'
const PREC: { [key: string]: number } = {
'(': -2,
')': -1,
or: 0,
and: 1,
not: 2,
}
const ASSOC: { [key: string]: string } = {
or: 'left',
and: 'left',
not: 'right',
}
/**
* Parses infix boolean expression (using Dijkstra's Shunting Yard algorithm)
* and builds a tree of expressions. The root node of the expression is returned.
*
* This expression can be evaluated by passing in an array of literals that resolve to true
*/
export default function parse(infix: string): Node {
const tokens = tokenize(infix)
if (tokens.length === 0) {
return new True()
}
const expressions: Node[] = []
const operators: string[] = []
let expectedTokenType = OPERAND
tokens.forEach(function(token) {
if (isUnary(token)) {
check(expectedTokenType, OPERAND)
operators.push(token)
expectedTokenType = OPERAND
} else if (isBinary(token)) {
check(expectedTokenType, OPERATOR)
while (
operators.length > 0 &&
isOp(peek(operators)) &&
((ASSOC[token] === 'left' && PREC[token] <= PREC[peek(operators)]) ||
(ASSOC[token] === 'right' && PREC[token] < PREC[peek(operators)]))
) {
pushExpr(pop(operators), expressions)
}
operators.push(token)
expectedTokenType = OPERAND
} else if ('(' === token) {
check(expectedTokenType, OPERAND)
operators.push(token)
expectedTokenType = OPERAND
} else if (')' === token) {
check(expectedTokenType, OPERATOR)
while (operators.length > 0 && peek(operators) !== '(') {
pushExpr(pop(operators), expressions)
}
if (operators.length === 0) {
throw Error('Syntax error. Unmatched )')
}
if (peek(operators) === '(') {
pop(operators)
}
expectedTokenType = OPERATOR
} else {
check(expectedTokenType, OPERAND)
pushExpr(token, expressions)
expectedTokenType = OPERATOR
}
})
while (operators.length > 0) {
if (peek(operators) === '(') {
throw Error('Syntax error. Unmatched (')
}
pushExpr(pop(operators), expressions)
}
return pop(expressions)
}
function tokenize(expr: string): string[] {
const tokens = []
let isEscaped = false
let token
for (let i = 0; i < expr.length; i++) {
const c = expr.charAt(i)
if ('\\' === c) {
isEscaped = true
} else {
if (/\s/.test(c)) {
// skip
if (token) {
// end of token
tokens.push(token.join(''))
token = undefined
}
} else {
if ((c === '(' || c === ')') && !isEscaped) {
if (token) {
// end of token
tokens.push(token.join(''))
token = undefined
}
tokens.push(c)
continue
}
token = token ? token : [] // start of token
token.push(c)
}
isEscaped = false
}
}
if (token) {
tokens.push(token.join(''))
}
return tokens
}
function isUnary(token: string) {
return 'not' === token
}
function isBinary(token: string) {
return 'or' === token || 'and' === token
}
function isOp(token: string) {
return ASSOC[token] !== undefined
}
function check(expectedTokenType: string, tokenType: string) {
if (expectedTokenType !== tokenType) {
throw new Error('Syntax error. Expected ' + expectedTokenType)
}
}
function peek(stack: string[]) {
return stack[stack.length - 1]
}
function pop<T>(stack: T[]): T {
if (stack.length === 0) {
throw new Error('empty stack')
}
return stack.pop()
}
function pushExpr(token: string, stack: Node[]) {
if (token === 'and') {
const rightAndExpr = pop(stack)
stack.push(new And(pop(stack), rightAndExpr))
} else if (token === 'or') {
const rightOrExpr = pop(stack)
stack.push(new Or(pop(stack), rightOrExpr))
} else if (token === 'not') {
stack.push(new Not(pop(stack)))
} else {
stack.push(new Literal(token))
}
}
interface Node {
evaluate(variables: string[]): boolean
}
class Literal implements Node {
constructor(private readonly value: string) {}
public evaluate(variables: string[]) {
return variables.indexOf(this.value) !== -1
}
public toString() {
return this.value.replace(/\(/g, '\\(').replace(/\)/g, '\\)')
}
}
class Or implements Node {
constructor(
private readonly leftExpr: Node,
private readonly rightExpr: Node
) {}
public evaluate(variables: string[]) {
return (
this.leftExpr.evaluate(variables) || this.rightExpr.evaluate(variables)
)
}
public toString() {
return (
'( ' +
this.leftExpr.toString() +
' or ' +
this.rightExpr.toString() +
' )'
)
}
}
class And implements Node {
constructor(
private readonly leftExpr: Node,
private readonly rightExpr: Node
) {}
public evaluate(variables: string[]) {
return (
this.leftExpr.evaluate(variables) && this.rightExpr.evaluate(variables)
)
}
public toString() {
return (
'( ' +
this.leftExpr.toString() +
' and ' +
this.rightExpr.toString() +
' )'
)
}
}
class Not implements Node {
constructor(private readonly expr: Node) {}
public evaluate(variables: string[]) {
return !this.expr.evaluate(variables)
}
public toString() {
return 'not ( ' + this.expr.toString() + ' )'
}
}
class True implements Node {
public evaluate(variables: string[]) {
return true
}
public toString() {
return 'true'
}
}