diff --git a/src/CodeFixes/ReturnsDelegateShouldReturnTaskFixer.cs b/src/CodeFixes/ReturnsDelegateShouldReturnTaskFixer.cs index 8e269d034..caf380cc9 100644 --- a/src/CodeFixes/ReturnsDelegateShouldReturnTaskFixer.cs +++ b/src/CodeFixes/ReturnsDelegateShouldReturnTaskFixer.cs @@ -62,6 +62,10 @@ private static Task ReplaceReturnsWithReturnsAsync( { SimpleNameSyntax oldName = memberAccess.Name; SimpleNameSyntax newName; + + // Moq's IReturns interface defines method-level generic overloads + // like Returns(Func). When a user writes .Returns(s => 42), + // the syntax is GenericNameSyntax. Preserve type arguments to maintain developer intent. if (oldName is GenericNameSyntax genericName) { newName = SyntaxFactory.GenericName( diff --git a/tests/Moq.Analyzers.Benchmarks/Moq1208ReturnsDelegateBenchmarks.cs b/tests/Moq.Analyzers.Benchmarks/Moq1208ReturnsDelegateBenchmarks.cs new file mode 100644 index 000000000..8fb8fed7d --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Moq1208ReturnsDelegateBenchmarks.cs @@ -0,0 +1,97 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Moq.Analyzers.Benchmarks.Helpers; + +namespace Moq.Analyzers.Benchmarks; + +[InProcess] +[MemoryDiagnoser] +[BenchmarkCategory("Moq1208")] +public class Moq1208ReturnsDelegateBenchmarks +{ +#pragma warning disable ECS0900 + [Params(1, 1_000)] +#pragma warning restore ECS0900 + public int FileCount { get; set; } + + [Params("Net80WithOldMoq", "Net80WithNewMoq")] + public string MoqKey { get; set; } = "Net80WithOldMoq"; + + private CompilationWithAnalyzers? BaselineCompilation { get; set; } + + private CompilationWithAnalyzers? TestCompilation { get; set; } + + [IterationSetup] + [SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Async setup not supported in BenchmarkDotNet.See https://github.com/dotnet/BenchmarkDotNet/issues/2442.")] + public void SetupCompilation() + { + List<(string Name, string Content)> sources = []; + for (int index = 0; index < FileCount; index++) + { + string name = "TypeName" + index; + sources.Add((name, @$" +using System; +using System.Threading.Tasks; +using Moq; + +public class AsyncClient{index} +{{ + public virtual Task GetValueAsync() => Task.FromResult(0); + public virtual Task GetNameAsync() => Task.FromResult(string.Empty); + public virtual ValueTask GetValueTaskAsync() => ValueTask.FromResult(0); +}} + +internal class {name} +{{ + private void Test() + {{ + new Mock().Setup(c => c.GetValueAsync()).Returns(() => 42); + _ = ""sample test""; // Add an expression that looks similar but does not match + }} +}} +")); + } + + Microsoft.CodeAnalysis.Testing.ReferenceAssemblies referenceAssemblies = CompilationCreator.GetReferenceAssemblies(MoqKey); + (BaselineCompilation, TestCompilation) = + BenchmarkCSharpCompilationFactory + .CreateAsync(sources.ToArray(), referenceAssemblies) + .GetAwaiter() + .GetResult(); + } + + [Benchmark] + public async Task Moq1208WithDiagnostics() + { + ImmutableArray diagnostics = + (await TestCompilation! + .GetAnalysisResultAsync(CancellationToken.None) + .ConfigureAwait(false)) + .AssertValidAnalysisResult() + .GetAllDiagnostics(); + + // Each file has one Returns(() => 42) on a Task method + if (diagnostics.Length != FileCount) + { + throw new InvalidOperationException($"Expected '{FileCount:N0}' analyzer diagnostics but found '{diagnostics.Length}'"); + } + } + + [Benchmark(Baseline = true)] + public async Task Moq1208Baseline() + { + ImmutableArray diagnostics = + (await BaselineCompilation! + .GetAnalysisResultAsync(CancellationToken.None) + .ConfigureAwait(false)) + .AssertValidAnalysisResult() + .GetAllDiagnostics(); + + if (diagnostics.Length != 0) + { + throw new InvalidOperationException($"Expected no analyzer diagnostics but found '{diagnostics.Length}'"); + } + } +} diff --git a/tests/Moq.Analyzers.Test/ReturnsDelegateShouldReturnTaskAnalyzerTests.cs b/tests/Moq.Analyzers.Test/ReturnsDelegateShouldReturnTaskAnalyzerTests.cs index 41723cf43..31bde622c 100644 --- a/tests/Moq.Analyzers.Test/ReturnsDelegateShouldReturnTaskAnalyzerTests.cs +++ b/tests/Moq.Analyzers.Test/ReturnsDelegateShouldReturnTaskAnalyzerTests.cs @@ -160,6 +160,12 @@ public static IEnumerable ValidWithCompilerSuppression() // Invocation as value, not delegate: GetInt() is a call, not a method group (GH PR #942 review thread) ["""new Mock().Setup(c => c.GetValueAsync()).Returns(GetInt());"""], + + // Generic Returns with sync lambda: target-type inference masks the mismatch (analyzer gap, see PR #1089) + ["""new Mock().Setup(c => c.ProcessAsync(It.IsAny())).Returns(s => s.Length);"""], + + // Generic Returns with async lambda (no mismatch) + ["""new Mock().Setup(c => c.ProcessAsync(It.IsAny())).Returns(async s => s.Length);"""], }; return data.WithNamespaces().WithMoqReferenceAssemblyGroups();