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