Skip to content

Commit f003c4f

Browse files
authored
Merge pull request #211 from CommunityToolkit/dev/fix-non-classes-nested-types
Fix generation of nested types that are not classes
2 parents 753842a + e698d06 commit f003c4f

File tree

4 files changed

+144
-15
lines changed

4 files changed

+144
-15
lines changed

CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.Syntax.cs

+6-8
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,26 @@ public CompilationUnitSyntax GetCompilationUnit(
3030
// Create the partial type declaration with the given member declarations.
3131
// This code produces a class declaration as follows:
3232
//
33-
// partial class <TYPE_NAME>
33+
// partial <TYPE_KIND> TYPE_NAME>
3434
// {
3535
// <MEMBERS>
3636
// }
37-
ClassDeclarationSyntax classDeclarationSyntax =
38-
ClassDeclaration(Names[0])
37+
TypeDeclarationSyntax typeDeclarationSyntax =
38+
Hierarchy[0].GetSyntax()
3939
.AddModifiers(Token(SyntaxKind.PartialKeyword))
4040
.AddMembers(memberDeclarations.ToArray());
4141

4242
// Add the base list, if present
4343
if (baseList is not null)
4444
{
45-
classDeclarationSyntax = classDeclarationSyntax.WithBaseList(baseList);
45+
typeDeclarationSyntax = typeDeclarationSyntax.WithBaseList(baseList);
4646
}
4747

48-
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
49-
5048
// Add all parent types in ascending order, if any
51-
foreach (string parentType in Names.AsSpan().Slice(1))
49+
foreach (TypeInfo parentType in Hierarchy.AsSpan().Slice(1))
5250
{
5351
typeDeclarationSyntax =
54-
ClassDeclaration(parentType)
52+
parentType.GetSyntax()
5553
.AddModifiers(Token(SyntaxKind.PartialKeyword))
5654
.AddMembers(typeDeclarationSyntax);
5755
}

CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.cs

+10-7
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Models;
2222
/// <param name="FilenameHint">The filename hint for the current type.</param>
2323
/// <param name="MetadataName">The metadata name for the current type.</param>
2424
/// <param name="Namespace">Gets the namespace for the current type.</param>
25-
/// <param name="Names">Gets the sequence of type definitions containing the current type.</param>
26-
internal sealed partial record HierarchyInfo(string FilenameHint, string MetadataName, string Namespace, ImmutableArray<string> Names)
25+
/// <param name="Hierarchy">Gets the sequence of type definitions containing the current type.</param>
26+
internal sealed partial record HierarchyInfo(string FilenameHint, string MetadataName, string Namespace, ImmutableArray<TypeInfo> Hierarchy)
2727
{
2828
/// <summary>
2929
/// Creates a new <see cref="HierarchyInfo"/> instance from a given <see cref="INamedTypeSymbol"/>.
@@ -32,20 +32,23 @@ internal sealed partial record HierarchyInfo(string FilenameHint, string Metadat
3232
/// <returns>A <see cref="HierarchyInfo"/> instance describing <paramref name="typeSymbol"/>.</returns>
3333
public static HierarchyInfo From(INamedTypeSymbol typeSymbol)
3434
{
35-
ImmutableArray<string>.Builder names = ImmutableArray.CreateBuilder<string>();
35+
ImmutableArray<TypeInfo>.Builder hierarchy = ImmutableArray.CreateBuilder<TypeInfo>();
3636

3737
for (INamedTypeSymbol? parent = typeSymbol;
3838
parent is not null;
3939
parent = parent.ContainingType)
4040
{
41-
names.Add(parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
41+
hierarchy.Add(new TypeInfo(
42+
parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
43+
parent.TypeKind,
44+
parent.IsRecord));
4245
}
4346

4447
return new(
4548
typeSymbol.GetFullMetadataNameForFileName(),
4649
typeSymbol.MetadataName,
4750
typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)),
48-
names.ToImmutable());
51+
hierarchy.ToImmutable());
4952
}
5053

5154
/// <summary>
@@ -59,7 +62,7 @@ protected override void AddToHashCode(ref HashCode hashCode, HierarchyInfo obj)
5962
hashCode.Add(obj.FilenameHint);
6063
hashCode.Add(obj.MetadataName);
6164
hashCode.Add(obj.Namespace);
62-
hashCode.AddRange(obj.Names);
65+
hashCode.AddRange(obj.Hierarchy);
6366
}
6467

