2
0
Эх сурвалжийг харах

Configurable value coercion under interop (#973)

Marko Lahma 3 жил өмнө
parent
commit
ed94d10a05

+ 118 - 0
Jint.Tests/Runtime/InteropTests.cs

@@ -819,11 +819,18 @@ namespace Jint.Tests.Runtime
 
         private class TestClass
         {
+            public string String { get; set; }
+            public int Int { get; set; }
             public int? NullableInt { get; set; }
             public DateTime? NullableDate { get; set; }
             public bool? NullableBool { get; set; }
+            public bool Bool { get; set; }
             public TestEnumInt32? NullableEnum { get; set; }
             public TestStruct? NullableStruct { get; set; }
+
+            public void SetBool(bool value) => Bool = value;
+            public void SetInt(int value) => Int = value;
+            public void SetString(string value) => String = value;
         }
 
         [Fact]
@@ -989,6 +996,117 @@ namespace Jint.Tests.Runtime
             Assert.Equal(true, value);
         }
 
+        [Fact]
+        public void ShouldAllowBooleanCoercion()
+        {
+            var engine = new Engine(options =>
+            {
+                options.Interop.ValueCoercion = ValueCoercionType.Boolean;
+            });
+
+            engine.SetValue("o", new TestClass());
+            Assert.True(engine.Evaluate("o.Bool = 1; return o.Bool;").AsBoolean());
+            Assert.True(engine.Evaluate("o.Bool = 'dog'; return o.Bool;").AsBoolean());
+            Assert.True(engine.Evaluate("o.Bool = {}; return o.Bool;").AsBoolean());
+            Assert.False(engine.Evaluate("o.Bool = 0; return o.Bool;").AsBoolean());
+            Assert.False(engine.Evaluate("o.Bool = ''; return o.Bool;").AsBoolean());
+            Assert.False(engine.Evaluate("o.Bool = null; return o.Bool;").AsBoolean());
+            Assert.False(engine.Evaluate("o.Bool = undefined; return o.Bool;").AsBoolean());
+
+            engine.Evaluate("class MyClass { valueOf() { return 42; } }");
+            Assert.Equal(true, engine.Evaluate("let obj = new MyClass(); o.Bool = obj; return o.Bool;").AsBoolean());
+
+            engine.SetValue("func3", new Action<bool, bool, bool>((param1, param2, param3) =>
+            {
+                Assert.True(param1);
+                Assert.True(param2);
+                Assert.True(param3);
+            }));
+            engine.Evaluate("func3(true, obj, [ 1, 2, 3])");
+
+            Assert.Equal(true, engine.Evaluate("o.SetBool(42); return o.Bool;").AsBoolean());
+            Assert.Equal(true, engine.Evaluate("o.SetBool(obj); return o.Bool;").AsBoolean());
+            Assert.Equal(true, engine.Evaluate("o.SetBool([ 1, 2, 3].length); return o.Bool;").AsBoolean());
+        }
+
+        [Fact]
+        public void ShouldAllowNumberCoercion()
+        {
+            var engine = new Engine(options =>
+            {
+                options.Interop.ValueCoercion = ValueCoercionType.Number;
+            });
+
+            engine.SetValue("o", new TestClass());
+            Assert.Equal(1, engine.Evaluate("o.Int = true; return o.Int;").AsNumber());
+            Assert.Equal(0, engine.Evaluate("o.Int = false; return o.Int;").AsNumber());
+
+            engine.Evaluate("class MyClass { valueOf() { return 42; } }");
+            Assert.Equal(42, engine.Evaluate("let obj = new MyClass(); o.Int = obj; return o.Int;").AsNumber());
+
+            // but null and undefined should be injected as nulls to nullable objects
+            Assert.True(engine.Evaluate("o.NullableInt = null; return o.NullableInt;").IsNull());
+            Assert.True(engine.Evaluate("o.NullableInt = undefined; return o.NullableInt;").IsNull());
+
+            engine.SetValue("func3", new Action<int, double, long>((param1, param2, param3) =>
+            {
+                Assert.Equal(1, param1);
+                Assert.Equal(42, param2);
+                Assert.Equal(3, param3);
+            }));
+            engine.Evaluate("func3(true, obj, [ 1, 2, 3].length)");
+
+            Assert.Equal(1, engine.Evaluate("o.SetInt(true); return o.Int;").AsNumber());
+            Assert.Equal(42, engine.Evaluate("o.SetInt(obj); return o.Int;").AsNumber());
+            Assert.Equal(3, engine.Evaluate("o.SetInt([ 1, 2, 3].length); return o.Int;").AsNumber());
+        }
+
+        [Fact]
+        public void ShouldAllowStringCoercion()
+        {
+            var engine = new Engine(options =>
+            {
+                options.Interop.ValueCoercion = ValueCoercionType.String;
+            });
+
+            // basic premise, booleans in JS are lower-case, so should the the toString under interop
+            Assert.Equal("true", engine.Evaluate("'' + true").AsString());
+
+            engine.SetValue("o", new TestClass());
+            Assert.Equal("false", engine.Evaluate("'' + o.Bool").AsString());
+
+            Assert.Equal("true", engine.Evaluate("o.Bool = true; o.String = o.Bool; return o.String;").AsString());
+
+            Assert.Equal("true", engine.Evaluate("o.String = true; return o.String;").AsString());
+
+            engine.SetValue("func1", new Func<bool>(() => true));
+            Assert.Equal("true", engine.Evaluate("'' + func1()").AsString());
+
+            engine.SetValue("func2", new Func<JsValue>(() => JsBoolean.True));
+            Assert.Equal("true", engine.Evaluate("'' + func2()").AsString());
+
+            // but null and undefined should be injected as nulls to c# objects
+            Assert.True(engine.Evaluate("o.String = null; return o.String;").IsNull());
+            Assert.True(engine.Evaluate("o.String = undefined; return o.String;").IsNull());
+
+            Assert.Equal("1,2,3", engine.Evaluate("o.String = [ 1, 2, 3 ]; return o.String;").AsString());
+
+            engine.Evaluate("class MyClass { toString() { return 'hello world'; } }");
+            Assert.Equal("hello world", engine.Evaluate("let obj = new MyClass(); o.String = obj; return o.String;").AsString());
+
+            engine.SetValue("func3", new Action<string, string, string>((param1, param2, param3) =>
+            {
+                Assert.Equal("true", param1);
+                Assert.Equal("hello world", param2);
+                Assert.Equal("1,2,3", param3);
+            }));
+            engine.Evaluate("func3(true, obj, [ 1, 2, 3])");
+
+            Assert.Equal("true", engine.Evaluate("o.SetString(true); return o.String;").AsString());
+            Assert.Equal("hello world", engine.Evaluate("o.SetString(obj); return o.String;").AsString());
+            Assert.Equal("1,2,3", engine.Evaluate("o.SetString([ 1, 2, 3]); return o.String;").AsString());
+        }
+
         [Fact]
         public void ShouldConvertDateInstanceToDateTime()
         {

+ 2 - 1
Jint/Engine.cs

@@ -106,6 +106,8 @@ namespace Jint
             Options = new Options();
             options?.Invoke(this, Options);
 
+            _extensionMethods = ExtensionMethodCache.Build(Options.Interop.ExtensionMethodTypes);
+
             Reset();
 
             // gather some options as fields for faster checks
@@ -118,7 +120,6 @@ namespace Jint
 
             _constraints = Options.Constraints.Constraints.ToArray();
             _referenceResolver = Options.ReferenceResolver;
-            _extensionMethods = ExtensionMethodCache.Build(Options.Interop.ExtensionMethodTypes);
             CallStack = new JintCallStack(Options.Constraints.MaxRecursionDepth >= 0);
 
             _referencePool = new ReferencePool();

+ 108 - 1
Jint/Extensions/ReflectionExtensions.cs

@@ -3,11 +3,15 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
 using System.Runtime.CompilerServices;
+using Jint.Native;
+using Jint.Runtime;
 
 namespace Jint.Extensions
 {
     internal static class ReflectionExtensions
     {
+        private static readonly Type nullableType = typeof(Nullable<>);
+
         internal static void SetValue(this MemberInfo memberInfo, object forObject, object value)
         {
             if (memberInfo.MemberType == MemberTypes.Field)
@@ -54,5 +58,108 @@ namespace Jint.Extensions
         {
             return methodInfo.IsDefined(typeof(ExtensionAttribute), true);
         }
+
+        public static bool IsNullable(this Type type)
+        {
+            return type is { IsGenericType: true } && type.GetGenericTypeDefinition() == nullableType;
+        }
+
+        public static bool IsNumeric(this Type type)
+        {
+            if (type == null || type.IsEnum)
+            {
+                return false;
+            }
+
+            switch (Type.GetTypeCode(type))
+            {
+                case TypeCode.Byte:
+                case TypeCode.Decimal:
+                case TypeCode.Double:
+                case TypeCode.Int16:
+                case TypeCode.Int32:
+                case TypeCode.Int64:
+                case TypeCode.SByte:
+                case TypeCode.Single:
+                case TypeCode.UInt16:
+                case TypeCode.UInt32:
+                case TypeCode.UInt64:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        public static bool IsClrNumericCoercible(this Type type)
+        {
+            if (type == null || type.IsEnum)
+            {
+                return false;
+            }
+
+            switch (Type.GetTypeCode(type))
+            {
+                case TypeCode.Decimal:
+                case TypeCode.Double:
+                case TypeCode.Int32:
+                case TypeCode.Int64:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        public static object AsNumberOfType(this double d, TypeCode type)
+        {
+            switch (type)
+            {
+                case TypeCode.Decimal:
+                    return (decimal) d;
+                case TypeCode.Double:
+                    return d;
+                case TypeCode.Int32:
+                    return (int) d;
+                case TypeCode.Int64:
+                    return (long) d;
+                default:
+                    ExceptionHelper.ThrowArgumentException("Cannot convert " + type);
+                    return null;
+            }
+        }
+
+        public static bool TryConvertViaTypeCoercion(
+            Type _memberType,
+            ValueCoercionType valueCoercionType,
+            JsValue value,
+            out object converted)
+        {
+            if (_memberType == typeof(bool) && (valueCoercionType & ValueCoercionType.Boolean) != 0)
+            {
+                converted = TypeConverter.ToBoolean(value);
+                return true;
+            }
+
+            if (_memberType == typeof(string)
+                && !value.IsNullOrUndefined()
+                && (valueCoercionType & ValueCoercionType.String) != 0)
+            {
+                // we know how to print out correct string presentation for primitives
+                // that are non-null and non-undefined
+                converted = TypeConverter.ToString(value);
+                return true;
+            }
+
+            if (_memberType.IsClrNumericCoercible() && (valueCoercionType & ValueCoercionType.Number) != 0)
+            {
+                // we know how to print out correct string presentation for primitives
+                // that are non-null and non-undefined
+                var number = TypeConverter.ToNumber(value);
+                converted = number.AsNumberOfType(Type.GetTypeCode(_memberType));
+                return true;
+            }
+
+            converted = null;
+            return false;
+        }
     }
-}
+}

+ 0 - 1
Jint/Native/Array/ArrayIteratorPrototype.cs

@@ -1,4 +1,3 @@
-using System;
 using Jint.Native.Iterator;
 using Jint.Native.Object;
 using Jint.Native.TypedArray;

+ 0 - 4
Jint/Native/JsValue.cs

@@ -2,13 +2,9 @@
 using System.Diagnostics;
 using System.Diagnostics.Contracts;
 using System.Runtime.CompilerServices;
-using Jint.Native.Array;
-using Jint.Native.Date;
 using Jint.Native.Iterator;
 using Jint.Native.Number;
 using Jint.Native.Object;
-using Jint.Native.Promise;
-using Jint.Native.RegExp;
 using Jint.Native.Symbol;
 using Jint.Runtime;
 

+ 39 - 0
Jint/Options.cs

@@ -240,6 +240,45 @@ namespace Jint
         /// As this object holds caching state same instance should be shared between engines, if possible.
         /// </remarks>
         public TypeResolver TypeResolver { get; set; } = TypeResolver.Default;
+
+        /// <summary>
+        /// When writing values to CLR objects, how should JS values be coerced to CLR types.
+        /// Defaults to only coercing to string values when writing to string targets.
+        /// </summary>
+        public ValueCoercionType ValueCoercion { get; set; } = ValueCoercionType.String;
+    }
+
+    /// <summary>
+    /// Rules for writing values to CLR fields.
+    /// </summary>
+    [Flags]
+    public enum ValueCoercionType
+    {
+        /// <summary>
+        /// No coercion will be done. If there's no type converter, and error will be thrown.
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// JS coercion using boolean rules "dog" == true, "" == false, 1 == true, 3 == true, 0 == false, { "prop": 1 } == true etc.
+        /// </summary>
+        Boolean = 1,
+
+        /// <summary>
+        /// JS coercion to numbers, false == 0, true == 1. valueOf functions will be used when available for object instances.
+        /// Valid against targets of type: Decimal, Double, Int32, Int64.
+        /// </summary>
+        Number = 2,
+
+        /// <summary>
+        /// JS coercion to strings, toString function will be used when available for objects.
+        /// </summary>
+        String = 4,
+
+        /// <summary>
+        /// All coercion rules enabled.
+        /// </summary>
+        All = Boolean | Number | String
     }
 
     public class ConstraintOptions

+ 2 - 2
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -8,6 +8,7 @@ using System.Linq.Expressions;
 using System.Reflection;
 using Jint.Extensions;
 using Jint.Native;
+using Jint.Runtime.Interop.Reflection;
 
 namespace Jint.Runtime.Interop
 {
@@ -23,7 +24,6 @@ namespace Jint.Runtime.Interop
         private static readonly ConcurrentDictionary<string, MethodInfo> _knownCastOperators = new ConcurrentDictionary<string, MethodInfo>();
 #endif
 
-        private static readonly Type nullableType = typeof(Nullable<>);
         private static readonly Type intType = typeof(int);
         private static readonly Type iCallableType = typeof(Func<JsValue, JsValue[], JsValue>);
         private static readonly Type jsValueType = typeof(JsValue);
@@ -59,7 +59,7 @@ namespace Jint.Runtime.Interop
                 return value;
             }
 
-            if (type.IsGenericType && type.GetGenericTypeDefinition() == nullableType)
+            if (type.IsNullable())
             {
                 type = Nullable.GetUnderlyingType(type);
             }

+ 23 - 10
Jint/Runtime/Interop/DelegateWrapper.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Globalization;
 using System.Reflection;
+using Jint.Extensions;
 using Jint.Native;
 using Jint.Native.Function;
 
@@ -56,24 +57,30 @@ namespace Jint.Runtime.Interop
             int jsArgumentsCount = jsArguments.Length;
             int jsArgumentsWithoutParamsCount = Math.Min(jsArgumentsCount, delegateNonParamsArgumentsCount);
 
+            var clrTypeConverter = Engine.ClrTypeConverter;
+            var valueCoercionType = Engine.Options.Interop.ValueCoercion;
             var parameters = new object[delegateArgumentsCount];
 
             // convert non params parameter to expected types
             for (var i = 0; i < jsArgumentsWithoutParamsCount; i++)
             {
                 var parameterType = parameterInfos[i].ParameterType;
+                var value = jsArguments[i];
+                object converted;
 
                 if (parameterType == typeof(JsValue))
                 {
-                    parameters[i] = jsArguments[i];
+                    converted = value;
                 }
-                else
+                else if (!ReflectionExtensions.TryConvertViaTypeCoercion(parameterType, valueCoercionType, value, out converted))
                 {
-                    parameters[i] = Engine.ClrTypeConverter.Convert(
-                        jsArguments[i].ToObject(),
+                    converted = clrTypeConverter.Convert(
+                        value.ToObject(),
                         parameterType,
                         CultureInfo.InvariantCulture);
                 }
+
+                parameters[i] = converted;
             }
 
             // assign null to parameters not provided
@@ -89,7 +96,7 @@ namespace Jint.Runtime.Interop
                 }
             }
 
-            // assign params to array and converts each objet to expected type
+            // assign params to array and converts each object to expected type
             if (_delegateContainsParamsArgument)
             {
                 int paramsArgumentIndex = delegateArgumentsCount - 1;
@@ -101,21 +108,27 @@ namespace Jint.Runtime.Interop
                 for (var i = paramsArgumentIndex; i < jsArgumentsCount; i++)
                 {
                     var paramsIndex = i - paramsArgumentIndex;
+                    var value = jsArguments[i];
+                    object converted;
 
                     if (paramsParameterType == typeof(JsValue))
                     {
-                        paramsParameter[paramsIndex] = jsArguments[i];
+                        converted = value;
                     }
-                    else
+                    else if (!ReflectionExtensions.TryConvertViaTypeCoercion(paramsParameterType, valueCoercionType, value, out converted))
                     {
-                        paramsParameter[paramsIndex] = Engine.ClrTypeConverter.Convert(
-                            jsArguments[i].ToObject(),
+                        converted = Engine.ClrTypeConverter.Convert(
+                            value.ToObject(),
                             paramsParameterType,
                             CultureInfo.InvariantCulture);
                     }
+
+                    paramsParameter[paramsIndex] = converted;
                 }
+
                 parameters[paramsArgumentIndex] = paramsParameter;
             }
+
             try
             {
                 return FromObject(Engine, _d.DynamicInvoke(parameters));
@@ -127,4 +140,4 @@ namespace Jint.Runtime.Interop
             }
         }
     }
-}
+}

+ 11 - 4
Jint/Runtime/Interop/MethodDescriptor.cs

@@ -3,6 +3,7 @@ using System;
 using System.Collections.Generic;
 using System.Reflection;
 using System.Runtime.CompilerServices;
+using Jint.Extensions;
 
 namespace Jint.Runtime.Interop
 {
@@ -106,23 +107,29 @@ namespace Jint.Runtime.Interop
         {
             var parameters = new object[arguments.Length];
             var methodParameters = Parameters;
+            var valueCoercionType = _engine.Options.Interop.ValueCoercion;
+
             try
             {
                 for (var i = 0; i < arguments.Length; i++)
                 {
                     var parameterType = methodParameters[i].ParameterType;
+                    var value = arguments[i];
+                    object converted;
 
                     if (typeof(JsValue).IsAssignableFrom(parameterType))
                     {
-                        parameters[i] = arguments[i];
+                        converted = value;
                     }
-                    else
+                    else if (!ReflectionExtensions.TryConvertViaTypeCoercion(parameterType, valueCoercionType, value, out converted))
                     {
-                        parameters[i] = _engine.ClrTypeConverter.Convert(
-                            arguments[i].ToObject(),
+                        converted = _engine.ClrTypeConverter.Convert(
+                            value.ToObject(),
                             parameterType,
                             System.Globalization.CultureInfo.InvariantCulture);
                     }
+
+                    parameters[i] = converted;
                 }
 
                 if (Method is MethodInfo m)

+ 3 - 1
Jint/Runtime/Interop/MethodInfoFunctionInstance.cs

@@ -2,6 +2,7 @@
 using System.Globalization;
 using System.Linq.Expressions;
 using System.Reflection;
+using Jint.Extensions;
 using Jint.Native;
 using Jint.Native.Function;
 
@@ -89,7 +90,8 @@ namespace Jint.Runtime.Interop
                     }
                     else
                     {
-                        if (!converter.TryConvert(argument.ToObject(), parameterType, CultureInfo.InvariantCulture, out parameters[i]))
+                        if (!ReflectionExtensions.TryConvertViaTypeCoercion(parameterType, _engine.Options.Interop.ValueCoercion, argument, out parameters[i])
+                            && !converter.TryConvert(argument.ToObject(), parameterType, CultureInfo.InvariantCulture, out parameters[i]))
                         {
                             argumentsMatch = false;
                             break;

+ 4 - 3
Jint/Runtime/Interop/Reflection/ReflectionAccessor.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Globalization;
 using System.Reflection;
+using Jint.Extensions;
 using Jint.Native;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Descriptors.Specialized;
@@ -39,7 +40,7 @@ namespace Jint.Runtime.Interop.Reflection
             {
                 return constantValue;
             }
-            
+
             // first check indexer so we don't confuse inherited properties etc
             var value = TryReadFromIndexer(target);
 
@@ -90,12 +91,12 @@ namespace Jint.Runtime.Interop.Reflection
 
         public void SetValue(Engine engine, object target, JsValue value)
         {
-            object converted;
+            object converted = null;
             if (_memberType == typeof(JsValue))
             {
                 converted = value;
             }
-            else
+            else if (!ReflectionExtensions.TryConvertViaTypeCoercion(_memberType, engine.Options.Interop.ValueCoercion, value, out converted))
             {
                 // attempt to convert the JsValue to the target type
                 converted = value.ToObject();