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
104 changes: 5 additions & 99 deletions TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ public static string GetAttributeObjectInitializer(Compilation compilation,

sourceCodeWriter.Append($"new {attributeName}({formattedConstructorArgs})");

if (formattedProperties.Length == 0
&& !HasNestedDataGeneratorProperties(attributeData))
// Only add object initializer if we have regular properties to set
// Don't include data source properties - they'll be handled by property injection
if (formattedProperties.Length == 0)
{
return sourceCodeWriter.ToString();
}
Expand All @@ -162,61 +163,11 @@ public static string GetAttributeObjectInitializer(Compilation compilation,
sourceCodeWriter.Append($"{property},");
}

WriteDataSourceGeneratorProperties(sourceCodeWriter, compilation, attributeData);

sourceCodeWriter.Append("}");

return sourceCodeWriter.ToString();
}

private static bool HasNestedDataGeneratorProperties(AttributeData attributeData)
{
if (attributeData.AttributeClass is not { } attributeClass)
{
return false;
}

if (attributeClass.GetMembersIncludingBase().OfType<IPropertySymbol>().Any(x => x.GetAttributes().Any(a => a.IsDataSourceAttribute())))
{
return true;
}

return false;
}

private static void WriteDataSourceGeneratorProperties(ICodeWriter sourceCodeWriter, Compilation compilation, AttributeData attributeData)
{
foreach (var propertySymbol in attributeData.AttributeClass?.GetMembers().OfType<IPropertySymbol>() ?? [])
{
if (propertySymbol.DeclaredAccessibility != Accessibility.Public)
{
continue;
}

if (propertySymbol.GetAttributes().FirstOrDefault(x => x.IsDataSourceAttribute()) is not { } dataSourceAttribute)
{
continue;
}

sourceCodeWriter.Append($"{propertySymbol.Name} = ");

var propertyType = propertySymbol.Type.GloballyQualified();
var isNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated;

if (propertySymbol.Type.IsReferenceType && !isNullable)
{
sourceCodeWriter.Append("null!,");
}
else if (propertySymbol.Type.IsValueType && !isNullable)
{
sourceCodeWriter.Append($"default({propertyType}),");
}
else
{
sourceCodeWriter.Append("null,");
}
}
}

