diff --git a/Jint.Tests/Jint.Tests.csproj b/Jint.Tests/Jint.Tests.csproj index 7dc532482f..500b5720c9 100644 --- a/Jint.Tests/Jint.Tests.csproj +++ b/Jint.Tests/Jint.Tests.csproj @@ -6,6 +6,7 @@ true false latest + 612 diff --git a/Jint.Tests/Runtime/Domain/HiddenMembers.cs b/Jint.Tests/Runtime/Domain/HiddenMembers.cs index f692d7bc93..2d27dacac6 100644 --- a/Jint.Tests/Runtime/Domain/HiddenMembers.cs +++ b/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 { + [Obsolete] + public string Field1 = "Field1"; + + public string Field2 = "Field2"; + + [Obsolete] public string Member1 { get; set; } = "Member1"; + 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"; } } diff --git a/Jint.Tests/Runtime/InteropTests.MemberAccess.cs b/Jint.Tests/Runtime/InteropTests.MemberAccess.cs new file mode 100644 index 0000000000..ae7f11cbd1 --- /dev/null +++ b/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; + } + } +} \ No newline at end of file diff --git a/Jint.Tests/Runtime/InteropTests.cs b/Jint.Tests/Runtime/InteropTests.cs index addd8a9ee9..5f790eb539 100644 --- a/Jint.Tests/Runtime/InteropTests.cs +++ b/Jint.Tests/Runtime/InteropTests.cs @@ -19,7 +19,7 @@ namespace Jint.Tests.Runtime { - public class InteropTests : IDisposable + public partial class InteropTests : IDisposable { private readonly Engine _engine; @@ -2278,49 +2278,6 @@ public void ShouldBeAbleToJsonStringifyClrObjects() 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] public void SettingValueViaIntegerIndexer() { @@ -2591,5 +2548,40 @@ public void ShouldHandleCyclicReferences() 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()); + } } } diff --git a/Jint/Engine.cs b/Jint/Engine.cs index c274d3e080..126b57d3ec 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -16,7 +16,6 @@ using Jint.Runtime.Descriptors; using Jint.Runtime.Environments; using Jint.Runtime.Interop; -using Jint.Runtime.Interop.Reflection; using Jint.Runtime.Interpreter; using Jint.Runtime.Interpreter.Expressions; using Jint.Runtime.References; @@ -82,8 +81,6 @@ public partial class Engine internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerConfigurable; internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerNonConfigurable; - internal static Dictionary ReflectionAccessors = new(); - internal readonly JintCallStack CallStack; // needed in initial engine setup, for example CLR function construction diff --git a/Jint/Options.cs b/Jint/Options.cs index b49692a8e5..978183f2bd 100644 --- a/Jint/Options.cs +++ b/Jint/Options.cs @@ -35,6 +35,7 @@ public sealed class Options private List _lookupAssemblies = new(); private Predicate _clrExceptionsHandler; private IReferenceResolver _referenceResolver = DefaultReferenceResolver.Instance; + private TypeResolver _typeResolver = TypeResolver.Default; private readonly List> _configurations = new(); private readonly List _extensionMethodClassTypes = new(); @@ -180,6 +181,16 @@ public Options SetTypeConverter(Func typeConverterFactor return this; } + /// + /// 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. + /// + public Options SetTypeResolver(TypeResolver resolver) + { + _typeResolver = resolver; + return this; + } + /// /// 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 @@ -365,7 +376,9 @@ internal void Apply(Engine engine) internal List _Constraints => _constraints; internal Func _WrapObjectHandler => _wrapObjectHandler; + internal MemberAccessorDelegate _MemberAccessor => _memberAccessor; + internal TypeResolver _TypeResolver => _typeResolver; internal int MaxRecursionDepth => _maxRecursionDepth; diff --git a/Jint/Runtime/Interop/ObjectWrapper.cs b/Jint/Runtime/Interop/ObjectWrapper.cs index c2b91e4eb3..f8967e5bf0 100644 --- a/Jint/Runtime/Interop/ObjectWrapper.cs +++ b/Jint/Runtime/Interop/ObjectWrapper.cs @@ -1,10 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Dynamic; using System.Globalization; using System.Reflection; -using System.Threading; using Jint.Native; using Jint.Native.Object; using Jint.Native.Symbol; @@ -57,7 +55,7 @@ public override bool Set(JsValue property, JsValue value, JsValue receiver) if (_properties is null || !_properties.ContainsKey(member)) { // can try utilize fast path - var accessor = GetAccessor(_engine, Target.GetType(), member); + var accessor = _engine.Options._TypeResolver.GetAccessor(_engine, Target.GetType(), member); // CanPut logic if (!accessor.Writable || !_engine.Options._IsClrWriteAllowed) @@ -117,7 +115,7 @@ public override JsValue Get(JsValue property, JsValue receiver) if (_properties is null || !_properties.ContainsKey(member)) { // 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); if (value is not null) { @@ -234,7 +232,7 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) 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); SetProperty(member, descriptor); return descriptor; @@ -254,212 +252,7 @@ ReflectionAccessor Factory() _ => null }; } - return GetAccessor(engine, target.GetType(), member.Name, Factory).CreatePropertyDescriptor(engine, target); - } - - private static ReflectionAccessor GetAccessor(Engine engine, Type type, string member, Func 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(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 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(); - list.Add(iprop); - } - } - } - - if (list?.Count == 1) - { - return new PropertyAccessor(memberName, list[0]); - } - - // try to find explicit method implementations - List explicitMethods = null; - foreach (Type iface in type.GetInterfaces()) - { - foreach (var imethod in iface.GetMethods()) - { - if (EqualsIgnoreCasing(imethod.Name, memberName)) - { - explicitMethods ??= new List(); - 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(); - 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 methods = null; - foreach (var m in type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public)) - { - if (EqualsIgnoreCasing(m.Name, memberName)) - { - methods ??= new List(); - 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) diff --git a/Jint/Runtime/Interop/Reflection/IndexerAccessor.cs b/Jint/Runtime/Interop/Reflection/IndexerAccessor.cs index 9f72781b79..3ff7777562 100644 --- a/Jint/Runtime/Interop/Reflection/IndexerAccessor.cs +++ b/Jint/Runtime/Interop/Reflection/IndexerAccessor.cs @@ -60,8 +60,10 @@ IndexerAccessor ComposeIndexerFactory(PropertyInfo candidate, Type paramType) return null; } + var filter = engine.Options._TypeResolver.MemberFilter; + // default indexer wins - if (typeof(IList).IsAssignableFrom(targetType)) + if (typeof(IList).IsAssignableFrom(targetType) && filter(_iListIndexer)) { indexerAccessor = ComposeIndexerFactory(_iListIndexer, typeof(int)); if (indexerAccessor != null) @@ -74,6 +76,11 @@ IndexerAccessor ComposeIndexerFactory(PropertyInfo candidate, Type paramType) // try to find first indexer having either public getter or setter with matching argument type foreach (var candidate in targetType.GetProperties()) { + if (!filter(candidate)) + { + continue; + } + var indexParameters = candidate.GetIndexParameters(); if (indexParameters.Length != 1) { diff --git a/Jint/Runtime/Interop/TypeReference.cs b/Jint/Runtime/Interop/TypeReference.cs index 425107f401..6141af32cb 100644 --- a/Jint/Runtime/Interop/TypeReference.cs +++ b/Jint/Runtime/Interop/TypeReference.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Reflection; using Jint.Collections; using Jint.Native; @@ -24,7 +23,7 @@ private TypeReference( { } - public Type ReferenceType { get; set; } + public Type ReferenceType { get; private set; } public static TypeReference CreateTypeReference(Engine engine, Type type) { @@ -137,23 +136,25 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) private PropertyDescriptor CreatePropertyDescriptor(string name) { - var accessor = _memberAccessors.GetOrAdd( - new Tuple(ReferenceType, name), - key => ResolveMemberAccessor(key.Item1, key.Item2) - ); + var key = new Tuple(ReferenceType, name); + var accessor = _memberAccessors.GetOrAdd(key, x => ResolveMemberAccessor(x.Item1, x.Item2)); 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) { + var memberNameComparer = typeResolver.MemberNameComparer; + var enumValues = Enum.GetValues(type); var enumNames = Enum.GetNames(type); 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); return new ConstantValueAccessor(JsNumber.Create(value)); @@ -163,36 +164,10 @@ private static ReflectionAccessor ResolveMemberAccessor(Type type, string name) 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 methods = null; - foreach (var mi in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) - { - if (mi.Name != name) - { - continue; - } - - methods ??= new List(); - 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; diff --git a/Jint/Runtime/Interop/TypeResolver.cs b/Jint/Runtime/Interop/TypeResolver.cs new file mode 100644 index 0000000000..8eb2e680d9 --- /dev/null +++ b/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 +{ + /// + /// Interop strategy for resolving types and members. + /// + public sealed class TypeResolver + { + public static readonly TypeResolver Default = new TypeResolver(); + + private Dictionary _reflectionAccessors = new(); + + /// + /// Registers a filter that determines whether given member is wrapped to interop or returned as undefined. + /// + public Predicate MemberFilter { get; init; } = _ => true; + + /// + /// 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. + /// + public StringComparer MemberNameComparer { get; init; } = DefaultMemberNameComparer.Instance; + + internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member, Func 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(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 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(); + list.Add(iprop); + } + } + } + + if (list?.Count == 1) + { + return new PropertyAccessor(memberName, list[0]); + } + + // try to find explicit method implementations + List 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(); + 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(); + 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 methods = null; + foreach (var m in type.GetMethods(bindingFlags)) + { + if (!MemberFilter(m)) + { + continue; + } + + if (MemberNameComparer.Equals(m.Name, memberName)) + { + methods ??= new List(); + 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(); + } + } + } +} \ No newline at end of file