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 @@ -38,13 +38,159 @@
}
GenerateIndividualPropertyInjectionSource(context, classData);
});

// Also generate property sources for closed generic types used in data source attributes
// This ensures AOT compatibility for types like ErrFixture<MyType>
var closedGenericTypesFromDataSources = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => IsClassWithDataSourceProperties(node),
transform: (ctx, _) => GetClosedGenericTypesFromDataSources(ctx))
.Where(x => x != null)
.SelectMany((types, _) => types ?? [])
.Collect()
.SelectMany((classes, _) => classes.DistinctBy(c => c.ClassSymbol, SymbolEqualityComparer.Default))
.Combine(enabledProvider);

context.RegisterSourceOutput(closedGenericTypesFromDataSources, (context, data) =>
{
var (classData, isEnabled) = data;
if (!isEnabled)
{
return;
}
GenerateIndividualPropertyInjectionSource(context, classData);
});
}

private static bool IsClassWithDataSourceProperties(SyntaxNode node)
{
return node is TypeDeclarationSyntax;
}

/// <summary>
/// Extracts closed generic types from data source attributes (like ClassDataSource&lt;ErrFixture&lt;MyType&gt;&gt;)
/// that have injectable properties. This enables AOT-compatible property injection for generic types.
/// </summary>
private static IEnumerable<ClassWithDataSourceProperties>? GetClosedGenericTypesFromDataSources(GeneratorSyntaxContext context)
{
var typeDecl = (TypeDeclarationSyntax)context.Node;
var semanticModel = context.SemanticModel;

if (semanticModel.GetDeclaredSymbol(typeDecl) is not INamedTypeSymbol typeSymbol)
{
return null;
}

var dataSourceInterface = semanticModel.Compilation.GetTypeByMetadataName("TUnit.Core.IDataSourceAttribute");
if (dataSourceInterface == null)
{
return null;
}

var closedGenericTypes = new List<ClassWithDataSourceProperties>();
var processedTypes = new HashSet<string>();

// Check all properties for data source attributes that reference closed generic types
var allProperties = typeSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Concat(typeSymbol.GetMembersIncludingBase().OfType<IPropertySymbol>());

foreach (var property in allProperties)
{
foreach (var attr in property.GetAttributes())
{
if (attr.AttributeClass == null ||
!attr.AttributeClass.AllInterfaces.Contains(dataSourceInterface, SymbolEqualityComparer.Default))
{
continue;
}

// Check if the attribute is a generic type like ClassDataSource<T>
if (attr.AttributeClass.IsGenericType && attr.AttributeClass.TypeArguments.Length > 0)
{
foreach (var typeArg in attr.AttributeClass.TypeArguments)
{
if (typeArg is INamedTypeSymbol namedTypeArg && namedTypeArg.IsGenericType &&
!namedTypeArg.IsUnboundGenericType && namedTypeArg.TypeParameters.Length == 0)
{
// This is a closed generic type (e.g., ErrFixture<MyType>)
var fullName = namedTypeArg.ToDisplayString();
if (!processedTypes.Add(fullName))
{
continue;
}

// Check if this type has properties with data source attributes
var classData = GetClassWithDataSourcePropertiesForType(namedTypeArg, semanticModel, dataSourceInterface);
if (classData != null)
{
closedGenericTypes.Add(classData);
}
}
}
}
}
}

return closedGenericTypes.Count > 0 ? closedGenericTypes : null;
}

/// <summary>
/// Creates a ClassWithDataSourceProperties for a specific type (used for closed generic types).
/// </summary>
private static ClassWithDataSourceProperties? GetClassWithDataSourcePropertiesForType(
INamedTypeSymbol typeSymbol,
SemanticModel semanticModel,
INamedTypeSymbol dataSourceInterface)
{
// Skip types that are not publicly accessible
if (!IsPubliclyAccessible(typeSymbol))
{
return null;
}

var propertiesWithDataSources = new List<PropertyWithDataSourceAttribute>();
var processedProperties = new HashSet<string>();

var allProperties = typeSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(CanSetProperty)
.ToList();

foreach (var property in allProperties)
{
if (!processedProperties.Add(property.Name))
{
continue;
}

foreach (var attr in property.GetAttributes())
{
if (attr.AttributeClass != null &&
attr.AttributeClass.AllInterfaces.Contains(dataSourceInterface, SymbolEqualityComparer.Default))
{
propertiesWithDataSources.Add(new PropertyWithDataSourceAttribute
{
Property = property,
DataSourceAttribute = attr
});
break;
}
}
}

if (propertiesWithDataSources.Count == 0)
{
return null;
}

return new ClassWithDataSourceProperties
{
ClassSymbol = typeSymbol,
Properties = propertiesWithDataSources.ToImmutableArray()
};
}