private static string FormatConstructorArgument(Compilation compilation, AttributeArgumentSyntax attributeArgumentSyntax)
{
Expand Down Expand Up @@ -253,11 +204,10 @@ public static void WriteAttributeWithoutSyntax(ICodeWriter sourceCodeWriter, Att

sourceCodeWriter.Append($"new {attributeName}({formattedConstructorArgs})");

// Check if we need to add properties (named arguments or data generator properties)
// Check if we need to add properties (named arguments only, not data source properties)
var hasNamedArgs = !string.IsNullOrEmpty(formattedNamedArgs);
var hasDataGeneratorProperties = HasNestedDataGeneratorProperties(attributeData);

if (!hasNamedArgs && !hasDataGeneratorProperties)
if (!hasNamedArgs)
{
return;
}
Expand All @@ -268,55 +218,11 @@ public static void WriteAttributeWithoutSyntax(ICodeWriter sourceCodeWriter, Att
if (hasNamedArgs)
{
sourceCodeWriter.Append($"{formattedNamedArgs}");
if (hasDataGeneratorProperties)
{
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("}");
}

private static void WriteDataSourceGeneratorPropertiesWithoutSyntax(ICodeWriter sourceCodeWriter, AttributeData attributeData)
{
foreach (var propertySymbol in attributeData.AttributeClass?.GetMembers().OfType<IPropertySymbol>() ?? [])
{
if (propertySymbol.DeclaredAccessibility != Accessibility.Public)
{
continue;
}

if (propertySymbol.GetAttributes().FirstOrDefault(x => x.IsDataSourceAttribute()) is not { } dataSourceAttribute)
{
continue;
}

sourceCodeWriter.Append($"{propertySymbol.Name} = ");

var propertyType = propertySymbol.Type.GloballyQualified();
var isNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated;

if (propertySymbol.Type.IsReferenceType && !isNullable)
{
sourceCodeWriter.Append("null!,");
}
else if (propertySymbol.Type.IsValueType && !isNullable)
{
sourceCodeWriter.Append($"default({propertyType}),");
}
else
{
sourceCodeWriter.Append("null,");
}
}
}

private static bool ShouldSkipFrameworkSpecificAttribute(Compilation compilation, AttributeData attributeData)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ private static void GeneratePropertyMetadata(StringBuilder sb, PropertyWithDataS
var propertyType = propInfo.Property.Type.ToDisplayString();
var propertyTypeForTypeof = GetNonNullableTypeString(propInfo.Property.Type);
var attributeTypeName = propInfo.DataSourceAttribute.AttributeClass!.ToDisplayString();
var attributeClass = propInfo.DataSourceAttribute.AttributeClass!;

sb.AppendLine(" yield return new PropertyInjectionMetadata");
sb.AppendLine(" {");
Expand All @@ -272,7 +273,7 @@ private static void GeneratePropertyMetadata(StringBuilder sb, PropertyWithDataS
// Generate CreateDataSource delegate
sb.AppendLine(" CreateDataSource = () =>");
sb.AppendLine(" {");
GenerateDataSourceCreation(sb, propInfo.DataSourceAttribute, attributeTypeName);
GenerateDataSourceCreation(sb, propInfo.DataSourceAttribute, attributeTypeName, attributeClass);
sb.AppendLine(" },");

// Generate SetProperty delegate
Expand All @@ -286,10 +287,14 @@ private static void GeneratePropertyMetadata(StringBuilder sb, PropertyWithDataS
sb.AppendLine();
}

private static void GenerateDataSourceCreation(StringBuilder sb, AttributeData attributeData, string attributeTypeName)
private static void GenerateDataSourceCreation(StringBuilder sb, AttributeData attributeData, string attributeTypeName, INamedTypeSymbol attributeClass)
{
var constructorArgs = string.Join(", ", attributeData.ConstructorArguments.Select(FormatTypedConstant));

// Check if this is a custom data source that might have its own properties needing injection
// We identify custom data sources as those that inherit from DataSourceGeneratorAttribute or AsyncDataSourceGeneratorAttribute
var isCustomDataSource = IsCustomDataSource(attributeClass);

sb.AppendLine($" var dataSource = new {attributeTypeName}({constructorArgs});");

foreach (var namedArg in attributeData.NamedArguments)
Expand All @@ -298,8 +303,16 @@ private static void GenerateDataSourceCreation(StringBuilder sb, AttributeData a
sb.AppendLine($" dataSource.{namedArg.Key} = {value};");
}

// For custom data sources, we don't initialize them here - that will be handled by DataSourceInitializer
// which is called by PropertyDataResolver.GetInitializedDataSourceAsync
sb.AppendLine(" return dataSource;");
}

private static bool IsCustomDataSource(INamedTypeSymbol attributeClass)
{
// Check if this class implements IDataSourceAttribute
return attributeClass.AllInterfaces.Any(i => i.Name == "IDataSourceAttribute");
}

private static void GeneratePropertySetting(StringBuilder sb, PropertyWithDataSourceAttribute propInfo, string propertyType, string instanceVariableName, string classTypeName)
{
Expand Down
73 changes: 13 additions & 60 deletions TUnit.Core/Attributes/TestData/AsyncDataSourceGeneratorAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using TUnit.Core.Extensions;

namespace TUnit.Core;

Expand All @@ -8,20 +7,10 @@ public abstract class AsyncDataSourceGeneratorAttribute<[DynamicallyAccessedMemb
{
protected abstract IAsyncEnumerable<Func<Task<T>>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata);

public override async IAsyncEnumerable<Func<Task<T>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
public sealed override async IAsyncEnumerable<Func<Task<T>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
// Inject properties into the data source attribute itself if we have context
// This is needed for custom data sources that have their own data source properties
if (dataGeneratorMetadata is { TestInformation: not null })
{
await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this,
dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag,
dataGeneratorMetadata.TestInformation,
dataGeneratorMetadata.TestBuilderContext.Current.Events);
}

await ObjectInitializer.InitializeAsync(this);

// Data source initialization is now handled externally by DataSourceInitializer
// This follows SRP - the attribute is only responsible for generating data, not initialization
await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata))
{
yield return generateDataSource;
Expand All @@ -38,19 +27,10 @@ public abstract class AsyncDataSourceGeneratorAttribute<
{
protected abstract IAsyncEnumerable<Func<Task<(T1, T2)>>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata);

public override async IAsyncEnumerable<Func<Task<(T1, T2)>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
public sealed override async IAsyncEnumerable<Func<Task<(T1, T2)>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
// Inject properties into the data source attribute itself if we have context
if (dataGeneratorMetadata is { TestInformation: not null })
{
await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this,
dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag,
dataGeneratorMetadata.TestInformation,
dataGeneratorMetadata.TestBuilderContext.Current.Events);
}

await ObjectInitializer.InitializeAsync(this);

// Data source initialization is now handled externally by DataSourceInitializer
// This follows SRP - the attribute is only responsible for generating data, not initialization
await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata))
{
yield return generateDataSource;
Expand All @@ -69,19 +49,10 @@ public abstract class AsyncDataSourceGeneratorAttribute<
{
protected abstract IAsyncEnumerable<Func<Task<(T1, T2, T3)>>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata);

public override async IAsyncEnumerable<Func<Task<(T1, T2, T3)>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
public sealed override async IAsyncEnumerable<Func<Task<(T1, T2, T3)>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
// Inject properties into the data source attribute itself if we have context
if (dataGeneratorMetadata is { TestInformation: not null })
{
await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this,
dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag,
dataGeneratorMetadata.TestInformation,
dataGeneratorMetadata.TestBuilderContext.Current.Events);
}

await ObjectInitializer.InitializeAsync(this);

// Data source initialization is now handled externally by DataSourceInitializer
// This follows SRP - the attribute is only responsible for generating data, not initialization
await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata))
{
yield return generateDataSource;
Expand All @@ -104,17 +75,8 @@ public abstract class AsyncDataSourceGeneratorAttribute<

public override async IAsyncEnumerable<Func<Task<(T1, T2, T3, T4)>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
// Inject properties into the data source attribute itself if we have context
if (dataGeneratorMetadata is { TestInformation: not null })
{
await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this,
dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag,
dataGeneratorMetadata.TestInformation,
dataGeneratorMetadata.TestBuilderContext.Current.Events);
}

await ObjectInitializer.InitializeAsync(this);

// Data source initialization is now handled externally by DataSourceInitializer
// This follows SRP - the attribute is only responsible for generating data, not initialization
await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata))
{
yield return generateDataSource;
Expand All @@ -139,17 +101,8 @@ public abstract class AsyncDataSourceGeneratorAttribute<

public override async IAsyncEnumerable<Func<Task<(T1, T2, T3, T4, T5)>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
// Inject properties into the data source attribute itself if we have context
if (dataGeneratorMetadata is { TestInformation: not null })
{
await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this,
dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag,
dataGeneratorMetadata.TestInformation,
dataGeneratorMetadata.TestBuilderContext.Current.Events);
}

