diff --git a/src/Moq.Analyzers/Analyzers/VerifyShouldBeCalledAnalyzer.cs b/src/Moq.Analyzers/Analyzers/VerifyShouldBeCalledAnalyzer.cs index 295f99b..2717e28 100644 --- a/src/Moq.Analyzers/Analyzers/VerifyShouldBeCalledAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/VerifyShouldBeCalledAnalyzer.cs @@ -62,7 +62,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) return; } - var variableNameModel = context.SemanticModel.GetDeclaredSymbol(variableName); + var variableNameModel = context.SemanticModel.GetDeclaredSymbol(variableName, context.CancellationToken); if (variableNameModel is null) { @@ -81,7 +81,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) // Retrieve all method invocation expressions. var invocationExpressions = parentMethod.DescendantNodes().OfType(); - var verifyAllCalled = invocationExpressions.Any(expression => IsMockVerifyAllInvocation(expression, variableNameModel, moqSymbols, context.SemanticModel)); + var verifyAllCalled = invocationExpressions.Any(expression => IsMockVerifyAllInvocation(expression, variableNameModel, moqSymbols, context.SemanticModel, context.CancellationToken)); if (!verifyAllCalled) { @@ -90,16 +90,15 @@ private static void Analyze(SyntaxNodeAnalysisContext context) } } - private static bool IsMockVerifyAllInvocation(InvocationExpressionSyntax invocation, ISymbol variableNameSymbol, MoqSymbols moqSymbols, SemanticModel semanticModel) + private static bool IsMockVerifyAllInvocation(InvocationExpressionSyntax invocation, ISymbol variableNameSymbol, MoqSymbols moqSymbols, SemanticModel semanticModel, CancellationToken cancellationToken) { if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) { return false; } - // We check if a "VerifyAll()" method is called (currently we don't know if it is on the Mock object, it can be on other object type) - // but we try to use this condition here to stop quickly the analysis. - var verifyMethod = semanticModel.GetSymbolInfo(memberAccess); + // Check if the invocation expression is a Verify() / VerifyAll() methods. + var verifyMethod = semanticModel.GetSymbolInfo(memberAccess, cancellationToken); if (verifyMethod.Symbol is null) { @@ -108,11 +107,33 @@ private static bool IsMockVerifyAllInvocation(InvocationExpressionSyntax invocat if (!moqSymbols.IsVerifyMethod(verifyMethod.Symbol) && !moqSymbols.IsVerifyAllMethod(verifyMethod.Symbol)) { + if (!moqSymbols.IsVerifyStaticMethod(verifyMethod.Symbol) && !moqSymbols.IsVerifyAllStaticMethod(verifyMethod.Symbol)) + { + return false; + } + + // Special case, the static method Verify() or VerifyAll() has been called. + // In this case, iterate on each arguments of the method called and check if the variableNameSymbol has been passed. + foreach (var argument in invocation.ArgumentList.Arguments) + { + var argumentSymbol = semanticModel.GetSymbolInfo(argument.Expression, cancellationToken); + + if (argumentSymbol.Symbol is null) + { + return false; + } + + if (SymbolEqualityComparer.Default.Equals(argumentSymbol.Symbol, variableNameSymbol)) + { + return true; + } + } + return false; } // Gets the variable name symbol. - var identifierSymbol = semanticModel.GetSymbolInfo(memberAccess.Expression); + var identifierSymbol = semanticModel.GetSymbolInfo(memberAccess.Expression, cancellationToken); // If the variable name of .VerifyAll() does not match the variable, so the VerifyAll() was for other Mock instance. if (!SymbolEqualityComparer.Default.Equals(identifierSymbol.Symbol, variableNameSymbol)) diff --git a/src/Moq.Analyzers/MoqSymbols.cs b/src/Moq.Analyzers/MoqSymbols.cs index 8539a42..87a7eda 100644 --- a/src/Moq.Analyzers/MoqSymbols.cs +++ b/src/Moq.Analyzers/MoqSymbols.cs @@ -13,7 +13,7 @@ internal sealed class MoqSymbols { private readonly INamedTypeSymbol mockBehaviorEnum; - private readonly INamedTypeSymbol mockClass; + private readonly INamedTypeSymbol mockGenericClass; private readonly IReadOnlyList setupMethods; @@ -21,30 +21,40 @@ internal sealed class MoqSymbols private readonly IReadOnlyList verifyMethods; + private readonly Lazy staticVerifyMethod; + + private readonly Lazy staticVerifyAllMethod; + + private readonly Lazy verifyAllMethod; + private readonly ISymbol mockBehaviorStrictField; private readonly ISymbol isAnyTypeClass; private readonly ISymbol asMethod; - private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum, ISymbol isAnyTypeClass, INamedTypeSymbol protectedMockInterface) + private MoqSymbols(INamedTypeSymbol mockGenericClass, INamedTypeSymbol mockBehaviorEnum, ISymbol isAnyTypeClass, INamedTypeSymbol protectedMockInterface) { - this.mockClass = mockClass; + this.mockGenericClass = mockGenericClass; this.mockBehaviorEnum = mockBehaviorEnum; this.isAnyTypeClass = isAnyTypeClass; - this.setupMethods = mockClass.GetMembers("Setup").OfType().ToArray(); + this.setupMethods = mockGenericClass.GetMembers("Setup").OfType().ToArray(); this.mockBehaviorStrictField = mockBehaviorEnum.GetMembers("Strict").First(); this.setupProtectedMethods = protectedMockInterface.GetMembers("Setup").OfType().ToArray(); - this.asMethod = mockClass.GetMembers("As").Single(); - this.verifyMethods = mockClass.GetMembers("Verify").OfType().ToArray(); + this.asMethod = mockGenericClass.GetMembers("As").Single(); + this.verifyMethods = mockGenericClass.GetMembers("Verify").OfType().ToArray(); + + this.staticVerifyMethod = new Lazy(() => mockGenericClass.BaseType!.GetMembers("Verify").Where(m => m.IsStatic).OfType().Single()); + this.staticVerifyAllMethod = new Lazy(() => mockGenericClass.BaseType!.GetMembers("VerifyAll").Where(m => m.IsStatic).OfType().Single()); + this.verifyAllMethod = new Lazy(() => mockGenericClass.GetMembers("VerifyAll").OfType().Single()); } public static MoqSymbols? FromCompilation(Compilation compilation) { - var mockClass = compilation.GetTypeByMetadataName("Moq.Mock`1"); + var mockGenericClass = compilation.GetTypeByMetadataName("Moq.Mock`1"); - if (mockClass is null) + if (mockGenericClass is null) { return null; } @@ -70,7 +80,7 @@ private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum return null; } - return new MoqSymbols(mockClass, mockBehaviorEnum, isAnyTypeClass, protectedMockInterface); + return new MoqSymbols(mockGenericClass, mockBehaviorEnum, isAnyTypeClass, protectedMockInterface); } public bool IsAnyType(ITypeSymbol symbol) @@ -90,7 +100,7 @@ public bool IsMock(ISymbol? symbol) return false; } - if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.mockClass)) + if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.mockGenericClass)) { return false; } @@ -158,9 +168,29 @@ public bool IsVerifyMethod(ISymbol? symbol) return false; } + public bool IsVerifyStaticMethod(ISymbol symbol) + { + if (!SymbolEqualityComparer.Default.Equals(symbol, this.staticVerifyMethod.Value)) + { + return false; + } + + return true; + } + public bool IsVerifyAllMethod(ISymbol symbol) { - if (symbol.Name != "VerifyAll") + if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.verifyAllMethod.Value)) + { + return false; + } + + return true; + } + + public bool IsVerifyAllStaticMethod(ISymbol symbol) + { + if (!SymbolEqualityComparer.Default.Equals(symbol, this.staticVerifyAllMethod.Value)) { return false; } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/VerifyShouldBeCalledAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/VerifyShouldBeCalledAnalyzerTest.cs index 4c09fe5..c843bf6 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/VerifyShouldBeCalledAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/VerifyShouldBeCalledAnalyzerTest.cs @@ -34,6 +34,12 @@ public void TestMethod() var mock2 = new Mock(); var mock3 = new Mock(); var mock4 = new Mock(); + var mock5 = new Mock(); + var mock6 = new Mock(); + var mock7 = new Mock(); + var mock8 = new Mock(); + var mock9 = new Mock(); + var mock10 = new Mock(); new Mock(); // No variable (ignored) @@ -45,7 +51,13 @@ public void TestMethod() mock2.Verify(); mock3.Verify(m => m.Method()); mock4.Verify(m => m.Method(), ""Foobar""); - } + + Mock.VerifyAll(mock5, mock6); + Mock.Verify(mock7, mock8); + + Mock.VerifyAll(mock9); + Mock.Verify(mock10); + } } public interface I diff --git a/tests/Moq.Analyzers.Tests/MoqLibrary.cs b/tests/Moq.Analyzers.Tests/MoqLibrary.cs index da48d33..43e67d0 100644 --- a/tests/Moq.Analyzers.Tests/MoqLibrary.cs +++ b/tests/Moq.Analyzers.Tests/MoqLibrary.cs @@ -14,7 +14,7 @@ namespace Moq using System; using System.Linq.Expressions; - public class Mock + public class Mock : Mock { public Mock(MockBehavior _ = MockBehavior.Loose, params object[] args) { } @@ -41,6 +41,13 @@ public void Verify(Expression> _, string failedMessage public object Property { get; set; } } + public class Mock + { + public static void Verify(params Mock[] mocks) { } + + public static void VerifyAll(params Mock[] mocks) { } + } + public enum MockBehavior { Strict, Loose } public interface ISetup