using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using Jint.Extensions; using Jint.Native; namespace Jint.Runtime.Interop; #pragma warning disable IL2072 internal sealed class InteropHelper { internal const DynamicallyAccessedMemberTypes DefaultDynamicallyAccessedMemberTypes = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicEvents; internal readonly record struct AssignableResult(int Score, Type MatchingGivenType) { public bool IsAssignable => Score >= 0; } /// /// resources: /// https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-examine-and-instantiate-generic-types-with-reflection /// https://stackoverflow.com/questions/74616/how-to-detect-if-type-is-another-generic-type/1075059#1075059 /// https://docs.microsoft.com/en-us/dotnet/api/system.type.isconstructedgenerictype?view=net-6.0 /// This can be improved upon - specifically as mentioned in the above MS document: /// GetGenericParameterConstraints() /// and array handling - i.e. /// GetElementType() /// internal static AssignableResult IsAssignableToGenericType( [DynamicallyAccessedMembers(DefaultDynamicallyAccessedMemberTypes | DynamicallyAccessedMemberTypes.Interfaces)] Type givenType, [DynamicallyAccessedMembers(DefaultDynamicallyAccessedMemberTypes | DynamicallyAccessedMemberTypes.Interfaces)] Type genericType) { if (givenType is null) { return new AssignableResult(-1, typeof(void)); } if (!genericType.IsConstructedGenericType) { // as mentioned here: // https://docs.microsoft.com/en-us/dotnet/api/system.type.isconstructedgenerictype?view=net-6.0 // this effectively means this generic type is open (i.e. not closed) - so any type is "possible" - without looking at the code in the method we don't know // whether any operations are being applied that "don't work" return new AssignableResult(2, givenType); } var interfaceTypes = givenType.GetInterfaces(); foreach (var it in interfaceTypes) { if (it.IsGenericType) { var givenTypeGenericDef = it.GetGenericTypeDefinition(); if (givenTypeGenericDef == genericType) { return new AssignableResult(0, it); } else if (genericType.IsGenericType && (givenTypeGenericDef == genericType.GetGenericTypeDefinition())) { return new AssignableResult(0, it); } // TPC: we could also add a loop to recurse and iterate thru the iterfaces of generic type - because of covariance/contravariance } } if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) { return new AssignableResult(0, givenType); } var baseType = givenType.BaseType; if (baseType == null) { return new AssignableResult(-1, givenType); } return IsAssignableToGenericType(baseType, genericType); } /// /// Determines how well parameter type matches target method's type. /// private static int CalculateMethodParameterScore(Engine engine, ParameterInfo parameter, JsValue parameterValue) { var paramType = parameter.ParameterType; var objectValue = parameterValue.ToObject(); var objectValueType = objectValue?.GetType(); if (objectValueType == paramType) { return 0; } if (objectValue is null) { if (!parameter.IsOptional && !TypeIsNullable(paramType)) { // this is bad return -1; } return 0; } if (paramType == typeof(JsValue)) { // JsValue is convertible to. But it is still not a perfect match return 1; } if (paramType == typeof(object)) { // a catch-all, prefer others over it return 5; } const int ScoreForDifferentTypeButFittingNumberRange = 2; if (parameterValue.IsNumber()) { var num = (JsNumber) parameterValue; var numValue = num._value; if (paramType == typeof(double)) { return 0; } if (paramType == typeof(float) && numValue is <= float.MaxValue and >= float.MinValue) { return ScoreForDifferentTypeButFittingNumberRange; } var isInteger = num.IsInteger() || TypeConverter.IsIntegralNumber(num._value); // if value is integral number and within allowed range for the parameter type, we consider this perfect match if (isInteger) { if (paramType == typeof(int)) { return 0; } if (paramType == typeof(long)) { return ScoreForDifferentTypeButFittingNumberRange; } // check if we can narrow without exception throwing versions (CanChangeType) var integerValue = (int) num._value; if (paramType == typeof(short) && integerValue is <= short.MaxValue and >= short.MinValue) { return ScoreForDifferentTypeButFittingNumberRange; } if (paramType == typeof(ushort) && integerValue is <= ushort.MaxValue and >= ushort.MinValue) { return ScoreForDifferentTypeButFittingNumberRange; } if (paramType == typeof(byte) && integerValue is <= byte.MaxValue and >= byte.MinValue) { return ScoreForDifferentTypeButFittingNumberRange; } if (paramType == typeof(sbyte) && integerValue is <= sbyte.MaxValue and >= sbyte.MinValue) { return ScoreForDifferentTypeButFittingNumberRange; } } } if (paramType.IsEnum && parameterValue is JsNumber jsNumber && jsNumber.IsInteger() && paramType.GetEnumUnderlyingType() == typeof(int) && Enum.IsDefined(paramType, jsNumber.AsInteger())) { // we can do conversion from int value to enum return 0; } if (paramType.IsAssignableFrom(objectValueType)) { // is-a-relation return 1; } if (parameterValue.IsArray() && paramType.IsArray) { // we have potential, TODO if we'd know JS array's internal type we could have exact match return 2; } // not sure the best point to start generic type tests if (paramType.IsGenericParameter) { var genericTypeAssignmentScore = IsAssignableToGenericType(objectValueType!, paramType); if (genericTypeAssignmentScore.Score != -1) { return genericTypeAssignmentScore.Score; } } if (CanChangeType(objectValue, paramType)) { // forcing conversion isn't ideal, but works, especially for int -> double for example return 3; } foreach (var m in objectValueType!.GetOperatorOverloadMethods()) { if (paramType.IsAssignableFrom(m.ReturnType) && m.Name is "op_Implicit" or "op_Explicit") { // implicit/explicit operator conversion is OK, but not ideal return 3; } } if (ReflectionExtensions.TryConvertViaTypeCoercion(paramType, engine.Options.Interop.ValueCoercion, parameterValue, out _)) { // gray JS zone where we start to do odd things return 10; } // will rarely succeed return 100; } /// /// Method's match score tells how far away it's from ideal candidate. 0 = ideal, bigger the the number, /// the farther away the candidate is from ideal match. Negative signals impossible match. /// private static int CalculateMethodScore(Engine engine, MethodDescriptor method, JsCallArguments arguments) { if (method.Parameters.Length == 0 && arguments.Length == 0) { // perfect return 0; } var score = 0; for (var i = 0; i < arguments.Length; i++) { var jsValue = arguments[i]; var parameterScore = CalculateMethodParameterScore(engine, method.Parameters[i], jsValue); if (parameterScore < 0) { return parameterScore; } score += parameterScore; } return score; } private static bool CanChangeType(object value, Type targetType) { if (value is null && !targetType.IsValueType) { return true; } if (value is not IConvertible) { return false; } try { Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture); return true; } catch { // nope return false; } } internal static bool TypeIsNullable(Type type) { return !type.IsValueType || Nullable.GetUnderlyingType(type) != null; } internal readonly record struct MethodMatch(MethodDescriptor Method, JsCallArguments Arguments, int Score = 0) : IComparable { public int CompareTo(MethodMatch other) => Score.CompareTo(other.Score); } internal static IEnumerable FindBestMatch( Engine engine, MethodDescriptor[] methods, Func argumentProvider, TState state) { List? matchingByParameterCount = null; foreach (var method in methods) { var parameterInfos = method.Parameters; var arguments = argumentProvider(method, state); if (arguments.Length <= parameterInfos.Length && arguments.Length >= parameterInfos.Length - method.ParameterDefaultValuesCount) { var score = CalculateMethodScore(engine, method, arguments); if (score == 0) { // perfect match yield return new MethodMatch(method, arguments); yield break; } if (score < 0) { // discard continue; } matchingByParameterCount ??= []; matchingByParameterCount.Add(new MethodMatch(method, arguments, score)); } } if (matchingByParameterCount == null) { yield break; } if (matchingByParameterCount.Count > 1) { matchingByParameterCount.Sort(); } foreach (var match in matchingByParameterCount) { yield return match; } } }