Skip to content

Commit 6f38c4b

Browse files
authored
feat: enhance AbstractTestClassWithDataSourcesAnalyzer to conditionally report diagnostics based on inheriting classes with [InheritsTests] (#3519)
1 parent eb0b786 commit 6f38c4b

File tree

2 files changed

+168
-5
lines changed

2 files changed

+168
-5
lines changed

TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,83 @@ public void DataDrivenTest(TestData data)
170170
.WithArguments("AbstractTestBase")
171171
);
172172
}
173+
174+
[Test]
175+
public async Task No_Warning_When_Concrete_Class_With_InheritsTests_Exists()
176+
{
177+
await Verifier
178+
.VerifyAnalyzerAsync(
179+
"""
180+
using TUnit.Core;
181+
182+
[InheritsTests]
183+
public class Tests1 : Tests { }
184+
185+
[InheritsTests]
186+
public class Tests2 : Tests { }
187+
188+
public abstract class Tests
189+
{
190+
[Test]
191+
[Arguments(true)]
192+
[Arguments(false)]
193+
public void TestName(bool flag) { }
194+
195+
[Test]
196+
public void TestName2() { }
197+
}
198+
"""
199+
);
200+
}
201+
202+
[Test]
203+
public async Task No_Warning_When_Single_Concrete_Class_With_InheritsTests_Exists()
204+
{
205+
await Verifier
206+
.VerifyAnalyzerAsync(
207+
"""
208+
using TUnit.Core;
209+
using System.Collections.Generic;
210+
211+
[InheritsTests]
212+
public class ConcreteTest : AbstractTestBase { }
213+
214+
public abstract class AbstractTestBase
215+
{
216+
public static IEnumerable<int> TestData() => new[] { 1, 2, 3 };
217+
218+
[Test]
219+
[MethodDataSource(nameof(TestData))]
220+
public void DataDrivenTest(int value)
221+
{
222+
}
223+
}
224+
"""
225+
);
226+
}
227+
228+
[Test]
229+
public async Task Warning_When_Concrete_Class_Exists_But_No_InheritsTests()
230+
{
231+
await Verifier
232+
.VerifyAnalyzerAsync(
233+
"""
234+
using TUnit.Core;
235+
236+
public class Tests1 : Tests { }
237+
238+
public abstract class {|#0:Tests|}
239+
{
240+
[Test]
241+
[Arguments(true)]
242+
[Arguments(false)]
243+
public void TestName(bool flag) { }
244+
}
245+
""",
246+
247+
Verifier.Diagnostic(Rules.AbstractTestClassWithDataSources)
248+
.WithLocation(0)
249+
.WithArguments("Tests")
250+
);
251+
}
173252
}

TUnit.Analyzers/AbstractTestClassWithDataSourcesAnalyzer.cs

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,95 @@ private void AnalyzeSymbol(SymbolAnalysisContext context)
8282

8383
if (hasDataSourceAttributes)
8484
{
85-
context.ReportDiagnostic(Diagnostic.Create(
86-
Rules.AbstractTestClassWithDataSources,
87-
namedTypeSymbol.Locations.FirstOrDefault(),
88-
namedTypeSymbol.Name)
89-
);
85+
// Check if there are any concrete classes that inherit from this abstract class with [InheritsTests]
86+
var hasInheritingClasses = HasConcreteInheritingClassesWithInheritsTests(context, namedTypeSymbol);
87+
88+
// Only report the diagnostic if no inheriting classes are found
89+
if (!hasInheritingClasses)
90+
{
91+
context.ReportDiagnostic(Diagnostic.Create(
92+
Rules.AbstractTestClassWithDataSources,
93+
namedTypeSymbol.Locations.FirstOrDefault(),
94+
namedTypeSymbol.Name)
95+
);
96+
}
97+
}
98+
}
99+
100+
private static bool HasConcreteInheritingClassesWithInheritsTests(SymbolAnalysisContext context, INamedTypeSymbol abstractClass)
101+
{
102+
// Get all named types in the compilation
103+
var allTypes = GetAllNamedTypes(context.Compilation.Assembly.GlobalNamespace);
104+
105+
// Check if any concrete class inherits from the abstract class and has [InheritsTests]
106+
foreach (var type in allTypes)
107+
{
108+
// Skip abstract classes
109+
if (type.IsAbstract)
110+
{
111+
continue;
112+
}
113+
114+
// Check if this type inherits from our abstract class
115+
var baseType = type.BaseType;
116+
while (baseType != null)
117+
{
118+
if (SymbolEqualityComparer.Default.Equals(baseType, abstractClass))
119+
{
120+
// Check if this type has [InheritsTests] attribute
121+
var hasInheritsTests = type.GetAttributes().Any(attr =>
122+
attr.AttributeClass?.GloballyQualified() ==
123+
WellKnown.AttributeFullyQualifiedClasses.InheritsTestsAttribute.WithGlobalPrefix);
124+
125+
if (hasInheritsTests)
126+
{
127+
return true;
128+
}
129+
130+
break;
131+
}
132+
133+
baseType = baseType.BaseType;
134+
}
135+
}
136+
137+
return false;
138+
}
139+
140+
private static IEnumerable<INamedTypeSymbol> GetAllNamedTypes(INamespaceSymbol namespaceSymbol)
141+
{
142+
foreach (var member in namespaceSymbol.GetMembers())
143+
{
144+
if (member is INamedTypeSymbol namedType)
145+
{
146+
yield return namedType;
147+
148+
// Recursively get nested types
149+
foreach (var nestedType in GetNestedTypes(namedType))
150+
{
151+
yield return nestedType;
152+
}
153+
}
154+
else if (member is INamespaceSymbol childNamespace)
155+
{
156+
foreach (var type in GetAllNamedTypes(childNamespace))
157+
{
158+
yield return type;
159+
}
160+
}
161+
}
162+
}
163+
164+
private static IEnumerable<INamedTypeSymbol> GetNestedTypes(INamedTypeSymbol typeSymbol)
165+
{
166+
foreach (var member in typeSymbol.GetTypeMembers())
167+
{
168+
yield return member;
169+
170+
foreach (var nestedType in GetNestedTypes(member))
171+
{
172+
yield return nestedType;
173+
}
90174
}
91175
}
92176
}

0 commit comments

Comments
 (0)