Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 19 additions & 1 deletion src/Moq.Analyzers/Common/ISymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Moq.Analyzers.Common;
using System.Runtime.CompilerServices;
Comment thread
rjmurillo marked this conversation as resolved.

namespace Moq.Analyzers.Common;

internal static class ISymbolExtensions
{
Expand Down Expand Up @@ -47,4 +49,20 @@ public static bool IsConstructor(this ISymbol symbol)
return symbol.DeclaredAccessibility != Accessibility.Private
&& symbol is IMethodSymbol { MethodKind: MethodKind.Constructor } and { IsStatic: false };
}

public static bool IsMethodReturnTypeTask(this ISymbol methodSymbol)
{
string type = methodSymbol.ToDisplayString();
return string.Equals(type, "System.Threading.Tasks.Task", StringComparison.Ordinal)
|| string.Equals(type, "System.Threading.ValueTask", StringComparison.Ordinal)
Comment thread
rjmurillo marked this conversation as resolved.
|| type.StartsWith("System.Threading.Tasks.Task<", StringComparison.Ordinal)
Comment thread
rjmurillo marked this conversation as resolved.
|| (type.StartsWith("System.Threading.Tasks.ValueTask<", StringComparison.Ordinal)
Comment thread
rjmurillo marked this conversation as resolved.
&& type.EndsWith(".Result", StringComparison.Ordinal));
Comment thread
rjmurillo marked this conversation as resolved.
}
Comment thread
rjmurillo marked this conversation as resolved.

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsOverridable(this ISymbol symbol)
{
return !symbol.IsSealed && (symbol.IsVirtual || symbol.IsAbstract || symbol.IsOverride);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Runtime.CompilerServices;
using ISymbolExtensions = Microsoft.CodeAnalysis.ISymbolExtensions;

namespace Moq.Analyzers;

/// <summary>
Expand Down Expand Up @@ -34,26 +37,81 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
{
InvocationExpressionSyntax setupInvocation = (InvocationExpressionSyntax)context.Node;

if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && context.SemanticModel.IsMoqSetupMethod(memberAccessExpression, context.CancellationToken))
if (setupInvocation.Expression is not MemberAccessExpressionSyntax memberAccessExpression
|| !context.SemanticModel.IsMoqSetupMethod(memberAccessExpression, context.CancellationToken))
{
return;
}

ExpressionSyntax? mockedMemberExpression = setupInvocation.FindMockedMemberExpressionFromSetupMethod();
if (mockedMemberExpression == null)
{
return;
}

SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(mockedMemberExpression, context.CancellationToken);
ISymbol? symbol = symbolInfo.Symbol;

if (symbol is null)
{
return;
}

// Skip if it's part of an interface
if (symbol.ContainingType.TypeKind == TypeKind.Interface)
{
return;
}
Comment thread
rjmurillo marked this conversation as resolved.

switch (symbol)
{
ExpressionSyntax? mockedMemberExpression = setupInvocation.FindMockedMemberExpressionFromSetupMethod();
if (mockedMemberExpression == null)
{
return;
}

SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(mockedMemberExpression, context.CancellationToken);
if (symbolInfo.Symbol is IPropertySymbol or IMethodSymbol
&& !IsMethodOverridable(symbolInfo.Symbol))
{
Diagnostic diagnostic = mockedMemberExpression.CreateDiagnostic(Rule);
context.ReportDiagnostic(diagnostic);
}
case IPropertySymbol propertySymbol:
// Check if the property is Task<T>.Result and skip diagnostic if it is
if (IsTaskResultProperty(propertySymbol, context))
{
return;
Comment thread
rjmurillo marked this conversation as resolved.
}

if (propertySymbol.IsOverridable())
{
return;
Comment thread
rjmurillo marked this conversation as resolved.
}

if (propertySymbol.IsMethodReturnTypeTask())
{
return;
Comment thread
rjmurillo marked this conversation as resolved.
}

Comment thread
rjmurillo marked this conversation as resolved.
break;
case IMethodSymbol methodSymbol:
if (methodSymbol.IsOverridable() || methodSymbol.IsMethodReturnTypeTask())
{
return;
Comment thread
rjmurillo marked this conversation as resolved.
}

break;
}

Diagnostic diagnostic = mockedMemberExpression.CreateDiagnostic(Rule);
context.ReportDiagnostic(diagnostic);
}

private static bool IsMethodOverridable(ISymbol methodSymbol)
private static bool IsTaskResultProperty(IPropertySymbol propertySymbol, SyntaxNodeAnalysisContext context)
{
return !methodSymbol.IsSealed && (methodSymbol.IsVirtual || methodSymbol.IsAbstract || methodSymbol.IsOverride);
// Check if the property is named "Result"
if (!string.Equals(propertySymbol.Name, "Result", StringComparison.Ordinal))
{
return false;
}

// Check if the containing type is Task<T>
INamedTypeSymbol? taskOfTType = context.SemanticModel.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1");

if (taskOfTType == null)
{
return false; // If Task<T> type cannot be found, we skip it
}

return SymbolEqualityComparer.Default.Equals(propertySymbol.ContainingType, taskOfTType);
}
}
20 changes: 2 additions & 18 deletions src/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,12 @@ private static void Analyze(SyntaxNodeAnalysisContext context)

SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(mockedMemberExpression, context.CancellationToken);
if ((symbolInfo.Symbol is IPropertySymbol || symbolInfo.Symbol is IMethodSymbol)
&& !IsMethodOverridable(symbolInfo.Symbol)
&& IsMethodReturnTypeTask(symbolInfo.Symbol))
&& !symbolInfo.Symbol.IsOverridable()
&& symbolInfo.Symbol.IsMethodReturnTypeTask())
{
Diagnostic diagnostic = mockedMemberExpression.GetLocation().CreateDiagnostic(Rule);
context.ReportDiagnostic(diagnostic);
}
}
}

private static bool IsMethodOverridable(ISymbol methodSymbol)
{
return !methodSymbol.IsSealed
&& (methodSymbol.IsVirtual || methodSymbol.IsAbstract || methodSymbol.IsOverride);
}

private static bool IsMethodReturnTypeTask(ISymbol methodSymbol)
{
string type = methodSymbol.ToDisplayString();
return string.Equals(type, "System.Threading.Tasks.Task", StringComparison.Ordinal)
|| string.Equals(type, "System.Threading.ValueTask", StringComparison.Ordinal)
|| type.StartsWith("System.Threading.Tasks.Task<", StringComparison.Ordinal)
|| (type.StartsWith("System.Threading.Tasks.ValueTask<", StringComparison.Ordinal)
&& type.EndsWith(".Result", StringComparison.Ordinal));
}
}
2 changes: 1 addition & 1 deletion src/Moq.Analyzers/SquiggleCop.Baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@
- {Id: EM0105, Title: Duplicate Case Type, Category: Logic, DefaultSeverity: Error, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: EnableGenerateDocumentationFile, Title: Set MSBuild property 'GenerateDocumentationFile' to 'true', Category: Style, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: IDE0004, Title: Remove Unnecessary Cast, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true}
- {Id: IDE0005, Title: Using directive is unnecessary., Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: IDE0005, Title: Using directive is unnecessary., Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true}
- {Id: IDE0005_gen, Title: Using directive is unnecessary., Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true}
- {Id: IDE0007, Title: Use implicit type, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: IDE0008, Title: Use explicit type, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using Xunit.Abstractions;
using Verifier = Moq.Analyzers.Test.Helpers.AnalyzerVerifier<Moq.Analyzers.SetupShouldBeUsedOnlyForOverridableMembersAnalyzer>;

namespace Moq.Analyzers.Test;

public class SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests
public class SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests(ITestOutputHelper output)
{
public static IEnumerable<object[]> TestData()
{
Expand All @@ -16,46 +17,56 @@ public static IEnumerable<object[]> TestData()
["""new Mock<ISampleInterface>().Setup(x => x.TestProperty);"""],
["""new Mock<SampleClass>().Setup(x => x.Calculate(It.IsAny<int>(), It.IsAny<int>()));"""],
["""new Mock<SampleClass>().Setup(x => x.DoSth());"""],
["""new Mock<IParameterlessAsyncMethod>().Setup(x => x.DoSomethingAsync().Result).Returns(true);"""],
Comment thread
rjmurillo marked this conversation as resolved.
}.WithNamespaces().WithMoqReferenceAssemblyGroups();
}

[Theory]
[MemberData(nameof(TestData))]
public async Task ShouldAnalyzeSetupForOverridableMembers(string referenceAssemblyGroup, string @namespace, string mock)
{
string source = $$"""
{{@namespace}}

public interface ISampleInterface
{
int Calculate(int a, int b);
int TestProperty { get; set; }
}

public interface IParameterlessAsyncMethod
{
Task<bool> DoSomethingAsync();
}

public abstract class BaseSampleClass
{
public int Calculate() => 0;
public abstract int Calculate(int a, int b);
public abstract int Calculate(int a, int b, int c);
}

public class SampleClass : BaseSampleClass
{
public override int Calculate(int a, int b) => 0;
public sealed override int Calculate(int a, int b, int c) => 0;
public virtual int DoSth() => 0;
public int Property { get; set; }
}

internal class UnitTest
{
private void Test()
{
{{mock}}
}
}
""";

output.WriteLine(source);

Comment thread
rjmurillo marked this conversation as resolved.
await Verifier.VerifyAnalyzerAsync(
$$"""
{{@namespace}}

public interface ISampleInterface
{
int Calculate(int a, int b);
int TestProperty { get; set; }
}

public abstract class BaseSampleClass
{
public int Calculate() => 0;
public abstract int Calculate(int a, int b);
public abstract int Calculate(int a, int b, int c);
}

public class SampleClass : BaseSampleClass
{
public override int Calculate(int a, int b) => 0;
public sealed override int Calculate(int a, int b, int c) => 0;
public virtual int DoSth() => 0;
public int Property { get; set; }
}

internal class UnitTest
{
private void Test()
{
{{mock}}
}
}
""",
source,
referenceAssemblyGroup);
}
}