Browse Source

Introduce TypeResolver with member filter and name comparer (#951)

* Support filtering interop members with predicate
* Support configuring custom property name comparer in interop
* Make TypeReference use TypeResolver resolution logic
Marko Lahma 4 years ago
parent
commit
71de3317f1

+ 1 - 0
Jint.Tests/Jint.Tests.csproj

@@ -6,6 +6,7 @@
     <SignAssembly>true</SignAssembly>
     <SignAssembly>true</SignAssembly>
     <IsPackable>false</IsPackable>
     <IsPackable>false</IsPackable>
     <LangVersion>latest</LangVersion>
     <LangVersion>latest</LangVersion>
+    <NoWarn>612</NoWarn>
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="Runtime\Scripts\*.*;Parser\Scripts\*.*" />
     <EmbeddedResource Include="Runtime\Scripts\*.*;Parser\Scripts\*.*" />

+ 15 - 9
Jint.Tests/Runtime/Domain/HiddenMembers.cs

@@ -1,16 +1,22 @@
-namespace Jint.Tests.Runtime.Domain
+using System;
+
+namespace Jint.Tests.Runtime.Domain
 {
 {
     public class HiddenMembers
     public class HiddenMembers
     {
     {
+        [Obsolete]
+        public string Field1 = "Field1";
+
+        public string Field2 = "Field2";
+
+        [Obsolete]
         public string Member1 { get; set; } = "Member1";
         public string Member1 { get; set; } = "Member1";
+
         public string Member2 { get; set; } = "Member2";
         public string Member2 { get; set; } = "Member2";
-        public string Method1()
-        {
-            return "Method1";
-        }
-        public string Method2()
-        {
-            return "Method2";
-        }
+
+        [Obsolete]
+        public string Method1() => "Method1";
+
+        public string Method2() => "Method2";
     }
     }
 }
 }

+ 128 - 0
Jint.Tests/Runtime/InteropTests.MemberAccess.cs

@@ -0,0 +1,128 @@
+using System;
+using Jint.Native;
+using Jint.Runtime.Interop;
+using Jint.Tests.Runtime.Domain;
+using Xunit;
+
+namespace Jint.Tests.Runtime
+{
+    public partial class InteropTests
+    {
+        [Fact]
+        public void ShouldHideSpecificMembers()
+        {
+            var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
+            {
+                if (target is HiddenMembers)
+                {
+                    if (member == nameof(HiddenMembers.Member2) || member == nameof(HiddenMembers.Method2))
+                    {
+                        return JsValue.Undefined;
+                    }
+                }
+
+                return null;
+            }));
+
+            engine.SetValue("m", new HiddenMembers());
+
+            Assert.Equal("Member1", engine.Evaluate("m.Member1").ToString());
+            Assert.Equal("undefined", engine.Evaluate("m.Member2").ToString());
+            Assert.Equal("Method1", engine.Evaluate("m.Method1()").ToString());
+            // check the method itself, not its invokation as it would mean invoking "undefined"
+            Assert.Equal("undefined", engine.Evaluate("m.Method2").ToString());
+        }
+
+        [Fact]
+        public void ShouldOverrideMembers()
+        {
+            var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
+            {
+                if (target is HiddenMembers && member == nameof(HiddenMembers.Member1))
+                {
+                    return "Orange";
+                }
+
+                return null;
+            }));
+
+            engine.SetValue("m", new HiddenMembers());
+
+            Assert.Equal("Orange", engine.Evaluate("m.Member1").ToString());
+        }
+
+        [Fact]
+        public void ShouldBeAbleToFilterMembers()
+        {
+            var engine = new Engine(options => options
+                .SetTypeResolver(new TypeResolver
+                {
+                    MemberFilter = member => !Attribute.IsDefined(member, typeof(ObsoleteAttribute))
+                })
+            );
+
+            engine.SetValue("m", new HiddenMembers());
+
+            Assert.True(engine.Evaluate("m.Field1").IsUndefined());
+            Assert.True(engine.Evaluate("m.Member1").IsUndefined());
+            Assert.True(engine.Evaluate("m.Method1").IsUndefined());
+
+            Assert.True(engine.Evaluate("m.Field2").IsString());
+            Assert.True(engine.Evaluate("m.Member2").IsString());
+            Assert.True(engine.Evaluate("m.Method2()").IsString());
+        }
+
+        [Fact]
+        public void ShouldBeAbleToHideGetType()
+        {
+            var engine = new Engine(options => options
+                .SetTypeResolver(new TypeResolver
+                {
+                    MemberFilter = member => !Attribute.IsDefined(member, typeof(ObsoleteAttribute))
+                })
+            );
+            engine.SetValue("m", new HiddenMembers());
+
+            Assert.True(engine.Evaluate("m.Method1").IsUndefined());
+
+            // reflection could bypass some safeguards
+            Assert.Equal("Method1", engine.Evaluate("m.GetType().GetMethod('Method1').Invoke(m, [])").AsString());
+
+            // but not when we forbid GetType
+            var hiddenGetTypeEngine = new Engine(options => options
+                .SetTypeResolver(new TypeResolver
+                {
+                    MemberFilter = member => member.Name != nameof(GetType)
+                })
+            );
+            hiddenGetTypeEngine.SetValue("m", new HiddenMembers());
+            Assert.True(hiddenGetTypeEngine.Evaluate("m.GetType").IsUndefined());
+        }
+
+        [Fact]
+        public void TypeReferenceShouldUseTypeResolverConfiguration()
+        {
+            var engine = new Engine(options =>
+            {
+                options.SetTypeResolver(new TypeResolver
+                {
+                    MemberFilter = member => !Attribute.IsDefined(member, typeof(ObsoleteAttribute))
+                });
+            });
+            engine.SetValue("EchoService", TypeReference.CreateTypeReference(engine, typeof(EchoService)));
+            Assert.Equal("anyone there", engine.Evaluate("EchoService.Echo('anyone there')").AsString());
+            Assert.Equal("anyone there", engine.Evaluate("EchoService.echo('anyone there')").AsString());
+            Assert.True(engine.Evaluate("EchoService.ECHO").IsUndefined());
+
+            Assert.True(engine.Evaluate("EchoService.Hidden").IsUndefined());
+        }
+
+        private static class EchoService
+        {
+            public static string Echo(string message) => message;
+
+            [Obsolete]
+            public static string Hidden(string message) => message;
+        }
+    }
+}

