Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions src/NSubstitute/Core/Arguments/ArgumentSpecificationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private IArgumentSpecification CreateSpecFromNonParamsArg(object? argument, IPar

private IArgumentSpecification CreateSpecFromParamsArg(object? argument, IParameterInfo parameterInfo, ISuppliedArgumentSpecifications suppliedArgumentSpecifications)
{
// Next specification is for the whole params array.
// Next specification is for the whole params argument.
if (suppliedArgumentSpecifications.IsNextFor(argument, parameterInfo.ParameterType))
{
return suppliedArgumentSpecifications.Dequeue();
Expand All @@ -43,21 +43,16 @@ private IArgumentSpecification CreateSpecFromParamsArg(object? argument, IParame
throw new AmbiguousArgumentsException();
}

// User passed "null" as the params array value.
// User passed "null" as the params value.
if (argument == null)
{
return new ArgumentSpecification(parameterInfo.ParameterType, new EqualsArgumentMatcher(null));
}

// User specified arguments using the native params syntax.
var arrayArg = argument as Array;
if (arrayArg == null)
{
throw new SubstituteInternalException($"Expected to get array argument, but got argument of '{argument.GetType().FullName}' type.");
}

var arrayArgumentSpecifications = UnwrapParamsArguments(arrayArg.Cast<object?>(), parameterInfo.ParameterType.GetElementType()!, suppliedArgumentSpecifications);
return new ArgumentSpecification(parameterInfo.ParameterType, new ArrayContentsArgumentMatcher(arrayArgumentSpecifications));
var elementType = ParamsSupport.GetElementType(parameterInfo.ParameterType);
var argumentValueSpecifications = UnwrapParamsArguments(ParamsSupport.UnwrapArgument(argument), elementType, suppliedArgumentSpecifications);
return new ArgumentSpecification(parameterInfo.ParameterType, new ParamsContentsArgumentMatcher(argumentValueSpecifications));
}

private IEnumerable<IArgumentSpecification> UnwrapParamsArguments(IEnumerable<object?> args, Type paramsElementType, ISuppliedArgumentSpecifications suppliedArgumentSpecifications)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace NSubstitute.Core.Arguments;

[Obsolete("Use ParamsContentsArgumentMatcher instead. This api will be removed in future versions of product.")]
public class ArrayContentsArgumentMatcher(IEnumerable<IArgumentSpecification> argumentSpecifications) : IArgumentMatcher, IArgumentFormatter
{
private readonly IArgumentSpecification[] _argumentSpecifications = argumentSpecifications.ToArray();
Expand Down Expand Up @@ -42,4 +43,4 @@ private IEnumerable<string> Format(object[] args, IArgumentSpecification[] specs
return hasSpecForThisArg ? specs[index].FormatArgument(arg) : ArgumentFormatter.Default.Format(arg, true);
});
}
}
}
44 changes: 44 additions & 0 deletions src/NSubstitute/Core/Arguments/ParamsContentsArgumentMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace NSubstitute.Core.Arguments;

public class ParamsContentsArgumentMatcher(IEnumerable<IArgumentSpecification> argumentSpecifications) : IArgumentMatcher, IArgumentFormatter
{
private readonly IArgumentSpecification[] _argumentSpecifications = argumentSpecifications.ToArray();

public bool IsSatisfiedBy(object? argument)
{
if (argument != null)
{
var argumentValues = ParamsSupport.UnwrapArgument(argument).ToArray();
if (argumentValues.Length == _argumentSpecifications.Length)
{
return _argumentSpecifications
.Select((spec, index) => spec.IsSatisfiedBy(argumentValues[index]))
.All(x => x);
}
}

return false;
}

public override string ToString() => string.Join(", ", _argumentSpecifications.Select(x => x.ToString()));

public string Format(object? argument, bool highlight)
{
ParamsSupport.TryUnwrapArgument(argument, out var argumentSequence);
var argumentValues = argumentSequence.ToArray();
return Format(argumentValues, _argumentSpecifications).Join(", ");
}

private IEnumerable<string> Format(object?[] argumentValues, IArgumentSpecification[] specs)
{
if (specs.Any() && !argumentValues.Any())
{
return new[] { "**" };
}
return argumentValues.Select((arg, index) =>
{
var hasSpecForThisArg = index < specs.Length;
return hasSpecForThisArg ? specs[index].FormatArgument(arg) : ArgumentFormatter.Default.Format(arg, true);
});
}
}
83 changes: 83 additions & 0 deletions src/NSubstitute/Core/Arguments/ParamsSupport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using NSubstitute.Exceptions;

namespace NSubstitute.Core.Arguments;

