Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
48cbc87
fix: replace string-based detection with symbol-based detection (#981)
rjmurillo Mar 7, 2026
5b781d9
refactor: consolidate similar single-type-arg event tests into Theory
rjmurillo Mar 7, 2026
36ac956
fix: remove redundant IsRaisesMethodCall duplicating IsRaisesInvocation
rjmurillo Mar 7, 2026
85b8918
fix: rename misleading test for multi-arg Action delegate
rjmurillo Mar 7, 2026
175ff77
docs: document why residual string checks are acceptable optimizations
rjmurillo Mar 7, 2026
60634e6
refactor: eliminate duplicated event name extraction and unify switch…
rjmurillo Mar 8, 2026
83549fb
fix(test): narrow IsRaisesMethodTests summary to match actual coverage
rjmurillo Mar 8, 2026
7fb8ef3
refactor: simplify code in PR #1030
rjmurillo Mar 8, 2026
5c5b109
refactor: inline pass-through methods in EventSyntaxExtensions
rjmurillo Mar 8, 2026
cc5fb64
perf: add fast-path name check in IsRaisesInvocation
rjmurillo Mar 8, 2026
afae7f1
refactor: simplify code in PR #1030
rjmurillo Mar 8, 2026
2fc2256
fix: use XML doc tags for BCL type references in EventSyntaxExtensions
rjmurillo Mar 8, 2026
db79fa8
test: add comprehensive unit tests for IsTaskOrValueResultProperty an…
rjmurillo Mar 8, 2026
f3b3055
test: add unit tests for CreateEventDiagnostic method
rjmurillo Mar 8, 2026
b3e119d
test: add false-positive tests for user-defined Raises methods
rjmurillo Mar 8, 2026
36939c2
test: add IsRaisesInvocation and Raises analyzer coverage
rjmurillo Mar 8, 2026
493fc41
test: add comprehensive ValidateEventArgumentTypes integration tests
rjmurillo Mar 8, 2026
0411d54
refactor: deduplicate code blocks in PR #1030
rjmurillo Mar 8, 2026
8fa1f52
merge: resolve conflicts with main branch
rjmurillo Mar 8, 2026
9075a13
fix: remove redundant null coalescing on non-nullable return value
rjmurillo Mar 8, 2026
ef85c31
refactor: eliminate DRY violations in event analyzer pair
rjmurillo Mar 8, 2026
1e301b0
fix: update package snapshot files to include THIRD-PARTY-NOTICES.TXT
rjmurillo Mar 8, 2026
63a7d0d
Merge remote-tracking branch 'origin/main' into fix/981-remove-string…
rjmurillo Mar 9, 2026
4cafcc8
Merge branch 'main' into fix/981-remove-string-detection
rjmurillo Mar 9, 2026
df575ce
refactor: eliminate similar-code violations in source and tests
rjmurillo Mar 9, 2026
6899351
fix: eliminate closure allocations, normalize delegate style, remove …
rjmurillo Mar 9, 2026
4977c9c
fix: initialize static fields in static constructor (ECS1300)
rjmurillo Mar 9, 2026
373a1e8
fix: suppress ECS1300 to resolve conflict with ECS1200
rjmurillo Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
cursor[bot] marked this conversation as resolved.
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);
}
}
6 changes: 3 additions & 3 deletions src/Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
166 changes: 108 additions & 58 deletions src/Common/EventSyntaxExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}

