From 30c729394c82dbe38bd20f21e2c759dda740f6f2 Mon Sep 17 00:00:00 2001 From: Domn Werner Date: Sat, 27 Aug 2022 09:29:58 -0700 Subject: [PATCH] Support inner union classes (#52) * Add support for nested union declarations. --- src/GenerateUnionRecord/UnionRecord.cs | 10 +- .../UnionRecordGenerator.cs | 61 +++++- src/GenerateUnionRecord/UnionRecordSource.cs | 13 ++ src/SyntaxExtensions.cs | 11 + .../NestedGenerationTests.cs | 190 ++++++++++++++++++ 5 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 test/GenerateUnionRecord/NestedGenerationTests.cs diff --git a/src/GenerateUnionRecord/UnionRecord.cs b/src/GenerateUnionRecord/UnionRecord.cs index 9fa9735..bd0a846 100644 --- a/src/GenerateUnionRecord/UnionRecord.cs +++ b/src/GenerateUnionRecord/UnionRecord.cs @@ -5,7 +5,8 @@ internal record UnionRecord( string? Namespace, string Name, List TypeParameters, - List Members + List Members, + Stack ParentTypes ); internal record UnionRecordMember( @@ -20,3 +21,10 @@ internal record TypeParameter(string Name) } internal record RecordProperty(string Type, string Name); + +/// +/// Represents a parent type declaration that nests a union record. +/// +/// Whether the type is a record or a plain class. +/// This type's name. +internal record ParentType(bool IsRecord, string Name); diff --git a/src/GenerateUnionRecord/UnionRecordGenerator.cs b/src/GenerateUnionRecord/UnionRecordGenerator.cs index 20bb113..884e79f 100644 --- a/src/GenerateUnionRecord/UnionRecordGenerator.cs +++ b/src/GenerateUnionRecord/UnionRecordGenerator.cs @@ -143,7 +143,8 @@ CancellationToken _ Namespace: @namespace, Name: recordSymbol.Name, TypeParameters: unionRecordTypeParameters?.ToList() ?? new(), - Members: unionRecordMembers + Members: unionRecordMembers, + ParentTypes: GetParentTypes(semanticModel, recordDeclaration) ); unionRecords.Add(record); @@ -151,4 +152,60 @@ CancellationToken _ return unionRecords; } -} + + private static Stack GetParentTypes( + SemanticModel semanticModel, + RecordDeclarationSyntax recordDeclaration + ) + { + var parentTypes = new Stack(); + + RecursivelyAddParentTypes(semanticModel, recordDeclaration, parentTypes); + + return parentTypes; + } + + private static void RecursivelyAddParentTypes( + SemanticModel semanticModel, + SyntaxNode declaration, + Stack parentTypes + ) + { + var parent = declaration.Parent; + + if (parent is null) + { + return; + } + + if (!parent.IsClassOrRecordDeclaration()) + { + return; + } + + var parentSymbol = semanticModel.GetDeclaredSymbol(parent); + + // Ignore top level statement synthentic program class. + if (parentSymbol?.ToDisplayString() is null or "") + { + return; + } + + var parentDeclaration = (TypeDeclarationSyntax)parent; + + // We can only declare a nested union type within a partial parent type declaration. + if (!parentDeclaration.IsPartial()) + { + return; + } + + var parentType = new ParentType( + IsRecord: parent.IsRecordDeclaration(), + Name: parentSymbol.Name + ); + + parentTypes.Push(parentType); + + RecursivelyAddParentTypes(semanticModel, parent, parentTypes); + } +} \ No newline at end of file diff --git a/src/GenerateUnionRecord/UnionRecordSource.cs b/src/GenerateUnionRecord/UnionRecordSource.cs index 96a0203..1697851 100644 --- a/src/GenerateUnionRecord/UnionRecordSource.cs +++ b/src/GenerateUnionRecord/UnionRecordSource.cs @@ -18,6 +18,14 @@ public static string GenerateRecord(UnionRecord record) builder.AppendLine($"namespace {record.Namespace};"); } + var parentTypes = record.ParentTypes; + + foreach (var type in parentTypes) + { + builder.AppendLine($"partial {(type.IsRecord ? "record" : "class")} {type.Name}"); + builder.AppendLine("{"); + } + builder.Append($"abstract partial record {record.Name}"); builder.AppendTypeParams(record.TypeParameters); builder.AppendLine(); @@ -82,6 +90,11 @@ public static string GenerateRecord(UnionRecord record) builder.AppendLine("}"); + foreach (var _ in parentTypes) + { + builder.AppendLine("}"); + } + return builder.ToString(); } diff --git a/src/SyntaxExtensions.cs b/src/SyntaxExtensions.cs index a0a7c15..3cab898 100644 --- a/src/SyntaxExtensions.cs +++ b/src/SyntaxExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Dunet; @@ -27,4 +28,14 @@ node is RecordDeclarationSyntax recordDeclaration public static bool IsImporting(this UsingDirectiveSyntax import, string name) => import.Name.ToString() == name; + + public static bool IsPartial(this TypeDeclarationSyntax declaration) => + declaration.Modifiers.Any(SyntaxKind.PartialKeyword); + + public static bool IsRecordDeclaration(this SyntaxNode node) => node is RecordDeclarationSyntax; + + public static bool IsClassDeclaration(this SyntaxNode node) => node is ClassDeclarationSyntax; + + public static bool IsClassOrRecordDeclaration(this SyntaxNode node) => + node.IsRecordDeclaration() || node.IsClassDeclaration(); } diff --git a/test/GenerateUnionRecord/NestedGenerationTests.cs b/test/GenerateUnionRecord/NestedGenerationTests.cs new file mode 100644 index 0000000..b8f8195 --- /dev/null +++ b/test/GenerateUnionRecord/NestedGenerationTests.cs @@ -0,0 +1,190 @@ +namespace Dunet.Test.GenerateUnionRecord; + +/// +/// Tests the unions are properly generated when their definitions are nested within other classes. +/// +public class NestedGenerationTests : UnionRecordTests +{ + [Fact] + public void CanReturnNestedMember() + { + // Arrange. + var programCs = + @" +using Dunet; + +var foo = Parent.Foo(); +var bar = Parent.Bar(); + +public partial class Parent +{ + [Union] + public partial record Nested + { + public partial record Member1; + public partial record Member2; + } + + public static Nested Foo() + { + return new Nested.Member1(); + } + + public static Nested Bar() + { + return new Nested.Member2(); + } +}"; + // Act. + var result = Compile.ToAssembly(programCs); + + // Assert. + result.CompilationErrors.Should().BeEmpty(); + result.GenerationDiagnostics.Should().BeEmpty(); + } + + [Fact] + public void CanReturnDeeplyNestedMember() + { + // Arrange. + var programCs = + @" +using Dunet; + +var foo = Parent1.Parent2.Parent3.Foo(); +var bar = Parent1.Parent2.Parent3.Bar(); + +public partial class Parent1 +{ + public partial class Parent2 + { + public partial class Parent3 + { + [Union] + public partial record Nested + { + public partial record Member1; + public partial record Member2; + } + + public static Nested Foo() + { + return new Nested.Member1(); + } + + public static Nested Bar() + { + return new Nested.Member2(); + } + } + } +}"; + // Act. + var result = Compile.ToAssembly(programCs); + + // Assert. + result.CompilationErrors.Should().BeEmpty(); + result.GenerationDiagnostics.Should().BeEmpty(); + } + + [Fact] + public void CanReturnDeeplyNestedMemberFromOtherNamespace() + { + var nestedCs = + @" +using Dunet; + +namespace NestedTests; + +public partial class Parent1 +{ + public partial class Parent2 + { + public partial class Parent3 + { + [Union] + public partial record Nested + { + public partial record Member1; + public partial record Member2; + } + + public static Nested Foo() + { + return new Nested.Member1(); + } + + public static Nested Bar() + { + return new Nested.Member2(); + } + } + } +}"; + // Arrange. + var programCs = + @" +using NestedTests; + +var foo = Parent1.Parent2.Parent3.Foo(); +var bar = Parent1.Parent2.Parent3.Bar(); +"; + // Act. + var result = Compile.ToAssembly(nestedCs, programCs); + + // Assert. + result.CompilationErrors.Should().BeEmpty(); + result.GenerationDiagnostics.Should().BeEmpty(); + } + + [Fact] + public void CanReturnMultipleDeeplyNestedUnionMembers() + { + // Arrange. + var programCs = + @" +using Dunet; + +var foo = Parent1.Parent2.Parent3.Foo(); +var bar = Parent1.Parent2.Parent3.Bar(); + +public partial class Parent1 +{ + public partial class Parent2 + { + public partial class Parent3 + { + [Union] + public partial record Nested1 + { + public partial record Member1; + public partial record Member2; + } + + [Union] + public partial record Nested2 + { + public partial record Member1; + public partial record Member2; + } + + public static Nested1 Foo() + { + return new Nested1.Member1(); + } + + public static Nested2 Bar() + { + return new Nested2.Member1(); + } + } + } +}"; + // Act. + var result = Compile.ToAssembly(programCs); + + // Assert. + result.CompilationErrors.Should().BeEmpty(); + result.GenerationDiagnostics.Should().BeEmpty(); + } +}