diff --git a/src/Framework/Framework/Binding/BindingFactory.cs b/src/Framework/Framework/Binding/BindingFactory.cs index dc6ae7d584..6b60c2b842 100644 --- a/src/Framework/Framework/Binding/BindingFactory.cs +++ b/src/Framework/Framework/Binding/BindingFactory.cs @@ -44,7 +44,7 @@ public static IBinding CreateBinding(this BindingCompilationService service, Typ if (ctor == null) throw new NotSupportedException($"Could not find .ctor(BindingCompilationService service, object[] properties) on binding '{type.FullName}'."); var bindingServiceParam = Expression.Parameter(typeof(BindingCompilationService)); var propertiesParam = Expression.Parameter(typeof(object?[])); - var expression = Expression.New(ctor, bindingServiceParam, TypeConversion.ImplicitConversion(propertiesParam, ctor.GetParameters()[1].ParameterType, throwException: true)!); + var expression = Expression.New(ctor, bindingServiceParam, TypeConversion.EnsureImplicitConversion(propertiesParam, ctor.GetParameters()[1].ParameterType)); return Expression.Lambda>(expression, bindingServiceParam, propertiesParam).CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression); })(service, properties); } diff --git a/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index dda018f62d..4d387a8742 100644 --- a/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -123,6 +123,7 @@ protected override Expression VisitInterpolatedStringExpression(InterpolatedStri { // Translate to a String.Format(...) call var arguments = node.Arguments.Select((arg, index) => HandleErrors(node.Arguments[index], Visit)!).ToArray(); + ThrowOnErrors(); return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format) }.Concat(arguments).ToArray()); } else @@ -294,7 +295,7 @@ protected override Expression VisitAssemblyQualifiedName(AssemblyQualifiedNameBi protected override Expression VisitConditionalExpression(ConditionalExpressionBindingParserNode node) { - var condition = HandleErrors(node.ConditionExpression, n => TypeConversion.ImplicitConversion(Visit(n), typeof(bool), true)); + var condition = HandleErrors(node.ConditionExpression, n => TypeConversion.EnsureImplicitConversion(Visit(n), typeof(bool))); var trueExpr = HandleErrors(node.TrueExpression, Visit)!; var falseExpr = HandleErrors(node.FalseExpression, Visit)!; ThrowOnErrors(); diff --git a/src/Framework/Framework/Compilation/Binding/ExpressionNullPropagationVisitor.cs b/src/Framework/Framework/Compilation/Binding/ExpressionNullPropagationVisitor.cs index 2c0f9c1729..51ead1e4ee 100644 --- a/src/Framework/Framework/Compilation/Binding/ExpressionNullPropagationVisitor.cs +++ b/src/Framework/Framework/Compilation/Binding/ExpressionNullPropagationVisitor.cs @@ -123,8 +123,8 @@ protected override Expression VisitConditional(ConditionalExpression node) if (ifTrue.Type != ifFalse.Type) { var nullable = ifTrue.Type.IsNullable() ? ifTrue.Type : ifFalse.Type; - ifTrue = TypeConversion.ImplicitConversion(ifTrue, nullable, throwException: true)!; - ifFalse = TypeConversion.ImplicitConversion(ifFalse, nullable, throwException: true)!; + ifTrue = TypeConversion.EnsureImplicitConversion(ifTrue, nullable); + ifFalse = TypeConversion.EnsureImplicitConversion(ifFalse, nullable); } return Expression.Condition(test, ifTrue, ifFalse); }); @@ -181,7 +181,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) { return CheckForNull(Visit(node.Arguments.First()), index => { - var convertedIndex = TypeConversion.ImplicitConversion(index, node.Method.GetParameters().First().ParameterType, throwException: true)!; + var convertedIndex = TypeConversion.EnsureImplicitConversion(index, node.Method.GetParameters().First().ParameterType); return Expression.Call(target, node.Method, new[] { convertedIndex }.Concat(node.Arguments.Skip(1))); }); }, suppress: node.Object?.Type?.IsNullable() ?? true); @@ -244,7 +244,7 @@ protected Expression CheckForNull(Expression? parameter, Func().Distinct().ToArray(); + + + // https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/expressions.md#1145-binary-operator-overload-resolution + // The set of candidate user-defined operators provided by X and Y for the operation operator «op»(x, y) is determined. The set consists of the union of the candidate operators provided by X and the candidate operators provided by Y, each determined using the rules of §11.4.6. + + var candidateMethods = + searchTypes + .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)) + .Where(m => m.Name == operatorName && !m.IsGenericMethod && m.GetParameters().Length == 2) + .Distinct() + .ToArray(); + + // The overload resolution rules of §11.6.4 are applied to the set of candidate operators to select the best operator with respect to the argument list (x, y), and this operator becomes the result of the overload resolution process. If overload resolution fails to select a single best operator, a binding-time error occurs. + + var matchingMethods = FindValidMethodOverloads(candidateMethods, operatorName, false, null, new[] { a, b }, null); + var liftToNull = matchingMethods.Count == 0 && (a.Type.IsNullable() || b.Type.IsNullable()); + if (liftToNull) + { + matchingMethods = FindValidMethodOverloads(candidateMethods, operatorName, false, null, new[] { a.UnwrapNullable(), b.UnwrapNullable() }, null); + } + + if (matchingMethods.Count == 0) + return null; + var overload = BestOverload(matchingMethods, searchTypes, operatorName); + var parameters = overload.Method.GetParameters(); + + return Expression.MakeBinary( + operatorType, + TypeConversion.EnsureImplicitConversion(a, parameters[0].ParameterType), + TypeConversion.EnsureImplicitConversion(b, parameters[1].ParameterType), + liftToNull: liftToNull, + method: overload.Method + ); + } private MethodRecognitionResult FindValidMethodOverloads(Expression? target, Type type, string name, BindingFlags flags, Type[]? typeArguments, Expression[] arguments, IDictionary? namedArgs) { + bool extensionMethods = false; var methods = FindValidMethodOverloads(type.GetAllMethods(flags), name, false, typeArguments, arguments, namedArgs); if (methods.Count == 1) return methods[0]; if (methods.Count == 0) { // We did not find any match in regular methods => try extension methods - if (target != null) + if (target != null && flags.HasFlag(BindingFlags.Instance)) { + extensionMethods = true; // Change to a static call var newArguments = new[] { target }.Concat(arguments).ToArray(); var extensions = FindValidMethodOverloads(GetAllExtensionMethods(), name, true, typeArguments, newArguments, namedArgs); @@ -270,13 +313,25 @@ private MethodRecognitionResult FindValidMethodOverloads(Expression? target, Typ } // There are multiple method candidates - methods = methods.OrderBy(s => s.CastCount).ThenBy(s => s.AutomaticTypeArgCount).ThenBy(s => s.HasParamsAttribute).ToList(); + return BestOverload(methods, extensionMethods ? Type.EmptyTypes : new[] { type }, name); + } + + private MethodRecognitionResult BestOverload(List methods, Type[] callingOnType, string name) + { + if (methods.Count == 1) + return methods[0]; + + methods = methods + .OrderBy(s => GetNearestInheritanceDistance(s.Method.DeclaringType, callingOnType)) + .ThenBy(s => s.CastCount) + .ThenBy(s => s.AutomaticTypeArgCount) + .ThenBy(s => s.HasParamsAttribute).ToList(); var method = methods.First(); var method2 = methods.Skip(1).First(); - if (method.AutomaticTypeArgCount == method2.AutomaticTypeArgCount && method.CastCount == method2.CastCount && method.HasParamsAttribute == method2.HasParamsAttribute) + if (method.AutomaticTypeArgCount == method2.AutomaticTypeArgCount && method.CastCount == method2.CastCount && method.HasParamsAttribute == method2.HasParamsAttribute && GetNearestInheritanceDistance(method.Method.DeclaringType, callingOnType) == GetNearestInheritanceDistance(method2.Method.DeclaringType, callingOnType)) { // TODO: this behavior is not completed. Implement the same behavior as in roslyn. - var foundOverloads = $"{method.Method}, {method2.Method}"; + var foundOverloads = $"{ReflectionUtils.FormatMethodInfo(method.Method, stripNamespace: true)}, {ReflectionUtils.FormatMethodInfo(method2.Method, stripNamespace: true)}"; throw new InvalidOperationException($"Found ambiguous overloads of method '{name}'. The following overloads were found: {foundOverloads}."); } return method; @@ -305,6 +360,40 @@ private List FindValidMethodOverloads(IEnumerable t.ToCode()))}'."); + return distance; + } + sealed class MethodRecognitionResult { public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Expression[] arguments, MethodInfo method, int paramsArrayCount, bool isExtension, bool hasParamsAttribute) @@ -329,6 +418,7 @@ public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Express private MethodRecognitionResult? TryCallMethod(MethodInfo method, Type[]? typeArguments, Expression[] positionalArguments, IDictionary? namedArguments) { + if (positionalArguments.Contains(null)) throw new ArgumentNullException("positionalArguments[]"); var parameters = method.GetParameters(); if (!TryPrepareArguments(parameters, positionalArguments, namedArguments, out var args, out var castCount)) @@ -406,7 +496,7 @@ public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Express if (args.Length == i + 1 && hasParamsArrayAttributes && !args[i].Type.IsArray) { var converted = positionalArguments.Skip(i) - .Select(a => TypeConversion.ImplicitConversion(a, elm, throwException: true)!) + .Select(a => TypeConversion.EnsureImplicitConversion(a, elm)) .ToArray(); args[i] = NewArrayExpression.NewArrayInit(elm, converted); } @@ -561,131 +651,11 @@ private static bool TryPrepareArguments(ParameterInfo[] parameters, Expression[] return null; } - public Expression EqualsMethod(Expression left, Expression right) - { - Expression? equatable = null; - Expression? theOther = null; - if (typeof(IEquatable<>).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IEquatable<>).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - var m = CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { theOther! }); - if (m != null) return m; - } - - if (left.Type.IsValueType) - { - equatable = left; - theOther = right; - } - else if (left.Type.IsValueType) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - theOther = TypeConversion.ImplicitConversion(theOther!, equatable.Type); - if (theOther != null) return Expression.Equal(equatable, theOther); - } - - return CallMethod(left, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { right }); - } - - public Expression CompareMethod(Expression left, Expression right) - { - Type compareType = typeof(object); - Expression? equatable = null; - Expression? theOther = null; - if (typeof(IComparable<>).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IComparable<>).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - else if (typeof(IComparable).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IComparable).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - return CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Compare", null, new[] { theOther! }); - } - throw new NotSupportedException("IComparable is not implemented on any of specified types"); - } - public Expression GetUnaryOperator(Expression expr, ExpressionType operation) { var binder = (DynamicMetaObjectBinder)Microsoft.CSharp.RuntimeBinder.Binder.UnaryOperation( CSharpBinderFlags.None, operation, typeof(object), ExpressionHelper.GetBinderArguments(1)); return ExpressionHelper.ApplyBinder(binder, true, expr)!; } - - public Expression GetBinaryOperator(Expression left, Expression right, ExpressionType operation) - { - if (operation == ExpressionType.Coalesce) - { - // in bindings, most expressions will be nullable due to automatic null-propagation - // the null propagation visitor however runs after this, so we need to convert left to nullable - // to make the validation in Expression.Coalesce happy - var leftNullable = - left.Type.IsValueType && !left.Type.IsNullable() - ? Expression.Convert(left, typeof(Nullable<>).MakeGenericType(left.Type)) - : left; - return Expression.Coalesce(leftNullable, right); - } - if (operation == ExpressionType.Assign) - { - return UpdateMember(left, TypeConversion.ImplicitConversion(right, left.Type, true, true)!) - .NotNull($"Expression '{right}' cannot be assigned into '{left}'."); - } - - // TODO: type conversions - if (operation == ExpressionType.AndAlso) return Expression.AndAlso(left, right); - else if (operation == ExpressionType.OrElse) return Expression.OrElse(left, right); - - var binder = (DynamicMetaObjectBinder)Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation( - CSharpBinderFlags.None, operation, typeof(object), ExpressionHelper.GetBinderArguments(2)); - var result = ExpressionHelper.ApplyBinder(binder, false, left, right); - if (result != null) return result; - if (operation == ExpressionType.Equal) return EqualsMethod(left, right); - if (operation == ExpressionType.NotEqual) return Expression.Not(EqualsMethod(left, right)); - - - // try converting left to right.Type and vice versa - // needed to enum with pseudo-string literal operations - // if (TypeConversion.ImplicitConversion(left, right.Type) is {} leftConverted) - // return GetBinaryOperator(leftConverted, right, operation); - // if (TypeConversion.ImplicitConversion(right, left.Type) is {} rightConverted) - // return GetBinaryOperator(left, rightConverted, operation); - - // lift the operator - if (left.Type.IsNullable() || right.Type.IsNullable()) - return GetBinaryOperator(left.UnwrapNullable(), right.UnwrapNullable(), operation); - - throw new Exception($"could not apply { operation } binary operator to { left } and { right }"); - // TODO: comparison operators - } } } diff --git a/src/Framework/Framework/Compilation/Binding/MethodGroupExpression.cs b/src/Framework/Framework/Compilation/Binding/MethodGroupExpression.cs index 3876324916..6a8e7f9c3c 100644 --- a/src/Framework/Framework/Compilation/Binding/MethodGroupExpression.cs +++ b/src/Framework/Framework/Compilation/Binding/MethodGroupExpression.cs @@ -92,6 +92,9 @@ public Expression CreateDelegateExpression() public Expression CreateMethodCall(IEnumerable args, MemberExpressionFactory memberExpressionFactory) { var argsArray = args.ToArray(); + if (Array.FindIndex(argsArray, a => a is null || a.Type == typeof(UnknownTypeSentinel)) is var argIdx && argIdx >= 0) + throw new Exception($"Argument {argIdx} is invalid: {this.MethodName}({string.Join(", ", argsArray.Select(a => a))})"); + if (IsStatic) { return memberExpressionFactory.CallMethod(((StaticClassIdentifierExpression)Target).Type, BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy, MethodName, TypeArgs, argsArray); diff --git a/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs new file mode 100644 index 0000000000..ba475be74d --- /dev/null +++ b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs @@ -0,0 +1,222 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using DotVVM.Framework.Utils; +using FastExpressionCompiler; + +namespace DotVVM.Framework.Compilation.Binding +{ + static class OperatorResolution + { + public static Expression GetBinaryOperator( + this MemberExpressionFactory expressionFactory, + Expression left, + Expression right, + ExpressionType operation) + { + if (operation == ExpressionType.Coalesce) + { + // in bindings, most expressions will be nullable due to automatic null-propagation + // the null propagation visitor however runs after this, so we need to convert left to nullable + // to make the validation in Expression.Coalesce happy + var leftNullable = + left.Type.IsValueType && !left.Type.IsNullable() + ? Expression.Convert(left, typeof(Nullable<>).MakeGenericType(left.Type)) + : left; + return Expression.Coalesce(leftNullable, right); + } + + if (operation == ExpressionType.Assign) + { + return expressionFactory.UpdateMember(left, TypeConversion.EnsureImplicitConversion(right, left.Type, true)) + .NotNull($"Expression '{right}' cannot be assigned into '{left}'."); + } + + // lift to nullable types when one side is `null` + if (left is ConstantExpression { Value: null } && right.Type.IsValueType) + { + left = Expression.Constant(null, right.Type.MakeNullableType()); + right = Expression.Convert(right, right.Type.MakeNullableType()); + } + if (right is ConstantExpression { Value: null } && left.Type.IsValueType) + { + left = Expression.Convert(left, left.Type.MakeNullableType()); + right = Expression.Constant(null, left.Type.MakeNullableType()); + } + + // lift the other side to null + if (left.Type.IsNullable() && right.Type.IsValueType && !right.Type.IsNullable()) + { + right = Expression.Convert(right, right.Type.MakeNullableType()); + } + else if (right.Type.IsNullable() && left.Type.IsValueType && !left.Type.IsNullable()) + { + left = Expression.Convert(left, left.Type.MakeNullableType()); + } + + var leftType = left.Type.UnwrapNullableType(); + var rightType = right.Type.UnwrapNullableType(); + + // we only support booleans + if (operation == ExpressionType.AndAlso) + return Expression.AndAlso(TypeConversion.EnsureImplicitConversion(left, typeof(bool)), TypeConversion.EnsureImplicitConversion(right, typeof(bool))); + else if (operation == ExpressionType.OrElse) + return Expression.OrElse(TypeConversion.EnsureImplicitConversion(left, typeof(bool)), TypeConversion.EnsureImplicitConversion(right, typeof(bool))); + + // skip the slow overload resolution if possible + if (leftType == rightType && !leftType.IsEnum && leftType.IsPrimitive) + return Expression.MakeBinary(operation, left, right); + if (operation == ExpressionType.Add && leftType == typeof(string) && rightType == typeof(string)) + { + return Expression.Add(left, right, typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) })); + } + + var customOperator = operation switch { + ExpressionType.Add => "op_Addition", + ExpressionType.Subtract => "op_Subtraction", + ExpressionType.Multiply => "op_Multiply", + ExpressionType.Divide => "op_Division", + ExpressionType.Modulo => "op_Modulus", + ExpressionType.LeftShift => "op_LeftShift", + ExpressionType.RightShift => "op_RightShift", + ExpressionType.And => "op_BitwiseAnd", + ExpressionType.Or => "op_BitwiseOr", + ExpressionType.ExclusiveOr => "op_ExclusiveOr", + ExpressionType.Equal => "op_Equality", + ExpressionType.NotEqual => "op_Inequality", + ExpressionType.GreaterThan => "op_GreaterThan", + ExpressionType.LessThan => "op_LessThan", + ExpressionType.GreaterThanOrEqual => "op_GreaterThanOrEqual", + ExpressionType.LessThanOrEqual => "op_LessThanOrEqual", + _ => null + }; + + // Try to find user defined operator + if (customOperator != null && (!leftType.IsPrimitive || !rightType.IsPrimitive)) + { + var customOperatorExpr = expressionFactory.TryCallCustomBinaryOperator(left, right, customOperator, operation); + if (customOperatorExpr is {}) + return customOperatorExpr; + } + + if (leftType.IsEnum && rightType.IsEnum && leftType != rightType) + { + throw new InvalidOperationException($"Cannot apply {operation} operator to two different enum types: {leftType.Name}, {rightType.Name}."); + } + + if (operation is ExpressionType.Equal or ExpressionType.NotEqual && !leftType.IsValueType && !rightType.IsValueType) + { + // https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/expressions.md#11117-reference-type-equality-operators + // Every class type C implicitly provides the following predefined reference type equality operators: + // bool operator ==(C x, C y); + // bool operator !=(C x, C y); + return ReferenceEquality(left, right, operation == ExpressionType.NotEqual); + } + + if (operation is ExpressionType.LeftShift or ExpressionType.RightShift) + { + // https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/expressions.md#1110-shift-operators + // * shift operators always take int32 as the second argument + var rightConverted = ConvertToMaybeNullable(right, typeof(int), true)!; + // * the first argument is int, uint, long, ulong (in this order) + var leftConverted = ConvertToMaybeNullable(left, typeof(int), false) ?? ConvertToMaybeNullable(left, typeof(uint), false) ?? ConvertToMaybeNullable(left, typeof(long), false) ?? ConvertToMaybeNullable(left, typeof(ulong), false)!; + if (leftConverted is null) + throw new InvalidOperationException($"Cannot apply {operation} operator to type {leftType.ToCode()}. The type must be convertible to an integer or have a custom operator defined."); + return operation == ExpressionType.LeftShift ? Expression.LeftShift(leftConverted, rightConverted) : Expression.RightShift(leftConverted, rightConverted); + } + + // List of types in order of precendence + var enumType = leftType.IsEnum ? leftType : rightType.IsEnum ? rightType : null; + // all operators have defined "overloads" for two + var typeList = operation switch { + ExpressionType.Or or ExpressionType.And or ExpressionType.ExclusiveOr => + new[] { typeof(bool), enumType, typeof(int), typeof(uint), typeof(long), typeof(ulong) }, + _ => + new[] { enumType, typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) } + }; + + foreach (var commonType in typeList) + { + if (commonType == null) continue; + + var leftConverted = ConvertToMaybeNullable(left, commonType, throwExceptions: false); + var rightConverted = ConvertToMaybeNullable(right, commonType, throwExceptions: false); + + if (leftConverted != null && rightConverted != null) + { + return MakeBinary(operation, leftConverted, rightConverted); + } + } + + if (operation == ExpressionType.Add && (leftType == typeof(string) || rightType == typeof(string))) + { + return Expression.Add( + Expression.Convert(left, typeof(object)), + Expression.Convert(right, typeof(object)), + typeof(string).GetMethod("Concat", new[] { typeof(object), typeof(object) }) + ); + } + + + // if (left.Type.IsNullable() || right.Type.IsNullable()) + // return GetBinaryOperator(expressionFactory, left.UnwrapNullable(), right.UnwrapNullable(), operation); + + throw new InvalidOperationException($"Cannot apply {operation} operator to types {left.Type.Name} and {right.Type.Name}."); + } + + static Expression ReferenceEquality(Expression left, Expression right, bool not) + { + // * It is a binding-time error to use the predefined reference type equality operators to compare two references that are known to be different at binding-time. For example, if the binding-time types of the operands are two class types, and if neither derives from the other, then it would be impossible for the two operands to reference the same object. Thus, the operation is considered a binding-time error. + var leftT = left.Type; + var rightT = right.Type; + if (leftT != rightT && !(leftT.IsAssignableFrom(rightT) || rightT.IsAssignableFrom(leftT))) + { + if (!leftT.IsInterface && rightT.IsInterface) + throw new InvalidOperationException($"Cannot compare types {leftT.ToCode()} and {rightT.ToCode()}, because the classes are unrelated."); + if (leftT.IsSealed || rightT.IsSealed) + throw new InvalidOperationException($"Cannot compare types {leftT.ToCode()} and {rightT.ToCode()}, because {(leftT.IsSealed ? leftT : rightT).ToCode(stripNamespace: true)} is sealed and does not implement {rightT.ToCode(stripNamespace: true)}."); + } + return not ? Expression.ReferenceNotEqual(left, right) : Expression.ReferenceEqual(left, right); + } + + static Expression? ConvertToMaybeNullable( + Expression expression, + Type targetType, + bool throwExceptions + ) + { + return TypeConversion.ImplicitConversion(expression, targetType) ?? + TypeConversion.ImplicitConversion(expression, targetType.MakeNullableType(), throwExceptions); + } + + static Expression MakeBinary(ExpressionType type, Expression left, Expression right) + { + // Expression.MakeBinary doesn't handle enums, we need to convert it to the int and back + // It works however, for Equals/NotEquals + Type? enumType = null; + + if (type != ExpressionType.Equal && type != ExpressionType.NotEqual) + { + if (left.Type.UnwrapNullableType().IsEnum) + { + enumType = left.Type.UnwrapNullableType(); + left = ConvertToMaybeNullable(left, Enum.GetUnderlyingType(enumType), true)!; + } + if (right.Type.UnwrapNullableType().IsEnum) + { + enumType = right.Type.UnwrapNullableType(); + right = ConvertToMaybeNullable(right, Enum.GetUnderlyingType(enumType), true)!; + } + } + + var result = Expression.MakeBinary(type, left, right); + if (enumType != null && result.Type != typeof(bool)) + { + return Expression.Convert(result, enumType); + } + return result; + } + } +} diff --git a/src/Framework/Framework/Compilation/Binding/TypeConversions.cs b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs index f9a297d5d9..0b2d47c600 100644 --- a/src/Framework/Framework/Compilation/Binding/TypeConversions.cs +++ b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs @@ -11,78 +11,24 @@ using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.HelperNamespace; +using FastExpressionCompiler; namespace DotVVM.Framework.Compilation.Binding { public class TypeConversion { - private static Dictionary> ImplicitNumericConversions = new Dictionary>(); - private static readonly Dictionary typePrecedence; - - /// - /// Performs implicit conversion between two expressions depending on their type precedence - /// - /// - /// - internal static void Convert(ref Expression le, ref Expression re) - { - if (typePrecedence.ContainsKey(le.Type) && typePrecedence.ContainsKey(re.Type)) - { - if (typePrecedence[le.Type] > typePrecedence[re.Type]) re = Expression.Convert(re, le.Type); - if (typePrecedence[le.Type] < typePrecedence[re.Type]) le = Expression.Convert(le, re.Type); - } - } - - /// - /// Performs implicit conversion on an expression against a specified type - /// - /// - /// - /// - internal static Expression Convert(Expression le, Type type) - { - if (typePrecedence.ContainsKey(le.Type) && typePrecedence.ContainsKey(type)) - { - if (typePrecedence[le.Type] < typePrecedence[type]) return Expression.Convert(le, type); - } - if (le.Type.IsNullable() && Nullable.GetUnderlyingType(le.Type) == type) - { - le = Expression.Property(le, "Value"); - } - if (type.IsNullable() && Nullable.GetUnderlyingType(type) == le.Type) - { - le = Expression.Convert(le, type); - } - if (type == typeof(object)) - { - return Expression.Convert(le, type); - } - if (le.Type == typeof(object)) - { - return Expression.Convert(le, type); - } - return le; - } - - /// - /// Compares two types for implicit conversion - /// - /// The source type - /// The destination type - /// -1 if conversion is not possible, 0 if no conversion necessary, +1 if conversion possible - internal static int CanConvert(Type from, Type to) - { - if (typePrecedence.ContainsKey(@from) && typePrecedence.ContainsKey(to)) - { - return typePrecedence[to] - typePrecedence[@from]; - } - else - { - if (@from == to) return 0; - if (to.IsAssignableFrom(@from)) return 1; - } - return -1; - } + private static Dictionary ImplicitNumericConversions = new() { + [typeof(sbyte)] = new Type[] { typeof(short), typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) }, + [typeof(byte)] = new Type[] { typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }, + [typeof(short)] = new Type[] { typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) }, + [typeof(ushort)] = new Type[] { typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }, + [typeof(int)] = new Type[] { typeof(long), typeof(float), typeof(double), typeof(decimal) }, + [typeof(uint)] = new Type[] { typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }, + [typeof(long)] = new Type[] { typeof(float), typeof(double), typeof(decimal) }, + [typeof(ulong)] = new Type[] { typeof(float), typeof(double), typeof(decimal) }, + [typeof(char)] = new Type[] { typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }, + [typeof(float)] = new Type[] { typeof(double) }, + }; // 6.1.7 Boxing Conversions // A boxing conversion permits a value-type to be implicitly converted to a reference type. A boxing conversion exists from any non-nullable-value-type to object and dynamic, @@ -160,12 +106,14 @@ public static Expression BoxToObject(Expression src) } //TODO: Refactor ImplicitConversion usages to EnsureImplicitConversion where applicable to take advantage of nullability - public static Expression EnsureImplicitConversion(Expression src, Type destType) - => ImplicitConversion(src, destType, true, false)!; + public static Expression EnsureImplicitConversion(Expression src, Type destType, bool allowToString = false) + => ImplicitConversion(src, destType, throwException: true, allowToString: allowToString)!; // 6.1 Implicit Conversions public static Expression? ImplicitConversion(Expression src, Type destType, bool throwException = false, bool allowToString = false) { + if (src is null) throw new ArgumentNullException(nameof(src)); + if (destType is null) throw new ArgumentNullException(nameof(destType)); if (src is MethodGroupExpression methodGroup) { return methodGroup.CreateDelegateExpression(destType, throwException); @@ -182,7 +130,7 @@ public static Expression EnsureImplicitConversion(Expression src, Type destType) { result = ToStringConversion(src); } - if (throwException && result == null) throw new InvalidOperationException($"Could not implicitly convert expression of type { src.Type } to { destType }."); + if (throwException && result == null) throw new InvalidOperationException($"Could not implicitly convert expression of type { src.Type.ToCode() } to { destType.ToCode() }."); return result; } @@ -241,42 +189,42 @@ public static bool IsStringConversionAllowed(Type fromType) { if (value >= SByte.MinValue && value <= SByte.MinValue) { - return Expression.Constant((sbyte)srcValue, typeof(sbyte)); + return Expression.Constant((sbyte)value, typeof(sbyte)); } } if (destType == typeof(byte)) { if (value >= Byte.MinValue && value <= Byte.MaxValue) { - return Expression.Constant((byte)srcValue, typeof(byte)); + return Expression.Constant((byte)value, typeof(byte)); } } if (destType == typeof(short)) { if (value >= Int16.MinValue && value <= Int16.MaxValue) { - return Expression.Constant((short)srcValue, typeof(short)); + return Expression.Constant((short)value, typeof(short)); } } if (destType == typeof(ushort)) { if (value >= UInt16.MinValue && value <= UInt16.MaxValue) { - return Expression.Constant((ushort)srcValue, typeof(ushort)); + return Expression.Constant((ushort)value, typeof(ushort)); } } if (destType == typeof(uint)) { if (value >= uint.MinValue) { - return Expression.Constant((uint)srcValue, typeof(uint)); + return Expression.Constant((uint)value, typeof(uint)); } } if (destType == typeof(ulong)) { if (value >= 0) { - return Expression.Constant((ulong)srcValue, typeof(ulong)); + return Expression.Constant((ulong)value, typeof(ulong)); } } } @@ -288,7 +236,7 @@ public static bool IsStringConversionAllowed(Type fromType) { if (value >= 0) { - return Expression.Constant((ulong)srcValue, typeof(ulong)); + return Expression.Constant((ulong)value, typeof(ulong)); } } } @@ -449,31 +397,5 @@ private static Type GetTaskType(Type taskType) else return null; } - - static TypeConversion() - { - typePrecedence = new Dictionary - { - {typeof (object), 0}, - {typeof (bool), 1}, - {typeof (byte), 2}, - {typeof (int), 3}, - {typeof (short), 4}, - {typeof (long), 5}, - {typeof (float), 6}, - {typeof (double), 7} - }; - - ImplicitNumericConversions.Add(typeof(sbyte), new List() { typeof(short), typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(byte), new List() { typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(short), new List() { typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(ushort), new List() { typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(int), new List() { typeof(long), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(uint), new List() { typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(long), new List() { typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(ulong), new List() { typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(char), new List() { typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(float), new List() { typeof(double) }); - } } } diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index dd942ca4c3..d69347e182 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -405,6 +405,9 @@ public static bool Implements(this Type type, Type ifc, [NotNullWhen(true)] out return (concreteInterface = type.GetInterfaces().FirstOrDefault(i => isInterface(i, ifc))) != null; } + public static bool IsAssignableFromNull(this Type t) => + !t.IsValueType || t.IsNullable(); + public static bool IsNullable(this Type type) { return Nullable.GetUnderlyingType(type) != null; diff --git a/src/Framework/Testing/BindingTestHelper.cs b/src/Framework/Testing/BindingTestHelper.cs index ebbd42e81c..4d11ca65d8 100644 --- a/src/Framework/Testing/BindingTestHelper.cs +++ b/src/Framework/Testing/BindingTestHelper.cs @@ -75,7 +75,7 @@ public Expression ParseBinding(string expression, DataContextStack context, Type var parsedExpression = ExpressionBuilder.ParseWithLambdaConversion(expression, context, BindingParserOptions.Value.AddImports(imports), expectedType); return TypeConversion.MagicLambdaConversion(parsedExpression, expectedType) ?? - TypeConversion.ImplicitConversion(parsedExpression, expectedType, true, true)!; + TypeConversion.EnsureImplicitConversion(parsedExpression, expectedType, allowToString: true)!; } /// Returns JavaScript code to which the translates. diff --git a/src/Samples/Tests/Tests/DotVVM.Samples.Tests.csproj b/src/Samples/Tests/Tests/DotVVM.Samples.Tests.csproj index ced2bba49f..1e4167c204 100644 --- a/src/Samples/Tests/Tests/DotVVM.Samples.Tests.csproj +++ b/src/Samples/Tests/Tests/DotVVM.Samples.Tests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 8a6d0d59a2..d607114948 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -585,6 +585,16 @@ public void BindingCompiler_Invalid_EnumStringComparison() }); } + [TestMethod] + public void BindingCompiler_Valid_EnumBitOps() + { + var viewModel = new TestViewModel { EnumProperty = TestEnum.A }; + Assert.AreEqual(TestEnum.A, ExecuteBinding("EnumProperty & 1", viewModel)); + Assert.AreEqual(TestEnum.B, ExecuteBinding("EnumProperty | 1", viewModel)); + Assert.AreEqual(TestEnum.B, ExecuteBinding("EnumProperty | 'B'", viewModel)); + Assert.AreEqual(TestEnum.C, ExecuteBinding("(EnumProperty | 'D') & 'C'", viewModel)); + } + [TestMethod] public void BindingCompiler_Valid_GenericMethodCall() @@ -944,7 +954,7 @@ public void BindingCompiler_MultiBlockExpression_EnumAtEnd_CorrectResult() } [TestMethod] - [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Could not implicitly convert expression of type System.Void to System.Object")] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Could not implicitly convert expression of type void to object")] public void BindingCompiler_MultiBlockExpression_EmptyBlockAtEnd_Throws() { TestViewModel vm = new TestViewModel { StringProp = "a" }; @@ -952,7 +962,7 @@ public void BindingCompiler_MultiBlockExpression_EmptyBlockAtEnd_Throws() } [TestMethod] - [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Could not implicitly convert expression of type System.Void to System.Object")] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Could not implicitly convert expression of type void to object")] public void BindingCompiler_MultiBlockExpression_WhitespaceBlockAtEnd_Throws() { TestViewModel vm = new TestViewModel { StringProp = "a" }; @@ -1050,7 +1060,7 @@ public void BindingCompiler_Errors_AssigningToType() { var aggEx = Assert.ThrowsException(() => ExecuteBinding("System.String = 123", new [] { new TestViewModel() })); var ex = aggEx.GetBaseException(); - StringAssert.Contains(ex.Message, "cannot be assigned into"); + StringAssert.Contains(ex.Message, "Expression '123' cannot be assigned into 'System.String'."); } [TestMethod] @@ -1103,6 +1113,51 @@ public void Error_DifferentDataContext() ); } + [DataTestMethod] + [DataRow("IntProp + 1L", 101L)] + [DataRow("1L + IntProp", 101L)] + [DataRow("1L + UIntProp", 3_000_000_001L)] + [DataRow("1 + UIntProp", (uint)3_000_000_001)] + [DataRow("ShortProp", short.MaxValue)] + [DataRow("ShortProp - 1", short.MaxValue - 1)] + [DataRow("DoubleProp - 1", 0.5)] + [DataRow("DoubleProp + ShortProp", short.MaxValue + 1.5)] + [DataRow("NullableDoubleProp + ShortProp", null)] + [DataRow("ByteProp | ByteProp", (byte)255)] + [DataRow("DateTime == DateTime", true)] + [DataRow("NullableTimeOnly == NullableTimeOnly", true)] + [DataRow("NullableTimeOnly != NullableTimeOnly", false)] + [DataRow("NullableTimeOnly == TimeOnly", false)] + [DataRow("EnumList[0] > EnumList[1]", false)] + [DataRow("EnumList[0] < EnumList[1]", true)] + [DataRow("EnumList[0] == 'A'", true)] + [DataRow("EnumList[0] < 'C'", true)] + [DataRow("(EnumList[1] | 'C') == 'C'", false)] + [DataRow("(EnumList[2] & 1) != 0", false)] + public void BindingCompiler_BinaryOperator_ResultType(string expr, object expectedResult) + { + var vm = new TestViewModel { IntProp = 100, DoubleProp = 1.5, EnumList = new () { TestEnum.A, TestEnum.B, TestEnum.C, TestEnum.D } }; + Assert.AreEqual(expectedResult, ExecuteBinding(expr, vm)); + } + + [TestMethod] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Reference equality is not defined for the types 'DotVVM.Framework.Tests.Binding.TestViewModel2' and 'DotVVM.Framework.Tests.Binding.TestViewModel'")] + public void BindingCompiler_InvalidReferenceComparison() => + ExecuteBinding("TestViewModel2 == _this", new TestViewModel()); + + [TestMethod] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Cannot apply Equal operator to types DateTime and Object.")] + public void BindingCompiler_InvalidStructReferenceComparison() => + ExecuteBinding("DateTime == Time", new TestViewModel()); + + [TestMethod] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Cannot apply Equal operator to types DateTime and Object.")] + public void BindingCompiler_InvalidStructComparison() => + ExecuteBinding("DateTime == Time", new TestViewModel()); + [TestMethod] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Cannot apply And operator to types TestEnum and Boolean")] + public void BindingCompiler_InvalidBitAndComparison() => + ExecuteBinding("EnumProperty & 2 == 0", new TestViewModel()); } class TestViewModel { @@ -1137,6 +1192,12 @@ class TestViewModel public TestViewModel2[] VmArray => new TestViewModel2[] { new TestViewModel2() }; public int[] IntArray { get; set; } public decimal DecimalProp { get; set; } + public byte ByteProp { get; set; } = 255; + public sbyte SByteProp { get; set; } = 127; + public short ShortProp { get; set; } = 32767; + public ushort UShortProp { get; set; } = 65535; + public uint UIntProp { get; set; } = 3_000_000_000; + public double? NullableDoubleProp { get; set; } public ReadOnlyCollection ReadOnlyCollection => new ReadOnlyCollection(new[] { 1, 2, 3 }); diff --git a/src/Tests/Binding/ImplicitConversionTests.cs b/src/Tests/Binding/ImplicitConversionTests.cs index 94a56465ed..8400dac2d3 100644 --- a/src/Tests/Binding/ImplicitConversionTests.cs +++ b/src/Tests/Binding/ImplicitConversionTests.cs @@ -13,30 +13,30 @@ public class ImplicitConversionTests [TestMethod] public void Conversion_IntToNullableDouble() { - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(int)), typeof(double?), throwException: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(int)), typeof(double?)); } [TestMethod] public void Conversion_DoubleNullable() { - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(double)), typeof(double?), throwException: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(double)), typeof(double?)); } [TestMethod] public void Conversion_IntToDouble() { - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(int)), typeof(double), throwException: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(int)), typeof(double)); } [TestMethod] public void Conversion_ValidToString() { - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(DateTime)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(int)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(string)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(double)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(TimeSpan)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(Tuple)), typeof(string), throwException: true, allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(DateTime)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(int)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(string)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(double)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(TimeSpan)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(Tuple)), typeof(string), allowToString: true); } [TestMethod] diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index 49191eaa39..193b4103d3 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -58,11 +58,57 @@ public string CompileBinding(Func, Expression> ex return JavascriptTranslator.FormatKnockoutScript(jsExpression); } - [TestMethod] - public void JavascriptCompilation_EnumComparison() - { - var js = CompileBinding($"_this == 'Local'", typeof(DateTimeKind)); - Assert.AreEqual("$data==\"Local\"", js); + [DataTestMethod] + [DataRow("_this == 'Local'", "$data==\"Local\"")] + [DataRow("'Local' == _this", "\"Local\"==$data")] + [DataRow("_this == 2", "$data==\"Local\"")] + [DataRow("2 == _this", "\"Local\"==$data")] + [DataRow("_this & 'Local'", "dotvvm.translations.enums.fromInt(dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\")&2,\"G0/GAE51KlQlMR5T\")")] + [DataRow("'Local' & _this", "dotvvm.translations.enums.fromInt(2&dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\"),\"G0/GAE51KlQlMR5T\")")] + [DataRow("_this | 'Local'", "dotvvm.translations.enums.fromInt(dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\")|2,\"G0/GAE51KlQlMR5T\")")] + [DataRow("_this & 1", "dotvvm.translations.enums.fromInt(dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\")&1,\"G0/GAE51KlQlMR5T\")")] + [DataRow("1 & _this", "dotvvm.translations.enums.fromInt(1&dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\"),\"G0/GAE51KlQlMR5T\")")] + public void JavascriptCompilation_EnumOperators(string expr, string expectedJs) + { + var js = CompileBinding(expr, typeof(DateTimeKind)); + Assert.AreEqual(expectedJs, js); + } + + [DataTestMethod] + [DataRow("StringProp + StringProp", "(StringProp()??\"\")+(StringProp()??\"\")")] + [DataRow("StringProp + null", "StringProp()??\"\"")] + [DataRow("null + StringProp", "StringProp()??\"\"")] + [DataRow("'' + StringProp", "StringProp()??\"\"")] + [DataRow("BoolProp + StringProp", "BoolProp()+(StringProp()??\"\")")] + [DataRow("IntProp + IntProp + 'aa'", "IntProp()+IntProp()+\"aa\"")] + [DataRow("DoubleProp + 'aa'", "DoubleProp()+\"aa\"")] + [DataRow("'a' + DoubleProp", "\"a\"+DoubleProp()")] + [DataRow("'a' + NullableIntProp + null", "\"a\"+(NullableIntProp()??\"\")")] + public void JavascriptCompilation_StringPlus(string expr, string expectedJs) + { + var js = CompileBinding(expr, typeof(TestViewModel)); + Assert.AreEqual(expectedJs, js); + } + + [DataTestMethod] + [DataRow("NullableIntProp + NullableDoubleProp", "NullableIntProp()+NullableDoubleProp()")] + [DataRow("NullableIntProp & NullableIntProp", "NullableIntProp()&NullableIntProp()")] + [DataRow("NullableIntProp | NullableIntProp", "NullableIntProp()|NullableIntProp()")] + [DataRow("NullableIntProp ^ NullableIntProp", "NullableIntProp()^NullableIntProp()")] + [DataRow("NullableIntProp + 10", "NullableIntProp()+10")] + [DataRow("NullableIntProp - 10L", "NullableIntProp()-10")] + [DataRow("NullableIntProp / 10.0", "NullableIntProp()/10.0")] + [DataRow("10.0 / NullableIntProp", "10.0/NullableIntProp()")] + [DataRow("null / NullableIntProp", "null/NullableIntProp()")] + [DataRow("null == NullableIntProp", "null==NullableIntProp()")] + [DataRow("10 > NullableIntProp", "10>NullableIntProp()")] + [DataRow("NullableDoubleProp < 10", "NullableDoubleProp()<10")] + [DataRow("10+null", "10+null")] + [DataRow("10+null", "10+null")] + public void JavascriptCompilation_NullableOps(string expr, string expectedJs) + { + var js = CompileBinding(expr, typeof(TestViewModel)); + Assert.AreEqual(expectedJs, js); } [TestMethod] @@ -132,6 +178,12 @@ public void JavascriptCompilation_Parent() [DataRow("IntProp ^ 1", "IntProp()^1", DisplayName = "IntProp ^ 1")] [DataRow("'xx' + IntProp", "\"xx\"+IntProp()", DisplayName = "'xx' + IntProp")] [DataRow("true == (IntProp == 1)", "true==(IntProp()==1)", DisplayName = "true == (IntProp == 1)")] + [DataRow("TestViewModel2 == null", "TestViewModel2()==null", DisplayName = "TestViewModel2 == null")] + [DataRow("NullableDateOnly == null", "NullableDateOnly()==null", DisplayName = "NullableDateOnly == null")] + [DataRow("NullableDateOnly == NullableDateOnly", "NullableDateOnly()==NullableDateOnly()", DisplayName = "NullableDateOnly == NullableDateOnly")] + [DataRow("null != StringProp", "null!=StringProp()", DisplayName = "null != StringProp")] + [DataRow("(EnumProperty & 2) == 0", "dotvvm.translations.enums.fromInt(dotvvm.translations.enums.toInt(EnumProperty(),\"nEayAzHQ5xyCfSP6\")&2,\"nEayAzHQ5xyCfSP6\")==\"A\"", DisplayName = "(EnumProperty & 2) == 0")] + [DataRow("EnumProperty == 'B'", "EnumProperty()==\"B\"", DisplayName = "EnumProperty & 2 == 0")] public void JavascriptCompilation_BinaryExpressions(string expr, string expectedJs) { var js = CompileBinding(expr, new [] { typeof(TestViewModel) }); diff --git a/src/Tests/Parser/Binding/BindingParserTests.cs b/src/Tests/Parser/Binding/BindingParserTests.cs index 550ee53584..4db7414a04 100644 --- a/src/Tests/Parser/Binding/BindingParserTests.cs +++ b/src/Tests/Parser/Binding/BindingParserTests.cs @@ -803,7 +803,7 @@ public void BindingParser_GenericExpression_MultipleInside() var parser = bindingParserNodeFactory.SetupParser(originalString); var node = parser.ReadExpression(); - Assert.IsTrue(node is TypeOrFunctionReferenceBindingParserNode); + Assert.IsInstanceOfType(node, typeof(TypeOrFunctionReferenceBindingParserNode)); Assert.IsTrue(string.Equals(originalString, node.ToDisplayString())); }