diff --git a/src/Analyzers/LinqToMocksExpressionShouldBeValidAnalyzer.cs b/src/Analyzers/LinqToMocksExpressionShouldBeValidAnalyzer.cs index c7e81796b..7713ad5c5 100644 --- a/src/Analyzers/LinqToMocksExpressionShouldBeValidAnalyzer.cs +++ b/src/Analyzers/LinqToMocksExpressionShouldBeValidAnalyzer.cs @@ -71,14 +71,7 @@ private static bool IsValidMockOfInvocation(IInvocationOperation invocation, Moq { IMethodSymbol targetMethod = invocation.TargetMethod; - // Check if this is a static method call to Mock.Of() - if (!targetMethod.IsStatic || !string.Equals(targetMethod.Name, "Of", StringComparison.Ordinal)) - { - return false; - } - - return targetMethod.ContainingType is not null && - targetMethod.ContainingType.Equals(knownSymbols.Mock, SymbolEqualityComparer.Default); + return targetMethod.IsStatic && targetMethod.IsInstanceOf(knownSymbols.MockOf); } private static void AnalyzeMockOfArguments(OperationAnalysisContext context, IInvocationOperation invocationOperation, MoqKnownSymbols knownSymbols) diff --git a/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs b/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs index 8024008c9..42be1ce2d 100644 --- a/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs +++ b/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs @@ -69,40 +69,14 @@ private static void Analyze(SyntaxNodeAnalysisContext context, MoqKnownSymbols k return; } - if (!TryGetRaiseMethodArguments(invocation, context.SemanticModel, knownSymbols, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes)) + if (!EventSyntaxExtensions.TryGetEventMethodArgumentsFromLambdaSelector(invocation, context.SemanticModel, knownSymbols, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes)) { return; } - // Extract event name from the first argument (event selector lambda) - string? eventName = null; - if (invocation.ArgumentList.Arguments.Count > 0) - { - ExpressionSyntax eventSelector = invocation.ArgumentList.Arguments[0].Expression; - context.SemanticModel.TryGetEventNameFromLambdaSelector(eventSelector, out eventName); - } + string eventName = EventSyntaxExtensions.GetEventNameFromSelector(invocation, context.SemanticModel); - context.ValidateEventArgumentTypes(eventArguments, expectedParameterTypes, invocation, Rule, eventName ?? "event"); - } - - private static bool TryGetRaiseMethodArguments( - InvocationExpressionSyntax invocation, - SemanticModel semanticModel, - KnownSymbols knownSymbols, - out ArgumentSyntax[] eventArguments, - out ITypeSymbol[] expectedParameterTypes) - { - return EventSyntaxExtensions.TryGetEventMethodArguments( - invocation, - semanticModel, - out eventArguments, - out expectedParameterTypes, - (sm, selector) => - { - bool success = sm.TryGetEventTypeFromLambdaSelector(selector, out ITypeSymbol? eventType); - return (success, eventType); - }, - knownSymbols); + context.ValidateEventArgumentTypes(eventArguments, expectedParameterTypes, invocation, Rule, eventName); } private static bool IsRaiseMethodCall(SemanticModel semanticModel, InvocationExpressionSyntax invocation, MoqKnownSymbols knownSymbols) diff --git a/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs b/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs index 738bbd8b0..537567f8c 100644 --- a/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs +++ b/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs @@ -66,39 +66,18 @@ private static void Analyze(SyntaxNodeAnalysisContext context, MoqKnownSymbols k { InvocationExpressionSyntax invocation = (InvocationExpressionSyntax)context.Node; - // Check if this is a Raises method call using symbol-based detection - if (!context.SemanticModel.IsRaisesInvocation(invocation, knownSymbols) && !invocation.IsRaisesMethodCall(context.SemanticModel, knownSymbols)) + if (!context.SemanticModel.IsRaisesInvocation(invocation, knownSymbols)) { return; } - if (!TryGetRaisesMethodArguments(invocation, context.SemanticModel, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes)) + if (!EventSyntaxExtensions.TryGetEventMethodArgumentsFromLambdaSelector(invocation, context.SemanticModel, knownSymbols, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes)) { return; } - // Extract event name from the first argument (event selector lambda) - string? eventName = null; - if (invocation.ArgumentList.Arguments.Count > 0) - { - ExpressionSyntax eventSelector = invocation.ArgumentList.Arguments[0].Expression; - context.SemanticModel.TryGetEventNameFromLambdaSelector(eventSelector, out eventName); - } + string eventName = EventSyntaxExtensions.GetEventNameFromSelector(invocation, context.SemanticModel); - context.ValidateEventArgumentTypes(eventArguments, expectedParameterTypes, invocation, Rule, eventName ?? "event"); - } - - private static bool TryGetRaisesMethodArguments(InvocationExpressionSyntax invocation, SemanticModel semanticModel, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes) - { - return EventSyntaxExtensions.TryGetEventMethodArguments( - invocation, - semanticModel, - out eventArguments, - out expectedParameterTypes, - (sm, selector) => - { - bool success = sm.TryGetEventTypeFromLambdaSelector(selector, out ITypeSymbol? eventType); - return (success, eventType); - }); + context.ValidateEventArgumentTypes(eventArguments, expectedParameterTypes, invocation, Rule, eventName); } } diff --git a/src/Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs b/src/Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs index 769e78bcb..903f465c9 100644 --- a/src/Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs +++ b/src/Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs @@ -40,12 +40,12 @@ private static void RegisterCompilationStartAction(CompilationStartAnalysisConte return; } - // Check Moq version and skip analysis if the version is 4.16.0 or later - AssemblyIdentity? moqAssembly = context.Compilation.ReferencedAssemblyNames.FirstOrDefault(a => a.Name.Equals("Moq", StringComparison.OrdinalIgnoreCase)); + // Check Moq version once per compilation, skip for 4.16.0 or later + AssemblyIdentity? moqAssembly = context.Compilation.ReferencedAssemblyNames + .FirstOrDefault(a => a.Name.Equals("Moq", StringComparison.OrdinalIgnoreCase)); if (moqAssembly != null && moqAssembly.Version >= new Version(4, 16, 0)) { - // Skip analysis for Moq 4.16.0 or later return; } diff --git a/src/Common/EventSyntaxExtensions.cs b/src/Common/EventSyntaxExtensions.cs index d7d7df516..8d8e632b2 100644 --- a/src/Common/EventSyntaxExtensions.cs +++ b/src/Common/EventSyntaxExtensions.cs @@ -24,59 +24,52 @@ internal static void ValidateEventArgumentTypes( { if (eventArguments.Length != expectedParameterTypes.Length) { - Location location; - if (eventArguments.Length < expectedParameterTypes.Length) - { - // Too few arguments: report on the invocation - location = invocation.GetLocation(); - } - else - { - // Too many arguments: report on the first extra argument - location = eventArguments[expectedParameterTypes.Length].GetLocation(); - } + Location location = eventArguments.Length < expectedParameterTypes.Length + ? invocation.GetLocation() + : eventArguments[expectedParameterTypes.Length].GetLocation(); - Diagnostic diagnostic = eventName != null - ? location.CreateDiagnostic(rule, eventName) - : location.CreateDiagnostic(rule); - context.ReportDiagnostic(diagnostic); + context.ReportDiagnostic(CreateEventDiagnostic(location, rule, eventName)); return; } - // Check each argument type matches the expected parameter type for (int i = 0; i < eventArguments.Length; i++) { TypeInfo argumentTypeInfo = context.SemanticModel.GetTypeInfo(eventArguments[i].Expression, context.CancellationToken); ITypeSymbol? argumentType = argumentTypeInfo.Type; - ITypeSymbol expectedType = expectedParameterTypes[i]; - if (argumentType != null && !context.SemanticModel.HasConversion(argumentType, expectedType)) + if (argumentType != null && !context.SemanticModel.HasConversion(argumentType, expectedParameterTypes[i])) { - // Report on the specific argument with the wrong type - Diagnostic diagnostic = eventName != null - ? eventArguments[i].GetLocation().CreateDiagnostic(rule, eventName) - : eventArguments[i].GetLocation().CreateDiagnostic(rule); - context.ReportDiagnostic(diagnostic); + context.ReportDiagnostic(CreateEventDiagnostic(eventArguments[i].GetLocation(), rule, eventName)); } } } /// /// Gets the parameter types for a given event delegate type. + /// This method handles various delegate types including Action delegates, EventHandler delegates, + /// and custom delegates by analyzing their structure and extracting parameter information. /// - /// The event delegate type. + /// The event delegate type to analyze. /// Known symbols for type checking. - /// An array of parameter types expected by the event delegate. - internal static ITypeSymbol[] GetEventParameterTypes(ITypeSymbol eventType, KnownSymbols? knownSymbols = null) + /// + /// An array of parameter types expected by the event delegate: + /// - For delegates: Returns all generic type arguments + /// - For delegates: Returns the single generic argument T + /// - For custom delegates: Returns parameters from the Invoke method + /// - For non-delegate types: Returns an empty array. + /// + internal static ITypeSymbol[] GetEventParameterTypes(ITypeSymbol eventType, KnownSymbols knownSymbols) { if (eventType is not INamedTypeSymbol namedType) { return []; } - ITypeSymbol[]? parameterTypes = TryGetActionDelegateParameters(namedType, knownSymbols) ?? - TryGetEventHandlerDelegateParameters(namedType, knownSymbols) ?? - TryGetCustomDelegateParameters(namedType); + // Try different delegate type handlers in order of specificity + ITypeSymbol[]? parameterTypes = + TryGetActionDelegateParameters(namedType, knownSymbols) ?? + TryGetEventHandlerDelegateParameters(namedType, knownSymbols) ?? + TryGetCustomDelegateParameters(namedType); return parameterTypes ?? []; } @@ -97,7 +90,7 @@ internal static bool TryGetEventMethodArguments( out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes, Func eventTypeExtractor, - KnownSymbols? knownSymbols = null) + KnownSymbols knownSymbols) { eventArguments = []; expectedParameterTypes = []; @@ -118,11 +111,7 @@ internal static bool TryGetEventMethodArguments( expectedParameterTypes = GetEventParameterTypes(eventType, knownSymbols); - if (arguments.Count <= 1) - { - eventArguments = []; - } - else + if (arguments.Count > 1) { eventArguments = new ArgumentSyntax[arguments.Count - 1]; for (int i = 1; i < arguments.Count; i++) @@ -134,34 +123,93 @@ internal static bool TryGetEventMethodArguments( return true; } + /// + /// Extracts event arguments from an event method invocation using the standard + /// lambda-based event type extraction pattern shared by Raise and Raises analyzers. + /// + /// The method invocation. + /// The semantic model. + /// Known symbols for type checking. + /// The extracted event arguments. + /// The expected parameter types. + /// if arguments were successfully extracted; otherwise, . + internal static bool TryGetEventMethodArgumentsFromLambdaSelector( + InvocationExpressionSyntax invocation, + SemanticModel semanticModel, + KnownSymbols knownSymbols, + out ArgumentSyntax[] eventArguments, + out ITypeSymbol[] expectedParameterTypes) + { + return TryGetEventMethodArguments( + invocation, + semanticModel, + out eventArguments, + out expectedParameterTypes, + static (sm, selector) => + { + bool success = sm.TryGetEventTypeFromLambdaSelector(selector, out ITypeSymbol? eventType); + return (success, eventType); + }, + knownSymbols); + } + + /// + /// Extracts the event name from the first argument (event selector lambda) of an invocation. + /// + /// The method invocation containing the lambda selector. + /// The semantic model. + /// The event name if found; otherwise "event" as a fallback. + internal static string GetEventNameFromSelector(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + { + SeparatedSyntaxList arguments = invocation.ArgumentList.Arguments; + if (arguments.Count < 1) + { + return "event"; + } + + ExpressionSyntax eventSelector = arguments[0].Expression; + + return semanticModel.TryGetEventNameFromLambdaSelector(eventSelector, out string? eventName) + ? eventName! + : "event"; + } + + /// + /// Creates a for an event-related rule violation. + /// When is provided, it is passed as a message format argument. + /// When is , no message arguments are included. + /// + /// The source location for the diagnostic. + /// The diagnostic descriptor for the rule. + /// The event name to include in the message, or . + /// A new instance. + internal static Diagnostic CreateEventDiagnostic(Location location, DiagnosticDescriptor rule, string? eventName) + { + return eventName != null + ? location.CreateDiagnostic(rule, eventName) + : location.CreateDiagnostic(rule); + } + /// /// Attempts to get parameter types from Action delegate types. /// /// The named type symbol to check. - /// Optional known symbols for enhanced type checking. + /// Known symbols for type checking. /// Parameter types if this is an Action delegate; otherwise null. - private static ITypeSymbol[]? TryGetActionDelegateParameters(INamedTypeSymbol namedType, KnownSymbols? knownSymbols) + private static ITypeSymbol[]? TryGetActionDelegateParameters(INamedTypeSymbol namedType, KnownSymbols knownSymbols) { - bool isActionDelegate = knownSymbols != null - ? namedType.IsActionDelegate(knownSymbols) - : IsActionDelegate(namedType); - - return isActionDelegate ? namedType.TypeArguments.ToArray() : null; + return namedType.IsActionDelegate(knownSymbols) ? namedType.TypeArguments.ToArray() : null; } /// /// Attempts to get parameter types from EventHandler delegate types. /// /// The named type symbol to check. - /// Optional known symbols for enhanced type checking. + /// Known symbols for type checking. /// Parameter types if this is an EventHandler delegate; otherwise null. - private static ITypeSymbol[]? TryGetEventHandlerDelegateParameters(INamedTypeSymbol namedType, KnownSymbols? knownSymbols) + private static ITypeSymbol[]? TryGetEventHandlerDelegateParameters(INamedTypeSymbol namedType, KnownSymbols knownSymbols) { - bool isEventHandlerDelegate = knownSymbols != null - ? namedType.IsEventHandlerDelegate(knownSymbols) - : IsEventHandlerDelegate(namedType); - - if (isEventHandlerDelegate && namedType.TypeArguments.Length > 0) + if (namedType.IsEventHandlerDelegate(knownSymbols) && namedType.TypeArguments.Length > 0) { return [namedType.TypeArguments[0]]; } @@ -177,16 +225,18 @@ internal static bool TryGetEventMethodArguments( private static ITypeSymbol[]? TryGetCustomDelegateParameters(INamedTypeSymbol namedType) { IMethodSymbol? invokeMethod = namedType.DelegateInvokeMethod; - return invokeMethod?.Parameters.Select(p => p.Type).ToArray(); - } + if (invokeMethod is null) + { + return null; + } - private static bool IsActionDelegate(INamedTypeSymbol namedType) - { - return string.Equals(namedType.Name, "Action", StringComparison.Ordinal); - } + ImmutableArray parameters = invokeMethod.Parameters; + ITypeSymbol[] types = new ITypeSymbol[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + types[i] = parameters[i].Type; + } - private static bool IsEventHandlerDelegate(INamedTypeSymbol namedType) - { - return string.Equals(namedType.Name, "EventHandler", StringComparison.Ordinal) && namedType.TypeArguments.Length == 1; + return types; } } diff --git a/src/Common/ISymbolExtensions.Moq.cs b/src/Common/ISymbolExtensions.Moq.cs index d61764e1e..b0428c29c 100644 --- a/src/Common/ISymbolExtensions.Moq.cs +++ b/src/Common/ISymbolExtensions.Moq.cs @@ -18,7 +18,6 @@ internal static bool IsTaskOrValueTaskType(this ITypeSymbol typeSymbol, MoqKnown return false; } - // Check for Task, Task, ValueTask, or ValueTask INamedTypeSymbol originalDefinition = namedType.OriginalDefinition; return SymbolEqualityComparer.Default.Equals(originalDefinition, knownSymbols.Task) || @@ -29,13 +28,13 @@ internal static bool IsTaskOrValueTaskType(this ITypeSymbol typeSymbol, MoqKnown internal static bool IsTaskOrValueResultProperty(this ISymbol symbol, MoqKnownSymbols knownSymbols) { - if (symbol is IPropertySymbol propertySymbol) + if (symbol is not IPropertySymbol propertySymbol) { - return IsGenericResultProperty(propertySymbol, knownSymbols.Task1) - || IsGenericResultProperty(propertySymbol, knownSymbols.ValueTask1); + return false; } - return false; + return IsGenericResultProperty(propertySymbol, knownSymbols.Task1) + || IsGenericResultProperty(propertySymbol, knownSymbols.ValueTask1); } internal static bool IsMoqSetupMethod(this ISymbol symbol, MoqKnownSymbols knownSymbols) @@ -253,22 +252,17 @@ private static bool IsRaiseableMethod(ISymbol symbol, MoqKnownSymbols knownSymbo /// /// Checks if a property is the 'Result' property on or . /// - private static bool IsGenericResultProperty(this ISymbol symbol, INamedTypeSymbol? genericType) + private static bool IsGenericResultProperty(IPropertySymbol propertySymbol, INamedTypeSymbol? genericType) { - if (symbol is IPropertySymbol propertySymbol) + // "Result" is a stable BCL property name on Task and ValueTask. + // The string check is safe here because the containing type is verified + // against the known generic type symbol below. + if (!string.Equals(propertySymbol.Name, "Result", StringComparison.Ordinal)) { - // Check if the property is named "Result" - if (!string.Equals(propertySymbol.Name, "Result", StringComparison.Ordinal)) - { - return false; - } - - return genericType != null && - - // If Task type cannot be found, we skip it - SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType.OriginalDefinition, genericType); + return false; } - return false; + return genericType != null && + SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType.OriginalDefinition, genericType); } } diff --git a/src/Common/InvocationExpressionSyntaxExtensions.cs b/src/Common/InvocationExpressionSyntaxExtensions.cs index dec57e2ef..0c118065a 100644 --- a/src/Common/InvocationExpressionSyntaxExtensions.cs +++ b/src/Common/InvocationExpressionSyntaxExtensions.cs @@ -52,38 +52,6 @@ internal static class InvocationExpressionSyntaxExtensions return null; } - /// - /// Determines if an invocation is a Raises method call using symbol-based detection. - /// This method verifies the method belongs to IRaiseable or IRaiseableAsync. - /// - /// The invocation expression to check. - /// The semantic model for symbol resolution. - /// The known Moq symbols for type checking. - /// if the invocation is a Raises method call; otherwise, . - internal static bool IsRaisesMethodCall(this InvocationExpressionSyntax invocation, SemanticModel semanticModel, MoqKnownSymbols knownSymbols) - { - if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) - { - return false; - } - - // Use symbol-based detection to verify this is a proper Moq Raises method - SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(memberAccess); - if (symbolInfo.Symbol?.IsMoqRaisesMethod(knownSymbols) == true) - { - return true; - } - - // Check candidate symbols in case of overload resolution failure - if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure && - symbolInfo.CandidateSymbols.Any(symbol => symbol.IsMoqRaisesMethod(knownSymbols))) - { - return true; - } - - return false; - } - private static LambdaExpressionSyntax? GetSetupLambdaArgument(InvocationExpressionSyntax? setupInvocation) { if (setupInvocation is null || setupInvocation.ArgumentList.Arguments.Count == 0) diff --git a/src/Common/SemanticModelExtensions.cs b/src/Common/SemanticModelExtensions.cs index cc0fb39a2..82cb4f4fc 100644 --- a/src/Common/SemanticModelExtensions.cs +++ b/src/Common/SemanticModelExtensions.cs @@ -7,6 +7,11 @@ namespace Moq.Analyzers.Common; /// internal static class SemanticModelExtensions { +#pragma warning disable ECS1300 // Conflicts with ECS1200; inline initialization is clearer + private static readonly string[] CallbackOrReturnNames = ["Callback", "Returns"]; + private static readonly string[] RaisesNames = ["Raises", "RaisesAsync"]; +#pragma warning restore ECS1300 + internal static InvocationExpressionSyntax? FindSetupMethodFromCallbackInvocation( this SemanticModel semanticModel, MoqKnownSymbols knownSymbols, @@ -48,53 +53,20 @@ internal static IEnumerable GetAllMatchingMockedMethodSymbolsFrom internal static bool IsCallbackOrReturnInvocation(this SemanticModel semanticModel, InvocationExpressionSyntax callbackOrReturnsInvocation, MoqKnownSymbols knownSymbols) { - MemberAccessExpressionSyntax? callbackOrReturnsMethod = callbackOrReturnsInvocation.Expression as MemberAccessExpressionSyntax; - - if (callbackOrReturnsMethod == null) - { - return false; - } - - string methodName = callbackOrReturnsMethod.Name.ToString(); - - // First fast check before walking semantic model - if (!string.Equals(methodName, "Callback", StringComparison.Ordinal) - && !string.Equals(methodName, "Returns", StringComparison.Ordinal)) - { - return false; - } - - SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(callbackOrReturnsMethod); - return symbolInfo.CandidateReason switch - { - CandidateReason.OverloadResolutionFailure => symbolInfo.CandidateSymbols.Any(symbol => IsCallbackOrReturnSymbol(symbol, knownSymbols)), - CandidateReason.None => IsCallbackOrReturnSymbol(symbolInfo.Symbol, knownSymbols), - _ => false, - }; + return semanticModel.IsMoqFluentInvocation( + callbackOrReturnsInvocation, + CallbackOrReturnNames, + knownSymbols, + static (symbol, ks) => IsCallbackOrReturnSymbol(symbol, ks)); } internal static bool IsRaisesInvocation(this SemanticModel semanticModel, InvocationExpressionSyntax raisesInvocation, MoqKnownSymbols knownSymbols) { - if (raisesInvocation.Expression is not MemberAccessExpressionSyntax raisesMethod) - { - return false; - } - - SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(raisesMethod); - - if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure) - { - return symbolInfo.CandidateSymbols.Any(symbol => symbol.IsMoqRaisesMethod(knownSymbols)); - } - - if (symbolInfo.CandidateReason == CandidateReason.None) - { - return IsRaisesSymbol(symbolInfo.Symbol, knownSymbols); - } - - // If symbol resolution failed for other reasons, return false. - // All valid Raises invocations should be detected via symbol-based analysis above. - return false; + return semanticModel.IsMoqFluentInvocation( + raisesInvocation, + RaisesNames, + knownSymbols, + static (symbol, ks) => symbol.IsMoqRaisesMethod(ks)); } /// @@ -251,18 +223,66 @@ private static bool TryGetEventSymbolFromLambdaSelector( return true; } - private static bool IsCallbackOrReturnSymbol(ISymbol? symbol, MoqKnownSymbols knownSymbols) + /// + /// Determines whether an invocation matches a Moq fluent-chain method by name and symbol. + /// + /// + /// The string check is an intentional optimization, + /// not detection logic. The is the authoritative + /// gate for correctness. The parameter is passed + /// through to the predicate to avoid closure allocations. + /// + /// The semantic model. + /// The invocation to check. + /// Method names for early rejection before the expensive GetSymbolInfo call. + /// Known symbols passed through to the predicate to avoid closure allocations. + /// Predicate that validates the resolved symbol against known symbols. + /// if the invocation matches; otherwise, . + private static bool IsMoqFluentInvocation( + this SemanticModel semanticModel, + InvocationExpressionSyntax invocation, + string[] fastPathNames, + MoqKnownSymbols knownSymbols, + Func symbolPredicate) { - if (symbol is null) + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) { return false; } - return symbol.IsMoqCallbackMethod(knownSymbols) || symbol.IsMoqReturnsMethod(knownSymbols); + string methodName = memberAccess.Name.Identifier.ValueText; + + bool nameMatches = false; + for (int i = 0; i < fastPathNames.Length; i++) + { + if (string.Equals(methodName, fastPathNames[i], StringComparison.Ordinal)) + { + nameMatches = true; + break; + } + } + + if (!nameMatches) + { + return false; + } + + SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(memberAccess); + return symbolInfo.CandidateReason switch + { + CandidateReason.OverloadResolutionFailure => symbolInfo.CandidateSymbols.Any(s => symbolPredicate(s, knownSymbols)), + CandidateReason.None => symbolInfo.Symbol is not null && symbolPredicate(symbolInfo.Symbol, knownSymbols), + _ => false, + }; } - private static bool IsRaisesSymbol(ISymbol? symbol, MoqKnownSymbols knownSymbols) + private static bool IsCallbackOrReturnSymbol(ISymbol? symbol, MoqKnownSymbols knownSymbols) { - return symbol?.IsMoqRaisesMethod(knownSymbols) == true; + if (symbol is null) + { + return false; + } + + return symbol.IsMoqCallbackMethod(knownSymbols) || symbol.IsMoqReturnsMethod(knownSymbols); } } diff --git a/tests/Moq.Analyzers.Test/Common/EventSyntaxExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/EventSyntaxExtensionsTests.cs index 4032a17b5..47f906179 100644 --- a/tests/Moq.Analyzers.Test/Common/EventSyntaxExtensionsTests.cs +++ b/tests/Moq.Analyzers.Test/Common/EventSyntaxExtensionsTests.cs @@ -1,174 +1,194 @@ using Moq.Analyzers.Common.WellKnown; +using RaisesVerifier = Moq.Analyzers.Test.Helpers.AnalyzerVerifier; +using RaiseVerifier = Moq.Analyzers.Test.Helpers.AnalyzerVerifier; namespace Moq.Analyzers.Test.Common; public class EventSyntaxExtensionsTests { - [Fact] - public void GetEventParameterTypes_ActionDelegate_ReturnsTypeArguments() +#pragma warning disable RS2008 // Enable analyzer release tracking (test-only descriptor) +#pragma warning disable ECS1300 // Test-only descriptor; inline init is simpler than static constructor + private static readonly DiagnosticDescriptor TestRuleWithPlaceholder = new( + "EVT0001", + "Test", + "Event '{0}' has wrong args", + "Test", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor TestRuleNoPlaceholder = new( + "EVT0002", + "Test", + "Event has wrong args", + "Test", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); +#pragma warning restore ECS1300 +#pragma warning restore RS2008 + + public static IEnumerable TooFewArgumentsData() { - const string code = @" -using System; -class C -{ - event Action MyEvent; -}"; - ITypeSymbol eventType = GetEventFieldType(code, "MyEvent"); - - ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType); - - Assert.Equal(2, result.Length); - Assert.Equal("int", result[0].ToDisplayString()); - Assert.Equal("string", result[1].ToDisplayString()); + return new object[][] + { + // Action expects 1 arg, Raises passes 0 + ["""{|Moq1204:mockProvider.Setup(x => x.Submit()).Raises(x => x.StringEvent += null)|};"""], + + // Custom delegate expects 2 params, Raises passes 1 + ["""{|Moq1204:mockProvider.Setup(x => x.Submit()).Raises(x => x.TwoParamDelegate += null, "only-one")|};"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } - [Fact] - public void GetEventParameterTypes_ActionWithSingleTypeArg_ReturnsSingleType() + public static IEnumerable TooManyArgumentsData() { - const string code = @" -using System; -class C -{ - event Action MyEvent; -}"; - ITypeSymbol eventType = GetEventFieldType(code, "MyEvent"); - - ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType); - - Assert.Single(result); - Assert.Equal("double", result[0].ToDisplayString()); + return new object[][] + { + // Action expects 1 arg, Raises passes 3 + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.StringEvent += null, "test", {|Moq1204:"extra1"|});"""], + + // Action event (no params) with extra arg + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.SimpleEvent += null, {|Moq1204:"unexpected"|});"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } - [Fact] - public void GetEventParameterTypes_EventHandlerGeneric_ReturnsSingleTypeArgument() + public static IEnumerable WrongArgumentTypesData() { - const string code = @" -using System; -class C -{ - event EventHandler MyEvent; -}"; - ITypeSymbol eventType = GetEventFieldType(code, "MyEvent"); + return new object[][] + { + // Action event with int argument + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.StringEvent += null, {|Moq1204:42|});"""], - ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType); + // Action event with string argument + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.NumberEvent += null, {|Moq1204:"wrong"|});"""], - Assert.Single(result); - Assert.Equal("System.EventArgs", result[0].ToDisplayString()); + // EventHandler with wrong type + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.CustomEvent += null, {|Moq1204:"notCustomArgs"|});"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } - [Fact] - public void GetEventParameterTypes_CustomDelegate_ReturnsInvokeMethodParameters() + public static IEnumerable ExactMatchData() { - const string code = @" -delegate void MyDelegate(int x, bool y); -class C -{ - event MyDelegate MyEvent; -}"; - ITypeSymbol eventType = GetEventFieldType(code, "MyEvent"); - - ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType); - - Assert.Equal(2, result.Length); - Assert.Equal("int", result[0].ToDisplayString()); - Assert.Equal("bool", result[1].ToDisplayString()); + return new object[][] + { + // Action with correct string + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.StringEvent += null, "correct");"""], + + // Action with correct int + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.NumberEvent += null, 42);"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } - [Fact] - public void GetEventParameterTypes_CustomDelegateNoParameters_ReturnsEmpty() + public static IEnumerable EventHandlerSubclassData() { - const string code = @" -delegate void MyDelegate(); -class C -{ - event MyDelegate MyEvent; -}"; - ITypeSymbol eventType = GetEventFieldType(code, "MyEvent"); - - ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType); - - Assert.Empty(result); + return new object[][] + { + // EventHandler with CustomArgs instance + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.CustomEvent += null, new CustomArgs());"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } - [Fact] - public void GetEventParameterTypes_NonNamedTypeSymbol_ReturnsEmpty() + public static IEnumerable ActionDelegateData() { - const string code = @" -class C -{ - int[] Field; -}"; - (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); - VariableDeclaratorSyntax fieldSyntax = tree.GetRoot() - .DescendantNodes().OfType().First(); - IFieldSymbol field = (IFieldSymbol)model.GetDeclaredSymbol(fieldSyntax)!; - ITypeSymbol arrayType = field.Type; + return new object[][] + { + // Action with implicit int-to-double conversion + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.DoubleEvent += null, 42);"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + } - Assert.IsNotAssignableFrom(arrayType); + public static IEnumerable CustomDelegateData() + { + return new object[][] + { + // Custom delegate with matching parameters + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.TwoParamDelegate += null, "hello", 99);"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + } - ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(arrayType); + public static IEnumerable ZeroExpectedParametersData() + { + return new object[][] + { + // Action event with zero parameters, no args passed + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.SimpleEvent += null);"""], + + // Parameterless custom delegate with no args + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.NoParamDelegate += null);"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + } - Assert.Empty(result); + public static IEnumerable ManyParametersMatchData() + { + return new object[][] + { + // Action with all correct types + ["""mockProvider.Raise(p => p.MultiParamEvent += null, 1, "two", true);"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } - [Fact] - public void GetEventParameterTypes_PlainEventHandler_ReturnsFallbackInvokeParams() + public static IEnumerable ManyParametersMismatchData() { - const string code = @" -using System; -class C -{ - event EventHandler MyEvent; -}"; - ITypeSymbol eventType = GetEventFieldType(code, "MyEvent"); + return new object[][] + { + // Action with third arg wrong type + ["""mockProvider.Raise(p => p.MultiParamEvent += null, 1, "two", {|Moq1202:"notBool"|});"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + } - ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType); + public static IEnumerable ZeroParamsWithExtraArgsData() + { + return new object[][] + { + // Parameterless delegate, but extra args are passed + ["""mockProvider.Setup(x => x.Submit()).Raises(x => x.NoParamDelegate += null, {|Moq1204:"extra"|});"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + } - Assert.Equal(2, result.Length); - Assert.Contains("object", result[0].ToDisplayString(), StringComparison.Ordinal); - Assert.Contains("EventArgs", result[1].ToDisplayString(), StringComparison.Ordinal); + public static IEnumerable RaiseWithEventNameData() + { + return new object[][] + { + // Raise (with event name) too few args + ["""{|Moq1202:mockProvider.Raise(p => p.StringOptionsChanged += null)|};"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } - [Fact] - public void GetEventParameterTypes_WithKnownSymbols_ActionDelegate_ReturnsTypeArguments() + [Theory] + [InlineData("using System;\nclass C { event Action MyEvent; }", "double")] + [InlineData("using System;\nclass C { event EventHandler MyEvent; }", "System.EventArgs")] + public void GetEventParameterTypes_SingleTypeArgDelegate_ReturnsSingleType( + string code, + string expectedTypeName) { - const string code = @" -using System; -class C -{ - event Action MyEvent; -}"; (ITypeSymbol eventType, KnownSymbols knownSymbols) = GetEventFieldTypeWithKnownSymbols(code, "MyEvent"); ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType, knownSymbols); - Assert.Equal(2, result.Length); - Assert.Equal("int", result[0].ToDisplayString()); - Assert.Equal("string", result[1].ToDisplayString()); + Assert.Single(result); + Assert.Equal(expectedTypeName, result[0].ToDisplayString()); } - [Fact] - public void GetEventParameterTypes_WithKnownSymbols_EventHandlerGeneric_ReturnsSingleTypeArgument() + [Theory] + [InlineData("using System;\nclass C { event Action MyEvent; }", "int", "string")] + [InlineData("delegate void MyDelegate(int x, bool y);\nclass C { event MyDelegate MyEvent; }", "int", "bool")] + public void GetEventParameterTypes_MultiParamDelegate_ReturnsAllParameterTypes( + string code, + string expectedFirst, + string expectedSecond) { - const string code = @" -using System; -class C -{ - event EventHandler MyEvent; -}"; (ITypeSymbol eventType, KnownSymbols knownSymbols) = GetEventFieldTypeWithKnownSymbols(code, "MyEvent"); ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType, knownSymbols); - Assert.Single(result); - Assert.Equal("System.EventArgs", result[0].ToDisplayString()); + Assert.Equal(2, result.Length); + Assert.Equal(expectedFirst, result[0].ToDisplayString()); + Assert.Equal(expectedSecond, result[1].ToDisplayString()); } [Fact] - public void GetEventParameterTypes_WithKnownSymbols_CustomDelegate_ReturnsInvokeMethodParameters() + public void GetEventParameterTypes_CustomDelegateNoParameters_ReturnsEmpty() { const string code = @" -delegate void MyDelegate(int x, bool y); +delegate void MyDelegate(); class C { event MyDelegate MyEvent; @@ -177,13 +197,11 @@ class C ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType, knownSymbols); - Assert.Equal(2, result.Length); - Assert.Equal("int", result[0].ToDisplayString()); - Assert.Equal("bool", result[1].ToDisplayString()); + Assert.Empty(result); } [Fact] - public void GetEventParameterTypes_WithKnownSymbols_NonNamedTypeSymbol_ReturnsEmpty() + public void GetEventParameterTypes_NonNamedTypeSymbol_ReturnsEmpty() { const string code = @" class C @@ -195,12 +213,33 @@ class C VariableDeclaratorSyntax fieldSyntax = tree.GetRoot() .DescendantNodes().OfType().First(); IFieldSymbol field = (IFieldSymbol)model.GetDeclaredSymbol(fieldSyntax)!; + ITypeSymbol arrayType = field.Type; - ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(field.Type, knownSymbols); + Assert.IsNotAssignableFrom(arrayType); + + ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(arrayType, knownSymbols); Assert.Empty(result); } + [Fact] + public void GetEventParameterTypes_PlainEventHandler_ReturnsFallbackInvokeParams() + { + const string code = @" +using System; +class C +{ + event EventHandler MyEvent; +}"; + (ITypeSymbol eventType, KnownSymbols knownSymbols) = GetEventFieldTypeWithKnownSymbols(code, "MyEvent"); + + ITypeSymbol[] result = EventSyntaxExtensions.GetEventParameterTypes(eventType, knownSymbols); + + Assert.Equal(2, result.Length); + Assert.Contains("object", result[0].ToDisplayString(), StringComparison.Ordinal); + Assert.Contains("EventArgs", result[1].ToDisplayString(), StringComparison.Ordinal); + } + [Fact] public void TryGetEventMethodArguments_NoArguments_ReturnsFalse() { @@ -214,6 +253,7 @@ void M() void SomeMethod() {} }"; (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -222,7 +262,8 @@ void SomeMethod() {} model, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes, - (_, _) => (true, null)); + (_, _) => (true, null), + knownSymbols); Assert.False(result); Assert.Empty(eventArguments); @@ -242,6 +283,7 @@ void M() void SomeMethod(int x) {} }"; (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -250,7 +292,8 @@ void SomeMethod(int x) {} model, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes, - (_, _) => (false, null)); + (_, _) => (false, null), + knownSymbols); Assert.False(result); Assert.Empty(eventArguments); @@ -270,6 +313,7 @@ void M() void SomeMethod(int x) {} }"; (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -278,7 +322,8 @@ void SomeMethod(int x) {} model, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes, - (_, _) => (true, null)); + (_, _) => (true, null), + knownSymbols); Assert.False(result); Assert.Empty(eventArguments); @@ -300,12 +345,13 @@ void M() } void SomeMethod(int selector) {} }"; - ITypeSymbol delegateType = GetEventFieldType( + (ITypeSymbol delegateType, KnownSymbols _) = GetEventFieldTypeWithKnownSymbols( @"delegate void MyDelegate(int x); class C { event MyDelegate MyEvent; }", "MyEvent"); (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -314,7 +360,8 @@ class C { event MyDelegate MyEvent; }", model, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes, - (_, _) => (true, delegateType)); + (_, _) => (true, delegateType), + knownSymbols); Assert.True(result); Assert.Empty(eventArguments); @@ -335,12 +382,13 @@ void M() } void SomeMethod(int selector, int a, string b) {} }"; - ITypeSymbol delegateType = GetEventFieldType( + (ITypeSymbol delegateType, KnownSymbols _) = GetEventFieldTypeWithKnownSymbols( @"delegate void MyDelegate(int x, string y); class C { event MyDelegate MyEvent; }", "MyEvent"); (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -349,7 +397,8 @@ class C { event MyDelegate MyEvent; }", model, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes, - (_, _) => (true, delegateType)); + (_, _) => (true, delegateType), + knownSymbols); Assert.True(result); Assert.Equal(2, eventArguments.Length); @@ -357,122 +406,167 @@ class C { event MyDelegate MyEvent; }", } [Fact] - public void TryGetEventMethodArguments_WithKnownSymbols_NoArguments_ReturnsFalse() - { - const string code = @" -class C -{ - void M() + public void CreateEventDiagnostic_WithEventName_SetsRuleLocationAndMessage() { - SomeMethod(); - } - void SomeMethod() {} -}"; - (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); - KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); - InvocationExpressionSyntax invocation = tree.GetRoot() - .DescendantNodes().OfType().First(); + SyntaxTree tree = CSharpSyntaxTree.ParseText("class C { }"); + Location location = tree.GetRoot().GetLocation(); - bool result = EventSyntaxExtensions.TryGetEventMethodArguments( - invocation, - model, - out ArgumentSyntax[] eventArguments, - out ITypeSymbol[] expectedParameterTypes, - (_, _) => (true, null), - knownSymbols); + Diagnostic diagnostic = EventSyntaxExtensions.CreateEventDiagnostic(location, TestRuleWithPlaceholder, "MyEvent"); - Assert.False(result); - Assert.Empty(eventArguments); - Assert.Empty(expectedParameterTypes); + Assert.Equal(TestRuleWithPlaceholder.Id, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + Assert.Contains("MyEvent", diagnostic.GetMessage(), StringComparison.Ordinal); + Assert.True(diagnostic.Location.IsInSource); + Assert.Equal(location.SourceSpan, diagnostic.Location.SourceSpan); } [Fact] - public void TryGetEventMethodArguments_WithKnownSymbols_ExtractorReturnsFalse_ReturnsFalse() - { - const string code = @" -class C -{ - void M() + public void CreateEventDiagnostic_WithNullEventName_SetsRuleLocationAndMessage() { - SomeMethod(42); - } - void SomeMethod(int x) {} -}"; - (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); - KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); - InvocationExpressionSyntax invocation = tree.GetRoot() - .DescendantNodes().OfType().First(); + SyntaxTree tree = CSharpSyntaxTree.ParseText("class C { }"); + Location location = tree.GetRoot().GetLocation(); - bool result = EventSyntaxExtensions.TryGetEventMethodArguments( - invocation, - model, - out ArgumentSyntax[] eventArguments, - out ITypeSymbol[] expectedParameterTypes, - (_, _) => (false, null), - knownSymbols); + Diagnostic diagnostic = EventSyntaxExtensions.CreateEventDiagnostic(location, TestRuleNoPlaceholder, null); - Assert.False(result); - Assert.Empty(eventArguments); - Assert.Empty(expectedParameterTypes); + Assert.Equal(TestRuleNoPlaceholder.Id, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + Assert.Equal("Event has wrong args", diagnostic.GetMessage()); + Assert.True(diagnostic.Location.IsInSource); + Assert.Equal(location.SourceSpan, diagnostic.Location.SourceSpan); } [Fact] - public void TryGetEventMethodArguments_WithKnownSymbols_ValidExtraction_ReturnsTrue() + public void CreateEventDiagnostic_WithEmptyStringEventName_PassesEmptyStringAsArg() { - const string code = @" -using System; -class C -{ - void M() + SyntaxTree tree = CSharpSyntaxTree.ParseText("class C { }"); + Location location = tree.GetRoot().GetLocation(); + + Diagnostic diagnostic = EventSyntaxExtensions.CreateEventDiagnostic(location, TestRuleWithPlaceholder, string.Empty); + + Assert.Equal("EVT0001", diagnostic.Id); + Assert.Equal("Event '' has wrong args", diagnostic.GetMessage()); + } + + [Fact] + public void CreateEventDiagnostic_WithSpecialCharactersInEventName_PassesNameAsArg() { - SomeMethod(0, 42); + SyntaxTree tree = CSharpSyntaxTree.ParseText("class C { }"); + Location location = tree.GetRoot().GetLocation(); + + Diagnostic diagnostic = EventSyntaxExtensions.CreateEventDiagnostic(location, TestRuleWithPlaceholder, "On"); + + Assert.Equal("EVT0001", diagnostic.Id); + Assert.Contains("On", diagnostic.GetMessage(), StringComparison.Ordinal); } - void SomeMethod(int selector, int a) {} -}"; - ITypeSymbol delegateType = GetEventFieldType( - @" -using System; -delegate void MyDelegate(int x); -class C { event MyDelegate MyEvent; }", - "MyEvent"); - (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); - KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); - InvocationExpressionSyntax invocation = tree.GetRoot() - .DescendantNodes().OfType().First(); + // ValidateEventArgumentTypes tests exercise the method indirectly through the + // analyzer pipeline. SyntaxNodeAnalysisContext has no public constructor, so direct + // unit testing is not feasible. + [Theory] + [MemberData(nameof(TooFewArgumentsData))] + [MemberData(nameof(TooManyArgumentsData))] + [MemberData(nameof(WrongArgumentTypesData))] + [MemberData(nameof(ExactMatchData))] + [MemberData(nameof(EventHandlerSubclassData))] + [MemberData(nameof(ActionDelegateData))] + [MemberData(nameof(CustomDelegateData))] + [MemberData(nameof(ZeroExpectedParametersData))] + [MemberData(nameof(ZeroParamsWithExtraArgsData))] + public async Task ValidateEventArgumentTypes_RaisesScenarios_VerifiesDiagnostics( + string referenceAssemblyGroup, + string @namespace, + string raisesCall) + { + await RaisesVerifier.VerifyAnalyzerAsync( + CreateRaisesTestCode(@namespace, raisesCall), + referenceAssemblyGroup); + } - bool result = EventSyntaxExtensions.TryGetEventMethodArguments( - invocation, - model, - out ArgumentSyntax[] eventArguments, - out ITypeSymbol[] expectedParameterTypes, - (_, _) => (true, delegateType), - knownSymbols); + [Theory] + [MemberData(nameof(ManyParametersMatchData))] + [MemberData(nameof(ManyParametersMismatchData))] + [MemberData(nameof(RaiseWithEventNameData))] + public async Task ValidateEventArgumentTypes_RaiseScenarios_VerifiesDiagnostics( + string referenceAssemblyGroup, + string @namespace, + string raiseCall) + { + await RaiseVerifier.VerifyAnalyzerAsync( + CreateRaiseTestCode(@namespace, raiseCall), + referenceAssemblyGroup); + } - Assert.True(result); - Assert.Single(eventArguments); - Assert.Single(expectedParameterTypes); + private static string CreateRaisesTestCode(string @namespace, string raisesCall) + { + return $$""" + {{@namespace}} + using Moq; + using System; + + internal class CustomArgs : EventArgs + { + public string Value { get; set; } + } + + internal delegate void TwoParamDelegate(string a, int b); + + internal delegate void NoParamDelegate(); + + internal interface ITestInterface + { + void Submit(); + event Action StringEvent; + event Action NumberEvent; + event Action DoubleEvent; + event EventHandler CustomEvent; + event Action SimpleEvent; + event TwoParamDelegate TwoParamDelegate; + event NoParamDelegate NoParamDelegate; + } + + internal class UnitTest + { + private void Test() + { + var mockProvider = new Mock(); + {{raisesCall}} + } + } + """; } - // ValidateEventArgumentTypes requires SyntaxNodeAnalysisContext, which has no public - // constructor and requires internal analyzer infrastructure to construct. These methods - // are tested indirectly through RaiseEventArgumentsShouldMatchEventSignatureAnalyzerTests - // and RaisesEventArgumentsShouldMatchEventSignatureAnalyzerTests, which exercise all - // branching logic (too few args, too many args, wrong type, matching types, with/without - // eventName). -#pragma warning disable ECS0900 // Boxing needed to cast to IEventSymbol from GetDeclaredSymbol - private static ITypeSymbol GetEventFieldType(string code, string eventName) + private static string CreateRaiseTestCode(string @namespace, string raiseCall) { - (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); - VariableDeclaratorSyntax variable = tree.GetRoot() - .DescendantNodes() - .OfType() - .First(v => v.Parent?.Parent is EventFieldDeclarationSyntax && - string.Equals(v.Identifier.Text, eventName, StringComparison.Ordinal)); - IEventSymbol eventSymbol = (IEventSymbol)model.GetDeclaredSymbol(variable)!; - return eventSymbol.Type; + return $$""" + {{@namespace}} + using Moq; + using System; + + internal class MyOptions { } + internal class Incorrect { } + + internal interface IOptionsProvider + { + event Action StringOptionsChanged; + event Action NumberChanged; + event Action OptionsChanged; + event Action SimpleEvent; + event Action DoubleChanged; + event Action MultiParamEvent; + } + + internal class UnitTest + { + private void Test() + { + var mockProvider = new Mock(); + {{raiseCall}} + } + } + """; } +#pragma warning disable ECS0900 // Boxing needed to cast to IEventSymbol from GetDeclaredSymbol private static (ITypeSymbol EventType, KnownSymbols KnownSymbols) GetEventFieldTypeWithKnownSymbols( string code, string eventName) diff --git a/tests/Moq.Analyzers.Test/Common/InvocationExpressionSyntaxExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/InvocationExpressionSyntaxExtensionsTests.cs index 0bc2cdd06..4476adbee 100644 --- a/tests/Moq.Analyzers.Test/Common/InvocationExpressionSyntaxExtensionsTests.cs +++ b/tests/Moq.Analyzers.Test/Common/InvocationExpressionSyntaxExtensionsTests.cs @@ -121,104 +121,4 @@ public void FindMockedMemberExpressionFromSetupMethod_EmptyArgumentList_ReturnsN Assert.Null(result); } - - [Fact] - public async Task IsRaisesMethodCall_ValidRaisesCall_ReturnsTrue() - { - const string code = @" -using System; -using Moq; - -public interface IService -{ - event EventHandler MyEvent; - void DoWork(); -} - -public class C -{ - public void M() - { - var mock = new Mock(); - mock.Setup(x => x.DoWork()).Raises(x => x.MyEvent += null, EventArgs.Empty); - } -}"; - (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); - SyntaxNode root = await tree.GetRootAsync(); - MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); - - // Find the Raises invocation - InvocationExpressionSyntax raisesInvocation = root - .DescendantNodes().OfType() - .First(i => i.Expression is MemberAccessExpressionSyntax ma - && string.Equals(ma.Name.Identifier.Text, "Raises", StringComparison.Ordinal)); - - bool result = raisesInvocation.IsRaisesMethodCall(model, knownSymbols); - - Assert.True(result); - } - - [Fact] - public async Task IsRaisesMethodCall_NonRaisesMethod_ReturnsFalse() - { - const string code = @" -using System; -using Moq; - -public interface IService -{ - void DoWork(); -} - -public class C -{ - public void M() - { - var mock = new Mock(); - mock.Setup(x => x.DoWork()); - } -}"; - (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); - SyntaxNode root = await tree.GetRootAsync(); - MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); - - // Find the Setup invocation (not Raises) - InvocationExpressionSyntax setupInvocation = root - .DescendantNodes().OfType() - .First(i => i.Expression is MemberAccessExpressionSyntax ma - && string.Equals(ma.Name.Identifier.Text, "Setup", StringComparison.Ordinal)); - - bool result = setupInvocation.IsRaisesMethodCall(model, knownSymbols); - - Assert.False(result); - } - - [Fact] - public async Task IsRaisesMethodCall_ExpressionNotMemberAccess_ReturnsFalse() - { - // A direct method call (not member access) such as a local function call - const string code = @" -using System; -using Moq; - -public class C -{ - public void M() - { - DoSomething(); - } - - private static void DoSomething() { } -}"; - (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); - SyntaxNode root = await tree.GetRootAsync(); - MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); - - InvocationExpressionSyntax invocation = root - .DescendantNodes().OfType().First(); - - bool result = invocation.IsRaisesMethodCall(model, knownSymbols); - - Assert.False(result); - } } diff --git a/tests/Moq.Analyzers.Test/Common/IsTaskOrValueResultPropertyTests.cs b/tests/Moq.Analyzers.Test/Common/IsTaskOrValueResultPropertyTests.cs new file mode 100644 index 000000000..b6f97c927 --- /dev/null +++ b/tests/Moq.Analyzers.Test/Common/IsTaskOrValueResultPropertyTests.cs @@ -0,0 +1,254 @@ +using Moq.Analyzers.Common.WellKnown; +using Moq.Analyzers.Test.Helpers; + +namespace Moq.Analyzers.Test.Common; + +public class IsTaskOrValueResultPropertyTests +{ +#pragma warning disable ECS1300 // Static field init is simpler than static constructor for single field + private static readonly MetadataReference SystemThreadingTasksReference = + MetadataReference.CreateFromFile(typeof(System.Threading.Tasks.Task).Assembly.Location); +#pragma warning restore ECS1300 + + private static MetadataReference[] CoreReferencesWithTasks => + [CompilationHelper.CorlibReference, CompilationHelper.SystemRuntimeReference, SystemThreadingTasksReference, CompilationHelper.SystemLinqReference]; + + // Positive cases: Result property on Task and ValueTask should return true. + [Theory] + [InlineData("Task t = Task.FromResult(42); int r = t.Result;", "Result")] + [InlineData("Task t = Task.FromResult(\"hello\"); string r = t.Result;", "Result")] + public void IsTaskOrValueResultProperty_TaskGenericResult_ReturnsTrue(string statement, string propertyName) + { + string code = $@" +using System.Threading.Tasks; +public class C +{{ + public void M() + {{ + {statement} + }} +}}"; + (IPropertySymbol prop, MoqKnownSymbols knownSymbols) = GetPropertyFromMemberAccess(code, propertyName); + Assert.True(((ISymbol)prop).IsTaskOrValueResultProperty(knownSymbols)); + } + + [Theory] + [InlineData("int", "42")] + [InlineData("string", "\"hello\"")] + public void IsTaskOrValueResultProperty_ValueTaskGenericResult_ReturnsTrue(string typeArg, string value) + { + string code = $@" +using System.Threading.Tasks; +public class C +{{ + public ValueTask<{typeArg}> GetValue() => new ValueTask<{typeArg}>({value}); + public void M() + {{ + var vt = GetValue(); + {typeArg} r = vt.Result; + }} +}}"; + (IPropertySymbol prop, MoqKnownSymbols knownSymbols) = GetPropertyFromMemberAccess(code, "Result"); + Assert.True(((ISymbol)prop).IsTaskOrValueResultProperty(knownSymbols)); + } + + // Negative cases: non-Result properties, non-Task types, and non-property symbols. + [Fact] + public void IsTaskOrValueResultProperty_MethodSymbol_ReturnsFalse() + { + string code = @" +using System.Threading.Tasks; +public class C +{ + public void M() + { + Task t = Task.FromResult(42); + } +}"; + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + InvocationExpressionSyntax invocation = tree.GetRoot() + .DescendantNodes().OfType().First(); + ISymbol? methodSymbol = model.GetSymbolInfo(invocation).Symbol; + + Assert.NotNull(methodSymbol); + Assert.IsAssignableFrom(methodSymbol); + Assert.False(methodSymbol.IsTaskOrValueResultProperty(knownSymbols)); + } + + [Fact] + public void IsTaskOrValueResultProperty_FieldSymbol_ReturnsFalse() + { + string code = @" +using System.Threading.Tasks; +public class C +{ + public int Result; + public void M() + { + int r = this.Result; + } +}"; + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + MemberAccessExpressionSyntax memberAccess = tree.GetRoot() + .DescendantNodes().OfType() + .First(m => string.Equals(m.Name.Identifier.Text, "Result", StringComparison.Ordinal)); + ISymbol? symbol = model.GetSymbolInfo(memberAccess).Symbol; + + Assert.NotNull(symbol); + Assert.IsAssignableFrom(symbol); + Assert.False(symbol.IsTaskOrValueResultProperty(knownSymbols)); + } + + [Fact] + public void IsTaskOrValueResultProperty_ResultPropertyOnCustomType_ReturnsFalse() + { + string code = @" +using System.Threading.Tasks; +public class MyClass +{ + public int Result { get; set; } +} +public class C +{ + public void M() + { + var obj = new MyClass(); + int r = obj.Result; + } +}"; + (IPropertySymbol prop, MoqKnownSymbols knownSymbols) = GetPropertyFromMemberAccess(code, "Result"); + Assert.False(((ISymbol)prop).IsTaskOrValueResultProperty(knownSymbols)); + } + + [Fact] + public void IsTaskOrValueResultProperty_NonResultPropertyOnTask_ReturnsFalse() + { + string code = @" +using System.Threading.Tasks; +public class C +{ + public void M() + { + Task t = Task.FromResult(42); + bool b = t.IsCompleted; + } +}"; + (IPropertySymbol prop, MoqKnownSymbols knownSymbols) = GetPropertyFromMemberAccess(code, "IsCompleted"); + Assert.False(((ISymbol)prop).IsTaskOrValueResultProperty(knownSymbols)); + } + + [Fact] + public void IsTaskOrValueResultProperty_NonGenericTaskProperty_ReturnsFalse() + { + // Non-generic Task has no Result property. Use IsCompleted as a stand-in + // to verify that properties on non-generic Task are not flagged. + string code = @" +using System.Threading.Tasks; +public class C +{ + public void M() + { + Task t = Task.CompletedTask; + bool b = t.IsCompleted; + } +}"; + (IPropertySymbol prop, MoqKnownSymbols knownSymbols) = GetPropertyFromMemberAccess(code, "IsCompleted"); + Assert.False(((ISymbol)prop).IsTaskOrValueResultProperty(knownSymbols)); + } + + [Fact] + public void IsTaskOrValueResultProperty_CustomClassWithResultProperty_ReturnsFalse() + { + string code = @" +using System.Threading.Tasks; +public class OperationResult +{ + public T Result { get; set; } +} +public class C +{ + public void M() + { + var op = new OperationResult(); + int r = op.Result; + } +}"; + (IPropertySymbol prop, MoqKnownSymbols knownSymbols) = GetPropertyFromMemberAccess(code, "Result"); + Assert.False(((ISymbol)prop).IsTaskOrValueResultProperty(knownSymbols)); + } + + // IsGenericResultProperty negative cases: non-generic types, wrong property names, unrelated generic types. + [Fact] + public void IsGenericResultProperty_NonGenericContainingType_ReturnsFalse() + { + string code = @" +using System.Threading.Tasks; +public class NonGenericHolder +{ + public int Result { get; set; } +} +public class C +{ + public void M() + { + var holder = new NonGenericHolder(); + int r = holder.Result; + } +}"; + (IPropertySymbol prop, MoqKnownSymbols knownSymbols) = GetPropertyFromMemberAccess(code, "Result"); + Assert.False(((ISymbol)prop).IsTaskOrValueResultProperty(knownSymbols)); + } + + [Fact] + public void IsGenericResultProperty_PropertyNotNamedResult_ReturnsFalse() + { + string code = @" +using System.Threading.Tasks; +public class C +{ + public void M() + { + Task t = Task.FromResult(42); + var s = t.Status; + } +}"; + (IPropertySymbol prop, MoqKnownSymbols knownSymbols) = GetPropertyFromMemberAccess(code, "Status"); + Assert.False(((ISymbol)prop).IsTaskOrValueResultProperty(knownSymbols)); + } + + [Fact] + public void IsGenericResultProperty_ResultOnUnrelatedGenericType_ReturnsFalse() + { + string code = @" +using System.Threading.Tasks; +public class MyWrapper +{ + public T Result { get; set; } +} +public class C +{ + public void M() + { + var wrapper = new MyWrapper(); + int r = wrapper.Result; + } +}"; + (IPropertySymbol prop, MoqKnownSymbols knownSymbols) = GetPropertyFromMemberAccess(code, "Result"); + Assert.False(((ISymbol)prop).IsTaskOrValueResultProperty(knownSymbols)); + } + + private static (IPropertySymbol Property, MoqKnownSymbols KnownSymbols) GetPropertyFromMemberAccess( + string code, + string propertyName) + { + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + MemberAccessExpressionSyntax memberAccess = tree.GetRoot() + .DescendantNodes().OfType() + .First(m => string.Equals(m.Name.Identifier.Text, propertyName, StringComparison.Ordinal)); + IPropertySymbol prop = (IPropertySymbol)model.GetSymbolInfo(memberAccess).Symbol!; + return (prop, knownSymbols); + } +} diff --git a/tests/Moq.Analyzers.Test/Common/SemanticModelExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/SemanticModelExtensionsTests.cs index 11fd59c67..fc7fe2a67 100644 --- a/tests/Moq.Analyzers.Test/Common/SemanticModelExtensionsTests.cs +++ b/tests/Moq.Analyzers.Test/Common/SemanticModelExtensionsTests.cs @@ -682,15 +682,29 @@ public void M() mock.Setup(x => x.Bar()).Raises(x => x.MyEvent += null, EventArgs.Empty); } }"; - (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); - MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); - SyntaxNode root = await tree.GetRootAsync(); - InvocationExpressionSyntax raisesInvocation = root - .DescendantNodes().OfType() - .First(i => i.Expression is MemberAccessExpressionSyntax ma - && string.Equals(ma.Name.Identifier.Text, "Raises", StringComparison.Ordinal)); - bool result = model.IsRaisesInvocation(raisesInvocation, knownSymbols); + bool result = await IsRaisesInvocationForMemberAccessAsync(code, "Raises"); + + Assert.True(result); + } + + [Fact] + public async Task IsRaisesInvocation_RaisesOnReturnsChain_ReturnsTrue() + { + const string code = @" +using Moq; +using System; +public interface IFoo { event EventHandler MyEvent; int Bar(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Bar()).Returns(42).Raises(x => x.MyEvent += null, EventArgs.Empty); + } +}"; + + bool result = await IsRaisesInvocationForMemberAccessAsync(code, "Raises"); Assert.True(result); } @@ -721,6 +735,46 @@ public void M() Assert.False(result); } + [Fact] + public async Task IsRaisesInvocation_SetupMethodName_ReturnsFalse() + { + const string code = @" +using Moq; +public interface IFoo { void Bar(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Bar()); + } +}"; + + bool result = await IsRaisesInvocationForMemberAccessAsync(code, "Setup"); + + Assert.False(result); + } + + [Fact] + public async Task IsRaisesInvocation_CallbackMethodName_ReturnsFalse() + { + const string code = @" +using Moq; +public interface IFoo { void Bar(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Bar()).Callback(() => { }); + } +}"; + + bool result = await IsRaisesInvocationForMemberAccessAsync(code, "Callback"); + + Assert.False(result); + } + [Fact] public async Task IsRaisesInvocation_NonMoqMethodNamedRaises_ReturnsFalse() { @@ -738,15 +792,32 @@ public void M() obj.Raises(); } }"; - (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); - MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); - SyntaxNode root = await tree.GetRootAsync(); - InvocationExpressionSyntax raisesInvocation = root - .DescendantNodes().OfType() - .First(i => i.Expression is MemberAccessExpressionSyntax ma - && string.Equals(ma.Name.Identifier.Text, "Raises", StringComparison.Ordinal)); - bool result = model.IsRaisesInvocation(raisesInvocation, knownSymbols); + bool result = await IsRaisesInvocationForMemberAccessAsync(code, "Raises"); + + Assert.False(result); + } + + [Fact] + public async Task IsRaisesInvocation_NonMoqMethodNamedRaisesAsync_ReturnsFalse() + { + const string code = @" +using Moq; +using System.Threading.Tasks; +public class MyClass +{ + public Task RaisesAsync() => Task.CompletedTask; +} +public class C +{ + public void M() + { + var obj = new MyClass(); + obj.RaisesAsync(); + } +}"; + + bool result = await IsRaisesInvocationForMemberAccessAsync(code, "RaisesAsync"); Assert.False(result); } @@ -770,6 +841,18 @@ private static (SemanticModel Model, ITypeSymbol FirstType, ITypeSymbol SecondTy return (model, firstSymbol.Type, secondSymbol.Type); } + private static async Task IsRaisesInvocationForMemberAccessAsync(string code, string methodName) + { + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code).ConfigureAwait(false); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + SyntaxNode root = await tree.GetRootAsync().ConfigureAwait(false); + InvocationExpressionSyntax invocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, methodName, StringComparison.Ordinal)); + return model.IsRaisesInvocation(invocation, knownSymbols); + } + private static (SemanticModel Model, ExpressionSyntax Lambda) GetLambdaFromVariableInitializer( string code, string variableName) diff --git a/tests/Moq.Analyzers.Test/IsRaisesMethodTests.cs b/tests/Moq.Analyzers.Test/IsRaisesMethodTests.cs index f957898eb..9c5d492b6 100644 --- a/tests/Moq.Analyzers.Test/IsRaisesMethodTests.cs +++ b/tests/Moq.Analyzers.Test/IsRaisesMethodTests.cs @@ -1,9 +1,8 @@ namespace Moq.Analyzers.Test; /// -/// Tests for the symbol-based Raises method detection functionality. -/// These tests verify that IsRaisesMethodCall and IsRaisesInvocation correctly identify -/// valid and invalid Raises patterns using symbol-based detection. +/// Verifies that symbol-based Raises detection does not produce +/// false-positive diagnostics on valid code patterns. /// public class IsRaisesMethodTests { @@ -38,7 +37,6 @@ public static IEnumerable InvalidRaisesPatterns() [MemberData(nameof(ValidRaisesPatterns))] public async Task ShouldDetectValidRaisesPatterns(string referenceAssemblyGroup, string @namespace, string raisesCall) { - // Test that valid Raises patterns don't trigger unwanted diagnostics static string Template(string ns, string call) => $$""" {{ns}} @@ -71,7 +69,6 @@ public void TestMethod() [MemberData(nameof(InvalidRaisesPatterns))] public async Task ShouldNotDetectInvalidRaisesPatterns(string referenceAssemblyGroup, string @namespace, string nonRaisesCall) { - // Test that non-Raises patterns don't get false positives static string Template(string ns, string call) => $$""" {{ns}} @@ -122,8 +119,7 @@ public class TestSimpleRaises public void TestMethod() { var mock = new Mock(MockBehavior.Strict); - - // Simple Raises call + mock.Setup(x => x.ProcessData()) .Raises(x => x.SimpleEvent += null, "data"); } diff --git a/tests/Moq.Analyzers.Test/RaisesEventArgumentsShouldMatchEventSignatureAnalyzerTests.cs b/tests/Moq.Analyzers.Test/RaisesEventArgumentsShouldMatchEventSignatureAnalyzerTests.cs index 10a1dbb8c..ff2c09683 100644 --- a/tests/Moq.Analyzers.Test/RaisesEventArgumentsShouldMatchEventSignatureAnalyzerTests.cs +++ b/tests/Moq.Analyzers.Test/RaisesEventArgumentsShouldMatchEventSignatureAnalyzerTests.cs @@ -25,6 +25,24 @@ public static IEnumerable ValidTestData() }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } + public static IEnumerable ValidRaisesOnReturnsChainTestData() + { + return new object[][] + { + // Valid: Raises on Returns chain with Action event with string argument + ["""mockProvider.Setup(x => x.GetValue()).Returns(1).Raises(x => x.StringEvent += null, "test");"""], + + // Valid: Raises on Returns chain with Action event with int argument + ["""mockProvider.Setup(x => x.GetValue()).Returns(1).Raises(x => x.NumberEvent += null, 42);"""], + + // Valid: Raises on Returns chain with EventHandler event with correct args + ["""mockProvider.Setup(x => x.GetValue()).Returns(1).Raises(x => x.CustomEvent += null, new CustomArgs());"""], + + // Valid: Raises on Returns chain with Action event with no parameters + ["""mockProvider.Setup(x => x.GetValue()).Returns(1).Raises(x => x.SimpleEvent += null);"""], + }.WithNamespaces().WithNewMoqReferenceAssemblyGroups(); + } + public static IEnumerable InvalidTestData() { return new object[][] @@ -46,6 +64,21 @@ public static IEnumerable InvalidTestData() }.WithNamespaces().WithMoqReferenceAssemblyGroups(); } + public static IEnumerable InvalidRaisesOnReturnsChainTestData() + { + return new object[][] + { + // Invalid: Raises on Returns chain with Action event with int argument + ["""mockProvider.Setup(x => x.GetValue()).Returns(1).Raises(x => x.StringEvent += null, {|Moq1204:42|});"""], + + // Invalid: Raises on Returns chain with Action event with string argument + ["""mockProvider.Setup(x => x.GetValue()).Returns(1).Raises(x => x.NumberEvent += null, {|Moq1204:"test"|});"""], + + // Invalid: Raises on Returns chain with EventHandler event with wrong type + ["""mockProvider.Setup(x => x.GetValue()).Returns(1).Raises(x => x.CustomEvent += null, {|Moq1204:"wrong"|});"""], + }.WithNamespaces().WithNewMoqReferenceAssemblyGroups(); + } + [Theory] [MemberData(nameof(ValidTestData))] public async Task ShouldNotReportDiagnosticForValidRaisesArguments(string referenceAssemblyGroup, string @namespace, string raisesCall) @@ -66,6 +99,7 @@ internal class CustomArgs : EventArgs internal interface ITestInterface { void Submit(); + int GetValue(); event Action StringEvent; event Action NumberEvent; event EventHandler CustomEvent; @@ -105,6 +139,87 @@ internal class CustomArgs : EventArgs internal interface ITestInterface { void Submit(); + int GetValue(); + event Action StringEvent; + event Action NumberEvent; + event EventHandler CustomEvent; + event Action SimpleEvent; + event MyDelegate CustomDelegate; + } + + internal class UnitTest + { + private void Test() + { + var mockProvider = new Mock(); + {{raisesCall}} + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(ValidRaisesOnReturnsChainTestData))] + public async Task ShouldNotReportDiagnosticForValidRaisesOnReturnsChainArguments(string referenceAssemblyGroup, string @namespace, string raisesCall) + { + await Verifier.VerifyAnalyzerAsync( + $$""" + {{@namespace}} + using Moq; + using System; + + internal class CustomArgs : EventArgs + { + public string Value { get; set; } + } + + internal delegate void MyDelegate(string value); + + internal interface ITestInterface + { + void Submit(); + int GetValue(); + event Action StringEvent; + event Action NumberEvent; + event EventHandler CustomEvent; + event Action SimpleEvent; + event MyDelegate CustomDelegate; + } + + internal class UnitTest + { + private void Test() + { + var mockProvider = new Mock(); + {{raisesCall}} + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(InvalidRaisesOnReturnsChainTestData))] + public async Task ShouldReportDiagnosticForInvalidRaisesOnReturnsChainArguments(string referenceAssemblyGroup, string @namespace, string raisesCall) + { + await Verifier.VerifyAnalyzerAsync( + $$""" + {{@namespace}} + using Moq; + using System; + + internal class CustomArgs : EventArgs + { + public string Value { get; set; } + } + + internal delegate void MyDelegate(string value); + + internal interface ITestInterface + { + void Submit(); + int GetValue(); event Action StringEvent; event Action NumberEvent; event EventHandler CustomEvent; @@ -132,4 +247,83 @@ await Verifier.VerifyAnalyzerAsync( DoppelgangerTestHelper.CreateTestCode(mockCode), ReferenceAssemblyCatalog.Net80WithNewMoq); } + + [Fact] + public async Task ShouldNotTriggerOnUserDefinedRaisesMethod() + { + await Verifier.VerifyAnalyzerAsync( + """ + using System; + + internal class MyEventEmitter + { + public void Raises(string eventName, EventArgs args) { } + public void RaisesAsync(string eventName, EventArgs args) { } + } + + internal class UnitTest + { + private void Test() + { + var emitter = new MyEventEmitter(); + emitter.Raises("Click", EventArgs.Empty); + emitter.RaisesAsync("Click", EventArgs.Empty); + } + } + """, + ReferenceAssemblyCatalog.Net80WithNewMoq); + } + + [Fact] + public async Task ShouldNotTriggerOnUserDefinedRaisesExtensionMethod() + { + await Verifier.VerifyAnalyzerAsync( + """ + using System; + + internal static class MyExtensions + { + public static void Raises(this object obj, string name) { } + } + + internal class UnitTest + { + private void Test() + { + var target = new object(); + target.Raises("test"); + } + } + """, + ReferenceAssemblyCatalog.Net80WithNewMoq); + } + + [Fact] + public async Task ShouldNotTriggerOnInterfaceWithRaisesMethod() + { + await Verifier.VerifyAnalyzerAsync( + """ + using System; + + internal interface IEventRaiser + { + void Raises(object sender, EventArgs e); + } + + internal class MyRaiser : IEventRaiser + { + public void Raises(object sender, EventArgs e) { } + } + + internal class UnitTest + { + private void Test() + { + IEventRaiser raiser = new MyRaiser(); + raiser.Raises(this, EventArgs.Empty); + } + } + """, + ReferenceAssemblyCatalog.Net80WithNewMoq); + } }