Skip to content

Instantly share code, notes, and snippets.

@ReubenBond
Created April 30, 2020 13:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ReubenBond/68c5c86e933ee0eb243616511cb1154a to your computer and use it in GitHub Desktop.
Save ReubenBond/68c5c86e933ee0eb243616511cb1154a to your computer and use it in GitHub Desktop.
CLR Type Parser/Formatter
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Text;
namespace gentest
{
/// <summary>
/// Utility methods for formatting <see cref="Type"/> instances in a way which can be parsed by
/// <see cref="Type.GetType(string)"/>.
/// </summary>
public static class RuntimeTypeNameFormatter
{
private static readonly Assembly SystemAssembly = typeof(int).Assembly;
private static readonly char[] SimpleNameTerminators = { '`', '*', '[', '&' };
private static readonly ConcurrentDictionary<Type, string> Cache = new ConcurrentDictionary<Type, string>();
static RuntimeTypeNameFormatter()
{
IncludeAssemblyName = type => !SystemAssembly.Equals(type.Assembly);
}
/// <summary>
/// Gets or sets the delegate used to determine whether an assembly name should be printed for the provided type.
/// </summary>
public static Func<Type, bool> IncludeAssemblyName { get; set; }
/// <summary>
/// Returns a <see cref="string"/> form of <paramref name="type"/> which can be parsed by <see cref="Type.GetType(string)"/>.
/// </summary>
/// <param name="type">The type to format.</param>
/// <returns>
/// A <see cref="string"/> form of <paramref name="type"/> which can be parsed by <see cref="Type.GetType(string)"/>.
/// </returns>
public static string Format(Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (!Cache.TryGetValue(type, out var result))
{
string FormatType(Type t)
{
var builder = new StringBuilder();
Format(builder, t, isElementType: false);
return builder.ToString();
}
result = Cache.GetOrAdd(type, FormatType);
}
return result;
}
private static void Format(StringBuilder builder, Type type, bool isElementType)
{
// Arrays, pointers, and by-ref types are all element types and need to be formatted with their own adornments.
if (type.HasElementType)
{
// Format the element type.
Format(builder, type.GetElementType(), isElementType: true);
// Format this type's adornments to the element type.
AddArrayRank(builder, type);
AddPointerSymbol(builder, type);
AddByRefSymbol(builder, type);
}
else
{
AddNamespace(builder, type);
AddClassName(builder, type);
AddGenericParameters(builder, type);
}
// Types which are used as elements are not formatted with their assembly name, since that is added after the
// element type's adornments.
if (!isElementType && IncludeAssemblyName(type))
{
AddAssembly(builder, type);
}
}
private static void AddNamespace(StringBuilder builder, Type type)
{
if (string.IsNullOrWhiteSpace(type.Namespace)) return;
builder.Append(type.Namespace);
builder.Append('.');
}
private static void AddClassName(StringBuilder builder, Type type)
{
// Format the declaring type.
if (type.IsNested)
{
AddClassName(builder, type.DeclaringType);
builder.Append('+');
}
// Format the simple type name.
var index = type.Name.IndexOfAny(SimpleNameTerminators);
builder.Append(index > 0 ? type.Name.Substring(0, index) : type.Name);
// Format this type's generic arity.
AddGenericArity(builder, type);
}
private static void AddGenericParameters(StringBuilder builder, Type type)
{
// Generic type definitions (eg, List<> without parameters) and non-generic types do not include any
// parameters in their formatting.
if (!type.IsConstructedGenericType) return;
var args = type.GetGenericArguments();
builder.Append('[');
for (var i = 0; i < args.Length; i++)
{
builder.Append('[');
Format(builder, args[i], isElementType: false);
builder.Append(']');
if (i + 1 < args.Length) builder.Append(',');
}
builder.Append(']');
}
private static void AddGenericArity(StringBuilder builder, Type type)
{
if (!type.IsGenericType) return;
// The arity is the number of generic parameters minus the number of generic parameters in the declaring types.
var baseTypeParameterCount =
type.IsNested ? type.DeclaringType.GetGenericArguments().Length : 0;
var arity = type.GetGenericArguments().Length - baseTypeParameterCount;
// If all of the generic parameters are in the declaring types then this type has no parameters of its own.
if (arity == 0) return;
builder.Append('`');
builder.Append(arity);
}
private static void AddPointerSymbol(StringBuilder builder, Type type)
{
if (!type.IsPointer) return;
builder.Append('*');
}
private static void AddByRefSymbol(StringBuilder builder, Type type)
{
if (!type.IsByRef) return;
builder.Append('&');
}
private static void AddArrayRank(StringBuilder builder, Type type)
{
if (!type.IsArray) return;
builder.Append('[');
builder.Append(',', type.GetArrayRank() - 1);
builder.Append(']');
}
private static void AddAssembly(StringBuilder builder, Type type)
{
builder.Append(',');
builder.Append(type.Assembly.GetName().Name);
}
}
}
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace gentest
{
internal class RuntimeTypeNameParser
{
private const int MaxAllowedGenericArity = 64;
private const char PointerIndicator = '*';
private const char ReferenceIndicator = '&';
private const char ArrayStartIndicator = '[';
private const char ArrayDimensionIndicator = ',';
private const char ArrayEndIndicator = ']';
private const char ParameterSeparator = ',';
private const char GenericTypeIndicator = '`';
private const char NestedTypeIndicator = '+';
private const char AssemblyIndicator = ',';
private static ReadOnlySpan<char> AssemblyDelimiters => new[] { ArrayEndIndicator };
private static ReadOnlySpan<char> TypeNameDelimiters => new[] { ArrayStartIndicator, ArrayEndIndicator, PointerIndicator, ReferenceIndicator, AssemblyIndicator, GenericTypeIndicator, NestedTypeIndicator };
private ref struct State
{
public ReadOnlySpan<char> Input;
public int Index;
public int TotalGenericArity;
public readonly ReadOnlySpan<char> Remaining => this.Input.Slice(Index);
public bool TryPeek(out char c)
{
if (Index < Input.Length)
{
c = Input[Index];
return true;
}
c = default;
return false;
}
public void Consume(int chars)
{
if (Index < Input.Length)
{
Index += chars;
return;
}
ThrowEndOfInput();
}
public void ConsumeCharacter(char assertChar)
{
if (Index < Input.Length)
{
var c = Input[Index];
if (assertChar != c)
{
ThrowUnexpectedCharacter(assertChar, c);
return;
}
++Index;
return;
}
ThrowEndOfInput();
}
public void ConsumeWhitespace()
{
while (char.IsWhiteSpace(Input[Index]))
{
++Index;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowUnexpectedCharacter(char expected, char actual) => throw new InvalidOperationException($"Encountered unexpected character. Expected '{expected}', actual '{actual}'.");
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowEndOfInput() => throw new InvalidOperationException("Tried to read past the end of the input");
}
public static TypeSpec Parse(ReadOnlySpan<char> input) => ParseInternal(ref input);
public static TypeSpec ParseInternal(ref ReadOnlySpan<char> input)
{
TypeSpec result;
char c;
State s = default;
s.Input = input;
// Read namespace and class name, including generic arity, which is a part of the class name.
NamedTypeSpec named = null;
while (true)
{
var typeName = ParseTypeName(ref s);
named = new NamedTypeSpec(named, new string(typeName), s.TotalGenericArity);
if (s.TryPeek(out c) && c == NestedTypeIndicator)
{
// Consume the nested type indicator, then loop to parse the nested type.
s.ConsumeCharacter(NestedTypeIndicator);
continue;
}
break;
}
// Parse generic type parameters
if (s.TotalGenericArity > 0 && s.TryPeek(out c) && c == ArrayStartIndicator)
{
s.ConsumeCharacter(ArrayStartIndicator);
var arguments = new TypeSpec[s.TotalGenericArity];
for (var i = 0; i < s.TotalGenericArity; i++)
{
if (i > 0)
{
s.ConsumeCharacter(ParameterSeparator);
}
// Parse the argument type
s.ConsumeCharacter(ArrayStartIndicator);
var remaining = s.Remaining;
arguments[i] = ParseInternal(ref remaining);
var consumed = s.Remaining.Length - remaining.Length;
s.Consume(consumed);
s.ConsumeCharacter(ArrayEndIndicator);
}
s.ConsumeCharacter(ArrayEndIndicator);
result = new ConstructedGenericTypeSpec(named, arguments);
}
else
{
// This is not a constructed generic type
result = named;
}
// Parse modifiers
bool hadModifier;
do
{
hadModifier = false;
if (!s.TryPeek(out c))
{
break;
}
switch (c)
{
case ArrayStartIndicator:
var dimensions = ParseArraySpecifier(ref s);
result = new ArrayTypeSpec(result, dimensions);
hadModifier = true;
break;
case PointerIndicator:
result = new PointerTypeSpec(result);
s.ConsumeCharacter(PointerIndicator);
hadModifier = true;
break;
case ReferenceIndicator:
result = new ReferenceTypeSpec(result);
s.ConsumeCharacter(ReferenceIndicator);
hadModifier = true;
break;
}
} while (hadModifier);
// Extract the assembly, if specified.
if (s.TryPeek(out c) && c == AssemblyIndicator)
{
s.ConsumeCharacter(AssemblyIndicator);
var assembly = ExtractAssemblySpec(ref s);
result = new AssemblyQualifiedTypeSpec(result, new string(assembly));
}
input = s.Remaining;
return result;
}
private static ReadOnlySpan<char> ParseTypeName(ref State s)
{
char c;
var start = s.Index;
var typeName = ParseSpan(ref s, TypeNameDelimiters);
var genericArityStart = -1;
while (s.TryPeek(out c))
{
if (genericArityStart < 0 && c == GenericTypeIndicator)
{
genericArityStart = s.Index + 1;
}
else if (genericArityStart < 0 || !char.IsDigit(c))
{
break;
}
s.ConsumeCharacter(c);
}
if (genericArityStart >= 0)
{
// The generic arity is additive, so that a generic class nested in a generic class has an arity
// equal to the sum of specified arity values. For example, "C`1+N`2" has an arity of 3.
s.TotalGenericArity += int.Parse(s.Input.Slice(genericArityStart, s.Index - genericArityStart));
if (s.TotalGenericArity > MaxAllowedGenericArity)
{
ThrowGenericArityTooLarge(s.TotalGenericArity);
}
// Include the generic arity in the type name.
typeName = s.Input.Slice(start, s.Index - start);
}
return typeName;
}
private static int ParseArraySpecifier(ref State s)
{
s.ConsumeCharacter(ArrayStartIndicator);
var dimensions = 1;
while (s.TryPeek(out var c) && c != ArrayEndIndicator)
{
s.ConsumeCharacter(ArrayDimensionIndicator);
++dimensions;
}
s.ConsumeCharacter(ArrayEndIndicator);
return dimensions;
}
private static ReadOnlySpan<char> ExtractAssemblySpec(ref State s)
{
s.ConsumeWhitespace();
return ParseSpan(ref s, AssemblyDelimiters);
}
private static ReadOnlySpan<char> ParseSpan(ref State s, ReadOnlySpan<char> delimiters)
{
ReadOnlySpan<char> result;
if (s.Remaining.IndexOfAny(delimiters) is int index && index > 0)
{
result = s.Remaining.Slice(0, index);
}
else
{
result = s.Remaining;
}
s.Consume(result.Length);
return result;
}
private static void ThrowGenericArityTooLarge(int arity) => throw new NotSupportedException($"An arity of {arity} is not supported");
public abstract class TypeSpec { }
public class PointerTypeSpec : TypeSpec
{
public PointerTypeSpec(TypeSpec elementType)
{
this.ElementType = elementType;
}
public TypeSpec ElementType { get; }
public override string ToString() => $"{this.ElementType}*";
}
public class ReferenceTypeSpec : TypeSpec
{
public ReferenceTypeSpec(TypeSpec elementType)
{
this.ElementType = elementType;
}
public TypeSpec ElementType { get; }
public override string ToString() => $"{this.ElementType}&";
}
public class ArrayTypeSpec : TypeSpec
{
public ArrayTypeSpec(TypeSpec elementType, int dimensions)
{
this.ElementType = elementType;
this.Dimensions = dimensions;
}
public int Dimensions { get; }
public TypeSpec ElementType { get; }
public override string ToString() => $"{this.ElementType}[{new string(',', this.Dimensions - 1)}]";
}
public class ConstructedGenericTypeSpec : TypeSpec
{
public ConstructedGenericTypeSpec(NamedTypeSpec unconstructedType, TypeSpec[] arguments)
{
this.UnconstructedType = unconstructedType;
this.Arguments = arguments;
}
public NamedTypeSpec UnconstructedType { get; }
public TypeSpec[] Arguments { get; }
public override string ToString() => $"{this.UnconstructedType}[{string.Join(",", this.Arguments.Select(a => $"[{a}]"))}]";
}
public class NamedTypeSpec : TypeSpec
{
public NamedTypeSpec(NamedTypeSpec containingType, string name, int arity)
{
this.ContainingType = containingType;
this.NamespaceQualifiedName = name;
this.GenericParameterCount = arity;
}
public int GenericParameterCount { get; }
public string NamespaceQualifiedName { get; }
public NamedTypeSpec ContainingType { get; }
public override string ToString() => ContainingType is object ? $"{this.ContainingType}+{this.NamespaceQualifiedName}" : this.NamespaceQualifiedName;
}
public class AssemblyQualifiedTypeSpec : TypeSpec
{
public AssemblyQualifiedTypeSpec(TypeSpec type, string assembly)
{
this.Type = type;
this.Assembly = assembly;
}
public string Assembly { get; }
public TypeSpec Type { get; }
public override string ToString() => $"{this.Type},{this.Assembly}";
}
}
}
@ReubenBond
Copy link
Author

Obviously, zero guarantees that this works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment