diff --git a/TUnit.Analyzers.Tests/DataDrivenTestArgumentsAnalyzerTests.cs b/TUnit.Analyzers.Tests/DataDrivenTestArgumentsAnalyzerTests.cs index 2b4f4a3cef..bb1cec15a9 100644 --- a/TUnit.Analyzers.Tests/DataDrivenTestArgumentsAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/DataDrivenTestArgumentsAnalyzerTests.cs @@ -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 + { + public override async IAsyncEnumerable>> 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 + { + public override async IAsyncEnumerable>> GetTypedDataRowsAsync( + DataGeneratorMetadata dataGeneratorMetadata) + { + await Task.Yield(); + yield return () => Task.FromResult("one"); + } + } + """ + ); + } } diff --git a/TUnit.Analyzers/TestDataAnalyzer.cs b/TUnit.Analyzers/TestDataAnalyzer.cs index 8906247bfb..7451f1d3d4 100644 --- a/TUnit.Analyzers/TestDataAnalyzer.cs +++ b/TUnit.Analyzers/TestDataAnalyzer.cs @@ -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; } @@ -189,6 +189,7 @@ private void Analyze(SymbolAnalysisContext context, context.Compilation.GetTypeByMetadataName(WellKnown.AttributeFullyQualifiedClasses.Arguments.WithoutGlobalPrefix))) { CheckArguments(context, attribute, parameters, propertySymbol); + continue; } if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, @@ -200,6 +201,7 @@ private void Analyze(SymbolAnalysisContext context, : parameters.Select(p => p.Type).ToImmutableArray().WithoutCancellationTokenParameter(); CheckMethodDataSource(context, attribute, testClassType, typesToValidate, propertySymbol); + continue; } if (attribute.AttributeClass?.IsGenericType is true @@ -211,50 +213,19 @@ private void Analyze(SymbolAnalysisContext context, ? ImmutableArray.Create(propertySymbol.Type) : parameters.Select(p => p.Type).ToImmutableArray().WithoutCancellationTokenParameter(); CheckMethodDataSource(context, attribute, testClassType, typesToValidate, propertySymbol); + continue; } - // Check for ClassDataSourceAttribute by fully qualified name - if (attribute.AttributeClass?.ToDisplayString()?.StartsWith("TUnit.Core.ClassDataSourceAttribute<") == true) + // For all other IDataSourceAttribute implementations, check type arguments + if (attribute.AttributeClass != null && + dataSourceInterface != null && + attribute.AttributeClass.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, dataSourceInterface))) { var typesToValidate = propertySymbol != null ? ImmutableArray.Create(propertySymbol.Type) : types; CheckDataGenerator(context, attribute, typesToValidate); } - - // Check for any custom data source generators that inherit from known base classes - // (excluding ClassDataSourceAttribute which is handled above) - if (attribute.AttributeClass != null && - !attribute.AttributeClass.ToDisplayString()?.StartsWith("TUnit.Core.ClassDataSourceAttribute<") == true) - { - var isDataSourceGenerator = false; - var selfAndBaseTypes = attribute.AttributeClass.GetSelfAndBaseTypes(); - - foreach (var type in selfAndBaseTypes) - { - if (type.IsGenericType && type.TypeArguments.Length > 0) - { - var originalDef = type.OriginalDefinition; - var metadataName = originalDef?.ToDisplayString(); - - if (metadataName?.Contains("DataSourceGeneratorAttribute") == true || - metadataName?.Contains("AsyncDataSourceGeneratorAttribute") == true) - { - isDataSourceGenerator = true; - break; - } - } - } - - if (isDataSourceGenerator) - { - var typesToValidate = propertySymbol != null - ? ImmutableArray.Create(propertySymbol.Type) - : types; - CheckDataGenerator(context, attribute, typesToValidate); - } - } - } } @@ -902,7 +873,7 @@ private void CheckDataGenerator(SymbolAnalysisContext context, // First, try the same approach as the source generator: look for ITypedDataSourceAttribute 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) {