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
173 changes: 173 additions & 0 deletions TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using Verifier = TUnit.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier<TUnit.Analyzers.AbstractTestClassWithDataSourcesAnalyzer>;

namespace TUnit.Analyzers.Tests;

public class AbstractTestClassWithDataSourcesAnalyzerTests
{
[Test]
public async Task No_Warning_For_Concrete_Class_With_Data_Source()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Collections.Generic;

public class ConcreteTests
{
public static IEnumerable<int> TestData() => new[] { 1, 2, 3 };

[Test]
[MethodDataSource(nameof(TestData))]
public void DataDrivenTest(int value)
{
}
}
"""
);
}

[Test]
public async Task No_Warning_For_Abstract_Class_Without_Data_Sources()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;

public abstract class AbstractTestBase
{
[Test]
public void SimpleTest()
{
}
}
"""
);
}

[Test]
public async Task No_Warning_For_Abstract_Class_Without_Tests()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;

public abstract class AbstractBase
{
public void HelperMethod()
{
}
}
"""
);
}

[Test]
public async Task Warning_For_Abstract_Class_With_MethodDataSource()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Collections.Generic;

public abstract class {|#0:AbstractTestBase|}
{
public static IEnumerable<int> TestData() => new[] { 1, 2, 3 };

[Test]
[MethodDataSource(nameof(TestData))]
public void DataDrivenTest(int value)
{
}
}
""",

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

[Test]
public async Task Warning_For_Abstract_Class_With_InstanceMethodDataSource()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Collections.Generic;

public abstract class {|#0:ServiceCollectionTest|}
{
public IEnumerable<int> SingletonServices() => new[] { 1, 2, 3 };

[Test]
[InstanceMethodDataSource(nameof(SingletonServices))]
public void ServiceCanBeCreatedAsSingleton(int value)
{
}
}
""",

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

[Test]
public async Task Warning_For_Abstract_Class_With_Arguments()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;

public abstract class {|#0:AbstractTestBase|}
{
[Test]
[Arguments(1)]
[Arguments(2)]
public void DataDrivenTest(int value)
{
}
}
""",

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

[Test]
public async Task Warning_For_Abstract_Class_With_ClassDataSource()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;

public class TestData
{
}

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

Verifier.Diagnostic(Rules.AbstractTestClassWithDataSources)
.WithLocation(0)
.WithArguments("AbstractTestBase")
);
}
}
92 changes: 92 additions & 0 deletions TUnit.Analyzers/AbstractTestClassWithDataSourcesAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using TUnit.Analyzers.Extensions;
using TUnit.Analyzers.Helpers;

namespace TUnit.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AbstractTestClassWithDataSourcesAnalyzer : ConcurrentDiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(Rules.AbstractTestClassWithDataSources);

protected override void InitializeInternal(AnalysisContext context)
{
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
}

private void AnalyzeSymbol(SymbolAnalysisContext context)
{
if (context.Symbol is not INamedTypeSymbol namedTypeSymbol)
{
return;
}

// Only analyze abstract classes
if (!namedTypeSymbol.IsAbstract)
{
return;
}

// Check if it's a test class
if (!namedTypeSymbol.IsTestClass(context.Compilation))
{
return;
}

// Get all test methods in this class
var testMethods = namedTypeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.Where(m => m.IsTestMethod(context.Compilation))
.ToList();

if (!testMethods.Any())
{
return;
}

// Check if any test method has a data source attribute
var hasDataSourceAttributes = testMethods.Any(method =>
{
var attributes = method.GetAttributes();
return attributes.Any(attr =>
{
var attributeClass = attr.AttributeClass;
if (attributeClass == null)
{
return false;
}

// Check for data source attributes
var currentType = attributeClass;
while (currentType != null)
{
var typeName = currentType.Name;

// Check for known data source attributes
if (typeName.Contains("DataSource") || typeName == "ArgumentsAttribute")
{
return true;
}

currentType = currentType.BaseType;
}

// Also check if it implements IDataSourceAttribute
return attributeClass.AllInterfaces.Any(i =>
i.GloballyQualified() == WellKnown.AttributeFullyQualifiedClasses.IDataSourceAttribute.WithGlobalPrefix);
});
});

if (hasDataSourceAttributes)
{
context.ReportDiagnostic(Diagnostic.Create(
Rules.AbstractTestClassWithDataSources,
namedTypeSymbol.Locations.FirstOrDefault(),
namedTypeSymbol.Name)
);
}
}
}
2 changes: 2 additions & 0 deletions TUnit.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

### New Rules

#### Test Method and Structure Rules

Check warning on line 5 in TUnit.Analyzers/AnalyzerReleases.Shipped.md

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Brak nagłówka wydania „#### Test Method and Structure Rules” w pliku wydania analizatora „AnalyzerReleases.Shipped.md” lub jest on nieprawidłowy (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 5 in TUnit.Analyzers/AnalyzerReleases.Shipped.md

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

