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..93924ce7 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,23 @@ 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, + parent.IsRecord)); } return new( typeSymbol.GetFullMetadataNameForFileName(), typeSymbol.MetadataName, typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), - names.ToImmutable()); + hierarchy.ToImmutable()); } /// @@ -59,7 +62,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 +72,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..cc3ef279 --- /dev/null +++ b/CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs @@ -0,0 +1,46 @@ +// 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; +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. +/// Whether the type is a record type. +internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord) +{ + /// + /// 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: + // + // + // { + // } + // + // 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 new file mode 100644 index 00000000..18819ebc --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs @@ -0,0 +1,82 @@ +// 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.NestedRecord.MyViewModel model = new(); + + Assert.IsNull(model.Name); + Assert.IsTrue(model.TestCommand is IRelayCommand); + } + + public partial struct NestedStructType + { + public partial interface NestedInterfaceType + { + public partial record NestedRecord + { + [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.NestedRecord.MyViewModel model = new(); + + Assert.IsNull(model.Name); + Assert.IsTrue(model.TestCommand is IRelayCommand); + } + + public partial struct NestedStructTypeWithGenerics + where T2 : struct + { + public partial interface NestedInterfaceType + { + public partial record NestedRecord + { + [INotifyPropertyChanged] + public partial class MyViewModel + { + [ObservableProperty] + private string? name; + + [ICommand] + private void Test() + { + } + } + } + } + } +}