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

Use WellKnownTypeProvider + KnownSymbols pattern to simplify analyzers #245

Merged
merged 15 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
4 changes: 4 additions & 0 deletions build/targets/codeanalysis/CodeAnalysis.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
1 change: 1 addition & 0 deletions build/targets/codeanalysis/Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.176" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.11.0-beta1.24454.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers" Version="3.11.0-beta1.24454.1" />
<PackageVersion Include="Roslynator.Analyzers" Version="4.12.8" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="SonarAnalyzer.CSharp" Version="9.32.0.97167" />
Expand Down
2 changes: 1 addition & 1 deletion build/targets/compiler/Compiler.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="PolySharp">
<PackageReference Include="Polyfill">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Expand Down
4 changes: 2 additions & 2 deletions build/targets/compiler/Packages.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<ItemGroup>
<PackageVersion Include="PolySharp" Version="1.14.1" />
</ItemGroup>
<PackageVersion Include="Polyfill" Version="7.1.2" />
MattKotsenas marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>
</Project>
15 changes: 6 additions & 9 deletions src/Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,18 @@ public override void Initialize(AnalysisContext context)

private static void RegisterCompilationStartAction(CompilationStartAnalysisContext context)
{
MoqKnownSymbols knownSymbols = new(context.Compilation);

// Ensure Moq is referenced in the compilation
ImmutableArray<INamedTypeSymbol> mockTypes = context.Compilation.GetMoqMock();
if (mockTypes.IsEmpty)
if (!knownSymbols.IsMockReferenced())
{
return;
}

// Look for the Mock.As() method and provide it to Analyze to avoid looking it up multiple times.
#pragma warning disable ECS0900 // Minimize boxing and unboxing
ImmutableArray<IMethodSymbol> asMethods = mockTypes
.SelectMany(mockType => mockType.GetMembers(WellKnownMoqNames.AsMethodName))
.OfType<IMethodSymbol>()
.Where(method => method.IsGenericMethod)
.ToImmutableArray();
#pragma warning restore ECS0900 // Minimize boxing and unboxing
ImmutableArray<IMethodSymbol> asMethods = ImmutableArray.CreateRange([
..knownSymbols.MockAs,
..knownSymbols.Mock1As]);
MattKotsenas marked this conversation as resolved.
Show resolved Hide resolved

if (asMethods.IsEmpty)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public override void Initialize(AnalysisContext context)

private static void Analyze(SyntaxNodeAnalysisContext context)
{
MoqKnownSymbols knownSymbols = new(context.SemanticModel.Compilation);

InvocationExpressionSyntax callbackOrReturnsInvocation = (InvocationExpressionSyntax)context.Node;

SeparatedSyntaxList<ArgumentSyntax> callbackOrReturnsMethodArguments = callbackOrReturnsInvocation.ArgumentList.Arguments;
Expand All @@ -49,7 +51,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
SeparatedSyntaxList<ParameterSyntax> lambdaParameters = callbackLambda.ParameterList.Parameters;
if (lambdaParameters.Count == 0) return;

InvocationExpressionSyntax? setupInvocation = context.SemanticModel.FindSetupMethodFromCallbackInvocation(callbackOrReturnsInvocation, context.CancellationToken);
InvocationExpressionSyntax? setupInvocation = context.SemanticModel.FindSetupMethodFromCallbackInvocation(knownSymbols, callbackOrReturnsInvocation, context.CancellationToken);
InvocationExpressionSyntax? mockedMethodInvocation = setupInvocation.FindMockedMethodInvocationFromSetupMethod();
if (mockedMethodInvocation == null) return;
MattKotsenas marked this conversation as resolved.
Show resolved Hide resolved

Expand Down
160 changes: 55 additions & 105 deletions src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,65 +68,42 @@
return null;
}

private static bool IsExpressionMockBehavior(SyntaxNodeAnalysisContext context, ExpressionSyntax? expression)
private static bool IsExpressionMockBehavior(SyntaxNodeAnalysisContext context, MoqKnownSymbols knownSymbols, ExpressionSyntax? expression)
{
if (expression == null)
if (expression is null)
{
return false;
}

if (expression is MemberAccessExpressionSyntax memberAccessExpressionSyntax)
SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(expression, context.CancellationToken);

if (symbolInfo.Symbol is null)
{
if (memberAccessExpressionSyntax.Expression is IdentifierNameSyntax identifierNameSyntax
&& string.Equals(identifierNameSyntax.Identifier.ValueText, WellKnownMoqNames.MockBehaviorTypeName, StringComparison.Ordinal))
{
return true;
}
return false;
}
else if (expression is IdentifierNameSyntax identifierNameSyntax)
{
SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(identifierNameSyntax, context.CancellationToken);

if (symbolInfo.Symbol == null)
{
return false;
}

ITypeSymbol? typeSymbol = null;
if (symbolInfo.Symbol is IParameterSymbol parameterSymbol)
{
typeSymbol = parameterSymbol.Type;
}
else if (symbolInfo.Symbol is ILocalSymbol localSymbol)
{
typeSymbol = localSymbol.Type;
}
else if (symbolInfo.Symbol is IFieldSymbol fieldSymbol)
{
typeSymbol = fieldSymbol.Type;
}

if (typeSymbol != null
&& string.Equals(typeSymbol.Name, WellKnownMoqNames.MockBehaviorTypeName, StringComparison.Ordinal))
{
return true;
}
ISymbol targetSymbol = symbolInfo.Symbol;
if (symbolInfo.Symbol is IParameterSymbol parameterSymbol)
{
targetSymbol = parameterSymbol.Type;
}

// Crude fallback to check if the expression is a Moq.MockBehavior enum
if (expression.ToString().StartsWith(WellKnownMoqNames.FullyQualifiedMoqBehaviorTypeName, StringComparison.Ordinal))
else if (symbolInfo.Symbol is ILocalSymbol localSymbol)
{
return true;
targetSymbol = localSymbol.Type;
}
else if (symbolInfo.Symbol is IFieldSymbol fieldSymbol)
{
targetSymbol = fieldSymbol.Type;
}

return false;
return targetSymbol.IsInstanceOf(knownSymbols.MockBehavior);
}

private static bool IsFirstArgumentMockBehavior(SyntaxNodeAnalysisContext context, ArgumentListSyntax? argumentList)
private static bool IsFirstArgumentMockBehavior(SyntaxNodeAnalysisContext context, MoqKnownSymbols knownSymbols, ArgumentListSyntax? argumentList)
{
ExpressionSyntax? expression = argumentList?.Arguments[0].Expression;

return IsExpressionMockBehavior(context, expression);
return IsExpressionMockBehavior(context, knownSymbols, expression);
MattKotsenas marked this conversation as resolved.
Show resolved Hide resolved
}

private static void VerifyDelegateMockAttempt(
Expand Down Expand Up @@ -174,106 +151,87 @@
return;
}

MoqKnownSymbols knownSymbols = new(context.Compilation);

// We're interested in the few ways to create mocks:
// - new Mock<T>()
// - Mock.Of<T>()
// - MockRepository.Create<T>()
//
// Ensure Moq is referenced in the compilation
ImmutableArray<INamedTypeSymbol> mockTypes = context.Compilation.GetMoqMock();
if (mockTypes.IsEmpty)
if (!knownSymbols.IsMockReferenced())
{
return;
}

// These are for classes
context.RegisterSyntaxNodeAction(AnalyzeNewObject, SyntaxKind.ObjectCreationExpression);
context.RegisterSyntaxNodeAction(AnalyzeInstanceCall, SyntaxKind.InvocationExpression);
context.RegisterSyntaxNodeAction(context => AnalyzeNewObject(context, knownSymbols), SyntaxKind.ObjectCreationExpression);
context.RegisterSyntaxNodeAction(context => AnalyzeInstanceCall(context, knownSymbols), SyntaxKind.InvocationExpression);
}

private static void AnalyzeInstanceCall(SyntaxNodeAnalysisContext context)
private static void AnalyzeInstanceCall(SyntaxNodeAnalysisContext context, MoqKnownSymbols knownSymbols)
{
InvocationExpressionSyntax invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node;

if (invocationExpressionSyntax.Expression is not MemberAccessExpressionSyntax memberAccessExpressionSyntax)
{
return;
}

if (memberAccessExpressionSyntax.Name is not GenericNameSyntax genericNameSyntax)
{
return;
}

if (genericNameSyntax.Identifier.Value is not string genericNameSyntaxIdentifierValue)
{
return;
}

if (string.Equals(genericNameSyntaxIdentifierValue, WellKnownMoqNames.CreateMethodName, StringComparison.Ordinal))
{
AnalyzeInvocation(context, invocationExpressionSyntax, WellKnownMoqNames.MockFactoryTypeName, true, true);
}
else if (string.Equals(genericNameSyntaxIdentifierValue, WellKnownMoqNames.OfMethodName, StringComparison.Ordinal))
{
AnalyzeInvocation(context, invocationExpressionSyntax, WellKnownMoqNames.MockTypeName, false, true);
}
AnalyzeInvocation(context, knownSymbols, invocationExpressionSyntax);
}

private static void AnalyzeInvocation(
SyntaxNodeAnalysisContext context,
InvocationExpressionSyntax invocationExpressionSyntax,
string expectedClassName,
bool hasReturnedMock,
bool hasMockBehavior)
MoqKnownSymbols knownSymbols,
InvocationExpressionSyntax invocationExpressionSyntax)
{
bool hasReturnedMock = true;
bool hasMockBehavior = true;

Check failure on line 185 in src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs#L185

Add the 'const' modifier to 'hasMockBehavior'.
SymbolInfo symbol = context.SemanticModel.GetSymbolInfo(invocationExpressionSyntax, context.CancellationToken);

if (symbol.Symbol is not IMethodSymbol method)
{
return;
}

if (!string.Equals(method.ContainingType.Name, expectedClassName, StringComparison.Ordinal))
if (!method.IsInstanceOf(knownSymbols.MockOf) && !method.IsInstanceOf(knownSymbols.MockRepositoryCreate))
{
return;
}

ITypeSymbol returnType = method.ReturnType;
if (hasReturnedMock)
{
if (returnType is not INamedTypeSymbol { IsGenericType: true } typeSymbol)
{
return;
}

returnType = typeSymbol.TypeArguments[0];
}

// We are calling MockRepository.Create<T> or Mock.Of<T>, determine which
ArgumentListSyntax? argumentList = null;
if (WellKnownMoqNames.OfMethodName.Equals(method.Name, StringComparison.Ordinal))
if (method.IsInstanceOf(knownSymbols.MockOf))
{
// Mock.Of<T> can specify condition for construction and MockBehavior, but
// cannot specify constructor parameters
//
// The only parameters that can be passed are not relevant for verification
// to just strip them
hasReturnedMock = false;
}
else
{
argumentList = invocationExpressionSyntax.ArgumentList;
}

VerifyMockAttempt(context, returnType, argumentList, hasMockBehavior);
ITypeSymbol returnType = method.ReturnType;
if (hasReturnedMock)
{
if (returnType is not INamedTypeSymbol { IsGenericType: true } typeSymbol)
{
return;
}

returnType = typeSymbol.TypeArguments[0];
}

VerifyMockAttempt(context, knownSymbols, returnType, argumentList, hasMockBehavior);
}

