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
89 changes: 89 additions & 0 deletions TUnit.Analyzers.Tests/DataSourceGeneratorAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,93 @@ public record BaseModel;
"""
);
}

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

namespace TUnit;

// This reproduces the issue from GitHub issue #2801
// Custom data source generator that wraps the target type
public sealed class ApplicationFixtureGeneratorAttribute<T> : DataSourceGeneratorAttribute<ApplicationFixture<T>>
where T : notnull
{
protected override IEnumerable<Func<ApplicationFixture<T>>> GenerateDataSources(
DataGeneratorMetadata dataGeneratorMetadata)
{
yield return () => new ApplicationFixture<T>();
}
}

public class ApplicationFixture<T>
{
// Some implementation
}

public interface IGantryMethods
{
// Some interface
}

// This should not produce TUnit0001 error since types match:
// Attribute produces: ApplicationFixture<IGantryMethods>
// Constructor expects: ApplicationFixture<IGantryMethods>
[ApplicationFixtureGenerator<IGantryMethods>]
public class MethodCallingTest(ApplicationFixture<IGantryMethods> appFixture)
{
[Test]
public void SomeTest()
{
}
}
"""
);
}

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

namespace TUnit;

// Custom data source generator that produces different type than expected
public sealed class WrongTypeGeneratorAttribute<T> : DataSourceGeneratorAttribute<string>
{
protected override IEnumerable<Func<string>> GenerateDataSources(
DataGeneratorMetadata dataGeneratorMetadata)
{
yield return () => "test";
}
}

// This should produce TUnit0001 error since types don't match:
// Attribute produces: string
// Constructor expects: int
[{|#0:WrongTypeGenerator<int>|}]
public class TypeMismatchTest(int value)
{
[Test]
public void SomeTest()
{
}
}
""",
Verifier.Diagnostic(Rules.WrongArgumentTypeTestData)
.WithLocation(0)
.WithArguments("string", "int")
);
}
}
1 change: 1 addition & 0 deletions TUnit.Analyzers/Helpers/WellKnown.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static class AttributeFullyQualifiedClasses
public static readonly FullyQualifiedTypeName DependsOnAttribute = GetTypeName("DependsOnAttribute");

public static readonly FullyQualifiedTypeName IDataSourceAttribute = GetTypeName("IDataSourceAttribute");
public static readonly FullyQualifiedTypeName ITypedDataSourceAttribute = GetTypeName("ITypedDataSourceAttribute");
public static readonly FullyQualifiedTypeName IAsyncUntypedDataSourceGeneratorAttribute = GetTypeName("IAsyncUntypedDataSourceGeneratorAttribute");

public static readonly FullyQualifiedTypeName CancellationToken = new("System.Threading.CancellationToken");
Expand Down
41 changes: 34 additions & 7 deletions TUnit.Analyzers/TestDataAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -811,22 +811,49 @@ private void CheckDataGenerator(SymbolAnalysisContext context,
// Get type arguments from the attribute or its base types
var typeArguments = ImmutableArray<ITypeSymbol>.Empty;

// First check if the attribute itself has type arguments
if (attribute.AttributeClass?.TypeArguments.IsEmpty == false)
// 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");

if (typedInterface != null)
{
typeArguments = attribute.AttributeClass.TypeArguments;
// If the type is a tuple, extract its elements
if (typedInterface.TypeArguments.Length == 1 &&
typedInterface.TypeArguments[0] is INamedTypeSymbol { IsTupleType: true } tupleType)
{
typeArguments = ImmutableArray.CreateRange(tupleType.TupleElements.Select(x => x.Type));
}
else
{
typeArguments = typedInterface.TypeArguments;
}
}
else
{
// Otherwise, look for type arguments in base types (e.g., DataSourceGeneratorAttribute<T>)
// Fallback: Look specifically for DataSourceGeneratorAttribute or AsyncDataSourceGeneratorAttribute base types
// which contain the actual data type arguments, not the custom attribute's type parameters
foreach (var baseType in selfAndBaseTypes)
{
if (!baseType.TypeArguments.IsEmpty)
if (baseType.IsGenericType && !baseType.TypeArguments.IsEmpty)
{
typeArguments = baseType.TypeArguments;
break;
var originalDef = baseType.OriginalDefinition;
var metadataName = originalDef?.ToDisplayString();

if (metadataName?.Contains("DataSourceGeneratorAttribute") == true ||
metadataName?.Contains("AsyncDataSourceGeneratorAttribute") == true)
{
typeArguments = baseType.TypeArguments;
break;
}
}
}

// Final fallback: if no specific data source generator base type found, use the attribute's own type arguments
if (typeArguments.IsEmpty && attribute.AttributeClass?.TypeArguments.IsEmpty == false)
{
typeArguments = attribute.AttributeClass.TypeArguments;
}
}

// If still no type arguments (like ArgumentsAttribute which returns object?[]?),
Expand Down
Loading