In der Analysetool-Releasedatei "AnalyzerReleases.Shipped.md" fehlt der Releaseheader "#### Test Method and Structure Rules" oder ist ungültig. (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 5 in TUnit.Analyzers/AnalyzerReleases.Shipped.md

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Le fichier de version d'analyseur 'AnalyzerReleases.Shipped.md' a un en-tête de version '#### Test Method and Structure Rules' non valide, ou celui-ci est manquant (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 5 in TUnit.Analyzers/AnalyzerReleases.Shipped.md

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Analyzer release file 'AnalyzerReleases.Shipped.md' has a missing or invalid release header '#### Test Method and Structure Rules' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 5 in TUnit.Analyzers/AnalyzerReleases.Shipped.md

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Analyzer release file 'AnalyzerReleases.Shipped.md' has a missing or invalid release header '#### Test Method and Structure Rules' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 5 in TUnit.Analyzers/AnalyzerReleases.Shipped.md

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Analyzer release file 'AnalyzerReleases.Shipped.md' has a missing or invalid release header '#### Test Method and Structure Rules' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
Rule ID | Category | Severity | Notes
--------|----------|----------|------------------------------------------------
TUnit0001 | Usage | Error | Test argument types don't match method parameters
Expand Down Expand Up @@ -32,6 +32,7 @@
TUnit0049 | Usage | Error | [Matrix] parameters require [MatrixDataSource] attribute on the test method
TUnit0050 | Usage | Error | Too many test arguments provided
TUnit0056 | Usage | Error | Instance data source methods must use [InstanceMethodDataSource] attribute
TUnit0060 | Usage | Info | Data source may produce no tests - ensure it provides at least one test case

#### Hook and Lifecycle Rules
Rule ID | Category | Severity | Notes
Expand All @@ -54,6 +55,7 @@
TUnit0029 | Usage | Error | Duplicate attribute where only one is allowed
TUnit0030 | Usage | Warning | Test class doesn't inherit base class tests - add [InheritsTests] to include them
TUnit0032 | Usage | Error | [DependsOn] and [NotInParallel] attributes conflict - tests with dependencies must support parallel execution
TUnit0059 | Usage | Warning | Abstract test class with data sources requires [InheritsTests] on concrete class to execute tests
TUnit0033 | Usage | Error | Circular or conflicting test dependencies detected

#### Async and Execution Rules
Expand Down
18 changes: 18 additions & 0 deletions TUnit.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,24 @@
<data name="TUnit0058Title" xml:space="preserve">
<value>Hook method has unknown parameters</value>
</data>
<data name="TUnit0059Description" xml:space="preserve">
<value>Abstract test class has test methods with data source attributes. Tests from abstract classes require a concrete class with [InheritsTests] attribute to be executed.</value>
</data>
<data name="TUnit0059MessageFormat" xml:space="preserve">
<value>Abstract test class '{0}' has test methods with data sources. Add [InheritsTests] on a concrete class to execute these tests.</value>
</data>
<data name="TUnit0059Title" xml:space="preserve">
<value>Abstract test class with data sources requires [InheritsTests]</value>
</data>
<data name="TUnit0060Description" xml:space="preserve">
<value>Instance method data source may return an empty collection at runtime, which would result in no tests being generated. Ensure the data source method returns at least one item.</value>
</data>
<data name="TUnit0060MessageFormat" xml:space="preserve">
<value>Data source '{0}' may return no data. Ensure it provides at least one test case.</value>
</data>
<data name="TUnit0060Title" xml:space="preserve">
<value>Data source may produce no tests</value>
</data>
<data name="TUnit0300Description" xml:space="preserve">
<value>Generic types and methods may not be AOT-compatible when using dynamic type creation. Consider using concrete types or ensure all generic combinations are known at compile time.</value>
</data>
Expand Down
6 changes: 6 additions & 0 deletions TUnit.Analyzers/Rules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ public static class Rules
public static readonly DiagnosticDescriptor HookUnknownParameters =
CreateDescriptor("TUnit0058", UsageCategory, DiagnosticSeverity.Error);

public static readonly DiagnosticDescriptor AbstractTestClassWithDataSources =
CreateDescriptor("TUnit0059", UsageCategory, DiagnosticSeverity.Warning);

public static readonly DiagnosticDescriptor PotentialEmptyDataSource =
CreateDescriptor("TUnit0060", UsageCategory, DiagnosticSeverity.Info);

public static readonly DiagnosticDescriptor GenericTypeNotAotCompatible =
CreateDescriptor("TUnit0300", UsageCategory, DiagnosticSeverity.Warning);

Expand Down
7 changes: 4 additions & 3 deletions TUnit.Analyzers/TestDataAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public class TestDataAnalyzer : ConcurrentDiagnosticAnalyzer
Rules.ReturnFunc,
Rules.MatrixDataSourceAttributeRequired,
Rules.TooManyArguments,
Rules.InstanceMethodSource
Rules.InstanceMethodSource,
Rules.PotentialEmptyDataSource
);

protected override void InitializeInternal(AnalysisContext context)
Expand Down Expand Up @@ -194,10 +195,10 @@ private void Analyze(SymbolAnalysisContext context,
context.Compilation.GetTypeByMetadataName(WellKnown.AttributeFullyQualifiedClasses.MethodDataSource.WithoutGlobalPrefix)))
{
// For property injection, only validate against the property type, not method parameters
var typesToValidate = propertySymbol != null
var typesToValidate = propertySymbol != null
? ImmutableArray.Create(propertySymbol.Type)
: parameters.Select(p => p.Type).ToImmutableArray().WithoutCancellationTokenParameter();

CheckMethodDataSource(context, attribute, testClassType, typesToValidate, propertySymbol);
}

Expand Down
Loading