Skip to content
Merged
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
203 changes: 115 additions & 88 deletions src/CommunityToolkit.Diagnostics/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace CommunityToolkit.Diagnostics;
Expand Down Expand Up @@ -43,97 +42,12 @@ public static class TypeExtensions
private static readonly ConditionalWeakTable<Type, string> DisplayNames = new();

/// <summary>
/// Returns a simple string representation of a type.
/// Returns a simple <see cref="string"/> representation of a type.
/// </summary>
/// <param name="type">The input type.</param>
/// <returns>The string representation of <paramref name="type"/>.</returns>
/// <returns>The <see cref="string"/> representation of <paramref name="type"/>.</returns>
public static string ToTypeString(this Type type)
{
// Local function to create the formatted string for a given type
static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan<Type> typeArguments)
{
// Primitive types use the keyword name
if (BuiltInTypesMap.TryGetValue(type, out string? typeName))
{
return typeName!;
}

// Array types are displayed as Foo[]
if (type.IsArray)
{
Type? elementType = type.GetElementType()!;
int rank = type.GetArrayRank();

return $"{FormatDisplayString(elementType, 0, elementType.GetGenericArguments())}[{new string(',', rank - 1)}]";
}

// By checking generic types here we are only interested in specific cases,
// ie. nullable value types or value tuples. We have a separate path for custom
// generic types, as we can't rely on this API in that case, as it doesn't show
// a difference between nested types that are themselves generic, or nested simple
// types from a generic declaring type. To deal with that, we need to manually track
// the offset within the array of generic arguments for the whole constructed type.
if (type.IsGenericType)
{
Type? genericTypeDefinition = type.GetGenericTypeDefinition();

// Nullable<T> types are displayed as T?
if (genericTypeDefinition == typeof(Nullable<>))
{
Type[]? nullableArguments = type.GetGenericArguments();

return $"{FormatDisplayString(nullableArguments[0], 0, nullableArguments)}?";
}

// ValueTuple<T1, T2> types are displayed as (T1, T2)
if (genericTypeDefinition == typeof(ValueTuple<>) ||
genericTypeDefinition == typeof(ValueTuple<,>) ||
genericTypeDefinition == typeof(ValueTuple<,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,,,,>))
{
IEnumerable<string>? formattedTypes = type.GetGenericArguments().Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));

return $"({string.Join(", ", formattedTypes)})";
}
}

string displayName;

// Generic types
if (type.Name.Contains('`'))
{
// Retrieve the current generic arguments for the current type (leaf or not)
string[]? tokens = type.Name.Split('`');
int genericArgumentsCount = int.Parse(tokens[1]);
int typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount;
Type[]? currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray();
IEnumerable<string>? formattedTypes = currentTypeArguments.Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));

// Standard generic types are displayed as Foo<T>
displayName = $"{tokens[0]}<{string.Join(", ", formattedTypes)}>";

// Track the current offset for the shared generic arguments list
genericTypeOffset += genericArgumentsCount;
}
else
{
// Simple custom types
displayName = type.Name;
}

// If the type is nested, recursively format the hierarchy as well
if (type.IsNested)
{
return $"{FormatDisplayString(type.DeclaringType!, genericTypeOffset, typeArguments)}.{displayName}";
}

return $"{type.Namespace}.{displayName}";
}

// Atomically get or build the display string for the current type.
return DisplayNames.GetValue(type, t =>
{
Expand Down Expand Up @@ -164,4 +78,117 @@ static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan
return FormatDisplayString(t, 0, t.GetGenericArguments());
});
}

/// <summary>
/// Formats a given <see cref="Type"/> instance to its <see cref="string"/> representation.
/// </summary>
/// <param name="type">The input type.</param>
/// <param name="genericTypeOffset">The offset into the generic type arguments for <paramref name="type"/>.</param>
/// <param name="typeArguments">The generic type arguments for <paramref name="type"/>.</param>
/// <returns>The <see cref="string"/> representation of <paramref name="type"/>.</returns>
private static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan<Type> typeArguments)
{
// Primitive types use the keyword name
if (BuiltInTypesMap.TryGetValue(type, out string? typeName))
{
return typeName!;
}

// Array types are displayed as Foo[]
if (type.IsArray)
{
Type elementType = type.GetElementType()!;
int rank = type.GetArrayRank();

return $"{FormatDisplayString(elementType, 0, elementType.GetGenericArguments())}[{new string(',', rank - 1)}]";
}

// By checking generic types here we are only interested in specific cases,
// ie. nullable value types or value tuples. We have a separate path for custom
// generic types, as we can't rely on this API in that case, as it doesn't show
// a difference between nested types that are themselves generic, or nested simple
// types from a generic declaring type. To deal with that, we need to manually track
// the offset within the array of generic arguments for the whole constructed type.
if (type.IsGenericType)
{
Type genericTypeDefinition = type.GetGenericTypeDefinition();

// Nullable<T> types are displayed as T?
if (genericTypeDefinition == typeof(Nullable<>))
{
Type[] nullableArguments = type.GetGenericArguments();

return $"{FormatDisplayString(nullableArguments[0], 0, nullableArguments)}?";
}

// ValueTuple<T1, T2> types are displayed as (T1, T2)
if (genericTypeDefinition == typeof(ValueTuple<>) ||
genericTypeDefinition == typeof(ValueTuple<,>) ||
genericTypeDefinition == typeof(ValueTuple<,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,,,,>))
{
IEnumerable<string> formattedTypes = FormatDisplayStringForAllTypes(type.GetGenericArguments());

return $"({string.Join(", ", formattedTypes)})";
}
}

string displayName;

// Generic types
if (type.Name.AsSpan().IndexOf('`') != -1)
{
// Retrieve the current generic arguments for the current type (leaf or not)
string[] tokens = type.Name.Split('`');
int genericArgumentsCount = int.Parse(tokens[1]);
int typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount;
Type[] currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray();
IEnumerable<string> formattedTypes = FormatDisplayStringForAllTypes(currentTypeArguments);

// Standard generic types are displayed as Foo<T>
displayName = $"{tokens[0]}<{string.Join(", ", formattedTypes)}>";

// Track the current offset for the shared generic arguments list
genericTypeOffset += genericArgumentsCount;
}
else
{
// Simple custom types
displayName = type.Name;
}

// If the type is nested, recursively format the hierarchy as well
if (type.IsNested)
{
return $"{FormatDisplayString(type.DeclaringType!, genericTypeOffset, typeArguments)}.{displayName}";
}

return $"{type.Namespace}.{displayName}";
}

/// <summary>
/// Formats a given sequence of <see cref="Type"/> instances to their <see cref="string"/> representations.
/// </summary>
/// <param name="types">The input types.</param>
/// <returns>The <see cref="string"/> representations of <paramref name="types"/>.</returns>
/// <remarks>
/// <para>
/// This method is explicitly an enumerator to avoid having to use LINQ in <see cref="FormatDisplayString"/>, which causes
/// a noticeable binary size increase in AOT scenarios if reflection is enabled (which is the only supported mode).
/// </para>
/// <para>
/// For future reference, see: <see href="https://github.com/dotnet/runtime/issues/82607#issuecomment-1444443656"/>.
/// </para>
/// </remarks>
private static IEnumerable<string> FormatDisplayStringForAllTypes(Type[] types)
{
foreach (Type type in types)
{
yield return FormatDisplayString(type, 0, type.GetGenericArguments());
}
}
}