From 025f4d03b293cb728b9181a72ce8433531ebf11f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:12:45 +0000 Subject: [PATCH 1/3] Initial plan From 6b2570a5aa3bdbf6fea67b2a190b8c6f9fde1bd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:24:43 +0000 Subject: [PATCH 2/3] Fix TUnit0001 analyzer to detect TypedDataSourceAttribute parameter mismatches - Fix dispatch in Analyze() to route ITypedDataSourceAttribute 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> --- .../DataDrivenTestArgumentsAnalyzerTests.cs | 72 +++++++++++++++++++ TUnit.Analyzers/TestDataAnalyzer.cs | 15 +++- 2 files changed, 84 insertions(+), 3 deletions(-) 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..fb7878df7e 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; } @@ -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 // (excluding ClassDataSourceAttribute which is handled above) if (attribute.AttributeClass != null && !attribute.AttributeClass.ToDisplayString()?.StartsWith("TUnit.Core.ClassDataSourceAttribute<") == true) @@ -246,6 +247,14 @@ private void Analyze(SymbolAnalysisContext context, } } + if (!isDataSourceGenerator) + { + // Check if the attribute implements ITypedDataSourceAttribute + isDataSourceGenerator = attribute.AttributeClass.AllInterfaces + .Any(i => i.IsGenericType && + i.ConstructedFrom.GloballyQualifiedNonGeneric() == WellKnown.AttributeFullyQualifiedClasses.ITypedDataSourceAttribute.WithGlobalPrefix); + } + if (isDataSourceGenerator) { var typesToValidate = propertySymbol != null @@ -902,7 +911,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) { From 42e4de5d2fe3c86b394d0c3b81fe8f0aa747edc1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:05:27 +0000 Subject: [PATCH 3/3] Refactor dispatch to use IDataSourceAttribute generically instead of special-casing subtypes Address review feedback: instead of checking for ITypedDataSourceAttribute specifically, use the base IDataSourceAttribute interface to route ALL data source attributes to CheckDataGenerator(). Added continue statements to prevent double-dispatching for attributes already handled by specific handlers (Arguments, MethodDataSource). Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com> --- TUnit.Analyzers/TestDataAnalyzer.cs | 52 ++++------------------------- 1 file changed, 7 insertions(+), 45 deletions(-) diff --git a/TUnit.Analyzers/TestDataAnalyzer.cs b/TUnit.Analyzers/TestDataAnalyzer.cs index fb7878df7e..7451f1d3d4 100644 --- a/TUnit.Analyzers/TestDataAnalyzer.cs +++ b/TUnit.Analyzers/TestDataAnalyzer.cs @@ -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,59 +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 - // or implement ITypedDataSourceAttribute - // (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) - { - // Check if the attribute implements ITypedDataSourceAttribute - isDataSourceGenerator = attribute.AttributeClass.AllInterfaces - .Any(i => i.IsGenericType && - i.ConstructedFrom.GloballyQualifiedNonGeneric() == WellKnown.AttributeFullyQualifiedClasses.ITypedDataSourceAttribute.WithGlobalPrefix); - } - - if (isDataSourceGenerator) - { - var typesToValidate = propertySymbol != null - ? ImmutableArray.Create(propertySymbol.Type) - : types; - CheckDataGenerator(context, attribute, typesToValidate); - } - } - } }