internal static class ParamsSupport
{
public static bool IsParams(ParameterInfo parameterInfo)
{
const string paramCollectionAttributeFullName = "System.Runtime.CompilerServices.ParamCollectionAttribute";

return parameterInfo.IsDefined(typeof(ParamArrayAttribute), inherit: false)
// Needed because attribute is available in .NET 9+ only
|| parameterInfo.GetCustomAttributesData().Any(x => x.AttributeType.FullName == paramCollectionAttributeFullName);
}

public static Type GetElementType(Type paramsParameterType)
{
if (paramsParameterType.IsArray)
{
return paramsParameterType.GetElementType()!;
}

if (TryGetEnumerableElementType(paramsParameterType, out var elementType))
{
return elementType;
}

// The parameter type implements only the non-generic IEnumerable (e.g. ArrayList, or a custom
// collection-initializer-pattern type without IEnumerable<T>). There's no "T" to read off an
// interface in that case - the compiler itself falls back to whatever single-argument Add the
// type exposes, so mirror that here.
if (TryGetAddMethodParameterType(paramsParameterType, out elementType))
{
return elementType;
}

throw new SubstituteInternalException($"Could not determine params element type for parameter of type '{paramsParameterType.FullName}'.");

static bool TryGetEnumerableElementType(Type type, [NotNullWhen(true)] out Type? elementType)
{
var enumerableOfT = type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)
? type
: type.GetInterfaces().FirstOrDefault(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));

elementType = enumerableOfT?.GetGenericArguments()[0];
return elementType != null;
}

static bool TryGetAddMethodParameterType(Type type, [NotNullWhen(true)] out Type? elementType)
{
var addMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(m => m.Name == "Add" && m.GetParameters().Length == 1);

elementType = addMethod?.GetParameters()[0].ParameterType;
return elementType != null;
}
}

public static IEnumerable<object?> UnwrapArgument(object argument)
{
if (TryUnwrapArgument(argument, out var result))
{
return result;
}

throw new SubstituteInternalException($"Expected to get collection argument, but got argument of '{argument.GetType().FullName}' type.");
Comment thread
dtchepak marked this conversation as resolved.
}

public static bool TryUnwrapArgument(object? argument, out IEnumerable<object?> result)
{
if (argument is IEnumerable enumerable)
{
result = enumerable.Cast<object?>();
return true;
}

result = [];
return false;
}
}
3 changes: 2 additions & 1 deletion src/NSubstitute/Core/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using NSubstitute.Core.Arguments;

namespace NSubstitute.Core;

Expand All @@ -25,7 +26,7 @@ public static class ReflectionExtensions

public static bool IsParams(this ParameterInfo parameterInfo)
{
return parameterInfo.IsDefined(typeof(ParamArrayAttribute), inherit: false);
return ParamsSupport.IsParams(parameterInfo);
}

private static bool CanBePropertySetterCall(MethodInfo call)
Expand Down
3 changes: 1 addition & 2 deletions src/NSubstitute/Core/SequenceChecking/SequenceFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Collections;
using System.Reflection;
using NSubstitute.Core.Arguments;

Expand Down Expand Up @@ -113,7 +112,7 @@ private IEnumerable<string> FormatArgs(ArgAndParamInfo[] arguments)
var argsWithParamsExpanded =
arguments
.SelectMany(a => a.ParamInfo.IsParams()
? ((IEnumerable)a.Argument!).Cast<object>()
? ParamsSupport.UnwrapArgument(a.Argument!)
: ToEnumerable(a.Argument))
.Select(x => ArgumentFormatter.Default.Format(x, false))
.ToArray();
Expand Down
15 changes: 7 additions & 8 deletions src/NSubstitute/Exceptions/AmbiguousArgumentsException.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Collections;
using System.Reflection;
using System.Text;
using NSubstitute.Core;
Expand Down Expand Up @@ -44,14 +43,14 @@ private static string BuildExceptionMessage(MethodInfo method,
string? matchedSpecificationsInfo = null;
if (CallFormatter.Default.CanFormat(method))
{
var argsWithInlinedParamsArray = invocationArguments.ToArray();
var argsWithInlinedParamsCollection = invocationArguments.ToArray();
// If last argument is `params`, we inline the value.
if (method.GetParameters().Last().IsParams()
&& argsWithInlinedParamsArray.Last() is IEnumerable paramsArray)
&& argsWithInlinedParamsCollection.Last() is { } paramsArgument)
{
argsWithInlinedParamsArray = argsWithInlinedParamsArray
.Take(argsWithInlinedParamsArray.Length - 1)
.Concat(paramsArray.Cast<object>())
argsWithInlinedParamsCollection = argsWithInlinedParamsCollection
.Take(argsWithInlinedParamsCollection.Length - 1)
.Concat(ParamsSupport.UnwrapArgument(paramsArgument))
.ToArray();
}

Expand All @@ -61,11 +60,11 @@ private static string BuildExceptionMessage(MethodInfo method,

methodArgsWithHighlightedPossibleArgSpecs = CallFormatter.Default.Format(
method,
FormatMethodArguments(argsWithInlinedParamsArray));
FormatMethodArguments(argsWithInlinedParamsCollection));

matchedSpecificationsInfo = CallFormatter.Default.Format(
method,
PadNonMatchedSpecifications(matchedSpecifications, argsWithInlinedParamsArray));
PadNonMatchedSpecifications(matchedSpecifications, argsWithInlinedParamsCollection));
}

var message = new StringBuilder();
Expand Down
Loading
Loading