diff --git a/docs/rules/Moq1100.md b/docs/rules/Moq1100.md index 369b351aa..f4fc513e6 100644 --- a/docs/rules/Moq1100.md +++ b/docs/rules/Moq1100.md @@ -70,6 +70,43 @@ var mock = new Mock() .Callback(new DoCallback((int i, string s, DateTime dt) => { })); ``` +## Advanced Patterns Supported + +This analyzer supports comprehensive validation of advanced callback patterns: + +### Multiple Callback Timing + +```csharp +mock.Setup(x => x.DoWork("test")) + .Callback(() => Console.WriteLine("Before")) + .Returns(42) + .Callback(() => Console.WriteLine("After")); +``` + +### Ref/Out Parameter Callbacks + +```csharp +delegate void ProcessDataCallback(ref string data); +mock.Setup(x => x.ProcessData(ref It.Ref.IsAny)) + .Callback(new ProcessDataCallback((ref string data) => data = "processed")); +``` + +### Complex Multi-Parameter Scenarios + +```csharp +mock.Setup(x => x.ProcessMultiple(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((int id, string name, DateTime timestamp) => Console.WriteLine($"Processing {id}")); +``` + +### Out Parameter Delegates + +```csharp +delegate bool TryProcessCallback(out int result); +mock.Setup(x => x.TryProcess(out It.Ref.IsAny)) + .Callback(new TryProcessCallback((out int result) => { result = 42; })) + .Returns(true); +``` + ## Suppress a warning If you just want to suppress a single violation, add preprocessor directives to diff --git a/tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodAnalyzerTests.cs b/tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodAnalyzerTests.cs new file mode 100644 index 000000000..f77fa314c --- /dev/null +++ b/tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodAnalyzerTests.cs @@ -0,0 +1,127 @@ +using Moq.Analyzers.Test.Helpers; + +using AnalyzerVerifier = Moq.Analyzers.Test.Helpers.AnalyzerVerifier; + +namespace Moq.Analyzers.Test; + +/// +/// Comprehensive tests for the CallbackSignatureShouldMatchMockedMethodAnalyzer. +/// Validates all advanced callback patterns including ref/out parameters, multiple callbacks, +/// generic callbacks, and complex scenarios from issue #434. +/// +public class CallbackSignatureShouldMatchMockedMethodAnalyzerTests(ITestOutputHelper output) +{ + /// + /// Consolidated test data for all callback validation scenarios. + /// Combines valid patterns (should not trigger diagnostics) and invalid patterns (should trigger diagnostics). + /// + /// Test data for comprehensive callback validation scenarios. + public static IEnumerable CallbackValidationData() + { + // Valid patterns that should NOT trigger the analyzer + IEnumerable validPatterns = new object[][] + { + // Multiple callbacks with correct signatures + ["""new Mock().Setup(x => x.DoWork("test")).Callback(() => { }).Returns(42).Callback(() => { });"""], + + // Ref parameter with correct signature + ["""new Mock().Setup(m => m.DoRef(ref It.Ref.IsAny)).Callback((ref string data) => { });"""], + + // Out parameter with correct signature + ["""new Mock().Setup(m => m.DoOut(out It.Ref.IsAny)).Callback((out int result) => { result = 42; });"""], + + // Basic callback with correct parameter type + ["""new Mock().Setup(x => x.DoWork("test")).Callback((string param) => { });"""], + + // No parameters callback for parameterized method (valid pattern) + ["""new Mock().Setup(x => x.DoWork("test")).Callback(() => { });"""], + + // Complex multiple parameter with correct signatures + ["""new Mock().Setup(x => x.ProcessMultiple(It.IsAny(), It.IsAny(), It.IsAny())).Callback((int id, string name, DateTime timestamp) => { });"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + + // Invalid patterns that SHOULD trigger the analyzer + IEnumerable invalidPatterns = new object[][] + { + // Basic callback with wrong parameter type + ["""new Mock().Setup(x => x.DoWork("test")).Callback(({|Moq1100:int wrongParam|}) => { });"""], + + // Ref parameter mismatch (missing ref) + ["""new Mock().Setup(m => m.DoRef(ref It.Ref.IsAny)).Callback(({|Moq1100:string data|}) => { });"""], + + // Out parameter mismatch (missing out) + ["""new Mock().Setup(m => m.DoOut(out It.Ref.IsAny)).Callback(({|Moq1100:int result|}) => { });"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + + return validPatterns.Concat(invalidPatterns); + } + + [Theory] + [MemberData(nameof(CallbackValidationData))] + public async Task ShouldValidateCallbackPatterns(string referenceAssemblyGroup, string @namespace, string testCode) + { + static string Template(string ns, string code) => + $$""" + {{ns}} + + public interface IFoo + { + int DoWork(string input); + bool ProcessMultiple(int id, string name, DateTime timestamp); + void ProcessData(ref string data); + bool TryProcess(out int result); + void ProcessMixed(int id, ref string data, out bool success); + void ProcessReadOnly(in DateTime timestamp); + int DoRef(ref string data); + bool DoOut(out int result); + string DoIn(in DateTime timestamp); + T ProcessGeneric(T input); + } + + public class TestClass + { + public void TestMethod() + { + {{code}} + } + } + """; + + string source = Template(@namespace, testCode); + output.WriteLine(source); + await AnalyzerVerifier.VerifyAnalyzerAsync(source, referenceAssemblyGroup); + } + + /// + /// Test to document the current limitation with generic callback validation. + /// This test documents that .Callback<T>() with wrong type parameters is NOT currently validated. + /// This could be enhanced in a future version. + /// + /// A task representing the asynchronous unit test. + [Fact] + public async Task GenericCallbackValidation_CurrentLimitation_IsDocumented() + { + const string source = """ + using Moq; + + public interface IFoo + { + int DoWork(string input); + } + + public class TestClass + { + public void TestGenericCallback() + { + var mock = new Mock(); + // Note: This currently does NOT trigger a diagnostic, which could be enhanced in the future + mock.Setup(x => x.DoWork("test")) + .Callback(wrongTypeParam => { }); // Should ideally trigger Moq1100 but currently doesn't + } + } + """; + + // This test documents the current limitation - no diagnostic is expected + await AnalyzerVerifier.VerifyAnalyzerAsync(source, "Net80WithOldMoq"); + } +}