Browse Source

Improve support for array operations under interop (#1828)

Eric J. Smith 1 năm trước cách đây
mục cha
commit
45e0e7b04d

+ 1 - 16
Jint.Benchmark/ListInteropBenchmark.cs

@@ -44,22 +44,7 @@ public class ListInteropBenchmark
     [GlobalSetup]
     public void Setup()
     {
-        _engine = new Engine(options =>
-        {
-            options
-                .SetWrapObjectHandler((engine, target, type) =>
-                {
-                    var instance = ObjectWrapper.Create(engine, target);
-                    var isArrayLike = IsArrayLike(target.GetType());
-                    if (isArrayLike)
-                    {
-                        instance.Prototype = engine.Intrinsics.Array.PrototypeObject;
-                    }
-
-                    return instance;
-                })
-                ;
-        });
+        _engine = new Engine();
 
         _properties = new JsValue[Count];
         var input = new List<Data>(Count);

+ 26 - 0
Jint.Tests.PublicInterface/InteropTests.NewtonsoftJson.cs

@@ -129,5 +129,31 @@ namespace Jint.Tests.PublicInterface
 
             Assert.Equal("CHANGED", result);
         }
+
+        [Fact]
+        public void ArraysShouldPassThroughCorrectly()
+        {
+            var engine = new Engine();
+
+            const string Json = """
+            {
+                'entries': [
+                    { 'id': 1, 'name': 'One' },
+                    { 'id': 2, 'name': 'Two' },
+                    { 'id': 3, 'name': 'Three' }
+                ]
+            }
+            """;
+
+            var obj = JObject.Parse(Json);
+            engine.SetValue("o", obj);
+
+            var names = engine.Evaluate("o.entries.map(e => e.name)").AsArray();
+
+            Assert.Equal((uint) 3, names.Length);
+            Assert.Equal("One", names[0]);
+            Assert.Equal("Two", names[1]);
+            Assert.Equal("Three", names[2]);
+        }
     }
 }

+ 136 - 122
Jint.Tests.PublicInterface/InteropTests.SystemTextJson.cs

@@ -5,148 +5,99 @@ using System.Text.Json;
 
 namespace Jint.Tests.PublicInterface;
 
