diff --git a/examples/Console/Exceptions/Program.cs b/examples/Console/Exceptions/Program.cs index 9f54766..0bceaae 100644 --- a/examples/Console/Exceptions/Program.cs +++ b/examples/Console/Exceptions/Program.cs @@ -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(); + DoMagic(42, null, ref foo); } catch (Exception ex) { @@ -47,22 +50,23 @@ public static class Program try { - await DoMagicAsync(42, null); + await DoMagicAsync(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 result) { try { CheckCredentials(foo, bar); + result = new List(); } 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 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 CheckCredentialsAsync(List qux, T[] otherArray, string[,] corgi) { await Task.Delay(0); throw new InvalidCredentialException("The credentials are invalid."); diff --git a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs b/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs index 3fdcd48..f823a2f 100644 --- a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs +++ b/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs @@ -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) diff --git a/src/Spectre.Console/Widgets/Exceptions/TypeNameHelper.cs b/src/Spectre.Console/Widgets/Exceptions/TypeNameHelper.cs new file mode 100644 index 0000000..fa25801 --- /dev/null +++ b/src/Spectre.Console/Widgets/Exceptions/TypeNameHelper.cs @@ -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 BuiltInTypeNames = new Dictionary + { + { 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 FSharpTypeNames = new Dictionary + { + { "Unit", "void" }, + { "FSharpOption", "Option" }, + { "FSharpAsync", "Async" }, + { "FSharpOption`1", "Option" }, + { "FSharpAsync`1", "Async" }, + }; + + /// + /// Pretty print a type name. + /// + /// The . + /// true to print a fully qualified name. + /// true to include generic parameter names. + /// The pretty printed type name. + 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; + } + + /// + /// Returns a name of given generic type without '`'. + /// + 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; } + } +} \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Data/Exceptions.cs b/test/Spectre.Console.Tests/Data/Exceptions.cs index eebf59c..e05d56e 100644 --- a/test/Spectre.Console.Tests/Data/Exceptions.cs +++ b/test/Spectre.Console.Tests/Data/Exceptions.cs @@ -29,4 +29,10 @@ public static class TestExceptions throw new InvalidOperationException("Something threw!", ex); } } + + public static List GenericMethodWithOutThatThrows(out List firstFewItems) + { + firstFewItems = new List(); + throw new InvalidOperationException("Throwing!"); + } } diff --git a/test/Spectre.Console.Tests/Expectations/Exception/CallSite.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Exception/CallSite.Output.verified.txt index f2001d9..51919ab 100644 --- a/test/Spectre.Console.Tests/Expectations/Exception/CallSite.Output.verified.txt +++ b/test/Spectre.Console.Tests/Expectations/Exception/CallSite.Output.verified.txt @@ -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.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(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.b__4_0() in /xyz/ExceptionTests.cs:nn + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn diff --git a/test/Spectre.Console.Tests/Expectations/Exception/Default.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Exception/Default.Output.verified.txt index a0b81bb..be7c93f 100644 --- a/test/Spectre.Console.Tests/Expectations/Exception/Default.Output.verified.txt +++ b/test/Spectre.Console.Tests/Expectations/Exception/Default.Output.verified.txt @@ -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.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.b__0_0() in /xyz/ExceptionTests.cs:nn + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn diff --git a/test/Spectre.Console.Tests/Expectations/Exception/InnerException.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Exception/InnerException.Output.verified.txt index 3627f39..2783df5 100644 --- a/test/Spectre.Console.Tests/Expectations/Exception/InnerException.Output.verified.txt +++ b/test/Spectre.Console.Tests/Expectations/Exception/InnerException.Output.verified.txt @@ -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.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.b__3_0() in /xyz/ExceptionTests.cs:nn + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn diff --git a/test/Spectre.Console.Tests/Expectations/Exception/OutParam.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Exception/OutParam.Output.verified.txt new file mode 100644 index 0000000..6c9871f --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Exception/OutParam.Output.verified.txt @@ -0,0 +1,4 @@ +InvalidOperationException: Throwing! + at List Spectre.Console.Tests.Data.TestExceptions.GenericMethodWithOutThatThrows(out List firstFewItems) in /xyz/Exceptions.cs:nn + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__5_0() in /xyz/ExceptionTests.cs:nn + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn diff --git a/test/Spectre.Console.Tests/Expectations/Exception/ShortenedMethods.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Exception/ShortenedMethods.Output.verified.txt index 06e5cc6..5362027 100644 --- a/test/Spectre.Console.Tests/Expectations/Exception/ShortenedMethods.Output.verified.txt +++ b/test/Spectre.Console.Tests/Expectations/Exception/ShortenedMethods.Output.verified.txt @@ -1,4 +1,4 @@ System.InvalidOperationException: Throwing! - at MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn - at 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 b__2_0() in /xyz/ExceptionTests.cs:nn + at Exception GetException(Action action) in /xyz/ExceptionTests.cs:nn diff --git a/test/Spectre.Console.Tests/Expectations/Exception/ShortenedTypes.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Exception/ShortenedTypes.Output.verified.txt index 3ce4a00..860c11c 100644 --- a/test/Spectre.Console.Tests/Expectations/Exception/ShortenedTypes.Output.verified.txt +++ b/test/Spectre.Console.Tests/Expectations/Exception/ShortenedTypes.Output.verified.txt @@ -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.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.b__1_0() in /xyz/ExceptionTests.cs:nn + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn diff --git a/test/Spectre.Console.Tests/Unit/ExceptionTests.cs b/test/Spectre.Console.Tests/Unit/ExceptionTests.cs index bafaa8a..8e25816 100644 --- a/test/Spectre.Console.Tests/Unit/ExceptionTests.cs +++ b/test/Spectre.Console.Tests/Unit/ExceptionTests.cs @@ -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(out _)); + + // When + var result = console.WriteNormalizedException(dex, ExceptionFormats.ShortenTypes); + + // Then + return Verifier.Verify(result); + } + public static Exception GetException(Action action) { try