Skip to content
Closed
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
6 changes: 3 additions & 3 deletions src/NSubstitute/Arg.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using NSubstitute.Core.Arguments;
using System.Linq.Expressions;
using NSubstitute.Core.Arguments;

namespace NSubstitute;

Expand Down Expand Up @@ -39,7 +39,7 @@ public static ref T Is<T>(T value)
/// Match argument that satisfies <paramref name="predicate"/>.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
/// </summary>
public static ref T Is<T>(Expression<Predicate<T?>> predicate)
public static ref T Is<T>(Expression<Predicate<T>> predicate)
{
return ref ArgumentMatcher.Enqueue<T>(new ExpressionArgumentMatcher<T>(predicate));
}
Expand All @@ -48,7 +48,7 @@ public static ref T Is<T>(Expression<Predicate<T?>> predicate)
/// Match argument that satisfies <paramref name="predicate"/>.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
/// </summary>
public static ref T Is<T>(Expression<Predicate<object?>> predicate) where T : AnyType
public static ref T Is<T>(Expression<Predicate<object>> predicate) where T : AnyType
{
return ref ArgumentMatcher.Enqueue<T>(new ExpressionArgumentMatcher<object>(predicate));
}
Expand Down
3 changes: 1 addition & 2 deletions src/NSubstitute/Core/Arguments/ArgumentMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using NSubstitute.Exceptions;

namespace NSubstitute.Core.Arguments;
Expand Down Expand Up @@ -38,7 +37,7 @@ private class GenericToNonGenericMatcherProxy<T>(IArgumentMatcher<T> matcher) :
{
protected readonly IArgumentMatcher<T> _matcher = matcher;

public bool IsSatisfiedBy(object? argument) => _matcher.IsSatisfiedBy((T?)argument!);
public bool IsSatisfiedBy(object? argument) => _matcher.IsSatisfiedBy((T)argument!);

public override string ToString() =>
_matcher is IDescribeSpecification describe
Expand Down
8 changes: 4 additions & 4 deletions src/NSubstitute/Core/Arguments/ExpressionArgumentMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace NSubstitute.Core.Arguments;

public class ExpressionArgumentMatcher<T>(Expression<Predicate<T?>> predicate) : IArgumentMatcher
public class ExpressionArgumentMatcher<T>(Expression<Predicate<T>> predicate) : IArgumentMatcher
{
private readonly string _predicateDescription = predicate.ToString();
private readonly Predicate<T?> _predicate = predicate.Compile();
private readonly Predicate<T> _predicate = predicate.Compile();

public bool IsSatisfiedBy(object? argument) => _predicate((T?)argument);
public bool IsSatisfiedBy(object? argument) => _predicate((T)argument!);

public override string ToString() => _predicateDescription;
}
}
7 changes: 3 additions & 4 deletions src/NSubstitute/Core/CallInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,10 @@ private void EnsureArgIsSettable(Argument argument, int index, object? value)
/// </summary>
/// <typeparam name="T">The type of the argument to retrieve</typeparam>
/// <returns>The argument passed to the call, or throws if there is not exactly one argument of this type</returns>
public T? Arg<T>()
public T Arg<T>()
{
T? arg;
if (TryGetArg(x => x.IsDeclaredTypeEqualToOrByRefVersionOf(typeof(T)), out arg)) return arg;
if (TryGetArg(x => x.IsValueAssignableTo(typeof(T)), out arg)) return arg;
if (TryGetArg(x => x.IsDeclaredTypeEqualToOrByRefVersionOf(typeof(T)), out T? arg) || TryGetArg(x => x.IsValueAssignableTo(typeof(T)), out arg))
return arg!;
throw new ArgumentNotFoundException("Can not find an argument of type " + typeof(T).FullName + " to this call.");
}

Expand Down
22 changes: 21 additions & 1 deletion src/NSubstitute/Core/CallSpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,27 @@ private static bool CanCompareGenericMethods(MethodInfo a, MethodInfo b)
return
AreEquivalentDefinitions(a, b)
&& TypesAreAllEquivalent(ParameterTypes(a), ParameterTypes(b))
&& TypesAreAllEquivalent(a.GetGenericArguments(), b.GetGenericArguments());
&& GenericTypeArgumentsAreCompatible(a.GetGenericArguments(), b.GetGenericArguments());
}


