diff --git a/src/DynamicExpresso.Core/ExpressionInterpreter.cs b/src/DynamicExpresso.Core/ExpressionInterpreter.cs new file mode 100644 index 00000000..8958c63a --- /dev/null +++ b/src/DynamicExpresso.Core/ExpressionInterpreter.cs @@ -0,0 +1,459 @@ +using DynamicExpresso.Parsing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using DynamicExpresso.Exceptions; +using DynamicExpresso.Reflection; +using DynamicExpresso.Visitors; + +namespace DynamicExpresso +{ + /// + /// Class used to parse and compile a text expression into an Expression or a Delegate that can be invoked. Expression are written using a subset of C# syntax. + /// Only get properties, Parse and Eval methods are thread safe. + /// + public class ExpressionInterpreter + { + private readonly ParserSettings _settings; + private readonly ISet _visitors = new HashSet(); + + #region Constructors + /// + /// Creates a new ExpressionInterpreter using InterpreterOptions.Default. + /// + public ExpressionInterpreter() + : this(InterpreterOptions.Default) + { + } + + /// + /// Creates a new ExpressionInterpreter using the specified options. + /// + /// + public ExpressionInterpreter(InterpreterOptions options) + { + var caseInsensitive = options.HasFlag(InterpreterOptions.CaseInsensitive); + + var lateBindObject = options.HasFlag(InterpreterOptions.LateBindObject); + + _settings = new ParserSettings(caseInsensitive, lateBindObject); + + if ((options & InterpreterOptions.SystemKeywords) == InterpreterOptions.SystemKeywords) + { + SetIdentifiers(LanguageConstants.Literals); + } + + if ((options & InterpreterOptions.PrimitiveTypes) == InterpreterOptions.PrimitiveTypes) + { + Reference(LanguageConstants.PrimitiveTypes); + Reference(LanguageConstants.CSharpPrimitiveTypes); + } + + if ((options & InterpreterOptions.CommonTypes) == InterpreterOptions.CommonTypes) + { + Reference(LanguageConstants.CommonTypes); + } + + if ((options & InterpreterOptions.LambdaExpressions) == InterpreterOptions.LambdaExpressions) + { + _settings.LambdaExpressions = true; + } + + _visitors.Add(new DisableReflectionVisitor()); + } + + /// + /// Create a new ExpressionInterpreter with the settings copied from another interpreter + /// + internal ExpressionInterpreter(ParserSettings settings) + { + _settings = settings; + } + #endregion + + #region Properties + public bool CaseInsensitive + { + get + { + return _settings.CaseInsensitive; + } + } + + /// + /// Gets a list of registeres types. Add types by using the Reference method. + /// + public IEnumerable ReferencedTypes + { + get + { + return _settings.KnownTypes + .Select(p => p.Value) + .ToList(); + } + } + + /// + /// Gets a list of known identifiers. Add identifiers using SetVariable, SetFunction or SetExpression methods. + /// + public IEnumerable Identifiers + { + get + { + return _settings.Identifiers + .Select(p => p.Value) + .ToList(); + } + } + + /// + /// Gets the available assignment operators. + /// + public AssignmentOperators AssignmentOperators + { + get { return _settings.AssignmentOperators; } + } + #endregion + + #region Options + + /// + /// Allow to set de default numeric type when no suffix is specified (Int by default, Double if real number) + /// + /// + /// + public ExpressionInterpreter SetDefaultNumberType(DefaultNumberType defaultNumberType) + { + _settings.DefaultNumberType = defaultNumberType; + return this; + } + + /// + /// Allows to enable/disable assignment operators. + /// For security when expression are generated by the users is more safe to disable assignment operators. + /// + /// + /// + public ExpressionInterpreter EnableAssignment(AssignmentOperators assignmentOperators) + { + _settings.AssignmentOperators = assignmentOperators; + + return this; + } + #endregion + + #region Visitors + public ISet Visitors + { + get { return _visitors; } + } + + /// + /// Enable reflection expression (like x.GetType().GetMethod() or typeof(double).Assembly) by removing the DisableReflectionVisitor. + /// + /// + public ExpressionInterpreter EnableReflection() + { + var visitor = Visitors.FirstOrDefault(p => p is DisableReflectionVisitor); + if (visitor != null) + Visitors.Remove(visitor); + + return this; + } + #endregion + + #region Register identifiers + /// + /// Allow the specified function delegate to be called from a parsed expression. + /// + /// + /// + /// + public ExpressionInterpreter SetFunction(string name, Delegate value) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + + if (_settings.Identifiers.TryGetValue(name, out var identifier) && identifier is FunctionIdentifier fIdentifier) + { + fIdentifier.AddOverload(value); + } + else + { + SetIdentifier(new FunctionIdentifier(name, value)); + } + + return this; + } + + /// + /// Allow the specified variable to be used in a parsed expression. + /// + /// + /// + /// + public ExpressionInterpreter SetVariable(string name, object value) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + + return SetExpression(name, Expression.Constant(value)); + } + + /// + /// Allow the specified variable to be used in a parsed expression. + /// + /// + /// + /// + public ExpressionInterpreter SetVariable(string name, T value) + { + return SetVariable(name, value, typeof(T)); + } + + /// + /// Allow the specified variable to be used in a parsed expression. + /// + /// + /// + /// + /// + public ExpressionInterpreter SetVariable(string name, object value, Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + + return SetExpression(name, Expression.Constant(value, type)); + } + + /// + /// Allow the specified Expression to be used in a parsed expression. + /// Basically add the specified expression as a known identifier. + /// + /// + /// + /// + public ExpressionInterpreter SetExpression(string name, Expression expression) + { + return SetIdentifier(new Identifier(name, expression)); + } + + /// + /// Allow the specified list of identifiers to be used in a parsed expression. + /// Basically add the specified expressions as a known identifier. + /// + /// + /// + public ExpressionInterpreter SetIdentifiers(IEnumerable identifiers) + { + foreach (var i in identifiers) + SetIdentifier(i); + + return this; + } + + /// + /// Allow the specified identifier to be used in a parsed expression. + /// Basically add the specified expression as a known identifier. + /// + /// + /// + public ExpressionInterpreter SetIdentifier(Identifier identifier) + { + if (identifier == null) + throw new ArgumentNullException(nameof(identifier)); + + if (LanguageConstants.ReservedKeywords.Contains(identifier.Name)) + throw new InvalidOperationException($"{identifier.Name} is a reserved word"); + + _settings.Identifiers[identifier.Name] = identifier; + + return this; + } + #endregion + + #region Register referenced types + /// + /// Allow the specified type to be used inside an expression. The type will be available using its name. + /// If the type contains method extensions methods they will be available inside expressions. + /// + /// + /// + public ExpressionInterpreter Reference(Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + return Reference(type, type.Name); + } + + /// + /// Allow the specified type to be used inside an expression. + /// See Reference(Type, string) method. + /// + /// + /// + public ExpressionInterpreter Reference(IEnumerable types) + { + if (types == null) + throw new ArgumentNullException(nameof(types)); + + foreach (var t in types) + Reference(t); + + return this; + } + + /// + /// Allow the specified type to be used inside an expression by using a custom alias. + /// If the type contains extensions methods they will be available inside expressions. + /// + /// + /// Public name that must be used in the expression. + /// + public ExpressionInterpreter Reference(Type type, string typeName) + { + return Reference(new ReferenceType(typeName, type)); + } + + /// + /// Allow the specified type to be used inside an expression by using a custom alias. + /// If the type contains extensions methods they will be available inside expressions. + /// + /// + /// + public ExpressionInterpreter Reference(ReferenceType type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + _settings.KnownTypes[type.Name] = type; + + foreach (var extensionMethod in type.ExtensionMethods) + { + _settings.ExtensionMethods.Add(extensionMethod); + } + + return this; + } + #endregion + + #region Parse + + /// + /// Parse a text expression and convert it into a ParseResult with Delegate type info. + /// + /// Expression statement + /// + /// + /// + public ParseResult Parse(string expressionText, params string[] parametersNames) + { + var delegateInfo = ReflectionExtensions.GetDelegateInfo(typeof(TDelegate), parametersNames); + var parseResult = Parse( + expressionText, + delegateInfo.ReturnType, + delegateInfo.Parameters.Select(x => x.Expression).ToArray()); + + return new ParseResult( + expression: parseResult.Expression, + usedParameters: parseResult.UsedParameters, + declaredParameters: parseResult.DeclaredParameters, + types: parseResult.Types, + identifiers: parseResult.Identifiers); + } + + /// + /// Parse a text expression. + /// + /// Expression statement + /// Expression input parameters + /// + /// + public ParseResult Parse(string expressionText, params ParameterExpression[] parameters) + { + return Parse(expressionText, typeof(void), parameters); + } + + /// + /// Parse a text expression. + /// + /// Expression statement + /// Expected expression return type + /// Expression input parameters + /// + /// + public ParseResult Parse(string expressionText, Type expressionReturnType, params ParameterExpression[] parameters) + { + return Parse(expressionText, expressionReturnType, parameters.AsEnumerable()); + } + + /// + /// Parse a text expression. + /// + /// Expression statement + /// Expected expression return type + /// Expression input parameters + /// + /// + public ParseResult Parse(string expressionText, Type expressionReturnType, IEnumerable parameters) + { + if (parameters == null) + parameters = Enumerable.Empty(); + + var arguments = new ParserArguments( + expressionText, + _settings, + expressionReturnType, + parameters.Select(x => new Parameter(x))); + + var expression = Visitors.Aggregate(Parser.Parse(arguments), (current, visitor) => visitor.Visit(current)); + + var lambda = new ParseResult( + expression: expression, + usedParameters: arguments.UsedParameters.Select(x => x.Expression), + declaredParameters: arguments.DeclaredParameters.Select(x => x.Expression), + types: arguments.UsedTypes, + identifiers: arguments.UsedIdentifiers); + +#if TEST_DetectIdentifiers + AssertDetectIdentifiers(lambda); +#endif + + return lambda; + } + + #endregion + + #region Detection + + public IdentifiersInfo DetectIdentifiers(string expression) + { + var detector = new Detector(_settings); + + return detector.DetectIdentifiers(expression); + } + + #endregion + + #region Private methods + +#if TEST_DetectIdentifiers + private void AssertDetectIdentifiers(Lambda lambda) + { + var info = DetectIdentifiers(lambda.ExpressionText); + + if (info.Identifiers.Count() != lambda.Identifiers.Count()) + throw new Exception("Detected identifiers doesn't match actual identifiers"); + if (info.Types.Count() != lambda.Types.Count()) + throw new Exception("Detected types doesn't match actual types"); + if (info.UnknownIdentifiers.Count() != lambda.UsedParameters.Count()) + throw new Exception("Detected unknown identifiers doesn't match actual parameters"); + } +#endif + #endregion + } +} diff --git a/src/DynamicExpresso.Core/InterpreterExtensions.cs b/src/DynamicExpresso.Core/InterpreterExtensions.cs new file mode 100644 index 00000000..e706bd6b --- /dev/null +++ b/src/DynamicExpresso.Core/InterpreterExtensions.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.ExceptionServices; +using DynamicExpresso.Exceptions; + +namespace DynamicExpresso +{ + /// + /// Interpreter extensions. + /// + public static class InterpreterExtensions + { + /// + /// Parse a text expression with expected return type. + /// + /// + /// Expression statement + /// + /// + /// + public static ParseResult ParseWithReturnType( + this ExpressionInterpreter interpreter, + string expressionText, + params ParameterExpression[] parameters) + { + return interpreter.Parse(expressionText, typeof(TReturnType), parameters); + } + + /// + /// Compiles lambda with declared parameters. + /// + public static Delegate Compile(this ParseResult parseResult) + { + var lambdaExpression = Expression.Lambda(parseResult.Expression, parseResult.DeclaredParameters.ToArray()); + + return lambdaExpression.Compile(); + } + + /// + /// Compiles lambda with declared parameters. + /// + public static TDelegate Compile(this ParseResult parseResult) + { + var lambdaExpression = Expression.Lambda(parseResult.Expression, parseResult.DeclaredParameters.ToArray()); + + return lambdaExpression.Compile(); + } + + /// + /// Compiles lambda with declared parameters. + /// + public static TDelegate Compile(this ParseResult parseResult) + { + return Compile((ParseResult) parseResult); + } + + /// + /// Convert parse result to expression. + /// + public static Expression AsExpression(this ParseResult parseResult) + { + return Expression.Lambda(parseResult.Expression, parseResult.DeclaredParameters.ToArray()); + } + + /// + /// Convert parse result to expression. + /// + public static Expression AsExpression(this ParseResult parseResult) + { + return ((ParseResult) parseResult).AsExpression(); + } + + /// + /// Convert parse result to lambda expression. + /// + public static LambdaExpression AsLambdaExpression(this ParseResult parseResult, Type delegateType) + { + return Expression.Lambda(delegateType, parseResult.Expression, parseResult.DeclaredParameters.ToArray()); + } + + /// + /// Evaluate expression. + /// + public static object Eval(this ExpressionInterpreter interpreter, string expressionText, + Func a1) + => interpreter.Eval(expressionText, a1.Value()); + + /// + /// Evaluate expression. + /// + public static object Eval(this ExpressionInterpreter interpreter, string expressionText, + Func a1, + Func a2) + => interpreter.Eval(expressionText, a1.Value(), a2.Value()); + + /// + /// Evaluate expression. + /// + public static object Eval(this ExpressionInterpreter interpreter, string expressionText, + Func a1, + Func a2, + Func a3) + => interpreter.Eval(expressionText, a1.Value(), a2.Value(), a3.Value()); + + /// + /// Evaluate expression. + /// + public static object Eval(this ExpressionInterpreter interpreter, string expressionText, + Func a1, + Func a2, + Func a3, + Func a4) + => interpreter.Eval(expressionText, a1.Value(), a2.Value(), a3.Value(), a4.Value()); + + private static Parameter Value(this Func parameter) + { + return new Parameter( + parameter.Method.GetParameters().First().Name, + parameter.Method.ReturnType, + parameter(default)); + } + + /// + /// Evaluate expression. + /// + public static object Eval(this ExpressionInterpreter interpreter, string expressionText, params Parameter[] parameters) + { + return interpreter.Eval(expressionText, typeof(void), parameters.AsEnumerable()); + } + + /// + /// Evaluate expression. + /// + public static TReturnType Eval(this ExpressionInterpreter interpreter, string expressionText, params Parameter[] parameters) + { + return (TReturnType) interpreter.Eval(expressionText, typeof(TReturnType), parameters.AsEnumerable()); + } + + /// + /// Evaluate expression. + /// + public static object Eval(this ExpressionInterpreter interpreter, string expressionText, Type expressionReturnType, params Parameter[] parameters) + { + return interpreter.Eval(expressionText, expressionReturnType, parameters.AsEnumerable()); + } + + /// + /// Evaluate expression. + /// + public static object Eval(this ExpressionInterpreter interpreter, string expressionText, Type expressionReturnType, IEnumerable parameters) + { + try + { + return interpreter + .Parse(expressionText, expressionReturnType, parameters.Select(x => Expression.Parameter(x.Type, x.Name))) + .Compile() + .DynamicInvoke(parameters.Select(x => x.Value).ToArray()); + } + catch (TargetInvocationException exc) + { + if (exc.InnerException != null) + ExceptionDispatchInfo.Capture(exc.InnerException).Throw(); + + throw; + } + } + } +} diff --git a/src/DynamicExpresso.Core/ParseResult.cs b/src/DynamicExpresso.Core/ParseResult.cs new file mode 100644 index 00000000..033ea456 --- /dev/null +++ b/src/DynamicExpresso.Core/ParseResult.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace DynamicExpresso +{ + /// + /// Represents an expression parse result. + /// + public class ParseResult + { + public ParseResult( + Expression expression, + IEnumerable usedParameters, + IEnumerable declaredParameters, + IEnumerable types, + IEnumerable identifiers) + { + Expression = expression; + UsedParameters = usedParameters; + DeclaredParameters = declaredParameters; + Types = types; + Identifiers = identifiers; + } + + /// + /// Gets the parsed expression. + /// + /// The expression. + public Expression Expression { get; } + + /// + /// Gets the parameters actually used in the expression parsed. + /// + /// The used parameters. + public IEnumerable UsedParameters { get; } + + /// + /// Gets the parameters declared when parsing the expression. + /// + /// The declared parameters. + public IEnumerable DeclaredParameters { get; } + + /// + /// Gets the references types in parsed expression. + /// + /// The references types. + public IEnumerable Types { get; } + + /// + /// Gets the identifiers in parsed expression. + /// + /// The identifiers. + public IEnumerable Identifiers { get; } + } + + public class ParseResult : ParseResult + { + public ParseResult( + Expression expression, + IEnumerable usedParameters, + IEnumerable declaredParameters, + IEnumerable types, + IEnumerable identifiers) : base(expression, usedParameters, declaredParameters, types, identifiers) + { + } + } +}