diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs index ba95f94019..c367d458a0 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs @@ -12,7 +12,7 @@ public static void WriteAttributes(ICodeWriter sourceCodeWriter, Compilation com ImmutableArray attributeDatas) { var attributesToWrite = new List(); - + // Filter out attributes that we can write foreach (var attributeData in attributeDatas) { @@ -26,7 +26,15 @@ public static void WriteAttributes(ICodeWriter sourceCodeWriter, Compilation com { continue; } - + + // Skip attributes with compiler-generated type arguments + if (attributeData.ConstructorArguments.Any(arg => + arg is { Kind: TypedConstantKind.Type, Value: ITypeSymbol typeSymbol } && + typeSymbol.IsCompilerGeneratedType())) + { + continue; + } + attributesToWrite.Add(attributeData); } } @@ -49,7 +57,7 @@ public static void WriteAttribute(ICodeWriter sourceCodeWriter, Compilation comp { if (attributeData.ApplicationSyntaxReference is null) { - // For attributes from other assemblies (like inherited methods), + // For attributes from other assemblies (like inherited methods), // use the WriteAttributeWithoutSyntax approach WriteAttributeWithoutSyntax(sourceCodeWriter, attributeData); } @@ -194,7 +202,7 @@ private static void WriteDataSourceGeneratorProperties(ICodeWriter sourceCodeWri var propertyType = propertySymbol.Type.GloballyQualified(); var isNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated; - + if (propertySymbol.Type.IsReferenceType && !isNullable) { sourceCodeWriter.Append("null!,"); @@ -229,6 +237,15 @@ public static void WriteAttributeWithoutSyntax(ICodeWriter sourceCodeWriter, Att { var attributeName = attributeData.AttributeClass!.GloballyQualified(); + // Skip if any constructor arguments contain compiler-generated types + if (attributeData.ConstructorArguments.Any(arg => + arg.Kind == TypedConstantKind.Type && + arg.Value is ITypeSymbol typeSymbol && + typeSymbol.IsCompilerGeneratedType())) + { + return; + } + var constructorArgs = attributeData.ConstructorArguments.Select(TypedConstantParser.GetRawTypedConstantValue); var formattedConstructorArgs = string.Join(", ", constructorArgs); @@ -240,7 +257,7 @@ public static void WriteAttributeWithoutSyntax(ICodeWriter sourceCodeWriter, Att // Check if we need to add properties (named arguments or data generator properties) var hasNamedArgs = !string.IsNullOrEmpty(formattedNamedArgs); var hasDataGeneratorProperties = HasNestedDataGeneratorProperties(attributeData); - + if (!hasNamedArgs && !hasDataGeneratorProperties) { return; @@ -248,7 +265,7 @@ public static void WriteAttributeWithoutSyntax(ICodeWriter sourceCodeWriter, Att sourceCodeWriter.AppendLine(); sourceCodeWriter.Append("{"); - + if (hasNamedArgs) { sourceCodeWriter.Append($"{formattedNamedArgs}"); @@ -257,14 +274,14 @@ public static void WriteAttributeWithoutSyntax(ICodeWriter sourceCodeWriter, Att sourceCodeWriter.Append(","); } } - + if (hasDataGeneratorProperties) { // For attributes without syntax, we still need to handle data generator properties // but we can't rely on syntax analysis, so we'll use a simpler approach WriteDataSourceGeneratorPropertiesWithoutSyntax(sourceCodeWriter, attributeData); } - + sourceCodeWriter.Append("}"); } @@ -286,7 +303,7 @@ private static void WriteDataSourceGeneratorPropertiesWithoutSyntax(ICodeWriter var propertyType = propertySymbol.Type.GloballyQualified(); var isNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated; - + if (propertySymbol.Type.IsReferenceType && !isNullable) { sourceCodeWriter.Append("null!,"); @@ -312,29 +329,29 @@ private static bool ShouldSkipFrameworkSpecificAttribute(Compilation compilation // Generic approach: Check if the attribute type is actually available in the target compilation // This works by seeing if we can resolve the type from the compilation's references var fullyQualifiedName = attributeData.AttributeClass.ToDisplayString(); - + // Check if this is a system/runtime attribute that might not exist on all frameworks if (fullyQualifiedName.StartsWith("System.") || fullyQualifiedName.StartsWith("Microsoft.")) { // Try to get the type from the compilation // If it doesn't exist in the compilation's references, we should skip it var typeSymbol = compilation.GetTypeByMetadataName(fullyQualifiedName); - + // If the type doesn't exist in the compilation, skip it if (typeSymbol == null) { return true; } - + // Special handling for attributes that exist but may not be usable // For example, nullable attributes exist in the reference assemblies but not at runtime for .NET Framework if (IsNullableAttribute(fullyQualifiedName)) { // Check if we're targeting .NET Framework by looking at references - var isNetFramework = compilation.References.Any(r => - r.Display?.Contains("mscorlib") == true && + var isNetFramework = compilation.References.Any(r => + r.Display?.Contains("mscorlib") == true && !r.Display.Contains("System.Runtime")); - + if (isNetFramework) { return true; // Skip nullable attributes on .NET Framework @@ -344,7 +361,7 @@ private static bool ShouldSkipFrameworkSpecificAttribute(Compilation compilation return false; } - + private static bool IsNullableAttribute(string fullyQualifiedName) { return fullyQualifiedName.Contains("NullableAttribute") || diff --git a/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs b/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs index ac59669abc..5014c65a26 100644 --- a/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs +++ b/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs @@ -223,6 +223,29 @@ public static string GloballyQualified(this ISymbol typeSymbol) return typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedGenericWithGlobalPrefix); } + + /// + /// Determines if a type is compiler-generated (e.g., async state machines, lambda closures). + /// These types typically contain angle brackets in their names and cannot be represented in source code. + /// + public static bool IsCompilerGeneratedType(this ITypeSymbol? typeSymbol) + { + if (typeSymbol == null) + { + return false; + } + + // Check the type name directly, not the display string + // Compiler-generated types have names that start with '<' or contain '<>' + // Examples: d__0, <>c__DisplayClass0_0, <>f__AnonymousType0 + var typeName = typeSymbol.Name; + + // Compiler-generated types typically: + // 1. Start with '<' (like d__0 for async state machines) + // 2. Contain '<>' (like <>c for compiler-generated classes) + // This won't match normal generic types like List because those don't have '<' in the type name itself + return typeName.StartsWith("<") || typeName.Contains("<>"); + } public static string GloballyQualifiedNonGeneric(this ISymbol typeSymbol) => typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedNonGenericWithGlobalPrefix); diff --git a/TUnit.TestProject.Library/AsyncBaseTests.cs b/TUnit.TestProject.Library/AsyncBaseTests.cs new file mode 100644 index 0000000000..05c2e6bb1e --- /dev/null +++ b/TUnit.TestProject.Library/AsyncBaseTests.cs @@ -0,0 +1,41 @@ +namespace TUnit.TestProject.Library; + +// Base class with async test methods in a different assembly +public abstract class AsyncBaseTests +{ + [Test] + public async Task BaseAsyncTest() + { + await Task.Delay(1); + Console.WriteLine("Base async test executed"); + } + + [Test] + public async Task BaseAsyncTestWithReturn() + { + await Task.Delay(1); + Console.WriteLine("Base async test with return executed"); + } + + [Test] + [Arguments("test1")] + [Arguments("test2")] + public async Task BaseAsyncTestWithArguments(string value) + { + await Task.Delay(1); + Console.WriteLine($"Base async test with argument: {value}"); + } + + [Test] + public void BaseSyncTest() + { + Console.WriteLine("Base sync test executed"); + } + + [Test] + public async Task BaseAsyncTestWithCancellation(CancellationToken cancellationToken) + { + await Task.Delay(1, cancellationToken); + Console.WriteLine("Base async test with cancellation token executed"); + } +} \ No newline at end of file diff --git a/TUnit.TestProject/AsyncInheritedTestsRepro.cs b/TUnit.TestProject/AsyncInheritedTestsRepro.cs new file mode 100644 index 0000000000..16bd0811d4 --- /dev/null +++ b/TUnit.TestProject/AsyncInheritedTestsRepro.cs @@ -0,0 +1,23 @@ +using TUnit.TestProject.Attributes; +using TUnit.TestProject.Library; + +namespace TUnit.TestProject; + +// Derived class that inherits the async tests from a different assembly +[EngineTest(ExpectedResult.Pass)] +[InheritsTests] +public class AsyncInheritedTestsRepro : AsyncBaseTests +{ + [Test] + public async Task DerivedAsyncTest() + { + await Task.Delay(1); + Console.WriteLine("Derived async test executed"); + } + + [Test] + public void DerivedSyncTest() + { + Console.WriteLine("Derived sync test executed"); + } +} \ No newline at end of file