@cucumber/tag-expressions
Version:
Cucumber Tag Expression parser
217 lines • 6.38 kB
JavaScript
const OPERAND = 'operand';
const OPERATOR = 'operator';
const PREC = {
'(': -2,
')': -1,
or: 0,
and: 1,
not: 2,
};
const ASSOC = {
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) {
const tokens = tokenize(infix);
if (tokens.length === 0) {
return new True();
}
const expressions = [];
const operators = [];
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 new Error(`Tag expression "${infix}" could not be parsed because of 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 new Error(`Tag expression "${infix}" could not be parsed because of syntax error: Unmatched (.`);
}
pushExpr(pop(operators), expressions);
}
return pop(expressions);
function check(expectedTokenType, tokenType) {
if (expectedTokenType !== tokenType) {
throw new Error(`Tag expression "${infix}" could not be parsed because of syntax error: Expected ${expectedTokenType}.`);
}
}
}
function tokenize(expr) {
const tokens = [];
let isEscaped = false;
let token = [];
for (let i = 0; i < expr.length; i++) {
const c = expr.charAt(i);
if (isEscaped) {
if (c === '(' || c === ')' || c === '\\' || /\s/.test(c)) {
token.push(c);
isEscaped = false;
}
else {
throw new Error(`Tag expression "${expr}" could not be parsed because of syntax error: Illegal escape before "${c}".`);
}
}
else if (c === '\\') {
isEscaped = true;
}
else if (c === '(' || c === ')' || /\s/.test(c)) {
if (token.length > 0) {
tokens.push(token.join(''));
token = [];
}
if (!/\s/.test(c)) {
tokens.push(c);
}
}
else {
token.push(c);
}
}
if (token.length > 0) {
tokens.push(token.join(''));
}
return tokens;
}
function isUnary(token) {
return 'not' === token;
}
function isBinary(token) {
return 'or' === token || 'and' === token;
}
function isOp(token) {
return ASSOC[token] !== undefined;
}
function peek(stack) {
return stack[stack.length - 1];
}
function pop(stack) {
if (stack.length === 0) {
throw new Error('empty stack');
}
return stack.pop();
}
function pushExpr(token, stack) {
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));
}
}
class Literal {
constructor(value) {
this.value = value;
}
evaluate(variables) {
return variables.indexOf(this.value) !== -1;
}
toString() {
return this.value
.replace(/\\/g, '\\\\')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)')
.replace(/\s/g, '\\ ');
}
}
class Or {
constructor(leftExpr, rightExpr) {
this.leftExpr = leftExpr;
this.rightExpr = rightExpr;
}
evaluate(variables) {
return this.leftExpr.evaluate(variables) || this.rightExpr.evaluate(variables);
}
toString() {
return '( ' + this.leftExpr.toString() + ' or ' + this.rightExpr.toString() + ' )';
}
}
class And {
constructor(leftExpr, rightExpr) {
this.leftExpr = leftExpr;
this.rightExpr = rightExpr;
}
evaluate(variables) {
return this.leftExpr.evaluate(variables) && this.rightExpr.evaluate(variables);
}
toString() {
return '( ' + this.leftExpr.toString() + ' and ' + this.rightExpr.toString() + ' )';
}
}
class Not {
constructor(expr) {
this.expr = expr;
}
evaluate(variables) {
return !this.expr.evaluate(variables);
}
toString() {
if (this.expr instanceof And || this.expr instanceof Or) {
// -- HINT: Binary Operators already have already '( ... )'.
return 'not ' + this.expr.toString();
}
// -- OTHERWISE:
return 'not ( ' + this.expr.toString() + ' )';
}
}
class True {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
evaluate(variables) {
return true;
}
toString() {
return 'true';
}
}
//# sourceMappingURL=index.js.map