Adding better type names for return types and parameters

Uses the typenamehelper from Ben.Demystifer to help break down things like generic lists into their actual type display name.
This commit is contained in:
Phil Scott
2022-02-03 13:49:33 -05:00
committed by Patrik Svensson
parent a0e20f299c
commit 78958aae27
11 changed files with 329 additions and 37 deletions

View File

@ -19,11 +19,7 @@ internal static class ExceptionFormatter
throw new ArgumentNullException(nameof(exception));
}
return new Rows(new IRenderable[]
{
GetMessage(exception, settings),
GetStackFrames(exception, settings),
}).Expand();
return new Rows(GetMessage(exception, settings), GetStackFrames(exception, settings)).Expand();
}
private static Markup GetMessage(Exception ex, ExceptionSettings settings)
@ -78,6 +74,13 @@ internal static class ExceptionFormatter
builder.Append("async ");
}
if (method is MethodInfo mi)
{
var returnParameter = mi.ReturnParameter;
builder.AppendWithStyle(styles.ParameterType, GetParameterName(returnParameter).EscapeMarkup());
builder.Append(' ');
}
builder.Append(Emphasize(methodName, new[] { '.' }, styles.Method, shortenMethods, settings));
builder.AppendWithStyle(styles.Parenthesis, "(");
AppendParameters(builder, method, settings);
@ -114,7 +117,9 @@ internal static class ExceptionFormatter
{
var typeColor = settings.Style.ParameterType.ToMarkup();
var nameColor = settings.Style.ParameterName.ToMarkup();
var parameters = method?.GetParameters().Select(x => $"[{typeColor}]{x.ParameterType.Name.EscapeMarkup()}[/] [{nameColor}]{x.Name?.EscapeMarkup()}[/]");
var parameters = method?.GetParameters()
.Select(x => $"[{typeColor}]{GetParameterName(x).EscapeMarkup()}[/] [{nameColor}]{x.Name?.EscapeMarkup()}[/]");
if (parameters != null)
{
builder.Append(string.Join(", ", parameters));
@ -150,7 +155,8 @@ internal static class ExceptionFormatter
}
}
private static string Emphasize(string input, char[] separators, Style color, bool compact, ExceptionSettings settings)
private static string Emphasize(string input, char[] separators, Style color, bool compact,
ExceptionSettings settings)
{
var builder = new StringBuilder();
@ -240,6 +246,46 @@ internal static class ExceptionFormatter
}
}
private static string GetPrefix(ParameterInfo parameter)
{
if (Attribute.IsDefined(parameter, typeof(ParamArrayAttribute), false))
{
return "params";
}
if (parameter.IsOut)
{
return "out";
}
if (parameter.IsIn)
{
return "in";
}
if (parameter.ParameterType.IsByRef)
{
return "ref";
}
return string.Empty;
}
private static string GetParameterName(ParameterInfo parameter)
{
var prefix = GetPrefix(parameter);
var parameterType = parameter.ParameterType;
if (parameterType.IsByRef && parameterType.GetElementType() is { } elementType)
{
parameterType = elementType;
}
var typeName = TypeNameHelper.GetTypeDisplayName(parameterType, false, true);
return string.IsNullOrWhiteSpace(prefix) ? typeName : $"{prefix} {typeName}";
}
private static string GetMethodName(ref MethodBase method, out bool isAsync)
{
var declaringType = method.DeclaringType;
@ -270,9 +316,9 @@ internal static class ExceptionFormatter
builder.Append(method.Name);
if (method.IsGenericMethod)
{
builder.Append('[');
builder.Append('<');
builder.Append(string.Join(",", method.GetGenericArguments().Select(t => t.Name)));
builder.Append(']');
builder.Append('>');
}
return builder.ToString();
@ -281,7 +327,8 @@ internal static class ExceptionFormatter
private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType)
{
// https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L400-L455
declaringType = method.DeclaringType ?? throw new ArgumentException("Method must have a declaring type.", nameof(method));
declaringType = method.DeclaringType ??
throw new ArgumentException("Method must have a declaring type.", nameof(method));
var parentType = declaringType.DeclaringType;
if (parentType == null)

View File

@ -0,0 +1,216 @@
namespace Spectre.Console;
internal static class TypeNameHelper
{
// from https://github.com/benaadams/Ben.Demystifier/blob/main/src/Ben.Demystifier/TypeNameHelper.cs
// which was adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.TypeNameHelper.Sources/TypeNameHelper.cs
public static readonly Dictionary<Type, string> BuiltInTypeNames = new Dictionary<Type, string>
{
{ typeof(void), "void" },
{ typeof(bool), "bool" },
{ typeof(byte), "byte" },
{ typeof(char), "char" },
{ typeof(decimal), "decimal" },
{ typeof(double), "double" },
{ typeof(float), "float" },
{ typeof(int), "int" },
{ typeof(long), "long" },
{ typeof(object), "object" },
{ typeof(sbyte), "sbyte" },
{ typeof(short), "short" },
{ typeof(string), "string" },
{ typeof(uint), "uint" },
{ typeof(ulong), "ulong" },
{ typeof(ushort), "ushort" },
};
public static readonly Dictionary<string, string> FSharpTypeNames = new Dictionary<string, string>
{
{ "Unit", "void" },
{ "FSharpOption", "Option" },
{ "FSharpAsync", "Async" },
{ "FSharpOption`1", "Option" },
{ "FSharpAsync`1", "Async" },
};
/// <summary>
/// Pretty print a type name.
/// </summary>
/// <param name="type">The <see cref="Type"/>.</param>
/// <param name="fullName"><c>true</c> to print a fully qualified name.</param>
/// <param name="includeGenericParameterNames"><c>true</c> to include generic parameter names.</param>
/// <returns>The pretty printed type name.</returns>
public static string GetTypeDisplayName(Type type, bool fullName = true, bool includeGenericParameterNames = false)
{
var builder = new StringBuilder();
ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames));
return builder.ToString();
}
public static StringBuilder AppendTypeDisplayName(this StringBuilder builder, Type type, bool fullName = true,
bool includeGenericParameterNames = false)
{
ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames));
return builder;
}
/// <summary>
/// Returns a name of given generic type without '`'.
/// </summary>
public static string GetTypeNameForGenericType(Type type)
{
if (!type.IsGenericType)
{
throw new ArgumentException("The given type should be generic", nameof(type));
}
var genericPartIndex = type.Name.IndexOf('`');
return (genericPartIndex >= 0) ? type.Name.Substring(0, genericPartIndex) : type.Name;
}
private static void ProcessType(StringBuilder builder, Type type, DisplayNameOptions options)
{
if (type.IsGenericType)
{
var underlyingType = Nullable.GetUnderlyingType(type);
if (underlyingType != null)
{
ProcessType(builder, underlyingType, options);
builder.Append('?');
}
else
{
var genericArguments = type.GetGenericArguments();
ProcessGenericType(builder, type, genericArguments, genericArguments.Length, options);
}
}
else if (type.IsArray)
{
ProcessArrayType(builder, type, options);
}
else if (BuiltInTypeNames.TryGetValue(type, out var builtInName))
{
builder.Append(builtInName);
}
else if (type.Namespace == nameof(System))
{
builder.Append(type.Name);
}
else if (type.Assembly.ManifestModule.Name == "FSharp.Core.dll"
&& FSharpTypeNames.TryGetValue(type.Name, out builtInName))
{
builder.Append(builtInName);
}
else if (type.IsGenericParameter)
{
if (options.IncludeGenericParameterNames)
{
builder.Append(type.Name);
}
}
else
{
builder.Append(options.FullName ? type.FullName ?? type.Name : type.Name);
}
}
private static void ProcessArrayType(StringBuilder builder, Type type, DisplayNameOptions options)
{
var innerType = type;
while (innerType.IsArray)
{
if (innerType.GetElementType() is { } inner)
{
innerType = inner;
}
}
ProcessType(builder, innerType, options);
while (type.IsArray)
{
builder.Append('[');
builder.Append(',', type.GetArrayRank() - 1);
builder.Append(']');
if (type.GetElementType() is not { } elementType)
{
break;
}
type = elementType;
}
}
private static void ProcessGenericType(StringBuilder builder, Type type, Type[] genericArguments, int length,
DisplayNameOptions options)
{
var offset = 0;
if (type.IsNested && type.DeclaringType is not null)
{
offset = type.DeclaringType.GetGenericArguments().Length;
}
if (options.FullName)
{
if (type.IsNested && type.DeclaringType is not null)
{
ProcessGenericType(builder, type.DeclaringType, genericArguments, offset, options);
builder.Append('+');
}
else if (!string.IsNullOrEmpty(type.Namespace))
{
builder.Append(type.Namespace);
builder.Append('.');
}
}
var genericPartIndex = type.Name.IndexOf('`');
if (genericPartIndex <= 0)
{
builder.Append(type.Name);
return;
}
if (type.Assembly.ManifestModule.Name == "FSharp.Core.dll"
&& FSharpTypeNames.TryGetValue(type.Name, out var builtInName))
{
builder.Append(builtInName);
}
else
{
builder.Append(type.Name, 0, genericPartIndex);
}
builder.Append('<');
for (var i = offset; i < length; i++)
{
ProcessType(builder, genericArguments[i], options);
if (i + 1 == length)
{
continue;
}
builder.Append(',');
if (options.IncludeGenericParameterNames || !genericArguments[i + 1].IsGenericParameter)
{
builder.Append(' ');
}
}
builder.Append('>');
}
private struct DisplayNameOptions
{
public DisplayNameOptions(bool fullName, bool includeGenericParameterNames)
{
FullName = fullName;
IncludeGenericParameterNames = includeGenericParameterNames;
}
public bool FullName { get; }
public bool IncludeGenericParameterNames { get; }
}
}