-public sealed class SystemTextJsonValueConverter : IObjectConverter
+public partial class InteropTests
 {
-    public static readonly SystemTextJsonValueConverter Instance = new();
+    [Fact]
+    public void ArrayPrototypeFindWithInteropJsonArray()
+    {
+        var engine = GetEngine();
 
-    private SystemTextJsonValueConverter()
+        var array = new JsonArray { "A", "B", "C" };
+        engine.SetValue("array", array);
+
+        Assert.Equal(1, engine.Evaluate("array.findIndex((x) => x === 'B')"));
+        Assert.Equal('B', engine.Evaluate("array.find((x) => x === 'B')"));
+    }
+
+    [Fact]
+    public void ArrayPrototypePushWithInteropJsonArray()
     {
+        var engine = GetEngine();
+
+        var array = new JsonArray { "A", "B", "C" };
+        engine.SetValue("array", array);
+
+        engine.Evaluate("array.push('D')");
+        Assert.Equal(4, array.Count);
+        Assert.Equal("D", array[3]?.ToString());
+        Assert.Equal(3, engine.Evaluate("array.lastIndexOf('D')"));
     }
 
-    public bool TryConvert(Engine engine, object value, out JsValue result)
+    [Fact]
+    public void ArrayPrototypePopWithInteropJsonArray()
     {
-        if (value is JsonValue jsonValue)
-        {
-            var valueKind = jsonValue.GetValueKind();
-            switch (valueKind)
-            {
-                case JsonValueKind.Object:
-                case JsonValueKind.Array:
-                    result = JsValue.FromObject(engine, jsonValue);
-                    break;
-                case JsonValueKind.String:
-                    result = jsonValue.ToString();
-                    break;
-                case JsonValueKind.Number:
-                    if (jsonValue.TryGetValue<double>(out var doubleValue))
-                    {
-                        result = JsNumber.Create(doubleValue);
-                    }
-                    else
-                    {
-                        result = JsValue.Undefined;
-                    }
-                    break;
-                case JsonValueKind.True:
-                    result = JsBoolean.True;
-                    break;
-                case JsonValueKind.False:
-                    result = JsBoolean.False;
-                    break;
-                case JsonValueKind.Undefined:
-                    result = JsValue.Undefined;
-                    break;
-                case JsonValueKind.Null:
-                    result = JsValue.Null;
-                    break;
-                default:
-                    result = JsValue.Undefined;
-                    break;
-            }
-            return true;
-        }
-        result = JsValue.Undefined;
-        return false;
+        var engine = GetEngine();
+
+        var array = new JsonArray { "A", "B", "C" };
+        engine.SetValue("array", array);
 
+        Assert.Equal(2, engine.Evaluate("array.lastIndexOf('C')"));
+        Assert.Equal(3, array.Count);
+        Assert.Equal("C", engine.Evaluate("array.pop()"));
+        Assert.Equal(2, array.Count);
+        Assert.Equal(-1, engine.Evaluate("array.lastIndexOf('C')"));
     }
-}
-public partial class InteropTests
-{
+
     [Fact]
     public void AccessingJsonNodeShouldWork()
     {
         const string Json = """
-        {
-            "falseValue": false,
-            "employees": {
-                "trueValue": true,
-                "falseValue": false,
-                "number": 123.456,
-                "zeroNumber": 0,
-                "emptyString":"",
-                "nullValue":null,
-                "other": "abc",
-                "type": "array",
-                "value": [
-                    {
-                        "firstName": "John",
-                        "lastName": "Doe"
-                    },
-                    {
-                        "firstName": "Jane",
-                        "lastName": "Doe"
-                    }
-                ]
-            }
-        }
-        """;
+                            {
+                                "falseValue": false,
+                                "employees": {
+                                    "trueValue": true,
+                                    "falseValue": false,
+                                    "number": 123.456,
+                                    "zeroNumber": 0,
+                                    "emptyString":"",
+                                    "nullValue":null,
+                                    "other": "abc",
+                                    "type": "array",
+                                    "value": [
+                                        {
+                                            "firstName": "John",
+                                            "lastName": "Doe"
+                                        },
+                                        {
+                                            "firstName": "Jane",
+                                            "lastName": "Doe"
+                                        }
+                                    ]
+                                }
+                            }
+                            """;
 
         var variables = JsonNode.Parse(Json);
 
-        var engine = new Engine(options =>
-        {
-#if !NET8_0_OR_GREATER
-            // Jint doesn't know about the types statically as they are not part of the out-of-the-box experience
-
-            // make JsonArray behave like JS array
-            options.Interop.WrapObjectHandler = static (e, target, type) =>
-            {
-                if (target is JsonArray)
-                {
-                    var wrapped = ObjectWrapper.Create(e, target);
-                    wrapped.Prototype = e.Intrinsics.Array.PrototypeObject;
-                    return wrapped;
-                }
-
-                return ObjectWrapper.Create(e, target);
-            };
-
-            options.AddObjectConverter(SystemTextJsonValueConverter.Instance);
-
-            // we cannot access this[string] with anything else than JsonObject, otherwise itw will throw
-            options.Interop.TypeResolver = new TypeResolver
-            {
-                MemberFilter = static info =>
-                {
-                    if (info.ReflectedType != typeof(JsonObject) && info.Name == "Item" && info is System.Reflection.PropertyInfo p)
-                    {
-                        var parameters = p.GetIndexParameters();
-                        return parameters.Length != 1 || parameters[0].ParameterType != typeof(string);
-                    }
-
-                    return true;
-                }
-            };
-#endif
-        });
+        var engine = GetEngine();
 
         engine
             .SetValue("falseValue", false)
             .SetValue("variables", variables)
             .Execute("""
-                 function populateFullName() {
-                     return variables['employees'].value.map(item => {
-                         var newItem =
-                         {
-                             "firstName": item.firstName,
-                             "lastName": item.lastName,
-                             "fullName": item.firstName + ' ' + item.lastName
-                         };
-
-                         return newItem;
-                     });
-                 }
-             """);
+                         function populateFullName() {
+                             return variables['employees'].value.map(item => {
+                                 var newItem =
+                                 {
+                                     "firstName": item.firstName,
+                                     "lastName": item.lastName,
+                                     "fullName": item.firstName + ' ' + item.lastName
+                                 };
+                     
+                                 return newItem;
+                             });
+                         }
+                     """);
 
         // reading data
         var result = engine.Evaluate("populateFullName()").AsArray();
@@ -202,4 +153,67 @@ public partial class InteropTests
         Assert.True(engine.Evaluate("variables.employees.number == 456.789").AsBoolean());
         Assert.True(engine.Evaluate("variables.employees.other == 'def'").AsBoolean());
     }
+
+    private static Engine GetEngine()
+    {
+        var engine = new Engine(options =>
+        {
+#if !NET8_0_OR_GREATER
+            // Jint doesn't know about the types statically as they are not part of the out-of-the-box experience
+            options.AddObjectConverter(SystemTextJsonValueConverter.Instance);
+#endif
+        });
+
+        return engine;
+    }
+}
+
+file sealed class SystemTextJsonValueConverter : IObjectConverter
+{
+    public static readonly SystemTextJsonValueConverter Instance = new();
+
+    private SystemTextJsonValueConverter()
+    {
+    }
+
+    public bool TryConvert(Engine engine, object value, out JsValue result)
+    {
+        if (value is JsonValue jsonValue)
+        {
+            var valueKind = jsonValue.GetValueKind();
+            switch (valueKind)
+            {
+                case JsonValueKind.Object:
+                case JsonValueKind.Array:
+                    result = JsValue.FromObject(engine, jsonValue);
+                    break;
+                case JsonValueKind.String:
+                    result = jsonValue.ToString();
+                    break;
+                case JsonValueKind.Number:
+                    result = jsonValue.TryGetValue<double>(out var doubleValue) ? JsNumber.Create(doubleValue) : JsValue.Undefined;
+                    break;
+                case JsonValueKind.True:
+                    result = JsBoolean.True;
+                    break;
+                case JsonValueKind.False:
+                    result = JsBoolean.False;
+                    break;
+                case JsonValueKind.Undefined:
+                    result = JsValue.Undefined;
+                    break;
+                case JsonValueKind.Null:
+                    result = JsValue.Null;
+                    break;
+                default:
+                    result = JsValue.Undefined;
+                    break;
+            }
+
+            return true;
+        }
+
+        result = JsValue.Undefined;
+        return false;
+    }
 }

+ 1 - 23
Jint.Tests/Runtime/InteropTests.cs

@@ -848,16 +848,6 @@ namespace Jint.Tests.Runtime
         {
             var e = new Engine(cfg => cfg
                 .AllowClr(typeof(Person).Assembly)
-                .SetWrapObjectHandler((engine, target, type) =>
-                {
-                    var instance = ObjectWrapper.Create(engine, target);
-                    if (instance.IsArrayLike)
-                    {
-                        instance.SetPrototypeOf(engine.Realm.Intrinsics.Array.PrototypeObject);
-                    }
-
-                    return instance;
-                })
             );
 
             var person = new Person
@@ -881,19 +871,7 @@ namespace Jint.Tests.Runtime
         [Fact]
         public void CanSetIsConcatSpreadableForArrays()
         {
-            var engine = new Engine(opt =>
-            {
-                opt.SetWrapObjectHandler((eng, obj, type) =>
-                {
-                    var wrapper = ObjectWrapper.Create(eng, obj);
-                    if (wrapper.IsArrayLike)
-                    {
-                        wrapper.SetPrototypeOf(eng.Realm.Intrinsics.Array.PrototypeObject);
-                        wrapper.Set(GlobalSymbolRegistry.IsConcatSpreadable, true);
-                    }
-                    return wrapper;
-                });
-            });
+            var engine = new Engine();
 
             engine
                 .SetValue("list1", new List<string> { "A", "B", "C" })

+ 76 - 0
Jint/Native/Array/ArrayOperations.cs

@@ -47,6 +47,11 @@ namespace Jint.Native.Array
                 return new JsTypedArrayOperations(typedArrayInstance);
             }
 
+            if (instance is ArrayLikeWrapper arrayWrapper)
+            {
+                return new ArrayLikeOperations(arrayWrapper);
+            }
+
             if (instance is ObjectWrapper wrapper)
             {
                 var descriptor = wrapper._typeDescriptor;
@@ -560,6 +565,77 @@ namespace Jint.Native.Array
             public override void DeletePropertyOrThrow(ulong index)
                 => _target.DeletePropertyOrThrow(index);
         }
+
+        private sealed class ArrayLikeOperations : ArrayOperations
+        {
+            private readonly ArrayLikeWrapper _target;
+
+            public ArrayLikeOperations(ArrayLikeWrapper wrapper)
+            {
+                _target = wrapper;
+            }
+
+            public override ObjectInstance Target => _target;
+
+            public override ulong GetSmallestIndex(ulong length) => 0;
+
+            public override uint GetLength() => (uint) _target.Length;
+
+            public override ulong GetLongLength() => GetLength();
+
+            public override void SetLength(ulong length)
+            {
+                while (_target.Length > (int) length)
+                {
+                    // shrink list to fit
+                    _target.RemoveAt(_target.Length - 1);
+                }
+                
+                while (_target.Length < (int) length)
+                {
+                    // expand list to fit
+                    _target.AddDefault();
+                }
+            }
+
+            public override void EnsureCapacity(ulong capacity)
+            {
+                _target.EnsureCapacity((int)capacity);
+            }
+
+            public override JsValue Get(ulong index) => index < (ulong) _target.Length ? ReadValue((int) index) : JsValue.Undefined;
+
+            public override bool TryGetValue(ulong index, out JsValue value)
+            {
+                if (index < (ulong) _target.Length)
+                {
+                    value = ReadValue((int) index);
+                    return true;
+                }
+
+                value = JsValue.Undefined;
+                return false;
+            }
+
+            private JsValue ReadValue(int index)
+            {
+                return (uint) index < _target.Length ? JsValue.FromObject(_target.Engine, _target.GetAt(index)) : JsValue.Undefined;
+            }
+
+            public override bool HasProperty(ulong index) => index < (ulong) _target.Length;
+
+            public override void CreateDataPropertyOrThrow(ulong index, JsValue value)
+                => _target.CreateDataPropertyOrThrow(index, value);
+
+            public override void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true)
+            {
+                EnsureCapacity(index + 1);
+                _target.SetAt((int)index, value);
+            }
+
+            public override void DeletePropertyOrThrow(ulong index)
+                => _target.DeletePropertyOrThrow(index);
+        }
     }
 
     /// <summary>

