Created
April 30, 2020 13:50
-
-
Save ReubenBond/68c5c86e933ee0eb243616511cb1154a to your computer and use it in GitHub Desktop.
CLR Type Parser/Formatter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Obviously, zero guarantees that this works