/// <summary>
/// Analyzes when a Mock`1 object is created to verify the provided constructor arguments
/// match an existing constructor of the mocked class.
/// </summary>
/// <param name="context">The context.</param>
private static void AnalyzeNewObject(SyntaxNodeAnalysisContext context)
/// <param name="knownSymbols">The <see cref="MoqKnownSymbols"/> used to lookup symbols against Moq types.</param>
private static void AnalyzeNewObject(SyntaxNodeAnalysisContext context, MoqKnownSymbols knownSymbols)
{
ObjectCreationExpressionSyntax newExpression = (ObjectCreationExpressionSyntax)context.Node;

Expand All @@ -283,33 +241,24 @@
return;
}

// Quick check
if (!string.Equals(
genericNameSyntax.Identifier.ValueText,
WellKnownMoqNames.MockTypeName,
StringComparison.Ordinal))
{
return;
}

// Full check
SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(newExpression, context.CancellationToken);

if (symbolInfo.Symbol is not IMethodSymbol mockConstructorMethod
|| mockConstructorMethod.MethodKind != MethodKind.Constructor
|| !string.Equals(mockConstructorMethod.ContainingType.ConstructedFrom.ContainingSymbol.Name, WellKnownMoqNames.MoqSymbolName, StringComparison.Ordinal))
if (!symbolInfo
.Symbol?
.IsInstanceOf(knownSymbols.Mock1?.Constructors ?? ImmutableArray<IMethodSymbol>.Empty)
?? false)
{
return;
}