+ 36 - 44
Jint.Tests/Runtime/InteropTests.cs

@@ -19,7 +19,7 @@ using Xunit;
 
 
 namespace Jint.Tests.Runtime
 namespace Jint.Tests.Runtime
 {
 {
-    public class InteropTests : IDisposable
+    public partial class InteropTests : IDisposable
     {
     {
         private readonly Engine _engine;
         private readonly Engine _engine;
 
 
@@ -2278,49 +2278,6 @@ namespace Jint.Tests.Runtime
             Assert.Equal(jsValue, clrValue);
             Assert.Equal(jsValue, clrValue);
         }
         }
 
 
-        [Fact]
-        public void ShouldHideSpecificMembers()
-        {
-            var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
-            {
-                if (target is HiddenMembers)
-                {
-                    if (member == nameof(HiddenMembers.Member2) || member == nameof(HiddenMembers.Method2))
-                    {
-                        return JsValue.Undefined;
-                    }
-                }
-
-                return null;
-            }));
-
-            engine.SetValue("m", new HiddenMembers());
-
-            Assert.Equal("Member1", engine.Evaluate("m.Member1").ToString());
-            Assert.Equal("undefined", engine.Evaluate("m.Member2").ToString());
-            Assert.Equal("Method1", engine.Evaluate("m.Method1()").ToString());
-            // check the method itself, not its invokation as it would mean invoking "undefined"
-            Assert.Equal("undefined", engine.Evaluate("m.Method2").ToString());
-        }
-
-        [Fact]
-        public void ShouldOverrideMembers()
-        {
-            var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
-            {
-                if (target is HiddenMembers && member == nameof(HiddenMembers.Member1))
-                {
-                    return "Orange";
-                }
-
-                return null;
-            }));
-
-            engine.SetValue("m", new HiddenMembers());
-
-            Assert.Equal("Orange", engine.Evaluate("m.Member1").ToString());
-        }
-
         [Fact]
         [Fact]
         public void SettingValueViaIntegerIndexer()
         public void SettingValueViaIntegerIndexer()
         {
         {
@@ -2591,5 +2548,40 @@ namespace Jint.Tests.Runtime
 
 
             Assert.Equal("Cyclic reference detected.", ex.Message);
             Assert.Equal("Cyclic reference detected.", ex.Message);
         }
         }
