diff --git a/TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs b/TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs index 40a938bec2..8cfc9bce56 100644 --- a/TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs @@ -65,15 +65,17 @@ public void HelperMethod() } [Test] - public async Task Warning_For_Abstract_Class_With_MethodDataSource() + public async Task No_Warning_For_Abstract_Class_With_MethodDataSource_When_No_Subclasses() { + // No warning when there are no subclasses - the abstract class may be in a library + // meant to be subclassed by consuming assemblies await Verifier .VerifyAnalyzerAsync( """ using TUnit.Core; using System.Collections.Generic; - public abstract class {|#0:AbstractTestBase|} + public abstract class AbstractTestBase { public static IEnumerable TestData() => new[] { 1, 2, 3 }; @@ -83,24 +85,22 @@ public void DataDrivenTest(int value) { } } - """, - - Verifier.Diagnostic(Rules.AbstractTestClassWithDataSources) - .WithLocation(0) - .WithArguments("AbstractTestBase") + """ ); } [Test] - public async Task Warning_For_Abstract_Class_With_InstanceMethodDataSource() + public async Task No_Warning_For_Abstract_Class_With_InstanceMethodDataSource_When_No_Subclasses() { + // No warning when there are no subclasses - the abstract class may be in a library + // meant to be subclassed by consuming assemblies await Verifier .VerifyAnalyzerAsync( """ using TUnit.Core; using System.Collections.Generic; - public abstract class {|#0:ServiceCollectionTest|} + public abstract class ServiceCollectionTest { public IEnumerable SingletonServices() => new[] { 1, 2, 3 }; @@ -110,23 +110,21 @@ public void ServiceCanBeCreatedAsSingleton(int value) { } } - """, - - Verifier.Diagnostic(Rules.AbstractTestClassWithDataSources) - .WithLocation(0) - .WithArguments("ServiceCollectionTest") + """ ); } [Test] - public async Task Warning_For_Abstract_Class_With_Arguments() + public async Task No_Warning_For_Abstract_Class_With_Arguments_When_No_Subclasses() { + // No warning when there are no subclasses - the abstract class may be in a library + // meant to be subclassed by consuming assemblies await Verifier .VerifyAnalyzerAsync( """ using TUnit.Core; - public abstract class {|#0:AbstractTestBase|} + public abstract class AbstractTestBase { [Test] [Arguments(1)] @@ -135,17 +133,15 @@ public void DataDrivenTest(int value) { } } - """, - - Verifier.Diagnostic(Rules.AbstractTestClassWithDataSources) - .WithLocation(0) - .WithArguments("AbstractTestBase") + """ ); } [Test] - public async Task Warning_For_Abstract_Class_With_ClassDataSource() + public async Task No_Warning_For_Abstract_Class_With_ClassDataSource_When_No_Subclasses() { + // No warning when there are no subclasses - the abstract class may be in a library + // meant to be subclassed by consuming assemblies await Verifier .VerifyAnalyzerAsync( """ @@ -155,7 +151,7 @@ public class TestData { } - public abstract class {|#0:AbstractTestBase|} + public abstract class AbstractTestBase { [Test] [ClassDataSource] @@ -163,11 +159,7 @@ public void DataDrivenTest(TestData data) { } } - """, - - Verifier.Diagnostic(Rules.AbstractTestClassWithDataSources) - .WithLocation(0) - .WithArguments("AbstractTestBase") + """ ); } diff --git a/TUnit.Analyzers/AbstractTestClassWithDataSourcesAnalyzer.cs b/TUnit.Analyzers/AbstractTestClassWithDataSourcesAnalyzer.cs index 6e8c53893f..1c0dc918b5 100644 --- a/TUnit.Analyzers/AbstractTestClassWithDataSourcesAnalyzer.cs +++ b/TUnit.Analyzers/AbstractTestClassWithDataSourcesAnalyzer.cs @@ -83,10 +83,12 @@ private void AnalyzeSymbol(SymbolAnalysisContext context) if (hasDataSourceAttributes) { // Check if there are any concrete classes that inherit from this abstract class with [InheritsTests] - var hasInheritingClasses = HasConcreteInheritingClassesWithInheritsTests(context, namedTypeSymbol); + var hasInheritingClassesWithAttribute = HasConcreteInheritingClassesWithInheritsTests(context, namedTypeSymbol, out var hasAnyConcreteSubclasses); - // Only report the diagnostic if no inheriting classes are found - if (!hasInheritingClasses) + // Only report the diagnostic if: + // 1. There ARE concrete subclasses in the source (if none exist, this is likely a library class meant to be subclassed externally) + // 2. None of those subclasses have [InheritsTests] + if (hasAnyConcreteSubclasses && !hasInheritingClassesWithAttribute) { context.ReportDiagnostic(Diagnostic.Create( Rules.AbstractTestClassWithDataSources, @@ -97,8 +99,10 @@ private void AnalyzeSymbol(SymbolAnalysisContext context) } } - private static bool HasConcreteInheritingClassesWithInheritsTests(SymbolAnalysisContext context, INamedTypeSymbol abstractClass) + private static bool HasConcreteInheritingClassesWithInheritsTests(SymbolAnalysisContext context, INamedTypeSymbol abstractClass, out bool hasAnyConcreteSubclasses) { + hasAnyConcreteSubclasses = false; + // Get all named types in the compilation (including referenced assemblies) var allTypes = GetAllNamedTypes(context.Compilation.GlobalNamespace); @@ -111,12 +115,21 @@ private static bool HasConcreteInheritingClassesWithInheritsTests(SymbolAnalysis continue; } + // Only consider types that are defined in source (not from referenced assemblies) + if (!type.Locations.Any(l => l.IsInSource)) + { + continue; + } + // Check if this type inherits from our abstract class var baseType = type.BaseType; while (baseType != null) { if (SymbolEqualityComparer.Default.Equals(baseType, abstractClass)) { + // Found a concrete subclass in the source + hasAnyConcreteSubclasses = true; + // Check if this type has [InheritsTests] attribute var hasInheritsTests = type.GetAttributes().Any(attr => attr.AttributeClass?.GloballyQualified() ==