Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static void WriteAttributes(ICodeWriter sourceCodeWriter, Compilation com
ImmutableArray<AttributeData> attributeDatas)
{
var attributesToWrite = new List<AttributeData>();

// Filter out attributes that we can write
foreach (var attributeData in attributeDatas)
{
Expand All @@ -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);
}
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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!,");
Expand Down Expand Up @@ -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);

Expand All @@ -240,15 +257,15 @@ 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;
}

sourceCodeWriter.AppendLine();
sourceCodeWriter.Append("{");

if (hasNamedArgs)
{
sourceCodeWriter.Append($"{formattedNamedArgs}");
Expand All @@ -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("}");
}

Expand All @@ -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!,");
Expand All @@ -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
Expand All @@ -344,7 +361,7 @@ private static bool ShouldSkipFrameworkSpecificAttribute(Compilation compilation

return false;
}

private static bool IsNullableAttribute(string fullyQualifiedName)
{
return fullyQualifiedName.Contains("NullableAttribute") ||
Expand Down
23 changes: 23 additions & 0 deletions TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,29 @@ public static string GloballyQualified(this ISymbol typeSymbol)

return typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedGenericWithGlobalPrefix);
}

/// <summary>
/// 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.
/// </summary>
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: <BaseAsyncTest>d__0, <>c__DisplayClass0_0, <>f__AnonymousType0
var typeName = typeSymbol.Name;

// Compiler-generated types typically:
// 1. Start with '<' (like <MethodName>d__0 for async state machines)
// 2. Contain '<>' (like <>c for compiler-generated classes)
// This won't match normal generic types like List<T> 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);
Expand Down
41 changes: 41 additions & 0 deletions TUnit.TestProject.Library/AsyncBaseTests.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
23 changes: 23 additions & 0 deletions TUnit.TestProject/AsyncInheritedTestsRepro.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading