-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace using a `Regex` on string-ified type names with the proper Roslyn API (usually Identifier.Name) and do some 'straightening' of the code using early-returns and pattern matching to reduce the triangle-ness. This is effectively a rewrite of `AsShouldBeUsedOnlyForInterfaceAnalyzer`. There's a smaller refactor for `CallbackSignatureShouldMatchMockedMethod*` as that code needs to be shared between the analyzer and the code fix provider (will be handled later) This refactor has a few benefits: 1. It addresses the bug mentioned in #58 (testing with a newer version is coming in another PR to keep code reviewable) 2. It improves performance a good amount using very basic Visual Studio tracing (e.g. ~20 fewer string allocations and saves 30% [20 ms] on a single call to Analyzer.Analyze()) 3. It plumbs the `CancellationToken` into more places to be a good IDE citizen This PR also starts to break apart the monolithic `Helpers` class into smaller components. In order to support the `.TryXXX` pattern I needed to bring in a polyfill for `[NotNullWhen]` for nullability analysis. I'm using PolySharp because, in addition to being the most popular polyfill, it uses a source generator to add polyfills to the current assembly, which avoid assembly loading overhead and the hassle of adding dependencies to the analyzer.
- Loading branch information
1 parent
0a7f49a
commit dad8699
Showing
14 changed files
with
189 additions
and
88 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,8 @@ namespace Moq.Analyzers; | |
[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 GitHub Actions / build (windows-2022)
Check warning on line 9 in Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs GitHub Actions / build (ubuntu-22.04)
|
||
Diagnostics.AsShouldBeUsedOnlyForInterfaceTitle, | ||
|
@@ -22,20 +24,37 @@ public override void Initialize(AnalysisContext context) | |
|
||
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())); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<ISampleInterface>() 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) | ||
{ | ||
if (syntax is GenericNameSyntax genericName) | ||
{ | ||
typeArguments = genericName.TypeArgumentList.Arguments; | ||
return true; | ||
} | ||
|
||
typeArguments = default; | ||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |