NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

AmbiguousArgumentsException exception thrown if struct type in matcher has ctor with this call

Open Tornhoof opened this issue 8 months ago • 2 comments

Describe the bug As soon as the readonly struct Path in the repro below has a call to another ctor the following AmbiguousArgumentsException is thrown:


NSubstitute.Exceptions.AmbiguousArgumentsException
  HResult=0x80131500
  Message=Cannot determine argument specifications to use. Please use specifications for all arguments of the same type.
Method signature:
    Do<Parent, Base, Child>(Path<Base, Parent>, Func<Parent, Base>)
Method arguments (possible arg matchers are indicated with '*'):
    Do<Parent, Base, Child>(NSubRepro.Path`2[NSubRepro.Base,NSubRepro.Parent], *<null>*)
All queued specifications:
    any Path<Base, Parent>
    any Func<Parent, Child>
Matched argument specifications:
    Do<Parent, Base, Child>(NSubRepro.Path`2[NSubRepro.Base,NSubRepro.Parent], ???)

  Source=NSubstitute
  StackTrace:
   at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs)
   at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs)
   at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call)
   at NSubstitute.Routing.Route.Handle(ICall call)
   at NSubstitute.Core.CallRouter.Route(ICall call)
   at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at Castle.Proxies.ObjectProxy.Do[TParent,TBaseChild,TChild](Path`2 parentPath, Func`2 relation)
   at NSubRepro.Program.Main(String[] args) in D:\Repros\NSubRepro\NSubRepro\Program.cs:line 15


To Reproduce

Repro code:

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using NSubstitute;

namespace NSubRepro
{

    internal class Program
    {
        static void Main(string[] args)
        {
            var testMock = Substitute.For<ITest>();
            testMock.Do<Parent, Base, Child>(Arg.Any<Path<Base,  Parent>>(), Arg.Any<Func<Parent, Child>>()).Returns(Task.CompletedTask);
        }
    }


    public readonly struct Path<TVisit, TMatch>
    {

        private readonly ImmutableList<TVisit> _nodes;


        // As soon as the call to the other ctor is commented out, it works
        public Path() : this(ImmutableList<TVisit>.Empty)
        {

        }


        private Path(TVisit node) : this(ImmutableList<TVisit>.Empty.Add(node))
        {
        }

        private Path(ImmutableList<TVisit> nodes)
        {
            _nodes = nodes;
        }
    }

    public interface ITest
    {
        Task Do<TParent, TBaseChild, TChild>(Path<Base,TParent> parentPath, Func<TParent, TBaseChild> relation)
            where TParent : Base where TBaseChild : Base where TChild : TBaseChild;
    }

    public abstract class Base;

    public sealed class Parent : Base
    {
        public List<Child> Children { get; } = [];
    }

    public sealed class Child : Base;
}

As soon as the this call in the Path ctor is commented, the exception is gone. I'm not sure how that call is responsible for that exception.

Expected behaviour No exception

Environment:

  • NSubstitute version: 5.3.0
  • NSubstitute.Analyzers version: Csharp 1.0.17
  • Platform: .NET 9, x64 Windows 11

Tornhoof avatar Feb 19 '25 16:02 Tornhoof