diff --git a/src/Analyzers/MSTest.Analyzers/PublicMethodShouldBeTestMethodAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/PublicMethodShouldBeTestMethodAnalyzer.cs index 1ac5e9cb06..6ec31e7116 100644 --- a/src/Analyzers/MSTest.Analyzers/PublicMethodShouldBeTestMethodAnalyzer.cs +++ b/src/Analyzers/MSTest.Analyzers/PublicMethodShouldBeTestMethodAnalyzer.cs @@ -64,7 +64,9 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbo return; } - if (!methodSymbol.HasValidTestMethodSignature(taskSymbol, valueTaskSymbol, canDiscoverInternals)) + if (!methodSymbol.HasValidTestMethodSignature(taskSymbol, valueTaskSymbol, canDiscoverInternals) + || methodSymbol.IsVirtual + || methodSymbol.IsOverride) { return; } @@ -85,6 +87,13 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbo return; } + // We consider that if the method implements an interface member, it is not a test method. + // Explicit implementations are not public so they are discarded earlier. + if (methodSymbol.IsImplementationOfAnyInterfaceMember()) + { + return; + } + ImmutableArray methodAttributes = methodSymbol.GetAttributes(); // check if the method has testMethod, testInitialize or testCleanup attribute bool hasValidAttribute = false; diff --git a/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IMethodSymbolExtensions.cs b/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IMethodSymbolExtensions.cs index 617e8e255c..f9d2e09b35 100644 --- a/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IMethodSymbolExtensions.cs +++ b/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IMethodSymbolExtensions.cs @@ -10,6 +10,38 @@ namespace Analyzer.Utilities.Extensions; internal static class IMethodSymbolExtensions { + public static bool IsImplementationOfAnyInterfaceMember(this ISymbol symbol) + => IsImplementationOfAnyInterfaceMember(symbol); + + /// + /// Checks if a given symbol implements an interface member implicitly + /// + public static bool IsImplementationOfAnyInterfaceMember(this ISymbol symbol) + where TSymbol : ISymbol + { + if (symbol.ContainingType == null) + { + return false; + } + + foreach (INamedTypeSymbol interfaceSymbol in symbol.ContainingType.AllInterfaces) + { + foreach (TSymbol interfaceMember in interfaceSymbol.GetMembers().OfType()) + { + if (IsImplementationOfInterfaceMember(symbol, interfaceMember)) + { + return true; + } + } + } + + return false; + } + + public static bool IsImplementationOfInterfaceMember(this ISymbol symbol, [NotNullWhen(returnValue: true)] ISymbol? interfaceMember) + => interfaceMember != null + && SymbolEqualityComparer.Default.Equals(symbol, symbol.ContainingType.FindImplementationForInterfaceMember(interfaceMember)); + /// /// Checks if the given method is an implementation of the given interface method /// Substituted with the given typeargument. diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/PublicMethodShouldBeTestMethodAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/PublicMethodShouldBeTestMethodAnalyzerTests.cs index 2a7548dbdd..32aaeee31b 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/PublicMethodShouldBeTestMethodAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/PublicMethodShouldBeTestMethodAnalyzerTests.cs @@ -252,4 +252,94 @@ public void TestCleanup() """; await VerifyCS.VerifyCodeFixAsync(code, code); } + + [TestMethod] + public async Task WhenMethodIsPublicAndImplementsDispose_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + + [TestClass] + public class MyTestClass : IDisposable + { + public void Dispose() + { + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenMethodIsPublicAndImplementsUserDefinedInterface_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + + public interface IMyInterface + { + void MyMethod(); + } + + [TestClass] + public class MyTestClass : IMyInterface + { + public void MyMethod() + { + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenMethodIsPublicAndImplementsExplicitlyUserDefinedInterface_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + + public interface IMyInterface + { + void MyMethod(); + } + + [TestClass] + public class MyTestClass : IMyInterface + { + void IMyInterface.MyMethod() + { + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenMethodIsPublicAndImplementsDisposeAsVirtual_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + + [TestClass] + public class MyTestClass : IDisposable + { + public virtual void Dispose() + { + } + } + + [TestClass] + public class SubTestClass : MyTestClass + { + public override void Dispose() + { + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(code); + } }