diff --git a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs b/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs index f823a2f..1d749a4 100644 --- a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs +++ b/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs @@ -276,16 +276,85 @@ internal static class ExceptionFormatter var prefix = GetPrefix(parameter); var parameterType = parameter.ParameterType; - if (parameterType.IsByRef && parameterType.GetElementType() is { } elementType) + string typeName; + if (parameterType.IsGenericType && TryGetTupleName(parameter, parameterType, out var s)) { - parameterType = elementType; + typeName = s; + } + else + { + if (parameterType.IsByRef && parameterType.GetElementType() is { } elementType) + { + parameterType = elementType; + } + + typeName = TypeNameHelper.GetTypeDisplayName(parameterType); } - var typeName = TypeNameHelper.GetTypeDisplayName(parameterType, false, true); - return string.IsNullOrWhiteSpace(prefix) ? typeName : $"{prefix} {typeName}"; } + private static bool TryGetTupleName(ParameterInfo parameter, Type parameterType, [NotNullWhen(true)] out string? tupleName) + { + var customAttribs = parameter.GetCustomAttributes(inherit: false); + + var tupleNameAttribute = customAttribs + .OfType() + .FirstOrDefault(a => + { + var attributeType = a.GetType(); + return attributeType.Namespace == "System.Runtime.CompilerServices" && + attributeType.Name == "TupleElementNamesAttribute"; + }); + + if (tupleNameAttribute != null) + { + var propertyInfo = tupleNameAttribute.GetType() + .GetProperty("TransformNames", BindingFlags.Instance | BindingFlags.Public)!; + var tupleNames = propertyInfo.GetValue(tupleNameAttribute) as IList; + if (tupleNames?.Count > 0) + { + var args = parameterType.GetGenericArguments(); + var sb = new StringBuilder(); + + sb.Append('('); + for (var i = 0; i < args.Length; i++) + { + if (i > 0) + { + sb.Append(", "); + } + + sb.Append(TypeNameHelper.GetTypeDisplayName(args[i])); + + if (i >= tupleNames.Count) + { + continue; + } + + var argName = tupleNames[i]; + + sb.Append(' '); + sb.Append(argName); + } + + sb.Append(')'); + + tupleName = sb.ToString(); + return true; + } + } + else if (parameterType.Namespace == "System" && parameterType.Name.Contains("ValueTuple`")) + { + var args = parameterType.GetGenericArguments().Select(i => TypeNameHelper.GetTypeDisplayName(i)); + tupleName = $"({string.Join(", ", args)})"; + return true; + } + + tupleName = null; + return false; + } + private static string GetMethodName(ref MethodBase method, out bool isAsync) { var declaringType = method.DeclaringType; diff --git a/src/Spectre.Console/Widgets/Exceptions/TypeNameHelper.cs b/src/Spectre.Console/Widgets/Exceptions/TypeNameHelper.cs index fa25801..41eb78a 100644 --- a/src/Spectre.Console/Widgets/Exceptions/TypeNameHelper.cs +++ b/src/Spectre.Console/Widgets/Exceptions/TypeNameHelper.cs @@ -40,35 +40,13 @@ internal static class TypeNameHelper /// 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) + public static string GetTypeDisplayName(Type type, bool fullName = false, bool includeGenericParameterNames = true) { 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) diff --git a/test/Spectre.Console.Tests/Data/Exceptions.cs b/test/Spectre.Console.Tests/Data/Exceptions.cs index e05d56e..b011a90 100644 --- a/test/Spectre.Console.Tests/Data/Exceptions.cs +++ b/test/Spectre.Console.Tests/Data/Exceptions.cs @@ -35,4 +35,10 @@ public static class TestExceptions firstFewItems = new List(); throw new InvalidOperationException("Throwing!"); } + + public static (string Key, List Values) GetTuplesWithInnerException((int First, string Second) myValue) + { + MethodThatThrows(0); + return ("key", new List()); + } } diff --git a/test/Spectre.Console.Tests/Expectations/Exception/Tuple.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Exception/Tuple.Output.verified.txt new file mode 100644 index 0000000..acd2a36 --- /dev/null +++ b/test/Spectre.Console.Tests/Expectations/Exception/Tuple.Output.verified.txt @@ -0,0 +1,5 @@ +InvalidOperationException: Throwing! + at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn + at (string Key, List Values) Spectre.Console.Tests.Data.TestExceptions.GetTuplesWithInnerException((int First, string Second) myValue) in /xyz/Exceptions.cs:nn + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__6_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 8e25816..5aa2b75 100644 --- a/test/Spectre.Console.Tests/Unit/ExceptionTests.cs +++ b/test/Spectre.Console.Tests/Unit/ExceptionTests.cs @@ -94,6 +94,21 @@ public sealed class ExceptionTests return Verifier.Verify(result); } + [Fact] + [Expectation("Tuple")] + public Task Should_Write_Exception_With_Tuple_Return() + { + // Given + var console = new TestConsole().Width(1024); + var dex = GetException(() => TestExceptions.GetTuplesWithInnerException((0, "value"))); + + // When + var result = console.WriteNormalizedException(dex, ExceptionFormats.ShortenTypes); + + // Then + return Verifier.Verify(result); + } + public static Exception GetException(Action action) { try