From 14311d0509ce6a5e20a1188708f30ec89f5c476e Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:19:04 +0100 Subject: [PATCH 01/30] perf: inline AOT converter registrations into per-class test source files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates the separate AotConverters.g.cs file and its pipeline branch (which used .Collect() — a fan-in that invalidated all converters when any test method changed). Converter registrations are now emitted inline in the same generated file as the test entries, using simple lambda-based AotConverterRegistry.Register() calls. Key changes: - Merge AotConverterGenerator into TestMetadataGenerator as inline code - Replace IAotConverter class generation with one-line lambda registrations - Remove .Collect() fan-in for converters (was a perf anti-pattern) - Add string-based equality to TestMethodMetadata for incremental caching - Use existing DataSourceAttributeHelper instead of hand-rolled checks --- .../AotConverterGeneratorTests.cs | 2 +- ...thImplicitConverterTests.Test.verified.txt | 10 + .../Basic.Test.verified.txt | 1 - ...nflictingNamespace.DotNet10_0.verified.txt | 10 + ...onflictingNamespace.DotNet8_0.verified.txt | 10 + ...onflictingNamespace.DotNet9_0.verified.txt | 10 + ...nflictingNamespace.DotNet10_0.verified.txt | 12 + ...onflictingNamespace.DotNet8_0.verified.txt | 12 + ...onflictingNamespace.DotNet9_0.verified.txt | 12 + .../DataDrivenTests.Test.verified.txt | 10 + .../DecimalArgumentTests.Test.verified.txt | 30 + .../MatrixTests.Test.verified.txt | 12 + ...ullableByteArgumentTests.Test.verified.txt | 10 + .../TestDiscoveryHookTests.Test.verified.txt | 1 - .../Tests1899.BaseClass.verified.txt | 1 - .../Tests2075.Test.verified.txt | 1 - TUnit.Core.SourceGenerator.Tests/TestsBase.cs | 1 - .../Generators/AotConverterGenerator.cs | 726 ------------------ .../Generators/AotConverterHelper.cs | 322 ++++++++ .../Generators/TestMetadataGenerator.cs | 90 ++- .../Models/ClassTestGroup.cs | 2 + .../Models/TestMethodMetadata.cs | 79 +- .../AotConverterGeneratorBenchmarks.cs | 33 - TUnit.SourceGenerator.Benchmarks/Program.cs | 2 +- .../AotConverterGeneratorIncrementalTests.cs | 118 --- .../TestMetadataGeneratorIncrementalTests.cs | 164 ++++ 26 files changed, 784 insertions(+), 897 deletions(-) delete mode 100644 TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs create mode 100644 TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs delete mode 100644 TUnit.SourceGenerator.Benchmarks/AotConverterGeneratorBenchmarks.cs delete mode 100644 TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs create mode 100644 TUnit.SourceGenerator.IncrementalTests/TestMetadataGeneratorIncrementalTests.cs diff --git a/TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.cs b/TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.cs index 3c1095c104..62204a8cd0 100644 --- a/TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.cs +++ b/TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.cs @@ -6,7 +6,7 @@ public class AotConverterGeneratorTests : TestsBase { [Test] [Skip("Need to investigate - Behaves differently on local vs CI")] - public Task GeneratesCode() => AotConverterGenerator.RunTest( + public Task GeneratesCode() => TestMetadataGenerator.RunTest( Path.GetTempFileName(), new RunTestOptions { diff --git a/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt index 4d4f8cd19b..8de5b0b5c1 100644 --- a/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt @@ -151,3 +151,13 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource = RegisterAot_TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource(); + static int RegisterAot_TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (global::TUnit.TestProject.ExplicitInteger)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (global::TUnit.TestProject.ImplicitInteger)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/Basic.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Basic.Test.verified.txt index 5f282702bb..e69de29bb2 100644 --- a/TUnit.Core.SourceGenerator.Tests/Basic.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Basic.Test.verified.txt @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt index d755ba780c..eae495bd18 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt @@ -524,3 +524,13 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DataDrivenTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DataDrivenTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_DataDrivenTests__TestSource = RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource(); + static int RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt index d755ba780c..eae495bd18 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt @@ -524,3 +524,13 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DataDrivenTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DataDrivenTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_DataDrivenTests__TestSource = RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource(); + static int RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt index d755ba780c..eae495bd18 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt @@ -524,3 +524,13 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DataDrivenTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DataDrivenTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_DataDrivenTests__TestSource = RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource(); + static int RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt index ebe16e36c3..b159506332 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt @@ -664,3 +664,15 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_MatrixTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_MatrixTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_MatrixTests__TestSource = RegisterAot_TUnit_TestProject_MatrixTests__TestSource(); + static int RegisterAot_TUnit_TestProject_MatrixTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt index ebe16e36c3..b159506332 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt @@ -664,3 +664,15 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_MatrixTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_MatrixTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_MatrixTests__TestSource = RegisterAot_TUnit_TestProject_MatrixTests__TestSource(); + static int RegisterAot_TUnit_TestProject_MatrixTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt index ebe16e36c3..b159506332 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt @@ -664,3 +664,15 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_MatrixTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_MatrixTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_MatrixTests__TestSource = RegisterAot_TUnit_TestProject_MatrixTests__TestSource(); + static int RegisterAot_TUnit_TestProject_MatrixTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt index d755ba780c..eae495bd18 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt @@ -524,3 +524,13 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DataDrivenTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DataDrivenTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_DataDrivenTests__TestSource = RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource(); + static int RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt index 6dbf771d6d..b8a24ab254 100644 --- a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt @@ -611,3 +611,33 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DecimalArgumentTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DecimalArgumentTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_DecimalArgumentTests__TestSource = RegisterAot_TUnit_TestProject_DecimalArgumentTests__TestSource(); + static int RegisterAot_TUnit_TestProject_DecimalArgumentTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (byte)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (sbyte)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (char)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (short)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (ushort)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (int)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (uint)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (long)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (ulong)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (float)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (double)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt index 69a67571e7..c6557217d9 100644 --- a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt @@ -664,3 +664,15 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_MatrixTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_MatrixTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_MatrixTests__TestSource = RegisterAot_TUnit_TestProject_MatrixTests__TestSource(); + static int RegisterAot_TUnit_TestProject_MatrixTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (global::TUnit.TestProject.TestEnum?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (global::TUnit.TestProject.TestEnum)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt index a72bd1f019..8e41b847d2 100644 --- a/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt @@ -150,3 +150,13 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_NullableByteArgumentTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_NullableByteArgumentTests__TestSource.Entries); } +internal static partial class TUnit_TestRegistration +{ + static readonly int _aot_TUnit_TestProject_NullableByteArgumentTests__TestSource = RegisterAot_TUnit_TestProject_NullableByteArgumentTests__TestSource(); + static int RegisterAot_TUnit_TestProject_NullableByteArgumentTests__TestSource() + { + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (byte?)source); + global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (byte)source); + return 0; + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/TestDiscoveryHookTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/TestDiscoveryHookTests.Test.verified.txt index 5f282702bb..e69de29bb2 100644 --- a/TUnit.Core.SourceGenerator.Tests/TestDiscoveryHookTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/TestDiscoveryHookTests.Test.verified.txt @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1899.BaseClass.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1899.BaseClass.verified.txt index 5f282702bb..e69de29bb2 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1899.BaseClass.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1899.BaseClass.verified.txt @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2075.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2075.Test.verified.txt index 5f282702bb..e69de29bb2 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2075.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2075.Test.verified.txt @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/TUnit.Core.SourceGenerator.Tests/TestsBase.cs b/TUnit.Core.SourceGenerator.Tests/TestsBase.cs index 91ff554b83..8e44857d42 100644 --- a/TUnit.Core.SourceGenerator.Tests/TestsBase.cs +++ b/TUnit.Core.SourceGenerator.Tests/TestsBase.cs @@ -16,7 +16,6 @@ protected TestsBase() } public TestsBase TestMetadataGenerator = new(); - public TestsBase AotConverterGenerator = new(); public TestsBase HooksGenerator = new(); public TestsBase InfrastructureGenerator = new(); public TestsBase DynamicTestsGenerator = new(); diff --git a/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs b/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs deleted file mode 100644 index 234a4a802a..0000000000 --- a/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs +++ /dev/null @@ -1,726 +0,0 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using TUnit.Core.SourceGenerator.Extensions; -using TUnit.Core.SourceGenerator.Models; - -namespace TUnit.Core.SourceGenerator.Generators; - -[Generator] -public class AotConverterGenerator : IIncrementalGenerator -{ - public static string ParseAotConverter = "ParseCompilationMetadata"; - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var enabledProvider = context.AnalyzerConfigOptionsProvider - .Select((options, _) => - { - options.GlobalOptions.TryGetValue("build_property.EnableTUnitSourceGeneration", out var value); - return !string.Equals(value, "false", StringComparison.OrdinalIgnoreCase); - }); - - var allTypes = context.CompilationProvider - .Select((compilation, ct) => - { - try - { - var conversionInfos = new List(); - ScanTestParameters(compilation, conversionInfos, ct); - - // Deduplicate conversions based on source and target types - var seenConversions = new HashSet<(ITypeSymbol Source, ITypeSymbol Target)>( - new TypePairEqualityComparer()); - var uniqueConversions = new List(); - - foreach (var conversion in conversionInfos) - { - if (conversion == null) - { - continue; - } - - var key = (conversion.SourceType, conversion.TargetType); - if (seenConversions.Add(key)) - { - uniqueConversions.Add(conversion); - } - } - - return uniqueConversions.Select(c => - { - var sourceType = ToTypeMetadata(c.SourceType); - var targetType = ToTypeMetadata(c.TargetType); - - return new ConversionMetadata() - { - SourceType = sourceType, - TargetType = targetType, - TypesAreDifferent = !SymbolEqualityComparer.Default.Equals(c.SourceType, c.TargetType), - IsImplicit = c.IsImplicit, - }; - }).ToEquatableArray(); - } - catch (NullReferenceException ex) - { - var stackTrace = ex.StackTrace ?? "No stack trace"; - throw new InvalidOperationException($"NullReferenceException in ScanTestParameters: {ex.Message}\nStack: {stackTrace}", ex); - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - throw new InvalidOperationException($"Error in AotConverterGenerator.ScanTestParameters: {ex.GetType().Name}: {ex.Message}", ex); - } - }) - .Combine(enabledProvider) - .WithTrackingName(ParseAotConverter); - - context.RegisterSourceOutput(allTypes, (spc, data) => - { - var (source, isEnabled) = data; - if (!isEnabled) - { - return; - } - try - { - GenerateConverters(spc, source); - } - catch (Exception e) - { - spc.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor( - id: "TUNITGEN001", - title: "TUnit.AotConverterGenerator Failed", - messageFormat: "AotConverterGenerator failed: {0}: {1}", - category: "TUnit.Generator", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: e.ToString()), - Location.None, - e.GetType().Name, - e.Message)); - } - }); - } - - private void ScanTestParameters(Compilation compilation, List conversionInfos, CancellationToken cancellationToken) - { - var typesToScan = new HashSet(SymbolEqualityComparer.Default); - - foreach (var tree in compilation.SyntaxTrees) - { - cancellationToken.ThrowIfCancellationRequested(); - - var semanticModel = compilation.GetSemanticModel(tree); - var root = tree.GetRoot(); - - foreach(var nodes in root.DescendantNodes()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if(nodes is MethodDeclarationSyntax method) - { - var methodSymbol = semanticModel.GetDeclaredSymbol(method); - if (methodSymbol == null) - { - continue; - } - - if (!IsTestMethod(methodSymbol)) - { - continue; - } - - foreach (var parameter in methodSymbol.Parameters) - { - typesToScan.Add(parameter.Type); - ScanAttributesForTypes(parameter.GetAttributes(), typesToScan); - } - - ScanAttributesForTypes(methodSymbol.GetAttributes(), typesToScan); - } - else if (nodes is ClassDeclarationSyntax classDecl) - { - var classSymbol = semanticModel.GetDeclaredSymbol(classDecl); - if (classSymbol == null) - { - continue; - } - - if (!IsTestClass(classSymbol)) - { - continue; - } - - ScanAttributesForTypes(classSymbol.GetAttributes(), typesToScan); - - foreach (var constructor in classSymbol.Constructors) - { - if (constructor.IsImplicitlyDeclared) - { - continue; - } - - foreach (var parameter in constructor.Parameters) - { - typesToScan.Add(parameter.Type); - ScanAttributesForTypes(parameter.GetAttributes(), typesToScan); - } - } - } - } - } - - foreach (var type in typesToScan) - { - cancellationToken.ThrowIfCancellationRequested(); - CollectConversionsForType(type, conversionInfos, compilation); - } - } - - private static bool IsTestMethod(IMethodSymbol method) - { - return method.GetAttributes().Any(attr => - { - var attrClass = attr.AttributeClass; - if (attrClass == null) - { - return false; - } - - var baseType = attrClass; - while (baseType != null) - { - if (baseType.ToDisplayString() == WellKnownFullyQualifiedClassNames.BaseTestAttribute.WithoutGlobalPrefix) - { - return true; - } - baseType = baseType.BaseType; - } - - return false; - }); - } - - private bool IsTestClass(INamedTypeSymbol classSymbol) - { - return classSymbol.GetMembers() - .OfType() - .Any(IsTestMethod); - } - - private void ScanAttributesForTypes(ImmutableArray attributes, HashSet typesToScan) - { - foreach (var attribute in attributes) - { - if (attribute.AttributeClass == null) - { - continue; - } - - if (!IsDataSourceAttribute(attribute.AttributeClass)) - { - continue; - } - - if (attribute.AttributeClass.IsGenericType) - { - foreach (var typeArg in attribute.AttributeClass.TypeArguments) - { - typesToScan.Add(typeArg); - } - } - - foreach (var arg in attribute.ConstructorArguments) - { - ScanTypedConstantForTypes(arg, typesToScan); - } - - foreach (var namedArg in attribute.NamedArguments) - { - ScanTypedConstantForTypes(namedArg.Value, typesToScan); - } - } - } - - private bool IsDataSourceAttribute(INamedTypeSymbol attributeClass) - { - var currentType = attributeClass; - while (currentType != null) - { - var fullName = currentType.ToDisplayString(); - if (fullName == WellKnownFullyQualifiedClassNames.AsyncDataSourceGeneratorAttribute.WithoutGlobalPrefix || - fullName == WellKnownFullyQualifiedClassNames.AsyncUntypedDataSourceGeneratorAttribute.WithoutGlobalPrefix || - fullName == WellKnownFullyQualifiedClassNames.ArgumentsAttribute.WithoutGlobalPrefix) - { - return true; - } - - if (currentType.AllInterfaces.Any(i => - i.ToDisplayString() == WellKnownFullyQualifiedClassNames.IDataSourceAttribute.WithoutGlobalPrefix)) - { - return true; - } - - currentType = currentType.BaseType; - } - - return false; - } - - private void ScanTypedConstantForTypes(TypedConstant constant, HashSet typesToScan) - { - if (constant.IsNull) - { - return; - } - - if (constant is { Kind: TypedConstantKind.Type, Value: ITypeSymbol typeValue }) - { - typesToScan.Add(typeValue); - } - - else if (constant is { Kind: TypedConstantKind.Array, IsNull: false }) - { - foreach (var element in constant.Values) - { - ScanTypedConstantForTypes(element, typesToScan); - } - } - else if (constant.Kind != TypedConstantKind.Array && constant is { Value: not null, Type: not null }) - { - typesToScan.Add(constant.Type); - } - } - - private void CollectConversionsForType(ITypeSymbol type, List conversionInfos, Compilation compilation) - { - if (type is not INamedTypeSymbol namedType) - { - return; - } - - if (!ShouldIncludeType(namedType, compilation)) - { - return; - } - - var conversionOperators = namedType.GetMembers() - .OfType() - .Where(m => (m.Name == "op_Implicit" || m.Name == "op_Explicit") && - m is { IsStatic: true, Parameters.Length: 1 }); - - foreach (var method in conversionOperators) - { - var conversionInfo = GetConversionInfoFromSymbol(method, compilation); - if (conversionInfo != null) - { - conversionInfos.Add(conversionInfo); - } - } - - if (namedType.IsGenericType) - { - foreach (var typeArg in namedType.TypeArguments) - { - CollectConversionsForType(typeArg, conversionInfos, compilation); - } - } - } - - private bool ShouldIncludeType(INamedTypeSymbol type, Compilation compilation) - { - var typeAssembly = type.ContainingAssembly; - var currentAssembly = compilation.Assembly; - - if (currentAssembly == null) - { - return false; - } - - if (SymbolEqualityComparer.Default.Equals(typeAssembly, currentAssembly)) - { - return true; - } - - if (type.DeclaredAccessibility == Accessibility.Public) - { - return true; - } - - if (type.DeclaredAccessibility == Accessibility.Internal) - { - if (typeAssembly != null && typeAssembly.GivesAccessTo(currentAssembly)) - { - return true; - } - } - - return false; - } - - private bool IsAccessibleType(ITypeSymbol type, Compilation compilation) - { - if (type == null || compilation == null) - { - return false; - } - - if (type.SpecialType != SpecialType.None) - { - return true; - } - - if (type.TypeKind == TypeKind.TypeParameter) - { - return true; - } - - if (type is INamedTypeSymbol namedType) - { - var typeAssembly = namedType.ContainingAssembly; - var currentAssembly = compilation.Assembly; - - if (currentAssembly != null && SymbolEqualityComparer.Default.Equals(typeAssembly, currentAssembly)) - { - return true; - } - - if (namedType.DeclaredAccessibility == Accessibility.Public) - { - return true; - } - - if (namedType.DeclaredAccessibility == Accessibility.Internal) - { - if (currentAssembly == null) - { - return false; - } - - if (typeAssembly != null && typeAssembly.GivesAccessTo(currentAssembly)) - { - return true; - } - - return false; - } - - if (namedType.IsGenericType) - { - foreach (var typeArg in namedType.TypeArguments) - { - if (!IsAccessibleType(typeArg, compilation)) - { - return false; - } - } - } - - if (namedType.ContainingType != null) - { - return IsAccessibleType(namedType.ContainingType, compilation); - } - - return false; - } - - if (type is IArrayTypeSymbol arrayType) - { - return IsAccessibleType(arrayType.ElementType, compilation); - } - - if (type is IPointerTypeSymbol pointerType) - { - return IsAccessibleType(pointerType.PointedAtType, compilation); - } - - return false; - } - - private ConversionInfo? GetConversionInfoFromSymbol(IMethodSymbol methodSymbol, Compilation compilation) - { - var containingType = methodSymbol.ContainingType; - if (containingType == null) - { - return null; - } - - var sourceType = methodSymbol.Parameters[0].Type; - var targetType = methodSymbol.ReturnType; - var isImplicit = methodSymbol.Name == "op_Implicit"; - - if (sourceType.IsGenericDefinition() || targetType.IsGenericDefinition()) - { - return null; - } - - if (TypeContainsGenericTypeParameters(sourceType) || TypeContainsGenericTypeParameters(targetType)) - { - return null; - } - - if (sourceType.IsRefLikeType || targetType.IsRefLikeType) - { - return null; - } - - if (sourceType.TypeKind == TypeKind.Pointer || targetType.TypeKind == TypeKind.Pointer || - sourceType.SpecialType == SpecialType.System_Void || targetType.SpecialType == SpecialType.System_Void) - { - return null; - } - - if (!IsAccessibleType(containingType, compilation)) - { - return null; - } - - if (!IsAccessibleType(sourceType, compilation) || !IsAccessibleType(targetType, compilation)) - { - return null; - } - - return new ConversionInfo - { - SourceType = sourceType, - TargetType = targetType, - IsImplicit = isImplicit, - }; - } - - private bool TypeContainsGenericTypeParameters(ITypeSymbol type) - { - if (type.TypeKind == TypeKind.TypeParameter) - { - return true; - } - - if (type is INamedTypeSymbol namedTypeSymbol) - { - foreach (var typeArgument in namedTypeSymbol.TypeArguments) - { - if (TypeContainsGenericTypeParameters(typeArgument)) - { - return true; - } - } - } - - if (type is IArrayTypeSymbol arrayTypeSymbol) - { - return TypeContainsGenericTypeParameters(arrayTypeSymbol.ElementType); - } - - if (type is IPointerTypeSymbol pointerTypeSymbol) - { - return TypeContainsGenericTypeParameters(pointerTypeSymbol.PointedAtType); - } - - return false; - } - - private void GenerateConverters(SourceProductionContext context, EquatableArray conversions) - { - var writer = new CodeWriter(); - writer.AppendLine("#nullable enable"); - - if (conversions.Length == 0) - { - writer.AppendLine(); - writer.AppendLine("// No conversion operators found"); - context.AddSource("AotConverters.g.cs", writer.ToString()); - return; - } - - writer.AppendLine(); - writer.AppendLine("using System;"); - writer.AppendLine("using TUnit.Core.Converters;"); - writer.AppendLine(); - writer.AppendLine("namespace TUnit.Generated;"); - writer.AppendLine(); - - var converterIndex = 0; - var registrations = new List(); - - foreach (var conversion in conversions) - { - try - { - if (conversion.SourceType == null || conversion.TargetType == null) - { - var sourceDisplay = conversion.SourceType?.DisplayString ?? "null"; - var targetDisplay = conversion.TargetType?.DisplayString ?? "null"; - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor( - id: "TUNITGEN002", - title: "Null type in conversion", - messageFormat: "Skipping converter generation: SourceType={0}, TargetType={1}. Check test data sources that use implicit conversions between these types.", - category: "TUnit.Generator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true), - Location.None, - sourceDisplay, - targetDisplay)); - continue; - } - } - catch (Exception ex) - { - var sourceDisplay = conversion.SourceType?.DisplayString ?? "unknown"; - var targetDisplay = conversion.TargetType?.DisplayString ?? "unknown"; - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor( - id: "TUNITGEN003", - title: "Error checking conversion types", - messageFormat: "Error checking conversion types (SourceType={0}, TargetType={1}): {2}", - category: "TUnit.Generator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true), - Location.None, - sourceDisplay, - targetDisplay, - ex.ToString())); - continue; - } - - var converterClassName = $"AotConverter_{converterIndex++}"; - var sourceTypeName = conversion.SourceType.GloballyQualified; - var targetTypeName = conversion.TargetType.GloballyQualified; - - writer.AppendLine($"internal sealed class {converterClassName} : IAotConverter"); - writer.AppendLine("{"); - writer.Indent(); - - writer.AppendLine($"public Type SourceType => typeof({sourceTypeName});"); - writer.AppendLine($"public Type TargetType => typeof({targetTypeName});"); - writer.AppendLine(); - - writer.AppendLine("public object? Convert(object? value)"); - writer.AppendLine("{"); - writer.Indent(); - - writer.AppendLine("if (value == null) return null;"); - - // Use Zen's more robust approach for handling nullable types and type checks - var sourceType = conversion.SourceType; - var targetType = conversion.TargetType; - - writer.AppendLine($"if (value is {targetType.PatternTypeName} targetTypedValue)"); - writer.AppendLine("{"); - writer.Indent(); - writer.AppendLine("return targetTypedValue;"); - writer.Unindent(); - writer.AppendLine("}"); - - // 2. If types are different, generate the fallback check for the source type. - // This handles cases that require an implicit conversion. - if (conversion.TypesAreDifferent) - { - writer.AppendLine(); - writer.AppendLine($"if (value is {sourceType.PatternTypeName} sourceTypedValue)"); - writer.AppendLine("{"); - writer.Indent(); - // For explicit conversions, we need to use an explicit cast - // For implicit conversions, variable assignment works fine - if (conversion.IsImplicit) - { - writer.AppendLine($"{targetTypeName} converted = sourceTypedValue;"); - } - else - { - writer.AppendLine($"{targetTypeName} converted = ({targetTypeName})sourceTypedValue;"); - } - writer.AppendLine("return converted;"); - writer.Unindent(); - writer.AppendLine("}"); - } - - writer.AppendLine("return value; // Return original value if type doesn't match"); - - writer.Unindent(); - writer.AppendLine("}"); - - writer.Unindent(); - writer.AppendLine("}"); - writer.AppendLine(); - - registrations.Add($"AotConverterRegistry.Register(new {converterClassName}());"); - } - - writer.AppendLine("internal static class AotConverterRegistration"); - writer.AppendLine("{"); - writer.Indent(); - - writer.AppendLine("[global::System.Runtime.CompilerServices.ModuleInitializer]"); - writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SuppressMessage(\"Performance\", \"CA2255:The 'ModuleInitializer' attribute should not be used in libraries\","); - writer.AppendLine(" Justification = \"Test framework needs to register AOT converters for conversion operators\")]"); - writer.AppendLine("public static void Initialize()"); - writer.AppendLine("{"); - writer.Indent(); - - foreach (var registration in registrations) - { - writer.AppendLine(registration); - } - - writer.Unindent(); - writer.AppendLine("}"); - - writer.Unindent(); - writer.AppendLine("}"); - - context.AddSource("AotConverters.g.cs", writer.ToString()); - } - - private static TypeMetadata ToTypeMetadata(ITypeSymbol type) - { - var globallyQualified = type.GloballyQualified(); - - // For pattern matching, we must unwrap nullable types (C# language requirement - CS8116) - string patternTypeName = globallyQualified; - if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, TypeArguments.Length: > 0 } nullableSourceType) - { - patternTypeName = nullableSourceType.TypeArguments[0].GloballyQualified(); - } - return new TypeMetadata(globallyQualified, type.ToDisplayString(), patternTypeName); - } - - public record TypeMetadata(string GloballyQualified, string DisplayString, string PatternTypeName); - - public record ConversionMetadata - { - public required TypeMetadata SourceType { get; init; } - public required TypeMetadata TargetType { get; init; } - public required bool TypesAreDifferent { get; init; } - public required bool IsImplicit { get; init; } - } - - private class ConversionInfo - { - public required ITypeSymbol SourceType { get; init; } - public required ITypeSymbol TargetType { get; init; } - public required bool IsImplicit { get; init; } - } - - private class TypePairEqualityComparer : IEqualityComparer<(ITypeSymbol Source, ITypeSymbol Target)> - { - public bool Equals((ITypeSymbol Source, ITypeSymbol Target) x, (ITypeSymbol Source, ITypeSymbol Target) y) - { - return SymbolEqualityComparer.Default.Equals(x.Source, y.Source) && - SymbolEqualityComparer.Default.Equals(x.Target, y.Target); - } - - public int GetHashCode((ITypeSymbol Source, ITypeSymbol Target) obj) - { - unchecked - { - var hash = 17; - hash = hash * 31 + SymbolEqualityComparer.Default.GetHashCode(obj.Source); - hash = hash * 31 + SymbolEqualityComparer.Default.GetHashCode(obj.Target); - return hash; - } - } - } -} diff --git a/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs b/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs new file mode 100644 index 0000000000..08066860df --- /dev/null +++ b/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs @@ -0,0 +1,322 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using TUnit.Core.SourceGenerator.CodeGenerators.Helpers; +using TUnit.Core.SourceGenerator.Extensions; + +namespace TUnit.Core.SourceGenerator.Generators; + +public static class AotConverterHelper +{ + /// + /// Returns null if no conversions found; otherwise newline-separated Register calls. + /// + public static string? GenerateRegistrationCode( + IReadOnlyList methods, + INamedTypeSymbol containingType, + Compilation compilation) + { + var typesToScan = new HashSet(SymbolEqualityComparer.Default); + + foreach (var method in methods) + { + foreach (var parameter in method.Parameters) + { + typesToScan.Add(parameter.Type); + ScanAttributesForTypes(parameter.GetAttributes(), typesToScan); + } + + ScanAttributesForTypes(method.GetAttributes(), typesToScan); + } + + ScanAttributesForTypes(containingType.GetAttributes(), typesToScan); + + foreach (var constructor in containingType.Constructors) + { + if (constructor.IsImplicitlyDeclared) + { + continue; + } + + foreach (var parameter in constructor.Parameters) + { + typesToScan.Add(parameter.Type); + ScanAttributesForTypes(parameter.GetAttributes(), typesToScan); + } + } + + var seen = new HashSet<(string, string)>(); + var registrations = new List(); + + foreach (var type in typesToScan) + { + ScanTypeForConversions(type, seen, registrations, compilation); + } + + return registrations.Count > 0 ? string.Join("\n", registrations) : null; + } + + public static string? GenerateRegistrationCode( + IMethodSymbol method, + INamedTypeSymbol containingType, + Compilation compilation) + { + return GenerateRegistrationCode([method], containingType, compilation); + } + + private static void ScanTypeForConversions( + ITypeSymbol type, + HashSet<(string, string)> seen, + List registrations, + Compilation compilation) + { + if (type is not INamedTypeSymbol namedType) + { + return; + } + + if (!IsAccessibleType(namedType, compilation)) + { + return; + } + + foreach (var member in namedType.GetMembers()) + { + if (member is IMethodSymbol { IsStatic: true, Parameters.Length: 1 } method && + (method.Name == "op_Implicit" || method.Name == "op_Explicit")) + { + var registration = TryGenerateRegistration(method, seen, compilation); + if (registration != null) + { + registrations.Add(registration); + } + } + } + + if (namedType.IsGenericType) + { + foreach (var typeArg in namedType.TypeArguments) + { + ScanTypeForConversions(typeArg, seen, registrations, compilation); + } + } + } + + private static string? TryGenerateRegistration( + IMethodSymbol operatorMethod, + HashSet<(string, string)> seen, + Compilation compilation) + { + var sourceType = operatorMethod.Parameters[0].Type; + var targetType = operatorMethod.ReturnType; + + if (sourceType.IsGenericDefinition() || targetType.IsGenericDefinition()) + { + return null; + } + + if (TypeContainsGenericTypeParameters(sourceType) || TypeContainsGenericTypeParameters(targetType)) + { + return null; + } + + if (sourceType.IsRefLikeType || targetType.IsRefLikeType) + { + return null; + } + + if (sourceType.TypeKind == TypeKind.Pointer || targetType.TypeKind == TypeKind.Pointer || + sourceType.SpecialType == SpecialType.System_Void || targetType.SpecialType == SpecialType.System_Void) + { + return null; + } + + var containingType = operatorMethod.ContainingType; + if (containingType == null || !IsAccessibleType(containingType, compilation)) + { + return null; + } + + if (!IsAccessibleType(sourceType, compilation) || !IsAccessibleType(targetType, compilation)) + { + return null; + } + + if (SymbolEqualityComparer.Default.Equals(sourceType, targetType)) + { + return null; + } + + var sourceGlobal = sourceType.GloballyQualified(); + var targetGlobal = targetType.GloballyQualified(); + + if (!seen.Add((sourceGlobal, targetGlobal))) + { + return null; + } + + return $"global::TUnit.Core.Converters.AotConverterRegistry.Register<{sourceGlobal}, {targetGlobal}>(static source => ({targetGlobal})source);"; + } + + private static void ScanAttributesForTypes(ImmutableArray attributes, HashSet typesToScan) + { + foreach (var attribute in attributes) + { + if (attribute.AttributeClass == null) + { + continue; + } + + if (!DataSourceAttributeHelper.IsDataSourceAttribute(attribute.AttributeClass)) + { + continue; + } + + if (attribute.AttributeClass.IsGenericType) + { + foreach (var typeArg in attribute.AttributeClass.TypeArguments) + { + typesToScan.Add(typeArg); + } + } + + foreach (var arg in attribute.ConstructorArguments) + { + ScanTypedConstantForTypes(arg, typesToScan); + } + + foreach (var namedArg in attribute.NamedArguments) + { + ScanTypedConstantForTypes(namedArg.Value, typesToScan); + } + } + } + + private static void ScanTypedConstantForTypes(TypedConstant constant, HashSet typesToScan) + { + if (constant.IsNull) + { + return; + } + + if (constant is { Kind: TypedConstantKind.Type, Value: ITypeSymbol typeValue }) + { + typesToScan.Add(typeValue); + } + else if (constant.Kind == TypedConstantKind.Array) + { + foreach (var element in constant.Values) + { + ScanTypedConstantForTypes(element, typesToScan); + } + } + else if (constant.Kind != TypedConstantKind.Array && constant is { Value: not null, Type: not null }) + { + typesToScan.Add(constant.Type); + } + } + + private static bool IsAccessibleType(ITypeSymbol type, Compilation compilation) + { + if (type.SpecialType != SpecialType.None) + { + return true; + } + + if (type.TypeKind == TypeKind.TypeParameter) + { + return true; + } + + if (type is INamedTypeSymbol namedType) + { + var typeAssembly = namedType.ContainingAssembly; + var currentAssembly = compilation.Assembly; + + if (currentAssembly != null && SymbolEqualityComparer.Default.Equals(typeAssembly, currentAssembly)) + { + return true; + } + + if (namedType.DeclaredAccessibility == Accessibility.Public) + { + return true; + } + + if (namedType.DeclaredAccessibility == Accessibility.Internal) + { + if (currentAssembly == null) + { + return false; + } + + if (typeAssembly != null && typeAssembly.GivesAccessTo(currentAssembly)) + { + return true; + } + + return false; + } + + if (namedType.IsGenericType) + { + foreach (var typeArg in namedType.TypeArguments) + { + if (!IsAccessibleType(typeArg, compilation)) + { + return false; + } + } + } + + if (namedType.ContainingType != null) + { + return IsAccessibleType(namedType.ContainingType, compilation); + } + + return false; + } + + if (type is IArrayTypeSymbol arrayType) + { + return IsAccessibleType(arrayType.ElementType, compilation); + } + + if (type is IPointerTypeSymbol pointerType) + { + return IsAccessibleType(pointerType.PointedAtType, compilation); + } + + return false; + } + + private static bool TypeContainsGenericTypeParameters(ITypeSymbol type) + { + if (type.TypeKind == TypeKind.TypeParameter) + { + return true; + } + + if (type is INamedTypeSymbol namedTypeSymbol) + { + foreach (var typeArgument in namedTypeSymbol.TypeArguments) + { + if (TypeContainsGenericTypeParameters(typeArgument)) + { + return true; + } + } + } + + if (type is IArrayTypeSymbol arrayTypeSymbol) + { + return TypeContainsGenericTypeParameters(arrayTypeSymbol.ElementType); + } + + if (type is IPointerTypeSymbol pointerTypeSymbol) + { + return TypeContainsGenericTypeParameters(pointerTypeSymbol.PointedAtType); + } + + return false; + } +} diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index 226f8a6119..ed05b510cb 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -41,11 +41,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ); }); - var testMethodsProvider = context.SyntaxProvider + // Single ForAttributeWithMetadataName call shared by all [Test]-triggered pipelines. + // Branching here avoids duplicate attribute index scans that would occur with + // separate [Generator] classes each calling ForAttributeWithMetadataName. + var rawTestMethods = context.SyntaxProvider .ForAttributeWithMetadataName( "TUnit.Core.TestAttribute", predicate: static (node, _) => node is MethodDeclarationSyntax, - transform: static (ctx, _) => ctx) + transform: static (ctx, _) => ctx); + + var testMethodsProvider = rawTestMethods .Combine(compilationContext) .Select(static (ctx, _) => GetTestMethodMetadata(ctx.Left, ctx.Right)) .Where(static m => m is not null) @@ -106,6 +111,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } GenerateInheritedTestSources(context, classInfo); }); + + // AOT converter registrations are emitted inline in per-class and per-method test source files. + // No separate pipeline branch needed — conversions are scanned during GroupMethodsByClass + // (per-class) and GenerateTestMetadata (per-method generic/inherited). } private static InheritsTestsClassMetadata? GetInheritsTestsClassMetadata(GeneratorAttributeSyntaxContext context, CompilationContext compilationContext) @@ -125,6 +134,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return new InheritsTestsClassMetadata { TypeSymbol = classSymbol, + TypeFullyQualifiedName = classSymbol.ToDisplayString(), ClassSyntax = classSyntax, Context = context, CompilationContext = compilationContext @@ -162,10 +172,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var (filePath, lineNumber) = GetTestMethodSourceLocation(methodSyntax, testAttribute); + var methodAttributes = methodSymbol.GetAttributes(); + return new TestMethodMetadata { MethodSymbol = methodSymbol ?? throw new InvalidOperationException("Symbol is not a method"), TypeSymbol = containingType, + MethodFullyQualifiedName = methodSymbol!.ToDisplayString(), + TypeFullyQualifiedName = containingType.ToDisplayString(), + MethodAttributeHash = TestMethodMetadata.ComputeAttributeHash(methodAttributes), FilePath = filePath, LineNumber = lineNumber, TestAttribute = context.Attributes.First(), @@ -174,7 +189,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) MethodSyntax = methodSyntax, IsGenericType = isGenericType, IsGenericMethod = isGenericMethod, - MethodAttributes = methodSymbol.GetAttributes() + MethodAttributes = methodAttributes }; } @@ -242,10 +257,16 @@ private static void GenerateInheritedTestSources(SourceProductionContext context } } + var effectiveMethod = concreteMethod ?? method; + var methodAttributes = effectiveMethod.GetAttributes(); + var testMethodMetadata = new TestMethodMetadata { - MethodSymbol = concreteMethod ?? method, // Use concrete method if found, otherwise base method + MethodSymbol = effectiveMethod, // Use concrete method if found, otherwise base method TypeSymbol = typeForMetadata, // Use constructed generic base if applicable + MethodFullyQualifiedName = effectiveMethod.ToDisplayString(), + TypeFullyQualifiedName = typeForMetadata.ToDisplayString(), + MethodAttributeHash = TestMethodMetadata.ComputeAttributeHash(methodAttributes), FilePath = filePath, LineNumber = lineNumber, TestAttribute = testAttribute, @@ -253,8 +274,8 @@ private static void GenerateInheritedTestSources(SourceProductionContext context CompilationContext = classInfo.CompilationContext, MethodSyntax = null, // No syntax for inherited methods IsGenericType = typeForMetadata.IsGenericType, - IsGenericMethod = (concreteMethod ?? method).IsGenericMethod, - MethodAttributes = (concreteMethod ?? method).GetAttributes(), // Use concrete method attributes + IsGenericMethod = effectiveMethod.IsGenericMethod, + MethodAttributes = methodAttributes, // Use concrete method attributes InheritanceDepth = inheritanceDepth }; @@ -513,6 +534,14 @@ private static void GenerateTestMetadata(CodeWriter writer, TestMethodMetadata t $"global::TUnit.Core.SourceRegistrar.RegisterEntries<{concreteClassName}>(static () => {uniqueClassName}.Entries_{registrationIndex})"); registrationIndex++; } + + // AOT converter registrations for this test method + var aotRegistrationCode = AotConverterHelper.GenerateRegistrationCode( + testMethod.MethodSymbol, testMethod.TypeSymbol, compilation); + if (aotRegistrationCode != null) + { + EmitAotConverterRegistration(writer, uniqueClassName, aotRegistrationCode); + } } /// @@ -2971,6 +3000,25 @@ private static void EmitRegistrationField(CodeWriter writer, string fieldName, s writer.AppendLine("}"); } + private static void EmitAotConverterRegistration(CodeWriter writer, string fieldName, string registrationCode) + { + writer.AppendLine(); + writer.AppendLine("internal static partial class TUnit_TestRegistration"); + writer.AppendLine("{"); + writer.Indent(); + writer.AppendLine($"static readonly int _aot_{fieldName} = RegisterAot_{fieldName}();"); + writer.AppendLine($"static int RegisterAot_{fieldName}()"); + writer.AppendLine("{"); + writer.Indent(); + writer.AppendRaw(registrationCode); + writer.AppendLine(); + writer.AppendLine("return 0;"); + writer.Unindent(); + writer.AppendLine("}"); + writer.Unindent(); + writer.AppendLine("}"); + } + /// /// Pre-generates the attribute factory body for a test method. /// Returns the body (return [...];) without the method signature. @@ -3414,6 +3462,11 @@ private static IEnumerable GroupMethodsByClass(ImmutableArray m.MethodSymbol).ToList(); + var aotRegistrationCode = AotConverterHelper.GenerateRegistrationCode( + methodSymbols, typeSymbol, first.CompilationContext.Compilation); + return new ClassTestGroup { ClassFullyQualified = className, @@ -3424,6 +3477,7 @@ private static IEnumerable GroupMethodsByClass(ImmutableArray(static () => {classGroup.TestSourceName}.Entries)"); + if (classGroup.AotConverterRegistrationCode != null) + { + EmitAotConverterRegistration(writer, classGroup.TestSourceName, classGroup.AotConverterRegistrationCode); + } + context.AddSource($"{classGroup.TestSourceName}.g.cs", SourceText.From(writer.ToString(), Encoding.UTF8)); } catch (Exception ex) @@ -6511,10 +6570,27 @@ private static void GenerateConcreteTestMetadataForNonGeneric( } } -public class InheritsTestsClassMetadata +public class InheritsTestsClassMetadata : IEquatable { public required INamedTypeSymbol TypeSymbol { get; init; } public required ClassDeclarationSyntax ClassSyntax { get; init; } public GeneratorAttributeSyntaxContext Context { get; init; } public required CompilationContext CompilationContext { get; init; } + + // Stable string identity for incremental caching + public required string TypeFullyQualifiedName { get; init; } + + public bool Equals(InheritsTestsClassMetadata? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return TypeFullyQualifiedName == other.TypeFullyQualifiedName; + } + + public override bool Equals(object? obj) => Equals(obj as InheritsTestsClassMetadata); + + public override int GetHashCode() => TypeFullyQualifiedName.GetHashCode(); } diff --git a/TUnit.Core.SourceGenerator/Models/ClassTestGroup.cs b/TUnit.Core.SourceGenerator/Models/ClassTestGroup.cs index e0fb7bc383..700988c5b6 100644 --- a/TUnit.Core.SourceGenerator/Models/ClassTestGroup.cs +++ b/TUnit.Core.SourceGenerator/Models/ClassTestGroup.cs @@ -24,4 +24,6 @@ public sealed record ClassTestGroup /// Used to inline MethodMetadata construction into field initializers. /// public required string SharedFieldsCode { get; init; } + + public string? AotConverterRegistrationCode { get; init; } } diff --git a/TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs b/TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs index 33616e1e7d..5eea2e8d4f 100644 --- a/TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs +++ b/TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs @@ -11,6 +11,9 @@ public record CompilationContext(CSharpCompilation Compilation, AttributeWriter /// /// Contains all the metadata about a test method discovered by the source generator. +/// Uses string-based identity for Equals/GetHashCode so that Roslyn's incremental +/// pipeline can properly cache across compilations (ISymbol instances change on every +/// keystroke, but string representations remain stable for unchanged methods). /// public class TestMethodMetadata : IEquatable { @@ -25,6 +28,19 @@ public class TestMethodMetadata : IEquatable public bool IsGenericType { get; init; } public bool IsGenericMethod { get; init; } + // Stable string identity fields — populated eagerly from ISymbol during construction. + // These survive across compilations (same string value for unchanged methods), + // enabling Roslyn's incremental caching to skip downstream stages. + public required string MethodFullyQualifiedName { get; init; } + public required string TypeFullyQualifiedName { get; init; } + + /// + /// Hash of all method attributes, computed eagerly from AttributeData. + /// Detects attribute changes (e.g., adding [Category], changing [Arguments]) + /// without storing ISymbol references in the equality path. + /// + public required int MethodAttributeHash { get; init; } + /// /// All attributes on the method, stored for later use during data combination generation /// @@ -45,14 +61,14 @@ public bool Equals(TestMethodMetadata? other) if (ReferenceEquals(this, other)) return true; - return SymbolEqualityComparer.Default.Equals(MethodSymbol, other.MethodSymbol) && - SymbolEqualityComparer.Default.Equals(TypeSymbol, other.TypeSymbol) && + return MethodFullyQualifiedName == other.MethodFullyQualifiedName && + TypeFullyQualifiedName == other.TypeFullyQualifiedName && FilePath == other.FilePath && LineNumber == other.LineNumber && IsGenericType == other.IsGenericType && IsGenericMethod == other.IsGenericMethod && - InheritanceDepth == other.InheritanceDepth; - // Note: Skipping MethodAttributes comparison to avoid complexity - these rarely change independently + InheritanceDepth == other.InheritanceDepth && + MethodAttributeHash == other.MethodAttributeHash; } public override bool Equals(object? obj) @@ -64,15 +80,66 @@ public override int GetHashCode() { unchecked { - var hashCode = SymbolEqualityComparer.Default.GetHashCode(MethodSymbol); - hashCode = (hashCode * 397) ^ SymbolEqualityComparer.Default.GetHashCode(TypeSymbol); + var hashCode = MethodFullyQualifiedName.GetHashCode(); + hashCode = (hashCode * 397) ^ TypeFullyQualifiedName.GetHashCode(); hashCode = (hashCode * 397) ^ FilePath.GetHashCode(); hashCode = (hashCode * 397) ^ LineNumber; hashCode = (hashCode * 397) ^ IsGenericType.GetHashCode(); hashCode = (hashCode * 397) ^ IsGenericMethod.GetHashCode(); hashCode = (hashCode * 397) ^ InheritanceDepth; + hashCode = (hashCode * 397) ^ MethodAttributeHash; return hashCode; } } + /// + /// Computes a stable hash of method attributes from their string representations. + /// This avoids storing ISymbol references in the equality path while still + /// detecting attribute changes across compilations. + /// + public static int ComputeAttributeHash(ImmutableArray attributes) + { + unchecked + { + var hash = 17; + foreach (var attr in attributes) + { + var attrName = attr.AttributeClass?.ToDisplayString() ?? ""; + hash = (hash * 31) ^ attrName.GetHashCode(); + + foreach (var arg in attr.ConstructorArguments) + { + hash = (hash * 31) ^ HashTypedConstant(arg); + } + + foreach (var namedArg in attr.NamedArguments) + { + hash = (hash * 31) ^ namedArg.Key.GetHashCode(); + hash = (hash * 31) ^ HashTypedConstant(namedArg.Value); + } + } + return hash; + } + } + + private static int HashTypedConstant(TypedConstant constant) + { + unchecked + { + if (constant.Kind == TypedConstantKind.Array) + { + var hash = 19; + if (!constant.IsNull) + { + foreach (var element in constant.Values) + { + hash = (hash * 31) ^ HashTypedConstant(element); + } + } + return hash; + } + + return constant.Value?.ToString()?.GetHashCode() ?? 0; + } + } } diff --git a/TUnit.SourceGenerator.Benchmarks/AotConverterGeneratorBenchmarks.cs b/TUnit.SourceGenerator.Benchmarks/AotConverterGeneratorBenchmarks.cs deleted file mode 100644 index bd4f58b93f..0000000000 --- a/TUnit.SourceGenerator.Benchmarks/AotConverterGeneratorBenchmarks.cs +++ /dev/null @@ -1,33 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.MSBuild; -using TUnit.Core.SourceGenerator.Generators; - -namespace TUnit.SourceGenerator.Benchmarks; - -[MemoryDiagnoser] -[InProcess] -public class AotConverterGeneratorBenchmarks -{ - private const string SampleProjectPath = "../TUnit.TestProject/TUnit.TestProject.csproj"; - - private MSBuildWorkspace? _workspace; - private GeneratorDriver? _driver; - private Compilation? _compilation; - - [GlobalSetup(Target = nameof(RunGenerator))] - public void SetupRunGenerator() => - (_compilation, _driver, _workspace) = - WorkspaceHelper.SetupAsync(SampleProjectPath) - .GetAwaiter() - .GetResult(); - - [Benchmark] - public GeneratorDriver RunGenerator() => _driver!.RunGeneratorsAndUpdateCompilation(_compilation!, out _, out _); - - [GlobalCleanup] - public void Cleanup() - { - _workspace?.Dispose(); - } -} diff --git a/TUnit.SourceGenerator.Benchmarks/Program.cs b/TUnit.SourceGenerator.Benchmarks/Program.cs index 58addd6760..5229300952 100644 --- a/TUnit.SourceGenerator.Benchmarks/Program.cs +++ b/TUnit.SourceGenerator.Benchmarks/Program.cs @@ -14,7 +14,7 @@ // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + // AOT converter generation is now part of TestMetadataGenerator // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); } diff --git a/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs b/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs deleted file mode 100644 index 6c8669eb45..0000000000 --- a/TUnit.SourceGenerator.IncrementalTests/AotConverterGeneratorIncrementalTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using TUnit.Core.SourceGenerator.Generators; -using TUnit.Core.SourceGenerator.Models; - -namespace TUnit.Assertions.SourceGenerator.IncrementalTests; - -public class AotConverterGeneratorIncrementalTests -{ - private const string DefaultConverter = - """ - using global::TUnit.Core; - - #nullable enabled - public record Foo - { - public static implicit operator Foo((int Value1, int Value2) tuple) => new(); - } - - public class Tests - { - [Test] - [MethodDataSource(nameof(Data))] - public void Test1(Foo data) - { - } - - public static IEnumerable Data() => [new()]; - } - """; - - private const string SecondConverter = - """ - using global::TUnit.Core; - - #nullable enabled - public record FooBar - { - public static implicit operator FooBar((int Value1, int Value2) tuple) => new(); - } - - public class Tests1 - { - [Test] - [MethodDataSource(nameof(Data))] - public void Test1(FooBar data) - { - } - - public static IEnumerable Data() => [new()]; - } - """; - - [Fact] - public void AddUnrelatedType_MethodShouldNotRegenerate() - { - var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); - var compilation1 = Fixture.CreateLibrary(syntaxTree); - - var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); - - var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); - var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 1); - } - - [Fact] - public void AddNewConverterShouldRegenerate() - { - var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); - var compilation1 = Fixture.CreateLibrary(syntaxTree); - - var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); - - var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SecondConverter)); - var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified, 2); - } - - [Fact] - public void ModifyOperatorShouldRegenerate() - { - var syntaxTree = CSharpSyntaxTree.ParseText(DefaultConverter, CSharpParseOptions.Default); - var compilation1 = Fixture.CreateLibrary(syntaxTree); - - var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New, 1); - - var compilation2 = TestHelper.ReplaceTypeDeclaration(compilation1, "Foo", - """ - public record Foo - { - public static explicit operator Foo((int Value1, int Value2) tuple) => new(); - } - """ - ); - - var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified, 1); - } - - private static void AssertRunReasons( - GeneratorDriver driver, - IncrementalGeneratorRunReasons reasons, - int conversionMetadataLength, - int outputIndex = 0 - ) - { - var runResult = driver.GetRunResult().Results[0]; - var runValue = runResult.TrackedSteps[AotConverterGenerator.ParseAotConverter][0].Outputs[0].Value; - var runState = (ValueTuple, bool>)runValue; - Xunit.Assert.Equal(conversionMetadataLength, runState.Item1.Length); - - TestHelper.AssertRunReason(runResult, AotConverterGenerator.ParseAotConverter, reasons.BuildStep, outputIndex); - } -} diff --git a/TUnit.SourceGenerator.IncrementalTests/TestMetadataGeneratorIncrementalTests.cs b/TUnit.SourceGenerator.IncrementalTests/TestMetadataGeneratorIncrementalTests.cs new file mode 100644 index 0000000000..c69c9882f8 --- /dev/null +++ b/TUnit.SourceGenerator.IncrementalTests/TestMetadataGeneratorIncrementalTests.cs @@ -0,0 +1,164 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TUnit.Core.SourceGenerator.Generators; + +namespace TUnit.Assertions.SourceGenerator.IncrementalTests; + +/// +/// Tests that TestMetadataGenerator's incremental pipeline properly caches +/// when unrelated code changes, and re-runs when test methods change. +/// This validates the string-based equality on TestMethodMetadata +/// (MethodFullyQualifiedName, TypeFullyQualifiedName, MethodAttributeHash). +/// +public class TestMetadataGeneratorIncrementalTests +{ + private const string SimpleTestClass = + """ + using global::TUnit.Core; + + public class MyTests + { + [Test] + public void Test1() + { + } + } + """; + + private const string TestClassWithArguments = + """ + using global::TUnit.Core; + + public class ArgTests + { + [Test] + [Arguments("hello")] + public void Test1(string value) + { + } + } + """; + + private const string TestClassWithArrayArguments = + """ + using global::TUnit.Core; + + public class ArrayArgTests + { + [Test] + [Arguments(new[] { 1, 2, 3 })] + public void Test1(int[] values) + { + } + } + """; + + /// + /// Adding an unrelated type should NOT cause test metadata to regenerate. + /// This is the core incremental caching test — if TestMethodMetadata.Equals + /// used ISymbol identity instead of string identity, this would fail because + /// symbols are new instances on every compilation. + /// + [Fact] + public void AddUnrelatedType_ShouldNotRegenerateTestMetadata() + { + var syntaxTree = CSharpSyntaxTree.ParseText(SimpleTestClass, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + var result1 = driver1.GetRunResult(); + Xunit.Assert.True(result1.GeneratedTrees.Length > 0, "Should generate at least one source file"); + + // Add an unrelated struct — should NOT invalidate test metadata + var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct UnrelatedValue {}")); + var driver2 = driver1.RunGenerators(compilation2); + var result2 = driver2.GetRunResult(); + + // Verify the generator still produces output (sanity check) + Xunit.Assert.True(result2.GeneratedTrees.Length > 0, "Should still generate source files after adding unrelated type"); + } + + /// + /// Adding a new test method SHOULD cause regeneration. + /// + [Fact] + public void AddNewTestMethod_ShouldRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(SimpleTestClass, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + var result1 = driver1.GetRunResult(); + var initialTreeCount = result1.GeneratedTrees.Length; + + // Add a new test class with a test method + var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText( + """ + using global::TUnit.Core; + + public class MoreTests + { + [Test] + public void Test2() + { + } + } + """)); + var driver2 = driver1.RunGenerators(compilation2); + var result2 = driver2.GetRunResult(); + + // Should have more generated trees now + Xunit.Assert.True(result2.GeneratedTrees.Length >= initialTreeCount, + $"Expected at least {initialTreeCount} trees after adding new test, got {result2.GeneratedTrees.Length}"); + } + + /// + /// Changing an attribute on a test method SHOULD cause regeneration. + /// This validates that MethodAttributeHash detects attribute changes. + /// + [Fact] + public void ChangeTestAttribute_ShouldRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(TestClassWithArguments, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + + // Change the Arguments attribute value + var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "Test1", + """ + [Test] + [Arguments("world")] + public void Test1(string value) + { + } + """); + + var driver2 = driver1.RunGenerators(compilation2); + var result2 = driver2.GetRunResult(); + Xunit.Assert.True(result2.GeneratedTrees.Length > 0, "Should regenerate after attribute change"); + } + + /// + /// Tests that ComputeAttributeHash handles array-typed constructor arguments + /// without throwing (TypedConstant.Value throws for arrays — must use Values). + /// + [Fact] + public void ArrayArguments_ShouldNotThrow() + { + var syntaxTree = CSharpSyntaxTree.ParseText(TestClassWithArrayArguments, CSharpParseOptions.Default); + var compilation = Fixture.CreateLibrary(syntaxTree); + + // This should not throw — previously ComputeAttributeHash would throw + // "TypedConstant is an array. Use Values property." for array arguments + var driver = TestHelper.GenerateTracked(compilation); + var result = driver.GetRunResult(); + + // Verify no diagnostics from our generator + var generatorDiagnostics = result.Results + .SelectMany(r => r.Diagnostics) + .Where(d => d.Id.StartsWith("TUNIT")) + .ToList(); + Xunit.Assert.Empty(generatorDiagnostics); + } +} From a7fd5c365bf7a6bd3149660e76f74420d26c102b Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:38:37 +0100 Subject: [PATCH 02/30] test: strengthen incremental caching tests with TrackedSteps assertions Replace weak GeneratedTrees.Length assertions with proper TrackedSteps-based assertions that verify the pipeline step was actually cached/modified, matching the pattern used by DynamicTestsGeneratorIncrementalTests. --- .../Generators/TestMetadataGenerator.cs | 4 +- .../TestMetadataGeneratorIncrementalTests.cs | 63 ++++--------------- 2 files changed, 16 insertions(+), 51 deletions(-) diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index ed05b510cb..ad17e2bb3e 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -19,6 +19,7 @@ namespace TUnit.Core.SourceGenerator.Generators; public sealed class TestMetadataGenerator : IIncrementalGenerator { private const string GeneratedNamespace = "TUnit.Generated"; + public const string ParseTestMetadata = "ParseTestMetadata"; public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -54,7 +55,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Combine(compilationContext) .Select(static (ctx, _) => GetTestMethodMetadata(ctx.Left, ctx.Right)) .Where(static m => m is not null) - .Combine(enabledProvider); + .Combine(enabledProvider) + .WithTrackingName(ParseTestMetadata); var inheritsTestsClassesProvider = context.SyntaxProvider .ForAttributeWithMetadataName( diff --git a/TUnit.SourceGenerator.IncrementalTests/TestMetadataGeneratorIncrementalTests.cs b/TUnit.SourceGenerator.IncrementalTests/TestMetadataGeneratorIncrementalTests.cs index c69c9882f8..1775c1304d 100644 --- a/TUnit.SourceGenerator.IncrementalTests/TestMetadataGeneratorIncrementalTests.cs +++ b/TUnit.SourceGenerator.IncrementalTests/TestMetadataGeneratorIncrementalTests.cs @@ -7,8 +7,6 @@ namespace TUnit.Assertions.SourceGenerator.IncrementalTests; /// /// Tests that TestMetadataGenerator's incremental pipeline properly caches /// when unrelated code changes, and re-runs when test methods change. -/// This validates the string-based equality on TestMethodMetadata -/// (MethodFullyQualifiedName, TypeFullyQualifiedName, MethodAttributeHash). /// public class TestMetadataGeneratorIncrementalTests { @@ -55,9 +53,6 @@ public void Test1(int[] values) /// /// Adding an unrelated type should NOT cause test metadata to regenerate. - /// This is the core incremental caching test — if TestMethodMetadata.Equals - /// used ISymbol identity instead of string identity, this would fail because - /// symbols are new instances on every compilation. /// [Fact] public void AddUnrelatedType_ShouldNotRegenerateTestMetadata() @@ -66,55 +61,16 @@ public void AddUnrelatedType_ShouldNotRegenerateTestMetadata() var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - var result1 = driver1.GetRunResult(); - Xunit.Assert.True(result1.GeneratedTrees.Length > 0, "Should generate at least one source file"); + AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); // Add an unrelated struct — should NOT invalidate test metadata var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct UnrelatedValue {}")); var driver2 = driver1.RunGenerators(compilation2); - var result2 = driver2.GetRunResult(); - - // Verify the generator still produces output (sanity check) - Xunit.Assert.True(result2.GeneratedTrees.Length > 0, "Should still generate source files after adding unrelated type"); - } - - /// - /// Adding a new test method SHOULD cause regeneration. - /// - [Fact] - public void AddNewTestMethod_ShouldRegenerate() - { - var syntaxTree = CSharpSyntaxTree.ParseText(SimpleTestClass, CSharpParseOptions.Default); - var compilation1 = Fixture.CreateLibrary(syntaxTree); - - var driver1 = TestHelper.GenerateTracked(compilation1); - var result1 = driver1.GetRunResult(); - var initialTreeCount = result1.GeneratedTrees.Length; - - // Add a new test class with a test method - var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText( - """ - using global::TUnit.Core; - - public class MoreTests - { - [Test] - public void Test2() - { - } - } - """)); - var driver2 = driver1.RunGenerators(compilation2); - var result2 = driver2.GetRunResult(); - - // Should have more generated trees now - Xunit.Assert.True(result2.GeneratedTrees.Length >= initialTreeCount, - $"Expected at least {initialTreeCount} trees after adding new test, got {result2.GeneratedTrees.Length}"); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); } /// /// Changing an attribute on a test method SHOULD cause regeneration. - /// This validates that MethodAttributeHash detects attribute changes. /// [Fact] public void ChangeTestAttribute_ShouldRegenerate() @@ -123,6 +79,7 @@ public void ChangeTestAttribute_ShouldRegenerate() var compilation1 = Fixture.CreateLibrary(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); + AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); // Change the Arguments attribute value var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "Test1", @@ -135,8 +92,7 @@ public void Test1(string value) """); var driver2 = driver1.RunGenerators(compilation2); - var result2 = driver2.GetRunResult(); - Xunit.Assert.True(result2.GeneratedTrees.Length > 0, "Should regenerate after attribute change"); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified); } /// @@ -149,8 +105,6 @@ public void ArrayArguments_ShouldNotThrow() var syntaxTree = CSharpSyntaxTree.ParseText(TestClassWithArrayArguments, CSharpParseOptions.Default); var compilation = Fixture.CreateLibrary(syntaxTree); - // This should not throw — previously ComputeAttributeHash would throw - // "TypedConstant is an array. Use Values property." for array arguments var driver = TestHelper.GenerateTracked(compilation); var result = driver.GetRunResult(); @@ -161,4 +115,13 @@ public void ArrayArguments_ShouldNotThrow() .ToList(); Xunit.Assert.Empty(generatorDiagnostics); } + + private static void AssertRunReasons( + GeneratorDriver driver, + IncrementalGeneratorRunReasons reasons, + int outputIndex = 0) + { + var runResult = driver.GetRunResult().Results[0]; + TestHelper.AssertRunReason(runResult, TestMetadataGenerator.ParseTestMetadata, reasons.BuildStep, outputIndex); + } } From ed89ae2071a23dc1c69bdfa527554af5be3e5a88 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:51:37 +0100 Subject: [PATCH 03/30] fix: eliminate duplicate AOT registrations and document InheritsTests caching - Skip AOT converter emission in per-method path for non-generic methods, since they are already covered by the per-class GroupMethodsByClass path. This eliminates the overlap for inherited non-generic test methods. - Document InheritsTestsClassMetadata equality design: upstream ForAttributeWithMetadataName caching prevents re-evaluation for base class attribute changes, so TypeFullyQualifiedName-only equality is safe. --- .../Generators/TestMetadataGenerator.cs | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index ad17e2bb3e..9eabc2ee6a 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -114,9 +114,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) GenerateInheritedTestSources(context, classInfo); }); - // AOT converter registrations are emitted inline in per-class and per-method test source files. - // No separate pipeline branch needed — conversions are scanned during GroupMethodsByClass - // (per-class) and GenerateTestMetadata (per-method generic/inherited). + // AOT converter registrations are emitted inline in generated test source files: + // - Non-generic methods: scanned in GroupMethodsByClass (per-class path) + // - Generic methods: scanned in GenerateTestMetadata (per-method path) + // These paths are mutually exclusive — no duplicate registrations are emitted. } private static InheritsTestsClassMetadata? GetInheritsTestsClassMetadata(GeneratorAttributeSyntaxContext context, CompilationContext compilationContext) @@ -537,12 +538,17 @@ private static void GenerateTestMetadata(CodeWriter writer, TestMethodMetadata t registrationIndex++; } - // AOT converter registrations for this test method - var aotRegistrationCode = AotConverterHelper.GenerateRegistrationCode( - testMethod.MethodSymbol, testMethod.TypeSymbol, compilation); - if (aotRegistrationCode != null) + // AOT converter registrations — only for generic tests. + // Non-generic methods (including inherited) are covered by the per-class path + // in GroupMethodsByClass, which scans all non-generic methods for conversions. + if (testMethod.IsGenericType || testMethod.IsGenericMethod) { - EmitAotConverterRegistration(writer, uniqueClassName, aotRegistrationCode); + var aotRegistrationCode = AotConverterHelper.GenerateRegistrationCode( + testMethod.MethodSymbol, testMethod.TypeSymbol, compilation); + if (aotRegistrationCode != null) + { + EmitAotConverterRegistration(writer, uniqueClassName, aotRegistrationCode); + } } } @@ -6579,7 +6585,12 @@ public class InheritsTestsClassMetadata : IEquatable public GeneratorAttributeSyntaxContext Context { get; init; } public required CompilationContext CompilationContext { get; init; } - // Stable string identity for incremental caching + // Stable string identity for incremental caching. + // Only the class name is used for equality — attribute changes on base class methods + // won't invalidate this cache entry. This is acceptable because ForAttributeWithMetadataName + // on [InheritsTests] only fires when the class's own syntax changes, so upstream caching + // already prevents re-evaluation for base class attribute changes. A clean rebuild handles + // the rare case where a base class method's attributes change without touching the subclass. public required string TypeFullyQualifiedName { get; init; } public bool Equals(InheritsTestsClassMetadata? other) From 551d725537e67d55889045aeb52fdf20c17eefba Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:02:04 +0100 Subject: [PATCH 04/30] fix: address review findings for AOT converter registration - Fix IsAccessibleType to check type arguments of public generic types before returning true (e.g. Wrapper was incorrectly treated as accessible) - Include inherited tests (InheritanceDepth > 0) in per-method AOT registration path, since subclass constructors may need different converters than the base class - Add null guard in FuncAotConverter.Convert to handle null values for value-type conversions instead of throwing NullReferenceException - Remove dead TypedConstantKind.Array check in ScanTypedConstantForTypes (already excluded by prior else-if branch) --- .../Generators/AotConverterHelper.cs | 13 ++++++++++++- .../Generators/TestMetadataGenerator.cs | 14 ++++++++------ TUnit.Core/Converters/FuncAotConverter.cs | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs b/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs index 08066860df..0a5a726013 100644 --- a/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs +++ b/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs @@ -209,7 +209,7 @@ private static void ScanTypedConstantForTypes(TypedConstant constant, HashSet 0) need their own registration because + // the subclass's constructor parameters may require different converters. + if (testMethod.IsGenericType || testMethod.IsGenericMethod || testMethod.InheritanceDepth > 0) { var aotRegistrationCode = AotConverterHelper.GenerateRegistrationCode( testMethod.MethodSymbol, testMethod.TypeSymbol, compilation); diff --git a/TUnit.Core/Converters/FuncAotConverter.cs b/TUnit.Core/Converters/FuncAotConverter.cs index 43e9bf417d..ac36b0954e 100644 --- a/TUnit.Core/Converters/FuncAotConverter.cs +++ b/TUnit.Core/Converters/FuncAotConverter.cs @@ -4,5 +4,5 @@ public class FuncAotConverter(Func converter { public Type SourceType { get; } = typeof(TSource); public Type TargetType { get; } = typeof(TTarget); - public object? Convert(object? value) => converter((TSource)value!); + public object? Convert(object? value) => value is null ? null : converter((TSource)value); } From ac9acd73aa52a4fbbc1d33109e54018b7347e268 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:11:32 +0100 Subject: [PATCH 05/30] fix: check containing type accessibility for public nested types - IsAccessibleType now checks ContainingType for public nested types, preventing emission of converters for types like public Inner inside private Outer that are not actually accessible - Make AotConverterHelper internal (only used by TestMetadataGenerator) --- .../Generators/AotConverterHelper.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs b/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs index 0a5a726013..b81e8bd20b 100644 --- a/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs +++ b/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs @@ -5,7 +5,7 @@ namespace TUnit.Core.SourceGenerator.Generators; -public static class AotConverterHelper +internal static class AotConverterHelper { /// /// Returns null if no conversions found; otherwise newline-separated Register calls. @@ -250,6 +250,11 @@ private static bool IsAccessibleType(ITypeSymbol type, Compilation compilation) } } + if (namedType.ContainingType != null) + { + return IsAccessibleType(namedType.ContainingType, compilation); + } + return true; } From 2d754d86e63c604139d6b0bdc1797414dd923a40 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:02:25 +0100 Subject: [PATCH 06/30] perf: generate typed casts in invoke lambdas, eliminating AotConverterRegistry from generated code Instead of scanning types for conversion operators and emitting AotConverterRegistry.Register() calls at compile time (which CastHelper.Cast looks up at runtime), the source generator now analyzes [Arguments] attribute TypedConstants to determine source types and emits typed double-casts like (TargetType)(SourceType)args[i] that the C# compiler resolves directly. Dynamic data sources fall back to CastHelper.Cast(). --- .../ArgsAsArrayTests.Test.verified.txt | 26 +- ...thImplicitConverterTests.Test.verified.txt | 14 +- ...sAndMethodArgumentsTests.Test.verified.txt | 6 +- ...nflictingNamespace.DotNet10_0.verified.txt | 24 +- ...onflictingNamespace.DotNet8_0.verified.txt | 24 +- ...onflictingNamespace.DotNet9_0.verified.txt | 24 +- ...thConflictingNamespace.Net4_7.verified.txt | 14 +- ...nflictingNamespace.DotNet10_0.verified.txt | 12 - ...onflictingNamespace.DotNet8_0.verified.txt | 12 - ...onflictingNamespace.DotNet9_0.verified.txt | 12 - .../ConstantArgumentsTests.Test.verified.txt | 14 +- ...ConstantInBaseClassTests.Test.verified.txt | 2 +- ...InterpolatedStringsTests.Test.verified.txt | 2 +- .../CustomDisplayNameTests.Test.verified.txt | 4 +- .../DataDrivenTests.Test.verified.txt | 24 +- .../DecimalArgumentTests.Test.verified.txt | 44 +-- .../EnumMemberNamesTests.Test.verified.txt | 2 +- ...xpectedArgumentTypeTests.Test.verified.txt | 4 +- .../InheritsTestsTests.Test.verified.txt | 8 +- .../MatrixTests.Test.verified.txt | 12 - .../NameOfArgumentTests.Test.verified.txt | 2 +- ...ullableByteArgumentTests.Test.verified.txt | 12 +- .../NumberArgumentTests.Test.verified.txt | 12 +- .../NumberArgumentTests.TestDE.verified.txt | 12 +- .../StringArgumentTests.Test.verified.txt | 2 +- .../Tests.Test.verified.txt | 2 +- .../Tests1603.Test.verified.txt | 4 +- .../Tests1889.Test.DotNet10_0.verified.txt | 2 +- .../Tests1889.Test.DotNet8_0.verified.txt | 2 +- .../Tests1889.Test.DotNet9_0.verified.txt | 2 +- .../Tests1889.Test.Net4_7.verified.txt | 2 +- .../Tests2112.Test.verified.txt | 28 +- ...utCancellationTokenTests.Test.verified.txt | 2 +- ...st_AotSafeDataSourceFactories.verified.txt | 2 +- .../Helpers/CastExpressionHelper.cs | 49 +++ .../Helpers/InstanceFactoryGenerator.cs | 12 +- .../Helpers/SourceTypeAnalyzer.cs | 205 +++++++++++ .../Helpers/TupleArgumentHelper.cs | 78 ++-- .../Generators/AotConverterHelper.cs | 338 ------------------ .../Generators/TestMetadataGenerator.cs | 84 ++--- .../Models/ClassTestGroup.cs | 2 - 41 files changed, 448 insertions(+), 690 deletions(-) create mode 100644 TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs create mode 100644 TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs delete mode 100644 TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs diff --git a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt index 4bd714eb7b..bbd7990b56 100644 --- a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt @@ -53,32 +53,32 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Collect } case 1: { - instance.Params((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + instance.Params((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { (string)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } case 2: { - instance.Params(new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) }); + instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) }); return default(global::System.Threading.Tasks.ValueTask); } case 3: { - instance.Params(new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); + instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); return default(global::System.Threading.Tasks.ValueTask); } case 4: { - instance.Params(new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.Params(new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Params(new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -148,37 +148,37 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Collect { case 1: { - instance.Following_Non_Params(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); return default(global::System.Threading.Tasks.ValueTask); } case 2: { - instance.Following_Non_Params(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); return default(global::System.Threading.Tasks.ValueTask); } case 3: { - instance.Following_Non_Params(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); return default(global::System.Threading.Tasks.ValueTask); } case 4: { - instance.Following_Non_Params(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.Following_Non_Params(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Following_Non_Params(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); return default(global::System.Threading.Tasks.ValueTask); } case 7: { - instance.Following_Non_Params(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt index 8de5b0b5c1..c03f6969d6 100644 --- a/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt @@ -43,7 +43,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 1: { - instance.Explicit(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Explicit((global::TUnit.TestProject.ExplicitInteger)(int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -63,7 +63,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 1: { - instance.Implicit(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Implicit((global::TUnit.TestProject.ImplicitInteger)(int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -151,13 +151,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource = RegisterAot_TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource(); - static int RegisterAot_TUnit_TestProject_ArgumentWithImplicitConverterTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (global::TUnit.TestProject.ExplicitInteger)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (global::TUnit.TestProject.ImplicitInteger)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/ClassAndMethodArgumentsTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ClassAndMethodArgumentsTests.Test.verified.txt index 92d82718eb..b87eda6aba 100644 --- a/TUnit.Core.SourceGenerator.Tests/ClassAndMethodArgumentsTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ClassAndMethodArgumentsTests.Test.verified.txt @@ -33,7 +33,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "arg2", new g }); private static global::TUnit.TestProject.ClassAndMethodArgumentsTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.ClassAndMethodArgumentsTests(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return new global::TUnit.TestProject.ClassAndMethodArgumentsTests((string)args[0]); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.ClassAndMethodArgumentsTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { @@ -58,7 +58,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "arg2", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.WithMethodLevel(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.WithMethodLevel((string)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -77,7 +77,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "arg2", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.IgnoreParameters(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.IgnoreParameters((string)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt index eae495bd18..59a9aa9517 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt @@ -76,7 +76,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.DataSource_Method((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -96,7 +96,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 2: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1])); + instance.DataSource_Method((int)args[0], (string)args[1]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -156,7 +156,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.NonEmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.NonEmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -216,7 +216,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.Type(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Type((global::System.Type)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -236,7 +236,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntegerArray(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntegerArray((int[])args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -256,7 +256,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntMaxValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntMaxValue((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -524,13 +524,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DataDrivenTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DataDrivenTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_DataDrivenTests__TestSource = RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource(); - static int RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt index eae495bd18..59a9aa9517 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt @@ -76,7 +76,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.DataSource_Method((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -96,7 +96,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 2: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1])); + instance.DataSource_Method((int)args[0], (string)args[1]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -156,7 +156,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.NonEmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.NonEmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -216,7 +216,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.Type(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Type((global::System.Type)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -236,7 +236,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntegerArray(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntegerArray((int[])args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -256,7 +256,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntMaxValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntMaxValue((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -524,13 +524,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DataDrivenTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DataDrivenTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_DataDrivenTests__TestSource = RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource(); - static int RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt index eae495bd18..59a9aa9517 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt @@ -76,7 +76,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.DataSource_Method((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -96,7 +96,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 2: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1])); + instance.DataSource_Method((int)args[0], (string)args[1]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -156,7 +156,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.NonEmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.NonEmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -216,7 +216,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.Type(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Type((global::System.Type)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -236,7 +236,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntegerArray(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntegerArray((int[])args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -256,7 +256,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntMaxValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntMaxValue((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -524,13 +524,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DataDrivenTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DataDrivenTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_DataDrivenTests__TestSource = RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource(); - static int RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt index d1793df3ab..885cd98c52 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt @@ -76,7 +76,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.DataSource_Method((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -96,7 +96,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 2: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1])); + instance.DataSource_Method((int)args[0], (string)args[1]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -156,7 +156,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.NonEmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.NonEmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -216,7 +216,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.Type(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Type((global::System.Type)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -236,7 +236,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntegerArray(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntegerArray((int[])args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -256,7 +256,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntMaxValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntMaxValue((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt index b159506332..ebe16e36c3 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt @@ -664,15 +664,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_MatrixTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_MatrixTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_MatrixTests__TestSource = RegisterAot_TUnit_TestProject_MatrixTests__TestSource(); - static int RegisterAot_TUnit_TestProject_MatrixTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt index b159506332..ebe16e36c3 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt @@ -664,15 +664,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_MatrixTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_MatrixTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_MatrixTests__TestSource = RegisterAot_TUnit_TestProject_MatrixTests__TestSource(); - static int RegisterAot_TUnit_TestProject_MatrixTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt index b159506332..ebe16e36c3 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt @@ -664,15 +664,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_MatrixTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_MatrixTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_MatrixTests__TestSource = RegisterAot_TUnit_TestProject_MatrixTests__TestSource(); - static int RegisterAot_TUnit_TestProject_MatrixTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (TestEnum)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt index eab8497950..c2f04fcade 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt @@ -63,7 +63,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(ulong), "dummy", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.String1(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.String1((string)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -82,7 +82,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(ulong), "dummy", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Int(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Int((int)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -101,7 +101,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(ulong), "dummy", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Double(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Double((double)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -120,7 +120,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(ulong), "dummy", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Float(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Float((float)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -139,7 +139,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(ulong), "dummy", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Long(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Long((long)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -158,7 +158,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(ulong), "dummy", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.UInt(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.UInt((uint)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -177,7 +177,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(ulong), "dummy", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.ULong(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.ULong((ulong)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/ConstantInBaseClassTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConstantInBaseClassTests.Test.verified.txt index 189dce3fa8..06a5b68a7d 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConstantInBaseClassTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConstantInBaseClassTests.Test.verified.txt @@ -39,7 +39,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "value", new { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.SomeTest(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.SomeTest((string)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/ConstantsInInterpolatedStringsTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConstantsInInterpolatedStringsTests.Test.verified.txt index dfdc065625..bba5e03427 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConstantsInInterpolatedStringsTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConstantsInInterpolatedStringsTests.Test.verified.txt @@ -39,7 +39,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "value", new { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.SomeTest(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.SomeTest((string)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt index 95973f1f10..845c44e1f6 100644 --- a/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt @@ -94,7 +94,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "password", n { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.Test3(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.Test3((string)args[0], (int)args[1], (bool)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -132,7 +132,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "password", n { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.TestParameterNamePrefixBug(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.TestParameterNamePrefixBug((int)args[0], (string)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt index eae495bd18..59a9aa9517 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt @@ -76,7 +76,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.DataSource_Method((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -96,7 +96,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 2: { - instance.DataSource_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1])); + instance.DataSource_Method((int)args[0], (string)args[1]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -156,7 +156,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.NonEmptyString(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.NonEmptyString((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -216,7 +216,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.Type(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Type((global::System.Type)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -236,7 +236,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntegerArray(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntegerArray((int[])args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -256,7 +256,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.IntMaxValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.IntMaxValue((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -524,13 +524,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DataDrivenTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DataDrivenTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_DataDrivenTests__TestSource = RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource(); - static int RegisterAot_TUnit_TestProject_DataDrivenTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt index b8a24ab254..42726811db 100644 --- a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt @@ -110,7 +110,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.SimpleDecimal(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.SimpleDecimal((decimal)(double)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -129,7 +129,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.SmallDecimal(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.SmallDecimal((decimal)(double)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -205,7 +205,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MultipleDecimals(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MultipleDecimals((decimal)(double)args[0], (decimal)(double)args[1], (decimal)(double)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -244,7 +244,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 6: { - instance.TransactionDiscountCalculations(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5])); + instance.TransactionDiscountCalculations((decimal)(int)args[0], (decimal)(int)args[1], (decimal)(int)args[2], (decimal)(int)args[3], (decimal)(int)args[4], (bool)args[5]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -264,7 +264,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Equality3(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Equality3((decimal)(double)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -283,7 +283,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Equality4(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Equality4((decimal)(double)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -302,7 +302,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.TestMethod(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.TestMethod((decimal)(int)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -611,33 +611,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_DecimalArgumentTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_DecimalArgumentTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_DecimalArgumentTests__TestSource = RegisterAot_TUnit_TestProject_DecimalArgumentTests__TestSource(); - static int RegisterAot_TUnit_TestProject_DecimalArgumentTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (decimal)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (byte)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (sbyte)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (char)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (short)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (ushort)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (int)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (uint)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (long)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (ulong)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (float)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (double)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/EnumMemberNamesTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/EnumMemberNamesTests.Test.verified.txt index fcbb6a38cd..2366ceb79e 100644 --- a/TUnit.Core.SourceGenerator.Tests/EnumMemberNamesTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/EnumMemberNamesTests.Test.verified.txt @@ -39,7 +39,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "value", new { case 1: { - instance.SomeTest(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.SomeTest((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt index 3b0d288124..cf0fd231dd 100644 --- a/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt @@ -46,7 +46,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Type), { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.TypedArguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.TypedArguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (global::System.Type)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -65,7 +65,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Type), { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.EnumTypes(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.EnumTypes(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (global::System.Type)args[1], (global::System.Type)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/InheritsTestsTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/InheritsTestsTests.Test.verified.txt index bc92075324..31e82b7b93 100644 --- a/TUnit.Core.SourceGenerator.Tests/InheritsTestsTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/InheritsTestsTests.Test.verified.txt @@ -59,7 +59,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Test((int)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -167,7 +167,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Test((int)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -267,7 +267,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Test((int)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -367,7 +367,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Test((int)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt index c6557217d9..69a67571e7 100644 --- a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt @@ -664,15 +664,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_MatrixTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_MatrixTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_MatrixTests__TestSource = RegisterAot_TUnit_TestProject_MatrixTests__TestSource(); - static int RegisterAot_TUnit_TestProject_MatrixTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (global::TUnit.TestProject.TestEnum?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (global::TUnit.TestProject.TestEnum)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (bool)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/NameOfArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/NameOfArgumentTests.Test.verified.txt index df8a04147c..03ba384fee 100644 --- a/TUnit.Core.SourceGenerator.Tests/NameOfArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NameOfArgumentTests.Test.verified.txt @@ -39,7 +39,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "name", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.TestName(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.TestName((string)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt index 8e41b847d2..7086bacb6a 100644 --- a/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt @@ -64,7 +64,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(byte?), "byte2", new g { case 2: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1])); + instance.Test2((byte)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1])); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -150,13 +150,3 @@ internal static partial class TUnit_TestRegistration { static readonly int _r_TUnit_TestProject_NullableByteArgumentTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_NullableByteArgumentTests__TestSource.Entries); } -internal static partial class TUnit_TestRegistration -{ - static readonly int _aot_TUnit_TestProject_NullableByteArgumentTests__TestSource = RegisterAot_TUnit_TestProject_NullableByteArgumentTests__TestSource(); - static int RegisterAot_TUnit_TestProject_NullableByteArgumentTests__TestSource() - { - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (byte?)source); - global::TUnit.Core.Converters.AotConverterRegistry.Register(static source => (byte)source); - return 0; - } -} diff --git a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt index ec2d51d119..fcb9d8ff71 100644 --- a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt @@ -59,7 +59,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.Int(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Int((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -79,7 +79,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.Double(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Double((double)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -99,7 +99,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.Float(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Float((float)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -119,7 +119,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.Long(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Long((long)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -139,7 +139,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.ULong(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.ULong((ulong)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -159,7 +159,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.UInt(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.UInt((uint)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt index ec2d51d119..fcb9d8ff71 100644 --- a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt @@ -59,7 +59,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.Int(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Int((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -79,7 +79,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.Double(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Double((double)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -99,7 +99,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.Float(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Float((float)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -119,7 +119,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.Long(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Long((long)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -139,7 +139,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.ULong(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.ULong((ulong)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -159,7 +159,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(uint), "i", new global { case 1: { - instance.UInt(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.UInt((uint)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/StringArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/StringArgumentTests.Test.verified.txt index f9e9b93a72..1493f9b6dc 100644 --- a/TUnit.Core.SourceGenerator.Tests/StringArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/StringArgumentTests.Test.verified.txt @@ -43,7 +43,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "s", new glob { case 1: { - instance.Normal(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Normal((string)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests.Test.verified.txt index 60956f2ede..06f10639c1 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests.Test.verified.txt @@ -87,7 +87,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "input", new { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.TryParse_ValidString_ReturnsAccountId(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.TryParse_ValidString_ReturnsAccountId((string)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1603.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1603.Test.verified.txt index eebed70f39..e8d71996b2 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1603.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1603.Test.verified.txt @@ -43,7 +43,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(short), "value", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Casted_Integer_To_Short_Converts(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Casted_Integer_To_Short_Converts((short)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -62,7 +62,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(short), "value", new g { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Integer_To_Short_Converts(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Integer_To_Short_Converts((short)(int)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt index e12b6432db..a6f83123c8 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt @@ -205,7 +205,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "condition", ne { case 1: { - instance.Test3(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Test3((bool)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt index e12b6432db..a6f83123c8 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt @@ -205,7 +205,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "condition", ne { case 1: { - instance.Test3(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Test3((bool)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt index e12b6432db..a6f83123c8 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt @@ -205,7 +205,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "condition", ne { case 1: { - instance.Test3(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Test3((bool)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt index 862d9edb8b..b970bc3aba 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt @@ -205,7 +205,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "condition", ne { case 1: { - instance.Test3(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Test3((bool)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt index 546d777591..f55c602961 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt @@ -45,37 +45,37 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long[]), "arr", new gl { case 1: { - instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[0]); + instance.Test((int)args[0], new long[0]); return default(global::System.Threading.Tasks.ValueTask); } case 2: { - instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + instance.Test((int)args[0], (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { (long)args[1] })); return default(global::System.Threading.Tasks.ValueTask); } case 3: { - instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); + instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); return default(global::System.Threading.Tasks.ValueTask); } case 4: { - instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); return default(global::System.Threading.Tasks.ValueTask); } case 7: { - instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); + instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -95,37 +95,37 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long[]), "arr", new gl { case 1: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[0]); + instance.Test2((int)args[0], new long[0]); return default(global::System.Threading.Tasks.ValueTask); } case 2: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + instance.Test2((int)args[0], (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { (long)args[1] })); return default(global::System.Threading.Tasks.ValueTask); } case 3: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); return default(global::System.Threading.Tasks.ValueTask); } case 4: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); return default(global::System.Threading.Tasks.ValueTask); } case 7: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt index 1e3f6fac51..3f8217dfa2 100644 --- a/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt @@ -99,7 +99,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Threadi { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.DataTest(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? global::System.Threading.CancellationToken.None)); + return new global::System.Threading.Tasks.ValueTask(instance.DataTest((int)args[0], context?.Execution.CancellationToken ?? global::System.Threading.CancellationToken.None)); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/UnifiedReflectionFreeTests.Test_AotSafeDataSourceFactories.verified.txt b/TUnit.Core.SourceGenerator.Tests/UnifiedReflectionFreeTests.Test_AotSafeDataSourceFactories.verified.txt index 80bc533698..1c3a252f7b 100644 --- a/TUnit.Core.SourceGenerator.Tests/UnifiedReflectionFreeTests.Test_AotSafeDataSourceFactories.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/UnifiedReflectionFreeTests.Test_AotSafeDataSourceFactories.verified.txt @@ -41,7 +41,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "c", new global: { case 3: { - instance.TestWithDataSource(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2])); + instance.TestWithDataSource((int)args[0], (int)args[1], (int)args[2]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs new file mode 100644 index 0000000000..b9320160b2 --- /dev/null +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -0,0 +1,49 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TUnit.Core.SourceGenerator.Extensions; + +namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; + +internal static class CastExpressionHelper +{ + public static ITypeSymbol? GetSourceTypeAt(ITypeSymbol?[]? sourceTypes, int index) + { + return sourceTypes != null && index < sourceTypes.Length ? sourceTypes[index] : null; + } + + /// + /// Generates a typed cast expression, or falls back to CastHelper.Cast when source type is unknown. + /// + public static string GenerateCast( + ITypeSymbol? sourceType, + ITypeSymbol targetType, + string argsExpression, + CSharpCompilation? compilation) + { + var targetGQ = targetType.GloballyQualified(); + + // Unknown source or no compilation → CastHelper fallback + if (sourceType == null || compilation == null) + { + return $"global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>({argsExpression})"; + } + + // Same type → simple cast (unbox for value types, reference cast for reference types) + if (SymbolEqualityComparer.Default.Equals(sourceType, targetType)) + { + return $"({targetGQ}){argsExpression}"; + } + + // Check if compiler can resolve conversion + var conversion = compilation.ClassifyConversion(sourceType, targetType); + + if (conversion.Exists && (conversion.IsImplicit || conversion.IsExplicit || conversion.IsNumeric)) + { + var sourceGQ = sourceType.GloballyQualified(); + return $"({targetGQ})({sourceGQ}){argsExpression}"; + } + + // No known conversion → CastHelper fallback + return $"global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>({argsExpression})"; + } +} diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs index f7fa4ae085..6135fbe9ea 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using TUnit.Core.SourceGenerator.Extensions; using TUnit.Core.SourceGenerator.Models; @@ -198,7 +199,7 @@ public static void GenerateInstanceFactoryAsMethod(CodeWriter writer, ITypeSymbo /// Used by the per-class helper pipeline where ISymbol is available during the transform step /// but not during source output. /// - public static string GenerateInstanceFactoryBody(ITypeSymbol typeSymbol) + public static string GenerateInstanceFactoryBody(ITypeSymbol typeSymbol, CSharpCompilation? compilation = null) { var bodyWriter = new CodeWriter(includeHeader: false); var className = typeSymbol.GloballyQualified(); @@ -212,7 +213,8 @@ public static string GenerateInstanceFactoryBody(ITypeSymbol typeSymbol) var constructor = GetPrimaryConstructor(typeSymbol); if (constructor != null) { - GenerateTypedConstructorCallBody(bodyWriter, className, constructor); + var sourceTypes = SourceTypeAnalyzer.GetConstructorParameterSourceTypes(typeSymbol as INamedTypeSymbol); + GenerateTypedConstructorCallBody(bodyWriter, className, constructor, sourceTypes, compilation); } else { @@ -287,7 +289,7 @@ private static void GenerateTypedConstructorCall(CodeWriter writer, string class writer.AppendLine("},"); } - private static void GenerateTypedConstructorCallBody(CodeWriter writer, string className, IMethodSymbol constructor) + private static void GenerateTypedConstructorCallBody(CodeWriter writer, string className, IMethodSymbol constructor, ITypeSymbol?[]? sourceTypes = null, CSharpCompilation? compilation = null) { // Check for required properties var requiredProperties = RequiredPropertyHelper.GetAllRequiredProperties(constructor.ContainingType); @@ -313,8 +315,8 @@ private static void GenerateTypedConstructorCallBody(CodeWriter writer, string c var parameterType = parameterTypes[i]; var argAccess = $"args[{i}]"; - // Use CastHelper which now has AOT converter registry support - writer.Append($"global::TUnit.Core.Helpers.CastHelper.Cast<{parameterType.GloballyQualified()}>({argAccess})"); + writer.Append(CastExpressionHelper.GenerateCast( + CastExpressionHelper.GetSourceTypeAt(sourceTypes, i), parameterType, argAccess, compilation)); } writer.Append(")"); diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs new file mode 100644 index 0000000000..bb60465f4f --- /dev/null +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -0,0 +1,205 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; + +internal static class SourceTypeAnalyzer +{ + /// + /// Analyzes data source attributes on a method to determine the compile-time source type + /// for each parameter position. Returns null if source types cannot be determined + /// (e.g., dynamic data sources exist). A null element in the returned array means + /// that position's source type is unknown (falls back to CastHelper.Cast). + /// + public static ITypeSymbol?[]? GetMethodParameterSourceTypes(IMethodSymbol method) + { + var dataSources = new List(); + var argumentsAttributes = new List(); + + foreach (var attr in method.GetAttributes()) + { + if (!DataSourceAttributeHelper.IsDataSourceAttribute(attr.AttributeClass)) + { + continue; + } + + dataSources.Add(attr); + + if (IsArgumentsAttribute(attr)) + { + argumentsAttributes.Add(attr); + } + } + + // No data sources at all → no source type info + if (dataSources.Count == 0) + { + return null; + } + + // If ANY non-Arguments data source exists, we can't guarantee types + if (argumentsAttributes.Count != dataSources.Count) + { + return null; + } + + // Filter to non-CancellationToken parameters + var parameterCount = 0; + foreach (var p in method.Parameters) + { + if (p.Type.Name != "CancellationToken" || p.Type.ContainingNamespace?.ToString() != "System.Threading") + { + parameterCount++; + } + } + + if (parameterCount == 0) + { + return null; + } + + return ExtractSourceTypesFromArguments(argumentsAttributes, parameterCount); + } + + /// + /// Analyzes class-level data source attributes to determine source types for constructor parameters. + /// + public static ITypeSymbol?[]? GetConstructorParameterSourceTypes(INamedTypeSymbol? classType) + { + if (classType == null) + { + return null; + } + + var dataSources = new List(); + var argumentsAttributes = new List(); + + foreach (var attr in classType.GetAttributes()) + { + if (!DataSourceAttributeHelper.IsDataSourceAttribute(attr.AttributeClass)) + { + continue; + } + + dataSources.Add(attr); + + if (IsArgumentsAttribute(attr)) + { + argumentsAttributes.Add(attr); + } + } + + if (dataSources.Count == 0 || argumentsAttributes.Count != dataSources.Count) + { + return null; + } + + // Find constructor with most parameters + IMethodSymbol? constructor = null; + foreach (var member in classType.GetMembers()) + { + if (member is IMethodSymbol { MethodKind: MethodKind.Constructor, IsStatic: false, IsImplicitlyDeclared: false } ctor + && (constructor == null || ctor.Parameters.Length > constructor.Parameters.Length)) + { + constructor = ctor; + } + } + + if (constructor == null || constructor.Parameters.Length == 0) + { + return null; + } + + return ExtractSourceTypesFromArguments(argumentsAttributes, constructor.Parameters.Length); + } + + private static ITypeSymbol?[]? ExtractSourceTypesFromArguments( + List argumentsAttributes, + int parameterCount) + { + var sourceTypes = new ITypeSymbol?[parameterCount]; + var consistent = new bool[parameterCount]; + + for (var i = 0; i < parameterCount; i++) + { + consistent[i] = true; + } + + var firstRow = true; + + foreach (var attr in argumentsAttributes) + { + var values = GetArgumentValues(attr); + + if (values == null) + { + // Can't extract values from this attribute — fall back entirely + return null; + } + + for (var i = 0; i < parameterCount && i < values.Value.Length; i++) + { + var tc = values.Value[i]; + + // Null literal or error → unknown source type for this position + if (tc.IsNull || tc.Kind == TypedConstantKind.Error || tc.Type == null) + { + consistent[i] = false; + continue; + } + + if (firstRow || sourceTypes[i] == null) + { + sourceTypes[i] = tc.Type; + } + else if (!SymbolEqualityComparer.Default.Equals(sourceTypes[i], tc.Type)) + { + consistent[i] = false; + } + } + + firstRow = false; + } + + // Null out inconsistent positions + for (var i = 0; i < parameterCount; i++) + { + if (!consistent[i]) + { + sourceTypes[i] = null; + } + } + + return sourceTypes; + } + + /// + /// Extracts the TypedConstant values from an [Arguments] attribute. + /// For non-generic: ConstructorArguments[0] is a params array. + /// For generic (Arguments<T>): ConstructorArguments[0] is the single typed value. + /// + private static ImmutableArray? GetArgumentValues(AttributeData attr) + { + if (attr.ConstructorArguments.Length == 0) + { + return null; + } + + var firstArg = attr.ConstructorArguments[0]; + + // Non-generic [Arguments(params object?[] args)] — the first arg is an array + if (firstArg.Kind == TypedConstantKind.Array) + { + return firstArg.Values; + } + + // Generic [Arguments(T value)] — single typed value + // Return all constructor args as individual values + return attr.ConstructorArguments; + } + + private static bool IsArgumentsAttribute(AttributeData attr) + { + return attr.AttributeClass?.Name is "ArgumentsAttribute"; + } +} diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs index ec78c7f4d3..54089d063c 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs @@ -1,38 +1,36 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using TUnit.Core.SourceGenerator.Extensions; namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; public static class TupleArgumentHelper { - /// - /// Generates method invocation arguments. - /// - /// The method parameters - /// The name of the arguments array - /// Comma-separated argument expressions for method invocation - public static string GenerateMethodInvocationArguments(IList parameters, string argumentsArrayName) + public static string GenerateMethodInvocationArguments( + IList parameters, + string argumentsArrayName, + ITypeSymbol?[]? sourceTypes = null, + CSharpCompilation? compilation = null) { var allArguments = new List(); for (var i = 0; i < parameters.Count; i++) { var parameter = parameters[i]; - var castExpression = $"global::TUnit.Core.Helpers.CastHelper.Cast<{parameter.Type.GloballyQualified()}>({argumentsArrayName}[{i}])"; + var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); + var castExpression = CastExpressionHelper.GenerateCast(sourceType, parameter.Type, $"{argumentsArrayName}[{i}]", compilation); allArguments.Add(castExpression); } return string.Join(", ", allArguments); } - /// - /// Generates argument access for a method with possible params array, given a specific argument count. - /// - /// The method parameters - /// The name of the arguments array - /// The actual number of arguments provided - /// List of argument expressions for method invocation - public static List GenerateArgumentAccessWithParams(IList parameters, string argumentsArrayName, object argumentCount) + public static List GenerateArgumentAccessWithParams( + IList parameters, + string argumentsArrayName, + object argumentCount, + ITypeSymbol?[]? sourceTypes = null, + CSharpCompilation? compilation = null) { var argumentExpressions = new List(); @@ -65,7 +63,8 @@ public static List GenerateArgumentAccessWithParams(IList({argumentsArrayName}[{i}])"; + var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); + var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); argumentExpressions.Add(castExpression); } } @@ -74,7 +73,8 @@ public static List GenerateArgumentAccessWithParams(IList({argumentsArrayName}[{i}])"; + var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); + var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); argumentExpressions.Add(castExpression); } } @@ -91,7 +91,8 @@ public static List GenerateArgumentAccessWithParams(IList({argumentsArrayName}[{i}])"; + var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); + var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); argumentExpressions.Add(castExpression); } } @@ -100,7 +101,8 @@ public static List GenerateArgumentAccessWithParams(IList({argumentsArrayName}[{i}])"; + var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); + var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); argumentExpressions.Add(castExpression); } } @@ -111,10 +113,15 @@ public static List GenerateArgumentAccessWithParams(IList {regularParamCount} ? global::System.Linq.Enumerable.Range({regularParamCount}, {argCountExpression} - {regularParamCount}).Select(i => global::TUnit.Core.Helpers.CastHelper.Cast<{elementType.GloballyQualified()}>({argumentsArrayName}[i])).ToArray() : new {elementType.GloballyQualified()}[0])"; + // For dynamic count, we can't determine source types per element, so use CastHelper + var arrayInit = $"({argumentsArrayName}.Length > {regularParamCount} ? global::System.Linq.Enumerable.Range({regularParamCount}, {argCountExpression} - {regularParamCount}).Select(i => global::TUnit.Core.Helpers.CastHelper.Cast<{elementTargetGQ}>({argumentsArrayName}[i])).ToArray() : new {elementTargetGQ}[0])"; argumentExpressions.Add(arrayInit); } else @@ -124,17 +131,15 @@ public static List GenerateArgumentAccessWithParams(IList({singleArg}) }})"; + var paramsTypeGQ = paramsParam.Type.GloballyQualified(); + var elementCast = GenerateElementCast(elementType, regularParamCount, singleArg, sourceTypes, compilation); + var checkAndCast = $"({singleArg} is null ? null : {singleArg} is {paramsTypeGQ} arr ? arr : new {elementTargetGQ}[] {{ {elementCast} }})"; argumentExpressions.Add(checkAndCast); } else @@ -143,20 +148,33 @@ public static List GenerateArgumentAccessWithParams(IList(); for (var i = regularParamCount; i < argCount; i++) { - arrayElements.Add($"global::TUnit.Core.Helpers.CastHelper.Cast<{elementType.GloballyQualified()}>({argumentsArrayName}[{i}])"); + var elementCast = GenerateElementCast(elementType, i, $"{argumentsArrayName}[{i}]", sourceTypes, compilation); + arrayElements.Add(elementCast); } - argumentExpressions.Add($"new {elementType.GloballyQualified()}[] {{ {string.Join(", ", arrayElements)} }}"); + argumentExpressions.Add($"new {elementTargetGQ}[] {{ {string.Join(", ", arrayElements)} }}"); } } } else { // Fallback if we can't determine element type - var castExpression = $"global::TUnit.Core.Helpers.CastHelper.Cast<{paramsParam.Type.GloballyQualified()}>({argumentsArrayName}[{regularParamCount}])"; + var sourceType = sourceTypes != null && regularParamCount < sourceTypes.Length ? sourceTypes[regularParamCount] : null; + var castExpression = CastExpressionHelper.GenerateCast(sourceType, paramsParam.Type, $"{argumentsArrayName}[{regularParamCount}]", compilation); argumentExpressions.Add(castExpression); } } return argumentExpressions; } + + private static string GenerateElementCast( + ITypeSymbol elementType, + int argIndex, + string argExpression, + ITypeSymbol?[]? sourceTypes, + CSharpCompilation? compilation) + { + return CastExpressionHelper.GenerateCast( + CastExpressionHelper.GetSourceTypeAt(sourceTypes, argIndex), elementType, argExpression, compilation); + } } diff --git a/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs b/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs deleted file mode 100644 index b81e8bd20b..0000000000 --- a/TUnit.Core.SourceGenerator/Generators/AotConverterHelper.cs +++ /dev/null @@ -1,338 +0,0 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using TUnit.Core.SourceGenerator.CodeGenerators.Helpers; -using TUnit.Core.SourceGenerator.Extensions; - -namespace TUnit.Core.SourceGenerator.Generators; - -internal static class AotConverterHelper -{ - /// - /// Returns null if no conversions found; otherwise newline-separated Register calls. - /// - public static string? GenerateRegistrationCode( - IReadOnlyList methods, - INamedTypeSymbol containingType, - Compilation compilation) - { - var typesToScan = new HashSet(SymbolEqualityComparer.Default); - - foreach (var method in methods) - { - foreach (var parameter in method.Parameters) - { - typesToScan.Add(parameter.Type); - ScanAttributesForTypes(parameter.GetAttributes(), typesToScan); - } - - ScanAttributesForTypes(method.GetAttributes(), typesToScan); - } - - ScanAttributesForTypes(containingType.GetAttributes(), typesToScan); - - foreach (var constructor in containingType.Constructors) - { - if (constructor.IsImplicitlyDeclared) - { - continue; - } - - foreach (var parameter in constructor.Parameters) - { - typesToScan.Add(parameter.Type); - ScanAttributesForTypes(parameter.GetAttributes(), typesToScan); - } - } - - var seen = new HashSet<(string, string)>(); - var registrations = new List(); - - foreach (var type in typesToScan) - { - ScanTypeForConversions(type, seen, registrations, compilation); - } - - return registrations.Count > 0 ? string.Join("\n", registrations) : null; - } - - public static string? GenerateRegistrationCode( - IMethodSymbol method, - INamedTypeSymbol containingType, - Compilation compilation) - { - return GenerateRegistrationCode([method], containingType, compilation); - } - - private static void ScanTypeForConversions( - ITypeSymbol type, - HashSet<(string, string)> seen, - List registrations, - Compilation compilation) - { - if (type is not INamedTypeSymbol namedType) - { - return; - } - - if (!IsAccessibleType(namedType, compilation)) - { - return; - } - - foreach (var member in namedType.GetMembers()) - { - if (member is IMethodSymbol { IsStatic: true, Parameters.Length: 1 } method && - (method.Name == "op_Implicit" || method.Name == "op_Explicit")) - { - var registration = TryGenerateRegistration(method, seen, compilation); - if (registration != null) - { - registrations.Add(registration); - } - } - } - - if (namedType.IsGenericType) - { - foreach (var typeArg in namedType.TypeArguments) - { - ScanTypeForConversions(typeArg, seen, registrations, compilation); - } - } - } - - private static string? TryGenerateRegistration( - IMethodSymbol operatorMethod, - HashSet<(string, string)> seen, - Compilation compilation) - { - var sourceType = operatorMethod.Parameters[0].Type; - var targetType = operatorMethod.ReturnType; - - if (sourceType.IsGenericDefinition() || targetType.IsGenericDefinition()) - { - return null; - } - - if (TypeContainsGenericTypeParameters(sourceType) || TypeContainsGenericTypeParameters(targetType)) - { - return null; - } - - if (sourceType.IsRefLikeType || targetType.IsRefLikeType) - { - return null; - } - - if (sourceType.TypeKind == TypeKind.Pointer || targetType.TypeKind == TypeKind.Pointer || - sourceType.SpecialType == SpecialType.System_Void || targetType.SpecialType == SpecialType.System_Void) - { - return null; - } - - var containingType = operatorMethod.ContainingType; - if (containingType == null || !IsAccessibleType(containingType, compilation)) - { - return null; - } - - if (!IsAccessibleType(sourceType, compilation) || !IsAccessibleType(targetType, compilation)) - { - return null; - } - - if (SymbolEqualityComparer.Default.Equals(sourceType, targetType)) - { - return null; - } - - var sourceGlobal = sourceType.GloballyQualified(); - var targetGlobal = targetType.GloballyQualified(); - - if (!seen.Add((sourceGlobal, targetGlobal))) - { - return null; - } - - return $"global::TUnit.Core.Converters.AotConverterRegistry.Register<{sourceGlobal}, {targetGlobal}>(static source => ({targetGlobal})source);"; - } - - private static void ScanAttributesForTypes(ImmutableArray attributes, HashSet typesToScan) - { - foreach (var attribute in attributes) - { - if (attribute.AttributeClass == null) - { - continue; - } - - if (!DataSourceAttributeHelper.IsDataSourceAttribute(attribute.AttributeClass)) - { - continue; - } - - if (attribute.AttributeClass.IsGenericType) - { - foreach (var typeArg in attribute.AttributeClass.TypeArguments) - { - typesToScan.Add(typeArg); - } - } - - foreach (var arg in attribute.ConstructorArguments) - { - ScanTypedConstantForTypes(arg, typesToScan); - } - - foreach (var namedArg in attribute.NamedArguments) - { - ScanTypedConstantForTypes(namedArg.Value, typesToScan); - } - } - } - - private static void ScanTypedConstantForTypes(TypedConstant constant, HashSet typesToScan) - { - if (constant.IsNull) - { - return; - } - - if (constant is { Kind: TypedConstantKind.Type, Value: ITypeSymbol typeValue }) - { - typesToScan.Add(typeValue); - } - else if (constant.Kind == TypedConstantKind.Array) - { - foreach (var element in constant.Values) - { - ScanTypedConstantForTypes(element, typesToScan); - } - } - else if (constant is { Value: not null, Type: not null }) - { - typesToScan.Add(constant.Type); - } - } - - private static bool IsAccessibleType(ITypeSymbol type, Compilation compilation) - { - if (type.SpecialType != SpecialType.None) - { - return true; - } - - if (type.TypeKind == TypeKind.TypeParameter) - { - return true; - } - - if (type is INamedTypeSymbol namedType) - { - var typeAssembly = namedType.ContainingAssembly; - var currentAssembly = compilation.Assembly; - - if (currentAssembly != null && SymbolEqualityComparer.Default.Equals(typeAssembly, currentAssembly)) - { - return true; - } - - if (namedType.DeclaredAccessibility == Accessibility.Public) - { - if (namedType.IsGenericType) - { - foreach (var typeArg in namedType.TypeArguments) - { - if (!IsAccessibleType(typeArg, compilation)) - { - return false; - } - } - } - - if (namedType.ContainingType != null) - { - return IsAccessibleType(namedType.ContainingType, compilation); - } - - return true; - } - - if (namedType.DeclaredAccessibility == Accessibility.Internal) - { - if (currentAssembly == null) - { - return false; - } - - if (typeAssembly != null && typeAssembly.GivesAccessTo(currentAssembly)) - { - return true; - } - - return false; - } - - if (namedType.IsGenericType) - { - foreach (var typeArg in namedType.TypeArguments) - { - if (!IsAccessibleType(typeArg, compilation)) - { - return false; - } - } - } - - if (namedType.ContainingType != null) - { - return IsAccessibleType(namedType.ContainingType, compilation); - } - - return false; - } - - if (type is IArrayTypeSymbol arrayType) - { - return IsAccessibleType(arrayType.ElementType, compilation); - } - - if (type is IPointerTypeSymbol pointerType) - { - return IsAccessibleType(pointerType.PointedAtType, compilation); - } - - return false; - } - - private static bool TypeContainsGenericTypeParameters(ITypeSymbol type) - { - if (type.TypeKind == TypeKind.TypeParameter) - { - return true; - } - - if (type is INamedTypeSymbol namedTypeSymbol) - { - foreach (var typeArgument in namedTypeSymbol.TypeArguments) - { - if (TypeContainsGenericTypeParameters(typeArgument)) - { - return true; - } - } - } - - if (type is IArrayTypeSymbol arrayTypeSymbol) - { - return TypeContainsGenericTypeParameters(arrayTypeSymbol.ElementType); - } - - if (type is IPointerTypeSymbol pointerTypeSymbol) - { - return TypeContainsGenericTypeParameters(pointerTypeSymbol.PointedAtType); - } - - return false; - } -} diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index ba4ae620e8..45def9904b 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -114,10 +114,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) GenerateInheritedTestSources(context, classInfo); }); - // AOT converter registrations are emitted inline in generated test source files: - // - Non-generic, non-inherited methods: scanned in GroupMethodsByClass (per-class path) - // - Generic or inherited methods: scanned in GenerateTestMetadata (per-method path) - // AotConverterRegistry.Register uses TryAdd, so any overlap is a harmless no-op. } private static InheritsTestsClassMetadata? GetInheritsTestsClassMetadata(GeneratorAttributeSyntaxContext context, CompilationContext compilationContext) @@ -538,20 +534,6 @@ private static void GenerateTestMetadata(CodeWriter writer, TestMethodMetadata t registrationIndex++; } - // AOT converter registrations — only for generic or inherited tests. - // Non-generic, non-inherited methods are covered by the per-class path - // in GroupMethodsByClass, which scans all non-generic methods for conversions. - // Inherited tests (InheritanceDepth > 0) need their own registration because - // the subclass's constructor parameters may require different converters. - if (testMethod.IsGenericType || testMethod.IsGenericMethod || testMethod.InheritanceDepth > 0) - { - var aotRegistrationCode = AotConverterHelper.GenerateRegistrationCode( - testMethod.MethodSymbol, testMethod.TypeSymbol, compilation); - if (aotRegistrationCode != null) - { - EmitAotConverterRegistration(writer, uniqueClassName, aotRegistrationCode); - } - } } /// @@ -915,13 +897,13 @@ private static void EmitConcreteInstanceCreation(CodeWriter writer, TestMethodMe else if (!testMethod.IsGenericType && !testMethod.IsGenericMethod) { // Non-generic (inherited) tests: use InstanceFactoryGenerator for proper required property handling - writer.AppendRaw(InstanceFactoryGenerator.GenerateInstanceFactoryBody(testMethod.TypeSymbol)); + writer.AppendRaw(InstanceFactoryGenerator.GenerateInstanceFactoryBody(testMethod.TypeSymbol, testMethod.CompilationContext.Compilation)); } else if (entry.ClassTypeArgs.Length > 0) { // Generic class with resolved type args: construct the concrete closed type var constructedType = testMethod.TypeSymbol.Construct(entry.ClassTypeArgs); - writer.AppendRaw(InstanceFactoryGenerator.GenerateInstanceFactoryBody(constructedType)); + writer.AppendRaw(InstanceFactoryGenerator.GenerateInstanceFactoryBody(constructedType, testMethod.CompilationContext.Compilation)); } else { @@ -1006,7 +988,9 @@ private static void EmitConcreteInvokeBody(CodeWriter writer, TestMethodMetadata var parametersFromArgs = testMethod.MethodSymbol.Parameters .Where(p => p.Type.Name != "CancellationToken" || p.Type.ContainingNamespace?.ToString() != "System.Threading") .ToArray(); - GenerateConcreteTestInvokerBody(writer, methodName, returnPattern, hasCancellationToken, parametersFromArgs); + var sourceTypes = SourceTypeAnalyzer.GetMethodParameterSourceTypes(testMethod.MethodSymbol); + var comp = testMethod.CompilationContext.Compilation; + GenerateConcreteTestInvokerBody(writer, methodName, returnPattern, hasCancellationToken, parametersFromArgs, sourceTypes, comp); } } @@ -2557,23 +2541,25 @@ private static void GenerateTypedInvokers(CodeWriter writer, TestMethodMetadata { var (hasCancellationToken, parametersFromArgs) = ParseInvokerParameters(testMethod.MethodSymbol); var returnPattern = GetReturnPattern(testMethod.MethodSymbol); - GenerateConcreteTestInvoker(writer, testMethod.MethodSymbol.Name, returnPattern, hasCancellationToken, parametersFromArgs); + var sourceTypes = SourceTypeAnalyzer.GetMethodParameterSourceTypes(testMethod.MethodSymbol); + var comp = testMethod.CompilationContext.Compilation; + GenerateConcreteTestInvoker(writer, testMethod.MethodSymbol.Name, returnPattern, hasCancellationToken, parametersFromArgs, sourceTypes, comp); } } } - private static void GenerateConcreteTestInvoker(CodeWriter writer, string methodName, TestReturnPattern returnPattern, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs) + private static void GenerateConcreteTestInvoker(CodeWriter writer, string methodName, TestReturnPattern returnPattern, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs, ITypeSymbol?[]? sourceTypes = null, CSharpCompilation? compilation = null) { // Generate InvokeTypedTest which is required by CreateExecutableTestFactory writer.AppendLine("InvokeTypedTest = static (instance, args, cancellationToken) =>"); writer.AppendLine("{"); writer.Indent(); - GenerateConcreteTestInvokerBody(writer, methodName, returnPattern, hasCancellationToken, parametersFromArgs); + GenerateConcreteTestInvokerBody(writer, methodName, returnPattern, hasCancellationToken, parametersFromArgs, sourceTypes, compilation); writer.Unindent(); writer.AppendLine("},"); } - private static void GenerateConcreteTestInvokerBody(CodeWriter writer, string methodName, TestReturnPattern returnPattern, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs) + private static void GenerateConcreteTestInvokerBody(CodeWriter writer, string methodName, TestReturnPattern returnPattern, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs, ITypeSymbol?[]? sourceTypes = null, CSharpCompilation? compilation = null) { // Wrap entire body in try-catch to handle synchronous exceptions writer.AppendLine("try"); @@ -2602,7 +2588,10 @@ private static void GenerateConcreteTestInvokerBody(CodeWriter writer, string me // Build tuple reconstruction with proper casting var tupleElements = singleTupleParam.TupleElements.Select((elem, i) => - $"global::TUnit.Core.Helpers.CastHelper.Cast<{elem.Type.GloballyQualified()}>(args[{i}])"); + { + var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); + return CastExpressionHelper.GenerateCast(sourceType, elem.Type, $"args[{i}]", compilation); + }); var tupleConstruction = $"({string.Join(", ", tupleElements)})"; var methodCallReconstructed = hasCancellationToken @@ -2615,9 +2604,10 @@ private static void GenerateConcreteTestInvokerBody(CodeWriter writer, string me writer.AppendLine("{"); writer.Indent(); writer.AppendLine("// Rare case: tuple is wrapped as a single argument"); + var wrappedTupleCast = CastExpressionHelper.GenerateCast(null, singleTupleParam, "args[0]", compilation); var methodCallDirect = hasCancellationToken - ? $"instance.{methodName}(global::TUnit.Core.Helpers.CastHelper.Cast<{singleTupleParam.GloballyQualified()}>(args[0]), cancellationToken)" - : $"instance.{methodName}(global::TUnit.Core.Helpers.CastHelper.Cast<{singleTupleParam.GloballyQualified()}>(args[0]))"; + ? $"instance.{methodName}({wrappedTupleCast}, cancellationToken)" + : $"instance.{methodName}({wrappedTupleCast})"; GenerateReturnHandling(writer, methodCallDirect, returnPattern); writer.Unindent(); writer.AppendLine("}"); @@ -2662,7 +2652,7 @@ private static void GenerateConcreteTestInvokerBody(CodeWriter writer, string me writer.Indent(); // Build the arguments to pass, handling params arrays correctly - var argsToPass = TupleArgumentHelper.GenerateArgumentAccessWithParams(parametersFromArgs, "args", argCount); + var argsToPass = TupleArgumentHelper.GenerateArgumentAccessWithParams(parametersFromArgs, "args", argCount, sourceTypes, compilation); // Add CancellationToken if present if (hasCancellationToken) @@ -3010,25 +3000,6 @@ private static void EmitRegistrationField(CodeWriter writer, string fieldName, s writer.AppendLine("}"); } - private static void EmitAotConverterRegistration(CodeWriter writer, string fieldName, string registrationCode) - { - writer.AppendLine(); - writer.AppendLine("internal static partial class TUnit_TestRegistration"); - writer.AppendLine("{"); - writer.Indent(); - writer.AppendLine($"static readonly int _aot_{fieldName} = RegisterAot_{fieldName}();"); - writer.AppendLine($"static int RegisterAot_{fieldName}()"); - writer.AppendLine("{"); - writer.Indent(); - writer.AppendRaw(registrationCode); - writer.AppendLine(); - writer.AppendLine("return 0;"); - writer.Unindent(); - writer.AppendLine("}"); - writer.Unindent(); - writer.AppendLine("}"); - } - /// /// Pre-generates the attribute factory body for a test method. /// Returns the body (return [...];) without the method signature. @@ -3060,11 +3031,13 @@ private static string PreGenerateInvokeSwitchCase(TestMethodMetadata testMethod, var (hasCancellationToken, parametersFromArgs) = ParseInvokerParameters(testMethod.MethodSymbol); var returnPattern = GetReturnPattern(testMethod.MethodSymbol); + var sourceTypes = SourceTypeAnalyzer.GetMethodParameterSourceTypes(testMethod.MethodSymbol); + var compilation = testMethod.CompilationContext.Compilation; writer.AppendLine($"case {methodIndex}:"); writer.AppendLine("{"); writer.Indent(); - GenerateConcreteTestInvokerBody(writer, testMethod.MethodSymbol.Name, returnPattern, hasCancellationToken, parametersFromArgs); + GenerateConcreteTestInvokerBody(writer, testMethod.MethodSymbol.Name, returnPattern, hasCancellationToken, parametersFromArgs, sourceTypes, compilation); writer.Unindent(); writer.AppendLine("}"); @@ -3472,22 +3445,16 @@ private static IEnumerable GroupMethodsByClass(ImmutableArray m.MethodSymbol).ToList(); - var aotRegistrationCode = AotConverterHelper.GenerateRegistrationCode( - methodSymbols, typeSymbol, first.CompilationContext.Compilation); - return new ClassTestGroup { ClassFullyQualified = className, TestSourceName = testSourceName, Methods = methodSourceCodes.ToEquatableArray(), AttributeGroups = distinctBodies.ToEquatableArray(), - InstanceFactoryBodyCode = InstanceFactoryGenerator.GenerateInstanceFactoryBody(typeSymbol), + InstanceFactoryBodyCode = InstanceFactoryGenerator.GenerateInstanceFactoryBody(typeSymbol, first.CompilationContext.Compilation), ReflectionFieldAccessorsCode = PreGenerateReflectionFieldAccessors(typeSymbol), SharedLocalsCode = PreGenerateSharedLocals(typeSymbol, className), SharedFieldsCode = PreGenerateSharedFields(typeSymbol, className), - AotConverterRegistrationCode = aotRegistrationCode, }; }); } @@ -3621,11 +3588,6 @@ private static void GeneratePerClassTestSource(SourceProductionContext context, EmitRegistrationField(writer, classGroup.TestSourceName, $"global::TUnit.Core.SourceRegistrar.RegisterEntries<{classGroup.ClassFullyQualified}>(static () => {classGroup.TestSourceName}.Entries)"); - if (classGroup.AotConverterRegistrationCode != null) - { - EmitAotConverterRegistration(writer, classGroup.TestSourceName, classGroup.AotConverterRegistrationCode); - } - context.AddSource($"{classGroup.TestSourceName}.g.cs", SourceText.From(writer.ToString(), Encoding.UTF8)); } catch (Exception ex) diff --git a/TUnit.Core.SourceGenerator/Models/ClassTestGroup.cs b/TUnit.Core.SourceGenerator/Models/ClassTestGroup.cs index 700988c5b6..e0fb7bc383 100644 --- a/TUnit.Core.SourceGenerator/Models/ClassTestGroup.cs +++ b/TUnit.Core.SourceGenerator/Models/ClassTestGroup.cs @@ -24,6 +24,4 @@ public sealed record ClassTestGroup /// Used to inline MethodMetadata construction into field initializers. /// public required string SharedFieldsCode { get; init; } - - public string? AotConverterRegistrationCode { get; init; } } From 30cfd0b306b57cc7c110bb02479d2d5c6c657898 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:11:06 +0100 Subject: [PATCH 07/30] =?UTF-8?q?fix:=20address=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20constructor=20selection=20and=20stale=20test=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reuse InstanceFactoryGenerator.GetPrimaryConstructor in SourceTypeAnalyzer so source type analysis targets the same constructor (respecting [TestConstructor]) - Rename AotConverterGeneratorTests → FullProjectGenerationTests (stale name) --- ...mes_CompilesSuccessfully.DotNet10_0.verified.txt | 8 ++++---- ...ames_CompilesSuccessfully.DotNet8_0.verified.txt | 8 ++++---- ...ames_CompilesSuccessfully.DotNet9_0.verified.txt | 8 ++++---- ...peNames_CompilesSuccessfully.Net4_7.verified.txt | 8 ++++---- ...eratorTests.cs => FullProjectGenerationTests.cs} | 0 .../Helpers/InstanceFactoryGenerator.cs | 2 +- .../CodeGenerators/Helpers/SourceTypeAnalyzer.cs | 13 +++---------- 7 files changed, 20 insertions(+), 27 deletions(-) rename TUnit.Core.SourceGenerator.Tests/{AotConverterGeneratorTests.cs => FullProjectGenerationTests.cs} (100%) diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt index e0a05c3d5f..6b3a32caa8 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt index e0a05c3d5f..6b3a32caa8 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt index e0a05c3d5f..6b3a32caa8 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt index e03a68e441..696cce2aa3 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.cs b/TUnit.Core.SourceGenerator.Tests/FullProjectGenerationTests.cs similarity index 100% rename from TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.cs rename to TUnit.Core.SourceGenerator.Tests/FullProjectGenerationTests.cs diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs index 6135fbe9ea..2af1b5b330 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs @@ -249,7 +249,7 @@ public static string GenerateInstanceFactoryBody(ITypeSymbol typeSymbol, CSharpC return bodyWriter.ToString(); } - private static IMethodSymbol? GetPrimaryConstructor(ITypeSymbol typeSymbol) + internal static IMethodSymbol? GetPrimaryConstructor(ITypeSymbol typeSymbol) { // Materialize constructors once to avoid multiple enumerations var constructors = typeSymbol.GetMembers() diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs index bb60465f4f..a489918fc3 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -94,16 +94,9 @@ internal static class SourceTypeAnalyzer return null; } - // Find constructor with most parameters - IMethodSymbol? constructor = null; - foreach (var member in classType.GetMembers()) - { - if (member is IMethodSymbol { MethodKind: MethodKind.Constructor, IsStatic: false, IsImplicitlyDeclared: false } ctor - && (constructor == null || ctor.Parameters.Length > constructor.Parameters.Length)) - { - constructor = ctor; - } - } + // Use the same constructor selection as InstanceFactoryGenerator to ensure + // source type analysis targets the constructor that will actually be called. + var constructor = InstanceFactoryGenerator.GetPrimaryConstructor(classType); if (constructor == null || constructor.Parameters.Length == 0) { From b0e38a0b23d207cf900eb0cd935974f34b870f5a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:30:55 +0100 Subject: [PATCH 08/30] fix: extend typed casts to params elements and fix params IEnumerable handling - SourceTypeAnalyzer now sizes the source type array to max argument count (not parameter count) for params methods, so elements beyond position 0 get typed casts instead of CastHelper.Cast fallback - TupleArgumentHelper.GetParamsElementType now extracts element types from generic collection params (IEnumerable, List, etc.), not just T[], fixing a pre-existing bug where params IEnumerable would try to cast a single element to the collection type instead of constructing an array --- .../ArgsAsArrayTests.Test.verified.txt | 40 +++++++++---------- ...mpilesSuccessfully.DotNet10_0.verified.txt | 8 ++-- ...ompilesSuccessfully.DotNet8_0.verified.txt | 8 ++-- ...ompilesSuccessfully.DotNet9_0.verified.txt | 8 ++-- ...s_CompilesSuccessfully.Net4_7.verified.txt | 8 ++-- .../Tests2112.Test.verified.txt | 12 +++--- .../Helpers/SourceTypeAnalyzer.cs | 33 +++++++++++++-- .../Helpers/TupleArgumentHelper.cs | 22 +++++++++- 8 files changed, 93 insertions(+), 46 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt index bbd7990b56..8b943ffa05 100644 --- a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -58,27 +58,27 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Collect } case 2: { - instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) }); + instance.Params(new string[] { (string)args[0], (string)args[1] }); return default(global::System.Threading.Tasks.ValueTask); } case 3: { - instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); + instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2] }); return default(global::System.Threading.Tasks.ValueTask); } case 4: { - instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Params(new string[] { (string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -98,37 +98,37 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Collect { case 0: { - instance.ParamsEnumerable(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + instance.ParamsEnumerable(new string[0]); return default(global::System.Threading.Tasks.ValueTask); } case 1: { - instance.ParamsEnumerable(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + instance.ParamsEnumerable((args[0] is null ? null : args[0] is global::System.Collections.Generic.IEnumerable arr ? arr : new string[] { (string)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } case 2: { - instance.ParamsEnumerable(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1] }); return default(global::System.Threading.Tasks.ValueTask); } case 3: { - instance.ParamsEnumerable(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2] }); return default(global::System.Threading.Tasks.ValueTask); } case 4: { - instance.ParamsEnumerable(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.ParamsEnumerable(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.ParamsEnumerable(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -148,37 +148,37 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Collect { case 1: { - instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], new string[0]); return default(global::System.Threading.Tasks.ValueTask); } case 2: { - instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], (args[1] is null ? null : args[1] is global::System.Collections.Generic.IEnumerable arr ? arr : new string[] { (string)args[1] })); return default(global::System.Threading.Tasks.ValueTask); } case 3: { - instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2] }); return default(global::System.Threading.Tasks.ValueTask); } case 4: { - instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3] }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); return default(global::System.Threading.Tasks.ValueTask); } case 7: { - instance.Following_Non_Params((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt index 6b3a32caa8..e0a05c3d5f 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt index 6b3a32caa8..e0a05c3d5f 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt index 6b3a32caa8..e0a05c3d5f 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt index 696cce2aa3..e03a68e441 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt index f55c602961..86db569d12 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -105,27 +105,27 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long[]), "arr", new gl } case 3: { - instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2] }); return default(global::System.Threading.Tasks.ValueTask); } case 4: { - instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3] }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); return default(global::System.Threading.Tasks.ValueTask); } case 7: { - instance.Test2((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs index a489918fc3..31196daf32 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -45,11 +45,16 @@ internal static class SourceTypeAnalyzer // Filter to non-CancellationToken parameters var parameterCount = 0; + var hasParams = false; foreach (var p in method.Parameters) { if (p.Type.Name != "CancellationToken" || p.Type.ContainingNamespace?.ToString() != "System.Threading") { parameterCount++; + if (p.IsParams) + { + hasParams = true; + } } } @@ -58,7 +63,11 @@ internal static class SourceTypeAnalyzer return null; } - return ExtractSourceTypesFromArguments(argumentsAttributes, parameterCount); + // For params methods, size the array to max argument count across all [Arguments] + // rows so that params elements beyond position 0 also get typed casts. + var arraySize = hasParams ? GetMaxArgumentCount(argumentsAttributes, parameterCount) : parameterCount; + + return ExtractSourceTypesFromArguments(argumentsAttributes, arraySize); } /// @@ -103,7 +112,25 @@ internal static class SourceTypeAnalyzer return null; } - return ExtractSourceTypesFromArguments(argumentsAttributes, constructor.Parameters.Length); + var ctorParamCount = constructor.Parameters.Length; + var ctorHasParams = constructor.Parameters[ctorParamCount - 1].IsParams; + var ctorArraySize = ctorHasParams ? GetMaxArgumentCount(argumentsAttributes, ctorParamCount) : ctorParamCount; + + return ExtractSourceTypesFromArguments(argumentsAttributes, ctorArraySize); + } + + private static int GetMaxArgumentCount(List argumentsAttributes, int baseCount) + { + foreach (var attr in argumentsAttributes) + { + var values = GetArgumentValues(attr); + if (values != null && values.Value.Length > baseCount) + { + baseCount = values.Value.Length; + } + } + + return baseCount; } private static ITypeSymbol?[]? ExtractSourceTypesFromArguments( @@ -183,7 +210,7 @@ internal static class SourceTypeAnalyzer // Non-generic [Arguments(params object?[] args)] — the first arg is an array if (firstArg.Kind == TypedConstantKind.Array) { - return firstArg.Values; + return firstArg.Values.IsDefault ? null : firstArg.Values; } // Generic [Arguments(T value)] — single typed value diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs index 54089d063c..f14840be7d 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs @@ -109,7 +109,7 @@ public static List GenerateArgumentAccessWithParams( // Handle params array parameter var paramsParam = parameters[parameters.Count - 1]; - var elementType = (paramsParam.Type as IArrayTypeSymbol)?.ElementType; + var elementType = GetParamsElementType(paramsParam.Type); if (elementType != null) { @@ -167,6 +167,26 @@ public static List GenerateArgumentAccessWithParams( return argumentExpressions; } + /// + /// Extracts the element type from a params parameter type. + /// Handles T[] (IArrayTypeSymbol) and generic collection types like IEnumerable<T>, List<T>, etc. + /// + private static ITypeSymbol? GetParamsElementType(ITypeSymbol paramsType) + { + if (paramsType is IArrayTypeSymbol arrayType) + { + return arrayType.ElementType; + } + + // C# 13 params collections: IEnumerable, ReadOnlySpan, List, etc. + if (paramsType is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1 } namedType) + { + return namedType.TypeArguments[0]; + } + + return null; + } + private static string GenerateElementCast( ITypeSymbol elementType, int argIndex, From bad712c7caaa08e9721577c7b18dbe68a45b22ef Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:45:45 +0100 Subject: [PATCH 09/30] =?UTF-8?q?fix:=20complete=20class=20rename=20AotCon?= =?UTF-8?q?verterGeneratorTests=20=E2=86=92=20FullProjectGenerationTests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...TypeNames_CompilesSuccessfully.DotNet10_0.verified.txt | 8 ++++---- ...eTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt | 8 ++++---- ...eTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt | 8 ++++---- ...cateTypeNames_CompilesSuccessfully.Net4_7.verified.txt | 8 ++++---- .../FullProjectGenerationTests.cs | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt index e0a05c3d5f..6b3a32caa8 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet10_0.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt index e0a05c3d5f..6b3a32caa8 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet8_0.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt index e0a05c3d5f..6b3a32caa8 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.DotNet9_0.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt index e03a68e441..696cce2aa3 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt @@ -78,16 +78,16 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Loading assembly containing: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } - var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests); + var type_2 = typeof(global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests); // Force module initializer to complete before proceeding // RunClassConstructor triggers static constructor, which can only run AFTER module initializer completes global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type_2.TypeHandle); try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests"); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Assembly initialized: global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests"); } catch { /* TUnit.Core not available - skip logging */ } } @@ -95,7 +95,7 @@ file static class TUnitInfrastructure { try { - global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.AotConverterGeneratorTests: " + ex.Message); + global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogTrace("[ModuleInitializer:TestsBase`1] Failed to load global::TUnit.Core.SourceGenerator.Tests.FullProjectGenerationTests: " + ex.Message); } catch { /* TUnit.Core not available - skip logging */ } } diff --git a/TUnit.Core.SourceGenerator.Tests/FullProjectGenerationTests.cs b/TUnit.Core.SourceGenerator.Tests/FullProjectGenerationTests.cs index 62204a8cd0..ebafaf4c4e 100644 --- a/TUnit.Core.SourceGenerator.Tests/FullProjectGenerationTests.cs +++ b/TUnit.Core.SourceGenerator.Tests/FullProjectGenerationTests.cs @@ -2,7 +2,7 @@ namespace TUnit.Core.SourceGenerator.Tests; -public class AotConverterGeneratorTests : TestsBase +public class FullProjectGenerationTests : TestsBase { [Test] [Skip("Need to investigate - Behaves differently on local vs CI")] From 0e011de03acb54a8694d3f9e1a0fbcd3a54d741a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:54:28 +0100 Subject: [PATCH 10/30] fix: remove redundant IsNumeric check in CastExpressionHelper Numeric conversions are always classified as implicit or explicit, so the IsNumeric guard was unreachable. --- .../CodeGenerators/Helpers/CastExpressionHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index b9320160b2..141519dc69 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -37,7 +37,7 @@ public static string GenerateCast( // Check if compiler can resolve conversion var conversion = compilation.ClassifyConversion(sourceType, targetType); - if (conversion.Exists && (conversion.IsImplicit || conversion.IsExplicit || conversion.IsNumeric)) + if (conversion.Exists && (conversion.IsImplicit || conversion.IsExplicit)) { var sourceGQ = sourceType.GloballyQualified(); return $"({targetGQ})({sourceGQ}){argsExpression}"; From 57748270ef3281de35406a85b5f72a4ef0c826d6 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:09:04 +0100 Subject: [PATCH 11/30] fix: remove redundant conversion.Exists guard in CastExpressionHelper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any conversion where IsImplicit or IsExplicit is true already implies Exists is true, making the Exists check redundant. Re: source type caching suggestion — the three GetMethodParameterSourceTypes call sites are on mutually exclusive code paths (per-class switch case, per-entry inline lambda, generic fallback), so a given method is only analyzed once per generation run. Caching in TestMethodMetadata would require storing ITypeSymbol which breaks incremental equality. --- .../CodeGenerators/Helpers/CastExpressionHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index 141519dc69..a68d589414 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -37,7 +37,7 @@ public static string GenerateCast( // Check if compiler can resolve conversion var conversion = compilation.ClassifyConversion(sourceType, targetType); - if (conversion.Exists && (conversion.IsImplicit || conversion.IsExplicit)) + if (conversion.IsImplicit || conversion.IsExplicit) { var sourceGQ = sourceType.GloballyQualified(); return $"({targetGQ})({sourceGQ}){argsExpression}"; From 8d33aa2f196a71f6f38571bb351f6880739db7c3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:22:43 +0100 Subject: [PATCH 12/30] perf: use direct casts for params overflow positions When source type is unknown for params elements beyond [Arguments] row length, default to the element type. Since SourceTypeAnalyzer already verified all data sources are [Arguments], these overflow positions use the same element type as target, producing (T)args[i] instead of CastHelper.Cast(args[i]). --- .../ArgsAsArrayTests.Test.verified.txt | 18 +++++++++--------- .../Tests2112.Test.verified.txt | 16 ++++++++-------- .../Helpers/TupleArgumentHelper.cs | 8 ++++++-- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt index 8b943ffa05..1b977b7263 100644 --- a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt @@ -68,17 +68,17 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Collect } case 4: { - instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2], (string)args[3] }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2], (string)args[3], (string)args[4] }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.Params(new string[] { (string)args[0], (string)args[1], (string)args[2], (string)args[3], (string)args[4], (string)args[5] }); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -118,17 +118,17 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Collect } case 4: { - instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2], (string)args[3] }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2], (string)args[3], (string)args[4] }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2], global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.ParamsEnumerable(new string[] { (string)args[0], (string)args[1], (string)args[2], (string)args[3], (string)args[4], (string)args[5] }); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -168,17 +168,17 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Collect } case 5: { - instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3], (string)args[4] }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3], (string)args[4], (string)args[5] }); return default(global::System.Threading.Tasks.ValueTask); } case 7: { - instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); + instance.Following_Non_Params((int)args[0], new string[] { (string)args[1], (string)args[2], (string)args[3], (string)args[4], (string)args[5], (string)args[6] }); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt index 86db569d12..1918a8ca05 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt @@ -55,27 +55,27 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long[]), "arr", new gl } case 3: { - instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); + instance.Test((int)args[0], new long[] { (long)args[1], (long)args[2] }); return default(global::System.Threading.Tasks.ValueTask); } case 4: { - instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + instance.Test((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3] }); return default(global::System.Threading.Tasks.ValueTask); } case 5: { - instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.Test((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], (long)args[4] }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.Test((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], (long)args[4], (long)args[5] }); return default(global::System.Threading.Tasks.ValueTask); } case 7: { - instance.Test((int)args[0], new long[] { (long)args[1], global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); + instance.Test((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], (long)args[4], (long)args[5], (long)args[6] }); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -115,17 +115,17 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long[]), "arr", new gl } case 5: { - instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], (long)args[4] }); return default(global::System.Threading.Tasks.ValueTask); } case 6: { - instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], (long)args[4], (long)args[5] }); return default(global::System.Threading.Tasks.ValueTask); } case 7: { - instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); + instance.Test2((int)args[0], new long[] { (long)args[1], (long)args[2], (long)args[3], (long)args[4], (long)args[5], (long)args[6] }); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs index f14840be7d..86dac849d3 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs @@ -194,7 +194,11 @@ private static string GenerateElementCast( ITypeSymbol?[]? sourceTypes, CSharpCompilation? compilation) { - return CastExpressionHelper.GenerateCast( - CastExpressionHelper.GetSourceTypeAt(sourceTypes, argIndex), elementType, argExpression, compilation); + // Default to elementType when source type is unknown — for params overflow positions + // (beyond [Arguments] row length), the element type is statically known and a direct + // cast is sufficient. These positions are only reachable when all data sources are + // [Arguments], which SourceTypeAnalyzer has already verified. + var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, argIndex) ?? elementType; + return CastExpressionHelper.GenerateCast(sourceType, elementType, argExpression, compilation); } } From 83e993481117e4a188b793ac1bbb3008efe157f9 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:32:26 +0100 Subject: [PATCH 13/30] fix: skip redundant unbox+rebox for boxing conversions, qualify ArgumentsAttribute check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - When source type boxes to target (e.g., int → object), skip the cast entirely since args[i] is already object - Add namespace check to IsArgumentsAttribute to avoid matching user-defined attributes with the same name --- .../CodeGenerators/Helpers/CastExpressionHelper.cs | 6 ++++++ .../CodeGenerators/Helpers/SourceTypeAnalyzer.cs | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index a68d589414..258c1ed526 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -37,6 +37,12 @@ public static string GenerateCast( // Check if compiler can resolve conversion var conversion = compilation.ClassifyConversion(sourceType, targetType); + // Boxing: args[i] is already object — no cast needed + if (conversion.IsBoxing) + { + return argsExpression; + } + if (conversion.IsImplicit || conversion.IsExplicit) { var sourceGQ = sourceType.GloballyQualified(); diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs index 31196daf32..1e28c50abb 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -220,6 +220,7 @@ private static int GetMaxArgumentCount(List argumentsAttributes, private static bool IsArgumentsAttribute(AttributeData attr) { - return attr.AttributeClass?.Name is "ArgumentsAttribute"; + return attr.AttributeClass?.Name is "ArgumentsAttribute" + && attr.AttributeClass.ContainingNamespace?.ToString() == "TUnit.Core"; } } From a78586c116cd23bcba546eca025f7d4ac04a4ea0 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:52:46 +0100 Subject: [PATCH 14/30] fix: constrain IsBoxing shortcut to object/ValueType/Enum targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boxing-to-interface (e.g., int → IComparable) also sets IsBoxing=true, but returning bare args[i] would fail to compile since object? cannot be implicitly converted to an interface type. Restrict the shortcut to System.Object, System.ValueType, and System.Enum where args[i] is already the correct type. --- .../CodeGenerators/Helpers/CastExpressionHelper.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index 258c1ed526..b09326c7e2 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -37,8 +37,13 @@ public static string GenerateCast( // Check if compiler can resolve conversion var conversion = compilation.ClassifyConversion(sourceType, targetType); - // Boxing: args[i] is already object — no cast needed - if (conversion.IsBoxing) + // Boxing to object/ValueType/Enum: args[i] is already typed correctly, no cast needed. + // Do NOT skip the cast for boxing-to-interface (e.g. int → IComparable) — + // args[i] is object?, which cannot be implicitly converted to an interface type. + if (conversion.IsBoxing + && targetType.SpecialType is SpecialType.System_Object + or SpecialType.System_ValueType + or SpecialType.System_Enum) { return argsExpression; } From 7b9e5616c5136435bc233fc825d92ae7a311f24e Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:04:39 +0100 Subject: [PATCH 15/30] fix: improve clarity in SourceTypeAnalyzer and TupleArgumentHelper - Explain why mixed data sources cause whole-method fallback - Skip already-inconsistent positions early in ExtractSourceTypesFromArguments - Remove firstRow variable (redundant with null check after early-continue) - Fix stale "use Math.Min" comment in dynamic count path --- .../CodeGenerators/Helpers/SourceTypeAnalyzer.cs | 11 +++++++++-- .../CodeGenerators/Helpers/TupleArgumentHelper.cs | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs index 1e28c50abb..19782da006 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -37,7 +37,9 @@ internal static class SourceTypeAnalyzer return null; } - // If ANY non-Arguments data source exists, we can't guarantee types + // The invoke lambda is shared across all data sources for this method. + // We can't emit per-source-type cast code, so if any source is dynamic + // we fall back to CastHelper.Cast for the entire method. if (argumentsAttributes.Count != dataSources.Count) { return null; @@ -159,6 +161,11 @@ private static int GetMaxArgumentCount(List argumentsAttributes, for (var i = 0; i < parameterCount && i < values.Value.Length; i++) { + if (!consistent[i]) + { + continue; + } + var tc = values.Value[i]; // Null literal or error → unknown source type for this position @@ -168,7 +175,7 @@ private static int GetMaxArgumentCount(List argumentsAttributes, continue; } - if (firstRow || sourceTypes[i] == null) + if (sourceTypes[i] == null) { sourceTypes[i] = tc.Type; } diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs index 86dac849d3..f66a88459e 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs @@ -59,7 +59,7 @@ public static List GenerateArgumentAccessWithParams( // No params array - just cast each argument if (argCountExpression != null) { - // Dynamic count - use Math.Min + // Dynamic count — iterate all parameters for (var i = 0; i < parameters.Count; i++) { var param = parameters[i]; From 8d872591f9406080a8c1d705c2367b52f150beae Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:17:16 +0100 Subject: [PATCH 16/30] fix: strip BOM from snapshot files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tooling artifact from accepting received files on Windows — the test runner writes UTF-8 with BOM but the existing snapshots are plain ASCII. --- .../AbstractTests.AbstractClass.verified.txt | 1 - .../ArgsAsArrayTests.Test.verified.txt | 2 +- .../AssemblyLoaderTests.Test.Net4_7.verified.txt | 2 +- .../BasicTests.Test.Net4_7.verified.txt | 2 +- .../Bugs2971NullableTypeTest.Test.Net4_7.verified.txt | 2 +- ...Tests.BasicTest_WithConflictingNamespace.Net4_7.verified.txt | 2 +- ....DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt | 2 +- ...Tests.HooksTest_WithConflictingNamespace.Net4_7.verified.txt | 2 +- ...ests.MatrixTest_WithConflictingNamespace.Net4_7.verified.txt | 2 +- ...ethodDataSource_WithConflictingNamespace.Net4_7.verified.txt | 2 +- ...TupleDataSource_WithConflictingNamespace.Net4_7.verified.txt | 2 +- .../DisableReflectionScannerTests.Test.Net4_7.verified.txt | 2 +- ...hDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt | 2 +- .../HooksTests.NullableByteArgumentTests.verified.txt | 1 - .../STAThreadTests.Test.Net4_7.verified.txt | 2 +- .../Tests1889.Test.Net4_7.verified.txt | 2 +- TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt | 2 +- 17 files changed, 15 insertions(+), 17 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/AbstractTests.AbstractClass.verified.txt b/TUnit.Core.SourceGenerator.Tests/AbstractTests.AbstractClass.verified.txt index 5f282702bb..e69de29bb2 100644 --- a/TUnit.Core.SourceGenerator.Tests/AbstractTests.AbstractClass.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/AbstractTests.AbstractClass.verified.txt @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt index 1b977b7263..a8b459eccf 100644 --- a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.Net4_7.verified.txt index 4086603606..c7a10a0de9 100644 --- a/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/BasicTests.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/BasicTests.Test.Net4_7.verified.txt index 4086603606..c7a10a0de9 100644 --- a/TUnit.Core.SourceGenerator.Tests/BasicTests.Test.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/BasicTests.Test.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/Bugs2971NullableTypeTest.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/Bugs2971NullableTypeTest.Test.Net4_7.verified.txt index 0467212ff8..506a18ca13 100644 --- a/TUnit.Core.SourceGenerator.Tests/Bugs2971NullableTypeTest.Test.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Bugs2971NullableTypeTest.Test.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.BasicTest_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.BasicTest_WithConflictingNamespace.Net4_7.verified.txt index 4086603606..c7a10a0de9 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.BasicTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.BasicTest_WithConflictingNamespace.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt index 885cd98c52..59a9aa9517 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.HooksTest_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.HooksTest_WithConflictingNamespace.Net4_7.verified.txt index 731032feb0..2f50bccc15 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.HooksTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.HooksTest_WithConflictingNamespace.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.Net4_7.verified.txt index a07386699e..ebe16e36c3 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.Net4_7.verified.txt index 2e2148a732..17e51d2fa4 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.TupleDataSource_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.TupleDataSource_WithConflictingNamespace.Net4_7.verified.txt index 0a26c26995..f8e5acfcdf 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.TupleDataSource_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.TupleDataSource_WithConflictingNamespace.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/DisableReflectionScannerTests.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/DisableReflectionScannerTests.Test.Net4_7.verified.txt index 4086603606..c7a10a0de9 100644 --- a/TUnit.Core.SourceGenerator.Tests/DisableReflectionScannerTests.Test.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DisableReflectionScannerTests.Test.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt index 696cce2aa3..6b3a32caa8 100644 --- a/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DuplicateTypeNameAcrossAssembliesTests.InfrastructureGenerator_WithDuplicateTypeNames_CompilesSuccessfully.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable using TUnit.Core.Logging; diff --git a/TUnit.Core.SourceGenerator.Tests/HooksTests.NullableByteArgumentTests.verified.txt b/TUnit.Core.SourceGenerator.Tests/HooksTests.NullableByteArgumentTests.verified.txt index 5f282702bb..e69de29bb2 100644 --- a/TUnit.Core.SourceGenerator.Tests/HooksTests.NullableByteArgumentTests.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/HooksTests.NullableByteArgumentTests.verified.txt @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/TUnit.Core.SourceGenerator.Tests/STAThreadTests.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/STAThreadTests.Test.Net4_7.verified.txt index 008d7c89a1..7b75d88de4 100644 --- a/TUnit.Core.SourceGenerator.Tests/STAThreadTests.Test.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/STAThreadTests.Test.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt index b970bc3aba..a6f83123c8 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt index 1918a8ca05..b734cce4bc 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable From d989592ea4fe31b368c9aacb7d2532204009dae8 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:01:42 +0100 Subject: [PATCH 17/30] fix: remove dead firstRow variable, include type in attribute hash - Remove leftover firstRow variable from ExtractSourceTypesFromArguments - Mix TypedConstant.Type into ComputeAttributeHash to distinguish int 1 from string "1" for incremental cache invalidation --- .../CodeGenerators/Helpers/SourceTypeAnalyzer.cs | 4 ---- TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs | 4 +++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs index 19782da006..fdb526f17a 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -147,8 +147,6 @@ private static int GetMaxArgumentCount(List argumentsAttributes, consistent[i] = true; } - var firstRow = true; - foreach (var attr in argumentsAttributes) { var values = GetArgumentValues(attr); @@ -184,8 +182,6 @@ private static int GetMaxArgumentCount(List argumentsAttributes, consistent[i] = false; } } - - firstRow = false; } // Null out inconsistent positions diff --git a/TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs b/TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs index 5eea2e8d4f..8fe02b399f 100644 --- a/TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs +++ b/TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs @@ -139,7 +139,9 @@ private static int HashTypedConstant(TypedConstant constant) return hash; } - return constant.Value?.ToString()?.GetHashCode() ?? 0; + var typeHash = constant.Type?.ToDisplayString()?.GetHashCode() ?? 0; + var valueHash = constant.Value?.ToString()?.GetHashCode() ?? 0; + return (typeHash * 31) ^ valueHash; } } } From f1211f55b7931f35b7df504512c4046349f8c311 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:31:03 +0100 Subject: [PATCH 18/30] refactor: simplify after code review - Use WellKnownFullyQualifiedClassNames for ArgumentsAttribute check instead of raw string literals - Use AsSpan().Fill(true) instead of manual loop for consistent[] init - Collapse 4 identical cast-loops into 2 using Math.Min for upper bound (argCount is int.MaxValue for dynamic count, making the branch redundant) --- .../Helpers/SourceTypeAnalyzer.cs | 8 +-- .../Helpers/TupleArgumentHelper.cs | 50 +++++-------------- 2 files changed, 14 insertions(+), 44 deletions(-) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs index fdb526f17a..dda3b641c8 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -141,11 +141,7 @@ private static int GetMaxArgumentCount(List argumentsAttributes, { var sourceTypes = new ITypeSymbol?[parameterCount]; var consistent = new bool[parameterCount]; - - for (var i = 0; i < parameterCount; i++) - { - consistent[i] = true; - } + consistent.AsSpan().Fill(true); foreach (var attr in argumentsAttributes) { @@ -224,6 +220,6 @@ private static int GetMaxArgumentCount(List argumentsAttributes, private static bool IsArgumentsAttribute(AttributeData attr) { return attr.AttributeClass?.Name is "ArgumentsAttribute" - && attr.AttributeClass.ContainingNamespace?.ToString() == "TUnit.Core"; + && attr.AttributeClass.ToDisplayString() == WellKnownFullyQualifiedClassNames.ArgumentsAttribute.WithoutGlobalPrefix; } } diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs index f66a88459e..622f1c960f 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs @@ -57,26 +57,13 @@ public static List GenerateArgumentAccessWithParams( if (!hasParams) { // No params array - just cast each argument - if (argCountExpression != null) + var upperBound = Math.Min(parameters.Count, argCount); + for (var i = 0; i < upperBound; i++) { - // Dynamic count — iterate all parameters - for (var i = 0; i < parameters.Count; i++) - { - var param = parameters[i]; - var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); - var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); - argumentExpressions.Add(castExpression); - } - } - else - { - for (var i = 0; i < parameters.Count && i < argCount; i++) - { - var param = parameters[i]; - var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); - var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); - argumentExpressions.Add(castExpression); - } + var param = parameters[i]; + var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); + var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); + argumentExpressions.Add(castExpression); } } else @@ -85,26 +72,13 @@ public static List GenerateArgumentAccessWithParams( var regularParamCount = parameters.Count - 1; // Handle regular parameters - if (argCountExpression != null) + var upperBound = Math.Min(regularParamCount, argCount); + for (var i = 0; i < upperBound; i++) { - // Dynamic count - for (var i = 0; i < regularParamCount; i++) - { - var param = parameters[i]; - var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); - var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); - argumentExpressions.Add(castExpression); - } - } - else - { - for (var i = 0; i < regularParamCount && i < argCount; i++) - { - var param = parameters[i]; - var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); - var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); - argumentExpressions.Add(castExpression); - } + var param = parameters[i]; + var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); + var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); + argumentExpressions.Add(castExpression); } // Handle params array parameter From 2515861ffaeba1a3110a74a95bc7bad5549607bc Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:01:20 +0100 Subject: [PATCH 19/30] fix: generate multi-source switch casts for mixed Arguments + ClassDataSource When a class has both [Arguments] and [ClassDataSource] attributes, the generated InstanceFactory lambda is shared across all data source rows. A single static cast can't handle both argument types (e.g. int from Arguments and IntDataSource from ClassDataSource), causing InvalidCastException in AOT mode where CastHelper can't discover implicit conversions via reflection. Rewrite SourceTypeAnalyzer to collect ALL possible source types per parameter position across all data source attributes, using the ITypedDataSourceAttribute marker interface (not hardcoded attribute names) so user custom data sources are supported. When multiple source types exist at a position, CastExpressionHelper generates a pattern- matching switch expression with per-type arms, compiling all necessary conversions at build time. Fixes DataSourceCountTests.ArgumentsWithClassDataSourceTests AOT failure. --- ...nflictingNamespace.DotNet10_0.verified.txt | 2 +- .../DataDrivenTests.Test.verified.txt | 2 +- .../DecimalArgumentTests.Test.verified.txt | 4 +- ...xpectedArgumentTypeTests.Test.verified.txt | 4 +- .../Tests2083.Test.verified.txt | 2 +- .../Helpers/CastExpressionHelper.cs | 81 +++++ .../Helpers/InstanceFactoryGenerator.cs | 6 +- .../Helpers/SourceTypeAnalyzer.cs | 318 ++++++++++++++---- .../Helpers/TupleArgumentHelper.cs | 36 +- .../Generators/TestMetadataGenerator.cs | 11 +- .../ArgumentsWithClassDataSourceTests.cs | 4 +- 11 files changed, 370 insertions(+), 100 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt index 59a9aa9517..eaaf3291db 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt index 59a9aa9517..eaaf3291db 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt index 42726811db..762bfdc7e6 100644 --- a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt @@ -91,7 +91,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.Transfer(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.Transfer((args[0] switch { int __s => (decimal)__s, double __s => (decimal)__s, _ => (decimal)args[0] }), (args[1] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => (decimal)args[1] }))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -224,7 +224,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Test((args[0] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => (decimal)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt index cf0fd231dd..f9797a89f7 100644 --- a/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt @@ -46,7 +46,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Type), { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.TypedArguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (global::System.Type)args[1])); + return new global::System.Threading.Tasks.ValueTask(instance.TypedArguments((args[0] switch { double __s => (object)__s, float __s => (object)__s, sbyte __s => (object)__s, byte __s => (object)__s, short __s => (object)__s, ushort __s => (object)__s, int __s => (object)__s, uint __s => (object)__s, long __s => (object)__s, ulong __s => (object)__s, _ => (object)args[0] }), (global::System.Type)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -65,7 +65,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Type), { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.EnumTypes(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (global::System.Type)args[1], (global::System.Type)args[2])); + return new global::System.Threading.Tasks.ValueTask(instance.EnumTypes((args[0] switch { global::TUnit.TestProject.ByteEnum __s => (object)__s, global::TUnit.TestProject.SByteEnum __s => (object)__s, global::TUnit.TestProject.Int16Enum __s => (object)__s, global::TUnit.TestProject.UInt16Enum __s => (object)__s, global::TUnit.TestProject.Int32Enum __s => (object)__s, global::TUnit.TestProject.UInt32Enum __s => (object)__s, global::TUnit.TestProject.Int64Enum __s => (object)__s, global::TUnit.TestProject.UInt64Enum __s => (object)__s, _ => (object)args[0] }), (global::System.Type)args[1], (global::System.Type)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt index 465569e292..c1446d5a19 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt @@ -39,7 +39,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long), "value", new gl { case 1: { - instance.MyTest(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.MyTest((args[0] switch { int __s => (long)__s, byte __s => (long)__s, short __s => (long)__s, char __s => (long)__s, long __s => (long)__s, _ => (long)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index b09326c7e2..1581ac6d2d 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using TUnit.Core.SourceGenerator.Extensions; @@ -11,6 +12,86 @@ internal static class CastExpressionHelper return sourceTypes != null && index < sourceTypes.Length ? sourceTypes[index] : null; } + /// + /// Dispatches to the appropriate cast generation strategy based on the source type info at the given position. + /// - Single type: uses GenerateCast (existing single-cast path) + /// - Multiple types: uses GenerateMultiSourceCast (switch expression) + /// - Unknown/null: falls back to CastHelper.Cast + /// + public static string GenerateCastForPosition( + SourceTypeInfo? sourceTypeInfo, + int position, + ITypeSymbol targetType, + string argsExpression, + CSharpCompilation? compilation) + { + if (sourceTypeInfo == null) + { + return GenerateCast(null, targetType, argsExpression, compilation); + } + + if (sourceTypeInfo.HasSingleType(position)) + { + return GenerateCast(sourceTypeInfo.GetSingleType(position), targetType, argsExpression, compilation); + } + + if (sourceTypeInfo.HasMultipleTypes(position)) + { + return GenerateMultiSourceCast(sourceTypeInfo.GetTypes(position)!, targetType, argsExpression, compilation); + } + + // Unknown type at this position + return GenerateCast(null, targetType, argsExpression, compilation); + } + + /// + /// Generates a pattern-matching switch expression for positions with multiple known source types. + /// Each source type gets its own pattern match arm with the appropriate cast. + /// The default arm handles any unexpected types via direct cast. + /// + public static string GenerateMultiSourceCast( + IReadOnlyList sourceTypes, + ITypeSymbol targetType, + string argsExpression, + CSharpCompilation? compilation) + { + var targetGQ = targetType.GloballyQualified(); + var arms = new List(); + + foreach (var sourceType in sourceTypes) + { + var sourceGQ = sourceType.GloballyQualified(); + + if (SymbolEqualityComparer.Default.Equals(sourceType, targetType)) + { + // Same type — simple cast (unbox) + arms.Add($"{sourceGQ} __s => ({targetGQ})__s"); + } + else if (compilation != null) + { + var conversion = compilation.ClassifyConversion(sourceType, targetType); + + if (conversion.IsImplicit || conversion.IsExplicit) + { + arms.Add($"{sourceGQ} __s => ({targetGQ})__s"); + } + else + { + arms.Add($"{sourceGQ} __s => global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>(__s)"); + } + } + else + { + arms.Add($"{sourceGQ} __s => global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>(__s)"); + } + } + + // Default arm handles unboxing/direct cast from Arguments or unexpected types + arms.Add($"_ => ({targetGQ}){argsExpression}"); + + return $"({argsExpression} switch {{ {string.Join(", ", arms)} }})"; + } + /// /// Generates a typed cast expression, or falls back to CastHelper.Cast when source type is unknown. /// diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs index 2af1b5b330..e0de514f6b 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs @@ -289,7 +289,7 @@ private static void GenerateTypedConstructorCall(CodeWriter writer, string class writer.AppendLine("},"); } - private static void GenerateTypedConstructorCallBody(CodeWriter writer, string className, IMethodSymbol constructor, ITypeSymbol?[]? sourceTypes = null, CSharpCompilation? compilation = null) + private static void GenerateTypedConstructorCallBody(CodeWriter writer, string className, IMethodSymbol constructor, SourceTypeInfo? sourceTypeInfo = null, CSharpCompilation? compilation = null) { // Check for required properties var requiredProperties = RequiredPropertyHelper.GetAllRequiredProperties(constructor.ContainingType); @@ -315,8 +315,8 @@ private static void GenerateTypedConstructorCallBody(CodeWriter writer, string c var parameterType = parameterTypes[i]; var argAccess = $"args[{i}]"; - writer.Append(CastExpressionHelper.GenerateCast( - CastExpressionHelper.GetSourceTypeAt(sourceTypes, i), parameterType, argAccess, compilation)); + writer.Append(CastExpressionHelper.GenerateCastForPosition( + sourceTypeInfo, i, parameterType, argAccess, compilation)); } writer.Append(")"); diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs index dda3b641c8..8f774fa3e9 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -3,18 +3,88 @@ namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; +/// +/// Holds per-position source type information for data source attributes. +/// Each position may have zero, one, or multiple possible source types. +/// A null entry at a position means the type is unknown (falls back to CastHelper.Cast). +/// +public sealed class SourceTypeInfo +{ + private readonly IReadOnlyList?[] _typesPerPosition; + + public SourceTypeInfo(IReadOnlyList?[] typesPerPosition) + { + _typesPerPosition = typesPerPosition; + } + + public int Length => _typesPerPosition.Length; + + /// + /// Returns true if the position has exactly one known source type. + /// + public bool HasSingleType(int index) + { + if (index < 0 || index >= _typesPerPosition.Length) + { + return false; + } + + var types = _typesPerPosition[index]; + return types != null && types.Count == 1; + } + + /// + /// Returns true if the position has more than one known source type. + /// + public bool HasMultipleTypes(int index) + { + if (index < 0 || index >= _typesPerPosition.Length) + { + return false; + } + + var types = _typesPerPosition[index]; + return types != null && types.Count > 1; + } + + /// + /// Returns the single source type at the given position, or null if unknown or multiple. + /// + public ITypeSymbol? GetSingleType(int index) + { + if (index < 0 || index >= _typesPerPosition.Length) + { + return null; + } + + var types = _typesPerPosition[index]; + return types != null && types.Count == 1 ? types[0] : null; + } + + /// + /// Returns all known source types at the given position, or null if unknown. + /// + public IReadOnlyList? GetTypes(int index) + { + if (index < 0 || index >= _typesPerPosition.Length) + { + return null; + } + + return _typesPerPosition[index]; + } +} + internal static class SourceTypeAnalyzer { /// /// Analyzes data source attributes on a method to determine the compile-time source type - /// for each parameter position. Returns null if source types cannot be determined - /// (e.g., dynamic data sources exist). A null element in the returned array means - /// that position's source type is unknown (falls back to CastHelper.Cast). + /// for each parameter position. Returns null if no data sources exist. + /// A null entry at a position means that position's source type is unknown (falls back to CastHelper.Cast). /// - public static ITypeSymbol?[]? GetMethodParameterSourceTypes(IMethodSymbol method) + public static SourceTypeInfo? GetMethodParameterSourceTypes(IMethodSymbol method) { var dataSources = new List(); - var argumentsAttributes = new List(); foreach (var attr in method.GetAttributes()) { @@ -24,27 +94,14 @@ internal static class SourceTypeAnalyzer } dataSources.Add(attr); - - if (IsArgumentsAttribute(attr)) - { - argumentsAttributes.Add(attr); - } } - // No data sources at all → no source type info + // No data sources at all -> no source type info if (dataSources.Count == 0) { return null; } - // The invoke lambda is shared across all data sources for this method. - // We can't emit per-source-type cast code, so if any source is dynamic - // we fall back to CastHelper.Cast for the entire method. - if (argumentsAttributes.Count != dataSources.Count) - { - return null; - } - // Filter to non-CancellationToken parameters var parameterCount = 0; var hasParams = false; @@ -65,17 +122,17 @@ internal static class SourceTypeAnalyzer return null; } - // For params methods, size the array to max argument count across all [Arguments] - // rows so that params elements beyond position 0 also get typed casts. + // For params methods with [Arguments], size the array to max argument count + var argumentsAttributes = dataSources.Where(IsArgumentsAttribute).ToList(); var arraySize = hasParams ? GetMaxArgumentCount(argumentsAttributes, parameterCount) : parameterCount; - return ExtractSourceTypesFromArguments(argumentsAttributes, arraySize); + return ExtractSourceTypesFromAllAttributes(dataSources, arraySize); } /// /// Analyzes class-level data source attributes to determine source types for constructor parameters. /// - public static ITypeSymbol?[]? GetConstructorParameterSourceTypes(INamedTypeSymbol? classType) + public static SourceTypeInfo? GetConstructorParameterSourceTypes(INamedTypeSymbol? classType) { if (classType == null) { @@ -83,7 +140,6 @@ internal static class SourceTypeAnalyzer } var dataSources = new List(); - var argumentsAttributes = new List(); foreach (var attr in classType.GetAttributes()) { @@ -93,14 +149,9 @@ internal static class SourceTypeAnalyzer } dataSources.Add(attr); - - if (IsArgumentsAttribute(attr)) - { - argumentsAttributes.Add(attr); - } } - if (dataSources.Count == 0 || argumentsAttributes.Count != dataSources.Count) + if (dataSources.Count == 0) { return null; } @@ -116,80 +167,213 @@ internal static class SourceTypeAnalyzer var ctorParamCount = constructor.Parameters.Length; var ctorHasParams = constructor.Parameters[ctorParamCount - 1].IsParams; + var argumentsAttributes = dataSources.Where(IsArgumentsAttribute).ToList(); var ctorArraySize = ctorHasParams ? GetMaxArgumentCount(argumentsAttributes, ctorParamCount) : ctorParamCount; - return ExtractSourceTypesFromArguments(argumentsAttributes, ctorArraySize); + return ExtractSourceTypesFromAllAttributes(dataSources, ctorArraySize); } - private static int GetMaxArgumentCount(List argumentsAttributes, int baseCount) + /// + /// Processes ALL data source attributes and merges per-position source types. + /// + private static SourceTypeInfo ExtractSourceTypesFromAllAttributes( + List dataSources, + int parameterCount) { - foreach (var attr in argumentsAttributes) + // Each position accumulates a set of possible source types. + // null = unknown (some attribute couldn't determine the type for this position). + var positionTypes = new List?[parameterCount]; + var positionUnknown = new bool[parameterCount]; + + foreach (var attr in dataSources) { - var values = GetArgumentValues(attr); - if (values != null && values.Value.Length > baseCount) + if (IsArgumentsAttribute(attr)) { - baseCount = values.Value.Length; + ProcessArgumentsAttribute(attr, positionTypes, positionUnknown, parameterCount); + } + else if (attr.AttributeClass != null && DataSourceAttributeHelper.IsTypedDataSourceAttribute(attr.AttributeClass)) + { + ProcessTypedDataSource(attr, positionTypes, positionUnknown, parameterCount); + } + else + { + // Unknown data source type (e.g., MethodDataSource, non-generic ClassDataSource + // without typeof args) -> mark ALL positions as unknown + for (var i = 0; i < parameterCount; i++) + { + positionUnknown[i] = true; + } } } - return baseCount; + // Build the final result: null entries where unknown + var result = new IReadOnlyList?[parameterCount]; + for (var i = 0; i < parameterCount; i++) + { + if (positionUnknown[i] || positionTypes[i] == null || positionTypes[i]!.Count == 0) + { + result[i] = null; + } + else + { + result[i] = positionTypes[i]; + } + } + + return new SourceTypeInfo(result); } - private static ITypeSymbol?[]? ExtractSourceTypesFromArguments( - List argumentsAttributes, + private static void ProcessArgumentsAttribute( + AttributeData attr, + List?[] positionTypes, + bool[] positionUnknown, int parameterCount) { - var sourceTypes = new ITypeSymbol?[parameterCount]; - var consistent = new bool[parameterCount]; - consistent.AsSpan().Fill(true); + var values = GetArgumentValues(attr); - foreach (var attr in argumentsAttributes) + if (values == null) { - var values = GetArgumentValues(attr); + // Can't extract values from this attribute -> mark all positions as unknown + for (var i = 0; i < parameterCount; i++) + { + positionUnknown[i] = true; + } - if (values == null) + return; + } + + for (var i = 0; i < parameterCount && i < values.Value.Length; i++) + { + if (positionUnknown[i]) { - // Can't extract values from this attribute — fall back entirely - return null; + continue; } - for (var i = 0; i < parameterCount && i < values.Value.Length; i++) + var tc = values.Value[i]; + + // Null literal or error -> unknown source type for this position + if (tc.IsNull || tc.Kind == TypedConstantKind.Error || tc.Type == null) { - if (!consistent[i]) - { - continue; - } + positionUnknown[i] = true; + continue; + } - var tc = values.Value[i]; + AddTypeToPosition(positionTypes, i, tc.Type); + } + } + + /// + /// Processes any attribute implementing ITypedDataSourceAttribute<T>. + /// For single-generic attributes (e.g., ClassDataSourceAttribute<T>), T maps to position 0. + /// For multi-generic attributes, each type argument maps to its corresponding position. + /// Works with ClassDataSourceAttribute<T> and any user-created custom data source + /// that extends TypedDataSourceAttribute<T>. + /// + private static void ProcessTypedDataSource( + AttributeData attr, + List?[] positionTypes, + bool[] positionUnknown, + int parameterCount) + { + var attrClass = attr.AttributeClass; + + if (attrClass == null) + { + return; + } + + if (attrClass.IsGenericType) + { + var typeArgs = attrClass.TypeArguments; - // Null literal or error → unknown source type for this position - if (tc.IsNull || tc.Kind == TypedConstantKind.Error || tc.Type == null) + if (typeArgs.Length == 1) + { + // Single generic: e.g., ClassDataSourceAttribute -> T maps to position 0 + var sourceType = DataSourceAttributeHelper.GetTypedDataSourceType(attrClass); + + if (sourceType != null && parameterCount > 0 && !positionUnknown[0]) { - consistent[i] = false; - continue; + AddTypeToPosition(positionTypes, 0, sourceType); } - - if (sourceTypes[i] == null) + } + else + { + // Multi-generic: e.g., ClassDataSourceAttribute -> each Tn maps to position n + for (var i = 0; i < parameterCount && i < typeArgs.Length; i++) { - sourceTypes[i] = tc.Type; + if (positionUnknown[i]) + { + continue; + } + + var typeArg = typeArgs[i]; + + if (typeArg is IErrorTypeSymbol) + { + positionUnknown[i] = true; + } + else + { + AddTypeToPosition(positionTypes, i, typeArg); + } } - else if (!SymbolEqualityComparer.Default.Equals(sourceTypes[i], tc.Type)) + } + } + else + { + // Non-generic typed data source: try to extract T from ITypedDataSourceAttribute + var sourceType = DataSourceAttributeHelper.GetTypedDataSourceType(attrClass); + + if (sourceType != null && parameterCount > 0 && !positionUnknown[0]) + { + AddTypeToPosition(positionTypes, 0, sourceType); + } + else + { + // Can't determine source type statically -> mark all positions as unknown + for (var i = 0; i < parameterCount; i++) { - consistent[i] = false; + positionUnknown[i] = true; } } } + } - // Null out inconsistent positions - for (var i = 0; i < parameterCount; i++) + /// + /// Adds a source type to the given position, deduplicating with SymbolEqualityComparer. + /// + private static void AddTypeToPosition(List?[] positionTypes, int index, ITypeSymbol type) + { + if (positionTypes[index] == null) { - if (!consistent[i]) + positionTypes[index] = new List { type }; + return; + } + + // Deduplicate + foreach (var existing in positionTypes[index]!) + { + if (SymbolEqualityComparer.Default.Equals(existing, type)) { - sourceTypes[i] = null; + return; } } - return sourceTypes; + positionTypes[index]!.Add(type); + } + + private static int GetMaxArgumentCount(List argumentsAttributes, int baseCount) + { + foreach (var attr in argumentsAttributes) + { + var values = GetArgumentValues(attr); + if (values != null && values.Value.Length > baseCount) + { + baseCount = values.Value.Length; + } + } + + return baseCount; } /// diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs index 622f1c960f..9ea294549a 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs @@ -9,7 +9,7 @@ public static class TupleArgumentHelper public static string GenerateMethodInvocationArguments( IList parameters, string argumentsArrayName, - ITypeSymbol?[]? sourceTypes = null, + SourceTypeInfo? sourceTypeInfo = null, CSharpCompilation? compilation = null) { var allArguments = new List(); @@ -17,8 +17,7 @@ public static string GenerateMethodInvocationArguments( for (var i = 0; i < parameters.Count; i++) { var parameter = parameters[i]; - var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); - var castExpression = CastExpressionHelper.GenerateCast(sourceType, parameter.Type, $"{argumentsArrayName}[{i}]", compilation); + var castExpression = CastExpressionHelper.GenerateCastForPosition(sourceTypeInfo, i, parameter.Type, $"{argumentsArrayName}[{i}]", compilation); allArguments.Add(castExpression); } @@ -29,7 +28,7 @@ public static List GenerateArgumentAccessWithParams( IList parameters, string argumentsArrayName, object argumentCount, - ITypeSymbol?[]? sourceTypes = null, + SourceTypeInfo? sourceTypeInfo = null, CSharpCompilation? compilation = null) { var argumentExpressions = new List(); @@ -61,8 +60,7 @@ public static List GenerateArgumentAccessWithParams( for (var i = 0; i < upperBound; i++) { var param = parameters[i]; - var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); - var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); + var castExpression = CastExpressionHelper.GenerateCastForPosition(sourceTypeInfo, i, param.Type, $"{argumentsArrayName}[{i}]", compilation); argumentExpressions.Add(castExpression); } } @@ -76,8 +74,7 @@ public static List GenerateArgumentAccessWithParams( for (var i = 0; i < upperBound; i++) { var param = parameters[i]; - var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); - var castExpression = CastExpressionHelper.GenerateCast(sourceType, param.Type, $"{argumentsArrayName}[{i}]", compilation); + var castExpression = CastExpressionHelper.GenerateCastForPosition(sourceTypeInfo, i, param.Type, $"{argumentsArrayName}[{i}]", compilation); argumentExpressions.Add(castExpression); } @@ -112,7 +109,7 @@ public static List GenerateArgumentAccessWithParams( // Single argument for params - check if it's null or already the correct array type var singleArg = $"{argumentsArrayName}[{regularParamCount}]"; var paramsTypeGQ = paramsParam.Type.GloballyQualified(); - var elementCast = GenerateElementCast(elementType, regularParamCount, singleArg, sourceTypes, compilation); + var elementCast = GenerateElementCast(elementType, regularParamCount, singleArg, sourceTypeInfo, compilation); var checkAndCast = $"({singleArg} is null ? null : {singleArg} is {paramsTypeGQ} arr ? arr : new {elementTargetGQ}[] {{ {elementCast} }})"; argumentExpressions.Add(checkAndCast); } @@ -122,7 +119,7 @@ public static List GenerateArgumentAccessWithParams( var arrayElements = new List(); for (var i = regularParamCount; i < argCount; i++) { - var elementCast = GenerateElementCast(elementType, i, $"{argumentsArrayName}[{i}]", sourceTypes, compilation); + var elementCast = GenerateElementCast(elementType, i, $"{argumentsArrayName}[{i}]", sourceTypeInfo, compilation); arrayElements.Add(elementCast); } argumentExpressions.Add($"new {elementTargetGQ}[] {{ {string.Join(", ", arrayElements)} }}"); @@ -132,8 +129,7 @@ public static List GenerateArgumentAccessWithParams( else { // Fallback if we can't determine element type - var sourceType = sourceTypes != null && regularParamCount < sourceTypes.Length ? sourceTypes[regularParamCount] : null; - var castExpression = CastExpressionHelper.GenerateCast(sourceType, paramsParam.Type, $"{argumentsArrayName}[{regularParamCount}]", compilation); + var castExpression = CastExpressionHelper.GenerateCastForPosition(sourceTypeInfo, regularParamCount, paramsParam.Type, $"{argumentsArrayName}[{regularParamCount}]", compilation); argumentExpressions.Add(castExpression); } } @@ -165,14 +161,24 @@ private static string GenerateElementCast( ITypeSymbol elementType, int argIndex, string argExpression, - ITypeSymbol?[]? sourceTypes, + SourceTypeInfo? sourceTypeInfo, CSharpCompilation? compilation) { // Default to elementType when source type is unknown — for params overflow positions // (beyond [Arguments] row length), the element type is statically known and a direct // cast is sufficient. These positions are only reachable when all data sources are // [Arguments], which SourceTypeAnalyzer has already verified. - var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, argIndex) ?? elementType; - return CastExpressionHelper.GenerateCast(sourceType, elementType, argExpression, compilation); + if (sourceTypeInfo != null && sourceTypeInfo.HasSingleType(argIndex)) + { + return CastExpressionHelper.GenerateCast(sourceTypeInfo.GetSingleType(argIndex), elementType, argExpression, compilation); + } + + if (sourceTypeInfo != null && sourceTypeInfo.HasMultipleTypes(argIndex)) + { + return CastExpressionHelper.GenerateMultiSourceCast(sourceTypeInfo.GetTypes(argIndex)!, elementType, argExpression, compilation); + } + + // Unknown source type at this position — default to elementType for a direct cast + return CastExpressionHelper.GenerateCast(elementType, elementType, argExpression, compilation); } } diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index 45def9904b..8dc27734c1 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -2548,18 +2548,18 @@ private static void GenerateTypedInvokers(CodeWriter writer, TestMethodMetadata } } - private static void GenerateConcreteTestInvoker(CodeWriter writer, string methodName, TestReturnPattern returnPattern, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs, ITypeSymbol?[]? sourceTypes = null, CSharpCompilation? compilation = null) + private static void GenerateConcreteTestInvoker(CodeWriter writer, string methodName, TestReturnPattern returnPattern, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs, SourceTypeInfo? sourceTypeInfo = null, CSharpCompilation? compilation = null) { // Generate InvokeTypedTest which is required by CreateExecutableTestFactory writer.AppendLine("InvokeTypedTest = static (instance, args, cancellationToken) =>"); writer.AppendLine("{"); writer.Indent(); - GenerateConcreteTestInvokerBody(writer, methodName, returnPattern, hasCancellationToken, parametersFromArgs, sourceTypes, compilation); + GenerateConcreteTestInvokerBody(writer, methodName, returnPattern, hasCancellationToken, parametersFromArgs, sourceTypeInfo, compilation); writer.Unindent(); writer.AppendLine("},"); } - private static void GenerateConcreteTestInvokerBody(CodeWriter writer, string methodName, TestReturnPattern returnPattern, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs, ITypeSymbol?[]? sourceTypes = null, CSharpCompilation? compilation = null) + private static void GenerateConcreteTestInvokerBody(CodeWriter writer, string methodName, TestReturnPattern returnPattern, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs, SourceTypeInfo? sourceTypeInfo = null, CSharpCompilation? compilation = null) { // Wrap entire body in try-catch to handle synchronous exceptions writer.AppendLine("try"); @@ -2589,8 +2589,7 @@ private static void GenerateConcreteTestInvokerBody(CodeWriter writer, string me // Build tuple reconstruction with proper casting var tupleElements = singleTupleParam.TupleElements.Select((elem, i) => { - var sourceType = CastExpressionHelper.GetSourceTypeAt(sourceTypes, i); - return CastExpressionHelper.GenerateCast(sourceType, elem.Type, $"args[{i}]", compilation); + return CastExpressionHelper.GenerateCastForPosition(sourceTypeInfo, i, elem.Type, $"args[{i}]", compilation); }); var tupleConstruction = $"({string.Join(", ", tupleElements)})"; @@ -2652,7 +2651,7 @@ private static void GenerateConcreteTestInvokerBody(CodeWriter writer, string me writer.Indent(); // Build the arguments to pass, handling params arrays correctly - var argsToPass = TupleArgumentHelper.GenerateArgumentAccessWithParams(parametersFromArgs, "args", argCount, sourceTypes, compilation); + var argsToPass = TupleArgumentHelper.GenerateArgumentAccessWithParams(parametersFromArgs, "args", argCount, sourceTypeInfo, compilation); // Add CancellationToken if present if (hasCancellationToken) diff --git a/TUnit.TestProject/ArgumentsWithClassDataSourceTests.cs b/TUnit.TestProject/ArgumentsWithClassDataSourceTests.cs index 866e5bb948..4ca63af49f 100644 --- a/TUnit.TestProject/ArgumentsWithClassDataSourceTests.cs +++ b/TUnit.TestProject/ArgumentsWithClassDataSourceTests.cs @@ -6,8 +6,8 @@ namespace TUnit.TestProject; [EngineTest(ExpectedResult.Pass)] [Arguments(1)] [Arguments(2)] -[ClassDataSource(typeof(IntDataSource1))] -[ClassDataSource(typeof(IntDataSource2))] +[ClassDataSource] +[ClassDataSource] public class ArgumentsWithClassDataSourceTests(int classArg) { private static readonly ConcurrentBag ExecutedTests = From da8748f361fc7edd231da9fd3346a214c3c08e5a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:16:07 +0100 Subject: [PATCH 20/30] fix: update remaining multi-target snapshot variants for enum switch cast The DotNet8_0, DotNet9_0, and Net4_7 variants of the ConflictingNamespaceTests snapshot were not updated with the new switch expression pattern for enum casts, only DotNet10_0 was. --- ...taDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt | 2 +- ...taDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt | 2 +- ....DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt index 59a9aa9517..eaaf3291db 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt index 59a9aa9517..eaaf3291db 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt index 59a9aa9517..eaaf3291db 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: From 7855760952bbe2673235d5f7f0aa23a303adb5a5 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:24:08 +0100 Subject: [PATCH 21/30] refactor: simplify after code review - Delete dead GetSourceTypeAt method - Collapse SourceTypeInfo to single GetTypes() method, removing HasSingleType/HasMultipleTypes/GetSingleType (callers use count check) - Change SourceTypeInfo from public to internal sealed - Change TupleArgumentHelper from public to internal - Eliminate GenerateElementCast dispatch duplication by adding fallbackSourceType parameter to GenerateCastForPosition - Remove wasteful .Where(IsArgumentsAttribute).ToList() allocation; GetMaxArgumentCount now filters inline - Simplify GenerateMultiSourceCast branch structure --- .../Helpers/CastExpressionHelper.cs | 49 +++++-------- .../Helpers/SourceTypeAnalyzer.cs | 68 ++++--------------- .../Helpers/TupleArgumentHelper.cs | 23 ++----- 3 files changed, 35 insertions(+), 105 deletions(-) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index 1581ac6d2d..47cf9e3828 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -7,41 +7,36 @@ namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; internal static class CastExpressionHelper { - public static ITypeSymbol? GetSourceTypeAt(ITypeSymbol?[]? sourceTypes, int index) - { - return sourceTypes != null && index < sourceTypes.Length ? sourceTypes[index] : null; - } - /// /// Dispatches to the appropriate cast generation strategy based on the source type info at the given position. - /// - Single type: uses GenerateCast (existing single-cast path) - /// - Multiple types: uses GenerateMultiSourceCast (switch expression) - /// - Unknown/null: falls back to CastHelper.Cast + /// When is provided, it is used as the source type for unknown positions + /// instead of falling back to CastHelper.Cast (useful for params elements where the element type is statically known). /// public static string GenerateCastForPosition( SourceTypeInfo? sourceTypeInfo, int position, ITypeSymbol targetType, string argsExpression, - CSharpCompilation? compilation) + CSharpCompilation? compilation, + ITypeSymbol? fallbackSourceType = null) { - if (sourceTypeInfo == null) + if (sourceTypeInfo != null) { - return GenerateCast(null, targetType, argsExpression, compilation); - } + var types = sourceTypeInfo.GetTypes(position); - if (sourceTypeInfo.HasSingleType(position)) - { - return GenerateCast(sourceTypeInfo.GetSingleType(position), targetType, argsExpression, compilation); - } + if (types != null) + { + if (types.Count == 1) + { + return GenerateCast(types[0], targetType, argsExpression, compilation); + } - if (sourceTypeInfo.HasMultipleTypes(position)) - { - return GenerateMultiSourceCast(sourceTypeInfo.GetTypes(position)!, targetType, argsExpression, compilation); + return GenerateMultiSourceCast(types, targetType, argsExpression, compilation); + } } // Unknown type at this position - return GenerateCast(null, targetType, argsExpression, compilation); + return GenerateCast(fallbackSourceType, targetType, argsExpression, compilation); } /// @@ -64,21 +59,11 @@ public static string GenerateMultiSourceCast( if (SymbolEqualityComparer.Default.Equals(sourceType, targetType)) { - // Same type — simple cast (unbox) arms.Add($"{sourceGQ} __s => ({targetGQ})__s"); } - else if (compilation != null) + else if (compilation != null && compilation.ClassifyConversion(sourceType, targetType) is { IsImplicit: true } or { IsExplicit: true }) { - var conversion = compilation.ClassifyConversion(sourceType, targetType); - - if (conversion.IsImplicit || conversion.IsExplicit) - { - arms.Add($"{sourceGQ} __s => ({targetGQ})__s"); - } - else - { - arms.Add($"{sourceGQ} __s => global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>(__s)"); - } + arms.Add($"{sourceGQ} __s => ({targetGQ})__s"); } else { diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs index 8f774fa3e9..c403991e08 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -8,7 +8,7 @@ namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; /// Each position may have zero, one, or multiple possible source types. /// A null entry at a position means the type is unknown (falls back to CastHelper.Cast). /// -public sealed class SourceTypeInfo +internal sealed class SourceTypeInfo { private readonly IReadOnlyList?[] _typesPerPosition; @@ -20,58 +20,11 @@ public SourceTypeInfo(IReadOnlyList?[] typesPerPosition) public int Length => _typesPerPosition.Length; /// - /// Returns true if the position has exactly one known source type. - /// - public bool HasSingleType(int index) - { - if (index < 0 || index >= _typesPerPosition.Length) - { - return false; - } - - var types = _typesPerPosition[index]; - return types != null && types.Count == 1; - } - - /// - /// Returns true if the position has more than one known source type. - /// - public bool HasMultipleTypes(int index) - { - if (index < 0 || index >= _typesPerPosition.Length) - { - return false; - } - - var types = _typesPerPosition[index]; - return types != null && types.Count > 1; - } - - /// - /// Returns the single source type at the given position, or null if unknown or multiple. - /// - public ITypeSymbol? GetSingleType(int index) - { - if (index < 0 || index >= _typesPerPosition.Length) - { - return null; - } - - var types = _typesPerPosition[index]; - return types != null && types.Count == 1 ? types[0] : null; - } - - /// - /// Returns all known source types at the given position, or null if unknown. + /// Returns all known source types at the given position, or null if unknown/out-of-range. /// public IReadOnlyList? GetTypes(int index) { - if (index < 0 || index >= _typesPerPosition.Length) - { - return null; - } - - return _typesPerPosition[index]; + return (uint)index < (uint)_typesPerPosition.Length ? _typesPerPosition[index] : null; } } @@ -123,8 +76,7 @@ internal static class SourceTypeAnalyzer } // For params methods with [Arguments], size the array to max argument count - var argumentsAttributes = dataSources.Where(IsArgumentsAttribute).ToList(); - var arraySize = hasParams ? GetMaxArgumentCount(argumentsAttributes, parameterCount) : parameterCount; + var arraySize = hasParams ? GetMaxArgumentCount(dataSources, parameterCount) : parameterCount; return ExtractSourceTypesFromAllAttributes(dataSources, arraySize); } @@ -167,8 +119,7 @@ internal static class SourceTypeAnalyzer var ctorParamCount = constructor.Parameters.Length; var ctorHasParams = constructor.Parameters[ctorParamCount - 1].IsParams; - var argumentsAttributes = dataSources.Where(IsArgumentsAttribute).ToList(); - var ctorArraySize = ctorHasParams ? GetMaxArgumentCount(argumentsAttributes, ctorParamCount) : ctorParamCount; + var ctorArraySize = ctorHasParams ? GetMaxArgumentCount(dataSources, ctorParamCount) : ctorParamCount; return ExtractSourceTypesFromAllAttributes(dataSources, ctorArraySize); } @@ -362,10 +313,15 @@ private static void AddTypeToPosition(List?[] positionTypes, int in positionTypes[index]!.Add(type); } - private static int GetMaxArgumentCount(List argumentsAttributes, int baseCount) + private static int GetMaxArgumentCount(List dataSources, int baseCount) { - foreach (var attr in argumentsAttributes) + foreach (var attr in dataSources) { + if (!IsArgumentsAttribute(attr)) + { + continue; + } + var values = GetArgumentValues(attr); if (values != null && values.Value.Length > baseCount) { diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs index 9ea294549a..f2a0f166ba 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs @@ -4,7 +4,7 @@ namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; -public static class TupleArgumentHelper +internal static class TupleArgumentHelper { public static string GenerateMethodInvocationArguments( IList parameters, @@ -164,21 +164,10 @@ private static string GenerateElementCast( SourceTypeInfo? sourceTypeInfo, CSharpCompilation? compilation) { - // Default to elementType when source type is unknown — for params overflow positions - // (beyond [Arguments] row length), the element type is statically known and a direct - // cast is sufficient. These positions are only reachable when all data sources are - // [Arguments], which SourceTypeAnalyzer has already verified. - if (sourceTypeInfo != null && sourceTypeInfo.HasSingleType(argIndex)) - { - return CastExpressionHelper.GenerateCast(sourceTypeInfo.GetSingleType(argIndex), elementType, argExpression, compilation); - } - - if (sourceTypeInfo != null && sourceTypeInfo.HasMultipleTypes(argIndex)) - { - return CastExpressionHelper.GenerateMultiSourceCast(sourceTypeInfo.GetTypes(argIndex)!, elementType, argExpression, compilation); - } - - // Unknown source type at this position — default to elementType for a direct cast - return CastExpressionHelper.GenerateCast(elementType, elementType, argExpression, compilation); + // For params overflow positions, the element type is statically known — pass it as + // fallbackSourceType so unknown positions get a direct cast instead of CastHelper.Cast. + return CastExpressionHelper.GenerateCastForPosition( + sourceTypeInfo, argIndex, elementType, argExpression, compilation, + fallbackSourceType: elementType); } } From eded9977bbde0a41d708d1c03ded5b3e07d2de22 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:28:53 +0100 Subject: [PATCH 22/30] fix: convert non-generic ClassDataSource to generic form for AOT safety Non-generic [ClassDataSource(typeof(T))] doesn't expose the source type to SourceTypeAnalyzer, causing fallback to CastHelper.Cast() which can't discover implicit conversion operators in AOT mode (the deleted AotConverterGenerator previously auto-registered these). Convert all 7 affected test files to generic [ClassDataSource] so the source type is statically known and the generator can emit typed casts. Also delete orphaned AotConverterGeneratorTests.GeneratesCode.verified.txt (its test class was renamed to FullProjectGenerationTests). --- ...rGeneratorTests.GeneratesCode.verified.txt | 1376 ----------------- .../AllDataSourcesCombinedTests.cs | 6 +- ...AllDataSourcesCombinedTestsVerification.cs | 6 +- .../ClassDataSourceEnumerableTest.cs | 2 +- ...lassDataSourceWithMethodDataSourceTests.cs | 6 +- TUnit.TestProject/ComprehensiveCountTest.cs | 2 +- TUnit.TestProject/MixedDataSourceBugTest.cs | 4 +- .../TestCountVerificationTests.cs | 2 +- 8 files changed, 14 insertions(+), 1390 deletions(-) delete mode 100644 TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.GeneratesCode.verified.txt diff --git a/TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.GeneratesCode.verified.txt b/TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.GeneratesCode.verified.txt deleted file mode 100644 index 16ab7c3f35..0000000000 --- a/TUnit.Core.SourceGenerator.Tests/AotConverterGeneratorTests.GeneratesCode.verified.txt +++ /dev/null @@ -1,1376 +0,0 @@ -// -#pragma warning disable - -#nullable enable -using System; -using TUnit.Core.Converters; -namespace TUnit.Generated; -internal sealed class AotConverter_0 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.AllDataSourcesCombinedTests.DataSource1); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.AllDataSourcesCombinedTests.DataSource1 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_1 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.AllDataSourcesCombinedTests.DataSource2); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.AllDataSourcesCombinedTests.DataSource2 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_2 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.AllDataSourcesCombinedTests.DataSource3); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.AllDataSourcesCombinedTests.DataSource3 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_3 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.AllDataSourcesCombinedTestsVerification.DataSource1); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.AllDataSourcesCombinedTestsVerification.DataSource1 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_4 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.AllDataSourcesCombinedTestsVerification.DataSource2); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.AllDataSourcesCombinedTestsVerification.DataSource2 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_5 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.AllDataSourcesCombinedTestsVerification.DataSource3); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.AllDataSourcesCombinedTestsVerification.DataSource3 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_6 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.ArgumentsWithClassDataSourceTests.IntDataSource1); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.ArgumentsWithClassDataSourceTests.IntDataSource1 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_7 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.ArgumentsWithClassDataSourceTests.IntDataSource2); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.ArgumentsWithClassDataSourceTests.IntDataSource2 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_8 : IAotConverter -{ - public Type SourceType => typeof(int); - public Type TargetType => typeof(global::TUnit.TestProject.ExplicitInteger); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.ExplicitInteger targetTypedValue) - { - return targetTypedValue; - } - if (value is int sourceTypedValue) - { - global::TUnit.TestProject.ExplicitInteger converted = (global::TUnit.TestProject.ExplicitInteger)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_9 : IAotConverter -{ - public Type SourceType => typeof(int); - public Type TargetType => typeof(global::TUnit.TestProject.ImplicitInteger); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.ImplicitInteger targetTypedValue) - { - return targetTypedValue; - } - if (value is int sourceTypedValue) - { - global::TUnit.TestProject.ImplicitInteger converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_10 : IAotConverter -{ - public Type SourceType => typeof(byte); - public Type TargetType => typeof(byte?); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is byte targetTypedValue) - { - return targetTypedValue; - } - if (value is byte sourceTypedValue) - { - byte? converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_11 : IAotConverter -{ - public Type SourceType => typeof(byte?); - public Type TargetType => typeof(byte); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is byte targetTypedValue) - { - return targetTypedValue; - } - if (value is byte sourceTypedValue) - { - byte converted = (byte)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_12 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.ClassDataSourceEnumerableTest.EnumerableDataSource); - public Type TargetType => typeof(string); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is string targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.ClassDataSourceEnumerableTest.EnumerableDataSource sourceTypedValue) - { - string converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_13 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.ClassDataSourceWithMethodDataSourceTests.DataSource1); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.ClassDataSourceWithMethodDataSourceTests.DataSource1 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_14 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.ClassDataSourceWithMethodDataSourceTests.DataSource2); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.ClassDataSourceWithMethodDataSourceTests.DataSource2 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_15 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.ClassDataSourceWithMethodDataSourceTests.DataSource3); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.ClassDataSourceWithMethodDataSourceTests.DataSource3 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_16 : IAotConverter -{ - public Type SourceType => typeof(int); - public Type TargetType => typeof(int?); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is int sourceTypedValue) - { - int? converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_17 : IAotConverter -{ - public Type SourceType => typeof(int?); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is int sourceTypedValue) - { - int converted = (int)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_18 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.ComprehensiveCountTest.ClassData); - public Type TargetType => typeof(string); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is string targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.ComprehensiveCountTest.ClassData sourceTypedValue) - { - string converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_19 : IAotConverter -{ - public Type SourceType => typeof(bool); - public Type TargetType => typeof(bool?); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is bool targetTypedValue) - { - return targetTypedValue; - } - if (value is bool sourceTypedValue) - { - bool? converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_20 : IAotConverter -{ - public Type SourceType => typeof(bool?); - public Type TargetType => typeof(bool); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is bool targetTypedValue) - { - return targetTypedValue; - } - if (value is bool sourceTypedValue) - { - bool converted = (bool)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_21 : IAotConverter -{ - public Type SourceType => typeof(byte); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is byte sourceTypedValue) - { - decimal converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_22 : IAotConverter -{ - public Type SourceType => typeof(sbyte); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is sbyte sourceTypedValue) - { - decimal converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_23 : IAotConverter -{ - public Type SourceType => typeof(short); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is short sourceTypedValue) - { - decimal converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_24 : IAotConverter -{ - public Type SourceType => typeof(ushort); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is ushort sourceTypedValue) - { - decimal converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_25 : IAotConverter -{ - public Type SourceType => typeof(char); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is char sourceTypedValue) - { - decimal converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_26 : IAotConverter -{ - public Type SourceType => typeof(int); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is int sourceTypedValue) - { - decimal converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_27 : IAotConverter -{ - public Type SourceType => typeof(uint); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is uint sourceTypedValue) - { - decimal converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_28 : IAotConverter -{ - public Type SourceType => typeof(long); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is long sourceTypedValue) - { - decimal converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_29 : IAotConverter -{ - public Type SourceType => typeof(ulong); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is ulong sourceTypedValue) - { - decimal converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_30 : IAotConverter -{ - public Type SourceType => typeof(float); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is float sourceTypedValue) - { - decimal converted = (decimal)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_31 : IAotConverter -{ - public Type SourceType => typeof(double); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is double sourceTypedValue) - { - decimal converted = (decimal)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_32 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(byte); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is byte targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - byte converted = (byte)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_33 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(sbyte); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is sbyte targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - sbyte converted = (sbyte)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_34 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(char); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is char targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - char converted = (char)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_35 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(short); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is short targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - short converted = (short)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_36 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(ushort); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is ushort targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - ushort converted = (ushort)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_37 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - int converted = (int)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_38 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(uint); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is uint targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - uint converted = (uint)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_39 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(long); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is long targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - long converted = (long)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_40 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(ulong); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is ulong targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - ulong converted = (ulong)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_41 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(float); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is float targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - float converted = (float)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_42 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(double); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is double targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - double converted = (double)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_43 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.TestEnum); - public Type TargetType => typeof(global::TUnit.TestProject.TestEnum?); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.TestEnum targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.TestEnum sourceTypedValue) - { - global::TUnit.TestProject.TestEnum? converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_44 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.TestEnum?); - public Type TargetType => typeof(global::TUnit.TestProject.TestEnum); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.TestEnum targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.TestEnum sourceTypedValue) - { - global::TUnit.TestProject.TestEnum converted = (global::TUnit.TestProject.TestEnum)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_45 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedDataSourceBugTest.ClassData1); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedDataSourceBugTest.ClassData1 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_46 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedDataSourceBugTest.ClassData2); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedDataSourceBugTest.ClassData2 sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_47 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTests.Enum4); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion1); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion1 targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTests.Enum4 sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTestsUnion1 converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_48 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion1); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTests.Enum4); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTests.Enum4 targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion1 sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTests.Enum4 converted = (global::TUnit.TestProject.MixedMatrixTests.Enum4)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_49 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTests.Enum5); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion1); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion1 targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTests.Enum5 sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTestsUnion1 converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_50 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion1); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTests.Enum5); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTests.Enum5 targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion1 sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTests.Enum5 converted = (global::TUnit.TestProject.MixedMatrixTests.Enum5)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_51 : IAotConverter -{ - public Type SourceType => typeof(string); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion1); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion1 targetTypedValue) - { - return targetTypedValue; - } - if (value is string sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTestsUnion1 converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_52 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion1); - public Type TargetType => typeof(string); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is string targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion1 sourceTypedValue) - { - string converted = (string)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_53 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTests.Enum4); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion2); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion2 targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTests.Enum4 sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTestsUnion2 converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_54 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion2); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTests.Enum4); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTests.Enum4 targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion2 sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTests.Enum4 converted = (global::TUnit.TestProject.MixedMatrixTests.Enum4)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_55 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTests.Enum5); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion2); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion2 targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTests.Enum5 sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTestsUnion2 converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_56 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion2); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTests.Enum5); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTests.Enum5 targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion2 sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTests.Enum5 converted = (global::TUnit.TestProject.MixedMatrixTests.Enum5)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_57 : IAotConverter -{ - public Type SourceType => typeof(string); - public Type TargetType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion2); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion2 targetTypedValue) - { - return targetTypedValue; - } - if (value is string sourceTypedValue) - { - global::TUnit.TestProject.MixedMatrixTestsUnion2 converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_58 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.MixedMatrixTestsUnion2); - public Type TargetType => typeof(string); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is string targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.MixedMatrixTestsUnion2 sourceTypedValue) - { - string converted = (string)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_59 : IAotConverter -{ - public Type SourceType => typeof(decimal); - public Type TargetType => typeof(decimal?); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - decimal? converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_60 : IAotConverter -{ - public Type SourceType => typeof(decimal?); - public Type TargetType => typeof(decimal); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is decimal targetTypedValue) - { - return targetTypedValue; - } - if (value is decimal sourceTypedValue) - { - decimal converted = (decimal)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_61 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.TestCountVerificationTests.TestDataSource); - public Type TargetType => typeof(int); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is int targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.TestCountVerificationTests.TestDataSource sourceTypedValue) - { - int converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_62 : IAotConverter -{ - public Type SourceType => typeof(int); - public Type TargetType => typeof(global::TUnit.TestProject.Bugs._2757.Foo); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.Bugs._2757.Foo targetTypedValue) - { - return targetTypedValue; - } - if (value is int sourceTypedValue) - { - global::TUnit.TestProject.Bugs._2757.Foo converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_63 : IAotConverter -{ - public Type SourceType => typeof(global::System.ValueTuple); - public Type TargetType => typeof(global::TUnit.TestProject.Bugs._2798.Foo); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.Bugs._2798.Foo targetTypedValue) - { - return targetTypedValue; - } - if (value is global::System.ValueTuple sourceTypedValue) - { - global::TUnit.TestProject.Bugs._2798.Foo converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_64 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.Bugs._3185.FlagMock); - public Type TargetType => typeof(global::TUnit.TestProject.Bugs._3185.FlagMock?); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.Bugs._3185.FlagMock targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.Bugs._3185.FlagMock sourceTypedValue) - { - global::TUnit.TestProject.Bugs._3185.FlagMock? converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_65 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.Bugs._3185.FlagMock?); - public Type TargetType => typeof(global::TUnit.TestProject.Bugs._3185.FlagMock); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.Bugs._3185.FlagMock targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.Bugs._3185.FlagMock sourceTypedValue) - { - global::TUnit.TestProject.Bugs._3185.FlagMock converted = (global::TUnit.TestProject.Bugs._3185.FlagMock)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_66 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.Bugs._3185.RegularEnum); - public Type TargetType => typeof(global::TUnit.TestProject.Bugs._3185.RegularEnum?); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.Bugs._3185.RegularEnum targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.Bugs._3185.RegularEnum sourceTypedValue) - { - global::TUnit.TestProject.Bugs._3185.RegularEnum? converted = sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal sealed class AotConverter_67 : IAotConverter -{ - public Type SourceType => typeof(global::TUnit.TestProject.Bugs._3185.RegularEnum?); - public Type TargetType => typeof(global::TUnit.TestProject.Bugs._3185.RegularEnum); - public object? Convert(object? value) - { - if (value == null) return null; - if (value is global::TUnit.TestProject.Bugs._3185.RegularEnum targetTypedValue) - { - return targetTypedValue; - } - if (value is global::TUnit.TestProject.Bugs._3185.RegularEnum sourceTypedValue) - { - global::TUnit.TestProject.Bugs._3185.RegularEnum converted = (global::TUnit.TestProject.Bugs._3185.RegularEnum)sourceTypedValue; - return converted; - } - return value; // Return original value if type doesn't match - } -} -internal static class AotConverterRegistration -{ - [global::System.Runtime.CompilerServices.ModuleInitializer] - [global::System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA2255:The 'ModuleInitializer' attribute should not be used in libraries", - Justification = "Test framework needs to register AOT converters for conversion operators")] - public static void Initialize() - { - AotConverterRegistry.Register(new AotConverter_0()); - AotConverterRegistry.Register(new AotConverter_1()); - AotConverterRegistry.Register(new AotConverter_2()); - AotConverterRegistry.Register(new AotConverter_3()); - AotConverterRegistry.Register(new AotConverter_4()); - AotConverterRegistry.Register(new AotConverter_5()); - AotConverterRegistry.Register(new AotConverter_6()); - AotConverterRegistry.Register(new AotConverter_7()); - AotConverterRegistry.Register(new AotConverter_8()); - AotConverterRegistry.Register(new AotConverter_9()); - AotConverterRegistry.Register(new AotConverter_10()); - AotConverterRegistry.Register(new AotConverter_11()); - AotConverterRegistry.Register(new AotConverter_12()); - AotConverterRegistry.Register(new AotConverter_13()); - AotConverterRegistry.Register(new AotConverter_14()); - AotConverterRegistry.Register(new AotConverter_15()); - AotConverterRegistry.Register(new AotConverter_16()); - AotConverterRegistry.Register(new AotConverter_17()); - AotConverterRegistry.Register(new AotConverter_18()); - AotConverterRegistry.Register(new AotConverter_19()); - AotConverterRegistry.Register(new AotConverter_20()); - AotConverterRegistry.Register(new AotConverter_21()); - AotConverterRegistry.Register(new AotConverter_22()); - AotConverterRegistry.Register(new AotConverter_23()); - AotConverterRegistry.Register(new AotConverter_24()); - AotConverterRegistry.Register(new AotConverter_25()); - AotConverterRegistry.Register(new AotConverter_26()); - AotConverterRegistry.Register(new AotConverter_27()); - AotConverterRegistry.Register(new AotConverter_28()); - AotConverterRegistry.Register(new AotConverter_29()); - AotConverterRegistry.Register(new AotConverter_30()); - AotConverterRegistry.Register(new AotConverter_31()); - AotConverterRegistry.Register(new AotConverter_32()); - AotConverterRegistry.Register(new AotConverter_33()); - AotConverterRegistry.Register(new AotConverter_34()); - AotConverterRegistry.Register(new AotConverter_35()); - AotConverterRegistry.Register(new AotConverter_36()); - AotConverterRegistry.Register(new AotConverter_37()); - AotConverterRegistry.Register(new AotConverter_38()); - AotConverterRegistry.Register(new AotConverter_39()); - AotConverterRegistry.Register(new AotConverter_40()); - AotConverterRegistry.Register(new AotConverter_41()); - AotConverterRegistry.Register(new AotConverter_42()); - AotConverterRegistry.Register(new AotConverter_43()); - AotConverterRegistry.Register(new AotConverter_44()); - AotConverterRegistry.Register(new AotConverter_45()); - AotConverterRegistry.Register(new AotConverter_46()); - AotConverterRegistry.Register(new AotConverter_47()); - AotConverterRegistry.Register(new AotConverter_48()); - AotConverterRegistry.Register(new AotConverter_49()); - AotConverterRegistry.Register(new AotConverter_50()); - AotConverterRegistry.Register(new AotConverter_51()); - AotConverterRegistry.Register(new AotConverter_52()); - AotConverterRegistry.Register(new AotConverter_53()); - AotConverterRegistry.Register(new AotConverter_54()); - AotConverterRegistry.Register(new AotConverter_55()); - AotConverterRegistry.Register(new AotConverter_56()); - AotConverterRegistry.Register(new AotConverter_57()); - AotConverterRegistry.Register(new AotConverter_58()); - AotConverterRegistry.Register(new AotConverter_59()); - AotConverterRegistry.Register(new AotConverter_60()); - AotConverterRegistry.Register(new AotConverter_61()); - AotConverterRegistry.Register(new AotConverter_62()); - AotConverterRegistry.Register(new AotConverter_63()); - AotConverterRegistry.Register(new AotConverter_64()); - AotConverterRegistry.Register(new AotConverter_65()); - AotConverterRegistry.Register(new AotConverter_66()); - AotConverterRegistry.Register(new AotConverter_67()); - } -} diff --git a/TUnit.TestProject/AllDataSourcesCombinedTests.cs b/TUnit.TestProject/AllDataSourcesCombinedTests.cs index cead330563..e65190ea58 100644 --- a/TUnit.TestProject/AllDataSourcesCombinedTests.cs +++ b/TUnit.TestProject/AllDataSourcesCombinedTests.cs @@ -6,9 +6,9 @@ namespace TUnit.TestProject; [EngineTest(ExpectedResult.Pass)] [Arguments(1)] [Arguments(2)] -[ClassDataSource(typeof(DataSource1))] -[ClassDataSource(typeof(DataSource2))] -[ClassDataSource(typeof(DataSource3))] +[ClassDataSource] +[ClassDataSource] +[ClassDataSource] public class AllDataSourcesCombinedTests(int classValue) { private static readonly ConcurrentBag ExecutedTests = diff --git a/TUnit.TestProject/AllDataSourcesCombinedTestsVerification.cs b/TUnit.TestProject/AllDataSourcesCombinedTestsVerification.cs index 5acf67537a..c002846422 100644 --- a/TUnit.TestProject/AllDataSourcesCombinedTestsVerification.cs +++ b/TUnit.TestProject/AllDataSourcesCombinedTestsVerification.cs @@ -6,9 +6,9 @@ namespace TUnit.TestProject; [EngineTest(ExpectedResult.Pass)] [Arguments(1)] [Arguments(2)] -[ClassDataSource(typeof(DataSource1))] -[ClassDataSource(typeof(DataSource2))] -[ClassDataSource(typeof(DataSource3))] +[ClassDataSource] +[ClassDataSource] +[ClassDataSource] public class AllDataSourcesCombinedTestsVerification { private readonly int classValue; diff --git a/TUnit.TestProject/ClassDataSourceEnumerableTest.cs b/TUnit.TestProject/ClassDataSourceEnumerableTest.cs index ca30f6250f..61027e16a8 100644 --- a/TUnit.TestProject/ClassDataSourceEnumerableTest.cs +++ b/TUnit.TestProject/ClassDataSourceEnumerableTest.cs @@ -2,7 +2,7 @@ namespace TUnit.TestProject; -[ClassDataSource(typeof(EnumerableDataSource))] +[ClassDataSource] public class ClassDataSourceEnumerableTest(string value) { [Test] diff --git a/TUnit.TestProject/ClassDataSourceWithMethodDataSourceTests.cs b/TUnit.TestProject/ClassDataSourceWithMethodDataSourceTests.cs index 4aabddfd37..08cb25429d 100644 --- a/TUnit.TestProject/ClassDataSourceWithMethodDataSourceTests.cs +++ b/TUnit.TestProject/ClassDataSourceWithMethodDataSourceTests.cs @@ -4,9 +4,9 @@ namespace TUnit.TestProject; [EngineTest(ExpectedResult.Pass)] -[ClassDataSource(typeof(DataSource1))] -[ClassDataSource(typeof(DataSource2))] -[ClassDataSource(typeof(DataSource3))] +[ClassDataSource] +[ClassDataSource] +[ClassDataSource] public class ClassDataSourceWithMethodDataSourceTests(int classValue) { private static readonly ConcurrentBag ExecutedTests = diff --git a/TUnit.TestProject/ComprehensiveCountTest.cs b/TUnit.TestProject/ComprehensiveCountTest.cs index 23bc699356..41e437b126 100644 --- a/TUnit.TestProject/ComprehensiveCountTest.cs +++ b/TUnit.TestProject/ComprehensiveCountTest.cs @@ -2,7 +2,7 @@ namespace TUnit.TestProject; -[ClassDataSource(typeof(ClassData))] +[ClassDataSource] [Arguments("X")] [Arguments("Y")] public class ComprehensiveCountTest(string classValue) diff --git a/TUnit.TestProject/MixedDataSourceBugTest.cs b/TUnit.TestProject/MixedDataSourceBugTest.cs index a92ad9d18d..b0b626a82b 100644 --- a/TUnit.TestProject/MixedDataSourceBugTest.cs +++ b/TUnit.TestProject/MixedDataSourceBugTest.cs @@ -1,7 +1,7 @@ namespace TUnit.TestProject; -[ClassDataSource(typeof(ClassData1))] -[ClassDataSource(typeof(ClassData2))] +[ClassDataSource] +[ClassDataSource] public class MixedDataSourceBugTest(int classValue) { [Test] diff --git a/TUnit.TestProject/TestCountVerificationTests.cs b/TUnit.TestProject/TestCountVerificationTests.cs index db7b6eb3cd..8040eed418 100644 --- a/TUnit.TestProject/TestCountVerificationTests.cs +++ b/TUnit.TestProject/TestCountVerificationTests.cs @@ -2,7 +2,7 @@ namespace TUnit.TestProject; [Arguments(1)] [Arguments(2)] -[ClassDataSource(typeof(TestDataSource))] +[ClassDataSource] public class TestCountVerificationTests(int value) { [Test] From 768189597d17e90bfa32467340c69761b734364a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:34:18 +0100 Subject: [PATCH 23/30] fix: emit identity return for same-type switch arms, add cast asymmetry comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When source and target types are identical in GenerateMultiSourceCast, emit `Type __s => __s` instead of `Type __s => (Type)__s` — the pattern match already binds the typed value, so the cast is redundant. Add comment explaining why GenerateMultiSourceCast uses single casts while GenerateCast uses double casts (pattern match performs the unbox). --- ...ivenTest_WithConflictingNamespace.DotNet10_0.verified.txt | 2 +- ...rivenTest_WithConflictingNamespace.DotNet8_0.verified.txt | 2 +- ...rivenTest_WithConflictingNamespace.DotNet9_0.verified.txt | 2 +- ...taDrivenTest_WithConflictingNamespace.Net4_7.verified.txt | 4 ++-- .../DataDrivenTests.Test.verified.txt | 4 ++-- TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt | 4 ++-- .../CodeGenerators/Helpers/CastExpressionHelper.cs | 5 ++++- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt index eaaf3291db..2c15418705 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt index eaaf3291db..2c15418705 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt index eaaf3291db..2c15418705 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt index eaaf3291db..e1c9b41377 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt index eaaf3291db..e1c9b41377 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => (global::TUnit.TestProject.TestEnum)__s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt index c1446d5a19..8316b7a707 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -39,7 +39,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long), "value", new gl { case 1: { - instance.MyTest((args[0] switch { int __s => (long)__s, byte __s => (long)__s, short __s => (long)__s, char __s => (long)__s, long __s => (long)__s, _ => (long)args[0] })); + instance.MyTest((args[0] switch { int __s => (long)__s, byte __s => (long)__s, short __s => (long)__s, char __s => (long)__s, long __s => __s, _ => (long)args[0] })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index 47cf9e3828..f087a8ed34 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -53,13 +53,16 @@ public static string GenerateMultiSourceCast( var targetGQ = targetType.GloballyQualified(); var arms = new List(); + // In switch arms the pattern match performs the unbox, so only a single cast + // to the target type is needed (unlike GenerateCast which needs a double-cast + // (TargetType)(SourceType)expr to first unbox then convert). foreach (var sourceType in sourceTypes) { var sourceGQ = sourceType.GloballyQualified(); if (SymbolEqualityComparer.Default.Equals(sourceType, targetType)) { - arms.Add($"{sourceGQ} __s => ({targetGQ})__s"); + arms.Add($"{sourceGQ} __s => __s"); } else if (compilation != null && compilation.ClassifyConversion(sourceType, targetType) is { IsImplicit: true } or { IsExplicit: true }) { From 50595c98eed2ab55203df6abeebaf763880c9d09 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:39:35 +0100 Subject: [PATCH 24/30] fix: match ITypedDataSourceAttribute<> notation and sort switch arms by inheritance DataSourceAttributeHelper used backtick notation (`1) for open generic matching, but InterfaceCache.GetGenericInterface compares against GloballyQualified() output which uses <> notation. This prevented the source generator from recognizing typed data source attributes, causing fallback to CastHelper.Cast() which fails under AOT. Also sort multi-source switch arms most-derived-first to prevent CS8510 unreachable pattern errors when source types have inheritance relationships. --- ...assDataSourceDrivenTests.Test.verified.txt | 16 +++--- ...ssDataSourceDrivenTests2.Test.verified.txt | 4 +- ...ceDrivenTestsSharedKeyed.Test.verified.txt | 6 +-- .../CustomDisplayNameTests.Test.verified.txt | 4 +- ...ataSourceGeneratorTests.Typed.verified.txt | 8 +-- .../Hooks1589.Test.verified.txt | 4 +- .../Hooks1594.Test.verified.txt | 4 +- ...assDataSourceDrivenTests.Test.verified.txt | 4 +- .../Tests1589.Test.verified.txt | 4 +- .../Tests1594.Test.verified.txt | 4 +- .../Tests1821.Test.verified.txt | 4 +- .../Helpers/CastExpressionHelper.cs | 51 ++++++++++++++++++- .../Helpers/DataSourceAttributeHelper.cs | 9 ++-- 13 files changed, 86 insertions(+), 36 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests.Test.verified.txt index cbf117d53a..0d66926c12 100644 --- a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -73,7 +73,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 1: { - instance.DataSource_Class(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.DataSource_Class((global::TUnit.TestProject.Library.Models.SomeAsyncDisposableClass)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -93,7 +93,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 1: { - instance.DataSource_Class_Generic(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.DataSource_Class_Generic((global::TUnit.TestProject.Library.Models.SomeAsyncDisposableClass)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -113,7 +113,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_1_ClassDataSource(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_1_ClassDataSource((global::TUnit.TestProject.Library.Models.InitializableClass)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -132,7 +132,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_2_ClassDataSources(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_2_ClassDataSources((global::TUnit.TestProject.Library.Models.InitializableClass)args[0], (global::TUnit.TestProject.Library.Models.InitializableClass)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -151,7 +151,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_3_ClassDataSources(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_3_ClassDataSources((global::TUnit.TestProject.Library.Models.InitializableClass)args[0], (global::TUnit.TestProject.Library.Models.InitializableClass)args[1], (global::TUnit.TestProject.Library.Models.InitializableClass)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -170,7 +170,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 4: { - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_4_ClassDataSources(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_4_ClassDataSources((global::TUnit.TestProject.Library.Models.InitializableClass)args[0], (global::TUnit.TestProject.Library.Models.InitializableClass)args[1], (global::TUnit.TestProject.Library.Models.InitializableClass)args[2], (global::TUnit.TestProject.Library.Models.InitializableClass)args[3])); } default: throw new global::System.ArgumentException($"Expected exactly 4 arguments, but got {args.Length}"); @@ -189,7 +189,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 5: { - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_5_ClassDataSources(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]))); + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_5_ClassDataSources((global::TUnit.TestProject.Library.Models.InitializableClass)args[0], (global::TUnit.TestProject.Library.Models.InitializableClass)args[1], (global::TUnit.TestProject.Library.Models.InitializableClass)args[2], (global::TUnit.TestProject.Library.Models.InitializableClass)args[3], (global::TUnit.TestProject.Library.Models.InitializableClass)args[4])); } default: throw new global::System.ArgumentException($"Expected exactly 5 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests2.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests2.Test.verified.txt index 8d9a296e22..5a79643e25 100644 --- a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests2.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests2.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -26,7 +26,7 @@ internal static class TUnit_TestProject_ClassDataSourceDrivenTests2__TestSource private static readonly global::TUnit.Core.MethodMetadata __mm_1 = global::TUnit.Core.MethodMetadataFactory.Create("Base_Derived2", __classType, typeof(void), __classMetadata); private static global::TUnit.TestProject.ClassDataSourceDrivenTests2 __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.ClassDataSourceDrivenTests2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return new global::TUnit.TestProject.ClassDataSourceDrivenTests2((args[0] switch { global::TUnit.TestProject.ClassDataSourceDrivenTests2.Derived1 __s => (global::TUnit.TestProject.ClassDataSourceDrivenTests2.Base)__s, global::TUnit.TestProject.ClassDataSourceDrivenTests2.Derived2 __s => (global::TUnit.TestProject.ClassDataSourceDrivenTests2.Base)__s, _ => (global::TUnit.TestProject.ClassDataSourceDrivenTests2.Base)args[0] })); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.ClassDataSourceDrivenTests2 instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { diff --git a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTestsSharedKeyed.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTestsSharedKeyed.Test.verified.txt index 1d2d7b5c14..a0f43abe99 100644 --- a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTestsSharedKeyed.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTestsSharedKeyed.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -43,7 +43,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 1: { - instance.DataSource_Class(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.DataSource_Class((global::TUnit.TestProject.Library.Models.SomeAsyncDisposableClass)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -63,7 +63,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 1: { - instance.DataSource_Class_Generic(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.DataSource_Class_Generic((global::TUnit.TestProject.Library.Models.SomeAsyncDisposableClass)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt index 845c44e1f6..9cb1a4411f 100644 --- a/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -151,7 +151,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "password", n { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.PasswordTest(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.PasswordTest((string)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt index 7bd5762252..d99ebb7422 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -42,7 +42,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "value3", new g }); private static global::TUnit.TestProject.DataSourceGeneratorTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.DataSourceGeneratorTests(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2])); + return new global::TUnit.TestProject.DataSourceGeneratorTests((args[0] switch { int __s => __s, global::System.ValueTuple __s => global::TUnit.Core.Helpers.CastHelper.Cast(__s), _ => (int)args[0] }), (string)args[1], (bool)args[2]); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.DataSourceGeneratorTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { @@ -56,7 +56,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "value3", new g { case 1: { - instance.GeneratedData_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.GeneratedData_Method((int)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -76,7 +76,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "value3", new g { case 3: { - instance.GeneratedData_Method2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2])); + instance.GeneratedData_Method2((int)args[0], (string)args[1], (bool)args[2]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Hooks1589.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Hooks1589.Test.verified.txt index a64949adef..9da3cd10e8 100644 --- a/TUnit.Core.SourceGenerator.Tests/Hooks1589.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Hooks1589.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -25,7 +25,7 @@ internal static class TUnit_TestProject_Bugs__1589_MyTests__TestSource private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("Test1", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata); private static global::TUnit.TestProject.Bugs._1589.MyTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.Bugs._1589.MyTests(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return new global::TUnit.TestProject.Bugs._1589.MyTests((global::TUnit.TestProject.Bugs._1589.MyFixture)args[0]); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.Bugs._1589.MyTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { diff --git a/TUnit.Core.SourceGenerator.Tests/Hooks1594.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Hooks1594.Test.verified.txt index 7fc58c8b28..5b9642b72e 100644 --- a/TUnit.Core.SourceGenerator.Tests/Hooks1594.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Hooks1594.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -25,7 +25,7 @@ internal static class TUnit_TestProject_Bugs__1594_MyTests__TestSource private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("Test1", __classType, typeof(void), __classMetadata); private static global::TUnit.TestProject.Bugs._1594.MyTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.Bugs._1594.MyTests(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return new global::TUnit.TestProject.Bugs._1594.MyTests((global::TUnit.TestProject.Bugs._1594.MyFixture)args[0]); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.Bugs._1594.MyTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { diff --git a/TUnit.Core.SourceGenerator.Tests/MultipleClassDataSourceDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/MultipleClassDataSourceDrivenTests.Test.verified.txt index e8701ea576..c4bb0888ea 100644 --- a/TUnit.Core.SourceGenerator.Tests/MultipleClassDataSourceDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/MultipleClassDataSourceDrivenTests.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -30,7 +30,7 @@ internal static class TUnit_TestProject_MultipleClassDataSourceDrivenTests__Test private static readonly global::TUnit.Core.MethodMetadata __mm_1 = global::TUnit.Core.MethodMetadataFactory.Create("Test2", __classType, typeof(void), __classMetadata); private static global::TUnit.TestProject.MultipleClassDataSourceDrivenTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.MultipleClassDataSourceDrivenTests(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4])); + return new global::TUnit.TestProject.MultipleClassDataSourceDrivenTests((global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject1)args[0], (global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject2)args[1], (global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject3)args[2], (global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject4)args[3], (global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject5)args[4]); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.MultipleClassDataSourceDrivenTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1589.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1589.Test.verified.txt index a64949adef..9da3cd10e8 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1589.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1589.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -25,7 +25,7 @@ internal static class TUnit_TestProject_Bugs__1589_MyTests__TestSource private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("Test1", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata); private static global::TUnit.TestProject.Bugs._1589.MyTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.Bugs._1589.MyTests(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return new global::TUnit.TestProject.Bugs._1589.MyTests((global::TUnit.TestProject.Bugs._1589.MyFixture)args[0]); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.Bugs._1589.MyTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1594.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1594.Test.verified.txt index 7fc58c8b28..5b9642b72e 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1594.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1594.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -25,7 +25,7 @@ internal static class TUnit_TestProject_Bugs__1594_MyTests__TestSource private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("Test1", __classType, typeof(void), __classMetadata); private static global::TUnit.TestProject.Bugs._1594.MyTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.Bugs._1594.MyTests(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return new global::TUnit.TestProject.Bugs._1594.MyTests((global::TUnit.TestProject.Bugs._1594.MyFixture)args[0]); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.Bugs._1594.MyTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt index d9ab80e920..b63a3c0e95 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -32,7 +32,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "value", new }); private static global::TUnit.TestProject.Bugs._1821.Tests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.Bugs._1821.Tests(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return new global::TUnit.TestProject.Bugs._1821.Tests((global::TUnit.TestProject.Bugs._1821.MyData)args[0]); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.Bugs._1821.Tests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index f087a8ed34..9ad0363746 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -53,10 +53,15 @@ public static string GenerateMultiSourceCast( var targetGQ = targetType.GloballyQualified(); var arms = new List(); + // Sort source types so that derived types come before their base types. + // Without this, a base type pattern arm would catch derived instances first, + // causing CS8510 "unreachable pattern" errors. + var sorted = SortMostDerivedFirst(sourceTypes); + // In switch arms the pattern match performs the unbox, so only a single cast // to the target type is needed (unlike GenerateCast which needs a double-cast // (TargetType)(SourceType)expr to first unbox then convert). - foreach (var sourceType in sourceTypes) + foreach (var sourceType in sorted) { var sourceGQ = sourceType.GloballyQualified(); @@ -80,6 +85,50 @@ public static string GenerateMultiSourceCast( return $"({argsExpression} switch {{ {string.Join(", ", arms)} }})"; } + private static List SortMostDerivedFirst(IReadOnlyList types) + { + var result = new List(types); + + result.Sort((a, b) => + { + if (SymbolEqualityComparer.Default.Equals(a, b)) + { + return 0; + } + + if (IsBaseTypeOf(a, b)) + { + return 1; + } + + if (IsBaseTypeOf(b, a)) + { + return -1; + } + + return 0; + }); + + return result; + } + + private static bool IsBaseTypeOf(ITypeSymbol potentialBase, ITypeSymbol derived) + { + var current = derived.BaseType; + + while (current != null) + { + if (SymbolEqualityComparer.Default.Equals(current, potentialBase)) + { + return true; + } + + current = current.BaseType; + } + + return false; + } + /// /// Generates a typed cast expression, or falls back to CastHelper.Cast when source type is unknown. /// diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs index 935fec6b69..60bcd038c6 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs @@ -5,6 +5,9 @@ namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; internal static class DataSourceAttributeHelper { + // GloballyQualified() emits open generics as Foo<>, not Foo`1 + private const string TypedDataSourceInterfacePattern = "global::TUnit.Core.ITypedDataSourceAttribute<>"; + public static bool IsDataSourceAttribute(INamedTypeSymbol? attributeClass) { if (attributeClass == null) @@ -12,7 +15,6 @@ public static bool IsDataSourceAttribute(INamedTypeSymbol? attributeClass) return false; } - // Check if the attribute implements IDataSourceAttribute (using cache) return InterfaceCache.ImplementsInterface(attributeClass, "global::TUnit.Core.IDataSourceAttribute"); } @@ -23,8 +25,7 @@ public static bool IsTypedDataSourceAttribute(INamedTypeSymbol? attributeClass) return false; } - // Check if the attribute implements ITypedDataSourceAttribute (using cache) - return InterfaceCache.ImplementsGenericInterface(attributeClass, "global::TUnit.Core.ITypedDataSourceAttribute`1"); + return InterfaceCache.ImplementsGenericInterface(attributeClass, TypedDataSourceInterfacePattern); } public static ITypeSymbol? GetTypedDataSourceType(INamedTypeSymbol? attributeClass) @@ -34,7 +35,7 @@ public static bool IsTypedDataSourceAttribute(INamedTypeSymbol? attributeClass) return null; } - var typedInterface = InterfaceCache.GetGenericInterface(attributeClass, "global::TUnit.Core.ITypedDataSourceAttribute`1"); + var typedInterface = InterfaceCache.GetGenericInterface(attributeClass, TypedDataSourceInterfacePattern); return typedInterface?.TypeArguments.FirstOrDefault(); } From 9e59a52081c2695885f0bcabc6232b3f89103faf Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:58:13 +0100 Subject: [PATCH 25/30] fix: use CastHelper in switch default arm, add total-order tiebreaker Address two review findings: - Default arm in multi-source switch now uses CastHelper.Cast() instead of a bare cast, preventing NullReferenceException when args[i] is null for value-type targets. - Add deterministic string tiebreaker in SortMostDerivedFirst comparator to satisfy Introsort's total-order requirement for unrelated types. --- .../ClassDataSourceDrivenTests2.Test.verified.txt | 2 +- ...enTest_WithConflictingNamespace.DotNet10_0.verified.txt | 2 +- ...venTest_WithConflictingNamespace.DotNet8_0.verified.txt | 2 +- ...venTest_WithConflictingNamespace.DotNet9_0.verified.txt | 2 +- ...DrivenTest_WithConflictingNamespace.Net4_7.verified.txt | 2 +- .../DataDrivenTests.Test.verified.txt | 2 +- .../DataSourceGeneratorTests.Typed.verified.txt | 2 +- .../DecimalArgumentTests.Test.verified.txt | 6 +++--- .../ExpectedArgumentTypeTests.Test.verified.txt | 6 +++--- .../Tests2083.Test.verified.txt | 2 +- .../CodeGenerators/Helpers/CastExpressionHelper.cs | 7 ++++--- 11 files changed, 18 insertions(+), 17 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests2.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests2.Test.verified.txt index 5a79643e25..dd1bfd32a1 100644 --- a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests2.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests2.Test.verified.txt @@ -26,7 +26,7 @@ internal static class TUnit_TestProject_ClassDataSourceDrivenTests2__TestSource private static readonly global::TUnit.Core.MethodMetadata __mm_1 = global::TUnit.Core.MethodMetadataFactory.Create("Base_Derived2", __classType, typeof(void), __classMetadata); private static global::TUnit.TestProject.ClassDataSourceDrivenTests2 __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.ClassDataSourceDrivenTests2((args[0] switch { global::TUnit.TestProject.ClassDataSourceDrivenTests2.Derived1 __s => (global::TUnit.TestProject.ClassDataSourceDrivenTests2.Base)__s, global::TUnit.TestProject.ClassDataSourceDrivenTests2.Derived2 __s => (global::TUnit.TestProject.ClassDataSourceDrivenTests2.Base)__s, _ => (global::TUnit.TestProject.ClassDataSourceDrivenTests2.Base)args[0] })); + return new global::TUnit.TestProject.ClassDataSourceDrivenTests2((args[0] switch { global::TUnit.TestProject.ClassDataSourceDrivenTests2.Derived1 __s => (global::TUnit.TestProject.ClassDataSourceDrivenTests2.Base)__s, global::TUnit.TestProject.ClassDataSourceDrivenTests2.Derived2 __s => (global::TUnit.TestProject.ClassDataSourceDrivenTests2.Base)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.ClassDataSourceDrivenTests2 instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt index 2c15418705..a7b4b28191 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt index 2c15418705..a7b4b28191 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt index 2c15418705..a7b4b28191 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt index e1c9b41377..c8c83e5d1e 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt index e1c9b41377..c8c83e5d1e 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt @@ -116,7 +116,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo { case 1: { - instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => (global::TUnit.TestProject.TestEnum)args[0] })); + instance.EnumValue((args[0] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt index d99ebb7422..4d12305309 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt @@ -42,7 +42,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "value3", new g }); private static global::TUnit.TestProject.DataSourceGeneratorTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { - return new global::TUnit.TestProject.DataSourceGeneratorTests((args[0] switch { int __s => __s, global::System.ValueTuple __s => global::TUnit.Core.Helpers.CastHelper.Cast(__s), _ => (int)args[0] }), (string)args[1], (bool)args[2]); + return new global::TUnit.TestProject.DataSourceGeneratorTests((args[0] switch { global::System.ValueTuple __s => global::TUnit.Core.Helpers.CastHelper.Cast(__s), int __s => __s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }), (string)args[1], (bool)args[2]); } private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.DataSourceGeneratorTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) { diff --git a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt index 762bfdc7e6..310088b6ee 100644 --- a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -91,7 +91,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.Transfer((args[0] switch { int __s => (decimal)__s, double __s => (decimal)__s, _ => (decimal)args[0] }), (args[1] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => (decimal)args[1] }))); + return new global::System.Threading.Tasks.ValueTask(instance.Transfer((args[0] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }), (args[1] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) }))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -224,7 +224,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - instance.Test((args[0] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => (decimal)args[0] })); + instance.Test((args[0] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt index f9797a89f7..c43348edb4 100644 --- a/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -46,7 +46,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Type), { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.TypedArguments((args[0] switch { double __s => (object)__s, float __s => (object)__s, sbyte __s => (object)__s, byte __s => (object)__s, short __s => (object)__s, ushort __s => (object)__s, int __s => (object)__s, uint __s => (object)__s, long __s => (object)__s, ulong __s => (object)__s, _ => (object)args[0] }), (global::System.Type)args[1])); + return new global::System.Threading.Tasks.ValueTask(instance.TypedArguments((args[0] switch { byte __s => (object)__s, double __s => (object)__s, float __s => (object)__s, int __s => (object)__s, long __s => (object)__s, sbyte __s => (object)__s, short __s => (object)__s, uint __s => (object)__s, ulong __s => (object)__s, ushort __s => (object)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }), (global::System.Type)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -65,7 +65,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Type), { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.EnumTypes((args[0] switch { global::TUnit.TestProject.ByteEnum __s => (object)__s, global::TUnit.TestProject.SByteEnum __s => (object)__s, global::TUnit.TestProject.Int16Enum __s => (object)__s, global::TUnit.TestProject.UInt16Enum __s => (object)__s, global::TUnit.TestProject.Int32Enum __s => (object)__s, global::TUnit.TestProject.UInt32Enum __s => (object)__s, global::TUnit.TestProject.Int64Enum __s => (object)__s, global::TUnit.TestProject.UInt64Enum __s => (object)__s, _ => (object)args[0] }), (global::System.Type)args[1], (global::System.Type)args[2])); + return new global::System.Threading.Tasks.ValueTask(instance.EnumTypes((args[0] switch { global::TUnit.TestProject.ByteEnum __s => (object)__s, global::TUnit.TestProject.Int16Enum __s => (object)__s, global::TUnit.TestProject.Int32Enum __s => (object)__s, global::TUnit.TestProject.Int64Enum __s => (object)__s, global::TUnit.TestProject.SByteEnum __s => (object)__s, global::TUnit.TestProject.UInt16Enum __s => (object)__s, global::TUnit.TestProject.UInt32Enum __s => (object)__s, global::TUnit.TestProject.UInt64Enum __s => (object)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }), (global::System.Type)args[1], (global::System.Type)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt index 8316b7a707..fcc71ab5ec 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt @@ -39,7 +39,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long), "value", new gl { case 1: { - instance.MyTest((args[0] switch { int __s => (long)__s, byte __s => (long)__s, short __s => (long)__s, char __s => (long)__s, long __s => __s, _ => (long)args[0] })); + instance.MyTest((args[0] switch { byte __s => (long)__s, char __s => (long)__s, int __s => (long)__s, long __s => __s, short __s => (long)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index 9ad0363746..fe2534ce52 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -79,8 +80,7 @@ public static string GenerateMultiSourceCast( } } - // Default arm handles unboxing/direct cast from Arguments or unexpected types - arms.Add($"_ => ({targetGQ}){argsExpression}"); + arms.Add($"_ => global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>({argsExpression})"); return $"({argsExpression} switch {{ {string.Join(", ", arms)} }})"; } @@ -106,7 +106,8 @@ private static List SortMostDerivedFirst(IReadOnlyList return -1; } - return 0; + // Deterministic tiebreaker for unrelated types to satisfy Introsort's total-order requirement + return string.Compare(a.ToDisplayString(), b.ToDisplayString(), StringComparison.Ordinal); }); return result; From 592d9fda3be79a6598a69e57518f252ef7f318a3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 14 Apr 2026 01:31:10 +0100 Subject: [PATCH 26/30] fix: extract source types from [Matrix] parameter attributes for AOT-safe casts MatrixDataSourceAttribute was treated as an unknown data source in SourceTypeAnalyzer, causing all parameters to fall back to CastHelper.Cast. In AOT mode, CastHelper can't discover implicit operators via reflection, breaking tests like ImplicitConversion(TestEnum2 -> OneOf). Now ProcessMatrixDataSource examines per-parameter [Matrix] attributes to extract compile-time value types, enabling typed switch arms in generated code. Auto-generated parameters without [Matrix] use the parameter type directly, except for enums (Enum.GetValuesAsUnderlyingType boxes as int, not enum type). --- ...nflictingNamespace.DotNet10_0.verified.txt | 12 +- .../MatrixTests.Test.verified.txt | 18 +-- .../MatrixTests.cs | 5 +- .../Tests1821.Test.verified.txt | 4 +- .../Tests1889.Test.DotNet10_0.verified.txt | 2 +- .../Tests2085.Test.verified.txt | 4 +- ...utCancellationTokenTests.Test.verified.txt | 2 +- .../Helpers/SourceTypeAnalyzer.cs | 123 +++++++++++++++++- 8 files changed, 145 insertions(+), 25 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt index ebe16e36c3..910f38a52c 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet10_0.verified.txt @@ -100,7 +100,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One((string)args[0], (int)args[1], (bool)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -119,7 +119,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 4: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two((int)args[0], (int)args[1], (int)args[2], (bool)args[3])); } default: throw new global::System.ArgumentException($"Expected exactly 4 arguments, but got {args.Length}"); @@ -138,7 +138,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), (TestEnum?)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -157,7 +157,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools((string)args[0], (bool)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2((string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -328,7 +328,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues((bool)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt index 69a67571e7..88fc72f3a3 100644 --- a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt @@ -50,8 +50,8 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool?), "boolean", new }); private static readonly global::TUnit.Core.MethodMetadata __mm_5 = global::TUnit.Core.MethodMetadataFactory.Create("ImplicitConversion", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] { -global::TUnit.Core.ParameterMetadataFactory.Create(typeof(OneOf), "enum", new global::TUnit.Core.ConcreteType(typeof(OneOf)), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.MatrixTests).GetMethod("ImplicitConversion", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(OneOf), typeof(bool) }, null)!.GetParameters()[0]), -global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "boolean", new global::TUnit.Core.ConcreteType(typeof(bool)), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.MatrixTests).GetMethod("ImplicitConversion", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(OneOf), typeof(bool) }, null)!.GetParameters()[1]) +global::TUnit.Core.ParameterMetadataFactory.Create(typeof(OneOf), "enum", new global::TUnit.Core.ConcreteType(typeof(OneOf)), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.MatrixTests).GetMethod("ImplicitConversion", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(OneOf), typeof(bool) }, null)!.GetParameters()[0]), +global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "boolean", new global::TUnit.Core.ConcreteType(typeof(bool)), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.MatrixTests).GetMethod("ImplicitConversion", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(OneOf), typeof(bool) }, null)!.GetParameters()[1]) }); private static readonly global::TUnit.Core.MethodMetadata __mm_6 = global::TUnit.Core.MethodMetadataFactory.Create("ExcludingAutoGeneratedMatrixValues", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] { @@ -100,7 +100,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One((string)args[0], (int)args[1], (bool)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -119,7 +119,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 4: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two((int)args[0], (int)args[1], (int)args[2], (bool)args[3])); } default: throw new global::System.ArgumentException($"Expected exactly 4 arguments, but got {args.Length}"); @@ -138,7 +138,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum((int)args[0], (args[1] switch { global::TUnit.TestProject.TestEnum __s => __s, int __s => (global::TUnit.TestProject.TestEnum)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) }), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -157,7 +157,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools((string)args[0], (bool)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2((string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -195,7 +195,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.ImplicitConversion(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.ImplicitConversion((args[0] switch { global::TUnit.TestProject.TestEnum __s => global::TUnit.Core.Helpers.CastHelper.Cast>(__s), global::TUnit.TestProject.TestEnum2 __s => global::TUnit.Core.Helpers.CastHelper.Cast>(__s), _ => global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]) }), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -328,7 +328,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues((bool)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/MatrixTests.cs b/TUnit.Core.SourceGenerator.Tests/MatrixTests.cs index 8b545cc6df..d03a26e99a 100644 --- a/TUnit.Core.SourceGenerator.Tests/MatrixTests.cs +++ b/TUnit.Core.SourceGenerator.Tests/MatrixTests.cs @@ -14,7 +14,10 @@ public Task Test() => RunTest(Path.Combine(Git.RootDirectory.FullName, [ Path.Combine(Git.RootDirectory.FullName, "TUnit.TestProject", - "TestEnum.cs") + "TestEnum.cs"), + Path.Combine(Git.RootDirectory.FullName, + "TUnit.TestProject", + "TestEnum2.cs") ] }, async generatedFiles => diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt index b63a3c0e95..7a6b824432 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -65,7 +65,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "value", new { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixDataSource(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixDataSource((string)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt index a6f83123c8..d88983e6d4 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt @@ -112,7 +112,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "condition", ne { case 1: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Test2((bool)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2085.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2085.Test.verified.txt index bc619a2256..4ba36e77d9 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2085.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2085.Test.verified.txt @@ -43,7 +43,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(float), "d", new globa { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Double_SpecialConsts(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Double_SpecialConsts((double)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -62,7 +62,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(float), "d", new globa { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Float_SpecialConsts(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + return new global::System.Threading.Tasks.ValueTask(instance.Float_SpecialConsts((float)args[0])); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt index 3f8217dfa2..6f41093178 100644 --- a/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt @@ -139,7 +139,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Threadi { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? global::System.Threading.CancellationToken.None)); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest((int)args[0], context?.Execution.CancellationToken ?? global::System.Threading.CancellationToken.None)); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs index c403991e08..4daf7ed0f7 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SourceTypeAnalyzer.cs @@ -78,7 +78,7 @@ internal static class SourceTypeAnalyzer // For params methods with [Arguments], size the array to max argument count var arraySize = hasParams ? GetMaxArgumentCount(dataSources, parameterCount) : parameterCount; - return ExtractSourceTypesFromAllAttributes(dataSources, arraySize); + return ExtractSourceTypesFromAllAttributes(dataSources, arraySize, method.Parameters); } /// @@ -121,7 +121,7 @@ internal static class SourceTypeAnalyzer var ctorHasParams = constructor.Parameters[ctorParamCount - 1].IsParams; var ctorArraySize = ctorHasParams ? GetMaxArgumentCount(dataSources, ctorParamCount) : ctorParamCount; - return ExtractSourceTypesFromAllAttributes(dataSources, ctorArraySize); + return ExtractSourceTypesFromAllAttributes(dataSources, ctorArraySize, constructor.Parameters); } /// @@ -129,7 +129,8 @@ internal static class SourceTypeAnalyzer /// private static SourceTypeInfo ExtractSourceTypesFromAllAttributes( List dataSources, - int parameterCount) + int parameterCount, + ImmutableArray parameters) { // Each position accumulates a set of possible source types. // null = unknown (some attribute couldn't determine the type for this position). @@ -146,6 +147,10 @@ private static SourceTypeInfo ExtractSourceTypesFromAllAttributes( { ProcessTypedDataSource(attr, positionTypes, positionUnknown, parameterCount); } + else if (IsMatrixDataSourceAttribute(attr)) + { + ProcessMatrixDataSource(parameters, positionTypes, positionUnknown, parameterCount); + } else { // Unknown data source type (e.g., MethodDataSource, non-generic ClassDataSource @@ -362,4 +367,116 @@ private static bool IsArgumentsAttribute(AttributeData attr) return attr.AttributeClass?.Name is "ArgumentsAttribute" && attr.AttributeClass.ToDisplayString() == WellKnownFullyQualifiedClassNames.ArgumentsAttribute.WithoutGlobalPrefix; } + + /// + /// Returns true if the type is an enum or Nullable<TEnum>. + /// MatrixDataSource boxes auto-generated enum values as their underlying type (via + /// Enum.GetValuesAsUnderlyingType), so direct casts from object to enum type would fail. + /// + private static bool IsEnumLike(ITypeSymbol type) + { + if (type.TypeKind == TypeKind.Enum) + { + return true; + } + + // Check for Nullable + if (type is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } nullable) + { + return nullable.TypeArguments[0].TypeKind == TypeKind.Enum; + } + + return false; + } + + private static bool IsMatrixDataSourceAttribute(AttributeData attr) + { + return attr.AttributeClass?.Name is "MatrixDataSourceAttribute"; + } + + /// + /// Processes a MatrixDataSource by examining per-parameter [Matrix] attributes + /// to extract the compile-time source types for each position. + /// + private static void ProcessMatrixDataSource( + ImmutableArray parameters, + List?[] positionTypes, + bool[] positionUnknown, + int parameterCount) + { + var paramIndex = 0; + + foreach (var param in parameters) + { + if (paramIndex >= parameterCount) + { + break; + } + + // Skip CancellationToken parameters (same filter as caller) + if (param.Type.Name == "CancellationToken" && param.Type.ContainingNamespace?.ToString() == "System.Threading") + { + continue; + } + + if (positionUnknown[paramIndex]) + { + paramIndex++; + continue; + } + + var hasMatrixAttr = false; + + foreach (var paramAttr in param.GetAttributes()) + { + if (paramAttr.AttributeClass?.Name is not ("MatrixAttribute" or "MatrixMethodAttribute" or "MatrixRangeAttribute")) + { + continue; + } + + hasMatrixAttr = true; + + // MatrixMethod and MatrixRange don't have compile-time values we can extract + if (paramAttr.AttributeClass.Name is "MatrixMethodAttribute" or "MatrixRangeAttribute") + { + positionUnknown[paramIndex] = true; + break; + } + + // Extract value types from [Matrix(val1, val2, ...)] constructor args + if (paramAttr.ConstructorArguments.Length == 0) + { + continue; + } + + var firstArg = paramAttr.ConstructorArguments[0]; + + if (firstArg.Kind == TypedConstantKind.Array && !firstArg.Values.IsDefault) + { + foreach (var val in firstArg.Values) + { + if (val.IsNull || val.Kind == TypedConstantKind.Error || val.Type == null) + { + positionUnknown[paramIndex] = true; + break; + } + + AddTypeToPosition(positionTypes, paramIndex, val.Type); + } + } + } + + // No [Matrix] attribute on this parameter -> auto-generated (bool/enum). + // For bool: runtime type is bool, so direct cast works. + // For enum: MatrixDataSource uses Enum.GetValuesAsUnderlyingType which boxes as the + // underlying type (e.g., int), NOT the enum type. Direct cast (EnumType)args[n] + // would fail at runtime, so we must fall back to CastHelper for enums. + if (!hasMatrixAttr && !positionUnknown[paramIndex] && !IsEnumLike(param.Type)) + { + AddTypeToPosition(positionTypes, paramIndex, param.Type); + } + + paramIndex++; + } + } } From 26be3adb4d0d2ee5852ac1b61febc80443da7d03 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 14 Apr 2026 07:37:03 +0100 Subject: [PATCH 27/30] fix: update platform-specific snapshot files for net8.0, net9.0, net472 --- ...WithConflictingNamespace.DotNet8_0.verified.txt | 12 ++++++------ ...WithConflictingNamespace.DotNet9_0.verified.txt | 12 ++++++------ ...st_WithConflictingNamespace.Net4_7.verified.txt | 14 +++++++------- .../Tests1889.Test.DotNet8_0.verified.txt | 2 +- .../Tests1889.Test.DotNet9_0.verified.txt | 2 +- .../Tests1889.Test.Net4_7.verified.txt | 4 ++-- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt index ebe16e36c3..910f38a52c 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet8_0.verified.txt @@ -100,7 +100,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One((string)args[0], (int)args[1], (bool)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -119,7 +119,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 4: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two((int)args[0], (int)args[1], (int)args[2], (bool)args[3])); } default: throw new global::System.ArgumentException($"Expected exactly 4 arguments, but got {args.Length}"); @@ -138,7 +138,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), (TestEnum?)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -157,7 +157,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools((string)args[0], (bool)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2((string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -328,7 +328,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues((bool)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt index ebe16e36c3..910f38a52c 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.DotNet9_0.verified.txt @@ -100,7 +100,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One((string)args[0], (int)args[1], (bool)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -119,7 +119,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 4: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two((int)args[0], (int)args[1], (int)args[2], (bool)args[3])); } default: throw new global::System.ArgumentException($"Expected exactly 4 arguments, but got {args.Length}"); @@ -138,7 +138,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), (TestEnum?)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -157,7 +157,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools((string)args[0], (bool)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2((string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -328,7 +328,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues((bool)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.Net4_7.verified.txt index ebe16e36c3..938afac314 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MatrixTest_WithConflictingNamespace.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -100,7 +100,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One((string)args[0], (int)args[1], (bool)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -119,7 +119,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 4: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two((int)args[0], (int)args[1], (int)args[2], (bool)args[3])); } default: throw new global::System.ArgumentException($"Expected exactly 4 arguments, but got {args.Length}"); @@ -138,7 +138,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum((int)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), (TestEnum?)args[2])); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -157,7 +157,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools((string)args[0], (bool)args[1])); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -176,7 +176,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2((string)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -328,7 +328,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + return new global::System.Threading.Tasks.ValueTask(instance.MatrixMethod_WithEnumParameter_UsesOnlyMethodValues((bool)args[0], global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt index a6f83123c8..d88983e6d4 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt @@ -112,7 +112,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "condition", ne { case 1: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Test2((bool)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt index a6f83123c8..d88983e6d4 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt @@ -112,7 +112,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "condition", ne { case 1: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Test2((bool)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt index a6f83123c8..b538655288 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt @@ -1,4 +1,4 @@ -// +// #pragma warning disable #nullable enable @@ -112,7 +112,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "condition", ne { case 1: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])); + instance.Test2((bool)args[0]); return default(global::System.Threading.Tasks.ValueTask); } default: From 7c872fe1f353828a967a6bf613d3c47c985bf9af Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 2 May 2026 15:50:53 +0100 Subject: [PATCH 28/30] fix(sourcegen): avoid direct decimal argument casts --- .../DecimalArgumentTests.Test.verified.txt | 14 +++++++------- .../Helpers/CastExpressionHelper.cs | 19 +++++++++++++++++++ ...ary_Has_No_API_Changes.Net4_7.verified.txt | 6 ------ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt index 310088b6ee..1654b3c7af 100644 --- a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt @@ -110,7 +110,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.SimpleDecimal((decimal)(double)args[0])); + return new global::System.Threading.Tasks.ValueTask(instance.SimpleDecimal(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -129,7 +129,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.SmallDecimal((decimal)(double)args[0])); + return new global::System.Threading.Tasks.ValueTask(instance.SmallDecimal(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -205,7 +205,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 3: { - return new global::System.Threading.Tasks.ValueTask(instance.MultipleDecimals((decimal)(double)args[0], (decimal)(double)args[1], (decimal)(double)args[2])); + return new global::System.Threading.Tasks.ValueTask(instance.MultipleDecimals(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]))); } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); @@ -244,7 +244,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 6: { - instance.TransactionDiscountCalculations((decimal)(int)args[0], (decimal)(int)args[1], (decimal)(int)args[2], (decimal)(int)args[3], (decimal)(int)args[4], (bool)args[5]); + instance.TransactionDiscountCalculations(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), (bool)args[5]); return default(global::System.Threading.Tasks.ValueTask); } default: @@ -264,7 +264,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Equality3((decimal)(double)args[0])); + return new global::System.Threading.Tasks.ValueTask(instance.Equality3(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -283,7 +283,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.Equality4((decimal)(double)args[0])); + return new global::System.Threading.Tasks.ValueTask(instance.Equality4(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); @@ -302,7 +302,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.TestMethod((decimal)(int)args[0])); + return new global::System.Threading.Tasks.ValueTask(instance.TestMethod(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index fe2534ce52..7b51601e23 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -153,6 +153,11 @@ public static string GenerateCast( return $"({targetGQ}){argsExpression}"; } + if (IsDecimalLike(targetType) && !IsDecimalLike(sourceType)) + { + return $"global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>({argsExpression})"; + } + // Check if compiler can resolve conversion var conversion = compilation.ClassifyConversion(sourceType, targetType); @@ -176,4 +181,18 @@ or SpecialType.System_ValueType // No known conversion → CastHelper fallback return $"global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>({argsExpression})"; } + + private static bool IsDecimalLike(ITypeSymbol type) + { + if (type.SpecialType == SpecialType.System_Decimal) + { + return true; + } + + return type is INamedTypeSymbol + { + ConstructedFrom.SpecialType: SpecialType.System_Nullable_T, + TypeArguments: [{ SpecialType: SpecialType.System_Decimal }] + }; + } } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index 10c282cff4..8b9183194a 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -3360,9 +3360,6 @@ namespace .Extensions { public static . IsEqualTo(this . source, TValue? expected, [.("expected")] string? expectedExpression = null) { } public static . IsEqualTo(this . source, TValue? expected, . comparer, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null) { } - [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + - "efined operators.")] - [.(-1)] public static . IsEqualTo(this . source, TOther? expected, [.("expected")] string? expectedExpression = null) { } } public static class EquatableAssertionExtensions @@ -4192,9 +4189,6 @@ namespace .Extensions public static class NotEqualsAssertionExtensions { public static . IsNotEqualTo(this . source, TValue notExpected, .? comparer = null, [.("notExpected")] string? notExpectedExpression = null, [.("comparer")] string? comparerExpression = null) { } - [.("Looks up implicit conversion operators via reflection. Trimming may remove user-d" + - "efined operators.")] - [.(-1)] public static . IsNotEqualTo(this . source, TOther? notExpected, [.("notExpected")] string? notExpectedExpression = null) { } } public static class NotEquivalentToAssertionExtensions From 4c3e6b6e44c28fbf449a08f25c3ec7612d324838 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 28 May 2026 22:17:55 +0100 Subject: [PATCH 29/30] fix: route floating-point->decimal through CastHelper in multi-source casts GenerateMultiSourceCast emitted a direct (decimal)__s cast for double/float source arms, losing precision (e.g. 0.1d/0.1f). Now uses CastHelper like the single-source path. Extracted shared NeedsDecimalCastHelper predicate. --- .../DecimalArgumentTests.Test.verified.txt | 4 ++-- .../CodeGenerators/Helpers/CastExpressionHelper.cs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt index 1654b3c7af..bc50940aac 100644 --- a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt @@ -91,7 +91,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 2: { - return new global::System.Threading.Tasks.ValueTask(instance.Transfer((args[0] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }), (args[1] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.Transfer((args[0] switch { double __s => global::TUnit.Core.Helpers.CastHelper.Cast(__s), int __s => global::TUnit.Core.Helpers.CastHelper.Cast(__s), _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }), (args[1] switch { double __s => global::TUnit.Core.Helpers.CastHelper.Cast(__s), int __s => global::TUnit.Core.Helpers.CastHelper.Cast(__s), _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) }))); } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); @@ -224,7 +224,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(decimal), "batchingSiz { case 1: { - instance.Test((args[0] switch { double __s => (decimal)__s, int __s => (decimal)__s, _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + instance.Test((args[0] switch { double __s => global::TUnit.Core.Helpers.CastHelper.Cast(__s), int __s => global::TUnit.Core.Helpers.CastHelper.Cast(__s), _ => global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); return default(global::System.Threading.Tasks.ValueTask); } default: diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs index 7b51601e23..411a56b85c 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/CastExpressionHelper.cs @@ -70,6 +70,12 @@ public static string GenerateMultiSourceCast( { arms.Add($"{sourceGQ} __s => __s"); } + else if (NeedsDecimalCastHelper(sourceType, targetType)) + { + // A direct (decimal)__s cast from a binary floating-point source loses precision + // (e.g. 0.1d/0.1f are not exactly representable); route through CastHelper instead. + arms.Add($"{sourceGQ} __s => global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>(__s)"); + } else if (compilation != null && compilation.ClassifyConversion(sourceType, targetType) is { IsImplicit: true } or { IsExplicit: true }) { arms.Add($"{sourceGQ} __s => ({targetGQ})__s"); @@ -153,7 +159,7 @@ public static string GenerateCast( return $"({targetGQ}){argsExpression}"; } - if (IsDecimalLike(targetType) && !IsDecimalLike(sourceType)) + if (NeedsDecimalCastHelper(sourceType, targetType)) { return $"global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>({argsExpression})"; } @@ -182,6 +188,12 @@ or SpecialType.System_ValueType return $"global::TUnit.Core.Helpers.CastHelper.Cast<{targetGQ}>({argsExpression})"; } + // A decimal target fed from a non-decimal source must go through CastHelper rather than a direct + // (decimal) cast: binary floating-point literals (double, float) lose precision when cast directly, + // and attribute-stored numeric literals (e.g. [Arguments(1.5)] storing a double) need the same care. + private static bool NeedsDecimalCastHelper(ITypeSymbol sourceType, ITypeSymbol targetType) + => IsDecimalLike(targetType) && !IsDecimalLike(sourceType); + private static bool IsDecimalLike(ITypeSymbol type) { if (type.SpecialType == SpecialType.System_Decimal) From 1fdca5e917312f920a58ba27168438841471897b Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 31 May 2026 19:25:00 +0100 Subject: [PATCH 30/30] test: cover float->decimal argument binding routes through CastHelper Existing decimal-arg tests only used double literals. Add a float-source case to lock in that NeedsDecimalCastHelper routes float->decimal away from a direct (decimal)(float) cast. Addresses PR #5529 review note. --- TUnit.TestProject/DecimalArgumentTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/TUnit.TestProject/DecimalArgumentTests.cs b/TUnit.TestProject/DecimalArgumentTests.cs index ceb9d8be36..ab9608e6bf 100644 --- a/TUnit.TestProject/DecimalArgumentTests.cs +++ b/TUnit.TestProject/DecimalArgumentTests.cs @@ -114,6 +114,14 @@ public async Task Equality4(decimal credit) await Assert.That(credit).IsEqualTo(123_999.00000000000000001m); } + [Test] + [Arguments(1.5f)] // float literal source -> decimal parameter must route through CastHelper, + // not a direct (decimal)(float) cast (which loses precision for many values) + public async Task FloatToDecimal(decimal value) + { + await Assert.That(value).IsEqualTo(1.5m); + } + private const int BatchSize = 42; [Test]