From 5acdffeb4cfa18ea2d8a7e95da212819b139bb2d Mon Sep 17 00:00:00 2001 From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:17:46 +1000 Subject: [PATCH 1/4] Handle generic types in method deduplication This fixes an error where methods with the same parameters but different _generic type parameters_ were being treated as identical during deduplication, resulting in the loss of interface methods. The fix is to use `genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters` when constucting the `SymbolDisplayFormat` used for deduplication. Before: `public void AMethod(Func getValue) {}` produced a deduplication signature of `AMethod(Func)` After: `public void AMethod(Func getValue) {}` produced a deduplication signature of `AMethod(Func)` --- .../AutomaticInterface/Builder.cs | 1 + AutomaticInterface/Tests/GeneratorTests.cs | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/AutomaticInterface/AutomaticInterface/Builder.cs b/AutomaticInterface/AutomaticInterface/Builder.cs index a75cd87..839183f 100644 --- a/AutomaticInterface/AutomaticInterface/Builder.cs +++ b/AutomaticInterface/AutomaticInterface/Builder.cs @@ -13,6 +13,7 @@ public static class Builder private static readonly SymbolDisplayFormat MethodSignatureDisplayFormat = new( + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, memberOptions: SymbolDisplayMemberOptions.IncludeParameters, parameterOptions: SymbolDisplayParameterOptions.IncludeType | SymbolDisplayParameterOptions.IncludeParamsRefOut diff --git a/AutomaticInterface/Tests/GeneratorTests.cs b/AutomaticInterface/Tests/GeneratorTests.cs index 9367e9a..68e26a1 100644 --- a/AutomaticInterface/Tests/GeneratorTests.cs +++ b/AutomaticInterface/Tests/GeneratorTests.cs @@ -1787,6 +1787,55 @@ public partial interface IDemoClass GenerateCode(code).Should().Be(expected); } + [Fact] + public void WorksWithGenericParameterOverloads() + { + const string code = """ + + using AutomaticInterface; + + namespace AutomaticInterfaceExample; + + [GenerateAutomaticInterface] + public class DemoClass + { + public void AMethod(Func getValue) {} + + public void AMethod(Func getValue) {} + } + + """; + + const string expected = """ + //-------------------------------------------------------------------------------------------------- + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. + // + //-------------------------------------------------------------------------------------------------- + + using System.CodeDom.Compiler; + using AutomaticInterface; + + namespace AutomaticInterfaceExample + { + [GeneratedCode("AutomaticInterface", "")] + public partial interface IDemoClass + { + /// + void AMethod(Func getValue); + + /// + void AMethod(Func getValue); + + } + } + + """; + GenerateCode(code).Should().Be(expected); + } + [Fact] public void WorksWithEventOverrides() { From c660b1453142630e82a5f660dcd33c47db66b247 Mon Sep 17 00:00:00 2001 From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:28:33 +1000 Subject: [PATCH 2/4] Bump version; update package release notes --- AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj b/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj index ad50e30..f076459 100644 --- a/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj +++ b/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj @@ -25,7 +25,7 @@ MIT True latest-Recommended - 5.0.1 + 5.0.2 README.md true 1701;1702;NU5128 From 45f3d3a2ccf08cd0bf23e78943df6d4bbe198878 Mon Sep 17 00:00:00 2001 From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com> Date: Fri, 16 Aug 2024 07:37:19 +1000 Subject: [PATCH 3/4] Fully qualify type names to prevent types with the same name from being seen as identical - Updated the test to show that nested generics work correctly - Added test to ensure same-named generic parameters are distinguishable from one another --- .../AutomaticInterface/Builder.cs | 3 +- AutomaticInterface/Tests/GeneratorTests.cs | 64 +++++++++++++++++-- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/AutomaticInterface/AutomaticInterface/Builder.cs b/AutomaticInterface/AutomaticInterface/Builder.cs index 839183f..6c9b7ce 100644 --- a/AutomaticInterface/AutomaticInterface/Builder.cs +++ b/AutomaticInterface/AutomaticInterface/Builder.cs @@ -16,7 +16,8 @@ public static class Builder genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, memberOptions: SymbolDisplayMemberOptions.IncludeParameters, parameterOptions: SymbolDisplayParameterOptions.IncludeType - | SymbolDisplayParameterOptions.IncludeParamsRefOut + | SymbolDisplayParameterOptions.IncludeParamsRefOut, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces ); private static readonly SymbolDisplayFormat TypeDisplayFormat = diff --git a/AutomaticInterface/Tests/GeneratorTests.cs b/AutomaticInterface/Tests/GeneratorTests.cs index 68e26a1..6e6abe2 100644 --- a/AutomaticInterface/Tests/GeneratorTests.cs +++ b/AutomaticInterface/Tests/GeneratorTests.cs @@ -1799,9 +1799,9 @@ namespace AutomaticInterfaceExample; [GenerateAutomaticInterface] public class DemoClass { - public void AMethod(Func getValue) {} + public void AMethod(Func> getValue) {} - public void AMethod(Func getValue) {} + public void AMethod(Func> getValue) {} } """; @@ -1815,19 +1815,71 @@ public void AMethod(Func getValue) {} // //-------------------------------------------------------------------------------------------------- - using System.CodeDom.Compiler; + namespace AutomaticInterfaceExample + { + [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")] + public partial interface IDemoClass + { + /// + void AMethod(Func> getValue); + + /// + void AMethod(Func> getValue); + + } + } + + """; + GenerateCode(code).Should().Be(expected); + } + + [Fact] + public void WorksWithGenericParameterOverloadsWithIdenticalTypeNames() + { + const string code = """ + using AutomaticInterface; namespace AutomaticInterfaceExample { - [GeneratedCode("AutomaticInterface", "")] + namespace Types1 { + public class Model; + } + + namespace Types2 { + public class Model; + } + + [GenerateAutomaticInterface] + public class DemoClass + { + public void AMethod(Func> getValue) {} + + public void AMethod(Func> getValue) {} + } + } + + """; + + const string expected = """ + //-------------------------------------------------------------------------------------------------- + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. + // + //-------------------------------------------------------------------------------------------------- + + namespace AutomaticInterfaceExample + { + [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")] public partial interface IDemoClass { /// - void AMethod(Func getValue); + void AMethod(Func> getValue); /// - void AMethod(Func getValue); + void AMethod(Func> getValue); } } From 1ffa50b39f3d5264d8e125c32700f049eb0ea35e Mon Sep 17 00:00:00 2001 From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com> Date: Mon, 2 Sep 2024 20:50:25 +1000 Subject: [PATCH 4/4] Unify `MethodSignatureDisplayFormat` and `TypeDisplayFormat` into a single `FullyQualifiedDisplayFormat` These two display formats were used for method deduplication and generating type signatures from strings. It makes sense that they should follow the same rules. --- .../AutomaticInterface/Builder.cs | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/AutomaticInterface/AutomaticInterface/Builder.cs b/AutomaticInterface/AutomaticInterface/Builder.cs index 6c9b7ce..b003114 100644 --- a/AutomaticInterface/AutomaticInterface/Builder.cs +++ b/AutomaticInterface/AutomaticInterface/Builder.cs @@ -11,20 +11,14 @@ public static class Builder { private const string InheritDoc = "/// "; // we use inherit doc because that should be able to fetch documentation from base classes. - private static readonly SymbolDisplayFormat MethodSignatureDisplayFormat = + private static readonly SymbolDisplayFormat FullyQualifiedDisplayFormat = new( genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, memberOptions: SymbolDisplayMemberOptions.IncludeParameters, parameterOptions: SymbolDisplayParameterOptions.IncludeType | SymbolDisplayParameterOptions.IncludeParamsRefOut, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces - ); - - private static readonly SymbolDisplayFormat TypeDisplayFormat = - new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier ); @@ -91,7 +85,7 @@ InterfaceBuilder codeGenerator .Where(x => !HasIgnoreAttribute(x)) .Where(x => x.ContainingType.Name != nameof(Object)) .Where(x => !HasIgnoreAttribute(x)) - .GroupBy(x => x.ToDisplayString(MethodSignatureDisplayFormat)) + .GroupBy(x => x.ToDisplayString(FullyQualifiedDisplayFormat)) .Select(g => g.First()) .ToList() .ForEach(method => @@ -112,13 +106,16 @@ private static void AddMethod(InterfaceBuilder codeGenerator, IMethodSymbol meth var typedArgs = method .TypeParameters.Select(arg => - (arg.ToDisplayString(TypeDisplayFormat), arg.GetWhereStatement(TypeDisplayFormat)) + ( + arg.ToDisplayString(FullyQualifiedDisplayFormat), + arg.GetWhereStatement(FullyQualifiedDisplayFormat) + ) ) .ToList(); codeGenerator.AddMethodToInterface( name, - returnType.ToDisplayString(TypeDisplayFormat), + returnType.ToDisplayString(FullyQualifiedDisplayFormat), InheritDoc, paramResult, typedArgs @@ -186,7 +183,7 @@ InterfaceBuilder codeGenerator .Where(x => x.DeclaredAccessibility == Accessibility.Public) .Where(x => !x.IsStatic) .Where(x => !HasIgnoreAttribute(x)) - .GroupBy(x => x.ToDisplayString(MethodSignatureDisplayFormat)) + .GroupBy(x => x.ToDisplayString(FullyQualifiedDisplayFormat)) .Select(g => g.First()) .ToList() .ForEach(evt => @@ -198,7 +195,7 @@ InterfaceBuilder codeGenerator codeGenerator.AddEventToInterface( name, - type.ToDisplayString(TypeDisplayFormat), + type.ToDisplayString(FullyQualifiedDisplayFormat), InheritDoc ); }); @@ -210,7 +207,7 @@ private static string GetMethodSignature(IParameterSymbol x) var refKindText = GetRefKind(x); var optionalValue = GetMethodOptionalValue(x); - return $"{refKindText}{x.Type.ToDisplayString(TypeDisplayFormat)} {name}{optionalValue}"; + return $"{refKindText}{x.Type.ToDisplayString(FullyQualifiedDisplayFormat)} {name}{optionalValue}"; } private static string GetMethodOptionalValue(IParameterSymbol x) @@ -226,7 +223,7 @@ private static string GetMethodOptionalValue(IParameterSymbol x) bool value => $" = {(value ? "true" : "false")}", // struct null when x.Type.IsValueType - => $" = default({x.Type.ToDisplayString(TypeDisplayFormat)})", + => $" = default({x.Type.ToDisplayString(FullyQualifiedDisplayFormat)})", null => " = null", _ => $" = {x.ExplicitDefaultValue}" }; @@ -283,7 +280,7 @@ InterfaceBuilder interfaceGenerator interfaceGenerator.AddPropertyToInterface( name, - type.ToDisplayString(TypeDisplayFormat), + type.ToDisplayString(FullyQualifiedDisplayFormat), hasGet, hasSet, isRef,