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