+
+        [Fact]
+        public void CanConfigurePropertyNameMatcher()
+        {
+            // defaults
+            var e = new Engine();
+            e.SetValue("a", new A());
+            Assert.True(e.Evaluate("a.call1").IsObject());
+            Assert.True(e.Evaluate("a.Call1").IsObject());
+            Assert.True(e.Evaluate("a.CALL1").IsUndefined());
+
+            e = new Engine(options =>
+            {
+                options.SetTypeResolver(new TypeResolver
+                {
+                    MemberNameComparer = StringComparer.Ordinal
+                });
+            });
+            e.SetValue("a", new A());
+            Assert.True(e.Evaluate("a.call1").IsUndefined());
+            Assert.True(e.Evaluate("a.Call1").IsObject());
+            Assert.True(e.Evaluate("a.CALL1").IsUndefined());
+
+            e = new Engine(options =>
+            {
+                options.SetTypeResolver(new TypeResolver
+                {
+                    MemberNameComparer = StringComparer.OrdinalIgnoreCase
+                });
+            });
+            e.SetValue("a", new A());
+            Assert.True(e.Evaluate("a.call1").IsObject());
+            Assert.True(e.Evaluate("a.Call1").IsObject());
+            Assert.True(e.Evaluate("a.CALL1").IsObject());
+        }
     }
     }
 }
 }

+ 0 - 3
Jint/Engine.cs

@@ -16,7 +16,6 @@ using Jint.Runtime.Debugger;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Interop;
 using Jint.Runtime.Interop;
-using Jint.Runtime.Interop.Reflection;
 using Jint.Runtime.Interpreter;
 using Jint.Runtime.Interpreter;
 using Jint.Runtime.Interpreter.Expressions;
 using Jint.Runtime.Interpreter.Expressions;
 using Jint.Runtime.References;
 using Jint.Runtime.References;
@@ -82,8 +81,6 @@ namespace Jint
         internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerConfigurable;
         internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerConfigurable;
         internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerNonConfigurable;
         internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerNonConfigurable;
 
 
-        internal static Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor> ReflectionAccessors = new();
-
         internal readonly JintCallStack CallStack;
         internal readonly JintCallStack CallStack;
 
 
         // needed in initial engine setup, for example CLR function construction
         // needed in initial engine setup, for example CLR function construction

+ 13 - 0
Jint/Options.cs

@@ -35,6 +35,7 @@ namespace Jint
         private List<Assembly> _lookupAssemblies = new();
         private List<Assembly> _lookupAssemblies = new();
         private Predicate<Exception> _clrExceptionsHandler;
         private Predicate<Exception> _clrExceptionsHandler;
         private IReferenceResolver _referenceResolver = DefaultReferenceResolver.Instance;
         private IReferenceResolver _referenceResolver = DefaultReferenceResolver.Instance;
+        private TypeResolver _typeResolver = TypeResolver.Default;
         private readonly List<Action<Engine>> _configurations = new();
         private readonly List<Action<Engine>> _configurations = new();
 
 
         private readonly List<Type> _extensionMethodClassTypes = new();
         private readonly List<Type> _extensionMethodClassTypes = new();
@@ -180,6 +181,16 @@ namespace Jint
             return this;
             return this;
         }
         }
 
 
+        /// <summary>
+        /// Sets member name comparison strategy when finding CLR objects members.
+        /// By default member's first character casing is ignored and rest of the name is compared with strict equality.
+        /// </summary>
+        public Options SetTypeResolver(TypeResolver resolver)
+        {
+            _typeResolver = resolver;
+            return this;
+        }
+
         /// <summary>
         /// <summary>
         /// Registers a delegate that is called when CLR members are invoked. This allows
         /// Registers a delegate that is called when CLR members are invoked. This allows
         /// to change what values are returned for specific CLR objects, or if any value
         /// to change what values are returned for specific CLR objects, or if any value
@@ -365,7 +376,9 @@ namespace Jint
         internal List<IConstraint> _Constraints => _constraints;
         internal List<IConstraint> _Constraints => _constraints;
 
 
         internal Func<Engine, object, ObjectInstance> _WrapObjectHandler => _wrapObjectHandler;
         internal Func<Engine, object, ObjectInstance> _WrapObjectHandler => _wrapObjectHandler;
+
         internal MemberAccessorDelegate _MemberAccessor => _memberAccessor;
         internal MemberAccessorDelegate _MemberAccessor => _memberAccessor;
+        internal TypeResolver _TypeResolver => _typeResolver;
 
 
         internal int MaxRecursionDepth => _maxRecursionDepth;
         internal int MaxRecursionDepth => _maxRecursionDepth;
 
 

+ 4 - 211
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -1,10 +1,8 @@
 using System;
 using System;
 using System.Collections;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Dynamic;
 using System.Globalization;
 using System.Globalization;
 using System.Reflection;
 using System.Reflection;
