From f34c0f3b496bb56e7767984da4202201964ec897 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 11 Apr 2022 14:28:14 +0200 Subject: [PATCH 1/3] Fix generation for nested types that are not classes --- .../Models/HierarchyInfo.Syntax.cs | 14 +++---- .../Models/HierarchyInfo.cs | 16 ++++---- .../Models/TypeInfo.cs | 37 +++++++++++++++++++ 3 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.Syntax.cs b/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.Syntax.cs index cbf6ddd6..03c803c7 100644 --- a/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.Syntax.cs +++ b/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.Syntax.cs @@ -30,28 +30,26 @@ public CompilationUnitSyntax GetCompilationUnit( // Create the partial type declaration with the given member declarations. // This code produces a class declaration as follows: // - // partial class + // partial TYPE_NAME> // { // // } - ClassDeclarationSyntax classDeclarationSyntax = - ClassDeclaration(Names[0]) + TypeDeclarationSyntax typeDeclarationSyntax = + Hierarchy[0].GetSyntax() .AddModifiers(Token(SyntaxKind.PartialKeyword)) .AddMembers(memberDeclarations.ToArray()); // Add the base list, if present if (baseList is not null) { - classDeclarationSyntax = classDeclarationSyntax.WithBaseList(baseList); + typeDeclarationSyntax = typeDeclarationSyntax.WithBaseList(baseList); } - TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; - // Add all parent types in ascending order, if any - foreach (string parentType in Names.AsSpan().Slice(1)) + foreach (TypeInfo parentType in Hierarchy.AsSpan().Slice(1)) { typeDeclarationSyntax = - ClassDeclaration(parentType) + parentType.GetSyntax() .AddModifiers(Token(SyntaxKind.PartialKeyword)) .AddMembers(typeDeclarationSyntax); } diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs b/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs index ae014fca..7c0afe9d 100644 --- a/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs +++ b/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs @@ -22,8 +22,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Models; /// The filename hint for the current type. /// The metadata name for the current type. /// Gets the namespace for the current type. -/// Gets the sequence of type definitions containing the current type. -internal sealed partial record HierarchyInfo(string FilenameHint, string MetadataName, string Namespace, ImmutableArray Names) +/// Gets the sequence of type definitions containing the current type. +internal sealed partial record HierarchyInfo(string FilenameHint, string MetadataName, string Namespace, ImmutableArray Hierarchy) { /// /// Creates a new instance from a given . @@ -32,20 +32,22 @@ internal sealed partial record HierarchyInfo(string FilenameHint, string Metadat /// A instance describing . public static HierarchyInfo From(INamedTypeSymbol typeSymbol) { - ImmutableArray.Builder names = ImmutableArray.CreateBuilder(); + ImmutableArray.Builder hierarchy = ImmutableArray.CreateBuilder(); for (INamedTypeSymbol? parent = typeSymbol; parent is not null; parent = parent.ContainingType) { - names.Add(parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); + hierarchy.Add(new TypeInfo( + parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + parent.TypeKind)); } return new( typeSymbol.GetFullMetadataNameForFileName(), typeSymbol.MetadataName, typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), - names.ToImmutable()); + hierarchy.ToImmutable()); } /// @@ -59,7 +61,7 @@ protected override void AddToHashCode(ref HashCode hashCode, HierarchyInfo obj) hashCode.Add(obj.FilenameHint); hashCode.Add(obj.MetadataName); hashCode.Add(obj.Namespace); - hashCode.AddRange(obj.Names); + hashCode.AddRange(obj.Hierarchy); } /// @@ -69,7 +71,7 @@ protected override bool AreEqual(HierarchyInfo x, HierarchyInfo y) x.FilenameHint == y.FilenameHint && x.MetadataName == y.MetadataName && x.Namespace == y.Namespace && - x.Names.SequenceEqual(y.Names); + x.Hierarchy.SequenceEqual(y.Hierarchy); } } } diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs b/CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs new file mode 100644 index 00000000..5954b216 --- /dev/null +++ b/CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Models; + +/// +/// A model describing a type info in a type hierarchy. +/// +/// The qualified name for the type. +/// The type of the type in the hierarchy. +internal sealed record TypeInfo(string QualifiedName, TypeKind Kind) +{ + /// + /// Creates a instance for the current info. + /// + /// A instance for the current info. + public TypeDeclarationSyntax GetSyntax() + { + // Create the partial type declaration with the kind. + // This code produces a class declaration as follows: + // + // + // { + // } + return Kind switch + { + TypeKind.Struct => StructDeclaration(QualifiedName), + TypeKind.Interface => InterfaceDeclaration(QualifiedName), + _ => ClassDeclaration(QualifiedName) + }; + } +} From 7f4888dba09b4c9647925e7df73d61181f357a2a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 11 Apr 2022 14:28:22 +0200 Subject: [PATCH 2/3] Add unit tests for nested types that are not classes --- .../Test_SourceGenerators.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs new file mode 100644 index 00000000..05863852 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.UnitTests; + +/// +/// This class contains general unit tests for source generators, without a specific dependency on one. +/// For instance, this can be used for tests that validate common generation helpers used by all generators. +/// +[TestClass] +public partial class Test_SourceGenerators +{ + [TestMethod] + public void Test_SourceGenerators_NestedTypesThatAreNotJustClasses() + { + // This test just needs to compile, mostly + NestedStructType.NestedInterfaceType.MyViewModel model = new(); + + Assert.IsNull(model.Name); + Assert.IsTrue(model.TestCommand is IRelayCommand); + } + + public partial struct NestedStructType + { + public partial interface NestedInterfaceType + { + [ObservableRecipient] + public partial class MyViewModel : ObservableValidator + { + [ObservableProperty] + [Required] + private string? name; + + [ICommand] + private void Test() + { + } + } + } + } + + [TestMethod] + public void Test_SourceGenerators_NestedTypesThatAreNotJustClassesAndWithGenerics() + { + // This test just needs to compile, mostly + NestedStructTypeWithGenerics.NestedInterfaceType.MyViewModel model = new(); + + Assert.IsNull(model.Name); + Assert.IsTrue(model.TestCommand is IRelayCommand); + } + + public partial struct NestedStructTypeWithGenerics + where T2 : struct + { + public partial interface NestedInterfaceType + { + [INotifyPropertyChanged] + public partial class MyViewModel + { + [ObservableProperty] + private string? name; + + [ICommand] + private void Test() + { + } + } + } + } +} From e698d066a94a12c9e3705e63e306e5b9326f9caf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 11 Apr 2022 15:14:26 +0200 Subject: [PATCH 3/3] Add support for nested record types --- .../Models/HierarchyInfo.cs | 3 +- .../Models/TypeInfo.cs | 11 ++++- .../Test_SourceGenerators.cs | 40 +++++++++++-------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs b/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs index 7c0afe9d..93924ce7 100644 --- a/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs +++ b/CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs @@ -40,7 +40,8 @@ public static HierarchyInfo From(INamedTypeSymbol typeSymbol) { hierarchy.Add(new TypeInfo( parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), - parent.TypeKind)); + parent.TypeKind, + parent.IsRecord)); } return new( diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs b/CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs index 5954b216..cc3ef279 100644 --- a/CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs +++ b/CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -13,7 +14,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Models; /// /// The qualified name for the type. /// The type of the type in the hierarchy. -internal sealed record TypeInfo(string QualifiedName, TypeKind Kind) +/// Whether the type is a record type. +internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord) { /// /// Creates a instance for the current info. @@ -27,10 +29,17 @@ public TypeDeclarationSyntax GetSyntax() // // { // } + // + // Note that specifically for record declarations, we also need to explicitly add the open + // and close brace tokens, otherwise member declarations will not be formatted correctly. return Kind switch { TypeKind.Struct => StructDeclaration(QualifiedName), TypeKind.Interface => InterfaceDeclaration(QualifiedName), + TypeKind.Class when IsRecord => + RecordDeclaration(Token(SyntaxKind.RecordKeyword), QualifiedName) + .WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)), _ => ClassDeclaration(QualifiedName) }; } diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs index 05863852..18819ebc 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs @@ -20,7 +20,7 @@ public partial class Test_SourceGenerators public void Test_SourceGenerators_NestedTypesThatAreNotJustClasses() { // This test just needs to compile, mostly - NestedStructType.NestedInterfaceType.MyViewModel model = new(); + NestedStructType.NestedInterfaceType.NestedRecord.MyViewModel model = new(); Assert.IsNull(model.Name); Assert.IsTrue(model.TestCommand is IRelayCommand); @@ -30,16 +30,19 @@ public partial struct NestedStructType { public partial interface NestedInterfaceType { - [ObservableRecipient] - public partial class MyViewModel : ObservableValidator + public partial record NestedRecord { - [ObservableProperty] - [Required] - private string? name; - - [ICommand] - private void Test() + [ObservableRecipient] + public partial class MyViewModel : ObservableValidator { + [ObservableProperty] + [Required] + private string? name; + + [ICommand] + private void Test() + { + } } } } @@ -49,7 +52,7 @@ private void Test() public void Test_SourceGenerators_NestedTypesThatAreNotJustClassesAndWithGenerics() { // This test just needs to compile, mostly - NestedStructTypeWithGenerics.NestedInterfaceType.MyViewModel model = new(); + NestedStructTypeWithGenerics.NestedInterfaceType.NestedRecord.MyViewModel model = new(); Assert.IsNull(model.Name); Assert.IsTrue(model.TestCommand is IRelayCommand); @@ -60,15 +63,18 @@ public partial struct NestedStructTypeWithGenerics { public partial interface NestedInterfaceType { - [INotifyPropertyChanged] - public partial class MyViewModel + public partial record NestedRecord { - [ObservableProperty] - private string? name; - - [ICommand] - private void Test() + [INotifyPropertyChanged] + public partial class MyViewModel { + [ObservableProperty] + private string? name; + + [ICommand] + private void Test() + { + } } } }