diff --git a/.gitignore b/.gitignore index 2e8c1ec..0a221d4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,5 @@ jspm_packages # Editors .idea -# Lib -lib - # others -.DS_Store \ No newline at end of file +.DS_Store diff --git a/README.md b/README.md index 321c0b5..989594f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ This is a very simple version of a compiler expression evaluation. It can be used for parsing expressions without introducing the risk of script injection via `eval` method. +# Submission + +For now we always should include updated prebuilt code into the repository. As for now for quick development we include this npm module directly as git repository. + +To do so, run before commit: +```sh +npm run prepublish +``` + # Commands - `npm run clean` - Remove `lib/` directory diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..1b2039e --- /dev/null +++ b/lib/index.js @@ -0,0 +1 @@ +'use strict';var _createClass=function(){function a(a,b){for(var c,d=0;d":3,"<":3,"==":2,"!=":2,"&&":1,"||":1};function hasPrecedence(a,b){return'('!==b&&')'!==b&&precedence[b]>=precedence[a]}function applyOp(c,d,e){switch(c){case'+':return e+d;case'-':return e-d;case'*':return e*d;case'/':return e/d;case'==':return e===d;case'!=':return e!==d;case'&&':return e&&d;case'||':return e||d;case'>':return e>d;case'<':return e','<','contains','hasLength','!'],oneParamOps=['!'],opsSplitters=['\\s+','\\(','\\)','!(?!=)'],splitRegexp=new RegExp('('+opsSplitters.join('|')+')');function evaluate(a,b){for(var c=a.split(splitRegexp).map(function(a){return a.trim()}).filter(function(a){return''!==a}),d=new Stack,e=new Stack,f=[],g=0;g true',function(){var a=(0,_index.evaluate)('!f',testData);a.should.equal(!0)}),it('!true => false',function(){var a=(0,_index.evaluate)('!t',testData);a.should.equal(!1)}),it('! with conditions',function(){var a=(0,_index.evaluate)('!t || !f',testData);a.should.equal(!0)}),it('!textEmpty => true',function(){var a=(0,_index.evaluate)('!textEmpty',testData);a.should.equal(!0)}),it('!text => false',function(){var a=(0,_index.evaluate)('!text',testData);a.should.equal(!1)}),it('== (true)',function(){var a=(0,_index.evaluate)('f == f',testData);a.should.equal(!0)}),it('== (false)',function(){var a=(0,_index.evaluate)('f == t',testData);a.should.equal(!1)}),it('!= (true)',function(){var a=(0,_index.evaluate)('f != t',testData);a.should.equal(!0)}),it('!= (false)',function(){var a=(0,_index.evaluate)('f != f',testData);a.should.equal(!1)})}),describe('logical operators',function(){it('test plain conditions',function(){var a=(0,_index.evaluate)('(someArrayWithText contains \'a\' || someArrayWithText contains \'d\')',testData);a.should.equal(!0)}),it('contains \'a\' && hasLength (true)',function(){var a=(0,_index.evaluate)('(someArrayWithText contains \'a\') && (someArrayWithText hasLength 3)',testData);a.should.equal(!0)}),it('contains \'a\' || contains \'b\'',function(){var a=(0,_index.evaluate)('someArrayWithText contains \'d\' && someArrayWithText contains \'b\'',testData);a.should.equal(!1)}),it('contains \'a\' && hasLength (true) && contains \'b\'',function(){var a=(0,_index.evaluate)('someArrayWithText contains \'a\' && someArrayWithText hasLength 3 && someArrayWithText contains \'b\'',testData);a.should.equal(!0)}),it('contains \'a\' && hasLength (false)',function(){var a=(0,_index.evaluate)('(someArrayWithText contains \'a\') && (someArrayWithText hasLength 2)',testData);a.should.equal(!1)}),it('contains \'a\' && hasLength (false) && contains \'b\'',function(){var a=(0,_index.evaluate)('((someArrayWithText contains \'a\') && (someArrayWithText hasLength 1) && (someArrayWithText contains \'b\'))',testData);a.should.equal(!1)})}),describe('expression format',function(){it('no spaces near parenthesis',function(){var a=(0,_index.evaluate)('someArray contains (a + b) * 2',testData);a.should.equal(!1)}),it('no spaces near parenthesis with conditions',function(){var a=(0,_index.evaluate)('(someArrayWithText contains \'a\') || (someArrayWithText contains \'b\') || (someArrayWithText contains \'c\')',testData);a.should.equal(!0)}),xit('multiple spaces',function(){var a=(0,_index.evaluate)('someArray contains ( b - a ) * 2',testData);a.should.equal(!0)})}),describe('+ operator',function(){it('should do simple addition',function(){var a=(0,_index.evaluate)('a + b',testData);a.should.equal(testData.a+testData.b)}),xit('should be read as separate from other tokens even if there are no spaces in between',function(){var a=(0,_index.evaluate)('a+b',testData);a.should.equal(testData.a+testData.b)}),xit('should identify signs in numbers',function(){var a=(0,_index.evaluate)('- a + ( + b)',testData);a.should.equal(-testData.a+testData.b)}),it('should be commutative',function(){var a=(0,_index.evaluate)('a + b',testData),b=(0,_index.evaluate)('b + a',testData);a.should.equal(b)}),it('should be associative',function(){var a=(0,_index.evaluate)('a + (b + c)',testData),b=(0,_index.evaluate)('(a + b) + c',testData);a.should.equal(b)}),it('should be distributive',function(){var a=(0,_index.evaluate)('a * (b + c)',testData),b=(0,_index.evaluate)('(a * b) + (a * c)',testData);a.should.equal(b)}),it('should have additive identity',function(){var a=(0,_index.evaluate)('a + 0',testData);a.should.equal(testData.a)}),xit('should return same if an argument is null',function(){var a=(0,_index.evaluate)('a + null',testData);a.should.equal(testData.a)}),xit('should return NaN if an argument is undefined',function(){var a=(0,_index.evaluate)('a + undefined',testData);a.should.be.NaN}),xit('should return NaN if an argument is NaN',function(){var a=(0,_index.evaluate)('a + NaN',testData);a.should.be.NaN}),it('should add true to numbers',function(){var a=(0,_index.evaluate)('t + 5',testData);a.should.equal(6)})}),describe('- operator',function(){it('should do simple subtraction',function(){var a=(0,_index.evaluate)('a - b',testData);a.should.equal(testData.a-testData.b)}),xit('should be read as separate from other tokens even if there are no spaces in between',function(){var a=(0,_index.evaluate)('a-b',testData);a.should.equal(testData.a-testData.b)}),it('should be distributive',function(){var a=(0,_index.evaluate)('a * (b - c)',testData),b=(0,_index.evaluate)('(a * b) - (a * c)',testData);a.should.equal(b)}),it('should have additive identity',function(){var a=(0,_index.evaluate)('a - 0',testData);a.should.equal(testData.a)}),xit('should return 0 if an argument is null',function(){var a=(0,_index.evaluate)('a - null',testData);a.should.equal(testData.a)}),it('should return NaN if an argument is undefined',function(){var a=(0,_index.evaluate)('a - undefined',testData);a.should.be.NaN}),it('should return NaN if an argument is NaN',function(){var a=(0,_index.evaluate)('a - NaN',testData);a.should.be.NaN}),it('should subtract true from numbers',function(){var a=(0,_index.evaluate)('f - 5',testData);a.should.equal(-5)})}),describe('* operator',function(){it('should do simple multiplication',function(){var a=(0,_index.evaluate)('a * b',testData);a.should.equal(testData.a*testData.b)}),xit('should be read as separate from other tokens even if there are no spaces in between',function(){var a=(0,_index.evaluate)('a*b',testData);a.should.equal(testData.a*testData.b)}),xit('should identify signs',function(){var a=(0,_index.evaluate)('a * (- b)',testData);a.should.equal(testData.a*-testData.b)}),it('should be commutative',function(){var a=(0,_index.evaluate)('a * b',testData),b=(0,_index.evaluate)('b * a',testData);a.should.equal(b)}),it('should be associative',function(){var a=(0,_index.evaluate)('a * (b * c)',testData),b=(0,_index.evaluate)('(a * b) * c',testData);a.should.equal(b)}),it('should have multiplicative identity',function(){var a=(0,_index.evaluate)('b * 1',testData);a.should.equal(testData.b)}),xit('should return 0 if an argument is null',function(){var a=(0,_index.evaluate)('a * null',testData);a.should.equal(0)}),it('should return NaN if an argument is undefined',function(){var a=(0,_index.evaluate)('a * undefined',testData);a.should.be.NaN}),it('should return NaN if an argument is NaN',function(){var a=(0,_index.evaluate)('a * NaN',testData);a.should.be.NaN}),xit('should flip the sign of Infinity',function(){var a=(0,_index.evaluate)('d * Infinity',testData);a.should.equal(-Infinity)}),it('should return NaN when multiplying 0 and Infinity',function(){var a=(0,_index.evaluate)('0 * Infinity',testData);isNaN(a).should.equal(!0)})}),describe('/ operator',function(){it('should do simple division',function(){var a=(0,_index.evaluate)('a / b',testData);a.should.equal(testData.a/testData.b)}),xit('should be read as separate from other tokens even if there are no spaces in between',function(){var a=(0,_index.evaluate)('a/b',testData);a.should.equal(testData.a*testData.b)}),it('should return Infinity when dividing be 0',function(){var a=(0,_index.evaluate)('a / 0',testData);a.should.equal(Infinity)}),xit('should identify sign in its arguments',function(){var a=(0,_index.evaluate)('- a / b',testData);a.should.equal(-testData.a/testData.b)}),it('should take precedence over addition and subtraction',function(){var a=(0,_index.evaluate)('a + b / c - d * b',testData);a.should.equal(testData.a+testData.b/testData.c-testData.d*testData.b)}),xit('should return 0 when dividing by Infinity',function(){var a=(0,_index.evaluate)('a / Infinity',testData);a.should.equal(0)}),it('should do 0 / 0 == NaN',function(){var a=(0,_index.evaluate)('0 / 0',testData);isNaN(a).should.equal(!0)}),it('should do Infinity / Infinity == NaN',function(){var a=(0,_index.evaluate)('Infinity / Infinity',testData);isNaN(a).should.equal(!0)}),xit('should return Infinity if an argument is null',function(){var a=(0,_index.evaluate)('a / null',testData);a.should.equal(0)}),it('should return NaN if an argument is undefined',function(){var a=(0,_index.evaluate)('a / undefined',testData);a.should.be.NaN}),it('should return NaN if an argument is NaN',function(){var a=(0,_index.evaluate)('a / NaN',testData);a.should.be.NaN})}),describe('== operator',function(){xit('should compare NaNs',function(){var a=(0,_index.evaluate)('NaN == NaN',testData);a.should.equal(!1)}),xit('should compare Infinities',function(){var a=(0,_index.evaluate)('Infinity == Infinity',testData);a.should.equal(!0)}),it('should compare nulls',function(){var a=(0,_index.evaluate)('null == null',testData);a.should.equal(!0)}),it('should compare undefined',function(){var a=(0,_index.evaluate)('undefined == undefined',testData);a.should.equal(!0)}),it('should compare boolean true',function(){var a=(0,_index.evaluate)('t == true',testData);a.should.equal(!0)}),it('should compare boolean false',function(){var a=(0,_index.evaluate)('f == false',testData);a.should.equal(!0)}),it('should compare string and number',function(){var a=(0,_index.evaluate)('stringa == a',testData);a.should.equal(!1)}),xit('should compare boolean and number',function(){var a=(0,_index.evaluate)('zero == f',testData);a.should.equal(!0)})}),describe('!= operator',function(){xit('should compare NaNs',function(){var a=(0,_index.evaluate)('NaN != NaN',testData);a.should.equal(!0)}),xit('should compare Infinities',function(){var a=(0,_index.evaluate)('Infinity != Infinity',testData);a.should.equal(!1)}),it('should compare nulls',function(){var a=(0,_index.evaluate)('null != null',testData);a.should.equal(!1)}),it('should compare undefined',function(){var a=(0,_index.evaluate)('undefined != undefined',testData);a.should.equal(!1)}),it('should compare string and number',function(){var a=(0,_index.evaluate)('stringa != a',testData);a.should.equal(!0)}),it('should compare inequality for boolean true',function(){var a=(0,_index.evaluate)('t != true',testData);a.should.equal(!1)}),it('should compare inequality for boolean false',function(){var a=(0,_index.evaluate)('f != false',testData);a.should.equal(!1)}),xit('should compare boolean and number',function(){var a=(0,_index.evaluate)('zero != f',testData);a.should.equal(!1)})}),describe('&& operator',function(){it('true && true => true',function(){var a=(0,_index.evaluate)('t && t',testData);a.should.equal(!0)}),it('true && false => false',function(){var a=(0,_index.evaluate)('t && f',testData);a.should.equal(!1)}),it('false && true => false',function(){var a=(0,_index.evaluate)('f && t',testData);a.should.equal(!1)}),it('false && false => false',function(){var a=(0,_index.evaluate)('f && f',testData);a.should.equal(!1)})}),describe('|| operator',function(){it('true || true => true',function(){var a=(0,_index.evaluate)('t || t',testData);a.should.equal(!0)}),it('true || false => true',function(){var a=(0,_index.evaluate)('t || f',testData);a.should.equal(!0)}),it('false || true => true',function(){var a=(0,_index.evaluate)('f || t',testData);a.should.equal(!0)}),it('false || false => false',function(){var a=(0,_index.evaluate)('f || f',testData);a.should.equal(!1)})}),describe('parser',function(){it('should parse literal constants like numbers',function(){var a=(0,_index.evaluate)('4 + 2',testData);a.should.equal(6)}),it('should throw error if one opening parenthesis is unbalanced',function(){try{(0,_index.evaluate)('((someArrayWithText contains \'a\') || (someArrayWithText contains \'b\') || (someArrayWithText contains \'c\')',testData)}catch(a){a.message.should.equal('Parens with the following token indexes are unbalanced: 0')}}),it('should throw error if multiple opening parenthesis are unbalanced',function(){try{(0,_index.evaluate)('(someArrayWithText contains \'a\') || ((someArrayWithText contains \'b\') || ((someArrayWithText contains \'c\')',testData)}catch(a){a.message.should.equal('Parens with the following token indexes are unbalanced: 6,13')}}),it('should throw error if one closing parenthesis is unbalanced',function(){try{(0,_index.evaluate)('(someArrayWithText contains \'a\')) || (someArrayWithText contains \'b\') || (someArrayWithText contains \'c\')',testData)}catch(a){a.message.should.equal('Parens with the following token indexes are unbalanced: 5')}}),it('should throw error if multiple closing parenthesis are unbalanced',function(){try{(0,_index.evaluate)('(someArrayWithText contains \'a\') || (someArrayWithText contains \'b\')) || (someArrayWithText contains \'c\'))',testData)}catch(a){a.message.should.equal('Parens with the following token indexes are unbalanced: 11,18')}}),xit('should parse literal constants like numbers with decimal numbers',function(){var a=(0,_index.evaluate)('4.2 + 2.3',testData);a.should.equal(4.2+2.3)}),xit('should parse literal constants like booleans',function(){var a=(0,_index.evaluate)('true && true',testData);a.should.equal(!0)}),xit('should treat unknown variables as undefined',function(){var a=(0,_index.evaluate)('unknownVariable',testData);a.should.be.undefined})}),describe('! operator',function(){it('!true => false',function(){var a=(0,_index.evaluate)('!t',testData);a.should.equal(!1)}),it('!false => true',function(){var a=(0,_index.evaluate)('!f',testData);a.should.equal(!0)}),xit('!undefined => true',function(){var a=(0,_index.evaluate)('!undefined',testData);a.should.equal(!0)}),xit('!null => true',function(){var a=(0,_index.evaluate)('! null',testData);a.should.equal(!0)})}),describe('> operator',function(){it('should compare numbers',function(){var a=(0,_index.evaluate)('a > b',testData);a.should.equal(testData.a>testData.b)}),it('should compare strings',function(){var a=(0,_index.evaluate)('\'absfd\' > \'fsb\'',testData);a.should.equal(!1)}),it('should compare numbers and strings',function(){var a=(0,_index.evaluate)('\'absfd\' > 1',testData);a.should.equal(!1)}),it('should compare undefined',function(){var a=(0,_index.evaluate)('undefined > 1',testData);a.should.equal(1 1',testData);a.should.equal(!1)}),it('should compare NaN',function(){var a=NaN,b=(0,_index.evaluate)('NaN > 1',testData);b.should.equal(1void 0)}),xit('should compare null',function(){var a=(0,_index.evaluate)('null < 1',testData);a.should.equal(!0)}),it('should compare NaN',function(){var a=NaN,b=(0,_index.evaluate)('NaN < 1',testData);b.should.equal(1>a)})})}),describe('Replace prepared conditions: ',function(){it('should replace single prepared condition',function(){var a=(0,_index.populatePreparedConditions)('PREPARED_CONDITION_1',preparedConditions);a.should.equal(preparedConditions.PREPARED_CONDITION_1)}),it('should replace multiple prepared conditions',function(){var a=(0,_index.populatePreparedConditions)('PREPARED_CONDITION_1 && PREPARED_CONDITION_2',preparedConditions);a.should.equal(preparedConditions.PREPARED_CONDITION_1+' && '+preparedConditions.PREPARED_CONDITION_2)}),it('should not replace prepared condition inside text literals',function(){var a=(0,_index.populatePreparedConditions)('someArray contains \'PREPARED_CONDITION_1\' && a == PREPARED_CONDITION_2',preparedConditions);a.should.equal('someArray contains \'PREPARED_CONDITION_1\' && a == '+preparedConditions.PREPARED_CONDITION_2)}),it('should not replace prepared condition inside object properties level 1',function(){var a=(0,_index.populatePreparedConditions)('nestedObject.PREPARED_CONDITION_1 && a == PREPARED_CONDITION_2',preparedConditions);a.should.equal('nestedObject.PREPARED_CONDITION_1 && a == '+preparedConditions.PREPARED_CONDITION_2)}),it('should not replace prepared condition inside object properties level 2',function(){var a=(0,_index.populatePreparedConditions)('nestedObject.propertyWithObject.PREPARED_CONDITION_1 && a == PREPARED_CONDITION_2',preparedConditions);a.should.equal('nestedObject.propertyWithObject.PREPARED_CONDITION_1 && a == '+preparedConditions.PREPARED_CONDITION_2)}),it('should not replace prepared condition in the part of another prepared condition variable',function(){var a=(0,_index.populatePreparedConditions)('SUFFIX_PREPARED_CONDITION_2 && PREPARED_CONDITION_2 && PREPARED_CONDITION_2_POSTFIX',preparedConditions);a.should.equal('SUFFIX_PREPARED_CONDITION_2 && '+preparedConditions.PREPARED_CONDITION_2+' && PREPARED_CONDITION_2_POSTFIX')}),it('should not replace prepared condition delimited by parentheses',function(){var a=(0,_index.populatePreparedConditions)('(PREPARED_CONDITION_2)',preparedConditions);a.should.equal('('+preparedConditions.PREPARED_CONDITION_2+')')}),it('should not replace prepared condition delimited preceded by "!"',function(){var a=(0,_index.populatePreparedConditions)('!PREPARED_CONDITION_2',preparedConditions);a.should.equal('!'+preparedConditions.PREPARED_CONDITION_2)})}); \ No newline at end of file diff --git a/src/index.js b/src/index.js index 2c1aa49..0ca5ed2 100644 --- a/src/index.js +++ b/src/index.js @@ -163,13 +163,17 @@ export function evaluate(expression, data) { // Stack for Operators: 'ops' const ops = new Stack(); + // Keep track of unbalanced parenthesis + const unbalancedParens = []; + for (let i = 0; i < tokens.length; i++) { if (tokens[i] === '(') { ops.push(tokens[i]); + unbalancedParens.push(i); // Closing brace encountered, solve expression since the last opening brace } else if (tokens[i] === ')') { - while (ops.peek() !== '(') { + while (ops.peek() !== '(' && !ops.empty()) { const op = ops.pop(); if (oneParamOps.indexOf(op) !== -1) { values.push(applyOp(op, values.pop())); @@ -177,7 +181,13 @@ export function evaluate(expression, data) { values.push(applyOp(op, values.pop(), values.pop())); } } - ops.pop();// removing opening brace + // if the ops array is empty means there is an unbalanced closing parenthesis + if (ops.empty()) { + unbalancedParens.push(i); + } else { + unbalancedParens.pop(); + ops.pop(); // removing opening brace + } // Current token is an operator. } else if (allowedOps.indexOf(tokens[i]) > -1) { @@ -213,6 +223,12 @@ export function evaluate(expression, data) { } } } + + // if there are unbalanced parenthesis throw an error + if (unbalancedParens.length !== 0) { + throw new Error(`Parens with the following token indexes are unbalanced: ${unbalancedParens}`); + } + // debugger // Parsed expression tokens are pushed to values/ops respectively, // Running while loop to evaluate the expression diff --git a/src/index.spec.js b/src/index.spec.js index 9066f6d..54eefa3 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -558,6 +558,46 @@ describe('Evaluate: ', () => { res.should.equal(4 + 2); }); + it('should throw error if one opening parenthesis is unbalanced', () => { + const expression = `((someArrayWithText contains 'a') || (someArrayWithText contains 'b') || (someArrayWithText contains 'c')`; + + try { + evaluate(expression, testData); + } catch (error) { + error.message.should.equal('Parens with the following token indexes are unbalanced: 0'); + } + }); + + it('should throw error if multiple opening parenthesis are unbalanced', () => { + const expression = `(someArrayWithText contains 'a') || ((someArrayWithText contains 'b') || ((someArrayWithText contains 'c')`; + + try { + evaluate(expression, testData); + } catch (error) { + error.message.should.equal('Parens with the following token indexes are unbalanced: 6,13'); + } + }); + + it('should throw error if one closing parenthesis is unbalanced', () => { + const expression = `(someArrayWithText contains 'a')) || (someArrayWithText contains 'b') || (someArrayWithText contains 'c')`; + + try { + evaluate(expression, testData); + } catch (error) { + error.message.should.equal('Parens with the following token indexes are unbalanced: 5'); + } + }); + + it('should throw error if multiple closing parenthesis are unbalanced', () => { + const expression = `(someArrayWithText contains 'a') || (someArrayWithText contains 'b')) || (someArrayWithText contains 'c'))`; + + try { + evaluate(expression, testData); + } catch (error) { + error.message.should.equal('Parens with the following token indexes are unbalanced: 11,18'); + } + }); + xit('should parse literal constants like numbers with decimal numbers', () => { const res = evaluate('4.2 + 2.3', testData); res.should.equal(4.2 + 2.3);