From ef04f2cfedf8ea1f1f4037f7f9bbcde7f8c6912a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:46:51 +0000 Subject: [PATCH] fix: TUnit0059 false positive when no subclasses exist in assembly Don't warn about abstract test classes with data sources when there are no concrete subclasses in the same assembly. This supports the library pattern where abstract test classes are meant to be subclassed by consuming assemblies. Fixes #4607 --- ...ctTestClassWithDataSourcesAnalyzerTests.cs | 48 ++++++++----------- ...bstractTestClassWithDataSourcesAnalyzer.cs | 21 ++++++-- 2 files changed, 37 insertions(+), 32 deletions(-) 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() ==