using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.Globalization;
using System.Reflection;
using System.Threading;
using Jint.Runtime.Interop.Reflection;
#pragma warning disable IL2067
#pragma warning disable IL2070
#pragma warning disable IL2072
#pragma warning disable IL2075
namespace Jint.Runtime.Interop;
///
/// Interop strategy for resolving types and members.
///
public sealed class TypeResolver
{
public static readonly TypeResolver Default = new();
///
/// Registers a filter that determines whether given member is wrapped to interop or returned as undefined.
/// By default allows all but will also be limited by configuration.
///
///
public Predicate MemberFilter { get; set; } = static _ => true;
internal bool Filter(Engine engine, Type targetType, MemberInfo m)
{
// some specific problematic indexer cases for JSON interop
if (string.Equals(m.Name, "Item", StringComparison.Ordinal) && m is PropertyInfo p)
{
var indexParameters = p.GetIndexParameters();
if (indexParameters.Length == 1)
{
var parameter = indexParameters[0];
if (string.Equals(m.DeclaringType?.FullName, "System.Text.Json.Nodes.JsonNode", StringComparison.Ordinal))
{
// STJ
return parameter.ParameterType == typeof(string) && string.Equals(targetType.FullName, "System.Text.Json.Nodes.JsonObject", StringComparison.Ordinal)
|| parameter.ParameterType == typeof(int) && string.Equals(targetType.FullName, "System.Text.Json.Nodes.JsonArray", StringComparison.Ordinal);
}
if (string.Equals(targetType.FullName, "Newtonsoft.Json.Linq.JArray", StringComparison.Ordinal))
{
// NJ
return parameter.ParameterType == typeof(int);
}
}
}
return (engine.Options.Interop.AllowGetType || !string.Equals(m.Name, nameof(GetType), StringComparison.Ordinal)) && MemberFilter(m);
}
///
/// Gives the exposed names for a member. Allows to expose C# convention following member like IsSelected
/// as more JS idiomatic "selected" for example. Defaults to returning the as-is.
///
public Func> MemberNameCreator { get; set; } = NameCreator;
private static IEnumerable NameCreator(MemberInfo info)
{
yield return info.Name;
}
///
/// 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,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.Interfaces)] Type type,
string member,
bool mustBeReadable,
bool mustBeWritable,
Func? accessorFactory = null)
{
var key = new Engine.ClrPropertyDescriptorFactoriesKey(type, member);
var factories = engine._reflectionAccessors;
if (factories.TryGetValue(key, out var accessor))
{
return accessor;
}
accessor = accessorFactory?.Invoke() ?? ResolvePropertyDescriptorFactory(engine, type, member, mustBeReadable, mustBeWritable);
// don't cache if numeric indexer
if (uint.TryParse(member, out _))
{
return accessor;
}
// 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 ReflectionAccessor ResolvePropertyDescriptorFactory(
Engine engine,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.Interfaces)] Type type,
string memberName,
bool mustBeReadable,
bool mustBeWritable)
{
var isInteger = long.TryParse(memberName, NumberStyles.Integer, CultureInfo.InvariantCulture, 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 (!isInteger
&& TryFindMemberAccessor(engine, type, memberName, bindingFlags: null, indexer, out var temp)
&& (!mustBeReadable || temp.Readable)
&& (!mustBeWritable || temp.Writable))
{
return temp;
}
if (typeof(DynamicObject).IsAssignableFrom(type))
{
return new DynamicObjectAccessor();
}
var typeResolverMemberNameComparer = MemberNameComparer;
var typeResolverMemberNameCreator = MemberNameCreator;
if (!isInteger)
{
// try to find a single explicit property implementation
List? list = null;
foreach (var iface in type.GetInterfaces())
{
foreach (var iprop in iface.GetProperties())
{
if (!Filter(engine, type, iprop))
{
continue;
}
if (string.Equals(iprop.Name, "Item", StringComparison.Ordinal) && iprop.GetIndexParameters().Length == 1)
{
// never take indexers, should use the actual indexer
continue;
}
foreach (var name in typeResolverMemberNameCreator(iprop))
{
if (typeResolverMemberNameComparer.Equals(name, memberName))
{
list ??= new List();
list.Add(iprop);
}
}
}
}
if (list?.Count == 1)
{
return new PropertyAccessor(list[0]);
}
// try to find explicit method implementations
List? explicitMethods = null;
foreach (var iface in type.GetInterfaces())
{
foreach (var imethod in iface.GetMethods())
{
if (!Filter(engine, type, imethod))
{
continue;
}
foreach (var name in typeResolverMemberNameCreator(imethod))
{
if (typeResolverMemberNameComparer.Equals(name, memberName))
{
explicitMethods ??= new List();
explicitMethods.Add(imethod);
}
}
}
}
if (explicitMethods?.Count > 0)
{
return new MethodAccessor(type, MethodDescriptor.Build(explicitMethods));
}
}
// if no methods are found check if target implemented indexing
var score = int.MaxValue;
if (indexerAccessor != null)
{
var parameter = indexerAccessor.FirstIndexParameter;
score = CalculateIndexerScore(parameter, isInteger);
}
if (score != 0)
{
// try to find explicit indexer implementations that has a better score than earlier
foreach (var interfaceType in type.GetInterfaces())
{
if (IndexerAccessor.TryFindIndexer(engine, interfaceType, memberName, out var accessor, out _))
{
// ensure that original type is allowed against indexer
if (!Filter(engine, type, accessor.Indexer))
{
continue;
}
var parameter = accessor.FirstIndexParameter;
var newScore = CalculateIndexerScore(parameter, isInteger);
if (newScore < score)
{
// found a better one
indexerAccessor = accessor;
score = newScore;
}
}
}
}
// use the best indexer we were able to find
if (indexerAccessor != null)
{
return indexerAccessor;
}
if (!isInteger && engine._extensionMethods.TryGetExtensionMethods(type, out var extensionMethods))
{
var matches = new List();
foreach (var method in extensionMethods)
{
if (!Filter(engine, type, method))
{
continue;
}
foreach (var name in typeResolverMemberNameCreator(method))
{
if (typeResolverMemberNameComparer.Equals(name, memberName))
{
matches.Add(method);
}
}
}
if (matches.Count > 0)
{
return new MethodAccessor(type, MethodDescriptor.Build(matches));
}
}
if (engine.Options.Interop.ThrowOnUnresolvedMember)
{
throw new MissingMemberException($"Cannot access property '{memberName}' on type '{type.FullName}");
}
return ConstantValueAccessor.NullAccessor;
}
private static int CalculateIndexerScore(ParameterInfo parameter, bool isInteger)
{
var paramType = parameter.ParameterType;
if (paramType == typeof(int))
{
return isInteger ? 0 : 10;
}
if (paramType == typeof(string))
{
return 1;
}
return 5;
}
internal bool TryFindMemberAccessor(
Engine engine,
[DynamicallyAccessedMembers(InteropHelper.DefaultDynamicallyAccessedMemberTypes | DynamicallyAccessedMemberTypes.Interfaces)] Type type,
string memberName,
BindingFlags? bindingFlags,
PropertyInfo? indexerToTry,
[NotNullWhen(true)] 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;
var memberNameComparer = MemberNameComparer;
var typeResolverMemberNameCreator = MemberNameCreator;
PropertyInfo? GetProperty([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type t)
{
foreach (var p in t.GetProperties(bindingFlags ?? engine.Options.Interop.ObjectWrapperReportedPropertyBindingFlags))
{
if (!Filter(engine, type, p))
{
continue;
}
// only if it's not an indexer, we can do case-ignoring matches
var isStandardIndexer = string.Equals(p.Name, "Item", StringComparison.Ordinal) && p.GetIndexParameters().Length == 1;
if (!isStandardIndexer)
{
foreach (var name in typeResolverMemberNameCreator(p))
{
if (memberNameComparer.Equals(name, memberName))
{
// If one property hides another (e.g., by public new), the derived property is returned.
if (property is not null
&& p.DeclaringType is not null
&& property.DeclaringType is not null
&& property.DeclaringType.IsSubclassOf(p.DeclaringType))
{
continue;
}
property = p;
break;
}
}
}
}
return property;
}
property = GetProperty(type);
if (property is null && type.IsInterface)
{
// check inherited interfaces
foreach (var iface in type.GetInterfaces())
{
property = GetProperty(iface);
if (property is not null)
{
break;
}
}
}
if (property is not null)
{
accessor = new PropertyAccessor(property, indexerToTry);
return true;
}
// look for a field
FieldInfo? field = null;
foreach (var f in type.GetFields(bindingFlags ?? engine.Options.Interop.ObjectWrapperReportedFieldBindingFlags))
{
if (!Filter(engine, type, f))
{
continue;
}
foreach (var name in typeResolverMemberNameCreator(f))
{
if (memberNameComparer.Equals(name, memberName))
{
field = f;
break;
}
}
}
if (field is not null)
{
accessor = new FieldAccessor(field, indexerToTry);
return true;
}
// if no properties were found then look for a method
List? methods = null;
void AddMethod(MethodInfo m)
{
if (!Filter(engine, type, m))
{
return;
}
foreach (var name in typeResolverMemberNameCreator(m))
{
if (memberNameComparer.Equals(name, memberName))
{
methods ??= new List();
methods.Add(m);
}
}
}
foreach (var m in type.GetMethods(bindingFlags ?? engine.Options.Interop.ObjectWrapperReportedMethodBindingFlags))
{
AddMethod(m);
}
foreach (var iface in type.GetInterfaces())
{
foreach (var m in iface.GetMethods())
{
AddMethod(m);
}
}
// TPC: need to grab the extension methods here - for overloads
if (engine._extensionMethods.TryGetExtensionMethods(type, out var extensionMethods))
{
foreach (var methodInfo in extensionMethods)
{
AddMethod(methodInfo);
}
}
// Add Object methods to interface
if (type.IsInterface)
{
foreach (var m in typeof(object).GetMethods(bindingFlags ?? engine.Options.Interop.ObjectWrapperReportedMethodBindingFlags))
{
AddMethod(m);
}
}
if (methods?.Count > 0)
{
accessor = new MethodAccessor(type, MethodDescriptor.Build(methods));
return true;
}
// look for nested type
var nestedType = type.GetNestedType(memberName, bindingFlags ?? BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static);
if (nestedType != null)
{
var typeReference = TypeReference.CreateTypeReference(engine, nestedType);
accessor = new NestedTypeAccessor(typeReference);
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 SUPPORTS_SPAN_PARSE
equals = x.AsSpan(1).SequenceEqual(y.AsSpan(1));
#else
equals = string.Equals(x.Substring(1), y.Substring(1), StringComparison.Ordinal);
#endif
}
return equals;
}
public override int GetHashCode(string obj)
{
throw new NotImplementedException();
}
}
}