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; set; } = _ => 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; set; } = 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._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();
}
}
}
}