Skip to content
Merged
Prev Previous commit
Next Next commit
Expand generic type fix to handle all open generic types, not just Sy…
…stem.Nullable<>

Co-authored-by: thomhurst <[email protected]>
  • Loading branch information
Copilot and thomhurst committed Aug 21, 2025
commit 26d775061a6da35e23d0cf2220e225c6879e7f5d
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#pragma warning disable
#nullable enable
namespace TUnit.Generated;
internal sealed class Tests_SimpleTest_TestSource_9c97c20c9f124d82999b00e7d24a75b1 : global::TUnit.Core.Interfaces.SourceGenerator.ITestSource
internal sealed class Tests_SimpleTest_TestSource_38be5ee0b5554088ba7b233e82649e27 : global::TUnit.Core.Interfaces.SourceGenerator.ITestSource
{
public async global::System.Collections.Generic.IAsyncEnumerable<global::TUnit.Core.TestMetadata> GetTestsAsync(string testSessionId, [global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default)
{
Expand All @@ -18,7 +18,7 @@ internal sealed class Tests_SimpleTest_TestSource_9c97c20c9f124d82999b00e7d24a75
AttributeFactory = () =>
[
new global::TUnit.Core.TestAttribute(),
new global::TUnit.TestProject.Bugs._2971.SomeAttribute(typeof(T?))
new global::TUnit.TestProject.Bugs._2971.SomeAttribute(typeof(global::System.Nullable<>))
],
DataSources = new global::TUnit.Core.IDataSourceAttribute[]
{
Expand Down Expand Up @@ -84,11 +84,11 @@ internal sealed class Tests_SimpleTest_TestSource_9c97c20c9f124d82999b00e7d24a75
yield break;
}
}
internal static class Tests_SimpleTest_ModuleInitializer_9c97c20c9f124d82999b00e7d24a75b1
internal static class Tests_SimpleTest_ModuleInitializer_38be5ee0b5554088ba7b233e82649e27
{
[global::System.Runtime.CompilerServices.ModuleInitializer]
public static void Initialize()
{
global::TUnit.Core.SourceRegistrar.Register(typeof(global::TUnit.TestProject.Bugs._2971.Tests), new Tests_SimpleTest_TestSource_9c97c20c9f124d82999b00e7d24a75b1());
global::TUnit.Core.SourceRegistrar.Register(typeof(global::TUnit.TestProject.Bugs._2971.Tests), new Tests_SimpleTest_TestSource_38be5ee0b5554088ba7b233e82649e27());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,6 @@ public string FormatForCode(TypedConstant constant, ITypeSymbol? targetType = nu

case TypedConstantKind.Type:
var type = (ITypeSymbol)constant.Value!;

// Special handling for System.Nullable<> when it would be displayed as "T?"
if (type is INamedTypeSymbol namedType &&
(namedType.SpecialType == SpecialType.System_Nullable_T ||
namedType.ConstructedFrom?.SpecialType == SpecialType.System_Nullable_T) &&
namedType.TypeArguments.Length == 1 &&
namedType.TypeArguments[0].TypeKind == TypeKind.TypeParameter)
{
return "typeof(global::System.Nullable<>)";
}

return $"typeof({type.GloballyQualified()})";

case TypedConstantKind.Array:
Expand Down
41 changes: 19 additions & 22 deletions TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,35 +208,32 @@ public static bool EnumerableGenericTypeIs(this ITypeSymbol enumerable, Generato

public static string GloballyQualified(this ISymbol typeSymbol)
{
// Special handling for System.Nullable<> generic type definition
// When Roslyn encounters System.Nullable<>, it displays it as "T?" which is not valid C# syntax
if (typeSymbol is INamedTypeSymbol namedTypeSymbol)
// Handle open generic types where type arguments are type parameters
// This prevents invalid C# like List<T>, Dictionary<TKey, TValue>, T? where type parameters are undefined
if (typeSymbol is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType)
{
// Check if this is System.Nullable<T> where T is unbound/type parameter
if (namedTypeSymbol.SpecialType == SpecialType.System_Nullable_T ||
namedTypeSymbol.ConstructedFrom?.SpecialType == SpecialType.System_Nullable_T)
// Check if this is an unbound generic type or has type parameter arguments
bool hasTypeParameters = namedTypeSymbol.TypeArguments.Any(t => t.TypeKind == TypeKind.TypeParameter);
bool isUnboundGeneric = namedTypeSymbol.IsUnboundGenericType;

if (hasTypeParameters || isUnboundGeneric)
{
// For the unbound generic case like System.Nullable<> or Nullable<T> where T is a type parameter
if (namedTypeSymbol.TypeArguments.Length == 1 &&
namedTypeSymbol.TypeArguments[0].TypeKind == TypeKind.TypeParameter)
// Special case for System.Nullable<> - Roslyn displays it as "T?" even for open generic
if (namedTypeSymbol.SpecialType == SpecialType.System_Nullable_T ||
namedTypeSymbol.ConstructedFrom?.SpecialType == SpecialType.System_Nullable_T)
{
return "global::System.Nullable<>";
}

// General case for other open generic types
var typeBuilder = new StringBuilder(typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedNonGenericWithGlobalPrefix));
typeBuilder.Append('<');
typeBuilder.Append(new string(',', namedTypeSymbol.TypeArguments.Length - 1));
typeBuilder.Append('>');

return typeBuilder.ToString();
}
}

// Only generate open generic form for types with unresolved type parameters
// This ensures we get BaseClass<> for generic definitions but List<int> for constructed types
if(typeSymbol is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol2 &&
namedTypeSymbol2.TypeArguments.Any(t => t.TypeKind == TypeKind.TypeParameter))
{
var typeBuilder = new StringBuilder(typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedNonGenericWithGlobalPrefix));
typeBuilder.Append('<');
typeBuilder.Append(new string(',', namedTypeSymbol2.TypeArguments.Length - 1));
typeBuilder.Append('>');

return typeBuilder.ToString();
}

return typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedGenericWithGlobalPrefix);
}
Expand Down