Skip to content

Commit

Permalink
Merge pull request #211 from CommunityToolkit/dev/fix-non-classes-nes…
Browse files Browse the repository at this point in the history
…ted-types

Fix generation of nested types that are not classes
  • Loading branch information
Sergio0694 authored Apr 11, 2022
2 parents 753842a + e698d06 commit f003c4f
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <TYPE_NAME>
// partial <TYPE_KIND> TYPE_NAME>
// {
// <MEMBERS>
// }
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);
}
Expand Down
17 changes: 10 additions & 7 deletions CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Models;
/// <param name="FilenameHint">The filename hint for the current type.</param>
/// <param name="MetadataName">The metadata name for the current type.</param>
/// <param name="Namespace">Gets the namespace for the current type.</param>
/// <param name="Names">Gets the sequence of type definitions containing the current type.</param>
internal sealed partial record HierarchyInfo(string FilenameHint, string MetadataName, string Namespace, ImmutableArray<string> Names)
/// <param name="Hierarchy">Gets the sequence of type definitions containing the current type.</param>
internal sealed partial record HierarchyInfo(string FilenameHint, string MetadataName, string Namespace, ImmutableArray<TypeInfo> Hierarchy)
{
/// <summary>
/// Creates a new <see cref="HierarchyInfo"/> instance from a given <see cref="INamedTypeSymbol"/>.
Expand All @@ -32,20 +32,23 @@ internal sealed partial record HierarchyInfo(string FilenameHint, string Metadat
/// <returns>A <see cref="HierarchyInfo"/> instance describing <paramref name="typeSymbol"/>.</returns>
public static HierarchyInfo From(INamedTypeSymbol typeSymbol)
{
ImmutableArray<string>.Builder names = ImmutableArray.CreateBuilder<string>();
ImmutableArray<TypeInfo>.Builder hierarchy = ImmutableArray.CreateBuilder<TypeInfo>();

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());
}

/// <summary>
Expand All @@ -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);
}

/// <inheritdoc/>
Expand All @@ -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);
}
}
}
46 changes: 46 additions & 0 deletions CommunityToolkit.Mvvm.SourceGenerators/Models/TypeInfo.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A model describing a type info in a type hierarchy.
/// </summary>
/// <param name="QualifiedName">The qualified name for the type.</param>
/// <param name="Kind">The type of the type in the hierarchy.</param>
/// <param name="IsRecord">Whether the type is a record type.</param>
internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord)
{
/// <summary>
/// Creates a <see cref="TypeDeclarationSyntax"/> instance for the current info.
/// </summary>
/// <returns>A <see cref="TypeDeclarationSyntax"/> instance for the current info.</returns>
public TypeDeclarationSyntax GetSyntax()
{
// Create the partial type declaration with the kind.
// This code produces a class declaration as follows:
//
// <TYPE_KIND> <TYPE_NAME>
// {
// }
//
// 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)
};
}
}
82 changes: 82 additions & 0 deletions tests/CommunityToolkit.Mvvm.UnitTests/Test_SourceGenerators.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
[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<int, float>.NestedInterfaceType<string>.NestedRecord<string>.MyViewModel model = new();

Assert.IsNull(model.Name);
Assert.IsTrue(model.TestCommand is IRelayCommand);
}

public partial struct NestedStructTypeWithGenerics<T1, T2>
where T2 : struct
{
public partial interface NestedInterfaceType<TFoo>
{
public partial record NestedRecord<TBar>
{
[INotifyPropertyChanged]
public partial class MyViewModel
{
[ObservableProperty]
private string? name;

[ICommand]
private void Test()
{
}
}
}
}
}
}

0 comments on commit f003c4f

Please sign in to comment.