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

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Authentication;
using System.Threading.Tasks;
@ -10,7 +12,8 @@ public static class Program
{
try
{
DoMagic(42, null);
var foo = new List<string>();
DoMagic(42, null, ref foo);
}
catch (Exception ex)
{
@ -47,22 +50,23 @@ public static class Program
try
{
await DoMagicAsync(42, null);
await DoMagicAsync<int>(42, null);
}
catch (Exception ex)
{
AnsiConsole.WriteLine();
AnsiConsole.Write(new Rule("Async").LeftAligned());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex);
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths);
}
}
private static void DoMagic(int foo, string[,] bar)
private static void DoMagic(int foo, string[,] bar, ref List<string> result)
{
try
{
CheckCredentials(foo, bar);
result = new List<string>();
}
catch (Exception ex)
{
@ -70,16 +74,16 @@ public static class Program
}
}
private static void CheckCredentials(int qux, string[,] corgi)
private static bool CheckCredentials(int? qux, string[,] corgi)
{
throw new InvalidCredentialException("The credentials are invalid.");
}
private static async Task DoMagicAsync(int foo, string[,] bar)
private static async Task DoMagicAsync<T>(T foo, string[,] bar)
{
try
{
await CheckCredentialsAsync(foo, bar);
await CheckCredentialsAsync(new[] { foo }.ToList(), new []{ foo }, bar);
}
catch (Exception ex)
{
@ -87,7 +91,7 @@ public static class Program
}
}
private static async Task CheckCredentialsAsync(int qux, string[,] corgi)
private static async Task<bool> CheckCredentialsAsync<T>(List<T> qux, T[] otherArray, string[,] corgi)
{
await Task.Delay(0);
throw new InvalidCredentialException("The credentials are invalid.");

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; }
}
}

View File

@ -29,4 +29,10 @@ public static class TestExceptions
throw new InvalidOperationException("Something threw!", ex);
}
}
public static List<T> GenericMethodWithOutThatThrows<T>(out List<T> firstFewItems)
{
firstFewItems = new List<T>();
throw new InvalidOperationException("Throwing!");
}
}

View File

@ -1,7 +1,7 @@
System.InvalidOperationException: Something threw!
System.InvalidOperationException: Throwing!
at Spectre.Console.Tests.Data.TestExceptions.GenericMethodThatThrows[T0,T1,TRet](Nullable`1 number) in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exceptions_With_Generic_Type_Parameters_In_Callsite_As_Expected>b__4_0() in /xyz/ExceptionTests.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn
at bool Spectre.Console.Tests.Data.TestExceptions.GenericMethodThatThrows<T0,T1,TRet>(int? number) in /xyz/Exceptions.cs:nn
at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:nn
at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:nn
at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exceptions_With_Generic_Type_Parameters_In_Callsite_As_Expected>b__4_0() in /xyz/ExceptionTests.cs:nn
at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn

View File

@ -1,4 +1,4 @@
System.InvalidOperationException: Throwing!
at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception>b__0_0() in /xyz/ExceptionTests.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn
at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn
at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception>b__0_0() in /xyz/ExceptionTests.cs:nn
at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn

View File

@ -1,7 +1,7 @@
System.InvalidOperationException: Something threw!
System.InvalidOperationException: Throwing!
at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Inner_Exception>b__3_0() in /xyz/ExceptionTests.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn
at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn
at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn
at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn
at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Inner_Exception>b__3_0() in /xyz/ExceptionTests.cs:nn
at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn

View File

@ -0,0 +1,4 @@
InvalidOperationException: Throwing!
at List<T> Spectre.Console.Tests.Data.TestExceptions.GenericMethodWithOutThatThrows<T>(out List<T> firstFewItems) in /xyz/Exceptions.cs:nn
at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Output_Param>b__5_0() in /xyz/ExceptionTests.cs:nn
at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn

View File

@ -1,4 +1,4 @@
System.InvalidOperationException: Throwing!
at MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn
at <Should_Write_Exception_With_Shortened_Methods>b__2_0() in /xyz/ExceptionTests.cs:nn
at GetException(Action action) in /xyz/ExceptionTests.cs:nn
at bool MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn
at void <Should_Write_Exception_With_Shortened_Methods>b__2_0() in /xyz/ExceptionTests.cs:nn
at Exception GetException(Action action) in /xyz/ExceptionTests.cs:nn

View File

@ -1,4 +1,4 @@
InvalidOperationException: Throwing!
at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Shortened_Types>b__1_0() in /xyz/ExceptionTests.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn
at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn
at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Shortened_Types>b__1_0() in /xyz/ExceptionTests.cs:nn
at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn

View File

@ -79,6 +79,21 @@ public sealed class ExceptionTests
return Verifier.Verify(result);
}
[Fact]
[Expectation("OutParam")]
public Task Should_Write_Exception_With_Output_Param()
{
// Given
var console = new TestConsole().Width(1024);
var dex = GetException(() => TestExceptions.GenericMethodWithOutThatThrows<int>(out _));
// When
var result = console.WriteNormalizedException(dex, ExceptionFormats.ShortenTypes);
// Then
return Verifier.Verify(result);
}
public static Exception GetException(Action action)
{
try