Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for nested/pointer/byref types for Type.ToTypeString() #3468

Merged
7 commits merged into from
Oct 2, 2020
Merged
Show file tree
Hide file tree
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
155 changes: 117 additions & 38 deletions Microsoft.Toolkit/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public static class TypeExtensions
[typeof(double)] = "double",
[typeof(decimal)] = "decimal",
[typeof(object)] = "object",
[typeof(string)] = "string"
[typeof(string)] = "string",
[typeof(void)] = "void"
};

/// <summary>
Expand All @@ -56,70 +57,146 @@ public static class TypeExtensions
public static string ToTypeString(this Type type)
{
// Local function to create the formatted string for a given type
static string FormatDisplayString(Type 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!;
}

// Generic types
if (
#if NETSTANDARD1_4
type.GetTypeInfo().IsGenericType &&
#else
type.IsGenericType &&
#endif
type.FullName is { } fullName &&
fullName.Split('`') is { } tokens &&
tokens.Length > 0 &&
tokens[0] is { } genericName &&
genericName.Length > 0)
// Array types are displayed as Foo[]
if (type.IsArray)
{
var typeArguments = type.GetGenericArguments().Select(FormatDisplayString);
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 typles. 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?
var genericType = type.GetGenericTypeDefinition();
if (genericType == typeof(Nullable<>))
if (genericTypeDefinition == typeof(Nullable<>))
{
return $"{typeArguments.First()}?";
var nullableArguments = type.GetGenericArguments();

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

// ValueTuple<T1, T2> types are displayed as (T1, T2)
if (genericType == typeof(ValueTuple<>) ||
genericType == typeof(ValueTuple<,>) ||
genericType == typeof(ValueTuple<,,>) ||
genericType == typeof(ValueTuple<,,,>) ||
genericType == typeof(ValueTuple<,,,,>) ||
genericType == typeof(ValueTuple<,,,,,>) ||
genericType == typeof(ValueTuple<,,,,,,>) ||
genericType == typeof(ValueTuple<,,,,,,,>))
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<,,,,,,,>))
{
return $"({string.Join(", ", typeArguments)})";
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>
return $"{genericName}<{string.Join(", ", typeArguments)}>";
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;
}

// Array types are displayed as Foo[]
if (type.IsArray)
// If the type is nested, recursively format the hierarchy as well
if (type.IsNested)
{
var elementType = type.GetElementType();
var rank = type.GetArrayRank();
var openDeclaringType = type.DeclaringType!;
var rootGenericArguments = typeArguments.Slice(0, typeArguments.Length - genericTypeOffset).ToArray();

// If the declaring type is generic, we need to reconstruct the closed type
// manually, as the declaring type instance doesn't retain type information.
if (rootGenericArguments.Length > 0)
{
var closedDeclaringType = openDeclaringType.GetGenericTypeDefinition().MakeGenericType(rootGenericArguments);

return $"{FormatDisplayString(elementType)}[{new string(',', rank - 1)}]";
return $"{FormatDisplayString(closedDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
}

return $"{FormatDisplayString(openDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
}

return type.ToString();
return $"{type.Namespace}.{displayName}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the output here significantly different or just better formatted here? i.e. if someone with 6.1 upgrades, will the result be different in the cases where things worked?

(Just want to check if we should mark this as a breaking change even though the API surface itself didn't change.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output should be the same for types that were properly supported already 😊
In that line I'm explicitly adding the namespace just because with this change we're now using the name of each type directly instead of the fullname (so that we can manually traverse each type in case of nested types), so we need to add the namespace at the start ourselves to match the previous output with the fully qualified name.

As for changes between this PR and the previous version, here's some examples.
I'm only displaying cases with actual differences, such as nested types or pointer/ref types:

typeof(Animal.Cat)
// 6.1: Namespace.Animal+Cat    (uses + instead of .)
// 7.0: Namespace.Animal.Cat

typeof(Animal.Cat<int>)
// 6.1: Namespace.Animal+Cat<int>    (same)
// 7.0: Namespace.Animal.Cat<int>

typeof(Animal.Cat<int>.Bar)
// 6.1: Namespace.Animal+Cat<int>    (the nested .Bar type got lost)
// 7.0: Namespace.Animal.Cat<int>.Bar

typeof(Animal.Llama<string, int[]>.Foo)
// 6.1: Namespace.Animal+Llama<string, int[]>    (same)
// 7.0: Namespace.Animal.Llama<string, int[]>.Foo

typeof(Animal.Llama<string, int[]>.Foo<byte>)
// 6.1: Namespace.Animal+Llama<string, int[], byte>    (nested type missing, wrong type arguments)
// 7.0: Namespace.Animal.Llama<string, int[]>.Foo<byte>

typeof(void)
// 6.1: System.Void
// 7.0: void

typeof(int**)
// 6.1: System.Int32**
// 7.0: int**

typeof(string).MakeByRefType()
// 6.1: System.String&
// 7.0: string&

In general, some cases just look nicer (eg. . instead of + for a simple nested type, or void instead of System.Void), while others are now actually correct, whereas currently they are either missing bits (eg. nested types getting lost in the process) and/or displayed with wrong type arguments (they're basically all combined together).

}

// Atomically get or build the display string for the current type.
// Manually create a static lambda here to enable caching of the generated closure.
// This is a workaround for the missing caching for method group conversions, and should
// be removed once this issue is resolved: https://github.com/dotnet/roslyn/issues/5835.
return DisplayNames.GetValue(type, t => FormatDisplayString(t));
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)
{
int 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());
});
}

