diff --git a/CodingSeb.ExpressionEvaluator.Tests/ExpressionEvaluatorTests.cs b/CodingSeb.ExpressionEvaluator.Tests/ExpressionEvaluatorTests.cs index 690189f..30b9d66 100644 --- a/CodingSeb.ExpressionEvaluator.Tests/ExpressionEvaluatorTests.cs +++ b/CodingSeb.ExpressionEvaluator.Tests/ExpressionEvaluatorTests.cs @@ -1111,6 +1111,13 @@ public static IEnumerable TestCasesForWithCustomVariablesExpressio yield return new TestCaseData("nullVar?[1][3]", onInstanceVariables, true).SetCategory("Instance Property,Null Conditional indexing").Returns(null); yield return new TestCaseData("simpleArray2?[3]?.Trim()", onInstanceVariables, true).SetCategory("Instance Property,Null Conditional indexing").Returns(null); + yield return new TestCaseData( "false && 1/0 == 0", onInstanceVariables, true ).SetCategory( "Instance Property,And Conditional" ).Returns( false ); + yield return new TestCaseData( "!string.IsNullOrEmpty(nullVar) && nullVar.StartsWith(\"ABC\")", onInstanceVariables, true ).SetCategory( "Instance Property,And Conditional" ).Returns( false ); + yield return new TestCaseData( "string.IsNullOrEmpty(nullVar) || nullVar.StartsWith(\"ABC\")", onInstanceVariables, true ).SetCategory( "Instance Property,Or Conditional" ).Returns( true ); + yield return new TestCaseData( "true || 1/0 == 0", onInstanceVariables, true ).SetCategory( "Instance Property,Or Conditional" ).Returns( true ); + yield return new TestCaseData( "false && true || true", onInstanceVariables, true ).SetCategory( "Instance Property,Or Conditional,And Conditional,Precedence check" ).Returns( true ); + yield return new TestCaseData( "true || true && false", onInstanceVariables, true ).SetCategory( "Instance Property,Or Conditional,And Conditional,Precedence check" ).Returns( true ); + yield return new TestCaseData("simpleInt.ToString()", onInstanceVariables, true).SetCategory("Instance Method").Returns("42"); yield return new TestCaseData("simpleInt.ToString().Length", onInstanceVariables, true).SetCategory("Instance Method,Instance Property").Returns(2); @@ -1141,6 +1148,11 @@ public static IEnumerable TestCasesForWithCustomVariablesExpressio yield return new TestCaseData("simpleInt++ - simpleInt", onInstanceVariables, true).SetCategory("Postfix operator, ++").Returns(-1); yield return new TestCaseData("simpleInt--", onInstanceVariables, true).SetCategory("Postfix operator, --").Returns(42); yield return new TestCaseData("simpleInt-- - simpleInt", onInstanceVariables, true).SetCategory("Postfix operator, --").Returns(1); + + yield return new TestCaseData("false && 1/0>0", onInstanceVariables, true ).SetCategory( "Conditional And, negative left operand (should respect left associativity)" ).Returns( false ); + yield return new TestCaseData("true || 1/0>0", onInstanceVariables, true ).SetCategory( "Conditional Or, positive left operand (should respect left associativity)" ).Returns( true ); + yield return new TestCaseData("false && (true && 1/0>0)", onInstanceVariables, true ).SetCategory( "Conditional And, negative left operand (should respect left associativity)" ).Returns( false ); + yield return new TestCaseData("true || (false || 1/0>0)", onInstanceVariables, true ).SetCategory( "Conditional Or, positive left operand (should respect left associativity)" ).Returns( true ); #endregion #region Delegates as a variable @@ -1426,7 +1438,7 @@ public static IEnumerable TestCasesForExceptionThrowingEvaluation evaluator = new ExpressionEvaluator(new Dictionary { { "P1var", "P1" }, - { "myObj", new ClassForTest1() }, + { "myObj", new ClassForTest1() } }); evaluator.PreEvaluateVariable += (sender, e) => @@ -1446,7 +1458,10 @@ public static IEnumerable TestCasesForExceptionThrowingEvaluation yield return new TestCaseData(evaluator, "myObj.PropertyThatWillFailed", typeof(ExpressionEvaluatorSyntaxErrorException)).SetCategory("OnTheFly canceled Var"); yield return new TestCaseData(evaluator, "myObj.Add3To(5)", typeof(ExpressionEvaluatorSyntaxErrorException)).SetCategory("OnTheFly canceled Func"); yield return new TestCaseData(evaluator, "Abs(-5)", typeof(ExpressionEvaluatorSyntaxErrorException)).SetCategory("OnTheFly canceled Func"); - + yield return new TestCaseData(evaluator, "true && 1/0>0",typeof(DivideByZeroException) ).SetCategory( "Conditional And, positive left operand (should lead to exception)" ); + yield return new TestCaseData(evaluator, "false || 1/0>0", typeof( DivideByZeroException ) ).SetCategory( "Conditional Or, positive left operand (should lead to exception associativity)" ); + yield return new TestCaseData(evaluator, "true && (true && 1/0>0)", typeof( DivideByZeroException ) ).SetCategory( "Conditional And, positive left operand (should lead to exception)" ); + yield return new TestCaseData(evaluator, "false || (false || 1/0>0)", typeof( DivideByZeroException ) ).SetCategory( "Conditional Or, positive left operand (should lead to exception associativity)" ); #endregion } } diff --git a/CodingSeb.ExpressionEvaluator.Tests/TestsUtils/XExpressionEvaluator3.cs b/CodingSeb.ExpressionEvaluator.Tests/TestsUtils/XExpressionEvaluator3.cs index e6d8cb1..8df912f 100644 --- a/CodingSeb.ExpressionEvaluator.Tests/TestsUtils/XExpressionEvaluator3.cs +++ b/CodingSeb.ExpressionEvaluator.Tests/TestsUtils/XExpressionEvaluator3.cs @@ -15,7 +15,7 @@ protected override void Init() /// /// To evaluate DateTimes objects with #year-month-day syntax (#2019-05-28) /// - protected virtual bool EvaluateDateTimeSyntax(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateDateTimeSyntax(string expression, Stack stack, ref int i) { Match match = Regex.Match(expression.Substring(i), @"^\s*#(?\d{4})-(?\d{2})-(?\d{2})"); @@ -40,7 +40,7 @@ protected virtual bool EvaluateDateTimeSyntax(string expression, Stack s /// /// To evaluate a string replace with custom ternary indicator /// - protected virtual bool EvaluateSpecialTernaryOperator(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateSpecialTernaryOperator(string expression, Stack stack, ref int i) { if (expression.Substring(i, 1).Equals("°")) { diff --git a/CodingSeb.ExpressionEvaluator/ExpressionEvaluator.cs b/CodingSeb.ExpressionEvaluator/ExpressionEvaluator.cs index 8a14432..12e57e9 100644 --- a/CodingSeb.ExpressionEvaluator/ExpressionEvaluator.cs +++ b/CodingSeb.ExpressionEvaluator/ExpressionEvaluator.cs @@ -216,7 +216,9 @@ protected static object IndexingOperatorFunc(dynamic left, dynamic right) { if (left is NullConditionalNullValue) return left; - + else if ( left is Exception ) { + return left; + } Type type = ((object)left).GetType(); if (left is IDictionary dictionaryLeft) @@ -297,11 +299,39 @@ protected static object IndexingOperatorFunc(dynamic left, dynamic right) }, new Dictionary>() { - {ExpressionOperator.ConditionalAnd, (dynamic left, dynamic right) => left && right }, + {ExpressionOperator.ConditionalAnd, (dynamic left, dynamic right) => { + if ( left is Exception ) { + throw left as Exception; + } else { + if ( !left ) { + return false; + } else { + if (right is Exception ) { + throw right as Exception; + }else{ + return left && right; + } + } + } + } }, }, new Dictionary>() { - {ExpressionOperator.ConditionalOr, (dynamic left, dynamic right) => left || right }, + {ExpressionOperator.ConditionalOr, (dynamic left, dynamic right) => { + if ( left is Exception ) { + throw left as Exception; + } else { + if ( left ) { + return true; + } else { + if (right is Exception ) { + throw right as Exception; + }else{ + return left || right; + } + } + } + } }, }, new Dictionary>() { @@ -845,6 +875,11 @@ protected virtual BindingFlags StaticBindingFlag private IDictionary variables = new Dictionary(StringComparer.Ordinal); + /// + /// Counts stack initialisations to determine if the expresseion enty point was reached. In that case the transported exception should be thrown. + /// + private int evaluationStackCount=0; + /// /// The Values of the variable use in the expressions /// @@ -1029,7 +1064,12 @@ object ManageJumpStatementsOrExpressionEval(string expression) return match.Value.Contains("(") ? "(" : string.Empty; }); - return Evaluate(expression); + var res=Evaluate(expression); + if (res is Exception ) { + throw res as Exception; + } + return res; + } object ScriptExpressionEvaluate(ref int index) @@ -1476,26 +1516,40 @@ public T Evaluate(string expression) public object Evaluate(string expression) { expression = expression.Trim(); - + Stack stack = new Stack(); + evaluationStackCount++; + try { + if ( GetLambdaExpression( expression, stack ) ) + return stack.Pop(); + + for ( int i = 0; i < expression.Length; i++ ) { + if ( !ParsingMethods.Any( parsingMethod => { + bool? pRes = parsingMethod( expression, stack, ref i ); + //Possibility to implement an option to toggle left associativity + //If "null" is returned, an error occured while parsing + if ( pRes.HasValue ) { + return pRes.Value; //normal case + } else { + return true; //Go on with parsing without throwing an exception. We want to reach the stack processing. + } + } ) ) { + string s = expression.Substring( i, 1 ); - if (GetLambdaExpression(expression, stack)) - return stack.Pop(); - - for (int i = 0; i < expression.Length; i++) - { - if (!ParsingMethods.Any(parsingMethod => parsingMethod(expression, stack, ref i))) - { - string s = expression.Substring(i, 1); - - if (!s.Trim().Equals(string.Empty)) - { - throw new ExpressionEvaluatorSyntaxErrorException($"Invalid character [{(int)s[0]}:{s}]"); + if ( !s.Trim().Equals( string.Empty ) ) { + throw new ExpressionEvaluatorSyntaxErrorException( $"Invalid character [{(int)s[ 0 ]}:{s}]" ); + } } } + var res = ProcessStack( stack ); + if ( evaluationStackCount == 1 && res is Exception ) { + //We reached the top level of the evaluation. So we want to throw the resulting exception. + throw res as Exception; + } + return res; + } finally { + evaluationStackCount--; } - - return ProcessStack(stack); } #endregion @@ -1504,7 +1558,7 @@ public object Evaluate(string expression) #region Sub parts evaluate methods (protected virtual) - protected virtual bool EvaluateCast(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateCast(string expression, Stack stack, ref int i) { Match castMatch = Regex.Match(expression.Substring(i), CastRegexPattern, optionCaseSensitiveEvaluationActive ? RegexOptions.None : RegexOptions.IgnoreCase); @@ -1526,7 +1580,7 @@ protected virtual bool EvaluateCast(string expression, Stack stack, ref return false; } - protected virtual bool EvaluateNumber(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateNumber(string expression, Stack stack, ref int i) { string restOfExpression = expression.Substring(i); Match numberMatch = Regex.Match(restOfExpression, numberRegexPattern, RegexOptions.IgnoreCase); @@ -1589,7 +1643,7 @@ protected virtual bool EvaluateNumber(string expression, Stack stack, re } } - protected virtual bool EvaluateInstanceCreationWithNewKeyword(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateInstanceCreationWithNewKeyword(string expression, Stack stack, ref int i) { if (!OptionNewKeywordEvaluationActive) return false; @@ -1759,7 +1813,7 @@ void Init(object element, List initArgs) } } - protected virtual bool EvaluateVarOrFunc(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateVarOrFunc(string expression, Stack stack, ref int i) { Match varFuncMatch = varOrFunctionRegEx.Match(expression.Substring(i)); @@ -1816,8 +1870,10 @@ protected virtual bool EvaluateVarOrFunc(string expression, Stack stack, else if (varFuncMatch.Groups["nullConditional"].Success && obj == null) { stack.Push(new NullConditionalNullValue()); - } - else + }else if(obj is Exception ) { + stack.Push( obj ); + return null; + } else { FunctionPreEvaluationEventArg functionPreEvaluationEventArg = new FunctionPreEvaluationEventArg(varFuncName, Evaluate, funcArgs, this, obj, genericsTypes, GetConcreteTypes); @@ -1918,7 +1974,9 @@ protected virtual bool EvaluateVarOrFunc(string expression, Stack stack, } catch (Exception ex) { - throw new ExpressionEvaluatorSyntaxErrorException($"The call of the method \"{varFuncName}\" on type [{objType}] generate this error : {ex.InnerException?.Message ?? ex.Message}", ex); + //Transort the exception in stack. + stack.Push( new ExpressionEvaluatorSyntaxErrorException( $"The call of the method \"{varFuncName}\" on type [{objType}] generate this error : {ex.InnerException?.Message ?? ex.Message}", ex ) ); + return null; //Signals an error to the parsing method array call } } else @@ -1998,8 +2056,10 @@ protected virtual bool EvaluateVarOrFunc(string expression, Stack stack, else if (varFuncMatch.Groups["nullConditional"].Success && obj == null) { stack.Push(new NullConditionalNullValue()); - } - else + } else if ( obj is Exception ) { + stack.Push( obj ); + return null; + } else { VariablePreEvaluationEventArg variablePreEvaluationEventArg = new VariablePreEvaluationEventArg(varFuncName, this, obj, genericsTypes, GetConcreteTypes); @@ -2357,7 +2417,7 @@ protected virtual bool EvaluateVarOrFunc(string expression, Stack stack, } } - protected virtual bool EvaluateChar(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateChar(string expression, Stack stack, ref int i) { if (!OptionCharEvaluationActive) return false; @@ -2408,7 +2468,7 @@ protected virtual bool EvaluateChar(string expression, Stack stack, ref } } - protected virtual bool EvaluateOperators(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateOperators(string expression, Stack stack, ref int i) { string regexPattern = "^(" + string.Join("|", operatorsDictionary .Keys @@ -2428,7 +2488,7 @@ protected virtual bool EvaluateOperators(string expression, Stack stack, return false; } - protected virtual bool EvaluateTernaryConditionalOperator(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateTernaryConditionalOperator(string expression, Stack stack, ref int i) { if (expression.Substring(i, 1).Equals("?")) { @@ -2468,7 +2528,7 @@ protected virtual bool EvaluateTernaryConditionalOperator(string expression, Sta return false; } - protected virtual bool EvaluateParenthis(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateParenthis(string expression, Stack stack, ref int i) { string s = expression.Substring(i, 1); @@ -2519,7 +2579,7 @@ protected virtual void CorrectStackWithUnaryPlusOrMinusBeforeParenthisIfNecessar } } - protected virtual bool EvaluateIndexing(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateIndexing(string expression, Stack stack, ref int i) { if (!OptionIndexingActive) return false; @@ -2568,6 +2628,9 @@ protected virtual bool EvaluateIndexing(string expression, Stack stack, { stack.Push(left); return true; + } else if ( left is Exception ) { + stack.Push( left ); + return true; } dynamic right = Evaluate(innerExp.ToString()); @@ -2642,7 +2705,7 @@ protected virtual bool EvaluateIndexing(string expression, Stack stack, return false; } - protected virtual bool EvaluateString(string expression, Stack stack, ref int i) + protected virtual bool? EvaluateString(string expression, Stack stack, ref int i) { if (!OptionStringEvaluationActive) return false; @@ -2792,19 +2855,49 @@ protected virtual object ProcessStack(Stack stack) { if (RightOperandOnlyOperatorsEvaluationDictionary.Contains(eOp)) { - list[i] = operatorEvalutationsDict[eOp](null, (dynamic)list[i - 1]); + try { + list[ i ] = operatorEvalutationsDict[ eOp ]( null, (dynamic)list[ i - 1 ] ); + } catch ( Exception ex ) { + var right = (dynamic)list[ i - 1 ]; + if ( right is Exception ) { + list[ i ] = right;//Bubble up the causing error + } else { + list[ i ] = ex;//Transport the processing error + } + } list.RemoveAt(i - 1); break; } else if (LeftOperandOnlyOperatorsEvaluationDictionary.Contains(eOp)) - { - list[i] = operatorEvalutationsDict[eOp]((dynamic)list[i + 1], null); + { + try { + list[ i ] = operatorEvalutationsDict[ eOp ]( (dynamic)list[ i + 1 ], null ); + } catch (Exception ex) { + var left = (dynamic)list[ i + 1 ]; + if ( left is Exception ) { + list[ i ] = left;//Bubble up the causing error + } else { + list[ i ] = ex;//Transport the processing error + } + } list.RemoveAt(i + 1); break; } else { - list[i] = operatorEvalutationsDict[eOp]((dynamic)list[i + 1], (dynamic)list[i - 1]); + try { + list[ i ] = operatorEvalutationsDict[ eOp ]( (dynamic)list[ i + 1 ], (dynamic)list[ i - 1 ] ); + }catch(Exception ex ) { + var left = ( dynamic )list[ i + 1 ]; + var right = ( dynamic )list[ i - 1 ]; + if( left is Exception ) { + list[ i ] = left; //Bubble up the causing error + } else if( right is Exception ) { + list[ i ] = right; //Bubble up the causing error + } else { + list[ i ] = ex; //Transport the processing error + } + } list.RemoveAt(i + 1); list.RemoveAt(i - 1); i--; @@ -2821,9 +2914,14 @@ protected virtual object ProcessStack(Stack stack) stack.Push(list[i]); } - if (stack.Count > 1) - throw new ExpressionEvaluatorSyntaxErrorException("Syntax error. Check that no operator is missing"); - + if ( stack.Count > 1 ) { + foreach ( var item in stack ) { + if ( item is Exception ) { + throw item as Exception; //Throw the first occuring error + } + } + throw new ExpressionEvaluatorSyntaxErrorException( "Syntax error. Check that no operator is missing" ); + } return stack.Pop(); } @@ -2866,7 +2964,10 @@ public string RemoveComments(string scriptWithComments) #region Utils methods for parsing and interpretation - protected delegate bool ParsingMethodDelegate(string expression, Stack stack, ref int i); + /// + /// Should return null if an error occurred which should not interrupt the parsing process + /// + protected delegate bool? ParsingMethodDelegate(string expression, Stack stack, ref int i); protected delegate dynamic InternalDelegate(params dynamic[] args);