await ObjectInitializer.InitializeAsync(this);

// Data source initialization is now handled externally by DataSourceInitializer
// This follows SRP - the attribute is only responsible for generating data, not initialization
await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata))
{
yield return generateDataSource;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,8 @@ public abstract class AsyncUntypedDataSourceGeneratorAttribute : Attribute, IAsy

public async IAsyncEnumerable<Func<Task<object?[]?>>> GenerateAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
if (dataGeneratorMetadata is { TestInformation: not null })
{
await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this, dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, dataGeneratorMetadata.TestInformation, dataGeneratorMetadata.TestBuilderContext.Current.Events);
}

await ObjectInitializer.InitializeAsync(this);

// Data source initialization is now handled externally by DataSourceInitializer
// This follows SRP - the attribute is only responsible for generating data, not initialization
await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata))
{
yield return generateDataSource;
Expand Down
13 changes: 7 additions & 6 deletions TUnit.Core/Data/ThreadSafeDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ namespace TUnit.Core.Data;
#if !DEBUG
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
#endif
public class ThreadSafeDictionary<TKey,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TValue>
public class ThreadSafeDictionary<TKey,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TValue>
where TKey : notnull
{
// Using Lazy<TValue> ensures factory functions are only executed once per key,
Expand All @@ -24,8 +24,9 @@ public TValue GetOrAdd(TKey key, Func<TKey, TValue> func)
// The Lazy wrapper ensures the factory function is only executed once,
// even if multiple threads race to add the same key
// We use ExecutionAndPublication mode for thread safety
var lazy = _innerDictionary.GetOrAdd(key,
var lazy = _innerDictionary.GetOrAdd(key,
k => new Lazy<TValue>(() => func(k), LazyThreadSafetyMode.ExecutionAndPublication));

return lazy.Value;
}

Expand All @@ -36,7 +37,7 @@ public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value)
value = lazy.Value!;
return true;
}

value = default!;
return false;
}
Expand All @@ -51,7 +52,7 @@ public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value)
return default(TValue?);
}

public TValue this[TKey key] => _innerDictionary.TryGetValue(key, out var lazy)
? lazy.Value
public TValue this[TKey key] => _innerDictionary.TryGetValue(key, out var lazy)
? lazy.Value
: throw new KeyNotFoundException($"Key '{key}' not found in dictionary");
}
Loading
Loading