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