From 9bec95dc5cdfc007439bd2647a370e2d63d6247e Mon Sep 17 00:00:00 2001 From: Pawel Gerr Date: Fri, 25 Oct 2024 10:51:00 +0200 Subject: [PATCH] Arrays can be used with unions as well --- .../DiscriminatedUnions/MemberTypeState.cs | 14 +- .../UnionSourceGenerator.cs | 23 +- .../UnionSourceGeneratorTests.cs | 311 ++++++++++++++++++ .../TestUnions/TestUnion_class_with_array.cs | 5 + .../UnionTests/AsValue.cs | 5 + .../UnionTests/ExplicitCasts.cs | 3 + .../UnionTests/ImplicitCasts.cs | 3 + .../UnionTests/IsValue.cs | 5 + .../UnionTests/Map.cs | 18 + .../UnionTests/ToString.cs | 3 + .../UnionTests/Value.cs | 3 + 11 files changed, 384 insertions(+), 9 deletions(-) create mode 100644 test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_with_array.cs diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs index 60aecc72..898715a4 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs @@ -14,11 +14,11 @@ public sealed class MemberTypeState : IEquatable, IMemberInform public MemberTypeSetting Setting { get; } public MemberTypeState( - INamedTypeSymbol type, + string typeName, ITypedMemberState typeState, MemberTypeSetting setting) { - Name = setting.Name ?? (typeState.IsNullableStruct ? $"Nullable{type.TypeArguments[0].Name}" : type.Name); + Name = setting.Name ?? typeName; TypeFullyQualified = typeState.TypeFullyQualified; TypeMinimallyQualified = typeState.TypeMinimallyQualified; IsReferenceType = typeState.IsReferenceType; @@ -30,6 +30,16 @@ public MemberTypeState( Setting = setting; } + public static string GetMemberTypeName(INamedTypeSymbol type, ITypedMemberState typeState) + { + return typeState.IsNullableStruct ? $"Nullable{type.TypeArguments[0].Name}" : type.Name; + } + + public static string GetMemberTypeName(IArrayTypeSymbol type) + { + return type.ElementType.Name + "Array"; + } + public override bool Equals(object? obj) { return obj is MemberTypeState other && Equals(other); diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs index 75e507ce..572512f4 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs @@ -159,17 +159,26 @@ private bool IsUnionCandidate(TypeDeclarationSyntax typeDeclaration) return null; } - if (memberType is not INamedTypeSymbol namedMemberType) - { - Logger.LogDebug("Type of the member must be a named type", tds); - return null; - } - var memberTypeSettings = settings.MemberTypeSettings[i]; memberType = memberType.IsReferenceType && memberTypeSettings.IsNullableReferenceType ? memberType.WithNullableAnnotation(NullableAnnotation.Annotated) : memberType; var typeState = factory.Create(memberType); - var memberTypeState = new MemberTypeState(namedMemberType, typeState, memberTypeSettings); + string memberTypeName; + + switch (memberType) + { + case INamedTypeSymbol namedTypeSymbol: + memberTypeName = MemberTypeState.GetMemberTypeName(namedTypeSymbol, typeState); + break; + case IArrayTypeSymbol arrayTypeSymbol: + memberTypeName = MemberTypeState.GetMemberTypeName(arrayTypeSymbol); + break; + default: + Logger.LogError("Type of the member must be a named type or array type", tds); + return null; + } + + var memberTypeState = new MemberTypeState(memberTypeName, typeState, memberTypeSettings); memberTypeStates = memberTypeStates.Add(memberTypeState); } diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs index 35938922..e8704234 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs @@ -4620,4 +4620,315 @@ public override int GetHashCode() """); } + [Fact] + public void Should_generate_class_with_array() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + outputs.Should().HaveCount(1); + + var mainOutput = outputs.Single(kvp => kvp.Key.Contains("Thinktecture.Tests.TestUnion.g.cs")).Value; + + AssertOutput(mainOutput, _GENERATED_HEADER + """ + namespace Thinktecture.Tests + { + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators + { + private static readonly int _typeHashCode = typeof(global::Thinktecture.Tests.TestUnion).GetHashCode(); + + private readonly int _valueIndex; + + private readonly string[]? _stringArray; + private readonly int _int32; + + /// + /// Indication whether the current value is of type string[]. + /// + public bool IsStringArray => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type int. + /// + public bool IsInt32 => this._valueIndex == 2; + + /// + /// Gets the current value as string[]. + /// + /// If the current value is not of type string[]. + public string[] AsStringArray => IsStringArray ? this._stringArray! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string[]'."); + + /// + /// Gets the current value as int. + /// + /// If the current value is not of type int. + public int AsInt32 => IsInt32 ? this._int32 : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'int'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => this._stringArray!, + 2 => this._int32, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string[] @stringArray) + { + this._stringArray = @stringArray; + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int @int32) + { + this._int32 = @int32; + this._valueIndex = 2; + } + + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type string[]. + /// The action to execute if the current value is of type int. + public void Switch( + global::System.Action @stringArray, + global::System.Action @int32) + { + switch (this._valueIndex) + { + case 1: + @stringArray(this._stringArray!); + return; + case 2: + @int32(this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Executes an action depending on the current value. + /// + /// Context to be passed to the callbacks. + /// The action to execute if the current value is of type string[]. + /// The action to execute if the current value is of type int. + public void Switch( + TContext context, + global::System.Action @stringArray, + global::System.Action @int32) + { + switch (this._valueIndex) + { + case 1: + @stringArray(context, this._stringArray!); + return; + case 2: + @int32(context, this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type string[]. + /// The function to execute if the current value is of type int. + public TResult Switch( + global::System.Func @stringArray, + global::System.Func @int32) + { + switch (this._valueIndex) + { + case 1: + return @stringArray(this._stringArray!); + case 2: + return @int32(this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Executes a function depending on the current value. + /// + /// Context to be passed to the callbacks. + /// The function to execute if the current value is of type string[]. + /// The function to execute if the current value is of type int. + public TResult Switch( + TContext context, + global::System.Func @stringArray, + global::System.Func @int32) + { + switch (this._valueIndex) + { + case 1: + return @stringArray(context, this._stringArray!); + case 2: + return @int32(context, this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type string[]. + /// The instance to return if the current value is of type int. + public TResult Map( + TResult @stringArray, + TResult @int32) + { + switch (this._valueIndex) + { + case 1: + return @stringArray; + case 2: + return @int32; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type string[]. + /// + /// Value to covert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(string[] @stringArray) + { + return new global::Thinktecture.Tests.TestUnion(@stringArray); + } + + /// + /// Implicit conversion from type int. + /// + /// Value to covert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(int @int32) + { + return new global::Thinktecture.Tests.TestUnion(@int32); + } + + /// + /// Implicit conversion to type string[]. + /// + /// Object to covert. + /// Inner value of type string[]. + /// If the inner value is not a string[]. + public static explicit operator string[](global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsStringArray; + } + + /// + /// Implicit conversion to type int. + /// + /// Object to covert. + /// Inner value of type int. + /// If the inner value is not a int. + public static explicit operator int(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsInt32; + } + + /// + /// Compares two instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// true if objects are equal; otherwise false. + public static bool operator ==(global::Thinktecture.Tests.TestUnion? obj, global::Thinktecture.Tests.TestUnion? other) + { + if (obj is null) + return other is null; + + return obj.Equals(other); + } + + /// + /// Compares two instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// false if objects are equal; otherwise true. + public static bool operator !=(global::Thinktecture.Tests.TestUnion? obj, global::Thinktecture.Tests.TestUnion? other) + { + return !(obj == other); + } + + /// + public override bool Equals(object? other) + { + return other is global::Thinktecture.Tests.TestUnion obj && Equals(obj); + } + + /// + public bool Equals(global::Thinktecture.Tests.TestUnion? other) + { + if (other is null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + if (this._valueIndex != other._valueIndex) + return false; + + return this._valueIndex switch + { + 1 => this._stringArray is null ? other._stringArray is null : this._stringArray.Equals(other._stringArray), + 2 => this._int32.Equals(other._int32), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._stringArray?.GetHashCode() ?? 0), + 2 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._int32.GetHashCode()), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._stringArray?.ToString(), + 2 => this._int32.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } + } + + """); + } + } diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_with_array.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_with_array.cs new file mode 100644 index 00000000..f5a0fb6a --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_with_array.cs @@ -0,0 +1,5 @@ +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union] +public partial class TestUnion_class_with_array; diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/AsValue.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/AsValue.cs index 9c3bcf68..b52abcd9 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/AsValue.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/AsValue.cs @@ -34,6 +34,11 @@ public void Should_return_correct_value_or_throw_exception_having_2_types() new TestUnion_struct_string_int("text").Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_struct_string_int' is not of type 'int'."); new TestUnion_struct_string_int(1).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_struct_string_int' is not of type 'string'."); new TestUnion_struct_string_int(1).AsInt32.Should().Be(1); + + new TestUnion_class_with_array(["text"]).AsStringArray.Should().BeEquivalentTo(["text"]); + new TestUnion_class_with_array(["text"]).Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_with_array' is not of type 'int'."); + new TestUnion_class_with_array(1).Invoking(u => u.AsStringArray.Should()).Should().Throw().WithMessage("'TestUnion_class_with_array' is not of type 'string[]'."); + new TestUnion_class_with_array(1).AsInt32.Should().Be(1); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ExplicitCasts.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ExplicitCasts.cs index d40c5c65..28a2daca 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ExplicitCasts.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ExplicitCasts.cs @@ -19,6 +19,9 @@ public void Should_have_explicit_casts_to_value_having_2_types() ((int)new TestUnion_struct_string_int(1)).Should().Be(1); FluentActions.Invoking(() => (string)new TestUnion_struct_string_int(1)).Should().Throw().WithMessage("'TestUnion_struct_string_int' is not of type 'string'."); + + ((string[])new TestUnion_class_with_array(["text"])).Should().BeEquivalentTo(["text"]); + FluentActions.Invoking(() => (int)new TestUnion_class_with_array(["text"])).Should().Throw().WithMessage("'TestUnion_class_with_array' is not of type 'int'."); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ImplicitCasts.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ImplicitCasts.cs index e7f2f865..771036d6 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ImplicitCasts.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ImplicitCasts.cs @@ -19,6 +19,9 @@ public void Should_have_implicit_casts_from_value_having_2_values() TestUnion_struct_string_int intStructUnion = 42; intStructUnion.Value.Should().Be(42); + + TestUnion_class_with_array arrayClassUnion = new[] { "text" }; + arrayClassUnion.Value.Should().BeEquivalentTo(new[] { "text" }); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/IsValue.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/IsValue.cs index a451232b..de085264 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/IsValue.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/IsValue.cs @@ -34,6 +34,11 @@ public void Should_use_correct_index_having_2_types() new TestUnion_struct_string_int("text").IsInt32.Should().BeFalse(); new TestUnion_struct_string_int(1).IsString.Should().BeFalse(); new TestUnion_struct_string_int(1).IsInt32.Should().BeTrue(); + + new TestUnion_class_with_array(["text"]).IsStringArray.Should().BeTrue(); + new TestUnion_class_with_array(["text"]).IsInt32.Should().BeFalse(); + new TestUnion_class_with_array(1).IsStringArray.Should().BeFalse(); + new TestUnion_class_with_array(1).IsInt32.Should().BeTrue(); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Map.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Map.cs index c81b51ec..e8096667 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Map.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Map.cs @@ -26,6 +26,24 @@ public void Should_use_correct_arg_having_2_values(int index, object expected) calledActionOn.Should().Be(expected); } + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_use_correct_arg_having_2_values_with_array(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_with_array(["text"]), + 2 => new TestUnion_class_with_array(42), + _ => throw new Exception() + }; + + var calledActionOn = value.Map(stringArray: (object)"text", + int32: 42); + + calledActionOn.Should().Be(expected); + } + [Theory] [InlineData(1, "text")] [InlineData(2, 42)] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ToString.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ToString.cs index 0cd2cfaf..762ade65 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ToString.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ToString.cs @@ -13,6 +13,9 @@ public void Should_return_string_representation_of_the_inner_value_having_2_type new TestUnion_struct_string_int("text").ToString().Should().Be("text"); new TestUnion_struct_string_int(42).ToString().Should().Be("42"); + + new TestUnion_class_with_array(["text"]).ToString().Should().Be("System.String[]"); + new TestUnion_class_with_array(42).ToString().Should().Be("42"); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Value.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Value.cs index 5d137da6..64580b4c 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Value.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Value.cs @@ -23,6 +23,9 @@ public void Should_return_correct_value_having_2_types() new TestUnion_struct_string_int("text").Value.Should().Be("text"); new TestUnion_struct_string_int(1).Value.Should().Be(1); + + new TestUnion_class_with_array(["text"]).Value.Should().BeEquivalentTo(new[] { "text" }); + new TestUnion_class_with_array(1).Value.Should().Be(1); } [Fact]