diff --git a/src/NSubstitute/Arg.cs b/src/NSubstitute/Arg.cs index 6d0fc49d..526da014 100644 --- a/src/NSubstitute/Arg.cs +++ b/src/NSubstitute/Arg.cs @@ -1,5 +1,5 @@ -using NSubstitute.Core.Arguments; using System.Linq.Expressions; +using NSubstitute.Core.Arguments; namespace NSubstitute; @@ -39,7 +39,7 @@ public static ref T Is(T value) /// Match argument that satisfies . /// If the throws an exception for an argument it will be treated as non-matching. /// - public static ref T Is(Expression> predicate) + public static ref T Is(Expression> predicate) { return ref ArgumentMatcher.Enqueue(new ExpressionArgumentMatcher(predicate)); } @@ -48,7 +48,7 @@ public static ref T Is(Expression> predicate) /// Match argument that satisfies . /// If the throws an exception for an argument it will be treated as non-matching. /// - public static ref T Is(Expression> predicate) where T : AnyType + public static ref T Is(Expression> predicate) where T : AnyType { return ref ArgumentMatcher.Enqueue(new ExpressionArgumentMatcher(predicate)); } diff --git a/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs b/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs index 55d8eb0a..691cd07c 100644 --- a/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs +++ b/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using NSubstitute.Exceptions; namespace NSubstitute.Core.Arguments; @@ -38,7 +37,7 @@ private class GenericToNonGenericMatcherProxy(IArgumentMatcher matcher) : { protected readonly IArgumentMatcher _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 diff --git a/src/NSubstitute/Core/Arguments/ExpressionArgumentMatcher.cs b/src/NSubstitute/Core/Arguments/ExpressionArgumentMatcher.cs index dd20d8d2..cef38def 100644 --- a/src/NSubstitute/Core/Arguments/ExpressionArgumentMatcher.cs +++ b/src/NSubstitute/Core/Arguments/ExpressionArgumentMatcher.cs @@ -2,12 +2,12 @@ namespace NSubstitute.Core.Arguments; -public class ExpressionArgumentMatcher(Expression> predicate) : IArgumentMatcher +public class ExpressionArgumentMatcher(Expression> predicate) : IArgumentMatcher { private readonly string _predicateDescription = predicate.ToString(); - private readonly Predicate _predicate = predicate.Compile(); + private readonly Predicate _predicate = predicate.Compile(); - public bool IsSatisfiedBy(object? argument) => _predicate((T?)argument); + public bool IsSatisfiedBy(object? argument) => _predicate((T)argument!); public override string ToString() => _predicateDescription; -} \ No newline at end of file +} diff --git a/src/NSubstitute/Core/CallInfo.cs b/src/NSubstitute/Core/CallInfo.cs index d21e4d2a..0e9cb402 100644 --- a/src/NSubstitute/Core/CallInfo.cs +++ b/src/NSubstitute/Core/CallInfo.cs @@ -59,11 +59,10 @@ private void EnsureArgIsSettable(Argument argument, int index, object? value) /// /// The type of the argument to retrieve /// The argument passed to the call, or throws if there is not exactly one argument of this type - public T? Arg() + public T Arg() { - 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."); } diff --git a/src/NSubstitute/Core/CallSpecification.cs b/src/NSubstitute/Core/CallSpecification.cs index e0d1ff15..6d8084b6 100644 --- a/src/NSubstitute/Core/CallSpecification.cs +++ b/src/NSubstitute/Core/CallSpecification.cs @@ -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) diff --git a/src/NSubstitute/Extensions/ExceptionExtensions.cs b/src/NSubstitute/Extensions/ExceptionExtensions.cs index 655706cb..1ab6c981 100644 --- a/src/NSubstitute/Extensions/ExceptionExtensions.cs +++ b/src/NSubstitute/Extensions/ExceptionExtensions.cs @@ -11,7 +11,7 @@ public static class ExceptionExtensions /// /// Exception to throw /// - public static ConfiguredCall Throws(this object value, Exception ex) => + public static ConfiguredCall Throws(this object? value, Exception ex) => value.Returns(_ => throw ex); /// @@ -20,7 +20,7 @@ public static ConfiguredCall Throws(this object value, Exception ex) => /// Type of exception to throw /// /// - public static ConfiguredCall Throws(this object value) + public static ConfiguredCall Throws(this object? value) where TException : notnull, Exception, new() { return value.Returns(_ => throw new TException()); @@ -32,7 +32,7 @@ public static ConfiguredCall Throws(this object value) /// /// Func creating exception object /// - public static ConfiguredCall Throws(this object value, Func createException) => + public static ConfiguredCall Throws(this object? value, Func createException) => value.Returns(ci => throw createException(ci)); /// @@ -41,7 +41,7 @@ public static ConfiguredCall Throws(this object value, Func /// /// Exception to throw /// - public static ConfiguredCall ThrowsForAnyArgs(this object value, Exception ex) => + public static ConfiguredCall ThrowsForAnyArgs(this object? value, Exception ex) => value.ReturnsForAnyArgs(_ => throw ex); /// @@ -50,7 +50,7 @@ public static ConfiguredCall ThrowsForAnyArgs(this object value, Exception ex) = /// Type of exception to throw /// /// - public static ConfiguredCall ThrowsForAnyArgs(this object value) + public static ConfiguredCall ThrowsForAnyArgs(this object? value) where TException : notnull, Exception, new() { return value.ReturnsForAnyArgs(_ => throw new TException()); @@ -62,7 +62,7 @@ public static ConfiguredCall ThrowsForAnyArgs(this object value) /// /// Func creating exception object /// - public static ConfiguredCall ThrowsForAnyArgs(this object value, Func createException) => + public static ConfiguredCall ThrowsForAnyArgs(this object? value, Func createException) => value.ReturnsForAnyArgs(ci => throw createException(ci)); /// diff --git a/src/NSubstitute/Extensions/ReturnsExtensions.cs b/src/NSubstitute/Extensions/ReturnsExtensions.cs index 29457d98..606b5f4a 100644 --- a/src/NSubstitute/Extensions/ReturnsExtensions.cs +++ b/src/NSubstitute/Extensions/ReturnsExtensions.cs @@ -8,13 +8,13 @@ public static class ReturnsExtensions /// /// Set null as returned value for this call. /// - public static ConfiguredCall ReturnsNull(this T value) where T : class => + public static ConfiguredCall ReturnsNull(this T value) where T : class? => value.Returns(default(T)); /// /// Set null as returned value for this call made with any arguments. /// - public static ConfiguredCall ReturnsNullForAnyArgs(this T value) where T : class => + public static ConfiguredCall ReturnsNullForAnyArgs(this T value) where T : class? => value.ReturnsForAnyArgs(default(T)); /// @@ -32,19 +32,19 @@ public static ConfiguredCall ReturnsNullForAnyArgs(this T? value) where T : s /// /// Set null as returned value for this call. /// - public static ConfiguredCall ReturnsNull(this Task value) where T : class => + public static ConfiguredCall ReturnsNull(this Task value) where T : class? => value.Returns(default(T)); /// /// Set null as returned value for this call. /// - public static ConfiguredCall ReturnsNull(this ValueTask value) where T : class => + public static ConfiguredCall ReturnsNull(this ValueTask value) where T : class? => value.Returns(default(T)); /// /// Set null as returned value for this call made with any arguments. /// - public static ConfiguredCall ReturnsNullForAnyArgs(this Task value) where T : class => + public static ConfiguredCall ReturnsNullForAnyArgs(this Task value) where T : class? => value.ReturnsForAnyArgs(default(T)); /// @@ -53,7 +53,7 @@ public static ConfiguredCall ReturnsNullForAnyArgs(this Task value) where /// /// /// - public static ConfiguredCall ReturnsNullForAnyArgs(this ValueTask value) where T : class => + public static ConfiguredCall ReturnsNullForAnyArgs(this ValueTask value) where T : class? => value.ReturnsForAnyArgs(default(T)); /// diff --git a/src/NSubstitute/Raise.cs b/src/NSubstitute/Raise.cs index 937ec5c2..74c80bf7 100644 --- a/src/NSubstitute/Raise.cs +++ b/src/NSubstitute/Raise.cs @@ -9,7 +9,7 @@ public static class Raise /// /// Raise an event for an EventHandler<TEventArgs> event with the provided and . /// - public static EventHandlerWrapper EventWith(object sender, TEventArgs eventArgs) where TEventArgs : EventArgs + public static EventHandlerWrapper EventWith(object? sender, TEventArgs eventArgs) where TEventArgs : EventArgs { return new EventHandlerWrapper(sender, eventArgs); } @@ -44,7 +44,7 @@ public static EventHandlerWrapper Event() /// Raise an event of type with the provided arguments. If no arguments are provided /// NSubstitute will try to provide reasonable defaults. /// - public static DelegateEventWrapper Event(params object[] arguments) + public static DelegateEventWrapper Event(params object?[] arguments) { var normalizedArgs = FixParamsArrayAmbiguity(arguments, typeof(THandler)); return new DelegateEventWrapper(normalizedArgs); @@ -55,7 +55,7 @@ public static DelegateEventWrapper Event(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. /// - 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) diff --git a/src/NSubstitute/SubstituteExtensions.Returns.Task.cs b/src/NSubstitute/SubstituteExtensions.Returns.Task.cs index 9d603679..583b889f 100644 --- a/src/NSubstitute/SubstituteExtensions.Returns.Task.cs +++ b/src/NSubstitute/SubstituteExtensions.Returns.Task.cs @@ -11,7 +11,7 @@ public static partial class SubstituteExtensions /// /// Value to return. Will be wrapped in a Task /// Optionally use these values next - public static ConfiguredCall Returns(this Task value, T? returnThis, params T[] returnThese) + public static ConfiguredCall Returns(this Task? value, T? returnThis, params T[] returnThese) { ReThrowOnNSubstituteFault(value); @@ -27,7 +27,7 @@ public static ConfiguredCall Returns(this Task value, T? returnThis, param /// /// Function to calculate the return value /// Optionally use these functions next - public static ConfiguredCall Returns(this Task value, Func returnThis, params Func[] returnThese) + public static ConfiguredCall Returns(this Task? value, Func returnThis, params Func[] returnThese) { ReThrowOnNSubstituteFault(value); @@ -43,7 +43,7 @@ public static ConfiguredCall Returns(this Task value, Func re /// /// Value to return /// Optionally return these values next - public static ConfiguredCall ReturnsForAnyArgs(this Task value, T? returnThis, params T[] returnThese) + public static ConfiguredCall ReturnsForAnyArgs(this Task? value, T? returnThis, params T[] returnThese) { ReThrowOnNSubstituteFault(value); @@ -59,7 +59,7 @@ public static ConfiguredCall ReturnsForAnyArgs(this Task value, T? returnT /// /// Function to calculate the return value /// Optionally use these functions next - public static ConfiguredCall ReturnsForAnyArgs(this Task value, Func returnThis, params Func[] returnThese) + public static ConfiguredCall ReturnsForAnyArgs(this Task? value, Func returnThis, params Func[] returnThese) { ReThrowOnNSubstituteFault(value); @@ -69,9 +69,9 @@ public static ConfiguredCall ReturnsForAnyArgs(this Task value, Func(Task task) + private static void ReThrowOnNSubstituteFault(Task? task) { - if (task.IsFaulted && task.Exception!.InnerExceptions.FirstOrDefault() is SubstituteException) + if (task is { IsFaulted: true } && task.Exception!.InnerExceptions.FirstOrDefault() is SubstituteException) { task.GetAwaiter().GetResult(); } diff --git a/tests/NSubstitute.Acceptance.Specs/FieldReports/Issue973_MatchingWithNullability.cs b/tests/NSubstitute.Acceptance.Specs/FieldReports/Issue973_MatchingWithNullability.cs new file mode 100644 index 00000000..b13b100d --- /dev/null +++ b/tests/NSubstitute.Acceptance.Specs/FieldReports/Issue973_MatchingWithNullability.cs @@ -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(); + + sub.DoSomething(Arg.Is(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(); + + sub.DoSomethingNullable(Arg.Is(x => x.StartsWith("12"))).Returns(42); + sub.DoSomethingNullable(Arg.Is(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 diff --git a/tests/NSubstitute.Acceptance.Specs/MatchingDerivedTypesForGenerics.cs b/tests/NSubstitute.Acceptance.Specs/MatchingDerivedTypesForGenerics.cs index 0422c570..7766d248 100644 --- a/tests/NSubstitute.Acceptance.Specs/MatchingDerivedTypesForGenerics.cs +++ b/tests/NSubstitute.Acceptance.Specs/MatchingDerivedTypesForGenerics.cs @@ -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()).Returns(42); + + Assert.That(_sub.IntCall(new GMParam1()), Is.EqualTo(42)); + Assert.That(_sub.IntCall(new GMParam1()), Is.EqualTo(default(int))); + } + + [Test] + public void Stub_for_derived_type_argument_is_not_returned_for_base_type_argument() + { + _sub.Get(Arg.Any()).Returns(new Box()); + + var derivedResult = _sub.Get("x"); + var baseResult = _sub.Get("x"); + + Assert.That(derivedResult, Is.TypeOf>()); + Assert.That(baseResult, Is.Not.Null); + Assert.That(baseResult, Is.Not.InstanceOf>()); + } + public interface IGenMethod { void Call(T param) where T : IGMParam; int IntCall(T param) where T : IGMParam; + Box Get(string key) where T : IGMParam; } public interface IGMParam { } public class GMParam1 : IGMParam { } public class GMParam2 : IGMParam { } + public class Box where T : IGMParam { } } [TestFixture]