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
@@ -1,6 +1,4 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using TUnit.Core.SourceGenerator.Enums;
using Microsoft.CodeAnalysis;
using TUnit.Core.SourceGenerator.Utilities;

namespace TUnit.Core.SourceGenerator.CodeGenerators.Writers;
Expand Down Expand Up @@ -58,46 +56,6 @@ public static void GenerateMethodInformation(ICodeWriter sourceCodeWriter,
sourceCodeWriter.AppendLine();
}

public static void GenerateMembers(ICodeWriter sourceCodeWriter, Compilation compilation, INamedTypeSymbol namedTypeSymbol, ImmutableArray<IParameterSymbol> parameters, IPropertySymbol? property, ArgumentsType argumentsType)
{
if (parameters.Length == 0 && property is null)
{
sourceCodeWriter.Append("[],");
return;
}

sourceCodeWriter.Append("[");

if (property is not null)
{
GeneratePropertyInformation(sourceCodeWriter, compilation, property, namedTypeSymbol);
}

foreach (var parameter in parameters)
{
GenerateParameterInformation(sourceCodeWriter, compilation, parameter, argumentsType, null);
}

sourceCodeWriter.Append("],");
}

public static void GeneratePropertyInformation(ICodeWriter sourceCodeWriter,
Compilation compilation, IPropertySymbol property, INamedTypeSymbol namedTypeSymbol)
{
MetadataGenerationHelper.WritePropertyMetadata(sourceCodeWriter, property, namedTypeSymbol);
sourceCodeWriter.Append(",");
}

public static void GenerateParameterInformation(ICodeWriter sourceCodeWriter,
Compilation context,
IParameterSymbol parameter, ArgumentsType argumentsType,
IDictionary<string, string>? genericSubstitutions)
{
// For now, use the generic version since it's what the existing code was doing
MetadataGenerationHelper.WriteParameterMetadataGeneric(sourceCodeWriter, parameter);
sourceCodeWriter.Append(",");
}

/// <summary>
/// Recursively generates parent ClassMetadata expression for nested types.
/// Returns null if the type has no containing type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,87 +211,39 @@ public static string GenerateAssemblyMetadataGetOrAdd(IAssemblySymbol assembly)
return $"global::TUnit.Core.AssemblyMetadata.GetOrAdd(\"{assembly.Name}\", \"{assembly.Name}\")";
}

/// <summary>
/// Generates code for creating a ParameterMetadata instance (generic version).
/// Delegates to <see cref="WriteParameterMetadata"/> for consistency.
/// </summary>
public static void WriteParameterMetadataGeneric(ICodeWriter writer, IParameterSymbol parameter, IMethodSymbol? containingMethod = null)
{
WriteParameterMetadata(writer, parameter, containingMethod);
}

/// <summary>
/// Generates code for creating a ParameterMetadata instance via ParameterMetadataFactory.Create().
/// Reflection info is attached per method/constructor by <see cref="WriteReflectionInfoAttachStart"/>.
/// </summary>
public static void WriteParameterMetadata(ICodeWriter writer, IParameterSymbol parameter, IMethodSymbol? containingMethod = null)
private static void WriteParameterMetadata(ICodeWriter writer, IParameterSymbol parameter)
{
var safeType = CodeGenerationHelpers.ContainsTypeParameter(parameter.Type) ? "object" : parameter.Type.GloballyQualified();
var reflectionInfoExpr = GenerateReflectionInfoForParameter(parameter, containingMethod);

writer.Append($"global::TUnit.Core.ParameterMetadataFactory.Create(typeof({safeType}), \"{parameter.Name}\", {CodeGenerationHelpers.GenerateTypeInfo(parameter.Type)}, {parameter.Type.IsNullable().ToString().ToLowerInvariant()}, reflectionInfoFactory: static () => {reflectionInfoExpr})");
writer.Append($"global::TUnit.Core.ParameterMetadataFactory.Create(typeof({safeType}), \"{parameter.Name}\", {CodeGenerationHelpers.GenerateTypeInfo(parameter.Type)}, {parameter.Type.IsNullable().ToString().ToLowerInvariant()})");
}

