diff --git a/Readme.md b/Readme.md
index e935689..0b4506e 100644
--- a/Readme.md
+++ b/Readme.md
@@ -85,9 +85,10 @@ Console.WriteLine(output); // "12345".
Dunet generates implicit conversions between union variants and the union type if your union meets all of the following conditions:
+- The union has no required properties.
- All variants contain a single property.
- Each variant's property is unique within the union.
-- No property is an interface type.
+- No variant's property is an interface type.
For example, consider a `Result` union type that represents success as a `double` and failure as an `Exception`:
diff --git a/src/Dunet.csproj b/src/Dunet.csproj
index 50083c9..6dc1d3f 100644
--- a/src/Dunet.csproj
+++ b/src/Dunet.csproj
@@ -14,10 +14,11 @@
Readme.md
https://github.com/domn1995/dunet
source; generator; discriminated; union; functional; tagged;
- 1.7.0
- 1.7.0
+ 1.7.1
+ 1.7.1
MIT
- 1.7.0 - Match on specific union variant.
+ 1.7.1 - Disables implicit conversions when union has a required property.
+1.7.0 - Match on specific union variant.
1.6.0 - Support generator cancellation.
1.5.0 - Support async Action match functions.
1.4.2 - Disables implicit conversions when union variant is an interface type.
@@ -43,7 +44,7 @@
git
favicon.png
False
- 1.7.0
+ 1.7.1
en
true
$(NoWarn);NU5128
diff --git a/src/GenerateUnionRecord/RecordDeclarationSyntaxParser.cs b/src/GenerateUnionRecord/RecordDeclarationSyntaxParser.cs
index 5c3cafe..9d9d65a 100644
--- a/src/GenerateUnionRecord/RecordDeclarationSyntaxParser.cs
+++ b/src/GenerateUnionRecord/RecordDeclarationSyntaxParser.cs
@@ -35,19 +35,19 @@ this RecordDeclarationSyntax record
);
///
- /// Gets the properties within this record declaration.
+ /// Gets the parameters in this record's primary constructor.
///
/// This record declaration.
/// The semantic model associated with this record declaration.
- /// The sequence of properties, if any. Otherwise, .
- public static IEnumerable? GetProperties(
+ /// The sequence of parameters, if any. Otherwise, .
+ public static IEnumerable? GetParameters(
this RecordDeclarationSyntax record,
SemanticModel semanticModel
) =>
record.ParameterList?.Parameters.Select(
parameter =>
- new Property(
- Type: new PropertyType(
+ new Parameter(
+ Type: new ParameterType(
Identifier: parameter.Type?.ToString() ?? "",
IsInterface: parameter.Type.IsInterfaceType(semanticModel)
),
@@ -55,6 +55,32 @@ SemanticModel semanticModel
)
);
+ ///
+ /// Gets the properties declared in this record.
+ ///
+ /// This record declaration.
+ /// The semantic model associated with this record declaration.
+ /// The sequence of properties.
+ public static IEnumerable GetProperties(
+ this RecordDeclarationSyntax record,
+ SemanticModel semanticModel
+ ) =>
+ record.Members
+ .OfType()
+ .Select(
+ propertyDeclaration =>
+ new Property(
+ Type: new PropertyType(
+ Identifier: propertyDeclaration.Type.ToString(),
+ IsInterface: propertyDeclaration.Type.IsInterfaceType(semanticModel)
+ ),
+ Identifier: propertyDeclaration.Identifier.ToString(),
+ IsRequired: propertyDeclaration.Modifiers.Any(
+ modifier => modifier.Value is "required"
+ )
+ )
+ );
+
///
/// Gets the record declarations within this record declaration.
///
@@ -75,7 +101,7 @@ SemanticModel semanticModel
{
Identifier = nestedRecord.Identifier.ToString(),
TypeParameters = nestedRecord.GetTypeParameters()?.ToList() ?? new(),
- Properties = nestedRecord.GetProperties(semanticModel)?.ToList() ?? new()
+ Parameters = nestedRecord.GetParameters(semanticModel)?.ToList() ?? new()
}
);
diff --git a/src/GenerateUnionRecord/UnionDeclaration.cs b/src/GenerateUnionRecord/UnionDeclaration.cs
index 3af8159..c60c48a 100644
--- a/src/GenerateUnionRecord/UnionDeclaration.cs
+++ b/src/GenerateUnionRecord/UnionDeclaration.cs
@@ -10,19 +10,48 @@ internal sealed record UnionDeclaration(
List TypeParameters,
List TypeParameterConstraints,
List Variants,
- Stack ParentTypes
+ Stack ParentTypes,
+ List Properties
)
{
// Extension methods cannot be generated for a union declared in a top level program (no namespace).
// It also doesn't make sense to generate Match extensions if there are no variants to match aginst.
public bool SupportsAsyncMatchExtensionMethods() => Namespace is not null && Variants.Count > 0;
+
+ public bool SupportsImplicitConversions()
+ {
+ var allVariantsHaveSingleProperty = () =>
+ Variants.All(static variant => variant.Parameters.Count is 1);
+
+ var allVariantsHaveNoInterfaceParameters = () =>
+ Variants
+ .SelectMany(static variant => variant.Parameters)
+ .All(static property => !property.Type.IsInterface);
+
+ var allVariantsHaveUniquePropertyTypes = () =>
+ {
+ var allPropertyTypes = Variants
+ .SelectMany(static variant => variant.Parameters)
+ .Select(static property => property.Type);
+ var allPropertyTypesCount = allPropertyTypes.Count();
+ var uniquePropertyTypesCount = allPropertyTypes.Distinct().Count();
+ return allPropertyTypesCount == uniquePropertyTypesCount;
+ };
+
+ var hasNoRequiredProperties = () => !Properties.Any(property => property.IsRequired);
+
+ return allVariantsHaveSingleProperty()
+ && allVariantsHaveNoInterfaceParameters()
+ && allVariantsHaveUniquePropertyTypes()
+ && hasNoRequiredProperties();
+ }
}
internal sealed record VariantDeclaration
{
public required string Identifier { get; init; }
public required List TypeParameters { get; init; }
- public required List Properties { get; init; }
+ public required List Parameters { get; init; }
}
internal sealed record TypeParameter(string Identifier)
@@ -30,7 +59,11 @@ internal sealed record TypeParameter(string Identifier)
public override string ToString() => Identifier;
}
-internal sealed record Property(PropertyType Type, string Identifier);
+internal sealed record Parameter(ParameterType Type, string Identifier);
+
+internal sealed record Property(PropertyType Type, string Identifier, bool IsRequired);
+
+internal sealed record ParameterType(string Identifier, bool IsInterface);
internal sealed record PropertyType(string Identifier, bool IsInterface);
diff --git a/src/GenerateUnionRecord/UnionGenerator.cs b/src/GenerateUnionRecord/UnionGenerator.cs
index 309c9d6..bcc23af 100644
--- a/src/GenerateUnionRecord/UnionGenerator.cs
+++ b/src/GenerateUnionRecord/UnionGenerator.cs
@@ -111,6 +111,7 @@ CancellationToken cancellation
var typeParameterConstraints = declaration.GetTypeParameterConstraints();
var variants = declaration.GetNestedRecordDeclarations(semanticModel);
var parentTypes = declaration.GetParentTypes(semanticModel);
+ var properties = declaration.GetProperties(semanticModel);
yield return new UnionDeclaration(
Imports: imports.ToList(),
@@ -120,7 +121,8 @@ CancellationToken cancellation
TypeParameters: typeParameters?.ToList() ?? new(),
TypeParameterConstraints: typeParameterConstraints?.ToList() ?? new(),
Variants: variants.ToList(),
- ParentTypes: parentTypes
+ ParentTypes: parentTypes,
+ Properties: properties.ToList()
);
}
}
diff --git a/src/GenerateUnionRecord/UnionSourceBuilder.cs b/src/GenerateUnionRecord/UnionSourceBuilder.cs
index 5aa6a04..7831859 100644
--- a/src/GenerateUnionRecord/UnionSourceBuilder.cs
+++ b/src/GenerateUnionRecord/UnionSourceBuilder.cs
@@ -38,14 +38,14 @@ public static string Build(UnionDeclaration union)
builder.AppendAbstractMatchMethods(union);
builder.AppendAbstractSpecificMatchMethods(union);
- if (SupportsImplicitConversions(union))
+ if (union.SupportsImplicitConversions())
{
foreach (var variant in union.Variants)
{
builder.Append($" public static implicit operator {union.Name}");
builder.AppendTypeParams(union.TypeParameters);
builder.AppendLine(
- $"({variant.Properties[0].Type.Identifier} value) => new {variant.Identifier}(value);"
+ $"({variant.Parameters[0].Type.Identifier} value) => new {variant.Identifier}(value);"
);
}
builder.AppendLine();
@@ -76,31 +76,6 @@ public static string Build(UnionDeclaration union)
return builder.ToString();
}
- private static bool SupportsImplicitConversions(UnionDeclaration union)
- {
- var allVariantsHaveSingleProperty = () =>
- union.Variants.All(static variant => variant.Properties.Count is 1);
-
- var allVariantsHaveNoInterfaceParameters = () =>
- union.Variants
- .SelectMany(static variant => variant.Properties)
- .All(static property => !property.Type.IsInterface);
-
- var allVariantsHaveUniquePropertyTypes = () =>
- {
- var allPropertyTypes = union.Variants
- .SelectMany(static variant => variant.Properties)
- .Select(static property => property.Type);
- var allPropertyTypesCount = allPropertyTypes.Count();
- var uniquePropertyTypesCount = allPropertyTypes.Distinct().Count();
- return allPropertyTypesCount == uniquePropertyTypesCount;
- };
-
- return allVariantsHaveSingleProperty()
- && allVariantsHaveNoInterfaceParameters()
- && allVariantsHaveUniquePropertyTypes();
- }
-
private static StringBuilder AppendAbstractMatchMethods(
this StringBuilder builder,
UnionDeclaration union
diff --git a/test/GenerateUnionRecord/GenerationTests.cs b/test/GenerateUnionRecord/GenerationTests.cs
index 713fddd..dbf02cd 100644
--- a/test/GenerateUnionRecord/GenerationTests.cs
+++ b/test/GenerateUnionRecord/GenerationTests.cs
@@ -176,4 +176,35 @@ partial record Failure(Exception Error);
result.CompilationErrors.Should().BeEmpty();
result.GenerationDiagnostics.Should().BeEmpty();
}
+
+ [Fact]
+ public void UnionTypeMayHaveRequiredProperties()
+ {
+ // Arrange.
+ var programCs = """
+using Dunet;
+using System;
+
+Result result1 = new Result.Success(Guid.NewGuid()) { Name = "Success" };
+Result result2 = new Result.Failure(new Exception("Boom!")) { Name = "Failure" };
+
+var result1Name = result1.Name;
+var result2Name = result2.Name;
+
+[Union]
+partial record Result
+{
+ public required string Name { get; init; }
+ partial record Success(Guid Id);
+ partial record Failure(Exception Error);
+}
+""";
+ // Act.
+ var result = Compiler.Compile(programCs);
+
+ // Assert.
+ using var scope = new AssertionScope();
+ result.CompilationErrors.Should().BeEmpty();
+ result.GenerationDiagnostics.Should().BeEmpty();
+ }
}