/// <summary>
/// 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.
/// </summary>
/// <param name="eventType">The event delegate type.</param>
/// <param name="eventType">The event delegate type to analyze.</param>
/// <param name="knownSymbols">Known symbols for type checking.</param>
/// <returns>An array of parameter types expected by the event delegate.</returns>
internal static ITypeSymbol[] GetEventParameterTypes(ITypeSymbol eventType, KnownSymbols? knownSymbols = null)
/// <returns>
/// An array of parameter types expected by the event delegate:
/// - For <see cref="System.Action"/> delegates: Returns all generic type arguments
/// - For <see cref="System.EventHandler{T}"/> delegates: Returns the single generic argument <c>T</c>
/// - For custom delegates: Returns parameters from the <c>Invoke</c> method
/// - For non-delegate types: Returns an empty array.
/// </returns>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 ?? [];
}
Expand All @@ -97,7 +90,7 @@ internal static bool TryGetEventMethodArguments(
out ArgumentSyntax[] eventArguments,
out ITypeSymbol[] expectedParameterTypes,
Func<SemanticModel, ExpressionSyntax, (bool Success, ITypeSymbol? EventType)> eventTypeExtractor,
KnownSymbols? knownSymbols = null)
KnownSymbols knownSymbols)
{
eventArguments = [];
expectedParameterTypes = [];
Expand All @@ -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++)
Expand All @@ -134,34 +123,93 @@ internal static bool TryGetEventMethodArguments(
return true;
}

/// <summary>
/// Extracts event arguments from an event method invocation using the standard
/// lambda-based event type extraction pattern shared by Raise and Raises analyzers.
/// </summary>
/// <param name="invocation">The method invocation.</param>
/// <param name="semanticModel">The semantic model.</param>
/// <param name="knownSymbols">Known symbols for type checking.</param>
/// <param name="eventArguments">The extracted event arguments.</param>
/// <param name="expectedParameterTypes">The expected parameter types.</param>
/// <returns><see langword="true" /> if arguments were successfully extracted; otherwise, <see langword="false" />.</returns>
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);
}

/// <summary>
/// Extracts the event name from the first argument (event selector lambda) of an invocation.
/// </summary>
/// <param name="invocation">The method invocation containing the lambda selector.</param>
/// <param name="semanticModel">The semantic model.</param>
/// <returns>The event name if found; otherwise "event" as a fallback.</returns>
internal static string GetEventNameFromSelector(InvocationExpressionSyntax invocation, SemanticModel semanticModel)
{
SeparatedSyntaxList<ArgumentSyntax> arguments = invocation.ArgumentList.Arguments;
if (arguments.Count < 1)
{
return "event";
}

ExpressionSyntax eventSelector = arguments[0].Expression;

return semanticModel.TryGetEventNameFromLambdaSelector(eventSelector, out string? eventName)
? eventName!
: "event";
}

/// <summary>
/// Creates a <see cref="Diagnostic"/> for an event-related rule violation.
/// When <paramref name="eventName"/> is provided, it is passed as a message format argument.
/// When <paramref name="eventName"/> is <see langword="null"/>, no message arguments are included.
/// </summary>
/// <param name="location">The source location for the diagnostic.</param>
/// <param name="rule">The diagnostic descriptor for the rule.</param>
/// <param name="eventName">The event name to include in the message, or <see langword="null"/>.</param>
/// <returns>A new <see cref="Diagnostic"/> instance.</returns>
internal static Diagnostic CreateEventDiagnostic(Location location, DiagnosticDescriptor rule, string? eventName)
{
return eventName != null
? location.CreateDiagnostic(rule, eventName)
: location.CreateDiagnostic(rule);
}

/// <summary>
/// Attempts to get parameter types from Action delegate types.
/// </summary>
/// <param name="namedType">The named type symbol to check.</param>
/// <param name="knownSymbols">Optional known symbols for enhanced type checking.</param>
/// <param name="knownSymbols">Known symbols for type checking.</param>
/// <returns>Parameter types if this is an Action delegate; otherwise null.</returns>
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;
}

/// <summary>
/// Attempts to get parameter types from EventHandler delegate types.
/// </summary>
/// <param name="namedType">The named type symbol to check.</param>
/// <param name="knownSymbols">Optional known symbols for enhanced type checking.</param>
/// <param name="knownSymbols">Known symbols for type checking.</param>
/// <returns>Parameter types if this is an EventHandler delegate; otherwise null.</returns>
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]];
}
Expand All @@ -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<IParameterSymbol> 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;
}
}
Loading
Loading