-using System.Threading;
 using Jint.Native;
 using Jint.Native;
 using Jint.Native.Object;
 using Jint.Native.Object;
 using Jint.Native.Symbol;
 using Jint.Native.Symbol;
@@ -57,7 +55,7 @@ namespace Jint.Runtime.Interop
                 if (_properties is null || !_properties.ContainsKey(member))
                 if (_properties is null || !_properties.ContainsKey(member))
                 {
                 {
                     // can try utilize fast path
                     // can try utilize fast path
-                    var accessor = GetAccessor(_engine, Target.GetType(), member);
+                    var accessor = _engine.Options._TypeResolver.GetAccessor(_engine, Target.GetType(), member);
 
 
                     // CanPut logic
                     // CanPut logic
                     if (!accessor.Writable || !_engine.Options._IsClrWriteAllowed)
                     if (!accessor.Writable || !_engine.Options._IsClrWriteAllowed)
@@ -117,7 +115,7 @@ namespace Jint.Runtime.Interop
                 if (_properties is null || !_properties.ContainsKey(member))
                 if (_properties is null || !_properties.ContainsKey(member))
                 {
                 {
                     // can try utilize fast path
                     // can try utilize fast path
-                    var accessor = GetAccessor(_engine, Target.GetType(), member);
+                    var accessor = _engine.Options._TypeResolver.GetAccessor(_engine, Target.GetType(), member);
                     var value = accessor.GetValue(_engine, Target);
                     var value = accessor.GetValue(_engine, Target);
                     if (value is not null)
                     if (value is not null)
                     {
                     {
@@ -234,7 +232,7 @@ namespace Jint.Runtime.Interop
                 return new PropertyDescriptor(result, PropertyFlag.OnlyEnumerable);
                 return new PropertyDescriptor(result, PropertyFlag.OnlyEnumerable);
             }
             }
 
 
-            var accessor = GetAccessor(_engine, Target.GetType(), member);
+            var accessor = _engine.Options._TypeResolver.GetAccessor(_engine, Target.GetType(), member);
             var descriptor = accessor.CreatePropertyDescriptor(_engine, Target);
             var descriptor = accessor.CreatePropertyDescriptor(_engine, Target);
             SetProperty(member, descriptor);
             SetProperty(member, descriptor);
             return descriptor;
             return descriptor;
@@ -254,212 +252,7 @@ namespace Jint.Runtime.Interop
                     _ => null
                     _ => null
                 };
                 };
             }
             }