+ 230 - 0
Jint/Runtime/Interop/ObjectWrapper.Specialized.cs

@@ -0,0 +1,230 @@
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Jint.Extensions;
+using Jint.Native;
+
+namespace Jint.Runtime.Interop;
+
+internal abstract class ArrayLikeWrapper : ObjectWrapper
+{
+#pragma warning disable CS0618 // Type or member is obsolete
+    protected ArrayLikeWrapper(
+        Engine engine,
+        object obj,
+        Type itemType,
+        Type? type = null) : base(engine, obj, type)
+#pragma warning restore CS0618 // Type or member is obsolete
+    {
+        ItemType = itemType;
+        if (engine.Options.Interop.AttachArrayPrototype)
+        {
+            Prototype = engine.Intrinsics.Array.PrototypeObject;
+        }
+    }
+
+    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields)]
+    private Type ItemType { get; }
+
+    public abstract int Length { get; }
+
+    public override JsValue Get(JsValue property, JsValue receiver)
+    {
+        if (property.IsInteger())
+        {
+            return FromObject(_engine, GetAt(property.AsInteger()));
+        }
+
+        return base.Get(property, receiver);
+    }
+
+    public override bool HasProperty(JsValue property)
+    {
+        if (property.IsNumber())
+        {
+            var value = ((JsNumber) property)._value;
+            if (TypeConverter.IsIntegralNumber(value))
+            {
+                var index = (int) value;
+                if (Target is ICollection collection && index < collection.Count)
+                {
+                    return true;
+                }
+            }
+        }
+
+        return base.HasProperty(property);
+    }
+
+    public override bool Delete(JsValue property)
+    {
+        if (!_engine.Options.Interop.AllowWrite)
+        {
+            return false;
+        }
+
+        if (property.IsNumber())
+        {
+            var value = ((JsNumber) property)._value;
+            if (TypeConverter.IsIntegralNumber(value))
+            {
+                DoSetAt((int) value, default);
+                return true;
+            }
+        }
+
+        return base.Delete(property);
+    }
+
+
+    public abstract object? GetAt(int index);
+
+    public void SetAt(int index, JsValue value)
+    {
+        if (_engine.Options.Interop.AllowWrite)
+        {
+            EnsureCapacity(index);
+            DoSetAt(index, ConvertToItemType(value));
+        }
+    }
+
+    protected abstract void DoSetAt(int index, object? value);
+
+    public abstract void AddDefault();
+
+    public abstract void Add(JsValue value);
+
+    public abstract void RemoveAt(int index);
+
+    public virtual void EnsureCapacity(int capacity)
+    {
+        while (Length < capacity)
+        {
+            AddDefault();
+        }
+    }
+
+    protected object? ConvertToItemType(JsValue value)
+    {
+        object? converted;
+        if (ItemType == typeof(JsValue))
+        {
+            converted = value;
+        }
+        else if (!ReflectionExtensions.TryConvertViaTypeCoercion(ItemType, Engine.Options.Interop.ValueCoercion, value, out converted))
+        {
+            // attempt to convert the JsValue to the target type
+            converted = value.ToObject();
+            if (converted != null && converted.GetType() != ItemType)
+            {
+                converted = Engine.TypeConverter.Convert(converted, ItemType, CultureInfo.InvariantCulture);
+            }
+        }
+
+        return converted;
+    }
+}
+
+internal class ListWrapper : ArrayLikeWrapper
+{
+    private readonly IList? _list;
+
+    internal ListWrapper(Engine engine, IList target, Type type)
+        : base(engine, target, typeof(object), type)
+    {
+        _list = target;
+    }
+
+    public override int Length => _list?.Count ?? 0;
+
+    public override object? GetAt(int index)
+    {
+        if (_list is not null && index >= 0 && index < _list.Count)
+        {
+            return _list[index];
+        }
+
+        return null;
+    }
+
+    protected override void DoSetAt(int index, object? value)
+    {
+        if (_list is not null)
+        {
+            _list[index] = value;
+        }
+    }
+
+    public override void AddDefault() => _list?.Add(null);
+
+    public override void Add(JsValue value) => _list?.Add(ConvertToItemType(value));
+
+    public override void RemoveAt(int index) => _list?.RemoveAt(index);
+}
+
+internal class GenericListWrapper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields)] T> : ArrayLikeWrapper
+{
+    private readonly IList<T> _list;
+
+    public GenericListWrapper(Engine engine, IList<T> target, Type? type)
+        : base(engine, target, typeof(T), type)
+    {
+        _list = target;
+    }
+
+    public override int Length => _list.Count;
+
+    public override object? GetAt(int index)
+    {
+        if (index >= 0 && index < _list.Count)
+        {
+            return _list[index];
+        }
+
+        return null;
+    }
+
+    protected override void DoSetAt(int index, object? value) => _list[index] = (T) value!;
+
+    public override void AddDefault() => _list.Add(default!);
+
+    public override void Add(JsValue value)
+    {
+        var converted = ConvertToItemType(value);
+        _list.Add((T) converted!);
+    }
+
+    public override void RemoveAt(int index) => _list.RemoveAt(index);
+}
+
+internal sealed class ReadOnlyListWrapper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields)] T> : ArrayLikeWrapper
+{
+    private readonly IReadOnlyList<T> _list;
+
+    public ReadOnlyListWrapper(Engine engine, IReadOnlyList<T> target, Type type) : base(engine, target, typeof(T), type)
+    {
+        _list = target;
+    }
+
+    public override int Length => _list.Count;
+
+    public override object? GetAt(int index)
+    {
+        if (index >= 0 && index < _list.Count)
+        {
+            return _list[index];
+        }
+
+        return null;
+    }
+
+    public override void AddDefault() => ExceptionHelper.ThrowNotSupportedException();
+
+    protected override void DoSetAt(int index, object? value) => ExceptionHelper.ThrowNotSupportedException();
+
+    public override void Add(JsValue value) => ExceptionHelper.ThrowNotSupportedException();
+
+    public override void RemoveAt(int index) => ExceptionHelper.ThrowNotSupportedException();
+
+    public override void EnsureCapacity(int capacity) => ExceptionHelper.ThrowNotSupportedException();
+}

+ 79 - 76
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -1,4 +1,5 @@
 using System.Collections;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Reflection;
 using Jint.Native;
@@ -17,7 +18,7 @@ namespace Jint.Runtime.Interop
     /// <summary>
     /// Wraps a CLR instance
     /// </summary>
-    public sealed class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable<ObjectWrapper>
+    public class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable<ObjectWrapper>
     {
         internal readonly TypeDescriptor _typeDescriptor;
 
@@ -42,7 +43,7 @@ namespace Jint.Runtime.Interop
                 if (_typeDescriptor.IsArrayLike && engine.Options.Interop.AttachArrayPrototype)
                 {
                     // if we have array-like object, we can attach array prototype
-                    SetPrototypeOf(engine.Intrinsics.Array.PrototypeObject);
+                    _prototype = engine.Intrinsics.Array.PrototypeObject;
                 }
             }
         }
@@ -50,21 +51,79 @@ namespace Jint.Runtime.Interop
         /// <summary>
         /// Creates a new object wrapper for given object instance and exposed type.
         /// </summary>
-        public static ObjectInstance Create(Engine engine, object obj, Type? type = null)
-#pragma warning disable CS0618 // Type or member is obsolete
+        public static ObjectInstance Create(Engine engine, object target, Type? type = null)
         {
+            if (target == null)
+            {
+                ExceptionHelper.ThrowArgumentNullException(nameof(target));
+            }
 
-#if NET8_0_OR_GREATER
-            if (type == typeof(System.Text.Json.Nodes.JsonNode))
+            // STJ integration
+            if (string.Equals(type?.FullName, "System.Text.Json.Nodes.JsonNode", StringComparison.Ordinal))
             {
                 // we need to always expose the actual type instead of the type nodes provide
-                type = obj.GetType();
+                type = target.GetType();
             }
-#endif
 
-            return new ObjectWrapper(engine, obj, type);
-        }
+            type ??= target.GetType();
+
+            if (TryBuildArrayLikeWrapper(engine, target, type, out var wrapper))
+            {
+                return wrapper;
+            }
+
+#pragma warning disable CS0618 // Type or member is obsolete
+            return new ObjectWrapper(engine, target, type);
 #pragma warning restore CS0618 // Type or member is obsolete
+        }
+
+        private static bool TryBuildArrayLikeWrapper(
+            Engine engine,
+            object target,
+            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type,
+            [NotNullWhen(true)] out ArrayLikeWrapper? result)
+        {
+#pragma warning disable IL2055
+#pragma warning disable IL3050
+
+            result = null;
+
+            // check for generic interfaces
+            foreach (var i in type.GetInterfaces())
+            {
+                if (!i.IsGenericType)
+                {
+                    continue;
+                }
+
+                var arrayItemType = i.GenericTypeArguments[0];
+
+                if (i.GetGenericTypeDefinition() == typeof(IList<>))
+                {
+                    var arrayWrapperType = typeof(GenericListWrapper<>).MakeGenericType(arrayItemType);
+                    result = (ArrayLikeWrapper) Activator.CreateInstance(arrayWrapperType, engine, target, type)!;
+                    break;
+                }
+
+                if (i.GetGenericTypeDefinition() == typeof(IReadOnlyList<>))
+                {
+                    var arrayWrapperType = typeof(ReadOnlyListWrapper<>).MakeGenericType(arrayItemType);
+                    result = (ArrayLikeWrapper) Activator.CreateInstance(arrayWrapperType, engine, target, type)!;
+                    break;
+                }
+            }
+
+#pragma warning restore IL3050
+#pragma warning restore IL2055
+
+            // least specific
+            if (result is null && target is IList list)
+            {
+                result = new ListWrapper(engine, list, type);
+            }
+
+            return result is not null;
+        }
 
         public object Target { get; }
         internal Type ClrType { get; }
@@ -129,70 +188,18 @@ namespace Jint.Runtime.Interop
             return true;
         }
 
-        public override object ToObject()
-        {
-            return Target;
-        }
+        public override object ToObject() => Target;
 
         public override void RemoveOwnProperty(JsValue property)
         {
-            if (_engine.Options.Interop.AllowWrite && property is JsString jsString)
+            if (_engine.Options.Interop.AllowWrite && property is JsString jsString && _typeDescriptor.RemoveMethod is not null)
             {
-                _typeDescriptor.Remove(Target, jsString.ToString());
+                _typeDescriptor.RemoveMethod.Invoke(Target, [jsString.ToString()]);
             }
         }
 
