Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Regex from analyzers #81

Merged
merged 7 commits into from
Jun 12, 2024
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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<Import Project="build/targets/compiler/Packages.props" />
<Import Project="build/targets/reproducible/Packages.props" />
<Import Project="build/targets/tests/Packages.props" />
<Import Project="build/targets/codeanalysis/Packages.props" />
Expand Down
43 changes: 31 additions & 12 deletions Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsShouldBeUsedOnlyForInterfaceAnalyzer : DiagnosticAnalyzer
{
private static readonly MoqMethodDescriptorBase MoqAsMethodDescriptor = new MoqAsMethodDescriptor();

private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
Diagnostics.AsShouldBeUsedOnlyForInterfaceId,

Check warning on line 9 in Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Enable analyzer release tracking for the analyzer project containing rule 'Moq1300' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 9 in Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Enable analyzer release tracking for the analyzer project containing rule 'Moq1300' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
Diagnostics.AsShouldBeUsedOnlyForInterfaceTitle,
Diagnostics.AsShouldBeUsedOnlyForInterfaceMessage,
Diagnostics.Category,
Expand All @@ -22,20 +24,37 @@

private static void Analyze(SyntaxNodeAnalysisContext context)
{
InvocationExpressionSyntax? asInvocation = (InvocationExpressionSyntax)context.Node;
if (context.Node is not InvocationExpressionSyntax invocationExpression)
{
return;
}

if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessSyntax)
{
return;
}

if (!MoqAsMethodDescriptor.IsMatch(context.SemanticModel, memberAccessSyntax, context.CancellationToken))
{
return;
}

if (!memberAccessSyntax.Name.TryGetGenericArguments(out SeparatedSyntaxList<TypeSyntax> typeArguments))
{
return;
}

if (typeArguments.Count != 1)
{
return;
}

TypeSyntax typeArgument = typeArguments[0];
SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(typeArgument, context.CancellationToken);

if (asInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression
&& Helpers.IsMoqAsMethod(context.SemanticModel, memberAccessExpression)
&& memberAccessExpression.Name is GenericNameSyntax genericName
&& genericName.TypeArgumentList.Arguments.Count == 1)
if (symbolInfo.Symbol is ITypeSymbol { TypeKind: not TypeKind.Interface })
{
TypeSyntax? typeArgument = genericName.TypeArgumentList.Arguments[0];
SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(typeArgument, context.CancellationToken);
if (symbolInfo.Symbol is ITypeSymbol typeSymbol && typeSymbol.TypeKind != TypeKind.Interface)
{
Diagnostic? diagnostic = Diagnostic.Create(Rule, typeArgument.GetLocation());
context.ReportDiagnostic(diagnostic);
}
context.ReportDiagnostic(Diagnostic.Create(Rule, typeArgument.GetLocation()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
public class CallbackSignatureShouldMatchMockedMethodAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
Diagnostics.CallbackSignatureShouldMatchMockedMethodId,

Check warning on line 7 in Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Enable analyzer release tracking for the analyzer project containing rule 'Moq1100' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 7 in Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Enable analyzer release tracking for the analyzer project containing rule 'Moq1100' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
Diagnostics.CallbackSignatureShouldMatchMockedMethodTitle,
Diagnostics.CallbackSignatureShouldMatchMockedMethodMessage,

Check warning on line 9 in Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period

Check warning on line 9 in Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period
Diagnostics.Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
Expand Down Expand Up @@ -43,7 +43,7 @@
SeparatedSyntaxList<ParameterSyntax> lambdaParameters = callbackLambda.ParameterList.Parameters;
if (lambdaParameters.Count == 0) return;

InvocationExpressionSyntax? setupInvocation = Helpers.FindSetupMethodFromCallbackInvocation(context.SemanticModel, callbackOrReturnsInvocation);
InvocationExpressionSyntax? setupInvocation = Helpers.FindSetupMethodFromCallbackInvocation(context.SemanticModel, callbackOrReturnsInvocation, context.CancellationToken);
InvocationExpressionSyntax? mockedMethodInvocation = Helpers.FindMockedMethodInvocationFromSetupMethod(setupInvocation);
if (mockedMethodInvocation == null) return;

Expand All @@ -59,7 +59,7 @@
for (int i = 0; i < mockedMethodArguments.Count; i++)
{
TypeInfo mockedMethodArgumentType = context.SemanticModel.GetTypeInfo(mockedMethodArguments[i].Expression, context.CancellationToken);
TypeInfo lambdaParameterType = context.SemanticModel.GetTypeInfo(lambdaParameters[i].Type, context.CancellationToken);

Check warning on line 62 in Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference argument for parameter 'expression' in 'TypeInfo CSharpExtensions.GetTypeInfo(SemanticModel? semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken = default(CancellationToken))'.

Check warning on line 62 in Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference argument for parameter 'expression' in 'TypeInfo CSharpExtensions.GetTypeInfo(SemanticModel? semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken = default(CancellationToken))'.
string? mockedMethodTypeName = mockedMethodArgumentType.ConvertedType?.ToString();
string? lambdaParameterTypeName = lambdaParameterType.ConvertedType?.ToString();
if (!string.Equals(mockedMethodTypeName, lambdaParameterTypeName, StringComparison.Ordinal))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ private async Task<Document> FixCallbackSignatureAsync(SyntaxNode root, Document
return document;
}

InvocationExpressionSyntax? setupMethodInvocation = Helpers.FindSetupMethodFromCallbackInvocation(semanticModel, callbackInvocation);
InvocationExpressionSyntax? setupMethodInvocation = Helpers.FindSetupMethodFromCallbackInvocation(semanticModel, callbackInvocation, cancellationToken);
Debug.Assert(setupMethodInvocation != null, nameof(setupMethodInvocation) + " != null");
IMethodSymbol[]? matchingMockedMethods = Helpers.GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(semanticModel, setupMethodInvocation).ToArray();

Expand Down
20 changes: 6 additions & 14 deletions Source/Moq.Analyzers/Helpers.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace Moq.Analyzers;

internal static class Helpers
{
private static readonly MoqMethodDescriptor MoqSetupMethodDescriptor = new("Setup", new Regex("^Moq\\.Mock<.*>\\.Setup\\.*"));
private static readonly MoqMethodDescriptorBase MoqSetupMethodDescriptor = new MoqSetupMethodDescriptor();

private static readonly MoqMethodDescriptor MoqAsMethodDescriptor = new("As", new Regex("^Moq\\.Mock\\.As<\\.*"), isGeneric: true);

internal static bool IsMoqSetupMethod(SemanticModel semanticModel, MemberAccessExpressionSyntax method)
{
return MoqSetupMethodDescriptor.IsMoqMethod(semanticModel, method);
}

internal static bool IsMoqAsMethod(SemanticModel semanticModel, MemberAccessExpressionSyntax method)
internal static bool IsMoqSetupMethod(SemanticModel semanticModel, MemberAccessExpressionSyntax method, CancellationToken cancellationToken)
{
return MoqAsMethodDescriptor.IsMoqMethod(semanticModel, method);
return MoqSetupMethodDescriptor.IsMatch(semanticModel, method, cancellationToken);
}

internal static bool IsCallbackOrReturnInvocation(SemanticModel semanticModel, InvocationExpressionSyntax callbackOrReturnsInvocation)
Expand Down Expand Up @@ -48,12 +40,12 @@
};
}

internal static InvocationExpressionSyntax? FindSetupMethodFromCallbackInvocation(SemanticModel semanticModel, ExpressionSyntax expression)
internal static InvocationExpressionSyntax? FindSetupMethodFromCallbackInvocation(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
{
InvocationExpressionSyntax? invocation = expression as InvocationExpressionSyntax;
if (invocation?.Expression is not MemberAccessExpressionSyntax method) return null;
if (IsMoqSetupMethod(semanticModel, method)) return invocation;
return FindSetupMethodFromCallbackInvocation(semanticModel, method.Expression);
if (IsMoqSetupMethod(semanticModel, method, cancellationToken)) return invocation;
rjmurillo marked this conversation as resolved.
Show resolved Hide resolved
return FindSetupMethodFromCallbackInvocation(semanticModel, method.Expression, cancellationToken);
}

internal static InvocationExpressionSyntax? FindMockedMethodInvocationFromSetupMethod(InvocationExpressionSyntax? setupInvocation)
Expand Down Expand Up @@ -85,7 +77,7 @@
SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(expression);
if (symbolInfo is { CandidateReason: CandidateReason.None, Symbol: T })
{
matchingSymbols.Add(symbolInfo.Symbol as T);

Check warning on line 80 in Source/Moq.Analyzers/Helpers.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference argument for parameter 'item' in 'void List<T>.Add(T item)'.

Check warning on line 80 in Source/Moq.Analyzers/Helpers.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference argument for parameter 'item' in 'void List<T>.Add(T item)'.
}
else if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure)
{
Expand All @@ -98,7 +90,7 @@

private static bool IsCallbackOrReturnSymbol(ISymbol? symbol)
{
// TODO: Check what is the best way to do such checks

Check warning on line 93 in Source/Moq.Analyzers/Helpers.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

TODO Check what is the best way to do such checks (https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0026.md)

Check warning on line 93 in Source/Moq.Analyzers/Helpers.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

TODO Check what is the best way to do such checks (https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0026.md)
if (symbol is not IMethodSymbol methodSymbol) return false;
string? methodName = methodSymbol.ToString();
return methodName.StartsWith("Moq.Language.ICallback", StringComparison.Ordinal)
Expand Down
32 changes: 32 additions & 0 deletions Source/Moq.Analyzers/MoqAsMethodDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Moq.Analyzers;

/// <summary>
/// A class that, given a <see cref="SemanticModel"/> and a <see cref="MemberAccessExpressionSyntax"/>, determines if
/// it is a call to the Moq `Mock.As()` method.
/// </summary>
internal class MoqAsMethodDescriptor : MoqMethodDescriptorBase
{
private const string MethodName = "As";

public override bool IsMatch(SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccessSyntax, CancellationToken cancellationToken)
{
if (!IsFastMatch(memberAccessSyntax, MethodName.AsSpan()))
{
return false;
}

ISymbol? symbol = semanticModel.GetSymbolInfo(memberAccessSyntax, cancellationToken).Symbol;

if (symbol is not IMethodSymbol methodSymbol)
{
return false;
}

if (!IsContainedInMockType(methodSymbol))
{
return false;
}

return methodSymbol.Name.AsSpan().SequenceEqual(MethodName.AsSpan()) && methodSymbol.IsGenericMethod;
MattKotsenas marked this conversation as resolved.
Show resolved Hide resolved
}
}
58 changes: 0 additions & 58 deletions Source/Moq.Analyzers/MoqMethodDescriptor.cs

This file was deleted.

38 changes: 38 additions & 0 deletions Source/Moq.Analyzers/MoqMethodDescriptorBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Moq.Analyzers;

/// <summary>
/// A base that that provides common functionality for identifying if a given <see cref="SyntaxNode"/>
/// is a specific Moq method.
/// </summary>
/// <remarks>
/// Currently the <see cref="IsMatch(SemanticModel, MemberAccessExpressionSyntax, CancellationToken)"/> abstract method
/// is specific to <see cref="MemberAccessExpressionSyntax"/> because that's the only type of syntax in use. I expect we'll need
/// to loosen this restriction if we start using other types of syntax.
/// </remarks>
internal abstract class MoqMethodDescriptorBase
{
private const string ContainingNamespace = "Moq";
private const string ContainingType = "Mock";

public abstract bool IsMatch(SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccessSyntax, CancellationToken cancellationToken);

protected static bool IsFastMatch(MemberAccessExpressionSyntax memberAccessSyntax, ReadOnlySpan<char> methodName)
{
return memberAccessSyntax.Name.Identifier.Text.AsSpan().SequenceEqual(methodName);
}

protected static bool IsContainedInMockType(IMethodSymbol methodSymbol)
{
return IsInMoqNamespace(methodSymbol) && IsInMockType(methodSymbol);
}

private static bool IsInMoqNamespace(ISymbol symbol)
{
return symbol.ContainingNamespace.Name.AsSpan().SequenceEqual(ContainingNamespace.AsSpan());
}

private static bool IsInMockType(ISymbol symbol)
{
return symbol.ContainingType.Name.AsSpan().SequenceEqual(ContainingType.AsSpan());
}
}
32 changes: 32 additions & 0 deletions Source/Moq.Analyzers/MoqSetupMethodDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Moq.Analyzers;

/// <summary>
/// A class that, given a <see cref="SemanticModel"/> and a <see cref="MemberAccessExpressionSyntax"/>, determines if
/// it is a call to the Moq `Mock.Setup()` method.
/// </summary>
internal class MoqSetupMethodDescriptor : MoqMethodDescriptorBase
{
private const string MethodName = "Setup";

public override bool IsMatch(SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccessSyntax, CancellationToken cancellationToken)
{
if (!IsFastMatch(memberAccessSyntax, MethodName.AsSpan()))
{
return false;
}

ISymbol? symbol = semanticModel.GetSymbolInfo(memberAccessSyntax, cancellationToken).Symbol;

if (symbol is not IMethodSymbol methodSymbol)
{
return false;
}

if (!IsContainedInMockType(methodSymbol))
{
return false;
}

return methodSymbol.Name.AsSpan().SequenceEqual(MethodName.AsSpan()) && methodSymbol.IsGenericMethod;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
{
InvocationExpressionSyntax? setupInvocation = (InvocationExpressionSyntax)context.Node;

if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression))
if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression, context.CancellationToken))
{
ExpressionSyntax? mockedMemberExpression = Helpers.FindMockedMemberExpressionFromSetupMethod(setupInvocation);
if (mockedMemberExpression == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
{
InvocationExpressionSyntax? setupInvocation = (InvocationExpressionSyntax)context.Node;

if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression))
if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression, context.CancellationToken))
{
ExpressionSyntax? mockedMemberExpression = Helpers.FindMockedMemberExpressionFromSetupMethod(setupInvocation);
if (mockedMemberExpression == null)
Expand Down
33 changes: 33 additions & 0 deletions Source/Moq.Analyzers/SyntaxExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Diagnostics.CodeAnalysis;

namespace Moq.Analyzers;

/// <summary>
/// Extensions methods for <see cref="SyntaxNode"/>s.
/// </summary>
internal static class SyntaxExtensions
{
/// <summary>
/// Tries to get the generic arguments of a given <see cref="NameSyntax"/>.
/// </summary>
/// <param name="syntax">The syntax to inspect.</param>
/// <param name="typeArguments">The collection of <see cref="TypeSyntax"/> elements on the <paramref name="syntax"/>.</param>
/// <returns><see langword="true"/> if <paramref name="syntax"/> has generic / type parameters; <see langword="false"/> otherwise.</returns>
/// <example>
/// x.As&lt;ISampleInterface&gt;() returns <see langword="true"/> and <paramref name="typeArguments"/> will contain <c>ISampleInterface</c>.
/// </example>
/// <example>
/// x.As() returns <see langword="false"/> and <paramref name="typeArguments"/> will be empty.
/// </example>
public static bool TryGetGenericArguments(this NameSyntax syntax, [NotNullWhen(true)] out SeparatedSyntaxList<TypeSyntax> typeArguments)
MattKotsenas marked this conversation as resolved.
Show resolved Hide resolved
{
if (syntax is GenericNameSyntax genericName)
{
typeArguments = genericName.TypeArgumentList.Arguments;
return true;
}

typeArguments = default;
return false;
}
}
7 changes: 7 additions & 0 deletions build/targets/compiler/Compiler.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="PolySharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
5 changes: 5 additions & 0 deletions build/targets/compiler/Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<ItemGroup>
<PackageVersion Include="PolySharp" Version="1.14.1" />
</ItemGroup>
</Project>
Loading