6568
/// <inheritdoc/>
@@ -69,7 +72,7 @@ protected override bool AreEqual(HierarchyInfo x, HierarchyInfo y)
6972
x.FilenameHint == y.FilenameHint &&
7073
x.MetadataName == y.MetadataName &&
7174
x.Namespace == y.Namespace &&
72-
x.Names.SequenceEqual(y.Names);
75+
x.Hierarchy.SequenceEqual(y.Hierarchy);
7376
}
7477
}
7578
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
9+
10+
namespace CommunityToolkit.Mvvm.SourceGenerators.Models;
11+
12+
/// <summary>
13+
/// A model describing a type info in a type hierarchy.
14+
/// </summary>
15+
/// <param name="QualifiedName">The qualified name for the type.</param>
16+
/// <param name="Kind">The type of the type in the hierarchy.</param>
17+
/// <param name="IsRecord">Whether the type is a record type.</param>
18+
internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord)
19+
{
20+
/// <summary>
21+
/// Creates a <see cref="TypeDeclarationSyntax"/> instance for the current info.
22+
/// </summary>
23+
/// <returns>A <see cref="TypeDeclarationSyntax"/> instance for the current info.</returns>
24+
public TypeDeclarationSyntax GetSyntax()
25+
{
26+
// Create the partial type declaration with the kind.
27+
// This code produces a class declaration as follows:
28+
//
29+
// <TYPE_KIND> <TYPE_NAME>
30+
// {
31+
// }
32+
//
33+
// Note that specifically for record declarations, we also need to explicitly add the open
34+
// and close brace tokens, otherwise member declarations will not be formatted correctly.
35+
return Kind switch
36+
{
37+
TypeKind.Struct => StructDeclaration(QualifiedName),
38+
TypeKind.Interface => InterfaceDeclaration(QualifiedName),
39+
TypeKind.Class when IsRecord =>
40+
RecordDeclaration(Token(SyntaxKind.RecordKeyword), QualifiedName)
41+
.WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken))
42+
.WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)),
43+
_ => ClassDeclaration(QualifiedName)
44+
};
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.ComponentModel.DataAnnotations;
6+
using CommunityToolkit.Mvvm.ComponentModel;
7+
using CommunityToolkit.Mvvm.Input;
8+
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
10+
namespace CommunityToolkit.Mvvm.UnitTests;
11+
12+
/// <summary>
13+
/// This class contains general unit tests for source generators, without a specific dependency on one.
14+
/// For instance, this can be used for tests that validate common generation helpers used by all generators.
15+
/// </summary>
16+
[TestClass]
17+
public partial class Test_SourceGenerators
18+
{
19+
[TestMethod]
20+
public void Test_SourceGenerators_NestedTypesThatAreNotJustClasses()
21+
{
22+
// This test just needs to compile, mostly
23+
NestedStructType.NestedInterfaceType.NestedRecord.MyViewModel model = new();
24+
25+
Assert.IsNull(model.Name);
26+
Assert.IsTrue(model.TestCommand is IRelayCommand);
27+
}
28+
29+
public partial struct NestedStructType
30+
{
31+
public partial interface NestedInterfaceType
32+
{
33+
public partial record NestedRecord
34+
{
35+
[ObservableRecipient]
36+
public partial class MyViewModel : ObservableValidator
37+
{
38+
[ObservableProperty]
39+
[Required]
40+
private string? name;
41+
42+
[ICommand]
43+
private void Test()
44+
{
45+
}
46+
}
47+
}
48+
}
49+
}
50+
51+
[TestMethod]
52+
public void Test_SourceGenerators_NestedTypesThatAreNotJustClassesAndWithGenerics()
53+
{
54+
// This test just needs to compile, mostly
55+
NestedStructTypeWithGenerics<int, float>.NestedInterfaceType<string>.NestedRecord<string>.MyViewModel model = new();
56+
57+
Assert.IsNull(model.Name);
58+
Assert.IsTrue(model.TestCommand is IRelayCommand);
59+
}
60+
61+
public partial struct NestedStructTypeWithGenerics<T1, T2>
62+
where T2 : struct
63+
{
64+
public partial interface NestedInterfaceType<TFoo>
65+
{
66+
public partial record NestedRecord<TBar>
67+
{
68+
[INotifyPropertyChanged]
69+
public partial class MyViewModel
70+
{
71+
[ObservableProperty]
72+
private string? name;
73+
74+
[ICommand]
75+
private void Test()
76+
{
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)