diff --git a/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs b/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs index 52a6f8555..4bc4a337c 100644 --- a/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs +++ b/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs @@ -1,4 +1,4 @@ -using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Operations; namespace Moq.Analyzers; @@ -20,6 +20,147 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(RegisterCompilationStartAction); } + /// + /// Extracts the mocked type name from the operation for use in diagnostic messages. + /// + /// The operation being analyzed. + /// The target method symbol. + /// The display name of the mocked type. + internal virtual string GetMockedTypeName(IOperation operation, IMethodSymbol target) + { + // For object creation (new Mock), get the type argument from the Mock type + if (operation is IObjectCreationOperation objectCreation + && objectCreation.Type is INamedTypeSymbol namedType + && namedType.TypeArguments.Length > 0) + { + return namedType.TypeArguments[0].ToDisplayString(); + } + + // For method invocation (Mock.Of), get the type argument from the method + if (operation is IInvocationOperation invocation && invocation.TargetMethod.TypeArguments.Length > 0) + { + return invocation.TargetMethod.TypeArguments[0].ToDisplayString(); + } + + // Try the containing type's type arguments (e.g. Mock.ctor) + if (target.ContainingType?.TypeArguments.Length > 0) + { + return target.ContainingType.TypeArguments[0].ToDisplayString(); + } + + return "T"; + } + + /// + /// Attempts to report a diagnostic for a MockBehavior parameter issue. + /// + /// The operation analysis context. + /// The method to check for MockBehavior parameter. + /// The known Moq symbols. + /// The diagnostic rule to report. + /// The type of edit for the code fix. + /// True if a diagnostic was reported; otherwise, false. + internal bool TryReportMockBehaviorDiagnostic( + OperationAnalysisContext context, + IMethodSymbol method, + MoqKnownSymbols knownSymbols, + DiagnosticDescriptor rule, + DiagnosticEditProperties.EditType editType) + { + if (!method.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) + { + return false; + } + + ImmutableDictionary properties = new DiagnosticEditProperties + { + TypeOfEdit = editType, + EditPosition = parameterMatch.Ordinal, + }.ToImmutableDictionary(); + + context.ReportDiagnostic(context.Operation.CreateDiagnostic(rule, properties)); + return true; + } + + /// + /// Attempts to report a diagnostic for a MockBehavior parameter issue, with the mocked type name. + /// + /// The operation analysis context. + /// The method to check for MockBehavior parameter. + /// The known Moq symbols. + /// The diagnostic rule to report. + /// The type of edit for the code fix. + /// The mocked type name to format into the diagnostic message. + /// True if a diagnostic was reported; otherwise, false. + internal bool TryReportMockBehaviorDiagnostic( + OperationAnalysisContext context, + IMethodSymbol method, + MoqKnownSymbols knownSymbols, + DiagnosticDescriptor rule, + DiagnosticEditProperties.EditType editType, + string mockedTypeName) + { + if (!method.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) + { + return false; + } + + ImmutableDictionary properties = new DiagnosticEditProperties + { + TypeOfEdit = editType, + EditPosition = parameterMatch.Ordinal, + }.ToImmutableDictionary(); + + context.ReportDiagnostic(context.Operation.CreateDiagnostic(rule, properties, mockedTypeName)); + return true; + } + + /// + /// Attempts to handle missing MockBehavior parameter by checking for overloads that accept it. + /// + /// The operation analysis context. + /// The MockBehavior parameter (should be null to trigger overload check). + /// The target method to check for overloads. + /// The known Moq symbols. + /// The diagnostic rule to report. + /// True if a diagnostic was reported; otherwise, false. + internal bool TryHandleMissingMockBehaviorParameter( + OperationAnalysisContext context, + IParameterSymbol? mockParameter, + IMethodSymbol target, + MoqKnownSymbols knownSymbols, + DiagnosticDescriptor rule) + { + // If the target method doesn't have a MockBehavior parameter, check if there's an overload that does + return mockParameter is null + && target.TryGetOverloadWithParameterOfType(knownSymbols.MockBehavior!, out IMethodSymbol? methodMatch, out _, cancellationToken: context.CancellationToken) + && TryReportMockBehaviorDiagnostic(context, methodMatch, knownSymbols, rule, DiagnosticEditProperties.EditType.Insert); + } + + /// + /// Attempts to handle missing MockBehavior parameter by checking for overloads that accept it, + /// with the mocked type name. + /// + /// The operation analysis context. + /// The MockBehavior parameter (should be null to trigger overload check). + /// The target method to check for overloads. + /// The known Moq symbols. + /// The diagnostic rule to report. + /// The mocked type name to format into the diagnostic message. + /// True if a diagnostic was reported; otherwise, false. + internal bool TryHandleMissingMockBehaviorParameter( + OperationAnalysisContext context, + IParameterSymbol? mockParameter, + IMethodSymbol target, + MoqKnownSymbols knownSymbols, + DiagnosticDescriptor rule, + string mockedTypeName) + { + return mockParameter is null + && target.TryGetOverloadWithParameterOfType(knownSymbols.MockBehavior!, out IMethodSymbol? methodMatch, out _, cancellationToken: context.CancellationToken) + && TryReportMockBehaviorDiagnostic(context, methodMatch, knownSymbols, rule, DiagnosticEditProperties.EditType.Insert, mockedTypeName); + } + private protected abstract void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols); private void RegisterCompilationStartAction(CompilationStartAnalysisContext context) diff --git a/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs b/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs index 9a70aaad8..8024008c9 100644 --- a/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs +++ b/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs @@ -82,7 +82,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context, MoqKnownSymbols k context.SemanticModel.TryGetEventNameFromLambdaSelector(eventSelector, out eventName); } - ValidateArgumentTypesWithEventName(context, eventArguments, expectedParameterTypes, invocation, eventName ?? "event"); + context.ValidateEventArgumentTypes(eventArguments, expectedParameterTypes, invocation, Rule, eventName ?? "event"); } private static bool TryGetRaiseMethodArguments( @@ -105,43 +105,6 @@ private static bool TryGetRaiseMethodArguments( knownSymbols); } - private static void ValidateArgumentTypesWithEventName(SyntaxNodeAnalysisContext context, ArgumentSyntax[] eventArguments, ITypeSymbol[] expectedParameterTypes, InvocationExpressionSyntax invocation, string eventName) - { - 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(); - } - - Diagnostic diagnostic = location.CreateDiagnostic(Rule, eventName); - context.ReportDiagnostic(diagnostic); - 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)) - { - // Report on the specific argument with the wrong type - Diagnostic diagnostic = eventArguments[i].GetLocation().CreateDiagnostic(Rule, eventName); - context.ReportDiagnostic(diagnostic); - } - } - } - private static bool IsRaiseMethodCall(SemanticModel semanticModel, InvocationExpressionSyntax invocation, MoqKnownSymbols knownSymbols) { SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(invocation); diff --git a/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs b/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs index 09d6ad2f0..738bbd8b0 100644 --- a/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs +++ b/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs @@ -77,10 +77,15 @@ private static void Analyze(SyntaxNodeAnalysisContext context, MoqKnownSymbols k return; } - // Extract event name from the lambda selector (first argument) - string eventName = TryGetEventNameFromLambdaSelector(invocation, context.SemanticModel) ?? "event"; + // 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); + } - EventSyntaxExtensions.ValidateEventArgumentTypes(context, eventArguments, expectedParameterTypes, invocation, Rule, eventName); + context.ValidateEventArgumentTypes(eventArguments, expectedParameterTypes, invocation, Rule, eventName ?? "event"); } private static bool TryGetRaisesMethodArguments(InvocationExpressionSyntax invocation, SemanticModel semanticModel, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes) @@ -96,50 +101,4 @@ private static bool TryGetRaisesMethodArguments(InvocationExpressionSyntax invoc return (success, eventType); }); } - - /// - /// Extracts the event name from a lambda selector of the form: x => x.EventName += null. - /// - /// The method invocation containing the lambda selector. - /// The semantic model. - /// The event name if found; otherwise null. - private static string? TryGetEventNameFromLambdaSelector(InvocationExpressionSyntax invocation, SemanticModel semanticModel) - { - // Get the first argument which should be the lambda selector - SeparatedSyntaxList arguments = invocation.ArgumentList.Arguments; - if (arguments.Count < 1) - { - return null; - } - - ExpressionSyntax eventSelector = arguments[0].Expression; - - // The event selector should be a lambda like: p => p.EventName += null - if (eventSelector is not LambdaExpressionSyntax lambda) - { - return null; - } - - // The body should be an assignment expression with += operator - if (lambda.Body is not AssignmentExpressionSyntax assignment || - !assignment.OperatorToken.IsKind(SyntaxKind.PlusEqualsToken)) - { - return null; - } - - // The left side should be a member access to the event - if (assignment.Left is not MemberAccessExpressionSyntax memberAccess) - { - return null; - } - - // Get the symbol for the event - SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(memberAccess); - if (symbolInfo.Symbol is IEventSymbol eventSymbol) - { - return eventSymbol.ToDisplayString(); - } - - return null; - } } diff --git a/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs b/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs index 6d5071479..06435c769 100644 --- a/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs +++ b/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs @@ -31,16 +31,14 @@ public class SetExplicitMockBehaviorAnalyzer : MockBehaviorDiagnosticAnalyzerBas private protected override void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols) { // Extract the type name for the diagnostic message - string typeName = GetMockedTypeName(context, target); + string typeName = GetMockedTypeName(context.Operation, target); // Check if the target method has a parameter of type MockBehavior IParameterSymbol? mockParameter = target.Parameters.DefaultIfNotSingle(parameter => parameter.Type.IsInstanceOf(knownSymbols.MockBehavior)); // If the target method doesn't have a MockBehavior parameter, check if there's an overload that does - if (mockParameter is null && target.TryGetOverloadWithParameterOfType(knownSymbols.MockBehavior!, out IMethodSymbol? methodMatch, out _, cancellationToken: context.CancellationToken)) + if (TryHandleMissingMockBehaviorParameter(context, mockParameter, target, knownSymbols, Rule, typeName)) { - // Using a method that doesn't accept a MockBehavior parameter, however there's an overload that does - ReportDiagnosticWithTypeName(context, methodMatch, typeName, knownSymbols, DiagnosticEditProperties.EditType.Insert); return; } @@ -49,7 +47,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM // Is the behavior set via a default value? if (mockArgument?.ArgumentKind == ArgumentKind.DefaultValue && mockArgument.Value.WalkDownConversion().ConstantValue.Value == knownSymbols.MockBehaviorDefault?.ConstantValue) { - ReportDiagnosticWithTypeName(context, target, typeName, knownSymbols, DiagnosticEditProperties.EditType.Insert); + TryReportMockBehaviorDiagnostic(context, target, knownSymbols, Rule, DiagnosticEditProperties.EditType.Insert, typeName); } // NOTE: This logic can't handle indirection (e.g. var x = MockBehavior.Default; new Mock(x);). We can't use the constant value either, @@ -58,52 +56,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM // The operation specifies a MockBehavior; is it MockBehavior.Default? if (mockArgument?.DescendantsAndSelf().OfType().Any(argument => argument.Member.IsInstanceOf(knownSymbols.MockBehaviorDefault)) == true) { - ReportDiagnosticWithTypeName(context, target, typeName, knownSymbols, DiagnosticEditProperties.EditType.Replace); + TryReportMockBehaviorDiagnostic(context, target, knownSymbols, Rule, DiagnosticEditProperties.EditType.Replace, typeName); } } - - private static string GetMockedTypeName(OperationAnalysisContext context, IMethodSymbol target) - { - // For object creation (new Mock), get the type argument from the Mock type - if (context.Operation is IObjectCreationOperation objectCreation && objectCreation.Type is INamedTypeSymbol namedType && namedType.TypeArguments.Length > 0) - { - return namedType.TypeArguments[0].ToDisplayString(); - } - - // For method invocation (MockRepository.Of), get the type argument from the method - if (context.Operation is IInvocationOperation invocation && invocation.TargetMethod.TypeArguments.Length > 0) - { - return invocation.TargetMethod.TypeArguments[0].ToDisplayString(); - } - - // If we can't determine the type, try to get it from the target method if it's generic - if (target.ContainingType?.TypeArguments.Length > 0) - { - return target.ContainingType.TypeArguments[0].ToDisplayString(); - } - - // Fallback to a generic name - return "T"; - } - - private void ReportDiagnosticWithTypeName( - OperationAnalysisContext context, - IMethodSymbol method, - string typeName, - MoqKnownSymbols knownSymbols, - DiagnosticEditProperties.EditType editType) - { - if (!method.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) - { - return; - } - - ImmutableDictionary properties = new DiagnosticEditProperties - { - TypeOfEdit = editType, - EditPosition = parameterMatch.Ordinal, - }.ToImmutableDictionary(); - - context.ReportDiagnostic(context.Operation.CreateDiagnostic(Rule, properties, typeName)); - } } diff --git a/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs b/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs index b5e6d26e1..af872c294 100644 --- a/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs +++ b/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs @@ -26,6 +26,30 @@ public class SetStrictMockBehaviorAnalyzer : MockBehaviorDiagnosticAnalyzerBase /// public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + /// + /// + /// The original strict analyzer resolved the mocked type name from 's + /// type arguments (not the invocation's) and fell back to "Unknown" instead of "T". + /// + internal override string GetMockedTypeName(IOperation operation, IMethodSymbol target) + { + // For object creation (new Mock), get the type argument from the Mock type + if (operation is IObjectCreationOperation objectCreation + && objectCreation.Type is INamedTypeSymbol namedType + && namedType.TypeArguments.Length > 0) + { + return namedType.TypeArguments[0].ToDisplayString(); + } + + // For any other case, use the target method's type arguments + if (target.TypeArguments.Length > 0) + { + return target.TypeArguments[0].ToDisplayString(); + } + + return "Unknown"; + } + /// [SuppressMessage("Design", "MA0051:Method is too long", Justification = "Should be fixed. Ignoring for now to avoid additional churn as part of larger refactor.")] private protected override void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols) @@ -37,7 +61,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM IParameterSymbol? mockParameter = target.Parameters.DefaultIfNotSingle(parameter => parameter.Type.IsInstanceOf(knownSymbols.MockBehavior)); // If the target method doesn't have a MockBehavior parameter, check if there's an overload that does - if (TryHandleMissingMockBehaviorParameter(context, mockParameter, target, knownSymbols, mockedTypeName)) + if (TryHandleMissingMockBehaviorParameter(context, mockParameter, target, knownSymbols, Rule, mockedTypeName)) { // Using a method that doesn't accept a MockBehavior parameter, however there's an overload that does return; @@ -47,7 +71,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM // Is the behavior set via a default value? if (mockArgument?.ArgumentKind == ArgumentKind.DefaultValue && mockArgument.Value.WalkDownConversion().ConstantValue.Value == knownSymbols.MockBehaviorDefault?.ConstantValue - && TryReportStrictMockBehaviorDiagnostic(context, target, knownSymbols, mockedTypeName, DiagnosticEditProperties.EditType.Insert)) + && TryReportMockBehaviorDiagnostic(context, target, knownSymbols, Rule, DiagnosticEditProperties.EditType.Insert, mockedTypeName)) { return; } @@ -58,85 +82,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM if (mockArgument?.Value.WalkDownConversion().ConstantValue.Value != knownSymbols.MockBehaviorStrict?.ConstantValue && mockArgument?.DescendantsAndSelf().OfType().Any(argument => argument.Member.IsInstanceOf(knownSymbols.MockBehaviorStrict)) != true) { - TryReportStrictMockBehaviorDiagnostic(context, target, knownSymbols, mockedTypeName, DiagnosticEditProperties.EditType.Replace); + TryReportMockBehaviorDiagnostic(context, target, knownSymbols, Rule, DiagnosticEditProperties.EditType.Replace, mockedTypeName); } } - - /// - /// Extracts the mocked type name from the operation. - /// - /// The operation being analyzed. - /// The target method symbol. - /// The name of the mocked type, or "Unknown" if it cannot be determined. - private static string GetMockedTypeName(IOperation operation, IMethodSymbol target) - { - // For object creation like new Mock() - if (operation is IObjectCreationOperation objectCreation - && objectCreation.Type is INamedTypeSymbol namedType - && namedType.TypeArguments.Length > 0) - { - return namedType.TypeArguments[0].ToDisplayString(); - } - - // For method invocation like Mock.Of() - if (operation is IInvocationOperation && target.TypeArguments.Length > 0) - { - return target.TypeArguments[0].ToDisplayString(); - } - - return "Unknown"; - } - - /// - /// Attempts to report a strict mock behavior diagnostic with the mocked type name. - /// - /// The operation analysis context. - /// The method to check for MockBehavior parameter. - /// The known Moq symbols. - /// The name of the mocked type. - /// The type of edit for the code fix. - /// True if a diagnostic was reported; otherwise, false. - private bool TryReportStrictMockBehaviorDiagnostic( - OperationAnalysisContext context, - IMethodSymbol method, - MoqKnownSymbols knownSymbols, - string mockedTypeName, - DiagnosticEditProperties.EditType editType) - { - if (!method.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) - { - return false; - } - - ImmutableDictionary properties = new DiagnosticEditProperties - { - TypeOfEdit = editType, - EditPosition = parameterMatch.Ordinal, - }.ToImmutableDictionary(); - - context.ReportDiagnostic(context.Operation.CreateDiagnostic(Rule, properties, mockedTypeName)); - return true; - } - - /// - /// Attempts to handle missing MockBehavior parameter by checking for overloads that accept it. - /// - /// The operation analysis context. - /// The MockBehavior parameter (should be null to trigger overload check). - /// The target method to check for overloads. - /// The known Moq symbols. - /// The name of the mocked type. - /// True if a diagnostic was reported; otherwise, false. - private bool TryHandleMissingMockBehaviorParameter( - OperationAnalysisContext context, - IParameterSymbol? mockParameter, - IMethodSymbol target, - MoqKnownSymbols knownSymbols, - string mockedTypeName) - { - // If the target method doesn't have a MockBehavior parameter, check if there's an overload that does - return mockParameter is null - && target.TryGetOverloadWithParameterOfType(knownSymbols.MockBehavior!, out IMethodSymbol? methodMatch, out _, cancellationToken: context.CancellationToken) - && TryReportStrictMockBehaviorDiagnostic(context, methodMatch, knownSymbols, mockedTypeName, DiagnosticEditProperties.EditType.Insert); - } } diff --git a/src/Analyzers/SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Analyzers/SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index 085bd44e2..f7de28c56 100644 --- a/src/Analyzers/SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Analyzers/SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.Operations; namespace Moq.Analyzers; @@ -48,7 +47,6 @@ private static void RegisterCompilationStartAction(CompilationStartAnalysisConte OperationKind.Invocation); } - [SuppressMessage("Design", "MA0051:Method is too long", Justification = "Should be fixed. Ignoring for now to avoid additional churn as part of larger refactor.")] private static void AnalyzeInvocation(OperationAnalysisContext context, MoqKnownSymbols knownSymbols) { if (context.Operation is not IInvocationOperation invocationOperation) @@ -56,86 +54,24 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, MoqKnown return; } - IMethodSymbol targetMethod = invocationOperation.TargetMethod; - - // 1. Check if the invoked method is a Moq SetupSequence method - if (!targetMethod.IsMoqSetupSequenceMethod(knownSymbols)) + if (!invocationOperation.TargetMethod.IsMoqSetupSequenceMethod(knownSymbols)) { return; } - // 2. Attempt to locate the member reference from the SetupSequence expression argument. - // Typically, Moq SetupSequence calls have a single lambda argument like x => x.SomeMember. - // We'll extract that member reference or invocation to see whether it is overridable. ISymbol? mockedMemberSymbol = MoqVerificationHelpers.TryGetMockedMemberSymbol(invocationOperation); - if (mockedMemberSymbol == null) - { - return; - } - - // 3. Skip if the symbol is part of an interface, those are always "overridable". - if (mockedMemberSymbol.ContainingType?.TypeKind == TypeKind.Interface) + if (mockedMemberSymbol is null + || mockedMemberSymbol.ContainingType?.TypeKind == TypeKind.Interface + || mockedMemberSymbol.IsOverridableOrAllowedMockMember(knownSymbols)) { return; } - // 4. Check if symbol is a property or method, and if it is overridable or is returning a Task (which Moq allows). - if (IsOverridableOrTaskResultMember(mockedMemberSymbol, knownSymbols)) - { - return; - } - - // 5. If we reach here, the member is neither overridable nor allowed by Moq - // So we report the diagnostic. - // - // Try to get the specific member syntax for a more precise diagnostic location + // Use the specific member syntax for a more precise diagnostic location when available SyntaxNode? memberSyntax = MoqVerificationHelpers.TryGetMockedMemberSyntax(invocationOperation); Location diagnosticLocation = memberSyntax?.GetLocation() ?? invocationOperation.Syntax.GetLocation(); Diagnostic diagnostic = diagnosticLocation.CreateDiagnostic(Rule, mockedMemberSymbol.ToDisplayString()); context.ReportDiagnostic(diagnostic); } - - /// - /// Determines whether a member symbol is either overridable or represents a / Result property - /// that Moq allows to be setup even if the underlying property is not overridable. - /// - /// The mocked member symbol. - /// A instance for resolving well-known types. - /// - /// Returns when the member is overridable or is a / Result property; otherwise . - /// - private static bool IsOverridableOrTaskResultMember(ISymbol mockedMemberSymbol, MoqKnownSymbols knownSymbols) - { - switch (mockedMemberSymbol) - { - case IPropertySymbol propertySymbol: - // Check if the property is Task.Result and skip diagnostic if it is - if (propertySymbol.IsTaskOrValueResultProperty(knownSymbols)) - { - return true; - } - - if (propertySymbol.IsOverridable()) - { - return true; - } - - break; - - case IMethodSymbol methodSymbol: - if (methodSymbol.IsOverridable()) - { - return true; - } - - break; - - default: - // If it's not a property or method, it's not overridable - return false; - } - - return false; - } } diff --git a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index 9e33955df..8c75f4920 100644 --- a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -90,7 +90,7 @@ private static bool IsSetupOnNonOverridableMember( ISymbol? candidate = MoqVerificationHelpers.TryGetMockedMemberSymbol(invocationOperation); if (candidate is null || candidate.ContainingType?.TypeKind == TypeKind.Interface - || IsPropertyOrMethod(candidate, knownSymbols)) + || candidate.IsOverridableOrAllowedMockMember(knownSymbols)) { return false; } @@ -98,49 +98,4 @@ private static bool IsSetupOnNonOverridableMember( mockedMemberSymbol = candidate; return true; } - - /// - /// Determines whether a property or method is either - /// , , , or - /// - OR - - /// if the is overridable. - /// - /// The mocked member symbol. - /// A instance for resolving well-known types. - /// - /// Returns when the diagnostic should not be triggered; otherwise . - /// - private static bool IsPropertyOrMethod(ISymbol mockedMemberSymbol, MoqKnownSymbols knownSymbols) - { - switch (mockedMemberSymbol) - { - case IPropertySymbol propertySymbol: - // Check if the property is Task.Result and skip diagnostic if it is - if (propertySymbol.IsTaskOrValueResultProperty(knownSymbols)) - { - return true; - } - - if (propertySymbol.IsOverridable()) - { - return true; - } - - break; - - case IMethodSymbol methodSymbol: - if (methodSymbol.IsOverridable()) - { - return true; - } - - break; - - default: - // If it's not a property or method, it's not overridable - return false; - } - - return false; - } } diff --git a/src/Analyzers/VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Analyzers/VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index 2a4459589..5d42606a3 100644 --- a/src/Analyzers/VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Analyzers/VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -112,7 +112,7 @@ private static bool IsMemberAllowedForVerification(ISymbol mockedMemberSymbol, I return mockedMemberSymbol is IPropertySymbol propertySymbol && propertySymbol.IsOverridable(); } - return IsAllowedMockMember(mockedMemberSymbol, knownSymbols); + return mockedMemberSymbol.IsOverridableOrAllowedMockMember(knownSymbols); } private static void ReportDiagnostic(OperationAnalysisContext context, IInvocationOperation invocationOperation, ISymbol mockedMemberSymbol) @@ -120,28 +120,4 @@ private static void ReportDiagnostic(OperationAnalysisContext context, IInvocati Diagnostic diagnostic = invocationOperation.Syntax.CreateDiagnostic(Rule, mockedMemberSymbol.Name); context.ReportDiagnostic(diagnostic); } - - /// - /// Determines whether a member can be mocked. - /// - /// The mocked member symbol. - /// The known symbols. - /// - /// Returns when the diagnostic should not be triggered; otherwise . - /// - private static bool IsAllowedMockMember(ISymbol mockedMemberSymbol, MoqKnownSymbols knownSymbols) - { - switch (mockedMemberSymbol) - { - case IPropertySymbol propertySymbol: - return propertySymbol.IsOverridable() || propertySymbol.IsTaskOrValueResultProperty(knownSymbols); - - case IMethodSymbol methodSymbol: - return methodSymbol.IsOverridable(); - - default: - // If it's not a property or method, it can't be mocked. This includes fields and events. - return false; - } - } } diff --git a/src/Common/EventSyntaxExtensions.cs b/src/Common/EventSyntaxExtensions.cs index 5162b8ec5..d7d7df516 100644 --- a/src/Common/EventSyntaxExtensions.cs +++ b/src/Common/EventSyntaxExtensions.cs @@ -5,24 +5,6 @@ namespace Moq.Analyzers.Common; /// internal static class EventSyntaxExtensions { - /// - /// Validates that event arguments match the expected parameter types. - /// - /// The analysis context. - /// The event arguments to validate. - /// The expected parameter types. - /// The invocation expression for error reporting. - /// The diagnostic rule to report. - internal static void ValidateEventArgumentTypes( - SyntaxNodeAnalysisContext context, - ArgumentSyntax[] eventArguments, - ITypeSymbol[] expectedParameterTypes, - InvocationExpressionSyntax invocation, - DiagnosticDescriptor rule) - { - ValidateEventArgumentTypes(context, eventArguments, expectedParameterTypes, invocation, rule, null); - } - /// /// Validates that event arguments match the expected parameter types. /// @@ -33,12 +15,12 @@ internal static void ValidateEventArgumentTypes( /// The diagnostic rule to report. /// The event name to include in diagnostic messages. internal static void ValidateEventArgumentTypes( - SyntaxNodeAnalysisContext context, + this SyntaxNodeAnalysisContext context, ArgumentSyntax[] eventArguments, ITypeSymbol[] expectedParameterTypes, InvocationExpressionSyntax invocation, DiagnosticDescriptor rule, - string? eventName) + string? eventName = null) { if (eventArguments.Length != expectedParameterTypes.Length) { @@ -79,44 +61,24 @@ internal static void ValidateEventArgumentTypes( } } - /// - /// Gets the parameter types for a given event delegate type. - /// - /// The event delegate type. - /// An array of parameter types expected by the event delegate. - internal static ITypeSymbol[] GetEventParameterTypes(ITypeSymbol eventType) - { - return GetEventParameterTypesInternal(eventType, null); - } - /// /// Gets the parameter types for a given event delegate type. /// /// The event delegate type. /// Known symbols for type checking. /// An array of parameter types expected by the event delegate. - internal static ITypeSymbol[] GetEventParameterTypes(ITypeSymbol eventType, KnownSymbols knownSymbols) + internal static ITypeSymbol[] GetEventParameterTypes(ITypeSymbol eventType, KnownSymbols? knownSymbols = null) { - return GetEventParameterTypesInternal(eventType, knownSymbols); - } + if (eventType is not INamedTypeSymbol namedType) + { + return []; + } - /// - /// Extracts arguments from an event method invocation. - /// - /// The method invocation. - /// The semantic model. - /// The extracted event arguments. - /// The expected parameter types. - /// Function to extract event type from the event selector. - /// if arguments were successfully extracted; otherwise, . - internal static bool TryGetEventMethodArguments( - InvocationExpressionSyntax invocation, - SemanticModel semanticModel, - out ArgumentSyntax[] eventArguments, - out ITypeSymbol[] expectedParameterTypes, - Func eventTypeExtractor) - { - return TryGetEventMethodArgumentsInternal(invocation, semanticModel, out eventArguments, out expectedParameterTypes, eventTypeExtractor, null); + ITypeSymbol[]? parameterTypes = TryGetActionDelegateParameters(namedType, knownSymbols) ?? + TryGetEventHandlerDelegateParameters(namedType, knownSymbols) ?? + TryGetCustomDelegateParameters(namedType); + + return parameterTypes ?? []; } /// @@ -135,32 +97,18 @@ internal static bool TryGetEventMethodArguments( out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes, Func eventTypeExtractor, - KnownSymbols knownSymbols) - { - return TryGetEventMethodArgumentsInternal(invocation, semanticModel, out eventArguments, out expectedParameterTypes, eventTypeExtractor, knownSymbols); - } - - private static bool TryGetEventMethodArgumentsInternal( - InvocationExpressionSyntax invocation, - SemanticModel semanticModel, - out ArgumentSyntax[] eventArguments, - out ITypeSymbol[] expectedParameterTypes, - Func eventTypeExtractor, - KnownSymbols? knownSymbols) + KnownSymbols? knownSymbols = null) { eventArguments = []; expectedParameterTypes = []; - // Get the arguments to the method SeparatedSyntaxList arguments = invocation.ArgumentList.Arguments; - // Method should have at least 1 argument (the event selector) if (arguments.Count < 1) { return false; } - // First argument should be a lambda that selects the event ExpressionSyntax eventSelector = arguments[0].Expression; (bool success, ITypeSymbol? eventType) = eventTypeExtractor(semanticModel, eventSelector); if (!success || eventType == null) @@ -168,10 +116,8 @@ private static bool TryGetEventMethodArgumentsInternal( return false; } - // Get expected parameter types from the event delegate - expectedParameterTypes = knownSymbols != null ? GetEventParameterTypes(eventType, knownSymbols) : GetEventParameterTypes(eventType); + expectedParameterTypes = GetEventParameterTypes(eventType, knownSymbols); - // The remaining arguments should match the event parameter types if (arguments.Count <= 1) { eventArguments = []; @@ -188,35 +134,6 @@ 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) - { - if (eventType is not INamedTypeSymbol namedType) - { - return []; - } - - // Try different delegate type handlers in order of specificity - ITypeSymbol[]? parameterTypes = TryGetActionDelegateParameters(namedType, knownSymbols) ?? - TryGetEventHandlerDelegateParameters(namedType, knownSymbols) ?? - TryGetCustomDelegateParameters(namedType); - - return parameterTypes ?? []; - } - /// /// Attempts to get parameter types from Action delegate types. /// diff --git a/src/Common/ISymbolExtensions.Moq.cs b/src/Common/ISymbolExtensions.Moq.cs index 788a36567..d61764e1e 100644 --- a/src/Common/ISymbolExtensions.Moq.cs +++ b/src/Common/ISymbolExtensions.Moq.cs @@ -145,6 +145,29 @@ internal static bool IsMoqCallbackMethod(this ISymbol symbol, MoqKnownSymbols kn symbol.IsInstanceOf(knownSymbols.ICallback2Callback); } + /// + /// Determines whether a member symbol is either overridable or represents a + /// / Result property + /// that Moq allows to be set up even if the underlying property is not overridable. + /// + /// The mocked member symbol. + /// A instance for resolving well-known types. + /// + /// when the member is overridable or is a Task/ValueTask Result property; + /// otherwise . + /// + internal static bool IsOverridableOrAllowedMockMember(this ISymbol mockedMemberSymbol, MoqKnownSymbols knownSymbols) + { + return mockedMemberSymbol switch + { + IPropertySymbol propertySymbol => + propertySymbol.IsOverridable() || propertySymbol.IsTaskOrValueResultProperty(knownSymbols), + IMethodSymbol methodSymbol => + methodSymbol.IsOverridable(), + _ => false, + }; + } + /// /// Determines whether a symbol is a Moq Raises method. /// diff --git a/src/Common/ISymbolExtensions.cs b/src/Common/ISymbolExtensions.cs index da0a5db72..5b681d1ef 100644 --- a/src/Common/ISymbolExtensions.cs +++ b/src/Common/ISymbolExtensions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Moq.Analyzers.Common;