-            return GetAccessor(engine, target.GetType(), member.Name, Factory).CreatePropertyDescriptor(engine, target);
-        }
-
-        private static ReflectionAccessor GetAccessor(Engine engine, Type type, string member, Func<ReflectionAccessor> accessorFactory = null)
-        {
-            var key = new ClrPropertyDescriptorFactoriesKey(type, member);
-
-            var factories = Engine.ReflectionAccessors;
-            if (factories.TryGetValue(key, out var accessor))
-            {
-                return accessor;
-            }
-
-            accessor = accessorFactory?.Invoke() ?? ResolvePropertyDescriptorFactory(engine, type, member);
-
-            // racy, we don't care, worst case we'll catch up later
-            Interlocked.CompareExchange(ref Engine.ReflectionAccessors,
-                new Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor>(factories)
-                {
-                    [key] = accessor
-                }, factories);
-
-            return accessor;
-        }
-
-        private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine, Type type, string memberName)
-        {
-            var isNumber = uint.TryParse(memberName, out _);
-
-            // we can always check indexer if there's one, and then fall back to properties if indexer returns null
-            IndexerAccessor.TryFindIndexer(engine, type, memberName, out var indexerAccessor, out var indexer);
-
-            // properties and fields cannot be numbers
-            if (!isNumber && TryFindStringPropertyAccessor(type, memberName, indexer, out var temp))
-            {
-                return temp;
-            }
-
-            if (typeof(DynamicObject).IsAssignableFrom(type))
-            {
-                return new DynamicObjectAccessor(type, memberName);
-            }
-
-            // if no methods are found check if target implemented indexing
-            if (indexerAccessor != null)
-            {
-                return indexerAccessor;
-            }
-
-            // try to find a single explicit property implementation
-            List<PropertyInfo> list = null;
-            foreach (Type iface in type.GetInterfaces())
-            {
-                foreach (var iprop in iface.GetProperties())
-                {
-                    if (iprop.Name == "Item" && iprop.GetIndexParameters().Length == 1)
-                    {
-                        // never take indexers, should use the actual indexer
-                        continue;
-                    }
-
-                    if (EqualsIgnoreCasing(iprop.Name, memberName))
-                    {
-                        list ??= new List<PropertyInfo>();
-                        list.Add(iprop);
-                    }
-                }
-            }
-
-            if (list?.Count == 1)
-            {
-                return new PropertyAccessor(memberName, list[0]);
-            }
-
-            // try to find explicit method implementations
-            List<MethodInfo> explicitMethods = null;
-            foreach (Type iface in type.GetInterfaces())
-            {
-                foreach (var imethod in iface.GetMethods())
-                {
-                    if (EqualsIgnoreCasing(imethod.Name, memberName))
-                    {
-                        explicitMethods ??= new List<MethodInfo>();
-                        explicitMethods.Add(imethod);
-                    }
-                }
-            }
-
-            if (explicitMethods?.Count > 0)
-            {
-                return new MethodAccessor(MethodDescriptor.Build(explicitMethods));
-            }
-
-            // try to find explicit indexer implementations
-            foreach (var interfaceType in type.GetInterfaces())
-            {
-                if (IndexerAccessor.TryFindIndexer(engine, interfaceType, memberName, out var accessor, out _))
-                {
-                    return accessor;
-                }
-            }
-
-            if (engine.Options._extensionMethods.TryGetExtensionMethods(type, out var extensionMethods))
-            {
-                var matches = new List<MethodInfo>();
-                foreach (var method in extensionMethods)
-                {
-                    if (EqualsIgnoreCasing(method.Name, memberName))
-                    {
-                        matches.Add(method);
-                    }
-                }
-                if (matches.Count > 0)
-                {
-                    return new MethodAccessor(MethodDescriptor.Build(matches));
-                }
-            }
-
-            return ConstantValueAccessor.NullAccessor;
-        }
-
-        private static bool TryFindStringPropertyAccessor(
-            Type type,
-            string memberName,
-            PropertyInfo indexerToTry,
-            out ReflectionAccessor wrapper)
-        {
-            // look for a property, bit be wary of indexers, we don't want indexers which have name "Item" to take precedence
-            PropertyInfo property = null;
-            foreach (var p in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
-            {
-                // only if it's not an indexer, we can do case-ignoring matches
-                var isStandardIndexer = p.GetIndexParameters().Length == 1 && p.Name == "Item";
-                if (!isStandardIndexer && EqualsIgnoreCasing(p.Name, memberName))
-                {
-                    property = p;
-                    break;
-                }
-            }
-
-            if (property != null)
-            {
-                wrapper = new PropertyAccessor(memberName, property, indexerToTry);
-                return true;
-            }
-
-            // look for a field
-            FieldInfo field = null;
-            foreach (var f in type.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
-            {
-                if (EqualsIgnoreCasing(f.Name, memberName))
-                {
-                    field = f;
-                    break;
-                }
-            }
-
-            if (field != null)
-            {
-                wrapper = new FieldAccessor(field, memberName, indexerToTry);
-                return true;
-            }
-
-            // 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, memberName))
-                {
-                    methods ??= new List<MethodInfo>();
-                    methods.Add(m);
-                }
-            }
-
-            if (methods?.Count > 0)
-            {
-                wrapper = new MethodAccessor(MethodDescriptor.Build(methods));
-                return true;
-            }
-
-            wrapper = default;
-            return false;
-        }
-
-        private static bool EqualsIgnoreCasing(string s1, string s2)
-        {
-            if (s1.Length != s2.Length)
-            {
-                return false;
-            }
-
-            var equals = false;
-            if (s1.Length > 0)
-            {
-                equals = char.ToLowerInvariant(s1[0]) == char.ToLowerInvariant(s2[0]);
-            }
-
-            if (@equals && s1.Length > 1)
-            {
-#if NETSTANDARD2_1
-                equals = s1.AsSpan(1).SequenceEqual(s2.AsSpan(1));
-#else
-                equals = s1.Substring(1) == s2.Substring(1);
-#endif
-            }
-            return equals;
+            return engine.Options._TypeResolver.GetAccessor(engine, target.GetType(), member.Name, Factory).CreatePropertyDescriptor(engine, target);
         }
         }
 
 
         public override bool Equals(JsValue obj)
         public override bool Equals(JsValue obj)

+ 8 - 1
Jint/Runtime/Interop/Reflection/IndexerAccessor.cs

@@ -60,8 +60,10 @@ namespace Jint.Runtime.Interop.Reflection
                 return null;
                 return null;
             }
             }
 
 
+            var filter = engine.Options._TypeResolver.MemberFilter;
+
             // default indexer wins
             // default indexer wins
