diff --git a/docs/reference/overloaded-methods.md b/docs/reference/overloaded-methods.md index 09a4dc2..aec1659 100644 --- a/docs/reference/overloaded-methods.md +++ b/docs/reference/overloaded-methods.md @@ -6,24 +6,65 @@ function by dynamically checking for different argument counts and/or types. ## Overload resolution -The JS [marshaller](../features/js-dotnet-marshalling) has limited support for overload -resolution. It can examine the count and types of arguments provided by the JavaScript caller and -select the best matching .NET overload accordingly. +The JS [marshaller](../features/js-dotnet-marshalling) has support for overload resolution. It can +examine the count and types of arguments provided by the JavaScript caller and select the best +matching .NET overload accordingly. ```C# [JSExport] public class OverloadsExample { public static void AddValue(string stringValue); - public static void AddValue(double numberValue); + public static void AddValue(int intValue); + public static void AddValue(double doubleValue); } ``` ```JS OverloadsExample.addValue('test'); // Calls AddValue(string) -OverloadsExample.addValue(77); // Calls AddValue(double) +OverloadsExample.addValue(77); // Calls AddValue(int) +OverloadsExample.addValue(0.5); // Calls AddValue(double) ``` -Currently the overload resolution is limited to examining the JavaScript type of each argument -(`string`, `number`, `object`, etc), but that is not sufficient to select between overloads that -differ only in the _type of object_. -[More advanced overload resolution is planned.](https://github.com/microsoft/node-api-dotnet/issues/134) +Overload resolution considers the following information when selecting the best match among method +overloads: + - **Argument count** - Resolution eliminates any overloads that do not accept the number of + arguments that were supplied, taking into account when some .NET method parameters are + optional or have default values. + - **Argument JS value types** - Resolution initially does a quick filter by matching only on + the [JavaScript value type](./dotnet/Microsoft.JavaScript.NodeApi/JSValueType) of each argument, + e.g. JS `string` matches .NET `string`, JS `number` matches any .NET numeric type, + JS `object` matches any .NET class, interface, or struct type. + - **Nullability** - JS `null` or `undefined` arguments match with any method parameters that are + .NET reference types or `Nullable` value types. (Non-nullable reference type annotations are + not considered.) + - **Number argument properties** - If there are multiple overloads with different .NET numeric + types (e.g. `int` and `double`), the properties of the JS number value are used to select the + best overload, including whether it is negative, an integer, or outside the bounds of the .NET + numeric type. + - **Proxied .NET object types** - When a JS argument value is actually + [a proxy to a .NET object](./classes-interfaces.md#marshalling-net-classes-to-js), + then the .NET type is matched to the method parameter type. + - **JS collection types** - When an argument value is a JS collection, the JS collection type + such as `Array` or `Map` is matched to a corresponding .NET collection type. (Generic collection + _element_ types are not considered, since JS collections do not have specific element types.) + - **Other special types** - Types with special marshalling behavior including [dates](./dates), + [guids](./other-types), [Tasks/Promises](./async-promises), and [delegates](./delegates) are + matched accordingly during overload resolution. + +If overload resolution finds multiple matches, or does not find any valid matches, then a +`TypeError` is thrown. + +### Performance considerations + +Unlike compiled languages where the compiler can bind to the appropriate overload at compile time, +with JavaScript the overload resolution process must be repeated at every invocation of the method. +It is not super expensive, but consider avoiding calls to overloaded methods in performance-critical +code. + +### Limitations + +When calling .NET methods from JavaScript, the dynamic overload resolution is not 100% consistent +with C#'s compile-time overload resolution. There are some unavoidable limitations due to the +dynamic-typed nature of JavaScript, and likely some deficienceies in the implementation. While it +should work sufficiently well for the majority of cases, if you find a situation where overload +resolution is not working as expected, please [report a bug](../support). diff --git a/src/NodeApi.DotNetHost/JSMarshaller.cs b/src/NodeApi.DotNetHost/JSMarshaller.cs index 00edec2..2db33de 100644 --- a/src/NodeApi.DotNetHost/JSMarshaller.cs +++ b/src/NodeApi.DotNetHost/JSMarshaller.cs @@ -1262,7 +1262,8 @@ public Expression> BuildConstructorOverloadDescriptor // TODO: Default parameter values Type[] parameterTypes = constructors[i].GetParameters() - .Select(p => p.ParameterType).ToArray(); + .Select(p => EnsureObjectCollectionTypeForOverloadResolution(p.ParameterType)) + .ToArray(); statements[i + 1] = Expression.Assign( Expression.ArrayAccess(overloadsVariable, Expression.Constant(i)), Expression.New( @@ -1353,7 +1354,8 @@ public Expression> BuildMethodOverloadDescriptorExpre // TODO: Default parameter values Type[] parameterTypes = methods[i].GetParameters() - .Select(p => p.ParameterType).ToArray(); + .Select(p => EnsureObjectCollectionTypeForOverloadResolution(p.ParameterType)) + .ToArray(); statements[i + 1] = Expression.Assign( Expression.ArrayAccess(overloadsVariable, Expression.Constant(i)), Expression.New( @@ -1381,6 +1383,77 @@ public Expression> BuildMethodOverloadDescriptorExpre Array.Empty()); } + /// + /// For purposes of overload resolution, convert non-generic collections and collections + /// with specific generic element types to collection interfaces with object element types. + /// This simplifies the checks required during overload resolution and avoids the need + /// for reflection at resolution time, which is not supported in AOT. (The JS collections + /// will still be marshalled to the more specific collection type after resolution when + /// an overload is invoked.) + /// + private static Type EnsureObjectCollectionTypeForOverloadResolution(Type type) + { + if (typeof(System.Collections.IEnumerable).IsAssignableFrom(type) && + !type.IsArray && type != typeof(string)) + { + if (TypeImplementsGenericInterface(type, typeof(IDictionary<,>)) || + TypeImplementsGenericInterface(type, typeof(IReadOnlyDictionary<,>)) || + typeof(System.Collections.IDictionary).IsAssignableFrom(type)) + { + type = typeof(IDictionary); + } + else if (TypeImplementsGenericInterface(type, typeof(IList<>)) || + TypeImplementsGenericInterface(type, typeof(IReadOnlyList<>)) || + typeof(System.Collections.IList).IsAssignableFrom(type)) + { + type = typeof(IList); + } + else if (TypeImplementsGenericInterface(type, typeof(ISet<>)) +#if READONLY_SET + || TypeImplementsGenericInterface(type, typeof(IReadOnlySet<>)) +#endif + ) + { + type = typeof(ISet); + } + else if (TypeImplementsGenericInterface(type, typeof(ICollection<>)) || + TypeImplementsGenericInterface(type, typeof(IReadOnlyCollection<>))) + { + type = typeof(ICollection); + } + else + { + type = typeof(IEnumerable); + } + } + else if (TypeImplementsGenericInterface(type, typeof(IAsyncEnumerable<>))) + { + type = typeof(IAsyncEnumerable); + } + + return type; + } + + private static bool TypeImplementsGenericInterface(Type type, Type genericInterface) + { + if (type.IsInterface && type.IsGenericType && + type.GetGenericTypeDefinition() == genericInterface) + { + return true; + } + + foreach (Type interfaceType in type.GetInterfaces()) + { + if (interfaceType.IsGenericType && + interfaceType.GetGenericTypeDefinition() == genericInterface) + { + return true; + } + } + + return false; + } + /// /// Gets overload information for a set of overloaded methods. /// @@ -2968,23 +3041,31 @@ private IEnumerable BuildFromJSToCollectionInterfaceExpressions( Type elementType = toType.GenericTypeArguments[0]; Type typeDefinition = toType.GetGenericTypeDefinition(); - if (typeDefinition == typeof(IList<>) || - typeDefinition == typeof(ICollection<>) || + if (typeDefinition == typeof(IEnumerable<>) || + typeDefinition == typeof(IAsyncEnumerable<>) || + typeDefinition == typeof(ISet<>) || #if READONLY_SET typeDefinition == typeof(IReadOnlySet<>) || #else // TODO: Support IReadOnlySet on .NET Framework / .NET Standard 2.0. #endif - typeDefinition == typeof(ISet<>)) + typeDefinition == typeof(IList<>) || + typeDefinition == typeof(IReadOnlyList<>)) { /* - * value.TryUnwrap() as ICollection ?? - * ((JSArray)value).AsCollection( - * (value) => (T)value, - * (value) => (JSValue)value); + * value.TryUnwrap() as IList ?? + * ((JSArray)value).AsList((value) => (T)value, (value) => (JSValue)value); */ - Type jsCollectionType = typeDefinition.Name.Contains("Set") ? - typeof(JSSet) : typeof(JSArray); + Type jsCollectionType = + typeDefinition == typeof(IEnumerable<>) ? typeof(JSIterable) : + typeDefinition == typeof(IAsyncEnumerable<>) ? typeof(JSAsyncIterable) : + typeDefinition.Name.Contains("Set") ? typeof(JSSet) : + typeof(JSArray); + bool isBidirectional = (typeDefinition == typeof(IList<>) || +#if READONLY_SET + typeDefinition == typeof(IReadOnlySet<>) || +#endif + typeDefinition == typeof(ISet<>)); MethodInfo asCollectionMethod = typeof(JSCollectionExtensions).GetStaticMethod( #if !STRING_AS_SPAN "As" + typeDefinition.Name.Substring(1, typeDefinition.Name.IndexOf('`') - 1), @@ -2992,47 +3073,82 @@ private IEnumerable BuildFromJSToCollectionInterfaceExpressions( string.Concat("As", typeDefinition.Name.AsSpan(1, typeDefinition.Name.IndexOf('`') - 1)), #endif - new[] { jsCollectionType, typeof(JSValue.To<>), typeof(JSValue.From<>) }, + isBidirectional ? + new[] { jsCollectionType, typeof(JSValue.To<>), typeof(JSValue.From<>) } : + new[] { jsCollectionType, typeof(JSValue.To<>) }, elementType); MethodInfo asJSCollectionMethod = jsCollectionType.GetExplicitConversion( typeof(JSValue), jsCollectionType); + Expression valueAsCollectionExpression = + Expression.Convert(valueExpression, jsCollectionType, asJSCollectionMethod); + Expression fromJSExpression = GetFromJSValueExpression(elementType); + Expression toJSExpression = GetToJSValueExpression(elementType); yield return Expression.Coalesce( Expression.TypeAs(Expression.Call(valueExpression, s_tryUnwrap), toType), - Expression.Call( - asCollectionMethod, - Expression.Convert(valueExpression, jsCollectionType, asJSCollectionMethod), - GetFromJSValueExpression(elementType), - GetToJSValueExpression(elementType))); + isBidirectional ? + Expression.Call( + asCollectionMethod, + valueAsCollectionExpression, + fromJSExpression, + toJSExpression) : + Expression.Call( + asCollectionMethod, + valueAsCollectionExpression, + fromJSExpression)); } - else if (typeDefinition == typeof(IReadOnlyList<>) || - typeDefinition == typeof(IReadOnlyCollection<>) || - typeDefinition == typeof(IEnumerable<>) || - typeDefinition == typeof(IAsyncEnumerable<>)) + else if (typeDefinition == typeof(ICollection<>) || + typeDefinition == typeof(IReadOnlyCollection<>)) { /* - * value.TryUnwrap() as IReadOnlyCollection ?? - * ((JSArray)value).AsReadOnlyCollection((value) => (T)value); + * value.TryUnwrap() as ICollection ?? value.IsArray() ? + * ((JSArray)value).AsCollection((value) => (T)value) : + * ((JSSet)value).AsCollection((value) => (T)value, (value) => (JSValue)value); */ - Type jsCollectionType = typeDefinition == typeof(IEnumerable<>) ? - typeof(JSIterable) : typeDefinition == typeof(IAsyncEnumerable<>) ? - typeof(JSAsyncIterable) : typeof(JSArray); - MethodInfo asCollectionMethod = typeof(JSCollectionExtensions).GetStaticMethod( -#if !STRING_AS_SPAN - "As" + typeDefinition.Name.Substring(1, typeDefinition.Name.IndexOf('`') - 1), -#else - string.Concat("As", - typeDefinition.Name.AsSpan(1, typeDefinition.Name.IndexOf('`') - 1)), -#endif - new[] { jsCollectionType, typeof(JSValue.To<>) }, + MethodInfo isArrayMethod = typeof(JSValue).GetInstanceMethod(nameof(JSValue.IsArray)); + bool isReadOnlyCollection = typeDefinition == typeof(IReadOnlyCollection<>); + string asCollectionMethodName = isReadOnlyCollection ? + nameof(JSCollectionExtensions.AsReadOnlyCollection) : + nameof(JSCollectionExtensions.AsCollection); + MethodInfo arrayAsCollectionMethod = typeof(JSCollectionExtensions).GetStaticMethod( + asCollectionMethodName, + isReadOnlyCollection ? + new[] { typeof(JSArray), typeof(JSValue.To<>) } : + new[] { typeof(JSArray), typeof(JSValue.To<>), typeof(JSValue.From<>) }, elementType); - MethodInfo asJSCollectionMethod = jsCollectionType.GetExplicitConversion( - typeof(JSValue), jsCollectionType); + MethodInfo setAsCollectionMethod = typeof(JSCollectionExtensions).GetStaticMethod( + asCollectionMethodName, + isReadOnlyCollection ? + new[] { typeof(JSSet), typeof(JSValue.To<>) } : + new[] { typeof(JSSet), typeof(JSValue.To<>), typeof(JSValue.From<>) }, + elementType); + MethodInfo asJSArrayMethod = typeof(JSArray).GetExplicitConversion( + typeof(JSValue), typeof(JSArray)); + MethodInfo asJSSetMethod = typeof(JSSet).GetExplicitConversion( + typeof(JSValue), typeof(JSSet)); yield return Expression.Coalesce( Expression.TypeAs(Expression.Call(valueExpression, s_tryUnwrap), toType), - Expression.Call( - asCollectionMethod, - Expression.Convert(valueExpression, jsCollectionType, asJSCollectionMethod), - GetFromJSValueExpression(elementType))); + Expression.Condition( + Expression.Call(valueExpression, isArrayMethod), + isReadOnlyCollection ? + Expression.Call( + arrayAsCollectionMethod, + Expression.Convert(valueExpression, typeof(JSArray), asJSArrayMethod), + GetFromJSValueExpression(elementType)) : + Expression.Call( + arrayAsCollectionMethod, + Expression.Convert(valueExpression, typeof(JSArray), asJSArrayMethod), + GetFromJSValueExpression(elementType), + GetToJSValueExpression(elementType)), + isReadOnlyCollection ? + Expression.Call( + setAsCollectionMethod, + Expression.Convert(valueExpression, typeof(JSSet), asJSSetMethod), + GetFromJSValueExpression(elementType)) : + Expression.Call( + setAsCollectionMethod, + Expression.Convert(valueExpression, typeof(JSSet), asJSSetMethod), + GetFromJSValueExpression(elementType), + GetToJSValueExpression(elementType)))); } else if (typeDefinition == typeof(IDictionary<,>)) { diff --git a/src/NodeApi.Generator/ExpressionExtensions.cs b/src/NodeApi.Generator/ExpressionExtensions.cs index 2e0b655..32d9aaf 100644 --- a/src/NodeApi.Generator/ExpressionExtensions.cs +++ b/src/NodeApi.Generator/ExpressionExtensions.cs @@ -102,9 +102,9 @@ private static string ToCS( ") { " + ToCS(conditional.IfTrue, path, variables) + "; }" + (conditional.IfFalse is DefaultExpression ? string.Empty : " else { " + ToCS(conditional.IfFalse, path, variables) + "; }") - : ToCS(conditional.Test, path, variables) + " ?\n" + + : '(' + ToCS(conditional.Test, path, variables) + " ?\n" + ToCS(conditional.IfTrue, path, variables) + " :\n" + - ToCS(conditional.IfFalse, path, variables), + ToCS(conditional.IfFalse, path, variables) + ')', MemberExpression { NodeType: ExpressionType.MemberAccess } member => member.Expression is ParameterExpression parameterExpression && @@ -375,6 +375,10 @@ internal static string FormatType(Type type) { return "string"; } + else if (type == typeof(object)) + { + return "object"; + } else if (type == typeof(void)) { return "void"; diff --git a/src/NodeApi.Generator/SymbolExtensions.cs b/src/NodeApi.Generator/SymbolExtensions.cs index ee9749a..62ca4b0 100644 --- a/src/NodeApi.Generator/SymbolExtensions.cs +++ b/src/NodeApi.Generator/SymbolExtensions.cs @@ -642,16 +642,14 @@ public static ConstructorInfo AsConstructorInfo(this IMethodSymbol methodSymbol) Type type = methodSymbol.ContainingType.AsType(); - // Ensure constructor parameter types are built. - foreach (IParameterSymbol parameter in methodSymbol.Parameters) - { - parameter.Type.AsType(type.GenericTypeArguments, buildType: true); - } + Type[] parameterTypes = methodSymbol.Parameters + .Select((p) => p.Type.AsType(type.GenericTypeArguments, buildType: true)) + .ToArray(); BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; ConstructorInfo? constructorInfo = type.GetConstructors(bindingFlags) - .FirstOrDefault((c) => c.GetParameters().Select((p) => p.Name).SequenceEqual( - methodSymbol.Parameters.Select((p) => p.Name))); + .FirstOrDefault((c) => c.GetParameters().Select((p) => p.ParameterType.FullName) + .SequenceEqual(parameterTypes.Select((t) => t.FullName))); return constructorInfo ?? throw new InvalidOperationException( $"Constructor not found for type: {type.Name}"); } @@ -674,18 +672,19 @@ public static MethodInfo AsMethodInfo(this IMethodSymbol methodSymbol) #endif typeParameters = typeParameters.Concat(methodTypeParameters).ToArray(); - foreach (IParameterSymbol parameter in methodSymbol.Parameters) - { - parameter.Type.AsType(typeParameters, buildType: true); - } - methodSymbol.ReturnType.AsType(type.GenericTypeArguments, buildType: true); + Type[] parameterTypes = methodSymbol.Parameters + .Select((p) => p.Type.AsType(typeParameters, buildType: true)) + .ToArray(); + Type returnType = methodSymbol.ReturnType.AsType( + type.GenericTypeArguments, buildType: true); BindingFlags bindingFlags = BindingFlags.Public | (methodSymbol.IsStatic ? BindingFlags.Static : BindingFlags.Instance); MethodInfo? methodInfo = type.GetMethods(bindingFlags) .FirstOrDefault((m) => m.Name == methodSymbol.Name && - m.GetParameters().Select((p) => p.Name).SequenceEqual( - methodSymbol.Parameters.Select((p) => p.Name))); + m.GetParameters().Select((p) => p.ParameterType.FullName) + .SequenceEqual(parameterTypes.Select((t) => t.FullName)) && + m.ReturnType.FullName == returnType.FullName); return methodInfo ?? throw new InvalidOperationException( $"Method not found: {type.Name}.{methodSymbol.Name}"); } diff --git a/src/NodeApi/Interop/JSCallbackOverload.cs b/src/NodeApi/Interop/JSCallbackOverload.cs index d64a160..d4d6f8b 100644 --- a/src/NodeApi/Interop/JSCallbackOverload.cs +++ b/src/NodeApi/Interop/JSCallbackOverload.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime; namespace Microsoft.JavaScript.NodeApi.Interop; @@ -100,7 +101,7 @@ private static JSValue ResolveAndInvoke(JSCallbackArgs args) throw new JSException("Missing overload resolution information."); } - JSCallbackOverload overload = Resolve(args, overloads); + JSCallbackOverload overload = Resolve(overloads, args); return Invoke(overload, args); } @@ -121,22 +122,22 @@ private static JSValue ResolveAndInvokeDeferred(JSCallbackArgs args) } JSCallbackOverload[] overloads = deferredOverloads.Value; - JSCallbackOverload overload = Resolve(args, overloads); + JSCallbackOverload overload = Resolve(overloads, args); return Invoke(overload, args); } /// - /// Selects a callback by finding the best match of the supplied arguments to method parameter - /// counts and types. + /// Selects one of multiple callback overloads by finding the best match of the supplied + /// arguments to method parameter counts and types. /// + /// List of overloads to be matched. /// Callback arguments that will be matched against overload /// parameter counts and types. - /// List of overloads to be matched. /// Callback for the resolved overload. /// No overload or multiple overloads were found for the /// supplied arguments. public static JSCallbackOverload Resolve( - JSCallbackArgs args, IReadOnlyList overloads) + IReadOnlyList overloads, JSCallbackArgs args) { // If there's only one overload in the list, no resolution logic is needed. if (overloads.Count == 1) @@ -144,177 +145,662 @@ public static JSCallbackOverload Resolve( return overloads[0]; } - // First try to match the supplied number of arguments to an overload parameter count. - // (Avoid using IEnumerable<> queries to prevent boxing the JSCallbackOverload struct.) int argsCount = args.Length; + + // This array tracks which overloads are still considered a match after each resolve step. + Span isMatch = stackalloc bool[overloads.Count]; + + // First try to match the supplied number of arguments to an overload parameter count. + JSCallbackOverload? matchingOverload = ResolveByArgumentCount( + overloads, argsCount, ref isMatch); + if (matchingOverload != null) + { + return matchingOverload!.Value; + } + + // Multiple matches were found for the supplied number of arguments. + // Next get the JS value type of each arg and try resolve by matching them .NET types. + Span argValueTypes = stackalloc JSValueType[argsCount]; + for (int i = 0; i < argsCount; i++) + { + argValueTypes[i] = args[i].TypeOf(); + } + + matchingOverload = ResolveByArgumentJSValueTypes( + overloads, args, ref argValueTypes, ref isMatch); + if (matchingOverload != null) + { + return matchingOverload!.Value; + } + + // Multiple matches were found for the supplied argument JS value types. + // Next try to resolve an overload by finding the best match of numeric types. + matchingOverload = ResolveByArgumentNumericTypes( + overloads, args, ref argValueTypes, ref isMatch); + if (matchingOverload != null) + { + return matchingOverload!.Value; + } + + // Matching numeric types still did not resolve to a single overload. + // Next try to resolve an overload by finding the best match of object types. + // This will either resolve a single overload or throw an exception. + return ResolveByArgumentObjectTypes(overloads, args, ref argValueTypes, ref isMatch); + } + + private static JSCallbackOverload? ResolveByArgumentCount( + IReadOnlyList overloads, + int argsCount, + ref Span isMatch) + { JSCallbackOverload? matchingOverload = null; - int matchingCallbackCount = 0; - foreach (JSCallbackOverload overload in overloads) + int matchCount = 0; + + for (int overloadIndex = 0; overloadIndex < overloads.Count; overloadIndex++) { - if ((overload.DefaultValues != null && - argsCount >= overload.ParameterTypes.Length - overload.DefaultValues.Length && - argsCount <= overload.ParameterTypes.Length) || - overload.ParameterTypes.Length == argsCount) + JSCallbackOverload overload = overloads[overloadIndex]; + int requiredArgsCount = overload.ParameterTypes.Length - + (overload.DefaultValues?.Length ?? 0); + int requiredAndOptionalArgsCount = overload.ParameterTypes.Length; + + if (argsCount >= requiredArgsCount && argsCount <= requiredAndOptionalArgsCount) { + isMatch[overloadIndex] = true; matchingOverload = overload; - if (++matchingCallbackCount > 1) + matchCount++; + } + } + + if (matchCount == 0) + { + throw new JSException(new JSError( + $"No overload was found for the supplied number of arguments ({argsCount}).", + JSErrorType.TypeError)); + } + + return matchCount == 1 ? matchingOverload : null; + } + + private static JSCallbackOverload? ResolveByArgumentJSValueTypes( + IReadOnlyList overloads, + JSCallbackArgs args, + ref Span argValueTypes, + ref Span isMatch) + { + JSCallbackOverload? matchingOverload = null; + int matchCount = 0; + + for (int overloadIndex = 0; overloadIndex < overloads.Count; overloadIndex++) + { + JSCallbackOverload overload = overloads[overloadIndex]; + + if (!isMatch[overloadIndex]) + { + // Skip overloads already unmatched by argument count. + continue; + } + + bool isMatchByArgTypes = true; + for (int argIndex = 0; argIndex < argValueTypes.Length; argIndex++) + { + Type parameterType = overload.ParameterTypes[argIndex]; + isMatchByArgTypes = parameterType.IsArray ? + argValueTypes[argIndex] == JSValueType.Object && args[argIndex].IsArray() : + IsArgumentJSValueTypeMatch(argValueTypes[argIndex], parameterType); + if (!isMatchByArgTypes) { break; } } + + if (isMatchByArgTypes) + { + matchingOverload = overload; + matchCount++; + } + else + { + isMatch[overloadIndex] = false; + } } - if (matchingCallbackCount == 1) + if (matchCount == 0) { - return matchingOverload!.Value; + string argTypesList = string.Join(", ", argValueTypes.ToArray()); + throw new JSException(new JSError( + $"No overload was found for the supplied argument types ({argTypesList}).", + JSErrorType.TypeError)); } - else if (matchingCallbackCount == 0) + + return matchCount == 1 ? matchingOverload : null; + } + + private static bool IsArgumentJSValueTypeMatch(JSValueType argumentType, Type parameterType) + { + // Note this does not consider nullable type annotations. + bool isNullable = parameterType.IsGenericType && + parameterType.GetGenericTypeDefinition() == typeof(Nullable<>); + if (isNullable) { - throw new JSException( - $"No overload was found for the supplied number of arguments ({argsCount})."); + parameterType = Nullable.GetUnderlyingType(parameterType)!; } - // Multiple matches were found for the supplied number of arguments. - // Get the JS value type of each arg and try to match them .NET types. - Span argTypes = stackalloc JSValueType[argsCount]; - for (int i = 0; i < argsCount; i++) + return argumentType switch { - argTypes[i] = args[i].TypeOf(); - } + JSValueType.Null or JSValueType.Undefined => isNullable || !parameterType.IsValueType, + JSValueType.Boolean => parameterType == typeof(bool), + JSValueType.Number => (parameterType.IsPrimitive && parameterType != typeof(bool)) || + parameterType.IsEnum || parameterType == typeof(TimeSpan), + JSValueType.String => parameterType == typeof(string) || parameterType == typeof(Guid), + JSValueType.Object => !parameterType.IsPrimitive && parameterType != typeof(string), + JSValueType.Function => typeof(Delegate).IsAssignableFrom(parameterType), + JSValueType.BigInt => parameterType == typeof(System.Numerics.BigInteger), + _ => false, + }; + } + + private static JSCallbackOverload? ResolveByArgumentNumericTypes( + IReadOnlyList overloads, + JSCallbackArgs args, + ref Span argValueTypes, + ref Span isMatch) + { + JSCallbackOverload? matchingOverload = null; - matchingOverload = null; - matchingCallbackCount = 0; - foreach (JSCallbackOverload overload in overloads) + JSFunction isIntegerFunction = (JSFunction)JSValue.Global["Number"]["isInteger"]; + JSFunction signFunction = (JSFunction)JSValue.Global["Math"]["sign"]; + + for (int argIndex = 0; argIndex < argValueTypes.Length; argIndex++) { - if ((overload.DefaultValues != null && - argsCount >= overload.ParameterTypes.Length - overload.DefaultValues.Length && - argsCount <= overload.ParameterTypes.Length) || - overload.ParameterTypes.Length == argsCount) + if (argValueTypes[argIndex] != JSValueType.Number) { - bool isMatch = true; - for (int i = 0; i < argsCount; i++) + // Skip arguments that are not JS numbers. + continue; + } + + // These properties will be evaluated (once) only if needed. + bool? isInteger = null; + bool? isLongInteger = null; + + // All overload parameters at this index are already confirmed to be some .NET numeric + // type when matching by JS value type. Try to choose one overload that is the best + // numeric type match to the supplied JS number. + + int matchCount = 0; + Type? matchingNumericType = null; + for (int overloadIndex = 0; overloadIndex < overloads.Count; overloadIndex++) + { + if (!isMatch[overloadIndex]) { - Type parameterType = overload.ParameterTypes[i]; - isMatch = parameterType.IsArray ? - argTypes[i] == JSValueType.Object && args[i].IsArray() : - IsArgumentTypeMatch(argTypes[i], overload.ParameterTypes[i]); - if (!isMatch) - { - break; - } + // Skip overloads already unmatched by argument count or JS value types. + continue; } - if (isMatch) + Type parameterType = overloads[overloadIndex].ParameterTypes[argIndex]; + if (parameterType.IsEnum) + { + parameterType = Enum.GetUnderlyingType(parameterType); + } + else if (parameterType == typeof(TimeSpan)) { - matchingOverload = overload; - if (++matchingCallbackCount > 1) + parameterType = typeof(long); + } + else if (parameterType.IsGenericType && + parameterType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + parameterType = Nullable.GetUnderlyingType(parameterType)!; + } + + int specificity = CompareNumericTypeSpecificity(matchingNumericType, parameterType); + if (specificity == 0) + { + // Multiple overloads have the same numeric type in this parameter index. + matchCount++; + } + else if (specificity == 1) + { + if (IsArgumentNumericTypeMatch( + args[argIndex], + parameterType, + isIntegerFunction, + signFunction, + ref isInteger, + ref isLongInteger)) { - break; + // Reset the match count because a more specific numeric type was found. + matchCount = 1; + matchingOverload = overloads[overloadIndex]; + matchingNumericType = parameterType; + } + else + { + isMatch[overloadIndex] = false; } } } + + if (matchingOverload == null) + { + // The numeric type arg could not be matched, e.g. a non-integer argument was + // provided for an integer param, or a signed argument for an unsigned param. + throw new JSException(new JSError( + "No overload was found for the supplied numeric argument " + + $"at position {argIndex}.", + JSErrorType.TypeError)); + } + else if (matchCount == 1) + { + return matchingOverload; + } } - if (matchingCallbackCount == 1) + return null; + } + + private static int CompareNumericTypeSpecificity(Type? currentType, Type newType) + { + if (currentType == null) { - return matchingOverload!.Value; + return 1; + } + else if (currentType == newType) + { + return 0; } - string argTypesList = string.Join(", ", argTypes.ToArray()); - if (matchingCallbackCount == 0) + // Integer types are more specific than floating-point types. + // Smaller integer types are more specific than larger integer types. + // For types of the same size, unsigned types are more specific than signed. + bool isCurrentTypeIntegral = IsIntegralType(currentType); + bool isNewTypeIntegral = IsIntegralType(newType); + if (isCurrentTypeIntegral && !isNewTypeIntegral) { - throw new JSException( - $"No overload was found for the supplied argument types ({argTypesList})."); + return -1; + } + else if (isCurrentTypeIntegral) + { + bool isCurrentTypeUnsigned = IsUnsignedIntegralType(currentType); + bool isNewTypeUnsigned = IsUnsignedIntegralType(newType); + if (isCurrentTypeUnsigned && !isNewTypeUnsigned) + { + return -1; + } + else if (!isCurrentTypeUnsigned && isNewTypeUnsigned) + { + return 1; + } + else + { + // Both types are signed or both are unsigned. + return SizeOfNumericType(currentType).CompareTo(SizeOfNumericType(newType)); + } + } + else if (isNewTypeIntegral) + { + return 1; } else { - // TODO: Try to match types more precisely, potentially using some additional type - // metadata supplied with JS arguments. + // Both types are floating point. + // Unlike with integer types, the larger floating-point type is considered + // more specific, to reduce the potential for loss of precision. + return SizeOfNumericType(newType).CompareTo(SizeOfNumericType(currentType)); + } + } + + private static bool IsArgumentNumericTypeMatch( + JSValue arg, + Type parameterType, + JSFunction isIntegerFunction, + JSFunction signFunction, + ref bool? isInteger, + ref bool? isNegativeInteger) + { + if (IsIntegralType(parameterType)) + { + isInteger ??= (bool)isIntegerFunction.CallAsStatic(arg); + if (!isInteger.Value) + { + return false; + } + + if (IsUnsignedIntegralType(parameterType)) + { + isNegativeInteger ??= (int)signFunction.CallAsStatic(arg) < 0; + if (isNegativeInteger.Value) + { + return false; + } - throw new JSException( - $"Multiple overloads were found for the supplied argument types ({argTypesList})."); + ulong integerValue = (ulong)arg; + return parameterType switch + { + Type t when t == typeof(byte) => integerValue <= byte.MaxValue, + Type t when t == typeof(ushort) => integerValue <= ushort.MaxValue, + Type t when t == typeof(uint) => integerValue <= uint.MaxValue, + _ => true, // No range check for nuint / ulong. + }; + } + else + { + long integerValue = (long)arg; + return parameterType switch + { + Type t when t == typeof(sbyte) => + integerValue >= sbyte.MinValue && integerValue <= sbyte.MaxValue, + Type t when t == typeof(short) => + integerValue >= short.MinValue && integerValue <= short.MaxValue, + Type t when t == typeof(int) => + integerValue >= int.MinValue && integerValue <= int.MaxValue, + _ => true, // No range check for nint / long. + }; + } } + + // Any JS number value can match .NET float or double parameter type. + return true; } - private static bool IsArgumentTypeMatch(JSValueType argumentType, Type parameterType) + private static bool IsIntegralType(Type type) { - static bool IsNullable(Type type) => !type.IsValueType || - (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); + return type switch + { + Type t when t == typeof(sbyte) => true, + Type t when t == typeof(byte) => true, + Type t when t == typeof(short) => true, + Type t when t == typeof(ushort) => true, + Type t when t == typeof(int) => true, + Type t when t == typeof(uint) => true, + Type t when t == typeof(nint) => true, + Type t when t == typeof(nuint) => true, + Type t when t == typeof(long) => true, + Type t when t == typeof(ulong) => true, + _ => false, + }; + } - return argumentType switch + private static bool IsUnsignedIntegralType(Type type) + { + return type switch { - JSValueType.Boolean => parameterType == typeof(bool), - JSValueType.Number => parameterType.IsPrimitive && parameterType != typeof(bool), - JSValueType.String => parameterType == typeof(string), - JSValueType.Null => IsNullable(parameterType), - JSValueType.Undefined => IsNullable(parameterType), - JSValueType.Object => !parameterType.IsPrimitive && parameterType != typeof(string), - JSValueType.Function => parameterType.BaseType == typeof(Delegate), - JSValueType.BigInt => parameterType == typeof(System.Numerics.BigInteger), + Type t when t == typeof(byte) => true, + Type t when t == typeof(ushort) => true, + Type t when t == typeof(uint) => true, + Type t when t == typeof(nuint) => true, + Type t when t == typeof(ulong) => true, _ => false, }; } - private static JSValue GetDefaultArg(Type parameterType, object? defaultValue) + private static int SizeOfNumericType(Type type) { - if (defaultValue == null) + return type switch { - // JS undefined will convert to null for reference types or default for value types. - return default; + Type t when t == typeof(sbyte) => sizeof(sbyte), + Type t when t == typeof(byte) => sizeof(byte), + Type t when t == typeof(short) => sizeof(short), + Type t when t == typeof(ushort) => sizeof(ushort), + Type t when t == typeof(int) => sizeof(int), + Type t when t == typeof(uint) => sizeof(uint), + Type t when t == typeof(long) => sizeof(long), + Type t when t == typeof(ulong) => sizeof(ulong), + Type t when t == typeof(float) => sizeof(float), + Type t when t == typeof(double) => sizeof(double), + + // The returned sizes are only used for specificity comparison purposes. + // For nint/nuint, return a size that is between the size of int and long. + Type t when t == typeof(nint) => sizeof(int) + sizeof(long) / 2, + Type t when t == typeof(nuint) => sizeof(uint) + sizeof(ulong) / 2, + + _ => throw new NotSupportedException( + "Numeric type not supported for overload resolution: " + type.Name), + }; + } + + private static JSCallbackOverload ResolveByArgumentObjectTypes( + IReadOnlyList overloads, + JSCallbackArgs args, + ref Span argValueTypes, + ref Span isMatch) + { + JSCallbackOverload? matchingOverload = null; + + for (int argIndex = 0; argIndex < argValueTypes.Length; argIndex++) + { + if (argValueTypes[argIndex] != JSValueType.Object) + { + // Skip arguments that are not JS objects. + continue; + } + + int matchCount = 0; + Type? matchedParameterType = null; + + JSValue arg = args[argIndex]; + object? obj = arg.TryUnwrap(); + Type? dotnetType = null; + if (obj != null) + { + dotnetType = obj.GetType(); + } + + // The JS type will be evaluated (once) only if needed. + JSValue? jsType = null; + + for (int overloadIndex = 0; overloadIndex < overloads.Count; overloadIndex++) + { + if (!isMatch[overloadIndex]) + { + // Skip overloads already unmatched for other reasons. + continue; + } + + Type parameterType = overloads[overloadIndex].ParameterTypes[argIndex]; + if (IsArgumentObjectTypeMatch(arg, parameterType, dotnetType, ref jsType)) + { + int specificity = CompareObjectTypeSpecificity( + matchedParameterType, parameterType); + if (specificity == 0) + { + // Either the types are the same or neither type is assignable to the other. + // This can result in ambiguity in overload resolution unless the overload + // is disambiguated by other parameters. + matchCount++; + } + else if (specificity > 0) + { + // Prefer a more specific type match when selecting an overload. + matchCount = 1; + matchingOverload = overloads[overloadIndex]; + matchedParameterType = parameterType; + } + } + else + { + // The object parameter type does not match. Skip this overload when + // evaluating the remaining parameters. + isMatch[overloadIndex] = false; + } + } + + if (matchCount == 0) + { + throw new JSException(new JSError( + "No overload was found for the supplied object argument " + + $"at position {argIndex}.", JSErrorType.TypeError)); + } + else if (matchCount == 1) + { + return matchingOverload!.Value; + } } - else if (parameterType == typeof(string)) + + throw new JSException(new JSError( + "Multiple overloads were found for the supplied argument types.", + JSErrorType.TypeError)); + } + + private static int CompareObjectTypeSpecificity(Type? currentType, Type newType) + { + if (currentType == null) { - return (JSValue)(string)defaultValue!; + return 1; } - else if (parameterType == typeof(bool)) + else if (newType == currentType) { - return (JSValue)(bool)defaultValue!; + return 0; } - else if (parameterType == typeof(sbyte)) + else if (currentType.IsArray && newType == typeof(IList)) { - return (JSValue)(sbyte)defaultValue!; + // IList<> is preferred over arrays because it supports marshal-by-reference. + return 1; } - else if (parameterType == typeof(byte)) + else if (currentType == typeof(IList) && newType.IsArray) { - return (JSValue)(byte)defaultValue!; + return -1; } - else if (parameterType == typeof(short)) + else if (currentType.IsAssignableFrom(newType) || + (currentType == typeof(IEnumerable) && + newType == typeof(IDictionary))) { - return (JSValue)(short)defaultValue!; + // IDictionary<> is a special case because the type matching for overload resolution + // converts interfaces to object element types, which makes the IsAssignableFrom check + // fail because IDictionary does not implement IEnumerable. + return 1; } - else if (parameterType == typeof(ushort)) + else if (newType.IsAssignableFrom(currentType)) { - return (JSValue)(ushort)defaultValue!; + return -1; } - else if (parameterType == typeof(int)) + else { - return (JSValue)(int)defaultValue!; + // Neither type is assignable to the other. This can result in ambiguity in + // overload resolution unless the overload is disambiguated by other parameters. + return 0; } - else if (parameterType == typeof(uint)) + } + + private static bool IsArgumentObjectTypeMatch( + JSValue arg, + Type parameterType, + Type? dotnetType, + ref JSValue? jsType + ) + { + if (dotnetType != null) { - return (JSValue)(uint)defaultValue!; + return parameterType.IsAssignableFrom(dotnetType); } - else if (parameterType == typeof(long)) + else if (parameterType.IsValueType && + !parameterType.IsPrimitive && !parameterType.IsEnum) // struct type { - return (JSValue)(long)defaultValue!; + jsType ??= arg["constructor"]; + + if ((parameterType == typeof(DateTime) || + parameterType == typeof(DateTimeOffset)) && + jsType == JSValue.Global["Date"]) + { + if (arg.HasProperty("offset")) + { + return parameterType == typeof(DateTimeOffset); + } + else if (arg.HasProperty("kind")) + { + return parameterType == typeof(DateTime); + } + else + { + return true; + } + } + else if (jsType == JSValue.Global["Object"]) + { + // TODO: Check for required (non-nullable) properties in the JS object? + // For now, assume any plain JS object can be marshalled as a .NET struct. + return true; + } + else + { + // For structs, the JS object does not directly wrap a .NET object, + // but the JS object's constructor may still wrap the .NET type. + dotnetType = jsType?.TryUnwrap() as Type; + return parameterType == dotnetType; + } + } + else if (parameterType == typeof(IEnumerable)) + { + // This only checks for IEnumerable (and not other type parameters) because + // supported collection parameter types have been converted to generic interfaces + // with object element types for the purposes of overload resolution by + // JSMarshaller.EnsureObjectCollectionTypeForOverloadResolution(). + + return arg.HasProperty(JSSymbol.Iterator); + } + else if (parameterType == typeof(IAsyncEnumerable)) + { + return arg.HasProperty(JSSymbol.AsyncIterator); } - else if (parameterType == typeof(ulong)) + else if (parameterType == typeof(ICollection)) { - return (JSValue)(ulong)defaultValue!; + // Either a JS array or a JS Set object can match this parameter type. + if (arg.IsArray()) + { + return true; + } + else + { + jsType ??= arg["constructor"]; + return jsType == JSValue.Global["Set"]; + } } - else if (parameterType == typeof(float)) + else if (parameterType == typeof(IList) || parameterType.IsArray) { - return (JSValue)(float)defaultValue!; + return arg.IsArray(); } - else if (parameterType == typeof(double)) + else if (parameterType == typeof(ISet)) { - return (JSValue)(double)defaultValue!; + jsType ??= arg["constructor"]; + return jsType == JSValue.Global["Set"]; } - else + else if (parameterType == typeof(IDictionary)) + { + jsType ??= arg["constructor"]; + return jsType == JSValue.Global["Map"]; + } + else if (parameterType == typeof(Task) || (parameterType.IsGenericType && + parameterType.GetGenericTypeDefinition() == typeof(Task<>))) { - throw new NotSupportedException( - "Default parameter type not supported: " + parameterType); + return arg.IsPromise(); } + + return false; + } + + private static JSValue GetDefaultArg(Type parameterType, object? defaultValue) + { + if (defaultValue == null) + { + // JS undefined will convert to null for reference types or default for value types. + return default; + } + + return parameterType switch + { + Type t when t == typeof(string) => (JSValue)(string)defaultValue, + Type t when t == typeof(bool) => (JSValue)(bool)defaultValue, + Type t when t == typeof(sbyte) => (JSValue)(sbyte)defaultValue, + Type t when t == typeof(byte) => (JSValue)(byte)defaultValue, + Type t when t == typeof(short) => (JSValue)(short)defaultValue, + Type t when t == typeof(ushort) => (JSValue)(ushort)defaultValue, + Type t when t == typeof(int) => (JSValue)(int)defaultValue, + Type t when t == typeof(uint) => (JSValue)(uint)defaultValue, + Type t when t == typeof(long) || t == typeof(nint) => (JSValue)(long)defaultValue, + Type t when t == typeof(ulong) || t == typeof(nuint) => (JSValue)(ulong)defaultValue, + Type t when t == typeof(float) => (JSValue)(float)defaultValue, + Type t when t == typeof(double) => (JSValue)(double)defaultValue, + _ => throw new NotSupportedException( + "Default parameter type not supported: " + parameterType.Name), + }; } private static JSValue Invoke(JSCallbackOverload overload, JSCallbackArgs args) diff --git a/src/NodeApi/NodeApi.csproj b/src/NodeApi/NodeApi.csproj index 9e184d2..97fec4c 100644 --- a/src/NodeApi/NodeApi.csproj +++ b/src/NodeApi/NodeApi.csproj @@ -18,6 +18,11 @@ true + + + true + + diff --git a/test/TestCases/napi-dotnet/Overloads.cs b/test/TestCases/napi-dotnet/Overloads.cs index 50e8b6e..814dee1 100644 --- a/test/TestCases/napi-dotnet/Overloads.cs +++ b/test/TestCases/napi-dotnet/Overloads.cs @@ -1,6 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Threading.Tasks; + namespace Microsoft.JavaScript.NodeApi.TestCases; [JSExport] @@ -62,4 +67,83 @@ public void SetValue(double doubleValue) { IntValue = (int)doubleValue; } + + public static string NumericMethod(int value) => $"{value}: int"; + public static string NumericMethod(long value) => $"{value}: long"; + public static string NumericMethod(float value) => $"{value}: float"; + public static string NumericMethod(double value) => $"{value}: double"; + public static string NumericMethod2(string value1, int value2) + => $"{value1}: string, {value2}: int"; + public static string NumericMethod2(string value1, double value2) + => $"{value1}: string, {value2}: double"; + + public static string NumericMethod3(byte value) => $"{value}: byte"; + public static string NumericMethod3(sbyte value) => $"{value}: sbyte"; + public static string NumericMethod3(ushort value) => $"{value}: ushort"; + public static string NumericMethod3(short value) => $"{value}: short"; + public static string NumericMethod3(uint value) => $"{value}: uint"; + public static string NumericMethod3(int value) => $"{value}: int"; + public static string NumericMethod3(ulong value) => $"{value}: ulong"; + public static string NumericMethod3(long value) => $"{value}: long"; + + public static string ClassMethod(ClassObject value) => $"{value.Value}: ClassObject"; + public static string ClassMethod(BaseClass value) => $"{value.Value1}: BaseClass"; + public static string ClassMethod(SubClass value) => $"{value.Value2}: SubClass"; + public static string ClassMethod(StructObject value) => $"{value.Value}: StructObject"; + + public static string InterfaceMethod(IBaseInterface value) + => $"{value.Value1}: IBaseInterface"; + public static string InterfaceMethod(ISubInterface value) + => $"{value.Value2}: ISubInterface"; + + public static string CollectionMethod1(int[] value) + => $"[{string.Join(", ", value)}]: int[]"; + public static string CollectionMethod1(ICollection value) + => $"[{string.Join(", ", value)}]: ICollection"; + public static string CollectionMethod1(IList value) + => $"[{string.Join(", ", value)}]: IList"; + public static string CollectionMethod1(ISet value) + => $"[{string.Join(", ", value)}]: ISet"; + public static string CollectionMethod1(IDictionary value) + => $"[{string.Join(", ", value)}]: IDictionary"; + + public static string CollectionMethod2(IEnumerable value) + => $"[{string.Join(", ", value)}]: IEnumerable"; + public static string CollectionMethod2(IReadOnlyCollection value) + => $"[{string.Join(", ", value)}]: IReadOnlyCollection"; + public static string CollectionMethod2(IReadOnlyList value) + => $"[{string.Join(", ", value)}]: IReadOnlyList"; + public static string CollectionMethod2(IReadOnlyDictionary value) + => $"[{string.Join(", ", value)}]: IReadOnlyDictionary"; + + public static string CollectionMethod3(IEnumerable value) + => $"[{string.Join(", ", value)}]: IEnumerable"; + public static string CollectionMethod3(ICollection value) + => $"[{string.Join(", ", value)}]: ICollection"; + + public static Task CollectionMethod4(IEnumerable value) + => Task.FromResult($"[{string.Join(", ", value)}]: IEnumerable"); + public static async Task CollectionMethod4(IAsyncEnumerable value) + { + List list = new(); + await foreach (var item in value) + { + list.Add(item); + } + return $"[{string.Join(", ", list)}]: IAsyncEnumerable"; + } + + public static string DateTimeMethod(DateTime value) => $"{value:s}: DateTime"; + public static string DateTimeMethod(DateTimeOffset value) => $"{value:s}: DateTimeOffset"; + public static string DateTimeMethod(TimeSpan value) => $"{value}: TimeSpan"; + + public static string OtherMethod(TestEnum value) => $"{value}: TestEnum"; + public static string OtherMethod(Guid value) => $"{value}: Guid"; + public static string OtherMethod(BigInteger value) => $"{value}: BigInteger"; + public static string OtherMethod(Task value) => $"Task"; + public static string OtherMethod(TestDelegate value) => $"{value("test")}: TestDelegate"; + + public static string NullableNumericMethod(int? value) => + $"{(value == null ? "null" : value.ToString())}: int?"; + public static string NullableNumericMethod(double value) => $"{value}: double"; } diff --git a/test/TestCases/napi-dotnet/overloads.js b/test/TestCases/napi-dotnet/overloads.js index 72d185c..c922fa3 100644 --- a/test/TestCases/napi-dotnet/overloads.js +++ b/test/TestCases/napi-dotnet/overloads.js @@ -8,6 +8,10 @@ const binding = require('../common').binding; const Overloads = binding.Overloads; const ClassObject = binding.ClassObject; +const BaseClass = binding.BaseClass; +const SubClass = binding.SubClass; +const StructObject = binding.StructObject; +const TestEnum = binding.TestEnum; // Overloaded constructor const emptyObj = new Overloads(); @@ -31,7 +35,7 @@ objValue.value = 'test'; const objFromClass = new Overloads(objValue); assert.strictEqual(objFromClass.stringValue, 'test'); -// Overloaded method +// Overloaded method with basic resolution by parameter count and JS type. const obj1 = new Overloads(); obj1.setValue(1); assert.strictEqual(obj1.intValue, 1); @@ -51,7 +55,105 @@ const obj4 = new Overloads(); obj4.setValue(objValue); assert.strictEqual(obj4.stringValue, 'test'); +// Overloaded C# method with explicit JS method name. const obj5 = new Overloads(); obj5.setDoubleValue(5.0); assert.strictEqual(obj5.intValue, 5); +// Overloaded method with resolution by matching numeric type. +assert.strictEqual(Overloads.numericMethod(1), '1: int'); +assert.strictEqual(Overloads.numericMethod(10000000000), '10000000000: long'); +assert.strictEqual(Overloads.numericMethod(1.11), '1.11: double'); +assert.strictEqual(Overloads.numericMethod2('test', 2), 'test: string, 2: int'); +assert.strictEqual(Overloads.numericMethod2('test', 2.22), 'test: string, 2.22: double'); + +// Overloaded method with resolution by selecting best numeric type specificity. +assert.strictEqual(Overloads.numericMethod3(1), '1: byte'); +assert.strictEqual(Overloads.numericMethod3(-1), '-1: sbyte'); +assert.strictEqual(Overloads.numericMethod3(1000), '1000: ushort'); +assert.strictEqual(Overloads.numericMethod3(-1000), '-1000: short'); +assert.strictEqual(Overloads.numericMethod3(1000000), '1000000: uint'); +assert.strictEqual(Overloads.numericMethod3(-1000000), '-1000000: int'); +assert.strictEqual(Overloads.numericMethod3(10000000000), '10000000000: ulong'); +assert.strictEqual(Overloads.numericMethod3(-10000000000), '-10000000000: long'); + +// Overloaded method with resolution by matching object type. +assert.strictEqual(Overloads.classMethod(new ClassObject('class')), 'class: ClassObject'); +assert.strictEqual(Overloads.classMethod(new BaseClass(1)), '1: BaseClass'); +assert.strictEqual(Overloads.classMethod(new SubClass(1, 2)), '2: SubClass'); +assert.strictEqual(Overloads.classMethod({ value: 'struct' }), 'struct: StructObject'); +assert.strictEqual(Overloads.classMethod(new StructObject('struct2')), 'struct2: StructObject'); + +// Overloaded method with resolution by matching interface type. +assert.strictEqual(Overloads.interfaceMethod(new BaseClass(1)), '1: IBaseInterface'); +assert.strictEqual(Overloads.interfaceMethod(new SubClass(1, 2)), '2: ISubInterface'); + +// Overloaded method with resolution by matching collection type. +assert.strictEqual(Overloads.collectionMethod1([1, 2, 3]), '[1, 2, 3]: IList'); +assert.strictEqual(Overloads.collectionMethod1(new Set([1, 2, 3])), '[1, 2, 3]: ISet'); +assert.strictEqual(Overloads.collectionMethod1( + new Map([[1, 10], [2, 20], [3, 30]])), '[[1, 10], [2, 20], [3, 30]]: IDictionary'); + +// Overloaded method with resolution by matching read-only collection type. +assert.strictEqual(Overloads.collectionMethod2( + [1, 2, 3]), '[1, 2, 3]: IReadOnlyList'); +assert.strictEqual(Overloads.collectionMethod2( + new Set([1, 2, 3])), '[1, 2, 3]: IReadOnlyCollection'); +assert.strictEqual(Overloads.collectionMethod2( + new Map([[1, 10], [2, 20], [3, 30]])), + '[[1, 10], [2, 20], [3, 30]]: IReadOnlyDictionary'); + +// Overloaded method with resolution by matching iterable or collection type. +const testIterable = { + [Symbol.iterator]: function* () { + yield 1; + yield 2; + yield 3; + } +}; +const testAsyncIterable = { + [Symbol.asyncIterator]: async function* () { + yield 1; + yield 2; + yield 3; + } +}; +assert.strictEqual(Overloads.collectionMethod3( + testIterable), '[1, 2, 3]: IEnumerable'); +assert.strictEqual(Overloads.collectionMethod3( + [1, 2, 3]), '[1, 2, 3]: ICollection'); +assert.strictEqual(Overloads.collectionMethod3( + new Set([1, 2, 3])), '[1, 2, 3]: ICollection'); +Overloads.collectionMethod4([1, 2, 3]).then( + (result) => assert.strictEqual(result, '[1, 2, 3]: IEnumerable')); +Overloads.collectionMethod4(testIterable).then( + (result) => assert.strictEqual(result, '[1, 2, 3]: IEnumerable')); +Overloads.collectionMethod4(testAsyncIterable).then( + (result) => assert.strictEqual(result, '[1, 2, 3]: IAsyncEnumerable')); + +// The following types have special marshalling behaviors: +// DateTime(Offset), TimeSpan, Enums, Guid, BigInteger, Task, Delegate + +const dateWithKind = new Date(Date.UTC(2024, 1, 29)); +dateWithKind.kind = 'utc'; +assert.strictEqual(Overloads.dateTimeMethod(dateWithKind), '2024-02-29T00:00:00: DateTime'); +const dateWithOffset = new Date(Date.UTC(2024, 1, 29)); +dateWithOffset.offset = -10 * 60; +assert.strictEqual(Overloads.dateTimeMethod(dateWithOffset), '2024-02-28T14:00:00: DateTimeOffset'); +assert.strictEqual(Overloads.dateTimeMethod(11 * 60 * 1000), '00:11:00: TimeSpan'); + +assert.strictEqual(Overloads.otherMethod(TestEnum.One), 'One: TestEnum'); +assert.strictEqual(Overloads.otherMethod('00000000-0000-0000-0000-000000000000'), + '00000000-0000-0000-0000-000000000000: Guid'); +assert.strictEqual(Overloads.otherMethod(1000000000000000000000000n), + '1000000000000000000000000: BigInteger'); +assert.strictEqual(Overloads.otherMethod(BigInt('1000000000000000000000000')), + '1000000000000000000000000: BigInteger'); +assert.strictEqual(Overloads.otherMethod(Promise.resolve()), 'Task'); +assert.strictEqual(Overloads.otherMethod((value) => value.toUpperCase()), 'TEST: TestDelegate'); + +// Overloaded method with resolution of null / undefined parameters. +assert.strictEqual(Overloads.nullableNumericMethod(null), 'null: int?'); +assert.strictEqual(Overloads.nullableNumericMethod(undefined), 'null: int?'); +assert.strictEqual(Overloads.nullableNumericMethod(3), '3: int?'); +assert.strictEqual(Overloads.nullableNumericMethod(4.4), '4.4: double');