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(); } } }