This commit is contained in:
bbradson
2022-10-24 18:56:18 +02:00
parent 01ef055ff2
commit 9cdc8ccb4f
70 changed files with 19754 additions and 4 deletions
@@ -0,0 +1,162 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using System.Runtime.CompilerServices;
namespace FisheryLib.Utility.Diagnostics;
/// <summary>
/// Helpers for working with types.
/// </summary>
public static class TypeExtensions
{
/// <summary>
/// The mapping of built-in types to their simple representation.
/// </summary>
private static readonly IReadOnlyDictionary<Type, string> BuiltInTypesMap = new Dictionary<Type, string>
{
[typeof(bool)] = "bool",
[typeof(byte)] = "byte",
[typeof(sbyte)] = "sbyte",
[typeof(short)] = "short",
[typeof(ushort)] = "ushort",
[typeof(char)] = "char",
[typeof(int)] = "int",
[typeof(uint)] = "uint",
[typeof(float)] = "float",
[typeof(long)] = "long",
[typeof(ulong)] = "ulong",
[typeof(double)] = "double",
[typeof(decimal)] = "decimal",
[typeof(object)] = "object",
[typeof(string)] = "string",
[typeof(void)] = "void"
};
/// <summary>
/// A thread-safe mapping of precomputed string representation of types.
/// </summary>
private static readonly ConditionalWeakTable<Type, string> DisplayNames = new();
/// <summary>
/// Returns a simple string representation of a type.
/// </summary>
/// <param name="type">The input type.</param>
/// <returns>The 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 var typeName))
{
return typeName!;
}
// Array types are displayed as Foo[]
if (type.IsArray)
{
var elementType = type.GetElementType()!;
var 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)
{
var genericTypeDefinition = type.GetGenericTypeDefinition();
// Nullable<T> types are displayed as T?
if (genericTypeDefinition == typeof(Nullable<>))
{
var 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<,,,,,,,>))
{
var 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)
var tokens = type.Name.Split('`');
var genericArgumentsCount = int.Parse(tokens[1]);
var typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount;
var currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray();
var 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
return type.IsNested
? $"{FormatDisplayString(type.DeclaringType!, genericTypeOffset, typeArguments)}.{displayName}"
: $"{type.Namespace}.{displayName}";
}
// Atomically get or build the display string for the current type.
return DisplayNames.GetValue(type, t =>
{
// By-ref types are displayed as T&
if (t.IsByRef)
{
t = t.GetElementType()!;
return $"{FormatDisplayString(t, 0, t.GetGenericArguments())}&";
}
// Pointer types are displayed as T*
if (t.IsPointer)
{
var depth = 0;
// Calculate the pointer indirection level
while (t.IsPointer)
{
depth++;
t = t.GetElementType()!;
}
return $"{FormatDisplayString(t, 0, t.GetGenericArguments())}{new string('*', depth)}";
}
// Standard path for concrete types
return FormatDisplayString(t, 0, t.GetGenericArguments());
});
}
}
@@ -0,0 +1,66 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
namespace FisheryLib.Utility.Diagnostics;
/// <summary>
/// Helpers for working with value types.
/// </summary>
public static class ValueTypeExtensions
{
/// <summary>
/// Gets the table of hex characters (doesn't allocate, maps to .text section, see <see href="https://github.com/dotnet/roslyn/pull/24621"/>).
/// </summary>
private static ReadOnlySpan<byte> HexCharactersTable => new[]
{
(byte)'0', (byte)'1', (byte)'2', (byte)'3',
(byte)'4', (byte)'5', (byte)'6', (byte)'7',
(byte)'8', (byte)'9', (byte)'A', (byte)'B',
(byte)'C', (byte)'D', (byte)'E', (byte)'F'
};
/// <summary>
/// Returns a hexadecimal <see cref="string"/> representation of a given <typeparamref name="T"/> value, left-padded and ordered as big-endian.
/// </summary>
/// <typeparam name="T">The input type to format to <see cref="string"/>.</typeparam>
/// <param name="value">The input value to format to <see cref="string"/>.</param>
/// <returns>
/// The hexadecimal representation of <paramref name="value"/> (with the '0x' prefix), left-padded to byte boundaries and ordered as big-endian.
/// </returns>
/// <remarks>
/// As a byte (8 bits) is represented by two hexadecimal digits (each representing a group of 4 bytes), each <see cref="string"/>
/// representation will always contain an even number of digits. For instance:
/// <code>
/// Console.WriteLine(1.ToHexString()); // "0x01"
/// Console.WriteLine(((byte)255).ToHexString()); // "0xFF"
/// Console.WriteLine((-1).ToHexString()); // "0xFFFFFFFF"
/// </code>
/// </remarks>
public static unsafe string ToHexString<T>(this T value)
where T : unmanaged
{
var sizeOfT = Unsafe.SizeOf<T>();
var bufferSize = (2 * sizeOfT) + 2;
var p = stackalloc char[bufferSize];
p[0] = '0';
p[1] = 'x';
ref var rh = ref HexCharactersTable.DangerousGetPinnableReference();
for (int i = 0, j = bufferSize - 2; i < sizeOfT; i++, j -= 2)
{
var b = ((byte*)&value)[i];
var low = b & 0x0F;
var high = (b & 0xF0) >> 4;
p[j + 1] = (char)Unsafe.Add(ref rh, low);
p[j] = (char)Unsafe.Add(ref rh, high);
}
return new(p, 0, bufferSize);
}
}