/// <summary>
/// Generates reflection info code for a parameter.
/// Delegates to <see cref="GenerateParameterInfoArrayExpression"/> to avoid duplicating the reflection lookup logic.
/// Writes the opening of a ParameterMetadataFactory.ForMethod/ForGenericMethod/ForConstructor call, which lazily
/// resolves the ParameterInfo of every parameter from a single shared method lookup on first access.
/// Must be followed by the ParameterMetadata[] array expression and a closing parenthesis.
/// </summary>
private static string GenerateReflectionInfoForParameter(IParameterSymbol parameter, IMethodSymbol? providedMethod = null)
private static void WriteReflectionInfoAttachStart(ICodeWriter writer, IMethodSymbol method)
{
var method = providedMethod ?? parameter.ContainingSymbol as IMethodSymbol;

if (method == null)
{
return "null!";
}

var parameterIndex = method.Parameters.IndexOf(parameter);
if (parameterIndex == -1)
{
return "null!";
}

var containingType = method.ContainingType.GloballyQualified();
var usesTypeParameters = method.Parameters.Any(p => CodeGenerationHelpers.ContainsTypeParameter(p.Type));

if (method.MethodKind == MethodKind.Constructor)
{
if (method.Parameters.Any(p => CodeGenerationHelpers.ContainsTypeParameter(p.Type)))
{
return $@"global::System.Linq.Enumerable.FirstOrDefault(typeof({containingType}).GetConstructors(), c => c.GetParameters().Length == {method.Parameters.Length})?.GetParameters()[{parameterIndex}]!";
}

var paramTypes = GenerateParameterTypesArrayForReflection(method);
return $@"typeof({containingType}).GetConstructor({paramTypes})!.GetParameters()[{parameterIndex}]";
}

if (method.TypeParameters.Length > 0 || method.Parameters.Any(p => CodeGenerationHelpers.ContainsTypeParameter(p.Type)))
{
return $@"global::System.Linq.Enumerable.FirstOrDefault(typeof({containingType}).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.Static), m => m.Name == ""{method.Name}"" && m.GetParameters().Length == {method.Parameters.Length})?.GetParameters()[{parameterIndex}]!";
writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForConstructor(typeof({containingType}), {usesTypeParameters.ToString().ToLowerInvariant()}, ");
}

else if (method.TypeParameters.Length > 0 || usesTypeParameters)
{
var bindingFlags = method.IsStatic
? "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static"
: "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance";
var paramTypes = GenerateParameterTypesArrayForReflection(method);
return $@"typeof({containingType}).GetMethod(""{method.Name}"", {bindingFlags}, null, {paramTypes}, null)!.GetParameters()[{parameterIndex}]";
writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof({containingType}), \"{method.Name}\", ");
}
}


private static string GenerateParameterTypesArrayForReflection(IMethodSymbol method)
{
if (method.Parameters.Length == 0)
else
{
return "global::System.Type.EmptyTypes";
writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof({containingType}), \"{method.Name}\", {method.IsStatic.ToString().ToLowerInvariant()}, ");
}

var paramTypes = method.Parameters.Select(p =>
{
var safeTypeName = p.Type.GloballyQualified();
return $"typeof({safeTypeName})";
});

return $"new global::System.Type[] {{ {string.Join(", ", paramTypes)} }}";
}

/// <summary>
Expand Down Expand Up @@ -369,6 +321,7 @@ private static void WriteParameterMetadataArrayForMethod(ICodeWriter writer, IMe
return;
}

WriteReflectionInfoAttachStart(writer, method);
writer.AppendLine("new global::TUnit.Core.ParameterMetadata[]");
writer.AppendLine("{");