-        public override bool HasProperty(JsValue property)
-        {
-            if (property.IsNumber())
-            {
-                var value = ((JsNumber) property)._value;
-                if (TypeConverter.IsIntegralNumber(value))
-                {
-                    var index = (int) value;
-                    if (Target is ICollection collection && index < collection.Count)
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            return base.HasProperty(property);
-        }
-
-        public override bool Delete(JsValue property)
-        {
-            if (Target is IList && property.IsNumber())
-            {
-                return true;
-            }
-            
-            return base.Delete(property);
-        }
-
         public override JsValue Get(JsValue property, JsValue receiver)
         {
-            if (property.IsInteger())
-            {
-                var index = (int) ((JsNumber) property)._value;
-                if (Target is IList list)
-                {
-                    return (uint) index < list.Count ? FromObject(_engine, list[index]) : Undefined;
-                }
-
-                if (Target is ICollection collection
-                    && _typeDescriptor.IntegerIndexerProperty is not null)
-                {
-                    // via reflection is slow, but better than nothing
-                    if (index < collection.Count)
-                    {
-                        return FromObject(_engine, _typeDescriptor.IntegerIndexerProperty.GetValue(Target, [index]));
-                    }
-
-                    return Undefined;
-                }
-            }
-
             if (!_typeDescriptor.IsDictionary
                 && Target is ICollection c
                 && CommonProperties.Length.Equals(property))
@@ -228,7 +235,7 @@ namespace Jint.Runtime.Interop
             var includeStrings = (types & Types.String) != Types.Empty;
             if (includeStrings && _typeDescriptor.IsStringKeyedGenericDictionary) // expando object for instance
             {
-                var keys = _typeDescriptor.GetKeys(Target);
+                var keys = (ICollection<string>) _typeDescriptor.KeysAccessor!.GetValue(Target)!;
                 foreach (var key in keys)
                 {
                     var jsString = JsString.Create(key);
@@ -368,18 +375,14 @@ namespace Jint.Runtime.Interop
             {
                 return obj.GetType();
             }
-            else
+
+            var underlyingType = Nullable.GetUnderlyingType(type);
+            if (underlyingType is not null)
             {
-                var underlyingType = Nullable.GetUnderlyingType(type);
-                if (underlyingType is not null)
-                {
-                    return underlyingType;
-                }
-                else
-                {
-                    return type;
-                }
+                return underlyingType;
             }
+
+            return type;
         }
 
         private static JsValue Iterator(JsValue thisObject, JsValue[] arguments)

+ 10 - 5
Jint/Runtime/Interop/Reflection/IndexerAccessor.cs

@@ -14,14 +14,15 @@ internal sealed class IndexerAccessor : ReflectionAccessor
 {
     private readonly object _key;
 
-    internal readonly PropertyInfo _indexer;
     private readonly MethodInfo? _getter;
     private readonly MethodInfo? _setter;
     private readonly MethodInfo? _containsKey;
 
     private IndexerAccessor(PropertyInfo indexer, MethodInfo? containsKey, object key) : base(indexer.PropertyType)
     {
-        _indexer = indexer;
+        Indexer = indexer;
+        FirstIndexParameter = indexer.GetIndexParameters()[0];
+
         _containsKey = containsKey;
         _key = key;
 
@@ -29,6 +30,10 @@ internal sealed class IndexerAccessor : ReflectionAccessor
         _setter = indexer.GetSetMethod();
     }
 
+    internal PropertyInfo Indexer { get; }
+
+    internal ParameterInfo FirstIndexParameter { get; }
+
     internal static bool TryFindIndexer(
         Engine engine,
         [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] Type targetType,
@@ -48,7 +53,7 @@ internal sealed class IndexerAccessor : ReflectionAccessor
             integerKey = intKeyTemp;
         }
 
-        var filter = new Func<MemberInfo, bool>(m => engine.Options.Interop.TypeResolver.Filter(engine, m));
+        var filter = new Func<MemberInfo, bool>(m => engine.Options.Interop.TypeResolver.Filter(engine, targetType, m));
 
         // default indexer wins
         var descriptor = TypeDescriptor.Get(targetType);
@@ -168,9 +173,9 @@ internal sealed class IndexerAccessor : ReflectionAccessor
     }
 
 
-    public override bool Readable => _indexer.CanRead;
+    public override bool Readable => Indexer.CanRead;
 
-    public override bool Writable => _indexer.CanWrite;
+    public override bool Writable => Indexer.CanWrite;
 
     protected override object? DoGetValue(object target, string memberName)
     {

+ 4 - 20
Jint/Runtime/Interop/TypeDescriptor.cs

@@ -73,6 +73,10 @@ namespace Jint.Runtime.Interop
 
         public bool Iterable => IsArrayLike || IsDictionary || IsEnumerable;
 
+        public MethodInfo? RemoveMethod => _removeMethod;
+
+        public PropertyInfo? KeysAccessor => _keysAccessor;
+
         public static TypeDescriptor Get(
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.Interfaces)]
             Type type)
@@ -203,25 +207,5 @@ namespace Jint.Runtime.Interop
                 return false;
             }
         }
-
-        public bool Remove(object target, string key)
-        {
-            if (_removeMethod is null)
-            {
-                return false;
-            }
-
-            return (bool) _removeMethod.Invoke(target, [key])!;
-        }
-
-        public ICollection<string> GetKeys(object target)
-        {
-            if (!IsStringKeyedGenericDictionary)
-            {
-                ExceptionHelper.ThrowInvalidOperationException("Not a string-keyed dictionary");
-            }
-
-            return (ICollection<string>) _keysAccessor!.GetValue(target)!;
-        }
     }
 }

+ 39 - 11
Jint/Runtime/Interop/TypeResolver.cs

@@ -24,10 +24,32 @@ namespace Jint.Runtime.Interop
         /// By default allows all but will also be limited by <see cref="Options.InteropOptions.AllowGetType"/> configuration.
         /// </summary>
         /// <seealso cref="Options.InteropOptions.AllowGetType"/>
-        public Predicate<MemberInfo> MemberFilter { get; set; } = _ => true;
+        public Predicate<MemberInfo> MemberFilter { get; set; } = static _ => true;
 