private static ClassWithDataSourceProperties? GetClassWithDataSourceProperties(GeneratorSyntaxContext context)
{
var typeDecl = (TypeDeclarationSyntax)context.Node;
Expand Down Expand Up @@ -89,7 +235,7 @@
var inheritedProperties = typeSymbol.GetMembersIncludingBase()
.OfType<IPropertySymbol>()
.Where(CanSetProperty)
.Where(p => p.ContainingType != typeSymbol)

Check warning on line 238 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Verwenden von "SymbolEqualityComparer" beim Vergleichen von Symbolen
.ToList();

foreach (var property in directProperties)
Expand Down
30 changes: 19 additions & 11 deletions TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -937,15 +937,23 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu

// Generate the attribute with factory
// We need to manually construct this to properly add the Factory property
var attrClass = attr.AttributeClass!;
var attrTypeName = attrClass.GloballyQualified();

// Determine if the data source is static or instance-based
var isStatic = dataSourceMethod?.IsStatic ?? dataSourceProperty?.GetMethod?.IsStatic ?? true;

// Use InstanceMethodDataSourceAttribute for instance-based data sources
// This implements IAccessesInstanceData which tells the engine to create an instance early
var attrTypeName = isStatic
? "global::TUnit.Core.MethodDataSourceAttribute"
: "global::TUnit.Core.InstanceMethodDataSourceAttribute";

if (attr.ConstructorArguments is
[
{ Value: ITypeSymbol typeArg } _, _, ..
])
{
// MethodDataSource(Type, string) constructor
// MethodDataSource(Type, string) constructor - only available on MethodDataSourceAttribute
// For instance data sources, we still use the same constructor signature
writer.AppendLine($"new {attrTypeName}(typeof({typeArg.GloballyQualified()}), \"{methodName}\")");
}
else
Expand Down Expand Up @@ -1048,7 +1056,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy
writer.AppendLine("else");
writer.AppendLine("{");
writer.Indent();
InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance");
writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");");
writer.Unindent();
writer.AppendLine("}");
writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};");
Expand Down Expand Up @@ -1080,7 +1088,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy
writer.AppendLine("else");
writer.AppendLine("{");
writer.Indent();
InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance");
writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");");
writer.Unindent();
writer.AppendLine("}");
writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};");
Expand Down Expand Up @@ -1124,7 +1132,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy
writer.AppendLine("else");
writer.AppendLine("{");
writer.Indent();
InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance");
writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");");
writer.Unindent();
writer.AppendLine("}");
writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};");
Expand Down Expand Up @@ -1167,7 +1175,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy
writer.AppendLine("else");
writer.AppendLine("{");
writer.Indent();
InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance");
writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");");
writer.Unindent();
writer.AppendLine("}");
writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};");
Expand Down Expand Up @@ -1217,7 +1225,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper
writer.AppendLine("else");
writer.AppendLine("{");
writer.Indent();
InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance");
writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");");
writer.Unindent();
writer.AppendLine("}");
writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};");
Expand Down Expand Up @@ -1249,7 +1257,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper
writer.AppendLine("else");
writer.AppendLine("{");
writer.Indent();
InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance");
writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");");
writer.Unindent();
writer.AppendLine("}");
writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};");
Expand Down Expand Up @@ -1293,7 +1301,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper
writer.AppendLine("else");
writer.AppendLine("{");
writer.Indent();
InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance");
writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");");
writer.Unindent();
writer.AppendLine("}");
writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};");
Expand Down Expand Up @@ -1336,7 +1344,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper
writer.AppendLine("else");
writer.AppendLine("{");
writer.Indent();
InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance");
writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");");
writer.Unindent();
writer.AppendLine("}");
writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};");
Expand Down
Loading
Loading