Skip to content
Merged
143 changes: 142 additions & 1 deletion src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Operations;

namespace Moq.Analyzers;

Expand All @@ -20,6 +20,147 @@ public override void Initialize(AnalysisContext context)
context.RegisterCompilationStartAction(RegisterCompilationStartAction);
}

/// <summary>
/// Extracts the mocked type name from the operation for use in diagnostic messages.
/// </summary>
/// <param name="operation">The operation being analyzed.</param>
/// <param name="target">The target method symbol.</param>
Comment thread
rjmurillo-bot marked this conversation as resolved.
/// <returns>The display name of the mocked type.</returns>
internal virtual string GetMockedTypeName(IOperation operation, IMethodSymbol target)
{
// For object creation (new Mock<T>), get the type argument from the Mock<T> 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<T>), 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<T>.ctor)
if (target.ContainingType?.TypeArguments.Length > 0)
{
return target.ContainingType.TypeArguments[0].ToDisplayString();
}

return "T";
}

/// <summary>
/// Attempts to report a diagnostic for a MockBehavior parameter issue.
/// </summary>
/// <param name="context">The operation analysis context.</param>
/// <param name="method">The method to check for MockBehavior parameter.</param>
/// <param name="knownSymbols">The known Moq symbols.</param>
/// <param name="rule">The diagnostic rule to report.</param>
/// <param name="editType">The type of edit for the code fix.</param>
/// <returns>True if a diagnostic was reported; otherwise, false.</returns>
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<string, string?> properties = new DiagnosticEditProperties
{
TypeOfEdit = editType,
EditPosition = parameterMatch.Ordinal,
}.ToImmutableDictionary();

context.ReportDiagnostic(context.Operation.CreateDiagnostic(rule, properties));
return true;
}

/// <summary>
/// Attempts to report a diagnostic for a MockBehavior parameter issue, with the mocked type name.
/// </summary>
/// <param name="context">The operation analysis context.</param>
/// <param name="method">The method to check for MockBehavior parameter.</param>
/// <param name="knownSymbols">The known Moq symbols.</param>
/// <param name="rule">The diagnostic rule to report.</param>
/// <param name="editType">The type of edit for the code fix.</param>
/// <param name="mockedTypeName">The mocked type name to format into the diagnostic message.</param>
/// <returns>True if a diagnostic was reported; otherwise, false.</returns>
internal bool TryReportMockBehaviorDiagnostic(
OperationAnalysisContext context,
IMethodSymbol method,
MoqKnownSymbols knownSymbols,
DiagnosticDescriptor rule,
DiagnosticEditProperties.EditType editType,
Comment thread
rjmurillo-bot marked this conversation as resolved.
Comment thread
rjmurillo-bot marked this conversation as resolved.
string mockedTypeName)
{
if (!method.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken))
{
return false;
}

ImmutableDictionary<string, string?> properties = new DiagnosticEditProperties
{
TypeOfEdit = editType,
EditPosition = parameterMatch.Ordinal,
}.ToImmutableDictionary();

context.ReportDiagnostic(context.Operation.CreateDiagnostic(rule, properties, mockedTypeName));
return true;
Comment thread
rjmurillo-bot marked this conversation as resolved.
}

/// <summary>
/// Attempts to handle missing MockBehavior parameter by checking for overloads that accept it.
/// </summary>
/// <param name="context">The operation analysis context.</param>
/// <param name="mockParameter">The MockBehavior parameter (should be null to trigger overload check).</param>
/// <param name="target">The target method to check for overloads.</param>
/// <param name="knownSymbols">The known Moq symbols.</param>
/// <param name="rule">The diagnostic rule to report.</param>
/// <returns>True if a diagnostic was reported; otherwise, false.</returns>
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);
}

/// <summary>
/// Attempts to handle missing MockBehavior parameter by checking for overloads that accept it,
/// with the mocked type name.
/// </summary>
/// <param name="context">The operation analysis context.</param>
/// <param name="mockParameter">The MockBehavior parameter (should be null to trigger overload check).</param>
/// <param name="target">The target method to check for overloads.</param>
/// <param name="knownSymbols">The known Moq symbols.</param>
/// <param name="rule">The diagnostic rule to report.</param>
/// <param name="mockedTypeName">The mocked type name to format into the diagnostic message.</param>
/// <returns>True if a diagnostic was reported; otherwise, false.</returns>
internal bool TryHandleMissingMockBehaviorParameter(
OperationAnalysisContext context,
IParameterSymbol? mockParameter,
IMethodSymbol target,
MoqKnownSymbols knownSymbols,
DiagnosticDescriptor rule,
Comment thread
rjmurillo-bot marked this conversation as resolved.
Comment thread
rjmurillo-bot marked this conversation as resolved.
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<IArgumentOperation> arguments, MoqKnownSymbols knownSymbols);

private void RegisterCompilationStartAction(CompilationStartAnalysisContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -96,50 +101,4 @@ private static bool TryGetRaisesMethodArguments(InvocationExpressionSyntax invoc
return (success, eventType);
});
}

/// <summary>
/// Extracts the event name from a lambda selector of the form: x => x.EventName += null.
/// </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 null.</returns>
private static string? TryGetEventNameFromLambdaSelector(InvocationExpressionSyntax invocation, SemanticModel semanticModel)
{
// Get the first argument which should be the lambda selector
SeparatedSyntaxList<ArgumentSyntax> 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;
}
}
55 changes: 4 additions & 51 deletions src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,14 @@ public class SetExplicitMockBehaviorAnalyzer : MockBehaviorDiagnosticAnalyzerBas
private protected override void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray<IArgumentOperation> 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;
}

Expand All @@ -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,
Expand All @@ -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<IFieldReferenceOperation>().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<T>), get the type argument from the Mock<T> 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<T>), 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<string, string?> properties = new DiagnosticEditProperties
{
TypeOfEdit = editType,
EditPosition = parameterMatch.Ordinal,
}.ToImmutableDictionary();

context.ReportDiagnostic(context.Operation.CreateDiagnostic(Rule, properties, typeName));
}
}
Loading
Loading