Skip to content

Commit

Permalink
Support inner union classes (#52)
Browse files Browse the repository at this point in the history
* Add support for nested union declarations.
  • Loading branch information
domn1995 authored Aug 27, 2022
1 parent 602d26e commit 30c7293
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 3 deletions.
10 changes: 9 additions & 1 deletion src/GenerateUnionRecord/UnionRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ internal record UnionRecord(
string? Namespace,
string Name,
List<TypeParameter> TypeParameters,
List<UnionRecordMember> Members
List<UnionRecordMember> Members,
Stack<ParentType> ParentTypes
);

internal record UnionRecordMember(
Expand All @@ -20,3 +21,10 @@ internal record TypeParameter(string Name)
}

internal record RecordProperty(string Type, string Name);

/// <summary>
/// Represents a parent type declaration that nests a union record.
/// </summary>
/// <param name="IsRecord">Whether the type is a record or a plain class.</param>
/// <param name="Name">This type's name.</param>
internal record ParentType(bool IsRecord, string Name);
61 changes: 59 additions & 2 deletions src/GenerateUnionRecord/UnionRecordGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,69 @@ CancellationToken _
Namespace: @namespace,
Name: recordSymbol.Name,
TypeParameters: unionRecordTypeParameters?.ToList() ?? new(),
Members: unionRecordMembers
Members: unionRecordMembers,
ParentTypes: GetParentTypes(semanticModel, recordDeclaration)
);

unionRecords.Add(record);
}

return unionRecords;
}
}

private static Stack<ParentType> GetParentTypes(
SemanticModel semanticModel,
RecordDeclarationSyntax recordDeclaration
)
{
var parentTypes = new Stack<ParentType>();

RecursivelyAddParentTypes(semanticModel, recordDeclaration, parentTypes);

return parentTypes;
}

private static void RecursivelyAddParentTypes(
SemanticModel semanticModel,
SyntaxNode declaration,
Stack<ParentType> 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 "<top-level-statements-entry-point>")
{
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);
}
}
13 changes: 13 additions & 0 deletions src/GenerateUnionRecord/UnionRecordSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -82,6 +90,11 @@ public static string GenerateRecord(UnionRecord record)

builder.AppendLine("}");

foreach (var _ in parentTypes)
{
builder.AppendLine("}");
}

return builder.ToString();
}

Expand Down
11 changes: 11 additions & 0 deletions src/SyntaxExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Dunet;
Expand Down Expand Up @@ -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();
}
190 changes: 190 additions & 0 deletions test/GenerateUnionRecord/NestedGenerationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
namespace Dunet.Test.GenerateUnionRecord;

/// <summary>
/// Tests the unions are properly generated when their definitions are nested within other classes.
/// </summary>
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();
}
}

0 comments on commit 30c7293

Please sign in to comment.