Browse Source

Enable array-like behavior from ICollection/arrays (#579)

Marko Lahma 6 years ago
parent
commit
4f94558569

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

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq;
 using System.Reflection;
 using System.Reflection;
 using Jint.Native;
 using Jint.Native;
 using Jint.Native.Array;
 using Jint.Native.Array;
@@ -1620,5 +1621,82 @@ namespace Jint.Tests.Runtime
             Assert.Equal(engine.Invoke("throwException3").AsString(), exceptionMessage);
             Assert.Equal(engine.Invoke("throwException3").AsString(), exceptionMessage);
             Assert.Throws<ArgumentNullException>(() => engine.Invoke("throwException4"));
             Assert.Throws<ArgumentNullException>(() => engine.Invoke("throwException4"));
         }
         }
+        
+        [Fact]
+        public void ArrayFromShouldConvertListToArrayLike()
+        {
+            var list = new List<Person>
+            {
+                new Person {Name = "Mike"},
+                new Person {Name = "Mika"}
+            };
+            _engine.SetValue("a", list);
+
+            RunTest(@"
+                var arr = new Array(a);
+                assert(arr.length === 2);
+                assert(arr[0].Name === 'Mike');
+                assert(arr[1].Name === 'Mika');
+            ");
+
+            RunTest(@"
+                var arr = Array.from(a);
+                assert(arr.length === 2);
+                assert(arr[0].Name === 'Mike');
+                assert(arr[1].Name === 'Mika');
+            ");
+        }
+        
+        [Fact]
+        public void ArrayFromShouldConvertArrayToArrayLike()
+        {
+            var list = new []
+            {
+                new Person {Name = "Mike"},
+                new Person {Name = "Mika"}
+            };
+            _engine.SetValue("a", list);
+
+            RunTest(@"
+                var arr = new Array(a);
+                assert(arr.length === 2);
+                assert(arr[0].Name === 'Mike');
+                assert(arr[1].Name === 'Mika');
+            ");
+
+            RunTest(@"
+                var arr = Array.from(a);
+                assert(arr.length === 2);
+                assert(arr[0].Name === 'Mike');
+                assert(arr[1].Name === 'Mika');
+            ");
+        }
+        
+        [Fact]
+        public void ArrayFromShouldConvertIEnumerable()
+        {
+            var enumerable = new []
+            {
+                new Person {Name = "Mike"},
+                new Person {Name = "Mika"}
+            }.Select(x => x);
+            
+            _engine.SetValue("a", enumerable);
+
+            RunTest(@"
+                var arr = new Array(a);
+                assert(arr.length === 2);
+                assert(arr[0].Name === 'Mike');
+                assert(arr[1].Name === 'Mika');
+            ");
+
+            RunTest(@"
+                var arr = Array.from(a);
+                assert(arr.length === 2);
+                assert(arr[0].Name === 'Mike');
+                assert(arr[1].Name === 'Mika');
+            ");
+        }
+
     }
     }
 }
 }

+ 70 - 45
Jint/Native/Array/ArrayConstructor.cs

@@ -78,54 +78,68 @@ namespace Jint.Native.Array
 
 
             if (objectInstance.IsArrayLike)
             if (objectInstance.IsArrayLike)
             {
             {
-                var operations = ArrayPrototype.ArrayOperations.For(objectInstance);
+                return ConstructArrayFromArrayLike(objectInstance, callable, thisArg);
+            }
 
 
-                var length = operations.GetLength();
+            if (objectInstance is ObjectWrapper wrapper && wrapper.Target is IEnumerable enumerable)
+            {
+                return ConstructArrayFromIEnumerable(enumerable);
+            }
 
 
-                var a = _engine.Array.ConstructFast(length);
-                var args = !ReferenceEquals(callable, null)
-                    ? _engine._jsValueArrayPool.RentArray(2)
-                    : null;
+            var instance = _engine.Array.ConstructFast(0);
+            if (objectInstance.TryGetIterator(_engine, out var iterator))
+            {
+                var protocol = new ArrayProtocol(_engine, thisArg, instance, iterator, callable);
+                protocol.Execute();
+            }
 
 
-                uint n = 0;
-                for (uint i = 0; i < length; i++)
-                {
-                    JsValue jsValue;
-                    operations.TryGetValue(i, out var value);
-                    if (!ReferenceEquals(callable, null))
-                    {
-                        args[0] = value;
-                        args[1] = i;
-                        jsValue = callable.Call(thisArg, args);
-
-                        // function can alter data
-                        length = operations.GetLength();
-                    }
-                    else
-                    {
-                        jsValue = value;
-                    }
-                    a.SetIndexValue(i, jsValue, updateLength: false);
-                    n++;
-                }
+            return instance;
+        }
+
+        private ArrayInstance ConstructArrayFromArrayLike(
+            ObjectInstance objectInstance, 
+            ICallable callable, 
+            JsValue thisArg)
+        {
+            var operations = ArrayPrototype.ArrayOperations.For(objectInstance);
 
 
+            var length = operations.GetLength();
+
+            var a = _engine.Array.ConstructFast(length);
+            var args = !ReferenceEquals(callable, null)
+                ? _engine._jsValueArrayPool.RentArray(2)
+                : null;
+
+            uint n = 0;
+            for (uint i = 0; i < length; i++)
+            {
+                JsValue jsValue;
+                operations.TryGetValue(i, out var value);
                 if (!ReferenceEquals(callable, null))
                 if (!ReferenceEquals(callable, null))
                 {
                 {
-                    _engine._jsValueArrayPool.ReturnArray(args);
+                    args[0] = value;
+                    args[1] = i;
+                    jsValue = callable.Call(thisArg, args);
+
+                    // function can alter data
+                    length = operations.GetLength();
+                }
+                else
+                {
+                    jsValue = value;
                 }
                 }
 
 
-                a.SetLength(length);
-                return a;
+                a.SetIndexValue(i, jsValue, updateLength: false);
+                n++;
             }
             }
 
 
-            var instance = _engine.Array.ConstructFast(0);
-            if (objectInstance.TryGetIterator(_engine, out var iterator))
+            if (!ReferenceEquals(callable, null))
             {
             {
-                var protocol = new ArrayProtocol(_engine, thisArg, instance, iterator, callable);
-                protocol.Execute();
+                _engine._jsValueArrayPool.ReturnArray(args);
             }
             }
 
 
-            return instance;
+            a.SetLength(length);
+            return a;
         }
         }
 
 
         internal sealed class ArrayProtocol : IteratorProtocol
         internal sealed class ArrayProtocol : IteratorProtocol
@@ -248,18 +262,14 @@ namespace Jint.Native.Array
             {
             {
                 if (objectWrapper.Target is IEnumerable enumerable)
                 if (objectWrapper.Target is IEnumerable enumerable)
                 {
                 {
-                    var jsArray = (ArrayInstance) Engine.Array.Construct(Arguments.Empty);
-                    var tempArray = _engine._jsValueArrayPool.RentArray(1);
-                    foreach (var item in enumerable)
-                    {
-                        var jsItem = FromObject(Engine, item);
-                        tempArray[0] = jsItem;
-                        Engine.Array.PrototypeObject.Push(jsArray, tempArray);
-                    }
-                    _engine._jsValueArrayPool.ReturnArray(tempArray);
-                    return jsArray;
+                    return ConstructArrayFromIEnumerable(enumerable);
                 }
                 }
             }
             }
+            else if (arguments.Length == 1 && arguments[0] is ArrayInstance arrayInstance)
+            {
+                // direct copy
+                return ConstructArrayFromArrayLike(arrayInstance, null, this);
+            }
             else
             else
             {
             {
                 instance._length = new PropertyDescriptor(0, PropertyFlag.OnlyWritable);
                 instance._length = new PropertyDescriptor(0, PropertyFlag.OnlyWritable);
@@ -272,6 +282,21 @@ namespace Jint.Native.Array
             return instance;
             return instance;
         }
         }
 
 
