Selaa lähdekoodia

Support explicit interface and hidden member of super class (#1613)

viruscamp 2 vuotta sitten
vanhempi
commit
0ebf7cd5a8

+ 186 - 0
Jint.Tests/Runtime/InteropExplicitTypeTests.cs

@@ -0,0 +1,186 @@
+using System.Reflection;
+
+namespace Jint.Tests.Runtime
+{
+    public class InteropExplicitTypeTests
+    {
+        public interface I1
+        {
+            string Name { get; }
+        }
+
+        public class Super
+        {
+            public string Name { get; } = "Super";
+        }
+
+        public class CI1 : Super, I1
+        {
+            public new string Name { get; } = "CI1";
+
+            string I1.Name { get; } = "CI1 as I1";
+        }
+
+        public class Indexer<T>
+        {
+            private readonly T t;
+            public Indexer(T t)
+            {
+                this.t = t;
+            }
+            public T this[int index]
+            {
+                get { return t; }
+            }
+        }
+
+        public class InterfaceHolder
+        {
+            public InterfaceHolder()
+            {
+                var ci1 = new CI1();
+                this.ci1 = ci1;
+                this.i1 = ci1;
+                this.super = ci1;
+
+                this.IndexerCI1 = new Indexer<CI1>(ci1);
+                this.IndexerI1 = new Indexer<I1>(ci1);
+                this.IndexerSuper = new Indexer<Super>(ci1);
+            }
+
+            public readonly CI1 ci1;
+            public readonly I1 i1;
+            public readonly Super super;
+
+            public CI1 CI1 { get => ci1; }
+            public I1 I1 { get => i1; }
+            public Super Super { get => super; }
+
+            public CI1 GetCI1() => ci1;
+            public I1 GetI1() => i1;
+            public Super GetSuper() => super;
+
+            public Indexer<CI1> IndexerCI1 { get; }
+            public Indexer<I1> IndexerI1 { get; }
+            public Indexer<Super> IndexerSuper { get; }
+
+        }
+
+        private readonly Engine _engine;
+        private readonly InterfaceHolder holder;
+
+        public InteropExplicitTypeTests()
+        {
+            holder = new InterfaceHolder();
+            _engine = new Engine(cfg => cfg.AllowClr(
+                        typeof(Console).GetTypeInfo().Assembly,
+                        typeof(File).GetTypeInfo().Assembly))
+                    .SetValue("log", new Action<object>(Console.WriteLine))
+                    .SetValue("assert", new Action<bool>(Assert.True))
+                    .SetValue("equal", new Action<object, object>(Assert.Equal))
+                    .SetValue("holder", holder)
+            ;
+        }
+        [Fact]
+        public void EqualTest()
+        {
+            Assert.Equal(_engine.Evaluate("holder.I1"), _engine.Evaluate("holder.i1"));
+            Assert.NotEqual(_engine.Evaluate("holder.I1"), _engine.Evaluate("holder.ci1"));
+
+            Assert.Equal(_engine.Evaluate("holder.Super"), _engine.Evaluate("holder.super"));
+            Assert.NotEqual(_engine.Evaluate("holder.Super"), _engine.Evaluate("holder.ci1"));
+        }
+
+        [Fact]
+        public void ExplicitInterfaceFromField()
+        {
+            Assert.Equal(holder.i1.Name, _engine.Evaluate("holder.i1.Name"));
+            Assert.NotEqual(holder.i1.Name, _engine.Evaluate("holder.ci1.Name"));
+        }
+
+        [Fact]
+        public void ExplicitInterfaceFromProperty()
+        {
+            Assert.Equal(holder.I1.Name, _engine.Evaluate("holder.I1.Name"));
+            Assert.NotEqual(holder.I1.Name, _engine.Evaluate("holder.CI1.Name"));
+        }
+
+        [Fact]
+        public void ExplicitInterfaceFromMethod()
+        {
+            Assert.Equal(holder.GetI1().Name, _engine.Evaluate("holder.GetI1().Name"));
+            Assert.NotEqual(holder.GetI1().Name, _engine.Evaluate("holder.GetCI1().Name"));
+        }
+
+        [Fact]
+        public void ExplicitInterfaceFromIndexer()
+        {
+            Assert.Equal(holder.IndexerI1[0].Name, _engine.Evaluate("holder.IndexerI1[0].Name"));
+        }
+
+
+        [Fact]
+        public void SuperClassFromField()
+        {
+            Assert.Equal(holder.super.Name, _engine.Evaluate("holder.super.Name"));
+            Assert.NotEqual(holder.super.Name, _engine.Evaluate("holder.ci1.Name"));
+        }
+
+        [Fact]
+        public void SuperClassFromProperty()
+        {
+            Assert.Equal(holder.Super.Name, _engine.Evaluate("holder.Super.Name"));
+            Assert.NotEqual(holder.Super.Name, _engine.Evaluate("holder.CI1.Name"));
+        }
+
+        [Fact]
+        public void SuperClassFromMethod()
+        {
+            Assert.Equal(holder.GetSuper().Name, _engine.Evaluate("holder.GetSuper().Name"));
+            Assert.NotEqual(holder.GetSuper().Name, _engine.Evaluate("holder.GetCI1().Name"));
+        }
+
+        [Fact]
+        public void SuperClassFromIndexer()
+        {
+            Assert.Equal(holder.IndexerSuper[0].Name, _engine.Evaluate("holder.IndexerSuper[0].Name"));
+        }
+
+        public struct NullabeStruct: I1
+        {
+            public NullabeStruct()
+            {
+            }
+            public string name = "NullabeStruct";
+
+            public string Name { get => name; }
+
+            string I1.Name { get => "NullabeStruct as I1"; }
+        }
+
+        public class NullableHolder
+        {
+            public I1? I1 { get; set; }
+            public NullabeStruct? NullabeStruct { get; set; }
+        }
+
+        [Fact]
+        public void TestNullable()
+        {
+            var nullableHolder = new NullableHolder();
+            _engine.SetValue("nullableHolder", nullableHolder);
+            _engine.SetValue("nullabeStruct", new NullabeStruct());
+
+            Assert.Equal(_engine.Evaluate("nullableHolder.NullabeStruct"), Native.JsValue.Null);
+            _engine.Evaluate("nullableHolder.NullabeStruct = nullabeStruct");
+            Assert.Equal(_engine.Evaluate("nullableHolder.NullabeStruct.Name"), nullableHolder.NullabeStruct?.Name);
+        }
+
+        [Fact]
+        public void TestUnwrapClr()
+        {
+            Assert.NotEqual(holder.CI1.Name, _engine.Evaluate("holder.I1.Name"));
+            Assert.Equal(holder.CI1.Name, _engine.Evaluate("unwrapClr(holder.I1).Name"));
+        }
+    }
+}

+ 2 - 2
Jint.Tests/Runtime/InteropTests.cs

@@ -849,7 +849,7 @@ namespace Jint.Tests.Runtime
         {
             var e = new Engine(cfg => cfg
                 .AllowClr(typeof(Person).Assembly)
-                .SetWrapObjectHandler((engine, target) =>
+                .SetWrapObjectHandler((engine, target, type) =>
                 {
                     var instance = new ObjectWrapper(engine, target);
                     if (instance.IsArrayLike)
@@ -884,7 +884,7 @@ namespace Jint.Tests.Runtime
         {
             var engine = new Engine(opt =>
             {
-                opt.SetWrapObjectHandler((eng, obj) =>
+                opt.SetWrapObjectHandler((eng, obj, type) =>
                 {
                     var wrapper = new ObjectWrapper(eng, obj);
                     if (wrapper.IsArrayLike)

+ 9 - 1
Jint/Native/JsValue.cs

@@ -110,6 +110,14 @@ namespace Jint.Native
         /// Creates a valid <see cref="JsValue"/> instance from any <see cref="Object"/> instance
         /// </summary>
         public static JsValue FromObject(Engine engine, object? value)
+        {
+            return FromObjectWithType(engine, value, null);
+        }
+
+        /// <summary>
+        /// Creates a valid <see cref="JsValue"/> instance from any <see cref="Object"/> instance, with a type
+        /// </summary>
+        public static JsValue FromObjectWithType(Engine engine, object? value, Type? type)
         {
             if (value is null)
             {
@@ -132,7 +140,7 @@ namespace Jint.Native
                 }
             }
 
-            if (DefaultObjectConverter.TryConvert(engine, value, out var defaultConversion))
+            if (DefaultObjectConverter.TryConvert(engine, value, type, out var defaultConversion))
             {
                 return defaultConversion;
             }

+ 18 - 2
Jint/Options.cs

@@ -15,7 +15,7 @@ namespace Jint
 {
     public delegate JsValue? MemberAccessorDelegate(Engine engine, object target, string member);
 
-    public delegate ObjectInstance? WrapObjectDelegate(Engine engine, object target);
+    public delegate ObjectInstance? WrapObjectDelegate(Engine engine, object target, Type? type);
 
     public delegate bool ExceptionHandlerDelegate(Exception exception);
 
@@ -120,6 +120,22 @@ namespace Jint
                         (thisObj, arguments) =>
                             new NamespaceReference(engine, TypeConverter.ToString(arguments.At(0)))),
                     PropertyFlag.AllForbidden));
+                engine.Realm.GlobalObject.SetProperty("unwrapClr", new PropertyDescriptor(new ClrFunctionInstance(
+                    engine,
+                    "unwrapClr",
+                    (thisObj, arguments) =>
+                    {
+                        var arg = arguments.At(0);
+                        if (arg is ObjectWrapper obj)
+                        {
+                            return new ObjectWrapper(engine, obj.Target);
+                        }
+                        else
+                        {
+                            return arg;
+                        }
+                    }),
+                    PropertyFlag.AllForbidden));
             }
 
             if (Interop.ExtensionMethodTypes.Count > 0)
@@ -282,7 +298,7 @@ namespace Jint
         /// ObjectInstance using class ObjectWrapper. This function can be used to
         /// change the behavior.
         /// </summary>
-        public WrapObjectDelegate WrapObjectHandler { get; set; } = static (engine, target) => new ObjectWrapper(engine, target);
+        public WrapObjectDelegate WrapObjectHandler { get; set; } = static (engine, target, type) => new ObjectWrapper(engine, target, type);
 
         /// <summary>
         ///

+ 2 - 1
Jint/Runtime/Descriptors/Specialized/ReflectionDescriptor.cs

@@ -29,7 +29,8 @@ namespace Jint.Runtime.Descriptors.Specialized
             get
             {
                 var value = _reflectionAccessor.GetValue(_engine, _target);
-                return JsValue.FromObject(_engine, value);
+                var type = _reflectionAccessor.MemberType;
+                return JsValue.FromObjectWithType(_engine, value, type);
             }
             set
             {

+ 3 - 3
Jint/Runtime/Interop/DefaultObjectConverter.cs

@@ -34,10 +34,10 @@ namespace Jint
             }
         };
 
-        public static bool TryConvert(Engine engine, object value, [NotNullWhen(true)] out JsValue? result)
+        public static bool TryConvert(Engine engine, object value, Type? type, [NotNullWhen(true)] out JsValue? result)
         {
             result = null;
-            var valueType = value.GetType();
+            Type valueType = ObjectWrapper.ClrType(value, type);
 
             var typeMappers = _typeMappers;
 
@@ -109,7 +109,7 @@ namespace Jint
                         }
                         else
                         {
-                            var wrapped = engine.Options.Interop.WrapObjectHandler.Invoke(engine, value);
+                            var wrapped = engine.Options.Interop.WrapObjectHandler.Invoke(engine, value, type);
                             result = wrapped;
 
                             if (engine.Options.Interop.TrackObjectWrapperIdentity && wrapped is not null)

+ 8 - 2
Jint/Runtime/Interop/MethodInfoFunctionInstance.cs

@@ -220,6 +220,12 @@ namespace Jint.Runtime.Interop
                     continue;
                 }
 
+                Type? returnType = null;
+                if (method.Method is MethodInfo methodInfo)
+                {
+                    returnType = methodInfo.ReturnType;
+                }
+
                 // todo: cache method info
                 try
                 {
@@ -227,10 +233,10 @@ namespace Jint.Runtime.Interop
                     {
                         var genericMethodInfo = resolvedMethod;
                         var result = genericMethodInfo.Invoke(thisObj, parameters);
-                        return FromObject(Engine, result);
+                        return FromObjectWithType(Engine, result, returnType);
                     }
 
-                    return FromObject(Engine, method.Method.Invoke(thisObj, parameters));
+                    return FromObjectWithType(Engine, method.Method.Invoke(thisObj, parameters), returnType);
                 }
                 catch (TargetInvocationException exception)
                 {

+ 37 - 11
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Collections;
 using System.Globalization;
 using System.Reflection;
@@ -10,18 +11,20 @@ using Jint.Runtime.Interop.Reflection;
 
 namespace Jint.Runtime.Interop
 {
-	/// <summary>
-	/// Wraps a CLR instance
-	/// </summary>
-	public sealed class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable<ObjectWrapper>
+    /// <summary>
+    /// Wraps a CLR instance
+    /// </summary>
+    public sealed class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable<ObjectWrapper>
     {
         private readonly TypeDescriptor _typeDescriptor;
+        private readonly Type _clrType;
 
-        public ObjectWrapper(Engine engine, object obj)
+        public ObjectWrapper(Engine engine, object obj, Type? type = null)
             : base(engine)
         {
             Target = obj;
-            _typeDescriptor = TypeDescriptor.Get(obj.GetType());
+            _clrType = ClrType(obj, type);
+            _typeDescriptor = TypeDescriptor.Get(_clrType);
             if (_typeDescriptor.LengthProperty is not null)
             {
                 // create a forwarder to produce length from Count or Length if one of them is present
@@ -48,7 +51,7 @@ namespace Jint.Runtime.Interop
                 if (_properties is null || !_properties.ContainsKey(member))
                 {
                     // can try utilize fast path
-                    var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member, forWrite: true);
+                    var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, _clrType, member, forWrite: true);
 
                     if (ReferenceEquals(accessor, ConstantValueAccessor.NullAccessor))
                     {
@@ -160,7 +163,7 @@ namespace Jint.Runtime.Interop
             else if (includeStrings)
             {
                 // we take public properties and fields
-                var type = Target.GetType();
+                var type = _clrType;
                 foreach (var p in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
                 {
                     var indexParameters = p.GetIndexParameters();
@@ -232,7 +235,7 @@ namespace Jint.Runtime.Interop
                 return new PropertyDescriptor(result, PropertyFlag.OnlyEnumerable);
             }
 
-            var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member);
+            var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, _clrType, member);
             var descriptor = accessor.CreatePropertyDescriptor(_engine, Target, enumerable: !isDictionary);
             if (!isDictionary && !ReferenceEquals(descriptor, PropertyDescriptor.Undefined))
             {
@@ -259,6 +262,26 @@ namespace Jint.Runtime.Interop
             return engine.Options.Interop.TypeResolver.GetAccessor(engine, target.GetType(), member.Name, Factory).CreatePropertyDescriptor(engine, target);
         }
 
+        public static Type ClrType(object obj, Type? type)
+        {
+            if (type is null || type == typeof(object))
+            {
+                return obj.GetType();
+            }
+            else
+            {
+                var underlyingType = Nullable.GetUnderlyingType(type);
+                if (underlyingType is not null)
+                {
+                    return underlyingType;
+                }
+                else
+                {
+                    return type;
+                }
+            }
+        }
+
         private static JsValue Iterator(JsValue thisObject, JsValue[] arguments)
         {
             var wrapper = (ObjectWrapper) thisObject;
@@ -296,12 +319,15 @@ namespace Jint.Runtime.Interop
                 return true;
             }
 
-            return Equals(Target, other.Target);
+            return Equals(Target, other.Target) && Equals(_clrType, other._clrType);
         }
 
         public override int GetHashCode()
         {
-            return Target?.GetHashCode() ?? 0;
+            var hashCode = -1468639730;
+            hashCode = hashCode * -1521134295 + Target.GetHashCode();
+            hashCode = hashCode * -1521134295 + _clrType.GetHashCode();
+            return hashCode;
         }
 
         private sealed class DictionaryIterator : IteratorInstance

+ 2 - 0
Jint/Runtime/Interop/Reflection/ReflectionAccessor.cs

@@ -16,6 +16,8 @@ namespace Jint.Runtime.Interop.Reflection
         private readonly object? _memberName;
         private readonly PropertyInfo? _indexer;
 
+        public Type MemberType => _memberType;
+
         protected ReflectionAccessor(
             Type memberType,
             object? memberName,