From 1ca7a9350f83ee976f049b0542f7b517b1f08af9 Mon Sep 17 00:00:00 2001 From: Ivan Maximov Date: Sat, 1 Mar 2025 22:28:33 +0300 Subject: [PATCH] Handle ordering of nullable arguments with constraints --- .../CodeTypeDeclarationExtensions.cs | 86 ++++++++++++++++--- .../ConstraintsOrdering.cs | 37 ++++++++ 2 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 src/PublicApiGeneratorTests/ConstraintsOrdering.cs diff --git a/src/PublicApiGenerator/CodeTypeDeclarationExtensions.cs b/src/PublicApiGenerator/CodeTypeDeclarationExtensions.cs index 551ff94..1e500b7 100644 --- a/src/PublicApiGenerator/CodeTypeDeclarationExtensions.cs +++ b/src/PublicApiGenerator/CodeTypeDeclarationExtensions.cs @@ -39,22 +39,24 @@ public static CodeTypeDeclarationEx Sort(this CodeTypeDeclarationEx original) sorted.UserData[key] = original.UserData[key]; } - var emptyParamList = new List(); - var sortedMembers = original.Members.OfType() .OrderBy(m => m.Attributes.HasFlag(MemberAttributes.Static)) .ThenBy(m => m.GetType().Name, StringComparer.Ordinal) .ThenBy(m => m is CodeMemberMethod ? CSharpOperatorKeyword.Get(m.Name) : m.Name, StringComparer.Ordinal) .ThenBy(m => m is CodeMemberMethod method - ? method.TypeParameters.Count - : 0) - .ThenBy(m => m is CodeMemberMethod method - ? method.Parameters.Count - : 0) + ? method.TypeParameters.Count + : 0) .ThenBy(m => m is CodeMemberMethod method - ? method.Parameters.OfType().ToList() - : emptyParamList, - new ParamListComparer()); + ? method.Parameters.Count + : 0) + .ThenBy(m => m is CodeMemberMethod method && method.Parameters.Count > 0 + ? method.Parameters.OfType().ToList() + : [], + ParamListComparer.Instance) + .ThenBy(m => m is CodeMemberMethod method && method.TypeParameters.Count > 0 + ? method.TypeParameters.OfType().ToList() + : [], + TypeParamListComparer.Instance); foreach (var member in sortedMembers) { @@ -66,6 +68,8 @@ public static CodeTypeDeclarationEx Sort(this CodeTypeDeclarationEx original) private class ParamListComparer : IComparer> { + public static readonly ParamListComparer Instance = new(); + public int Compare(List x, List y) { var paramIndex = 0; @@ -92,14 +96,21 @@ public int Compare(List x, List().ToList(); - var typeArgsY = y.TypeArguments.OfType().ToList(); + var typeArgsX = x.BaseType == "System.Nullable`1" && x.TypeArguments.Count == 1 ? [] : x.TypeArguments.OfType().ToList(); + var typeArgsY = y.BaseType == "System.Nullable`1" && y.TypeArguments.Count == 1 ? [] : y.TypeArguments.OfType().ToList(); var typeArgIndex = 0; for (; typeArgIndex < typeArgsX.Count; ++typeArgIndex) @@ -119,4 +130,53 @@ private int Compare(CodeTypeReference x, CodeTypeReference y) return typeArgIndex < typeArgsY.Count ? -1 : 0; } } + + private class TypeParamListComparer : IComparer> + { + public static readonly TypeParamListComparer Instance = new(); + + public int Compare(List x, List y) + { + var paramIndex = 0; + for (; paramIndex < x.Count; ++paramIndex) + { + var paramX = x[paramIndex]; + var paramY = y[paramIndex]; + + var name = string.CompareOrdinal(paramX.Name, paramY.Name); + if (name != 0) + { + return name; + } + + var constraints = Compare(paramX.Constraints, paramY.Constraints); + if (constraints != 0) + { + return constraints; + } + } + + return paramIndex < y.Count ? -1 : 0; + } + + private int Compare(CodeTypeReferenceCollection x, CodeTypeReferenceCollection y) + { + var count = x.Count.CompareTo(y.Count); + if (count != 0) + { + return count; + } + + for (int i = 0; i < x.Count; ++i) + { + var baseType = x[i].BaseType.CompareTo(y[i].BaseType); + if (baseType != 0) + { + return baseType; + } + } + + return 0; + } + } } diff --git a/src/PublicApiGeneratorTests/ConstraintsOrdering.cs b/src/PublicApiGeneratorTests/ConstraintsOrdering.cs new file mode 100644 index 0000000..5fb3b52 --- /dev/null +++ b/src/PublicApiGeneratorTests/ConstraintsOrdering.cs @@ -0,0 +1,37 @@ +using PublicApiGeneratorTests.Examples; + +namespace PublicApiGeneratorTests +{ + public class ConstraintsOrdering : ApiGeneratorTestsBase + { + [Fact] + public void Should_Order_Class_First() + { + AssertPublicApi(typeof(ClassWithNullableSignatureOfDifferentConstraint), +""" +namespace PublicApiGeneratorTests.Examples +{ + public static class ClassWithNullableSignatureOfDifferentConstraint + { + public static T ShouldNotBeNull([System.Diagnostics.CodeAnalysis.NotNull] this T? actual, string? customMessage = null) + where T : class { } + public static T ShouldNotBeNull([System.Diagnostics.CodeAnalysis.NotNull] this T? actual, string? customMessage = null) + where T : struct { } + } +} +"""); + } + } + + namespace Examples + { + public static class ClassWithNullableSignatureOfDifferentConstraint + { + public static T ShouldNotBeNull([System.Diagnostics.CodeAnalysis.NotNull] this T? actual, string? customMessage = null) + where T : struct => throw null; + + public static T ShouldNotBeNull([System.Diagnostics.CodeAnalysis.NotNull] this T? actual, string? customMessage = null) + where T : class => throw null; + } + } +}