if (mockConstructorMethod.ReceiverType is not INamedTypeSymbol { IsGenericType: true } typeSymbol)
if (symbolInfo.Symbol?.ContainingType is not INamedTypeSymbol { IsGenericType: true } typeSymbol)
{
return;
}

ITypeSymbol mockedClass = typeSymbol.TypeArguments[0];

VerifyMockAttempt(context, mockedClass, newExpression.ArgumentList, true);
VerifyMockAttempt(context, knownSymbols, mockedClass, newExpression.ArgumentList, true);
}

/// <summary>
Expand Down Expand Up @@ -423,6 +372,7 @@

private static void VerifyMockAttempt(
SyntaxNodeAnalysisContext context,
MoqKnownSymbols knownSymbols,
ITypeSymbol mockedClass,
ArgumentListSyntax? argumentList,
bool hasMockBehavior)
Expand All @@ -436,7 +386,7 @@
ArgumentSyntax[] arguments = argumentList?.Arguments.ToArray() ?? [];
#pragma warning restore ECS0900 // Consider using an alternative implementation to avoid boxing and unboxing

if (hasMockBehavior && arguments.Length > 0 && IsFirstArgumentMockBehavior(context, argumentList))
if (hasMockBehavior && arguments.Length > 0 && IsFirstArgumentMockBehavior(context, knownSymbols, argumentList))
{
// They passed a mock behavior as the first argument; ignore as Moq swallows it
arguments = arguments.RemoveAt(0);
Expand Down
18 changes: 9 additions & 9 deletions src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,28 @@ public override void Initialize(AnalysisContext context)

private static void RegisterCompilationStartAction(CompilationStartAnalysisContext context)
{
MoqKnownSymbols knownSymbols = new(context.Compilation);

// Ensure Moq is referenced in the compilation
ImmutableArray<INamedTypeSymbol> mockTypes = context.Compilation.GetMoqMock();
if (mockTypes.IsEmpty)
if (!knownSymbols.IsMockReferenced())
{
return;
}

// Look for the MockBehavior type and provide it to Analyze to avoid looking it up multiple times.
INamedTypeSymbol? mockBehaviorSymbol = context.Compilation.GetTypeByMetadataName(WellKnownMoqNames.FullyQualifiedMoqBehaviorTypeName);
INamedTypeSymbol? mockBehaviorSymbol = knownSymbols.MockBehavior;
if (mockBehaviorSymbol is null)
{
return;
}

// Look for the Mock.Of() method and provide it to Analyze to avoid looking it up multiple times.
#pragma warning disable ECS0900 // Minimize boxing and unboxing
ImmutableArray<IMethodSymbol> ofMethods = mockTypes
.SelectMany(mockType => mockType.GetMembers(WellKnownMoqNames.OfMethodName))
.OfType<IMethodSymbol>()
.Where(method => method.IsGenericMethod)
ImmutableArray<IMethodSymbol> ofMethods = knownSymbols.MockOf;

ImmutableArray<INamedTypeSymbol> mockTypes =
new INamedTypeSymbol?[] { knownSymbols.Mock1, knownSymbols.MockRepository }
.WhereNotNull()
MattKotsenas marked this conversation as resolved.
Show resolved Hide resolved
.ToImmutableArray();
#pragma warning restore ECS0900 // Minimize boxing and unboxing

context.RegisterOperationAction(
context => AnalyzeNewObject(context, mockTypes, mockBehaviorSymbol),
Expand Down
Loading