From 006cbdb5a621820daa79e4de44660d11477f527f Mon Sep 17 00:00:00 2001 From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com> Date: Sat, 5 Apr 2025 08:17:01 +1100 Subject: [PATCH 1/3] Emit fully qualified type constraints for generic interfaces - Use RoslynExtensions to generate type constraints, as we do for method type constraints - Move generic interface definition tests into a new file - Add test for generation of an interface with base class type constraints - Remove custom `GetFullTypeString` method from RoslynExtensions - the same result is produced by using `ToDisplayString`. --- .../AutomaticInterface/Builder.cs | 22 ++--- .../AutomaticInterface/RoslynExtensions.cs | 50 +--------- ...rfaces.MakesGenericInterface.verified.txt} | 2 +- ...rfaceWithClassTypeConstraints.verified.txt | 18 ++++ ...eWithDependentTypeConstraints.verified.txt | 18 ++++ ...eWithInterfaceTypeConstraints.verified.txt | 18 ++++ .../GenericInterfaces/GenericInterfaces.cs | 94 +++++++++++++++++++ AutomaticInterface/Tests/Misc/Misc.cs | 23 ----- 8 files changed, 160 insertions(+), 85 deletions(-) rename AutomaticInterface/Tests/{Misc/Misc.MakesGenericInterface.verified.txt => GenericInterfaces/GenericInterfaces.MakesGenericInterface.verified.txt} (90%) create mode 100644 AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithClassTypeConstraints.verified.txt create mode 100644 AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithDependentTypeConstraints.verified.txt create mode 100644 AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithInterfaceTypeConstraints.verified.txt create mode 100644 AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.cs diff --git a/AutomaticInterface/AutomaticInterface/Builder.cs b/AutomaticInterface/AutomaticInterface/Builder.cs index 0a61b40..1f028e9 100644 --- a/AutomaticInterface/AutomaticInterface/Builder.cs +++ b/AutomaticInterface/AutomaticInterface/Builder.cs @@ -46,12 +46,12 @@ public static string BuildInterfaceFor(ITypeSymbol typeSymbol) { if ( typeSymbol.DeclaringSyntaxReferences.First().GetSyntax() - is not ClassDeclarationSyntax classSyntax + is not ClassDeclarationSyntax classSyntax + || typeSymbol is not INamedTypeSymbol namedTypeSymbol ) { return string.Empty; } - var namespaceName = GetNameSpace(typeSymbol); var interfaceName = $"I{classSyntax.GetClassName()}"; @@ -59,7 +59,7 @@ is not ClassDeclarationSyntax classSyntax var interfaceGenerator = new InterfaceBuilder(namespaceName, interfaceName); interfaceGenerator.AddClassDocumentation(GetDocumentationForClass(classSyntax)); - interfaceGenerator.AddGeneric(GetGeneric(classSyntax)); + interfaceGenerator.AddGeneric(GetGeneric(classSyntax, namedTypeSymbol)); var members = typeSymbol .GetAllMembers() @@ -298,16 +298,14 @@ private static string GetDocumentationForClass(CSharpSyntaxNode classSyntax) return trivia.ToFullString().Trim(); } - private static string GetGeneric(TypeDeclarationSyntax classSyntax) + private static string GetGeneric(TypeDeclarationSyntax classSyntax, INamedTypeSymbol typeSymbol) { - if (classSyntax.TypeParameterList?.Parameters.Count == 0) - { - return string.Empty; - } - - var formattedGeneric = - $"{classSyntax.TypeParameterList?.ToFullString().Trim()} {classSyntax.ConstraintClauses}".Trim(); + var whereStatements = typeSymbol + .TypeParameters.Select(typeParameter => + typeParameter.GetWhereStatement(FullyQualifiedDisplayFormat) + ) + .Where(constraint => !string.IsNullOrEmpty(constraint)); - return formattedGeneric; + return $"{classSyntax.TypeParameterList?.ToFullString().Trim()} {string.Join(" ", whereStatements)}".Trim(); } } diff --git a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs index 09a77ee..6a717eb 100644 --- a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs +++ b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs @@ -82,7 +82,7 @@ SymbolDisplayFormat typeDisplayFormat isFirstConstraint = false; } - constraints += constraintType.GetFullTypeString(typeDisplayFormat); + constraints += constraintType.ToDisplayString(typeDisplayFormat); } if (string.IsNullOrEmpty(constraints)) @@ -94,53 +94,5 @@ SymbolDisplayFormat typeDisplayFormat return result; } - - private static string GetFullTypeString( - this ISymbol type, - SymbolDisplayFormat typeDisplayFormat - ) - { - return type.ToDisplayString(typeDisplayFormat) - + type.GetTypeArgsStr( - typeDisplayFormat, - symbol => ((INamedTypeSymbol)symbol).TypeArguments - ); - } - - private static string GetTypeArgsStr( - this ISymbol symbol, - SymbolDisplayFormat typeDisplayFormat, - Func> typeArgGetter - ) - { - var typeArgs = typeArgGetter(symbol); - - if (!typeArgs.Any()) - return string.Empty; - - var stringsToAdd = new List(); - foreach (var arg in typeArgs) - { - string strToAdd; - - if (arg is ITypeParameterSymbol typeParameterSymbol) - { - // this is a generic argument - strToAdd = typeParameterSymbol.ToDisplayString(typeDisplayFormat); - } - else - { - // this is a generic argument value. - var namedTypeSymbol = arg as INamedTypeSymbol; - strToAdd = namedTypeSymbol!.GetFullTypeString(typeDisplayFormat); - } - - stringsToAdd.Add(strToAdd); - } - - var result = $"<{string.Join(", ", stringsToAdd)}>"; - - return result; - } } } diff --git a/AutomaticInterface/Tests/Misc/Misc.MakesGenericInterface.verified.txt b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterface.verified.txt similarity index 90% rename from AutomaticInterface/Tests/Misc/Misc.MakesGenericInterface.verified.txt rename to AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterface.verified.txt index 2675ff2..a05b037 100644 --- a/AutomaticInterface/Tests/Misc/Misc.MakesGenericInterface.verified.txt +++ b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterface.verified.txt @@ -12,7 +12,7 @@ namespace AutomaticInterfaceExample /// Bla bla /// [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")] - public partial interface IDemoClass where T:class + public partial interface IDemoClass where T : class { } } diff --git a/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithClassTypeConstraints.verified.txt b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithClassTypeConstraints.verified.txt new file mode 100644 index 0000000..61478af --- /dev/null +++ b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithClassTypeConstraints.verified.txt @@ -0,0 +1,18 @@ +//-------------------------------------------------------------------------------------------------- +// +// 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 +{ + /// + /// Bla bla + /// + [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")] + public partial interface IDemoClass where T : global::AutomaticInterfaceExample.DemoModel + { + } +} diff --git a/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithDependentTypeConstraints.verified.txt b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithDependentTypeConstraints.verified.txt new file mode 100644 index 0000000..47dd7a0 --- /dev/null +++ b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithDependentTypeConstraints.verified.txt @@ -0,0 +1,18 @@ +//-------------------------------------------------------------------------------------------------- +// +// 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 +{ + /// + /// Bla bla + /// + [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")] + public partial interface IDemoClass where T : U where V : List + { + } +} diff --git a/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithInterfaceTypeConstraints.verified.txt b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithInterfaceTypeConstraints.verified.txt new file mode 100644 index 0000000..88f14e2 --- /dev/null +++ b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithInterfaceTypeConstraints.verified.txt @@ -0,0 +1,18 @@ +//-------------------------------------------------------------------------------------------------- +// +// 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 +{ + /// + /// Bla bla + /// + [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")] + public partial interface IDemoClass where T : class, global::AutomaticInterfaceExample.IDemoModel where U : struct, global::AutomaticInterfaceExample.IDemoModel where V : notnull, global::AutomaticInterfaceExample.IDemoModel + { + } +} diff --git a/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.cs b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.cs new file mode 100644 index 0000000..d6b6a6e --- /dev/null +++ b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.cs @@ -0,0 +1,94 @@ +namespace Tests.GenericInterfaces; + +public class GenericInterfaces +{ + [Fact] + public async Task MakesGenericInterface() + { + const string code = """ + + using AutomaticInterface; + + namespace AutomaticInterfaceExample + { + /// + /// Bla bla + /// + [GenerateAutomaticInterface] + class DemoClass where T:class + { + } + } + + """; + + await Verify(Infrastructure.GenerateCode(code)); + } + + [Fact] + public async Task MakesGenericInterfaceWithInterfaceTypeConstraints() + { + const string code = """ + + using AutomaticInterface; + + namespace AutomaticInterfaceExample; + + public interface IDemoModel; + + /// + /// Bla bla + /// + [GenerateAutomaticInterface] + public class DemoClass + where T: class, IDemoModel + where U: struct, IDemoModel + where V: notnull, IDemoModel; + """; + + await Verify(Infrastructure.GenerateCode(code)); + } + + [Fact] + public async Task MakesGenericInterfaceWithClassTypeConstraints() + { + const string code = """ + + using AutomaticInterface; + + namespace AutomaticInterfaceExample; + + public class DemoModel; + + /// + /// Bla bla + /// + [GenerateAutomaticInterface] + public class DemoClass + where T: DemoModel; + """; + + await Verify(Infrastructure.GenerateCode(code)); + } + + [Fact] + public async Task MakesGenericInterfaceWithDependentTypeConstraints() + { + const string code = """ + + using AutomaticInterface; + + namespace AutomaticInterfaceExample; + + /// + /// Bla bla + /// + [GenerateAutomaticInterface] + public class DemoClass + where T: U + where V: List; + """; + + await Verify(Infrastructure.GenerateCode(code)); + } +} diff --git a/AutomaticInterface/Tests/Misc/Misc.cs b/AutomaticInterface/Tests/Misc/Misc.cs index 68fcd09..c573a0b 100644 --- a/AutomaticInterface/Tests/Misc/Misc.cs +++ b/AutomaticInterface/Tests/Misc/Misc.cs @@ -132,29 +132,6 @@ public static string StaticMethod() // method await Verify(Infrastructure.GenerateCode(code)); } - [Fact] - public async Task MakesGenericInterface() - { - const string code = """ - - using AutomaticInterface; - - namespace AutomaticInterfaceExample - { - /// - /// Bla bla - /// - [GenerateAutomaticInterface] - class DemoClass where T:class - { - } - } - - """; - - await Verify(Infrastructure.GenerateCode(code)); - } - [Fact] public async Task DoesNotCopyIndexerToInterface() { From 6b03b558538225c59581fcea473b7361ff462a1f Mon Sep 17 00:00:00 2001 From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com> Date: Sun, 6 Apr 2025 11:47:03 +1000 Subject: [PATCH 2/3] Add test for `new()` constraint, update logic to ensure that `new()` is always last --- .../AutomaticInterface/RoslynExtensions.cs | 44 +++++++------------ ...rfaceWithClassTypeConstraints.verified.txt | 2 +- .../GenericInterfaces/GenericInterfaces.cs | 5 ++- 3 files changed, 19 insertions(+), 32 deletions(-) diff --git a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs index 6a717eb..6606fe4 100644 --- a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs +++ b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs @@ -42,55 +42,41 @@ SymbolDisplayFormat typeDisplayFormat { var result = $"where {typeParameterSymbol.Name} : "; - var constraints = ""; - - var isFirstConstraint = true; + var constraints = new List(); if (typeParameterSymbol.HasReferenceTypeConstraint) { - constraints += "class"; - isFirstConstraint = false; + constraints.Add("class"); } if (typeParameterSymbol.HasValueTypeConstraint) { - constraints += "struct"; - isFirstConstraint = false; - } - - if (typeParameterSymbol.HasConstructorConstraint) - { - constraints += "new()"; - isFirstConstraint = false; + constraints.Add("struct"); } if (typeParameterSymbol.HasNotNullConstraint) { - constraints += "notnull"; - isFirstConstraint = false; + constraints.Add("notnull"); } - foreach (var constraintType in typeParameterSymbol.ConstraintTypes) - { - // if not first constraint prepend with comma - if (!isFirstConstraint) - { - constraints += ", "; - } - else - { - isFirstConstraint = false; - } + constraints.AddRange( + typeParameterSymbol.ConstraintTypes.Select(t => + t.ToDisplayString(typeDisplayFormat) + ) + ); - constraints += constraintType.ToDisplayString(typeDisplayFormat); + // The new() constraint must be last + if (typeParameterSymbol.HasConstructorConstraint) + { + constraints.Add("new()"); } - if (string.IsNullOrEmpty(constraints)) + if (constraints.Count == 0) { return ""; } - result += constraints; + result += string.Join(", ", constraints); return result; } diff --git a/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithClassTypeConstraints.verified.txt b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithClassTypeConstraints.verified.txt index 61478af..c788c6a 100644 --- a/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithClassTypeConstraints.verified.txt +++ b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.MakesGenericInterfaceWithClassTypeConstraints.verified.txt @@ -12,7 +12,7 @@ namespace AutomaticInterfaceExample /// Bla bla /// [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")] - public partial interface IDemoClass where T : global::AutomaticInterfaceExample.DemoModel + public partial interface IDemoClass where T1 : global::AutomaticInterfaceExample.DemoModel, new() where T2 : global::AutomaticInterfaceExample.DemoModel { } } diff --git a/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.cs b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.cs index d6b6a6e..303bb46 100644 --- a/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.cs +++ b/AutomaticInterface/Tests/GenericInterfaces/GenericInterfaces.cs @@ -64,8 +64,9 @@ public class DemoModel; /// Bla bla /// [GenerateAutomaticInterface] - public class DemoClass - where T: DemoModel; + public class DemoClass + where T1: DemoModel, new() + where T2: DemoModel; """; await Verify(Infrastructure.GenerateCode(code)); From af65e421e9325debbe7ac31d4c7f2409a2af6bdc Mon Sep 17 00:00:00 2001 From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com> Date: Sun, 6 Apr 2025 17:43:11 +1000 Subject: [PATCH 3/3] Bump package version, update release notes --- .../AutomaticInterface/AutomaticInterface.csproj | 2 +- README.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj b/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj index 4de54bd..78913f7 100644 --- a/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj +++ b/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj @@ -25,7 +25,7 @@ MIT True latest-Recommended - 5.1.3 + 5.1.4 README.md true 1701;1702;NU5128 diff --git a/README.md b/README.md index 2789cac..bfbd2f5 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,11 @@ Note that we use [Verify](https://github.com/VerifyTests/Verify) for testing. It ## Changelog -### 5.1.3. +### 5.1.4 + +- Emit fully qualified type constraints on generic interfaces + +### 5.1.3 - Emit `notnull` type constraints on generic type parameters