-            if (typeof(IList).IsAssignableFrom(targetType))
+            if (typeof(IList).IsAssignableFrom(targetType) && filter(_iListIndexer))
             {
             {
                 indexerAccessor = ComposeIndexerFactory(_iListIndexer, typeof(int));
                 indexerAccessor = ComposeIndexerFactory(_iListIndexer, typeof(int));
                 if (indexerAccessor != null)
                 if (indexerAccessor != null)
@@ -74,6 +76,11 @@ namespace Jint.Runtime.Interop.Reflection
             // try to find first indexer having either public getter or setter with matching argument type
             // try to find first indexer having either public getter or setter with matching argument type
             foreach (var candidate in targetType.GetProperties())
             foreach (var candidate in targetType.GetProperties())
             {
             {
+                if (!filter(candidate))
+                {
+                    continue;
+                }
+
                 var indexParameters = candidate.GetIndexParameters();
                 var indexParameters = candidate.GetIndexParameters();
                 if (indexParameters.Length != 1)
                 if (indexParameters.Length != 1)
                 {
                 {

+ 13 - 38
Jint/Runtime/Interop/TypeReference.cs

@@ -1,6 +1,5 @@
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
-using System.Collections.Generic;
 using System.Reflection;
 using System.Reflection;
 using Jint.Collections;
 using Jint.Collections;
 using Jint.Native;
 using Jint.Native;
@@ -24,7 +23,7 @@ namespace Jint.Runtime.Interop
         {
         {
         }
         }
 
 
-        public Type ReferenceType { get; set; }
+        public Type ReferenceType { get; private set; }
 
 
         public static TypeReference CreateTypeReference(Engine engine, Type type)
         public static TypeReference CreateTypeReference(Engine engine, Type type)
         {
         {
@@ -137,23 +136,25 @@ namespace Jint.Runtime.Interop
 
 
         private PropertyDescriptor CreatePropertyDescriptor(string name)
         private PropertyDescriptor CreatePropertyDescriptor(string name)
         {
         {
-            var accessor = _memberAccessors.GetOrAdd(
-                new Tuple<Type, string>(ReferenceType, name),
-                key => ResolveMemberAccessor(key.Item1, key.Item2)
-            );
+            var key = new Tuple<Type, string>(ReferenceType, name);
+            var accessor = _memberAccessors.GetOrAdd(key, x => ResolveMemberAccessor(x.Item1, x.Item2));
             return accessor.CreatePropertyDescriptor(_engine, ReferenceType);
             return accessor.CreatePropertyDescriptor(_engine, ReferenceType);
         }
         }
 
 
-        private static ReflectionAccessor ResolveMemberAccessor(Type type, string name)
+        private ReflectionAccessor ResolveMemberAccessor(Type type, string name)
         {
         {
+            var typeResolver = _engine.Options._TypeResolver;
+
             if (type.IsEnum)
             if (type.IsEnum)
             {
             {
+                var memberNameComparer = typeResolver.MemberNameComparer;
+
                 var enumValues = Enum.GetValues(type);
                 var enumValues = Enum.GetValues(type);
                 var enumNames = Enum.GetNames(type);
                 var enumNames = Enum.GetNames(type);
 
 
                 for (var i = 0; i < enumValues.Length; i++)
                 for (var i = 0; i < enumValues.Length; i++)
                 {
                 {
-                    if (enumNames.GetValue(i) as string == name)
+                    if (memberNameComparer.Equals(enumNames.GetValue(i), name))
                     {
                     {
                         var value = enumValues.GetValue(i);
                         var value = enumValues.GetValue(i);
                         return new ConstantValueAccessor(JsNumber.Create(value));
                         return new ConstantValueAccessor(JsNumber.Create(value));
@@ -163,36 +164,10 @@ namespace Jint.Runtime.Interop
                 return ConstantValueAccessor.NullAccessor;
                 return ConstantValueAccessor.NullAccessor;
             }
             }
 
 
-            var propertyInfo = type.GetProperty(name, BindingFlags.Public | BindingFlags.Static);
-            if (propertyInfo != null)
-            {
-                return new PropertyAccessor(name, propertyInfo);
-            }
-
-            var fieldInfo = type.GetField(name, BindingFlags.Public | BindingFlags.Static);
-            if (fieldInfo != null)
-            {
-                return new FieldAccessor(fieldInfo, name);
-            }
-
-            List<MethodInfo> methods = null;
-            foreach (var mi in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
-            {
-                if (mi.Name != name)
-                {
-                    continue;
-                }
-
-                methods ??= new List<MethodInfo>();
-                methods.Add(mi);
-            }
-
-            if (methods == null || methods.Count == 0)
-            {
-                return ConstantValueAccessor.NullAccessor;
-            }
-
-            return new MethodAccessor(MethodDescriptor.Build(methods));
+            const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Static;
+            return typeResolver.TryFindMemberAccessor(type, name, bindingFlags, indexerToTry: null, out var accessor)
+                ? accessor
+                : ConstantValueAccessor.NullAccessor;
         }
         }
 
 
         public object Target => ReferenceType;
         public object Target => ReferenceType;

+ 299 - 0
Jint/Runtime/Interop/TypeResolver.cs

@@ -0,0 +1,299 @@
+using System;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Reflection;
+using System.Threading;
+using Jint.Runtime.Interop.Reflection;
+
+namespace Jint.Runtime.Interop
+{
+    /// <summary>
+    /// Interop strategy for resolving types and members.
+    /// </summary>
+    public sealed class TypeResolver
+    {
+        public static readonly TypeResolver Default = new TypeResolver();
+
+        private Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor> _reflectionAccessors = new();
+
+        /// <summary>
+        /// Registers a filter that determines whether given member is wrapped to interop or returned as undefined.
+        /// </summary>
+        public Predicate<MemberInfo> MemberFilter { get; init; } = _ => true;
+
+        /// <summary>
+        /// Sets member name comparison strategy when finding CLR objects members.
+        /// By default member's first character casing is ignored and rest of the name is compared with strict equality.
+        /// </summary>
+        public StringComparer MemberNameComparer { get; init; } = DefaultMemberNameComparer.Instance;
+
+        internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member, Func<ReflectionAccessor> accessorFactory = null)
+        {
+            var key = new ClrPropertyDescriptorFactoriesKey(type, member);
+
+            var factories = _reflectionAccessors;
+            if (factories.TryGetValue(key, out var accessor))
+            {
+                return accessor;
+            }
+
+            accessor = accessorFactory?.Invoke() ?? ResolvePropertyDescriptorFactory(engine, type, member);
+
+            // racy, we don't care, worst case we'll catch up later
+            Interlocked.CompareExchange(ref _reflectionAccessors,
+                new Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor>(factories)
+                {
+                    [key] = accessor
+                }, factories);
+
+            return accessor;
+        }
+
+        private ReflectionAccessor ResolvePropertyDescriptorFactory(
+            Engine engine,
+            Type type,
+            string memberName)
+        {
+            var isNumber = uint.TryParse(memberName, out _);
+
+            // we can always check indexer if there's one, and then fall back to properties if indexer returns null
+            IndexerAccessor.TryFindIndexer(engine, type, memberName, out var indexerAccessor, out var indexer);
+
+            const BindingFlags bindingFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public;
+
+            // properties and fields cannot be numbers
+            if (!isNumber && TryFindMemberAccessor(type, memberName, bindingFlags, indexer, out var temp))
+            {
+                return temp;
+            }
+
+            if (typeof(DynamicObject).IsAssignableFrom(type))
+            {
+                return new DynamicObjectAccessor(type, memberName);
+            }
+
+            // if no methods are found check if target implemented indexing
+            if (indexerAccessor != null)
+            {
+                return indexerAccessor;
+            }
+
+            // try to find a single explicit property implementation
+            List<PropertyInfo> list = null;
+            var typeResolverMemberNameComparer = MemberNameComparer;
+            foreach (var iface in type.GetInterfaces())
+            {
+                foreach (var iprop in iface.GetProperties())
+                {
+                    if (!MemberFilter(iprop))
+                    {
+                        continue;
+                    }
+
+                    if (iprop.Name == "Item" && iprop.GetIndexParameters().Length == 1)
+                    {
+                        // never take indexers, should use the actual indexer
+                        continue;
+                    }
+
+                    if (typeResolverMemberNameComparer.Equals(iprop.Name, memberName))
+                    {
+                        list ??= new List<PropertyInfo>();
+                        list.Add(iprop);
+                    }
+                }
+            }
+
+            if (list?.Count == 1)
+            {
+                return new PropertyAccessor(memberName, list[0]);
+            }
+
+            // try to find explicit method implementations
+            List<MethodInfo> explicitMethods = null;
+            foreach (var iface in type.GetInterfaces())
+            {
+                foreach (var imethod in iface.GetMethods())
+                {
+                    if (!MemberFilter(imethod))
+                    {
+                        continue;
+                    }
+
+                    if (typeResolverMemberNameComparer.Equals(imethod.Name, memberName))
+                    {
+                        explicitMethods ??= new List<MethodInfo>();
+                        explicitMethods.Add(imethod);
+                    }
+                }
+            }
+
+            if (explicitMethods?.Count > 0)
+            {
+                return new MethodAccessor(MethodDescriptor.Build(explicitMethods));
+            }
+
+            // try to find explicit indexer implementations
+            foreach (var interfaceType in type.GetInterfaces())
+            {
+                if (IndexerAccessor.TryFindIndexer(engine, interfaceType, memberName, out var accessor, out _))
+                {
+                    return accessor;
+                }
+            }
+
+            if (engine.Options._extensionMethods.TryGetExtensionMethods(type, out var extensionMethods))
+            {
+                var matches = new List<MethodInfo>();
+                foreach (var method in extensionMethods)
+                {
+                    if (!MemberFilter(method))
+                    {
+                        continue;
+                    }
+
+                    if (typeResolverMemberNameComparer.Equals(method.Name, memberName))
+                    {
+                        matches.Add(method);
+                    }
+                }
+
+                if (matches.Count > 0)
+                {
+                    return new MethodAccessor(MethodDescriptor.Build(matches));
+                }
+            }
+
+            return ConstantValueAccessor.NullAccessor;
+        }
+
+        internal bool TryFindMemberAccessor(
+            Type type,
+            string memberName,
+            BindingFlags bindingFlags,
+            PropertyInfo indexerToTry,
+            out ReflectionAccessor accessor)
+        {
+            // look for a property, bit be wary of indexers, we don't want indexers which have name "Item" to take precedence
+            PropertyInfo property = null;
+            foreach (var p in type.GetProperties(bindingFlags))
+            {
+                if (!MemberFilter(p))
+                {
+                    continue;
+                }
+
+                // only if it's not an indexer, we can do case-ignoring matches
+                var isStandardIndexer = p.GetIndexParameters().Length == 1 && p.Name == "Item";
+                if (!isStandardIndexer && MemberNameComparer.Equals(p.Name, memberName))
+                {
+                    property = p;
+                    break;
+                }
+            }
+
+            if (property != null)
+            {
+                accessor = new PropertyAccessor(memberName, property, indexerToTry);
+                return true;
+            }
+
+            // look for a field
+            FieldInfo field = null;
+            foreach (var f in type.GetFields(bindingFlags))
+            {
+                if (!MemberFilter(f))
+                {
+                    continue;
+                }
+
+                if (MemberNameComparer.Equals(f.Name, memberName))
+                {
+                    field = f;
+                    break;
+                }
+            }
+
+            if (field != null)
+            {
+                accessor = new FieldAccessor(field, memberName, indexerToTry);
+                return true;
+            }
+
+            // if no properties were found then look for a method
+            List<MethodInfo> methods = null;
+            foreach (var m in type.GetMethods(bindingFlags))
+            {
+                if (!MemberFilter(m))
+                {
+                    continue;
+                }
+
+                if (MemberNameComparer.Equals(m.Name, memberName))
+                {
+                    methods ??= new List<MethodInfo>();
+                    methods.Add(m);
+                }
+            }
+
+            if (methods?.Count > 0)
+            {
+                accessor = new MethodAccessor(MethodDescriptor.Build(methods));
+                return true;
+            }
+
+            accessor = default;
+            return false;
+        }
+
+        private sealed class DefaultMemberNameComparer : StringComparer
+        {
+            public static readonly StringComparer Instance = new DefaultMemberNameComparer();
+
+            public override int Compare(string x, string y)
+            {
+                throw new NotImplementedException();
+            }
+
+            public override bool Equals(string x, string y)
+            {
+                if (ReferenceEquals(x, y))
+                {
+                    return true;
+                }
+
+                if (x == null || y == null)
+                {
+                    return false;
+                }
+
+                if (x.Length != y.Length)
+                {
+                    return false;
+                }
+
+                var equals = false;
+                if (x.Length > 0)
+                {
+                    equals = char.ToLowerInvariant(x[0]) == char.ToLowerInvariant(y[0]);
+                }
+
+                if (equals && x.Length > 1)
+                {
+#if NETSTANDARD2_1
+                    equals = x.AsSpan(1).SequenceEqual(y.AsSpan(1));
+#else
+                    equals = x.Substring(1) == y.Substring(1);
+#endif
+                }
+
+                return equals;
+            }
+
+            public override int GetHashCode(string obj)
+            {
+                throw new NotImplementedException();
+            }
+        }
+    }
+}