Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Fix TUnit0001 analyzer to detect TypedDataSourceAttribute parameter m…
…ismatches

- Fix dispatch in Analyze() to route ITypedDataSourceAttribute<T> implementations to CheckDataGenerator()
- Fix interface detection filter to use AllInterfaces for indirect IDataSourceAttribute implementations
- Fix CheckDataGenerator to use GloballyQualifiedNonGeneric() for correct open generic comparison
- Add tests for TypedDataSourceAttribute parameter validation

Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
  • Loading branch information
Copilot and thomhurst committed Feb 11, 2026
commit 6b2570a5aa3bdbf6fea67b2a190b8c6f9fde1bd6
72 changes: 72 additions & 0 deletions TUnit.Analyzers.Tests/DataDrivenTestArgumentsAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,76 @@ public void Create_Unit_Test(string? something)
.WithLocation(0)
);
}

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

public class MyClass
{
[Test]
[{|#0:CustomData|}]
public void MyTest(string asdf, int notExistingParameter)
{
}
}

[AttributeUsage(AttributeTargets.Method)]
public class CustomData : TypedDataSourceAttribute<string>
{
public override async IAsyncEnumerable<Func<Task<string>>> GetTypedDataRowsAsync(
DataGeneratorMetadata dataGeneratorMetadata)
{
await Task.Yield();
yield return () => Task.FromResult("one");
}
}
""",

Verifier.Diagnostic(Rules.WrongArgumentTypeTestData)
.WithLocation(0)
.WithArguments("string", "string, int")
);
}

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

public class MyClass
{
[Test]
[CustomData]
public void MyTest(string value)
{
}
}

[AttributeUsage(AttributeTargets.Method)]
public class CustomData : TypedDataSourceAttribute<string>
{
public override async IAsyncEnumerable<Func<Task<string>>> GetTypedDataRowsAsync(
DataGeneratorMetadata dataGeneratorMetadata)
{
await Task.Yield();
yield return () => Task.FromResult("one");
}
}
"""
);
}
}
15 changes: 12 additions & 3 deletions TUnit.Analyzers/TestDataAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ private void Analyze(SymbolAnalysisContext context,
}
}

// Also check if we have IDataSourceAttribute interface
// Also check if we have IDataSourceAttribute interface (use AllInterfaces to catch indirect implementations)
if (dataSourceInterface != null &&
currentType.Interfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, dataSourceInterface)))
currentType.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, dataSourceInterface)))
{
return true;
}
Expand Down Expand Up @@ -223,6 +223,7 @@ private void Analyze(SymbolAnalysisContext context,
}

// Check for any custom data source generators that inherit from known base classes
// or implement ITypedDataSourceAttribute<T>
// (excluding ClassDataSourceAttribute which is handled above)
if (attribute.AttributeClass != null &&
!attribute.AttributeClass.ToDisplayString()?.StartsWith("TUnit.Core.ClassDataSourceAttribute<") == true)
Expand All @@ -246,6 +247,14 @@ private void Analyze(SymbolAnalysisContext context,
}
}

if (!isDataSourceGenerator)
{
// Check if the attribute implements ITypedDataSourceAttribute<T>
isDataSourceGenerator = attribute.AttributeClass.AllInterfaces
.Any(i => i.IsGenericType &&
i.ConstructedFrom.GloballyQualifiedNonGeneric() == WellKnown.AttributeFullyQualifiedClasses.ITypedDataSourceAttribute.WithGlobalPrefix);
}

if (isDataSourceGenerator)
{
var typesToValidate = propertySymbol != null
Expand Down Expand Up @@ -902,7 +911,7 @@ private void CheckDataGenerator(SymbolAnalysisContext context,
// First, try the same approach as the source generator: look for ITypedDataSourceAttribute<T> interface
var typedInterface = attribute.AttributeClass?.AllInterfaces
.FirstOrDefault(i => i.IsGenericType &&
i.ConstructedFrom.GloballyQualified() == WellKnown.AttributeFullyQualifiedClasses.ITypedDataSourceAttribute.WithGlobalPrefix + "`1");
i.ConstructedFrom.GloballyQualifiedNonGeneric() == WellKnown.AttributeFullyQualifiedClasses.ITypedDataSourceAttribute.WithGlobalPrefix);

if (typedInterface != null)
{
Expand Down