private static bool GenericTypeArgumentsAreCompatible(Type[] specificationArguments, Type[] callArguments)
{
if (specificationArguments.Length != callArguments.Length) return false;

for (var i = 0; i < specificationArguments.Length; i++)
{
var specificationArgument = specificationArguments[i];
var callArgument = callArguments[i];

var isCompatible = specificationArgument.IsAssignableFrom(callArgument)
|| typeof(Arg.AnyType).IsAssignableFrom(specificationArgument)
|| typeof(Arg.AnyType).IsAssignableFrom(callArgument);

if (!isCompatible) return false;
}

return true;
}

private static Type[] ParameterTypes(MethodInfo info)
Expand Down
12 changes: 6 additions & 6 deletions src/NSubstitute/Extensions/ExceptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static class ExceptionExtensions
/// <param name="value"></param>
/// <param name="ex">Exception to throw</param>
/// <returns></returns>
public static ConfiguredCall Throws(this object value, Exception ex) =>
public static ConfiguredCall Throws(this object? value, Exception ex) =>
value.Returns(_ => throw ex);

/// <summary>
Expand All @@ -20,7 +20,7 @@ public static ConfiguredCall Throws(this object value, Exception ex) =>
/// <typeparam name="TException">Type of exception to throw</typeparam>
/// <param name="value"></param>
/// <returns></returns>
public static ConfiguredCall Throws<TException>(this object value)
public static ConfiguredCall Throws<TException>(this object? value)
where TException : notnull, Exception, new()
{
return value.Returns(_ => throw new TException());
Expand All @@ -32,7 +32,7 @@ public static ConfiguredCall Throws<TException>(this object value)
/// <param name="value"></param>
/// <param name="createException">Func creating exception object</param>
/// <returns></returns>
public static ConfiguredCall Throws(this object value, Func<CallInfo, Exception> createException) =>
public static ConfiguredCall Throws(this object? value, Func<CallInfo, Exception> createException) =>
value.Returns(ci => throw createException(ci));

/// <summary>
Expand All @@ -41,7 +41,7 @@ public static ConfiguredCall Throws(this object value, Func<CallInfo, Exception>
/// <param name="value"></param>
/// <param name="ex">Exception to throw</param>
/// <returns></returns>
public static ConfiguredCall ThrowsForAnyArgs(this object value, Exception ex) =>
public static ConfiguredCall ThrowsForAnyArgs(this object? value, Exception ex) =>
value.ReturnsForAnyArgs(_ => throw ex);

/// <summary>
Expand All @@ -50,7 +50,7 @@ public static ConfiguredCall ThrowsForAnyArgs(this object value, Exception ex) =
/// <typeparam name="TException">Type of exception to throw</typeparam>
/// <param name="value"></param>
/// <returns></returns>
public static ConfiguredCall ThrowsForAnyArgs<TException>(this object value)
public static ConfiguredCall ThrowsForAnyArgs<TException>(this object? value)
where TException : notnull, Exception, new()
{
return value.ReturnsForAnyArgs(_ => throw new TException());
Expand All @@ -62,7 +62,7 @@ public static ConfiguredCall ThrowsForAnyArgs<TException>(this object value)
/// <param name="value"></param>
/// <param name="createException">Func creating exception object</param>
/// <returns></returns>
public static ConfiguredCall ThrowsForAnyArgs(this object value, Func<CallInfo, Exception> createException) =>
public static ConfiguredCall ThrowsForAnyArgs(this object? value, Func<CallInfo, Exception> createException) =>
value.ReturnsForAnyArgs(ci => throw createException(ci));

/// <summary>
Expand Down
12 changes: 6 additions & 6 deletions src/NSubstitute/Extensions/ReturnsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ public static class ReturnsExtensions
/// <summary>
/// Set null as returned value for this call.
/// </summary>
public static ConfiguredCall ReturnsNull<T>(this T value) where T : class =>
public static ConfiguredCall ReturnsNull<T>(this T value) where T : class? =>
value.Returns(default(T));

/// <summary>
/// Set null as returned value for this call made with any arguments.
/// </summary>
public static ConfiguredCall ReturnsNullForAnyArgs<T>(this T value) where T : class =>
public static ConfiguredCall ReturnsNullForAnyArgs<T>(this T value) where T : class? =>
value.ReturnsForAnyArgs(default(T));

/// <summary>
Expand All @@ -32,19 +32,19 @@ public static ConfiguredCall ReturnsNullForAnyArgs<T>(this T? value) where T : s
/// <summary>
/// Set null as returned value for this call.
/// </summary>
public static ConfiguredCall ReturnsNull<T>(this Task<T> value) where T : class =>
public static ConfiguredCall ReturnsNull<T>(this Task<T> value) where T : class? =>
value.Returns(default(T));

/// <summary>
/// Set null as returned value for this call.
/// </summary>
public static ConfiguredCall ReturnsNull<T>(this ValueTask<T> value) where T : class =>
public static ConfiguredCall ReturnsNull<T>(this ValueTask<T> value) where T : class? =>
value.Returns(default(T));

/// <summary>
/// Set null as returned value for this call made with any arguments.
/// </summary>
public static ConfiguredCall ReturnsNullForAnyArgs<T>(this Task<T> value) where T : class =>
public static ConfiguredCall ReturnsNullForAnyArgs<T>(this Task<T> value) where T : class? =>
value.ReturnsForAnyArgs(default(T));

/// <summary>
Expand All @@ -53,7 +53,7 @@ public static ConfiguredCall ReturnsNullForAnyArgs<T>(this Task<T> value) where
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <returns></returns>
public static ConfiguredCall ReturnsNullForAnyArgs<T>(this ValueTask<T> value) where T : class =>
public static ConfiguredCall ReturnsNullForAnyArgs<T>(this ValueTask<T> value) where T : class? =>
value.ReturnsForAnyArgs(default(T));

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions src/NSubstitute/Raise.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public static class Raise
/// <summary>
/// Raise an event for an <c>EventHandler&lt;TEventArgs&gt;</c> event with the provided <paramref name="sender"/> and <paramref name="eventArgs"/>.
/// </summary>
public static EventHandlerWrapper<TEventArgs> EventWith<TEventArgs>(object sender, TEventArgs eventArgs) where TEventArgs : EventArgs
public static EventHandlerWrapper<TEventArgs> EventWith<TEventArgs>(object? sender, TEventArgs eventArgs) where TEventArgs : EventArgs
{
return new EventHandlerWrapper<TEventArgs>(sender, eventArgs);
}
Expand Down Expand Up @@ -44,7 +44,7 @@ public static EventHandlerWrapper<EventArgs> Event()
/// Raise an event of type <typeparamref name="THandler" /> with the provided arguments. If no arguments are provided
/// NSubstitute will try to provide reasonable defaults.
/// </summary>
public static DelegateEventWrapper<THandler> Event<THandler>(params object[] arguments)
public static DelegateEventWrapper<THandler> Event<THandler>(params object?[] arguments)
{
var normalizedArgs = FixParamsArrayAmbiguity(arguments, typeof(THandler));
return new DelegateEventWrapper<THandler>(normalizedArgs);
Expand All @@ -55,7 +55,7 @@ public static DelegateEventWrapper<THandler> Event<THandler>(params object[] arg
/// whether input array represents all arguments, or the first argument only.
/// If we find that ambiguity might happen, we wrap user input in an extra array.
/// </summary>
private static object[] FixParamsArrayAmbiguity(object[] arguments, Type delegateType)
private static object?[] FixParamsArrayAmbiguity(object?[] arguments, Type delegateType)
{
ParameterInfo[] invokeMethodParameters = delegateType.GetInvokeMethod().GetParameters();
if (invokeMethodParameters.Length != 1)
Expand Down
12 changes: 6 additions & 6 deletions src/NSubstitute/SubstituteExtensions.Returns.Task.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static partial class SubstituteExtensions
/// <param name="value"></param>
/// <param name="returnThis">Value to return. Will be wrapped in a Task</param>
/// <param name="returnThese">Optionally use these values next</param>
public static ConfiguredCall Returns<T>(this Task<T> value, T? returnThis, params T[] returnThese)
public static ConfiguredCall Returns<T>(this Task<T>? value, T? returnThis, params T[] returnThese)
{
ReThrowOnNSubstituteFault(value);

Expand All @@ -27,7 +27,7 @@ public static ConfiguredCall Returns<T>(this Task<T> value, T? returnThis, param
/// <param name="value"></param>
/// <param name="returnThis">Function to calculate the return value</param>
/// <param name="returnThese">Optionally use these functions next</param>
public static ConfiguredCall Returns<T>(this Task<T> value, Func<CallInfo, T> returnThis, params Func<CallInfo, T>[] returnThese)
public static ConfiguredCall Returns<T>(this Task<T>? value, Func<CallInfo, T> returnThis, params Func<CallInfo, T>[] returnThese)
{
ReThrowOnNSubstituteFault(value);

Expand All @@ -43,7 +43,7 @@ public static ConfiguredCall Returns<T>(this Task<T> value, Func<CallInfo, T> re
/// <param name="value"></param>
/// <param name="returnThis">Value to return</param>
/// <param name="returnThese">Optionally return these values next</param>
public static ConfiguredCall ReturnsForAnyArgs<T>(this Task<T> value, T? returnThis, params T[] returnThese)
public static ConfiguredCall ReturnsForAnyArgs<T>(this Task<T>? value, T? returnThis, params T[] returnThese)
{
ReThrowOnNSubstituteFault(value);

Expand All @@ -59,7 +59,7 @@ public static ConfiguredCall ReturnsForAnyArgs<T>(this Task<T> value, T? returnT
/// <param name="value"></param>
/// <param name="returnThis">Function to calculate the return value</param>
/// <param name="returnThese">Optionally use these functions next</param>
public static ConfiguredCall ReturnsForAnyArgs<T>(this Task<T> value, Func<CallInfo, T> returnThis, params Func<CallInfo, T>[] returnThese)
public static ConfiguredCall ReturnsForAnyArgs<T>(this Task<T>? value, Func<CallInfo, T> returnThis, params Func<CallInfo, T>[] returnThese)
{
ReThrowOnNSubstituteFault(value);

Expand All @@ -69,9 +69,9 @@ public static ConfiguredCall ReturnsForAnyArgs<T>(this Task<T> value, Func<CallI
return ConfigureReturn(MatchArgs.Any, wrappedFunc, wrappedReturnThese);
}

private static void ReThrowOnNSubstituteFault<T>(Task<T> task)
private static void ReThrowOnNSubstituteFault<T>(Task<T>? task)
{
if (task.IsFaulted && task.Exception!.InnerExceptions.FirstOrDefault() is SubstituteException)
if (task is { IsFaulted: true } && task.Exception!.InnerExceptions.FirstOrDefault() is SubstituteException)
{
task.GetAwaiter().GetResult();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using NUnit.Framework;

namespace NSubstitute.Acceptance.Specs.FieldReports;

#nullable enable

public class Issue973_MatchingWithNullability
{
public interface ISomething
{
int DoSomething(string s);
int DoSomethingNullable(string? s);
}

[Test]
public void Match_non_null()
{
var sub = Substitute.For<ISomething>();

sub.DoSomething(Arg.Is<string>(x => x.StartsWith("12"))).Returns(42);

Assert.That(sub.DoSomething("123"), Is.EqualTo(42));
Assert.That(sub.DoSomething("abc"), Is.EqualTo(0));
}

[Test]
public void Match_nullable()
{
var sub = Substitute.For<ISomething>();

sub.DoSomethingNullable(Arg.Is<string>(x => x.StartsWith("12"))).Returns(42);
sub.DoSomethingNullable(Arg.Is<string?>(x => x == null)).Returns(456);

Assert.That(sub.DoSomethingNullable("123"), Is.EqualTo(42));
Assert.That(sub.DoSomethingNullable("hi"), Is.EqualTo(0));
Assert.That(sub.DoSomethingNullable(null), Is.EqualTo(456));
}
}

#nullable restore
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,38 @@ public void Stub_generic_method_with_specific_subtype()
Assert.That(_sub.IntCall(new GMParam1()), Is.EqualTo(default(int)));
}

[Test]
public void Stub_for_derived_type_argument_is_not_used_for_base_type_argument()
{
_sub.IntCall(Arg.Any<GMParam1>()).Returns(42);

Assert.That(_sub.IntCall(new GMParam1()), Is.EqualTo(42));
Assert.That(_sub.IntCall<IGMParam>(new GMParam1()), Is.EqualTo(default(int)));
}

[Test]
public void Stub_for_derived_type_argument_is_not_returned_for_base_type_argument()
{
_sub.Get<GMParam1>(Arg.Any<string>()).Returns(new Box<GMParam1>());

var derivedResult = _sub.Get<GMParam1>("x");
var baseResult = _sub.Get<IGMParam>("x");

Assert.That(derivedResult, Is.TypeOf<Box<GMParam1>>());
Assert.That(baseResult, Is.Not.Null);
Assert.That(baseResult, Is.Not.InstanceOf<Box<GMParam1>>());
}

public interface IGenMethod
{
void Call<T>(T param) where T : IGMParam;
int IntCall<T>(T param) where T : IGMParam;
Box<T> Get<T>(string key) where T : IGMParam;
}
public interface IGMParam { }
public class GMParam1 : IGMParam { }
public class GMParam2 : IGMParam { }
public class Box<T> where T : IGMParam { }
}

[TestFixture]
Expand Down
Loading