Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> TestData() => new[] { 1, 2, 3 };

Expand All @@ -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<int> SingletonServices() => new[] { 1, 2, 3 };

Expand All @@ -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)]
Expand All @@ -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(
"""
Expand All @@ -155,19 +151,15 @@ public class TestData
{
}

public abstract class {|#0:AbstractTestBase|}
public abstract class AbstractTestBase
{
[Test]
[ClassDataSource<TestData>]
public void DataDrivenTest(TestData data)
{
}
}
""",

Verifier.Diagnostic(Rules.AbstractTestClassWithDataSources)
.WithLocation(0)
.WithArguments("AbstractTestBase")
"""
);
}

Expand Down
21 changes: 17 additions & 4 deletions TUnit.Analyzers/AbstractTestClassWithDataSourcesAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);

Expand All @@ -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() ==
Expand Down
Loading