sklose/NCalc2

Expression.HasErrors() not flagging error with if function

aintJoshinya opened this issue · 1 comments

var expr = new Expression("if (1 = 1, 'office')");
Console.WriteLine($"has error:{expr.HasErrors()}"); //false
Console.WriteLine(expr.Evaluate()); //throws error!

Running the above code with the latest version (2.2.80), the expression will not show as having errors ( the second line), but when evaluating it. it willk throw an argument exception (third line).

Why is this happening? is there a better way to ensure a given expression does not have errors?

We ended up writing a custom visitor that implemented some parameter checking for us. Just in case it helps anyone else, this is roughly what we are using:

//example usage
public List<string> ValidateExpression(string expression)
        {
            var expr = new Expression(expression);
            if (expr.HasErrors())
                return new List<string>() {"Expression has Invalid Syntax"};
            
            var functionParamValidator = new FunctionParamValidator();
            expr.ParsedExpression.Accept(functionParamValidator);
            return functionParamValidator.Errors;
        }


public enum NCalcFunctionParamRequirementType
    {
        Exact,
        Minimum
    }
    class NCalcFunctionParamRequirement
    {
        public NCalcFunctionParamRequirementType Type { get; private set; }
        public int Value { get; private set; }

        public NCalcFunctionParamRequirement(int value, NCalcFunctionParamRequirementType type)
        {
            Value = value;
            Type = type;
        }

        public bool RequirementMet(int parameterCount) =>
            Type switch
            {
                NCalcFunctionParamRequirementType.Exact => parameterCount == Value,
                NCalcFunctionParamRequirementType.Minimum => parameterCount >= Value,
                _ => throw new ArgumentOutOfRangeException("Invalid Enum Value")
            };

    }

    class FunctionParamValidator : LogicalExpressionVisitor
    {
        public List<string> Errors { get; private set; } = new List<string>();

        private readonly Dictionary<string, NCalcFunctionParamRequirement> CustomFunctionParamCount;

        private readonly Dictionary<string, NCalcFunctionParamRequirement> NCalcuFunctionParamCount = new Dictionary<string, NCalcFunctionParamRequirement>()
        {
            {"Abs", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Acos", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Asin", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Atan", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Ceiling", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Cos", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Exp", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Floor", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"IEEERemainder", new NCalcFunctionParamRequirement(2, NCalcFunctionParamRequirementType.Exact) },
            {"Log", new NCalcFunctionParamRequirement(2, NCalcFunctionParamRequirementType.Exact) },
            {"Log10", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Pow", new NCalcFunctionParamRequirement(2, NCalcFunctionParamRequirementType.Exact) },
            {"Round", new NCalcFunctionParamRequirement(2, NCalcFunctionParamRequirementType.Exact) },
            {"Sign", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Sin", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Sqrt", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Tan", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Truncate", new NCalcFunctionParamRequirement(1, NCalcFunctionParamRequirementType.Exact) },
            {"Max", new NCalcFunctionParamRequirement(2, NCalcFunctionParamRequirementType.Exact) },
            {"Min", new NCalcFunctionParamRequirement(2, NCalcFunctionParamRequirementType.Exact) },
            {"if",  new NCalcFunctionParamRequirement(3, NCalcFunctionParamRequirementType.Exact) },
            {"in",  new NCalcFunctionParamRequirement(2, NCalcFunctionParamRequirementType.Minimum) }
        };

        public FunctionParamValidator(Dictionary<string, NCalcFunctionParamRequirement> customFunctionParamCount = null)
        {
            CustomFunctionParamCount = customFunctionParamCount ?? new Dictionary<string, NCalcFunctionParamRequirement>();
        }

        public override void Visit(NCalc.Domain.Identifier function)
        {
        }

        public override void Visit(NCalc.Domain.UnaryExpression expression)
        {
            expression.Expression.Accept(this);
        }

        public override void Visit(NCalc.Domain.BinaryExpression expression)
        {
            //Visit left and right
            expression.LeftExpression.Accept(this);
            expression.RightExpression.Accept(this);
        }

        public override void Visit(NCalc.Domain.TernaryExpression expression)
        {
            //Visit left, right and middle
            expression.LeftExpression.Accept(this);
            expression.RightExpression.Accept(this);
            expression.MiddleExpression.Accept(this);
        }

        public override void Visit(Function function)
        {
            if (CustomFunctionParamCount.ContainsKey(function.Identifier.Name))
            {
                ValidateFunctionParamCount(function, CustomFunctionParamCount[function.Identifier.Name]);
            }
            else if (NCalcuFunctionParamCount.ContainsKey(function.Identifier.Name))
            {
                ValidateFunctionParamCount(function, NCalcuFunctionParamCount[function.Identifier.Name]);
            }
            else
            {
                //in this case, its possible a function name in the expression has a typo. It's also possible 
                //the function exists, but we don't have parameter info for it. In either case, I think it would
                //be good to report a validation issue!
                Errors.Add($"No function parameter count info was found for the function '{function.Identifier.Name}'. This may be a typo" +
                    $"or we may need to add additional parameter count data for a custom function for this validation to work!.");
            }

            foreach (var expr in function.Expressions)
            {
                expr.Accept(this);
            }
        }
        public override void Visit(LogicalExpression expression)
        {

        }

        public override void Visit(ValueExpression expression)
        {

        }

        private void ValidateFunctionParamCount(Function function, NCalcFunctionParamRequirement paramRequirement)
        {
            if (!paramRequirement.RequirementMet(function.Expressions.Length))
            {
                var requirementTypeToEnglishPhrase = paramRequirement.Type == NCalcFunctionParamRequirementType.Minimum
                    ? "a minimum of"
                    : "exactly";
                Errors.Add($"function '{function.Identifier.Name}' is expected to have " +
                    $"{requirementTypeToEnglishPhrase} {paramRequirement.Value} parameters," +
                    $" but {function.Expressions.Length} were specified! " +
                    $"Parameters: ({string.Join(",", function.Expressions.Select(x => x.ToString()).ToList())})");
            }
        }
    }