Expand All @@ -379,7 +332,7 @@ private static void WriteParameterMetadataArrayForMethod(ICodeWriter writer, IMe
for (var i = 0; i < method.Parameters.Length; i++)
{
var param = method.Parameters[i];
WriteParameterMetadata(writer, param, method);
WriteParameterMetadata(writer, param);

if (i < method.Parameters.Length - 1)
{
Expand All @@ -390,7 +343,7 @@ private static void WriteParameterMetadataArrayForMethod(ICodeWriter writer, IMe
// Manually restore indent level
writer.SetIndentLevel(currentIndent);
writer.AppendLine();
writer.Append("}");
writer.Append("})");
}

/// <summary>
Expand All @@ -414,6 +367,7 @@ private static void WriteParameterMetadataArrayForConstructor(ICodeWriter writer
return;
}

WriteReflectionInfoAttachStart(writer, constructor);
writer.AppendLine("new global::TUnit.Core.ParameterMetadata[]");
writer.AppendLine("{");

Expand All @@ -424,7 +378,7 @@ private static void WriteParameterMetadataArrayForConstructor(ICodeWriter writer
for (var i = 0; i < constructor.Parameters.Length; i++)
{
var param = constructor.Parameters[i];
WriteParameterMetadata(writer, param, constructor);
WriteParameterMetadata(writer, param);

if (i < constructor.Parameters.Length - 1)
{
Expand All @@ -435,7 +389,7 @@ private static void WriteParameterMetadataArrayForConstructor(ICodeWriter writer
// Manually restore indent level
writer.SetIndentLevel(currentIndent);
writer.AppendLine();
writer.Append("}");
writer.Append("})");
}

/// <summary>
Expand All @@ -449,14 +403,15 @@ private static string GenerateParameterMetadataArrayForConstructor(IMethodSymbol
}

var writer = new CodeWriter("", includeHeader: false).SetIndentLevel(currentIndentLevel);
WriteReflectionInfoAttachStart(writer, constructor);
writer.AppendLine("new global::TUnit.Core.ParameterMetadata[]");
writer.AppendLine("{");
writer.Indent();

for (var i = 0; i < constructor.Parameters.Length; i++)
{
var param = constructor.Parameters[i];
WriteParameterMetadata(writer, param, constructor);
WriteParameterMetadata(writer, param);

if (i < constructor.Parameters.Length - 1)
{
Expand All @@ -465,7 +420,7 @@ private static string GenerateParameterMetadataArrayForConstructor(IMethodSymbol
}

writer.Unindent();
writer.Append("}");
writer.Append("})");

return writer.ToString();
}
Expand Down
20 changes: 18 additions & 2 deletions src/TUnit.Core/Models/TestModels/ParameterMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,33 @@ public record ParameterMetadata([DynamicallyAccessedMembers(DynamicallyAccessedM
public ParameterInfo ReflectionInfo
{
get => field ??= ReflectionInfoFactory?.Invoke()
?? throw new InvalidOperationException(
$"ReflectionInfo for parameter '{Name}' was not set and no ReflectionInfoFactory was provided.");
?? ReflectionInfoResolver?.Get(ReflectionInfoIndex)
?? throw CreateMissingReflectionInfoException();
set;
} = null!;

private InvalidOperationException CreateMissingReflectionInfoException()
{
return ReflectionInfoResolver is { } resolver
? new InvalidOperationException(
$"ReflectionInfo for parameter '{Name}' (index {ReflectionInfoIndex}) could not be resolved: {resolver.Describe()} was not found.")
: new InvalidOperationException(
$"ReflectionInfo for parameter '{Name}' was not set and no ReflectionInfoFactory was provided.");
}

/// <summary>
/// Lazy factory for ReflectionInfo. Set by source generator to defer reflection to first access.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public Func<ParameterInfo>? ReflectionInfoFactory { get; init; }

/// <summary>
/// Shared per-method lazy lookup attached by <see cref="ParameterMetadataFactory"/> for generated code.
/// </summary>
internal ParameterInfoResolver? ReflectionInfoResolver { get; set; }

internal int ReflectionInfoIndex { get; set; }

public bool IsParams => CachedIsParams ?? ReflectionInfo.IsDefined(typeof(ParamArrayAttribute), false);
public bool IsOptional => CachedIsOptional ?? ReflectionInfo.IsOptional;
public bool IsNullable { get; init; }
Expand Down
Loading
Loading