diff --git a/src/Dunet.Generator/UnionGeneration/UnionDeclaration.cs b/src/Dunet.Generator/UnionGeneration/UnionDeclaration.cs index 144b899..d6b0c7e 100644 --- a/src/Dunet.Generator/UnionGeneration/UnionDeclaration.cs +++ b/src/Dunet.Generator/UnionGeneration/UnionDeclaration.cs @@ -18,33 +18,44 @@ ImmutableEquatableArray Properties // It also doesn't make sense to generate Match extensions if there are no variants to match against. public bool SupportsExtensionMethods() => Namespace is not null && Variants.Count > 0; - public bool SupportsImplicitConversions() + public List VariantsWithImplicitConversionSupport() { - var allVariantsHaveSingleParameter = () => - Variants.All(static variant => variant.Parameters.Count is 1); + var hasRequiredProperties = Properties.Any(static property => property.IsRequired); - var noVariantHasInterfaceParameter = () => - Variants - .SelectMany(static variant => variant.Parameters) - .All(static parameter => !parameter.Type.IsInterface); + // We cannot generate implicit conversions for unions with required properties because + // we cannot initialize the required value as part of performing the conversion. + if (hasRequiredProperties) + { + return []; + } - var allVariantsParameterTypesAreDifferent = () => + static bool eachVariantHasUniqueParameterType(IEnumerable variants) { - var allParameterTypes = Variants + var allParameterTypes = variants + // Ignore variants with no parameters since they don't impact uniqueness. + .Where(static variant => variant.Parameters.Count > 0) .SelectMany(static variant => variant.Parameters) - .Select(static parameter => parameter.Type.Identifier); - var numAllParameterTypes = allParameterTypes.Count(); + .Select(static parameter => parameter.Type.Identifier) + .ToList(); + var numAllParameterTypes = allParameterTypes.Count; var numUniqueParameterTypes = allParameterTypes.Distinct().Count(); return numAllParameterTypes == numUniqueParameterTypes; - }; + } + + if (!eachVariantHasUniqueParameterType(Variants)) + { + return []; + } + + // Isolate the variants that have a single parameter because only those can have implicit conversions. + bool hasSingleParameter(VariantDeclaration variant) => variant.Parameters.Count is 1; - var unionHasNoRequiredProperties = () => - !Properties.Any(static property => property.IsRequired); + static bool hasInterfaceParameter(VariantDeclaration variant) => + variant.Parameters.Any(static parameter => parameter.Type.IsInterface); - return allVariantsHaveSingleParameter() - && noVariantHasInterfaceParameter() - && allVariantsParameterTypesAreDifferent() - && unionHasNoRequiredProperties(); + return Variants + .Where(variant => hasSingleParameter(variant) && !hasInterfaceParameter(variant)) + .ToList(); } } diff --git a/src/Dunet.Generator/UnionGeneration/UnionSourceBuilder.cs b/src/Dunet.Generator/UnionGeneration/UnionSourceBuilder.cs index c56d651..19253bd 100644 --- a/src/Dunet.Generator/UnionGeneration/UnionSourceBuilder.cs +++ b/src/Dunet.Generator/UnionGeneration/UnionSourceBuilder.cs @@ -40,18 +40,15 @@ public static string Build(UnionDeclaration union) builder.AppendAbstractSpecificMatchMethods(union); builder.AppendAbstractUnwrapMethods(union); - if (union.SupportsImplicitConversions()) + foreach (var variant in union.VariantsWithImplicitConversionSupport()) { - foreach (var variant in union.Variants) - { - builder.Append($" public static implicit operator {union.Name}"); - builder.AppendTypeParams(union.TypeParameters); - builder.AppendLine( - $"({variant.Parameters[0].Type.Identifier} value) => new {variant.Identifier}(value);" - ); - } - builder.AppendLine(); + builder.Append($" public static implicit operator {union.Name}"); + builder.AppendTypeParams(union.TypeParameters); + builder.AppendLine( + $"({variant.Parameters[0].Type.Identifier} value) => new {variant.Identifier}(value);" + ); } + builder.AppendLine(); foreach (var variant in union.Variants) { diff --git a/test/UnionGeneration/ImplicitConversionTests.cs b/test/UnionGeneration/ImplicitConversionTests.cs index 67a97ed..9b96891 100644 --- a/test/UnionGeneration/ImplicitConversionTests.cs +++ b/test/UnionGeneration/ImplicitConversionTests.cs @@ -142,4 +142,32 @@ partial record Failure(IEnumerable Errors); result.CompilationErrors.Should().BeEmpty(); result.GenerationDiagnostics.Should().BeEmpty(); } + + [Fact] + public void IgnoresEmptyMembersWhenGeneratingImplicitConversions() + { + var programCs = """ + using Dunet; + using System; + using static Option; + + Option success = 42; + Option none = new None(); + + [Union] + partial record Option + { + partial record Some(T Value); + partial record None(); + } + """; + + // Act. + using var scope = new AssertionScope(); + var result = Compiler.Compile(programCs); + + // Assert. + result.CompilationErrors.Should().BeEmpty(); + result.GenerationDiagnostics.Should().BeEmpty(); + } }