From e6fdd3fd46aac3282c9dc5379f2788565a824df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 18 Oct 2025 16:59:02 +0200 Subject: [PATCH 1/3] fix: formatting of nested types within generic types --- .../Formatting/ValueFormatters.Type.cs | 34 ++++++++++++++-- .../Formatting/ValueFormatters.TypeTests.cs | 39 ++++++++++++++++++- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs b/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs index c4cd99956..0a2bc5079 100644 --- a/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs +++ b/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; using System.Text; using aweXpect.Core.Helpers; @@ -103,10 +104,16 @@ public static void Format( FormatType(value, stringBuilder); } -#pragma warning disable S3776 // https://rules.sonarsource.com/csharp/RSPEC-3776 private static void FormatType( Type value, StringBuilder stringBuilder) + => FormatType(value, stringBuilder, null); + +#pragma warning disable S3776 // https://rules.sonarsource.com/csharp/RSPEC-3776 + private static void FormatType( + Type value, + StringBuilder stringBuilder, + Type[]? genericArguments) { if (value == typeof(void)) { @@ -125,7 +132,15 @@ private static void FormatType( { if (value.IsNested && value.DeclaringType is not null) { - FormatType(value.DeclaringType, stringBuilder); + Type[]? declaringTypeGenericArguments = null; + if (value.IsGenericType) + { + int arity = GetArityOfGenericParameters(value.DeclaringType); + declaringTypeGenericArguments = [..value.GenericTypeArguments.Take(arity)]; + genericArguments = [..(genericArguments ?? value.GenericTypeArguments).Skip(arity)]; + } + + FormatType(value.DeclaringType, stringBuilder, declaringTypeGenericArguments); stringBuilder.Append('.'); } @@ -133,9 +148,15 @@ private static void FormatType( { Type genericTypeDefinition = value.GetGenericTypeDefinition(); stringBuilder.Append(genericTypeDefinition.Name.SubstringUntilFirst('`')); + if (genericArguments?.Length == 0) + { + return; + } + stringBuilder.Append('<'); bool isFirstArgument = true; - foreach (Type argument in value.GetGenericArguments()) + genericArguments ??= value.GetGenericArguments(); + foreach (var argument in genericArguments) { if (!isFirstArgument) { @@ -158,6 +179,11 @@ private static void FormatType( } } #pragma warning restore S3776 + + private static int GetArityOfGenericParameters(Type type) + => type.Name.LastIndexOf('`') != -1 + ? int.Parse(type.Name[(type.Name.LastIndexOf('`') + 1)..], CultureInfo.InvariantCulture) + : 0; private static bool AppendedPrimitiveAlias(Type value, StringBuilder stringBuilder) { diff --git a/Tests/aweXpect.Core.Tests/Formatting/ValueFormatters.TypeTests.cs b/Tests/aweXpect.Core.Tests/Formatting/ValueFormatters.TypeTests.cs index 265a15426..0b3f7ab0b 100644 --- a/Tests/aweXpect.Core.Tests/Formatting/ValueFormatters.TypeTests.cs +++ b/Tests/aweXpect.Core.Tests/Formatting/ValueFormatters.TypeTests.cs @@ -26,6 +26,38 @@ public async Task NestedGenericTypes_ShouldIncludeTheDeclaringTypeAndName() await That(sb.ToString()).IsEqualTo(expectedResult); } + [Fact] + public async Task NestedGenericTypeInGenericTypes_ShouldIncludeTheDeclaringTypeAndName() + { + Type value = typeof(NestedGenericType.InnerClass); + string expectedResult = "ValueFormatters.TypeTests.NestedGenericType.InnerClass"; + StringBuilder sb = new(); + + string result = Formatter.Format(value); + string objectResult = Formatter.Format((object?)value); + Formatter.Format(sb, value); + + await That(result).IsEqualTo(expectedResult); + await That(objectResult).IsEqualTo(expectedResult); + await That(sb.ToString()).IsEqualTo(expectedResult); + } + + [Fact] + public async Task NestedTypeInGenericTypes_ShouldIncludeTheDeclaringTypeAndName() + { + Type value = typeof(NestedGenericType.InnerRegularClass); + string expectedResult = "ValueFormatters.TypeTests.NestedGenericType.InnerRegularClass"; + StringBuilder sb = new(); + + string result = Formatter.Format(value); + string objectResult = Formatter.Format((object?)value); + Formatter.Format(sb, value); + + await That(result).IsEqualTo(expectedResult); + await That(objectResult).IsEqualTo(expectedResult); + await That(sb.ToString()).IsEqualTo(expectedResult); + } + [Fact] public async Task NestedTypes_ShouldIncludeTheDeclaringTypeAndName() { @@ -259,7 +291,12 @@ public async Task WhenVoid_ShouldUseSimpleName() } // ReSharper disable once UnusedTypeParameter - private class NestedGenericType; + private class NestedGenericType + { + public sealed class InnerClass; + + public sealed class InnerRegularClass; + } // ReSharper disable once UnusedParameter.Local private static void DummyMethodToGetSpecialTypes(TParameter value) From deeaf5e4673fce4cbf96661e1993fc9e036f0bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 18 Oct 2025 17:02:00 +0200 Subject: [PATCH 2/3] Update Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Formatting/ValueFormatters.Type.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs b/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs index 0a2bc5079..b9cead08c 100644 --- a/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs +++ b/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs @@ -181,9 +181,18 @@ private static void FormatType( #pragma warning restore S3776 private static int GetArityOfGenericParameters(Type type) - => type.Name.LastIndexOf('`') != -1 - ? int.Parse(type.Name[(type.Name.LastIndexOf('`') + 1)..], CultureInfo.InvariantCulture) - : 0; + { + int tickIndex = type.Name.LastIndexOf('`'); + if (tickIndex != -1) + { + var arityStr = type.Name[(tickIndex + 1)..]; + if (int.TryParse(arityStr, NumberStyles.None, CultureInfo.InvariantCulture, out int arity)) + { + return arity; + } + } + return 0; + } private static bool AppendedPrimitiveAlias(Type value, StringBuilder stringBuilder) { From fc2536d39d3e1c5abdc6e95621f92386a3690cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 18 Oct 2025 17:05:12 +0200 Subject: [PATCH 3/3] Fix review issues --- .../Formatting/ValueFormatters.Type.cs | 16 ++++---- .../Core/StringDifferenceTests.cs | 14 +++---- .../Formatting/ValueFormatters.TypeTests.cs | 40 ++++++++++--------- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs b/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs index b9cead08c..3a9f9753a 100644 --- a/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs +++ b/Source/aweXpect.Core/Formatting/ValueFormatters.Type.cs @@ -136,10 +136,10 @@ private static void FormatType( if (value.IsGenericType) { int arity = GetArityOfGenericParameters(value.DeclaringType); - declaringTypeGenericArguments = [..value.GenericTypeArguments.Take(arity)]; - genericArguments = [..(genericArguments ?? value.GenericTypeArguments).Skip(arity)]; + declaringTypeGenericArguments = [..value.GenericTypeArguments.Take(arity),]; + genericArguments = [..(genericArguments ?? value.GenericTypeArguments).Skip(arity),]; } - + FormatType(value.DeclaringType, stringBuilder, declaringTypeGenericArguments); stringBuilder.Append('.'); } @@ -152,11 +152,11 @@ private static void FormatType( { return; } - + stringBuilder.Append('<'); bool isFirstArgument = true; genericArguments ??= value.GetGenericArguments(); - foreach (var argument in genericArguments) + foreach (Type? argument in genericArguments) { if (!isFirstArgument) { @@ -179,18 +179,19 @@ private static void FormatType( } } #pragma warning restore S3776 - + private static int GetArityOfGenericParameters(Type type) { int tickIndex = type.Name.LastIndexOf('`'); if (tickIndex != -1) { - var arityStr = type.Name[(tickIndex + 1)..]; + string? arityStr = type.Name[(tickIndex + 1)..]; if (int.TryParse(arityStr, NumberStyles.None, CultureInfo.InvariantCulture, out int arity)) { return arity; } } + return 0; } @@ -211,6 +212,7 @@ private static bool AppendedPrimitiveAlias(Type value, StringBuilder stringBuild stringBuilder.Append(underlyingAlias).Append('?'); return true; } + FormatType(underlyingType, stringBuilder); stringBuilder.Append('?'); return true; diff --git a/Tests/aweXpect.Core.Tests/Core/StringDifferenceTests.cs b/Tests/aweXpect.Core.Tests/Core/StringDifferenceTests.cs index 15edf3477..aba6f0203 100644 --- a/Tests/aweXpect.Core.Tests/Core/StringDifferenceTests.cs +++ b/Tests/aweXpect.Core.Tests/Core/StringDifferenceTests.cs @@ -230,19 +230,19 @@ await That(result).IsEqualTo( [Fact] public async Task WhenStringContainsWhitespace_ShouldPositionArrowsCorrectly() { - const string actual = "foo\rbar\nBAZ"; - const string expected = "foo\rbar\nbaz"; + const string actual = "foo\r\tbar\nBAZ"; + const string expected = "foo\r\tbar\nbaz"; StringDifference sut = new(actual, expected); - await That(sut.IndexOfFirstMismatch(StringDifference.MatchType.Equality)).IsEqualTo(8); + await That(sut.IndexOfFirstMismatch(StringDifference.MatchType.Equality)).IsEqualTo(9); await That(sut.ToString()).IsEqualTo( """ differs on line 2 and column 1: - ↓ (actual) - "foo\rbar\nBAZ" - "foo\rbar\nbaz" - ↑ (expected) + ↓ (actual) + "foo\r\tbar\nBAZ" + "foo\r\tbar\nbaz" + ↑ (expected) """); } diff --git a/Tests/aweXpect.Core.Tests/Formatting/ValueFormatters.TypeTests.cs b/Tests/aweXpect.Core.Tests/Formatting/ValueFormatters.TypeTests.cs index 0b3f7ab0b..b050a6ba4 100644 --- a/Tests/aweXpect.Core.Tests/Formatting/ValueFormatters.TypeTests.cs +++ b/Tests/aweXpect.Core.Tests/Formatting/ValueFormatters.TypeTests.cs @@ -11,10 +11,11 @@ public partial class ValueFormatters public sealed class TypeTests { [Fact] - public async Task NestedGenericTypes_ShouldIncludeTheDeclaringTypeAndName() + public async Task NestedGenericTypeInGenericTypes_ShouldIncludeTheDeclaringTypeAndName() { - Type value = typeof(NestedGenericType); - string expectedResult = "ValueFormatters.TypeTests.NestedGenericType"; + Type value = typeof(NestedGenericType.InnerClass); + string expectedResult = + "ValueFormatters.TypeTests.NestedGenericType.InnerClass"; StringBuilder sb = new(); string result = Formatter.Format(value); @@ -27,10 +28,10 @@ public async Task NestedGenericTypes_ShouldIncludeTheDeclaringTypeAndName() } [Fact] - public async Task NestedGenericTypeInGenericTypes_ShouldIncludeTheDeclaringTypeAndName() + public async Task NestedGenericTypes_ShouldIncludeTheDeclaringTypeAndName() { - Type value = typeof(NestedGenericType.InnerClass); - string expectedResult = "ValueFormatters.TypeTests.NestedGenericType.InnerClass"; + Type value = typeof(NestedGenericType); + string expectedResult = "ValueFormatters.TypeTests.NestedGenericType"; StringBuilder sb = new(); string result = Formatter.Format(value); @@ -46,7 +47,8 @@ public async Task NestedGenericTypeInGenericTypes_ShouldIncludeTheDeclaringTypeA public async Task NestedTypeInGenericTypes_ShouldIncludeTheDeclaringTypeAndName() { Type value = typeof(NestedGenericType.InnerRegularClass); - string expectedResult = "ValueFormatters.TypeTests.NestedGenericType.InnerRegularClass"; + string expectedResult = + "ValueFormatters.TypeTests.NestedGenericType.InnerRegularClass"; StringBuilder sb = new(); string result = Formatter.Format(value); @@ -241,34 +243,34 @@ public async Task WhenGenericParameter_ShouldUseOnlyName() } [Fact] - public async Task WhenNullable_ShouldUseQuestionMarkSyntax() + public async Task WhenNull_ShouldUseDefaultNullString() { - string expectedResult = "DateTime?"; - Type value = typeof(DateTime?); + Type? value = null; StringBuilder sb = new(); string result = Formatter.Format(value); string objectResult = Formatter.Format((object?)value); Formatter.Format(sb, value); - await That(result).IsEqualTo(expectedResult); - await That(objectResult).IsEqualTo(expectedResult); - await That(sb.ToString()).IsEqualTo(expectedResult); + await That(result).IsEqualTo(ValueFormatter.NullString); + await That(objectResult).IsEqualTo(ValueFormatter.NullString); + await That(sb.ToString()).IsEqualTo(ValueFormatter.NullString); } [Fact] - public async Task WhenNull_ShouldUseDefaultNullString() + public async Task WhenNullable_ShouldUseQuestionMarkSyntax() { - Type? value = null; + string expectedResult = "DateTime?"; + Type value = typeof(DateTime?); StringBuilder sb = new(); string result = Formatter.Format(value); string objectResult = Formatter.Format((object?)value); Formatter.Format(sb, value); - await That(result).IsEqualTo(ValueFormatter.NullString); - await That(objectResult).IsEqualTo(ValueFormatter.NullString); - await That(sb.ToString()).IsEqualTo(ValueFormatter.NullString); + await That(result).IsEqualTo(expectedResult); + await That(objectResult).IsEqualTo(expectedResult); + await That(sb.ToString()).IsEqualTo(expectedResult); } [Fact] @@ -405,7 +407,7 @@ public static TheoryData SimpleTypes }, { typeof(void), "void" - } + }, }; } }