/// <summary>
/// Returns whether or not a given type is generic.
/// </summary>
/// <param name="type">The input type.</param>
/// <returns>Whether or not the input type is generic.</returns>
[Pure]
private static bool IsGenericType(this Type type)
{
#if NETSTANDARD1_4
return type.GetTypeInfo().IsGenericType;
#else
return type.IsGenericType;
#endif
}

#if NETSTANDARD1_4
Expand All @@ -128,6 +205,7 @@ tokens[0] is { } genericName &&
/// </summary>
/// <param name="type">The input type.</param>
/// <returns>An array of types representing the generic arguments.</returns>
[Pure]
private static Type[] GetGenericArguments(this Type type)
{
return type.GetTypeInfo().GenericTypeParameters;
Expand All @@ -139,6 +217,7 @@ private static Type[] GetGenericArguments(this Type type)
/// <param name="type">The input type.</param>
/// <param name="value">The type to check against.</param>
/// <returns><see langword="true"/> if <paramref name="type"/> is an instance of <paramref name="value"/>, <see langword="false"/> otherwise.</returns>
[Pure]
internal static bool IsInstanceOfType(this Type type, object value)
{
return type.GetTypeInfo().IsAssignableFrom(value.GetType().GetTypeInfo());
Expand Down
157 changes: 136 additions & 21 deletions UnitTests/UnitTests.Shared/Extensions/Test_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.Diagnostics.CodeAnalysis;
using Microsoft.Toolkit.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

Expand All @@ -15,32 +14,148 @@ public class Test_TypeExtensions
{
[TestCategory("TypeExtensions")]
[TestMethod]
public void Test_TypeExtensions_BuiltInTypes()
[DataRow("bool", typeof(bool))]
[DataRow("int", typeof(int))]
[DataRow("float", typeof(float))]
[DataRow("double", typeof(double))]
[DataRow("decimal", typeof(decimal))]
[DataRow("object", typeof(object))]
[DataRow("string", typeof(string))]
[DataRow("void", typeof(void))]
public void Test_TypeExtensions_BuiltInTypes(string name, Type type)
{
Assert.AreEqual("bool", typeof(bool).ToTypeString());
Assert.AreEqual("int", typeof(int).ToTypeString());
Assert.AreEqual("float", typeof(float).ToTypeString());
Assert.AreEqual("double", typeof(double).ToTypeString());
Assert.AreEqual("decimal", typeof(decimal).ToTypeString());
Assert.AreEqual("object", typeof(object).ToTypeString());
Assert.AreEqual("string", typeof(string).ToTypeString());
Assert.AreEqual(name, type.ToTypeString());
}

[TestCategory("TypeExtensions")]
[TestMethod]
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009", Justification = "Nullable value tuple type")]
public void Test_TypeExtensions_GenericTypes()
[DataRow("int?", typeof(int?))]
[DataRow("System.DateTime?", typeof(DateTime?))]
[DataRow("(int, float)", typeof((int, float)))]
[DataRow("(double?, string, int)?", typeof((double?, string, int)?))]
[DataRow("int[]", typeof(int[]))]
[DataRow("int[,]", typeof(int[,]))]
[DataRow("System.Span<float>", typeof(Span<float>))]
[DataRow("System.Memory<char>", typeof(Memory<char>))]
[DataRow("System.Collections.Generic.IEnumerable<int>", typeof(IEnumerable<int>))]
[DataRow("System.Collections.Generic.Dictionary<int, System.Collections.Generic.List<float>>", typeof(Dictionary<int, List<float>>))]
public void Test_TypeExtensions_GenericTypes(string name, Type type)
{
Assert.AreEqual("int?", typeof(int?).ToTypeString());
Assert.AreEqual("System.DateTime?", typeof(DateTime?).ToTypeString());
Assert.AreEqual("(int, float)", typeof((int, float)).ToTypeString());
Assert.AreEqual("(double?, string, int)?", typeof((double?, string, int)?).ToTypeString());
Assert.AreEqual("int[]", typeof(int[]).ToTypeString());
Assert.AreEqual(typeof(int[,]).ToTypeString(), "int[,]");
Assert.AreEqual("System.Span<float>", typeof(Span<float>).ToTypeString());
Assert.AreEqual("System.Memory<char>", typeof(Memory<char>).ToTypeString());
Assert.AreEqual("System.Collections.Generic.IEnumerable<int>", typeof(IEnumerable<int>).ToTypeString());
Assert.AreEqual(typeof(Dictionary<int, List<float>>).ToTypeString(), "System.Collections.Generic.Dictionary<int, System.Collections.Generic.List<float>>");
Assert.AreEqual(name, type.ToTypeString());
}

[TestCategory("TypeExtensions")]
[TestMethod]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal", typeof(Animal))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat", typeof(Animal.Cat))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Dog", typeof(Animal.Dog))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int?>", typeof(Animal.Rabbit<int?>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<string>", typeof(Animal.Rabbit<string>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo", typeof(Animal.Rabbit<int>.Foo))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo", typeof(Animal.Rabbit<(string, int)?>.Foo))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo<string>", typeof(Animal.Rabbit<int>.Foo<string>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo<int[]>", typeof(Animal.Rabbit<int>.Foo<int[]>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<string[]>.Foo<object>", typeof(Animal.Rabbit<string[]>.Foo<object>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo<(int, int?)>", typeof(Animal.Rabbit<(string, int)?>.Foo<(int, int?)>))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No 🦙? 😒

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jk

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too late! e74eb65 🙈

[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime>", typeof(Animal.Llama<float, DateTime>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<string, (int?, object)>", typeof(Animal.Llama<string, (int?, object)>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<string, (int?, object)?>.Foo", typeof(Animal.Llama<string, (int?, object)?>.Foo))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime>.Foo", typeof(Animal.Llama<float, DateTime>.Foo))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<string, (int?, object)?>.Foo<string>", typeof(Animal.Llama<string, (int?, object)?>.Foo<string>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime>.Foo<(float?, int)?>", typeof(Animal.Llama<float, DateTime>.Foo<(float?, int)?>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Vehicle<double>", typeof(Vehicle<double>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Vehicle<int?>[]", typeof(Vehicle<int?>[]))]
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Vehicle<int>>", typeof(List<Vehicle<int>>))]
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int?>>", typeof(List<Animal.Rabbit<int?>>))]
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime[]>>", typeof(List<Animal.Llama<float, DateTime[]>>))]
public void Test_TypeExtensions_NestedTypes(string name, Type type)
{
Assert.AreEqual(name, type.ToTypeString());
}

#pragma warning disable SA1015 // Closing generic brackets should be spaced correctly
[TestCategory("TypeExtensions")]
[TestMethod]
[DataRow("void*", typeof(void*))]
[DataRow("int**", typeof(int**))]
[DataRow("byte***", typeof(byte***))]
[DataRow("System.Guid*", typeof(Guid*))]
[DataRow("UnitTests.Extensions.Foo<int>*", typeof(Foo<int>*))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat**", typeof(Animal.Cat**))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<int>*", typeof(Animal.Cat<int>*))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<float>.Bar**", typeof(Animal.Cat<float>.Bar**))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<double>.Bar<int>***", typeof(Animal.Cat<double>.Bar<int>***))]
public void Test_TypeExtensions_PointerTypes(string name, Type type)
{
Assert.AreEqual(name, type.ToTypeString());
}
#pragma warning restore SA1015

[TestCategory("TypeExtensions")]
[TestMethod]
[DataRow("int&", typeof(int))]
[DataRow("byte&", typeof(byte))]
[DataRow("System.Guid&", typeof(Guid))]
[DataRow("UnitTests.Extensions.Foo<int>&", typeof(Foo<int>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat&", typeof(Animal.Cat))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<int>&", typeof(Animal.Cat<int>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<float>.Bar&", typeof(Animal.Cat<float>.Bar))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<double>.Bar<int>&", typeof(Animal.Cat<double>.Bar<int>))]
public void Test_TypeExtensions_RefTypes(string name, Type type)
{
Assert.AreEqual(name, type.MakeByRefType().ToTypeString());
}

private class Animal
{
public struct Cat
{
}

public struct Cat<T1>
{
public struct Bar
{
}

public struct Bar<T2>
{
}
}

public class Dog
{
}

public class Rabbit<T>
{
public class Foo
{
}

public class Foo<T2>
{
}
}

public class Llama<T1, T2>
{
public class Foo
{
}

public class Foo<T3>
{
}
}
}

private class Vehicle<T>
{
}
}

internal struct Foo<T>
{
}
}