+        private ArrayInstance ConstructArrayFromIEnumerable(IEnumerable enumerable)
+        {
+            var jsArray = (ArrayInstance) Engine.Array.Construct(Arguments.Empty);
+            var tempArray = _engine._jsValueArrayPool.RentArray(1);
+            foreach (var item in enumerable)
+            {
+                var jsItem = FromObject(Engine, item);
+                tempArray[0] = jsItem;
+                Engine.Array.PrototypeObject.Push(jsArray, tempArray);
+            }
+
+            _engine._jsValueArrayPool.ReturnArray(tempArray);
+            return jsArray;
+        }
+
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal ArrayInstance ConstructFast(uint length)
         internal ArrayInstance ConstructFast(uint length)
         {
         {

+ 53 - 35
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Reflection;
 using System.Reflection;
 using Jint.Native;
 using Jint.Native;
@@ -17,10 +18,20 @@ namespace Jint.Runtime.Interop
             : base(engine)
             : base(engine)
         {
         {
             Target = obj;
             Target = obj;
+            if (obj is ICollection collection)
+            {
+                IsArrayLike = true;
+                // create a forwarder to produce length from Count
+                var functionInstance = new ClrFunctionInstance(engine, "length", (thisObj, arguments) => collection.Count);
+                var descriptor = new GetSetPropertyDescriptor(functionInstance, Undefined, PropertyFlag.AllForbidden);
+                AddProperty("length", descriptor);
+            }
         }
         }
 
 
         public object Target { get; }
         public object Target { get; }
 
 
+        internal override bool IsArrayLike { get; }
+
         public override void Put(string propertyName, JsValue value, bool throwOnError)
         public override void Put(string propertyName, JsValue value, bool throwOnError)
         {
         {
             if (!CanPut(propertyName))
             if (!CanPut(propertyName))
@@ -68,55 +79,62 @@ namespace Jint.Runtime.Interop
             AddProperty(propertyName, descriptor);
             AddProperty(propertyName, descriptor);
             return descriptor;
             return descriptor;
         }
         }
-
+        
         private static Func<Engine, object, PropertyDescriptor> ResolveProperty(Type type, string propertyName)
         private static Func<Engine, object, PropertyDescriptor> ResolveProperty(Type type, string propertyName)
         {
         {
-            // look for a property
-            PropertyInfo property = null;
-            foreach (var p in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
+            var isNumber = uint.TryParse(propertyName, out _);
+
+            // properties and fields cannot be numbers
+            if (!isNumber)
             {
             {
-                if (EqualsIgnoreCasing(p.Name, propertyName))
+                // look for a property
+                PropertyInfo property = null;
+                foreach (var p in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
                 {
                 {
-                    property = p;
-                    break;
+                    if (EqualsIgnoreCasing(p.Name, propertyName))
+                    {
+                        property = p;
+                        break;
+                    }
                 }
                 }
-            }
 
 
-            if (property != null)
-            {
-                return (engine, target) => new PropertyInfoDescriptor(engine, property, target);
-            }
+                if (property != null)
+                {
+                    return (engine, target) => new PropertyInfoDescriptor(engine, property, target);
+                }
 
 
-            // look for a field
-            FieldInfo field = null;
-            foreach (var f in type.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
-            {
-                if (EqualsIgnoreCasing(f.Name, propertyName))
+                // look for a field
+                FieldInfo field = null;
+                foreach (var f in type.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
                 {
                 {
-                    field = f;
-                    break;
+                    if (EqualsIgnoreCasing(f.Name, propertyName))
+                    {
+                        field = f;
+                        break;
+                    }
                 }
                 }
-            }
 
 
-            if (field != null)
-            {
-                return (engine, target) => new FieldInfoDescriptor(engine, field, target);
-            }
+                if (field != null)
+                {
+                    return (engine, target) => new FieldInfoDescriptor(engine, field, target);
+                }
+                
+                // if no properties were found then look for a method
+                List<MethodInfo> methods = null;
+                foreach (var m in type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
+                {
+                    if (EqualsIgnoreCasing(m.Name, propertyName))
+                    {
+                        methods = methods ?? new List<MethodInfo>();
+                        methods.Add(m);
+                    }
+                }
 
 
-            // if no properties were found then look for a method
-            List<MethodInfo> methods = null;
-            foreach (var m in type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
-            {
-                if (EqualsIgnoreCasing(m.Name, propertyName))
+                if (methods?.Count > 0)
                 {
                 {
-                    methods = methods ?? new List<MethodInfo>();
-                    methods.Add(m);
+                    return (engine, target) => new PropertyDescriptor(new MethodInfoFunctionInstance(engine, methods.ToArray()), PropertyFlag.OnlyEnumerable);
                 }
                 }
-            }
 
 
-            if (methods?.Count > 0)
-            {
-                return (engine, target) => new PropertyDescriptor(new MethodInfoFunctionInstance(engine, methods.ToArray()), PropertyFlag.OnlyEnumerable);
             }
             }
 
 
             // if no methods are found check if target implemented indexing
             // if no methods are found check if target implemented indexing