diff --git a/src/Common/EventSyntaxExtensions.cs b/src/Common/EventSyntaxExtensions.cs index 4549b8ee6..368a028cc 100644 --- a/src/Common/EventSyntaxExtensions.cs +++ b/src/Common/EventSyntaxExtensions.cs @@ -211,42 +211,79 @@ private static bool TryGetEventMethodArgumentsInternal( return true; } + /// + /// 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 to analyze. + /// Known symbols for enhanced type checking and recognition. + /// + /// An array of parameter types expected by the event delegate: + /// - For Action delegates: Returns all generic type arguments + /// - For EventHandler<T> delegates: Returns the single generic argument T + /// - For custom delegates: Returns parameters from the Invoke method + /// - For non-delegate types: Returns empty array. + /// private static ITypeSymbol[] GetEventParameterTypesInternal(ITypeSymbol eventType, KnownSymbols? knownSymbols) { - // For delegates like Action, we need to get the generic type arguments - if (eventType is INamedTypeSymbol namedType) + if (eventType is not INamedTypeSymbol namedType) { - // Handle Action delegates - if (knownSymbols != null && namedType.IsActionDelegate(knownSymbols)) - { - return namedType.TypeArguments.ToArray(); - } + return []; + } - if (knownSymbols == null && IsActionDelegate(namedType)) - { - return namedType.TypeArguments.ToArray(); - } + // Try different delegate type handlers in order of specificity + ITypeSymbol[]? parameterTypes = TryGetActionDelegateParameters(namedType, knownSymbols) ?? + TryGetEventHandlerDelegateParameters(namedType, knownSymbols) ?? + TryGetCustomDelegateParameters(namedType); - // Handle EventHandler - expects single argument of type T (not the sender/args pattern) - if (knownSymbols != null && namedType.IsEventHandlerDelegate(knownSymbols)) - { - return [namedType.TypeArguments[0]]; - } + return parameterTypes ?? []; + } - if (knownSymbols == null && IsEventHandlerDelegate(namedType)) - { - return [namedType.TypeArguments[0]]; - } + /// + /// Attempts to get parameter types from Action delegate types. + /// + /// The named type symbol to check. + /// Optional known symbols for enhanced type checking. + /// Parameter types if this is an Action delegate; otherwise null. + private static ITypeSymbol[]? TryGetActionDelegateParameters(INamedTypeSymbol namedType, KnownSymbols? knownSymbols) + { + bool isActionDelegate = knownSymbols != null + ? namedType.IsActionDelegate(knownSymbols) + : IsActionDelegate(namedType); - // Handle custom delegates by getting the Invoke method parameters - IMethodSymbol? invokeMethod = namedType.DelegateInvokeMethod; - if (invokeMethod != null) - { - return invokeMethod.Parameters.Select(p => p.Type).ToArray(); - } + return isActionDelegate ? 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. + /// Parameter types if this is an EventHandler delegate; otherwise null. + private static ITypeSymbol[]? TryGetEventHandlerDelegateParameters(INamedTypeSymbol namedType, KnownSymbols? knownSymbols) + { + bool isEventHandlerDelegate = knownSymbols != null + ? namedType.IsEventHandlerDelegate(knownSymbols) + : IsEventHandlerDelegate(namedType); + + if (isEventHandlerDelegate && namedType.TypeArguments.Length > 0) + { + return [namedType.TypeArguments[0]]; } - return []; + return null; + } + + /// + /// Attempts to get parameter types from custom delegate types using the Invoke method. + /// + /// The named type symbol to check. + /// Parameter types if this has a delegate Invoke method; otherwise null. + private static ITypeSymbol[]? TryGetCustomDelegateParameters(INamedTypeSymbol namedType) + { + IMethodSymbol? invokeMethod = namedType.DelegateInvokeMethod; + return invokeMethod?.Parameters.Select(p => p.Type).ToArray(); } private static bool IsActionDelegate(INamedTypeSymbol namedType) diff --git a/src/Common/ISymbolExtensions.cs b/src/Common/ISymbolExtensions.cs index 18d5dc2d8..dcb33a537 100644 --- a/src/Common/ISymbolExtensions.cs +++ b/src/Common/ISymbolExtensions.cs @@ -207,7 +207,7 @@ internal static bool IsMoqCallbackMethod(this ISymbol symbol, MoqKnownSymbols kn /// /// The symbol to check. /// The known symbols for type checking. - /// True if the symbol is a Raises or RaisesAsync method from Moq.Language.IRaiseable or IRaiseableAsync; otherwise false. + /// True if the symbol is a Raises or RaisesAsync method from Moq.Language interfaces; otherwise false. internal static bool IsMoqRaisesMethod(this ISymbol symbol, MoqKnownSymbols knownSymbols) { if (symbol is not IMethodSymbol methodSymbol) @@ -215,30 +215,91 @@ internal static bool IsMoqRaisesMethod(this ISymbol symbol, MoqKnownSymbols know return false; } - // Check if this method symbol matches any of the known Raises methods - // Try the ICallback and IReturns interfaces which are more likely to contain Raises - bool symbolBasedResult = symbol.IsInstanceOf(knownSymbols.ICallbackRaises) || - symbol.IsInstanceOf(knownSymbols.ICallback1Raises) || - symbol.IsInstanceOf(knownSymbols.ICallback2Raises) || - symbol.IsInstanceOf(knownSymbols.IReturnsRaises) || - symbol.IsInstanceOf(knownSymbols.IReturns1Raises) || - symbol.IsInstanceOf(knownSymbols.IReturns2Raises) || - symbol.IsInstanceOf(knownSymbols.IRaiseableRaises) || - symbol.IsInstanceOf(knownSymbols.IRaiseableAsyncRaisesAsync); - - if (symbolBasedResult) + // Primary: Use symbol-based detection for known Moq interfaces + if (IsKnownMoqRaisesMethod(symbol, knownSymbols)) { return true; } - // Fallback: Check if it's a Raises/RaisesAsync method on any Moq.Language interface - // This provides compatibility until the correct interface names are identified - string? containingTypeName = methodSymbol.ContainingType?.ToDisplayString(); + // TODO: Remove this fallback once symbol-based detection is complete + // This is a temporary safety net for cases where symbol resolution fails + // but should be replaced with comprehensive symbol-based approach + return IsLikelyMoqRaisesMethodByName(methodSymbol); + } + + /// + /// Checks if the symbol matches any of the known Moq Raises method symbols. + /// This method handles all supported Moq interfaces that provide Raises functionality. + /// + /// The symbol to check. + /// The known symbols for type checking. + /// True if the symbol matches a known Moq Raises method; otherwise false. + private static bool IsKnownMoqRaisesMethod(ISymbol symbol, MoqKnownSymbols knownSymbols) + { + return IsCallbackRaisesMethod(symbol, knownSymbols) || + IsReturnsRaisesMethod(symbol, knownSymbols) || + IsRaiseableMethod(symbol, knownSymbols); + } + + /// + /// Checks if the symbol is a Raises method from ICallback interfaces. + /// + /// The symbol to check. + /// The known symbols for type checking. + /// True if the symbol is a callback Raises method; otherwise false. + private static bool IsCallbackRaisesMethod(ISymbol symbol, MoqKnownSymbols knownSymbols) + { + return symbol.IsInstanceOf(knownSymbols.ICallbackRaises) || + symbol.IsInstanceOf(knownSymbols.ICallback1Raises) || + symbol.IsInstanceOf(knownSymbols.ICallback2Raises); + } + + /// + /// Checks if the symbol is a Raises method from IReturns interfaces. + /// + /// The symbol to check. + /// The known symbols for type checking. + /// True if the symbol is a returns Raises method; otherwise false. + private static bool IsReturnsRaisesMethod(ISymbol symbol, MoqKnownSymbols knownSymbols) + { + return symbol.IsInstanceOf(knownSymbols.IReturnsRaises) || + symbol.IsInstanceOf(knownSymbols.IReturns1Raises) || + symbol.IsInstanceOf(knownSymbols.IReturns2Raises); + } + + /// + /// Checks if the symbol is a Raises method from IRaiseable interfaces. + /// + /// The symbol to check. + /// The known symbols for type checking. + /// True if the symbol is a raiseable method; otherwise false. + private static bool IsRaiseableMethod(ISymbol symbol, MoqKnownSymbols knownSymbols) + { + return symbol.IsInstanceOf(knownSymbols.IRaiseableRaises) || + symbol.IsInstanceOf(knownSymbols.IRaiseableAsyncRaisesAsync); + } + + /// + /// TEMPORARY: Conservative fallback for Moq Raises method detection. + /// This should be removed once symbol-based detection is comprehensive. + /// Only matches methods that are clearly Moq Raises methods. + /// + /// The method symbol to check. + /// True if this is likely a Moq Raises method; otherwise false. + private static bool IsLikelyMoqRaisesMethodByName(IMethodSymbol methodSymbol) + { string methodName = methodSymbol.Name; - return (string.Equals(methodName, "Raises", StringComparison.Ordinal) || - string.Equals(methodName, "RaisesAsync", StringComparison.Ordinal)) && - containingTypeName?.Contains("Moq.Language", StringComparison.Ordinal) == true; + // Only match exact "Raises" or "RaisesAsync" method names + if (!string.Equals(methodName, "Raises", StringComparison.Ordinal) && + !string.Equals(methodName, "RaisesAsync", StringComparison.Ordinal)) + { + return false; + } + + // Must be in a type that contains "Moq" in its namespace to reduce false positives + string? containingTypeNamespace = methodSymbol.ContainingType?.ContainingNamespace?.ToDisplayString(); + return containingTypeNamespace?.StartsWith("Moq", StringComparison.Ordinal) == true; } ///