-        internal bool Filter(Engine engine, MemberInfo m)
+        internal bool Filter(Engine engine, Type targetType, MemberInfo m)
         {
+            // some specific problematic indexer cases for JSON interop
+            if (string.Equals(m.Name, "Item", StringComparison.Ordinal) && m is PropertyInfo p)
+            {
+                var indexParameters = p.GetIndexParameters();
+                if (indexParameters.Length == 1)
+                {
+                    var parameter = indexParameters[0];
+                    if (string.Equals(m.DeclaringType?.FullName, "System.Text.Json.Nodes.JsonNode", StringComparison.Ordinal))
+                    {
+                        // STJ
+                        return parameter.ParameterType == typeof(string) && string.Equals(targetType.FullName, "System.Text.Json.Nodes.JsonObject", StringComparison.Ordinal)
+                               || parameter.ParameterType == typeof(int) && string.Equals(targetType.FullName, "System.Text.Json.Nodes.JsonArray", StringComparison.Ordinal);
+                    }
+
+                    if (string.Equals(targetType.FullName, "Newtonsoft.Json.Linq.JArray", StringComparison.Ordinal))
+                    {
+                        // NJ
+                        return parameter.ParameterType == typeof(int);
+                    }
+                }
+            }
+
             return (engine.Options.Interop.AllowGetType || !string.Equals(m.Name, nameof(GetType), StringComparison.Ordinal)) && MemberFilter(m);
         }
 
@@ -121,7 +143,7 @@ namespace Jint.Runtime.Interop
                 {
                     foreach (var iprop in iface.GetProperties())
                     {
-                        if (!Filter(engine, iprop))
+                        if (!Filter(engine, type, iprop))
                         {
                             continue;
                         }
@@ -154,7 +176,7 @@ namespace Jint.Runtime.Interop
                 {
                     foreach (var imethod in iface.GetMethods())
                     {
-                        if (!Filter(engine, imethod))
+                        if (!Filter(engine, type, imethod))
                         {
                             continue;
                         }
@@ -180,7 +202,7 @@ namespace Jint.Runtime.Interop
             var score = int.MaxValue;
             if (indexerAccessor != null)
             {
-                var parameter = indexerAccessor._indexer.GetIndexParameters()[0];
+                var parameter = indexerAccessor.FirstIndexParameter;
                 score = CalculateIndexerScore(parameter, isInteger);
             }
 
@@ -191,7 +213,13 @@ namespace Jint.Runtime.Interop
                 {
                     if (IndexerAccessor.TryFindIndexer(engine, interfaceType, memberName, out var accessor, out _))
                     {
-                        var parameter = accessor._indexer.GetIndexParameters()[0];
+                        // ensure that original type is allowed against indexer
+                        if (!Filter(engine, type, accessor.Indexer))
+                        {
+                            continue;
+                        }
+
+                        var parameter = accessor.FirstIndexParameter;
                         var newScore = CalculateIndexerScore(parameter, isInteger);
                         if (newScore < score)
                         {
@@ -214,7 +242,7 @@ namespace Jint.Runtime.Interop
                 var matches = new List<MethodInfo>();
                 foreach (var method in extensionMethods)
                 {
-                    if (!Filter(engine, method))
+                    if (!Filter(engine, type, method))
                     {
                         continue;
                     }
@@ -271,13 +299,13 @@ namespace Jint.Runtime.Interop
             {
                 foreach (var p in t.GetProperties(bindingFlags))
                 {
-                    if (!Filter(engine, p))
+                    if (!Filter(engine, type, p))
                     {
                         continue;
                     }
 
                     // only if it's not an indexer, we can do case-ignoring matches
-                    var isStandardIndexer = p.GetIndexParameters().Length == 1 && string.Equals(p.Name, "Item", StringComparison.Ordinal);
+                    var isStandardIndexer = string.Equals(p.Name, "Item", StringComparison.Ordinal) && p.GetIndexParameters().Length == 1;
                     if (!isStandardIndexer)
                     {
                         foreach (var name in typeResolverMemberNameCreator(p))
@@ -319,7 +347,7 @@ namespace Jint.Runtime.Interop
             FieldInfo? field = null;
             foreach (var f in type.GetFields(bindingFlags))
             {
-                if (!Filter(engine, f))
+                if (!Filter(engine, type, f))
                 {
                     continue;
                 }
@@ -344,7 +372,7 @@ namespace Jint.Runtime.Interop
             List<MethodInfo>? methods = null;
             void AddMethod(MethodInfo m)
             {
-                if (!Filter(engine, m))
+                if (!Filter(engine, type, m))
                 {
                     return;
                 }