Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions src/Dunet.Generator/UnionGeneration/UnionDeclaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,44 @@ ImmutableEquatableArray<Property> 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<VariantDeclaration> 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<VariantDeclaration> 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();
}
}

Expand Down
17 changes: 7 additions & 10 deletions src/Dunet.Generator/UnionGeneration/UnionSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
28 changes: 28 additions & 0 deletions test/UnionGeneration/ImplicitConversionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,32 @@ partial record Failure(IEnumerable<string> Errors);
result.CompilationErrors.Should().BeEmpty();
result.GenerationDiagnostics.Should().BeEmpty();
}

[Fact]
public void IgnoresEmptyMembersWhenGeneratingImplicitConversions()
{
var programCs = """
using Dunet;
using System;
using static Option<int>;

Option<int> success = 42;
Option<int> none = new None();

[Union]
partial record Option<T>
{
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();
}
}