diff --git a/.claude/CLAUDE-ATTRIBUTES.md b/.claude/CLAUDE-ATTRIBUTES.md index d6773a9f..baa9e203 100644 --- a/.claude/CLAUDE-ATTRIBUTES.md +++ b/.claude/CLAUDE-ATTRIBUTES.md @@ -179,10 +179,11 @@ Ad-hoc unions for 2-5 types (generic syntax). **Properties** (per generic parameter): -| Property | Type | Default | Description | -|---------------------------------------------------------------|-----------|-----------|--------------------------------------------------------------------------| -| `T1Name`, `T2Name`, ... | `string?` | Type name | Override member name for T1, T2, etc. | -| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ... | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types) | +| Property | Type | Default | Description | +|---------------------------------------------------------------|-----------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `T1Name`, `T2Name`, ... | `string?` | Type name | Override member name for T1, T2, etc. | +| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ... | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types). Automatically set to `true` for reference types when `TXIsStateless = true` | +| `T1IsStateless`, `T2IsStateless`, ... | `bool` | `false` | Mark T1, T2, etc. as a stateless type that carries no instance data. Reduces memory by storing only discriminator index. Accessors return `default(T)`. Automatically sets `TXIsNullableReferenceType = true` for reference types. **Recommended: Use struct types for stateless members to avoid null-handling complexity.** | **Inherits all properties from `UnionAttributeBase`** (see above). @@ -202,12 +203,13 @@ AdHocUnionAttribute(Type t1, Type t2, Type? t3 = null, Type? t4 = null, Type? t5 **Properties**: -| Property | Type | Default | Description | -|--------------------------------------------------------------------------------------------|-----------|------------|--------------------------------------------------------------------------| -| `T1`, `T2` | `Type` | (required) | Required member types | -| `T3`, `T4`, `T5` | `Type?` | `null` | Optional member types | -| `T1Name`, `T2Name`, ..., `T5Name` | `string?` | Type name | Override member name for T1, T2, etc. | -| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ..., `T5IsNullableReferenceType` | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types) | +| Property | Type | Default | Description | +|--------------------------------------------------------------------------------------------|-----------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `T1`, `T2` | `Type` | (required) | Required member types | +| `T3`, `T4`, `T5` | `Type?` | `null` | Optional member types | +| `T1Name`, `T2Name`, ..., `T5Name` | `string?` | Type name | Override member name for T1, T2, etc. | +| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ..., `T5IsNullableReferenceType` | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types). Automatically set to `true` for reference types when `TXIsStateless = true` | +| `T1IsStateless`, `T2IsStateless`, ..., `T5IsStateless` | `bool` | `false` | Mark T1, T2, etc. as a stateless type that carries no instance data. Reduces memory by storing only discriminator index. Accessors return `default(T)`. Automatically sets `TXIsNullableReferenceType = true` for reference types. **Recommended: Use struct types for stateless members to avoid null-handling complexity.** | **Inherits all properties from `UnionAttributeBase`** (see above). diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 1de39f33..5a5585f1 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -151,6 +151,7 @@ This is a .NET library providing **Smart Enums**, **Value Objects**, and **Discr - **Ad-hoc `[Union]` or `[AdHocUnion(typeof(T1), typeof(T2))]`**: Simple 2-5 type combinations - Implicit conversion operators, IsT1/AsT1 properties, Switch/Map + - Stateless types (`TXIsStateless = true`): Memory-efficient members that store only discriminator, not instance data (prefer structs to avoid null-handling) - **Regular `[Union]`**: Inheritance-based unions with derived types - Static factory methods, Switch/Map over all derived types diff --git a/.serena/project.yml b/.serena/project.yml index 9fdd2282..0002661b 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -1,9 +1,3 @@ -# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) -# * For C, use cpp -# * For JavaScript, use typescript -# Special requirements: -# * csharp: Requires the presence of a .sln file in the project folder. -language: csharp # whether to use the project's gitignore file to ignore files # Added on 2025-04-07 @@ -64,5 +58,58 @@ excluded_tools: [] # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: "" - +# the name by which the project can be referenced within Serena project_name: "Thinktecture.Runtime.Extensions" + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: utf-8 + + +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# powershell python python_jedi r rego +# ruby ruby_solargraph rust scala swift +# terraform toml typescript typescript_vts vue +# yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- csharp diff --git a/docs b/docs index 58e558ba..b83366b8 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 58e558bae06bc04bfe8750cd1d606c3a7cea0f92 +Subproject commit b83366b827e0c6bdc488b47e10f0458a9f154afd diff --git a/samples/Basic.Samples/Unions/ApiResponseWithStateless.cs b/samples/Basic.Samples/Unions/ApiResponseWithStateless.cs new file mode 100644 index 00000000..9691d285 --- /dev/null +++ b/samples/Basic.Samples/Unions/ApiResponseWithStateless.cs @@ -0,0 +1,30 @@ +namespace Thinktecture.Unions; + +/// +/// API response union demonstrating stateless types for memory optimization. +/// NotFound and Unauthorized are stateless types that carry no instance data. +/// +[Union( + T1Name = "Success", + T2Name = "NotFound", T2IsStateless = true, + T3Name = "Unauthorized", T3IsStateless = true)] +public partial class ApiResponseWithStateless; + +public sealed class SuccessResponse +{ + public required string Data { get; init; } +} + +/// +/// Stateless type for "not found" state. +/// No backing field is allocated - only the discriminator index is stored. +/// Using a struct avoids null-handling complexity since default(NotFound) is a valid value. +/// +public readonly record struct NotFound; + +/// +/// Stateless type for "unauthorized" state. +/// No backing field is allocated - only the discriminator index is stored. +/// Using a struct avoids null-handling complexity since default(Unauthorized) is a valid value. +/// +public readonly record struct Unauthorized; diff --git a/samples/Basic.Samples/Unions/DiscriminatedUnionsDemos.cs b/samples/Basic.Samples/Unions/DiscriminatedUnionsDemos.cs index e7858f3d..3a968c76 100644 --- a/samples/Basic.Samples/Unions/DiscriminatedUnionsDemos.cs +++ b/samples/Basic.Samples/Unions/DiscriminatedUnionsDemos.cs @@ -15,6 +15,7 @@ public static void Demo(ILogger logger) DemoForJurisdiction(logger); DemoForPartiallyKnownDate(logger); DemoForNestedUnionsAndSwitchMapOverloads(logger); + DemoForStatelessTypes(logger); } private static void DemoForAdHocUnions(ILogger logger) @@ -336,8 +337,8 @@ void HandleFailure(ApiResponse.Failure failure) // With simple parameter naming, nested types use only their own name apiResponseSimple.Switch( success: success => logger.Information("[Switch] Success"), - notFound: notFound => logger.Information("[Switch] Not Found"), // Simple name - unauthorized: unauthorized => logger.Information("[Switch] Unauthorized") // Simple name + notFound: notFound => logger.Information("[Switch] Not Found"), // Simple name + unauthorized: unauthorized => logger.Information("[Switch] Unauthorized") // Simple name ); // Non-exhaustive overload (stopped at Failure level) @@ -353,4 +354,89 @@ void HandleFailureSimple(ApiResponseWithSimpleParameterNames.Failure failure) ); } } + + private static void DemoForStatelessTypes(ILogger logger) + { + logger.Information(""" + + + ==== Demo for Stateless Types (Memory Optimization) ==== + + """); + + // Creating different API responses + ApiResponseWithStateless successResponse = new SuccessResponse { Data = "Hello, World!" }; + ApiResponseWithStateless notFoundResponse = new NotFound(); + ApiResponseWithStateless unauthorizedResponse = new Unauthorized(); + + logger.Information("--- Type Checking ---"); + logger.Information("Success response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}", + successResponse.IsSuccess, successResponse.IsNotFound, successResponse.IsUnauthorized); + logger.Information("NotFound response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}", + notFoundResponse.IsSuccess, notFoundResponse.IsNotFound, notFoundResponse.IsUnauthorized); + logger.Information("Unauthorized response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}", + unauthorizedResponse.IsSuccess, unauthorizedResponse.IsNotFound, unauthorizedResponse.IsUnauthorized); + + logger.Information("--- Accessing Values ---"); + // For stateless types, accessors return default(T) + var notFoundValue = notFoundResponse.AsNotFound; + logger.Information("NotFound accessor returns: {Value} (default struct)", notFoundValue); + + var unauthorizedValue = unauthorizedResponse.AsUnauthorized; + logger.Information("Unauthorized accessor returns: {Value} (default struct)", unauthorizedValue); + + // Regular member still returns the actual instance + var successValue = successResponse.AsSuccess; + logger.Information("Success accessor returns: {Data}", successValue.Data); + + logger.Information("--- Switch Pattern Matching ---"); + successResponse.Switch( + success: s => logger.Information("[Switch] Success with data: {Data}", s.Data), + notFound: _ => logger.Information("[Switch] Not Found"), + unauthorized: _ => logger.Information("[Switch] Unauthorized") + ); + + notFoundResponse.Switch( + success: s => logger.Information("[Switch] Success with data: {Data}", s.Data), + notFound: _ => logger.Information("[Switch] Not Found"), + unauthorized: _ => logger.Information("[Switch] Unauthorized") + ); + + unauthorizedResponse.Switch( + success: s => logger.Information("[Switch] Success with data: {Data}", s.Data), + notFound: _ => logger.Information("[Switch] Not Found"), + unauthorized: _ => logger.Information("[Switch] Unauthorized") + ); + + logger.Information("--- Map Pattern Matching ---"); + var statusCode = successResponse.Map( + success: 200, + notFound: 404, + unauthorized: 401 + ); + logger.Information("Success response maps to status code: {StatusCode}", statusCode); + + statusCode = notFoundResponse.Map( + success: 200, + notFound: 404, + unauthorized: 401 + ); + logger.Information("NotFound response maps to status code: {StatusCode}", statusCode); + + statusCode = unauthorizedResponse.Map( + success: 200, + notFound: 404, + unauthorized: 401 + ); + logger.Information("Unauthorized response maps to status code: {StatusCode}", statusCode); + + logger.Information("--- Equality Comparison ---"); + var anotherNotFound = new NotFound(); + logger.Information("notFoundResponse == anotherNotFound: {IsEqual}", notFoundResponse == anotherNotFound); + + var anotherUnauthorized = new Unauthorized(); + logger.Information("unauthorizedResponse == anotherUnauthorized: {IsEqual}", unauthorizedResponse == anotherUnauthorized); + + logger.Information("notFoundResponse == unauthorizedResponse: {IsEqual}", notFoundResponse == unauthorizedResponse); + } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionCodeGenerator.cs index 8ccda474..2d641bdd 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionCodeGenerator.cs @@ -18,7 +18,7 @@ public AdHocUnionCodeGenerator( _state = state; _sb = sb; _useSharedObjectForRefTypes = state.Settings.UseSingleBackingField - || _state.MemberTypes.Where(t => t.IsReferenceType && t.TypeDuplicateCounter <= 1).Select(t => t.TypeFullyQualified).Count() >= 2; + || _state.MemberTypes.Where(t => t.IsReferenceType && t is { TypeDuplicateCounter: <= 1, Setting.IsStateless: false }).Select(t => t.TypeFullyQualified).Count() >= 2; } public override void Generate(CancellationToken cancellationToken) @@ -126,12 +126,12 @@ private void GenerateUnion(CancellationToken cancellationToken) GenerateConversionsFromValue(); GenerateConversionsToValue(); - + if (!_state.Settings.SkipEqualityComparison) { - GenerateEqualityOperators(); - GenerateEquals(); - GenerateGetHashCode(); + GenerateEqualityOperators(); + GenerateEquals(); + GenerateGetHashCode(); } if (!_state.Settings.SkipToString) @@ -252,7 +252,16 @@ private void GenerateToString() var memberType = _state.MemberTypes[i]; _sb.Append(@" - ").Append(i + 1).Append(" => ").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType); + ").Append(i + 1).Append(" => "); + + if (memberType.Setting.IsStateless) + { + _sb.Append("default(").AppendTypeFullyQualifiedWithoutNullAnnotation(memberType).Append(")"); + } + else + { + _sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType); + } if (memberType.SpecialType != SpecialType.System_String) { @@ -294,7 +303,14 @@ public override int GetHashCode() _sb.Append(@" ").Append(i + 1).Append(" => "); - _sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType); + if (memberType.Setting.IsStateless) + { + _sb.Append("typeof(").AppendTypeFullyQualifiedWithoutNullAnnotation(memberType).Append(")"); + } + else + { + _sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType); + } if (memberType.IsReferenceType) _sb.Append("?"); @@ -376,22 +392,31 @@ public bool Equals(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" ot _sb.Append(@" ").Append(i + 1).Append(" => "); - if (memberType.IsReferenceType) + if (memberType.Setting.IsStateless) { - _sb.Append("this.").AppendBackingFieldName(useSharedObjectBackingField, memberType).Append(" is null ? other.").AppendBackingFieldName(_state, _useSharedObjectForRefTypes, memberType).Append(" is null : "); + _sb.Append("true"); } - - if (useSharedObjectBackingField) + else { - _sb.Append("this._valueIndex == other._valueIndex && "); - } + if (memberType.IsReferenceType) + { + _sb.Append("this.").AppendBackingFieldName(useSharedObjectBackingField, memberType).Append(" is null ? other.").AppendBackingFieldName(_state, _useSharedObjectForRefTypes, memberType).Append(" is null : "); + } - _sb.AppendBackingFieldAccess(useSharedObjectBackingField, memberType, nullAnnotated: false, suppressed: true).Append(".Equals(").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType, qualifier: "other"); + if (useSharedObjectBackingField) + { + _sb.Append("this._valueIndex == other._valueIndex && "); + } - if (memberType.SpecialType == SpecialType.System_String) - _sb.Append(", global::System.StringComparison.").Append(_state.Settings.DefaultStringComparison); + _sb.AppendBackingFieldAccess(useSharedObjectBackingField, memberType, nullAnnotated: false, suppressed: true).Append(".Equals(").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType, qualifier: "other"); + + if (memberType.SpecialType == SpecialType.System_String) + _sb.Append(", global::System.StringComparison.").Append(_state.Settings.DefaultStringComparison); - _sb.Append("),"); + _sb.Append(")"); + } + + _sb.Append(","); } _sb.Append(@" @@ -538,7 +563,16 @@ private void GenerateIndexBasedActionSwitchBody(bool withState, bool isPartially if (withState) _sb.AppendEscaped(_state.Settings.SwitchMapStateParameterName).Append(", "); - _sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null).Append(@"); + if (memberType.Setting.IsStateless) + { + _sb.Append("default(").AppendTypeFullyQualifiedWithoutNullAnnotation(memberType).Append(")"); + } + else + { + _sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null); + } + + _sb.Append(@"); return;"); } @@ -701,7 +735,16 @@ private void GenerateIndexBasedFuncSwitchBody(bool withState, bool isPartially) if (withState) _sb.AppendEscaped(_state.Settings.SwitchMapStateParameterName).Append(", "); - _sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType).Append(memberType is { IsReferenceType: true, Setting.IsNullableReferenceType: false } ? "!" : null).Append(");"); + if (memberType.Setting.IsStateless) + { + _sb.Append("default(").AppendTypeFullyQualifiedWithoutNullAnnotation(memberType).Append(")"); + } + else + { + _sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType).Append(memberType is { IsReferenceType: true, Setting.IsNullableReferenceType: false } ? "!" : null); + } + + _sb.Append(");"); } _sb.Append(@" @@ -884,8 +927,15 @@ private void GenerateConstructors() } _sb.Append(@") - { - ").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType, false).Append(" = ").AppendEscaped(argName).Append(@"; + {"); + + if (!memberType.Setting.IsStateless) + { + _sb.Append(@" + ").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType, false).Append(" = ").AppendEscaped(argName).Append(";"); + } + + _sb.Append(@" this._valueIndex = "); if (hasDuplicates) @@ -935,6 +985,9 @@ private void GenerateMemberTypeFieldsAndProps() { var memberType = _state.MemberTypes[i]; + if (memberType.Setting.IsStateless) + continue; + if (memberType.TypeDuplicateCounter > 1) continue; @@ -978,8 +1031,18 @@ private void GenerateMemberTypeFieldsAndProps() /// /// If the current value is not of type ").AppendTypeFullyQualifiedForXmlComment(memberType).Append(@". public ").AppendTypeFullyQualified(memberType).Append(" As").Append(memberType.Name).Append(" => Is").Append(memberType.Name) - .Append(" ? ").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null) - .Append(" : throw new global::System.InvalidOperationException($\"'{nameof(").AppendTypeFullyQualified(_state).Append(")}' is not of type '").AppendTypeMinimallyQualified(memberType).Append("'.\");"); + .Append(" ? "); + + if (memberType.Setting.IsStateless) + { + _sb.Append("default(").AppendTypeFullyQualifiedWithoutNullAnnotation(memberType).Append(")"); + } + else + { + _sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null); + } + + _sb.Append(" : throw new global::System.InvalidOperationException($\"'{nameof(").AppendTypeFullyQualified(_state).Append(")}' is not of type '").AppendTypeMinimallyQualified(memberType).Append("'.\");"); } } @@ -1022,7 +1085,18 @@ private void GenerateRawValueGetter() var memberType = _state.MemberTypes[i]; _sb.Append(@" - ").Append(i + 1).Append(" => ").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType, withCast: false, nullAnnotated: false, suppressed: false).Append(memberType.IsReferenceType && !hasNullableTypes ? "!" : null).Append(","); + ").Append(i + 1).Append(" => "); + + if (memberType.Setting.IsStateless) + { + _sb.Append("default(").AppendTypeFullyQualifiedWithoutNullAnnotation(memberType).Append(")"); + } + else + { + _sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType, withCast: false, nullAnnotated: false, suppressed: false).Append(memberType.IsReferenceType && !hasNullableTypes ? "!" : null); + } + + _sb.Append(","); } _sb.Append(@" diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionMemberTypeSetting.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionMemberTypeSetting.cs index ad87d058..c83844e0 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionMemberTypeSetting.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionMemberTypeSetting.cs @@ -3,13 +3,16 @@ namespace Thinktecture.CodeAnalysis.AdHocUnions; public readonly struct AdHocUnionMemberTypeSetting : IEquatable, IHashCodeComputable { public bool IsNullableReferenceType { get; } + public bool IsStateless { get; } public string? Name { get; } public AdHocUnionMemberTypeSetting( bool isNullableReferenceType, + bool isStateless, string? name) { IsNullableReferenceType = isNullableReferenceType; + IsStateless = isStateless; Name = name; } @@ -21,6 +24,7 @@ public override bool Equals(object? obj) public bool Equals(AdHocUnionMemberTypeSetting other) { return IsNullableReferenceType == other.IsNullableReferenceType + && IsStateless == other.IsStateless && Name == other.Name; } @@ -28,7 +32,10 @@ public override int GetHashCode() { unchecked { - return (IsNullableReferenceType.GetHashCode() * 397) ^ (Name?.GetHashCode() ?? 0); + var hashCode = IsNullableReferenceType.GetHashCode(); + hashCode = (hashCode * 397) ^ IsStateless.GetHashCode(); + hashCode = (hashCode * 397) ^ (Name?.GetHashCode() ?? 0); + return hashCode; } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionSettings.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionSettings.cs index 4be08ece..2d550a2b 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionSettings.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionSettings.cs @@ -13,7 +13,6 @@ public sealed class AdHocUnionSettings : IEquatable public string SwitchMapStateParameterName { get; } public bool UseSingleBackingField { get; } public bool SkipEqualityComparison { get; } - public AdHocUnionSettings( AttributeData attribute, @@ -35,6 +34,7 @@ public AdHocUnionSettings( for (var i = 0; i < numberOfMemberTypes; i++) { memberTypeSettings.Add(new AdHocUnionMemberTypeSetting(attribute.FindTxIsNullableReferenceType(i + 1), + attribute.FindTxIsStateless(i + 1), attribute.FindTxName(i + 1))); } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionSourceGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionSourceGenerator.cs index 73fad220..68db34b2 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionSourceGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionSourceGenerator.cs @@ -174,7 +174,9 @@ private static bool IsCandidate(SyntaxNode syntaxNode, CancellationToken cancell return new SourceGenDiagnostic(tds, DiagnosticsDescriptors.ErrorDuringCodeAnalysis, [type.ToMinimallyQualifiedDisplayString(), $"The member type '{memberType.Name}' could not be resolved"]); var memberTypeSettings = settings.MemberTypeSettings[i]; - memberType = memberType.IsReferenceType && memberTypeSettings.IsNullableReferenceType ? memberType.WithNullableAnnotation(NullableAnnotation.Annotated) : memberType; + memberType = memberType.IsReferenceType && (memberTypeSettings.IsNullableReferenceType || memberTypeSettings.IsStateless) + ? memberType.WithNullableAnnotation(NullableAnnotation.Annotated) + : memberType; var typeState = factory.Create(memberType); var typeDuplicateCounter = 0; diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs index 034e867a..3e4d7973 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -187,13 +187,27 @@ public static bool FindTxIsNullableReferenceType(this AttributeData attributeDat { var name = index switch { - 0 => "T0IsNullableReferenceType", 1 => "T1IsNullableReferenceType", 2 => "T2IsNullableReferenceType", 3 => "T3IsNullableReferenceType", 4 => "T4IsNullableReferenceType", 5 => "T5IsNullableReferenceType", - _ => $"T{index}IsNullableReferenceType", + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + return GetBooleanParameterValue(attributeData, name) ?? false; + } + + public static bool FindTxIsStateless(this AttributeData attributeData, int index) + { + var name = index switch + { + 1 => "T1IsStateless", + 2 => "T2IsStateless", + 3 => "T3IsStateless", + 4 => "T4IsStateless", + 5 => "T5IsStateless", + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; return GetBooleanParameterValue(attributeData, name) ?? false; @@ -218,13 +232,12 @@ public static bool FindHasCorrespondingConstructor(this AttributeData attributeD { var name = index switch { - 0 => "T0Name", 1 => "T1Name", 2 => "T2Name", 3 => "T3Name", 4 => "T4Name", 5 => "T5Name", - _ => $"T{index}Name", + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; return GetStringParameterValue(attributeData, name); diff --git a/src/Thinktecture.Runtime.Extensions/AdHocUnionAttribute.cs b/src/Thinktecture.Runtime.Extensions/AdHocUnionAttribute.cs index af439579..b36de1e7 100644 --- a/src/Thinktecture.Runtime.Extensions/AdHocUnionAttribute.cs +++ b/src/Thinktecture.Runtime.Extensions/AdHocUnionAttribute.cs @@ -41,7 +41,22 @@ public sealed class AdHocUnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T1IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T1 is a reference type (since default(T) for reference types equals null). + /// + public bool T1IsNullableReferenceType + { + get => field || (T1.IsClass && T1IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T1) from accessors like AsT1 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T1IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -53,7 +68,22 @@ public sealed class AdHocUnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T2IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T2 is a reference type (since default(T) for reference types equals null). + /// + public bool T2IsNullableReferenceType + { + get => field || (T2.IsClass && T2IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T2) from accessors like AsT2 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T2IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -65,7 +95,22 @@ public sealed class AdHocUnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T3IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T3 is a reference type (since default(T) for reference types equals null). + /// + public bool T3IsNullableReferenceType + { + get => field || ((T3?.IsClass ?? false) && T3IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T3) from accessors like AsT3 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T3IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -77,7 +122,22 @@ public sealed class AdHocUnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T4IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T4 is a reference type (since default(T) for reference types equals null). + /// + public bool T4IsNullableReferenceType + { + get => field || ((T4?.IsClass ?? false) && T4IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T4) from accessors like AsT4 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T4IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -89,7 +149,22 @@ public sealed class AdHocUnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T5IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T5 is a reference type (since default(T) for reference types equals null). + /// + public bool T5IsNullableReferenceType + { + get => field || ((T5?.IsClass ?? false) && T5IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T5) from accessors like AsT5 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T5IsStateless { get; set; } /// /// Initializes a new instance of . diff --git a/src/Thinktecture.Runtime.Extensions/UnionAttribute`2.cs b/src/Thinktecture.Runtime.Extensions/UnionAttribute`2.cs index 113bdc0f..ad567c60 100644 --- a/src/Thinktecture.Runtime.Extensions/UnionAttribute`2.cs +++ b/src/Thinktecture.Runtime.Extensions/UnionAttribute`2.cs @@ -18,7 +18,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T1IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T1 is a reference type (since default(T) for reference types equals null). + /// + public bool T1IsNullableReferenceType + { + get => field || (typeof(T1).IsClass && T1IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T1) from accessors like AsT1 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T1IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -30,7 +45,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T2IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T2 is a reference type (since default(T) for reference types equals null). + /// + public bool T2IsNullableReferenceType + { + get => field || (typeof(T2).IsClass && T2IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T2) from accessors like AsT2 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T2IsStateless { get; set; } } /// @@ -52,7 +82,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T1IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T1 is a reference type (since default(T) for reference types equals null). + /// + public bool T1IsNullableReferenceType + { + get => field || (typeof(T1).IsClass && T1IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T1) from accessors like AsT1 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T1IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -64,7 +109,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T2IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T2 is a reference type (since default(T) for reference types equals null). + /// + public bool T2IsNullableReferenceType + { + get => field || (typeof(T2).IsClass && T2IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T2) from accessors like AsT2 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T2IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -76,7 +136,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T3IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T3 is a reference type (since default(T) for reference types equals null). + /// + public bool T3IsNullableReferenceType + { + get => field || (typeof(T3).IsClass && T3IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T3) from accessors like AsT3 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T3IsStateless { get; set; } } /// @@ -99,7 +174,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T1IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T1 is a reference type (since default(T) for reference types equals null). + /// + public bool T1IsNullableReferenceType + { + get => field || (typeof(T1).IsClass && T1IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T1) from accessors like AsT1 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T1IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -111,7 +201,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T2IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T2 is a reference type (since default(T) for reference types equals null). + /// + public bool T2IsNullableReferenceType + { + get => field || (typeof(T2).IsClass && T2IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T2) from accessors like AsT2 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T2IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -123,7 +228,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T3IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T3 is a reference type (since default(T) for reference types equals null). + /// + public bool T3IsNullableReferenceType + { + get => field || (typeof(T3).IsClass && T3IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T3) from accessors like AsT3 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T3IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -135,7 +255,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T4IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T4 is a reference type (since default(T) for reference types equals null). + /// + public bool T4IsNullableReferenceType + { + get => field || (typeof(T4).IsClass && T4IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T4) from accessors like AsT4 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T4IsStateless { get; set; } } /// @@ -159,7 +294,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T1IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T1 is a reference type (since default(T) for reference types equals null). + /// + public bool T1IsNullableReferenceType + { + get => field || (typeof(T1).IsClass && T1IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T1) from accessors like AsT1 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T1IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -171,7 +321,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T2IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T2 is a reference type (since default(T) for reference types equals null). + /// + public bool T2IsNullableReferenceType + { + get => field || (typeof(T2).IsClass && T2IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T2) from accessors like AsT2 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T2IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -183,7 +348,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T3IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T3 is a reference type (since default(T) for reference types equals null). + /// + public bool T3IsNullableReferenceType + { + get => field || (typeof(T3).IsClass && T3IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T3) from accessors like AsT3 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T3IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -195,7 +375,22 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T4IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T4 is a reference type (since default(T) for reference types equals null). + /// + public bool T4IsNullableReferenceType + { + get => field || (typeof(T4).IsClass && T4IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T4) from accessors like AsT4 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T4IsStateless { get; set; } /// /// Changes the name of all members regarding . @@ -207,5 +402,20 @@ public sealed class UnionAttribute : UnionAttributeBase /// Makes the type argument a nullable reference type. /// This setting has no effect if is a struct. /// - public bool T5IsNullableReferenceType { get; set; } + /// + /// Returns true when explicitly set to true or when is true and T5 is a reference type (since default(T) for reference types equals null). + /// + public bool T5IsNullableReferenceType + { + get => field || (typeof(T5).IsClass && T5IsStateless); + set; + } + + /// + /// Indicates that is a stateless type that carries no meaningful instance data. + /// Stateless types reduce memory footprint by storing only the discriminator index rather than type data. + /// The generated code will return default(T5) from accessors like AsT5 and Value. + /// It is recommended to use struct types for stateless members to avoid null-handling complexity. + /// + public bool T5IsStateless { get; set; } } diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/CodeAnalysis/InstanceMemberInfoTests/CreateOrNull_Property.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/CodeAnalysis/InstanceMemberInfoTests/CreateOrNull_Property.cs index 6c5feb75..21643109 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/CodeAnalysis/InstanceMemberInfoTests/CreateOrNull_Property.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/CodeAnalysis/InstanceMemberInfoTests/CreateOrNull_Property.cs @@ -262,7 +262,7 @@ public class TestClass public string Property { get; } } "; - var compilation = CreateCompilation(src); + var compilation = CreateCompilation(src, additionalReferences: [typeof(System.Text.Json.Serialization.JsonConverterAttribute).Assembly.Location]); var factory = TypedMemberStateFactory.Create(compilation); var type = GetTypeSymbol(compilation, "Test.TestClass"); var property = (IPropertySymbol)type.GetMembers("Property").First(); diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Extensions/AttributeDataExtensionsTests/FindTxName.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Extensions/AttributeDataExtensionsTests/FindTxName.cs index fa950ec9..1aff80a8 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Extensions/AttributeDataExtensionsTests/FindTxName.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Extensions/AttributeDataExtensionsTests/FindTxName.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; namespace Thinktecture.Runtime.Tests.AttributeDataExtensionsTests; @@ -169,7 +170,7 @@ public partial class TestUnion } [Fact] - public void Should_return_null_for_invalid_index() + public void Should_throw_for_invalid_index() { // Arrange var source = """ @@ -187,11 +188,10 @@ public partial class TestUnion var typeSymbol = GetTypeSymbol(compilation, "Test.TestUnion"); var attributeData = typeSymbol.GetAttributes().Single(); - // Act - var result = attributeData.FindTxName(10); - - // Assert - result.Should().BeNull(); + // Act + Assert + attributeData.Invoking(ad => ad.FindTxName(10)) + .Should().Throw() + .WithMessage("Specified argument was out of the range of valid values. (Parameter 'index')"); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_struct_union_with_duplicate_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_struct_union_with_duplicate_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..4cfca0df --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_struct_union_with_duplicate_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,371 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Auto)] + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + readonly partial struct TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValue), + typeof(global::Thinktecture.Tests.NullValue), + typeof(int) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly int _int32; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue1 => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue2 => this._valueIndex == 2; + + /// + /// Indication whether the current value is of type . + /// + public bool IsInt32 => this._valueIndex == 3; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue1 => IsNullValue1 ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue2 => IsNullValue2 ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + 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 . + /// + /// If the union (struct) is not initialized or initialized with default value. + public object Value => this._valueIndex switch + { + 0 => throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."), + 1 => default(global::Thinktecture.Tests.NullValue), + 2 => default(global::Thinktecture.Tests.NullValue), + 3 => this._int32, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + private TestUnion(global::Thinktecture.Tests.NullValue @value, int @valueIndex) + { + this._valueIndex = @valueIndex; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int @int32) + { + this._int32 = @int32; + this._valueIndex = 3; + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateNullValue1(global::Thinktecture.Tests.NullValue @nullValue1) + { + return new TestUnion(@nullValue1, 1); + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateNullValue2(global::Thinktecture.Tests.NullValue @nullValue2) + { + return new TestUnion(@nullValue2, 2); + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32) + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + @nullValue1(default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @nullValue2(default(global::Thinktecture.Tests.NullValue)); + return; + case 3: + @int32(this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + @nullValue1(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @nullValue2(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 3: + @int32(@state, this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + return @nullValue1(default(global::Thinktecture.Tests.NullValue)); + case 2: + return @nullValue2(default(global::Thinktecture.Tests.NullValue)); + case 3: + return @int32(this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + return @nullValue1(@state, default(global::Thinktecture.Tests.NullValue)); + case 2: + return @nullValue2(@state, default(global::Thinktecture.Tests.NullValue)); + case 3: + return @int32(@state, this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValue1, + TResult @nullValue2, + TResult @int32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + return @nullValue1; + case 2: + return @nullValue2; + case 3: + return @int32; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(int @int32) + { + return new global::Thinktecture.Tests.TestUnion(@int32); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + 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) + { + 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 (this._valueIndex != other._valueIndex) + return false; + + return this._valueIndex switch + { + 0 => throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."), + 1 => true, + 2 => true, + 3 => this._int32.Equals(other._int32), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 0 => throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."), + 1 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 2 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 3 => this._int32.GetHashCode(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 0 => throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."), + 1 => default(global::Thinktecture.Tests.NullValue).ToString(), + 2 => default(global::Thinktecture.Tests.NullValue).ToString(), + 3 => this._int32.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_struct_union_with_stateless_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_struct_union_with_stateless_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..0b1795b7 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_struct_union_with_stateless_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,340 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Auto)] + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + readonly partial struct TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValue), + typeof(int) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly int _int32; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsInt32 => this._valueIndex == 2; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue => IsNullValue ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + 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 . + /// + /// If the union (struct) is not initialized or initialized with default value. + public object Value => this._valueIndex switch + { + 0 => throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."), + 1 => default(global::Thinktecture.Tests.NullValue), + 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(global::Thinktecture.Tests.NullValue @nullValue) + { + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int @int32) + { + this._int32 = @int32; + this._valueIndex = 2; + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32) + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + @nullValue(default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @int32(this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @int32(@state, this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + return @nullValue(default(global::Thinktecture.Tests.NullValue)); + case 2: + return @int32(this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + return @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + case 2: + return @int32(@state, this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// If the union (struct) is not initialized or initialized with default value. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValue, + TResult @int32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 0: + throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."); + case 1: + return @nullValue; + case 2: + return @int32; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + return new global::Thinktecture.Tests.TestUnion(@nullValue); + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(int @int32) + { + return new global::Thinktecture.Tests.TestUnion(@int32); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.NullValue(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsNullValue; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + 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) + { + 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 (this._valueIndex != other._valueIndex) + return false; + + return this._valueIndex switch + { + 0 => throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."), + 1 => true, + 2 => this._int32.Equals(other._int32), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 0 => throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."), + 1 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 2 => this._int32.GetHashCode(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 0 => throw new global::System.InvalidOperationException($"This struct of type 'TestUnion' is not initialized. Make sure all fields, properties and variables are initialized with non-default values."), + 1 => default(global::Thinktecture.Tests.NullValue).ToString(), + 2 => this._int32.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_T1_as_stateless_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_T1_as_stateless_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..d6b31234 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_T1_as_stateless_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,328 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValue), + typeof(string) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString => this._valueIndex == 2; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue => IsNullValue ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue), + 2 => this._string!, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 2; + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) + { + switch (this._valueIndex) + { + case 1: + @nullValue(default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @string(this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @string(@state, this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(default(global::Thinktecture.Tests.NullValue)); + case 2: + return @string(this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + case 2: + return @string(@state, this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValue, + TResult @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue; + case 2: + return @string; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + return new global::Thinktecture.Tests.TestUnion(@nullValue); + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(string @string) + { + return new global::Thinktecture.Tests.TestUnion(@string); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.NullValue(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsNullValue; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// 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 => true, + 2 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 2 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue).ToString(), + 2 => this._string, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_T2_as_stateless_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_T2_as_stateless_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..c9083a1c --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_T2_as_stateless_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,328 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(string), + typeof(global::Thinktecture.Tests.EmptyState) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsEmptyState => this._valueIndex == 2; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.EmptyState AsEmptyState => IsEmptyState ? default(global::Thinktecture.Tests.EmptyState) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'EmptyState'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => this._string!, + 2 => default(global::Thinktecture.Tests.EmptyState), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.EmptyState @emptyState) + { + this._valueIndex = 2; + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState) + { + switch (this._valueIndex) + { + case 1: + @string(this._string!); + return; + case 2: + @emptyState(default(global::Thinktecture.Tests.EmptyState)); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @string(@state, this._string!); + return; + case 2: + @emptyState(@state, default(global::Thinktecture.Tests.EmptyState)); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @string(this._string!); + case 2: + return @emptyState(default(global::Thinktecture.Tests.EmptyState)); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @string(@state, this._string!); + case 2: + return @emptyState(@state, default(global::Thinktecture.Tests.EmptyState)); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @string, + TResult @emptyState) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @string; + case 2: + return @emptyState; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(string @string) + { + return new global::Thinktecture.Tests.TestUnion(@string); + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.EmptyState @emptyState) + { + return new global::Thinktecture.Tests.TestUnion(@emptyState); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.EmptyState(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsEmptyState; + } + + /// + /// 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._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + 2 => true, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + 2 => typeof(global::Thinktecture.Tests.EmptyState).GetHashCode(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._string, + 2 => default(global::Thinktecture.Tests.EmptyState).ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_all_three_types_as_stateless_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_all_three_types_as_stateless_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..2240ab75 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_all_three_types_as_stateless_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,394 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValue), + typeof(global::Thinktecture.Tests.EmptyState), + typeof(global::Thinktecture.Tests.UndefinedValue) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsEmptyState => this._valueIndex == 2; + + /// + /// Indication whether the current value is of type . + /// + public bool IsUndefinedValue => this._valueIndex == 3; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue => IsNullValue ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.EmptyState AsEmptyState => IsEmptyState ? default(global::Thinktecture.Tests.EmptyState) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'EmptyState'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.UndefinedValue AsUndefinedValue => IsUndefinedValue ? default(global::Thinktecture.Tests.UndefinedValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'UndefinedValue'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue), + 2 => default(global::Thinktecture.Tests.EmptyState), + 3 => default(global::Thinktecture.Tests.UndefinedValue), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.EmptyState @emptyState) + { + this._valueIndex = 2; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.UndefinedValue @undefinedValue) + { + this._valueIndex = 3; + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @undefinedValue) + { + switch (this._valueIndex) + { + case 1: + @nullValue(default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @emptyState(default(global::Thinktecture.Tests.EmptyState)); + return; + case 3: + @undefinedValue(default(global::Thinktecture.Tests.UndefinedValue)); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @undefinedValue) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @emptyState(@state, default(global::Thinktecture.Tests.EmptyState)); + return; + case 3: + @undefinedValue(@state, default(global::Thinktecture.Tests.UndefinedValue)); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @undefinedValue) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(default(global::Thinktecture.Tests.NullValue)); + case 2: + return @emptyState(default(global::Thinktecture.Tests.EmptyState)); + case 3: + return @undefinedValue(default(global::Thinktecture.Tests.UndefinedValue)); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @undefinedValue) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + case 2: + return @emptyState(@state, default(global::Thinktecture.Tests.EmptyState)); + case 3: + return @undefinedValue(@state, default(global::Thinktecture.Tests.UndefinedValue)); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValue, + TResult @emptyState, + TResult @undefinedValue) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue; + case 2: + return @emptyState; + case 3: + return @undefinedValue; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + return new global::Thinktecture.Tests.TestUnion(@nullValue); + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.EmptyState @emptyState) + { + return new global::Thinktecture.Tests.TestUnion(@emptyState); + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.UndefinedValue @undefinedValue) + { + return new global::Thinktecture.Tests.TestUnion(@undefinedValue); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.NullValue(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsNullValue; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.EmptyState(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsEmptyState; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.UndefinedValue(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsUndefinedValue; + } + + /// + /// 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 => true, + 2 => true, + 3 => true, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 2 => typeof(global::Thinktecture.Tests.EmptyState).GetHashCode(), + 3 => typeof(global::Thinktecture.Tests.UndefinedValue).GetHashCode(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue).ToString(), + 2 => default(global::Thinktecture.Tests.EmptyState).ToString(), + 3 => default(global::Thinktecture.Tests.UndefinedValue).ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_reference_type_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_reference_type_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..a46d4557 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_reference_type_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,359 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValueClass), + typeof(global::Thinktecture.Tests.NullValueClass), + typeof(int) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly int _int32; + + /// + /// Indication whether the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// + public bool IsNullValueClass1 => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// + public bool IsNullValueClass2 => this._valueIndex == 2; + + /// + /// Indication whether the current value is of type . + /// + public bool IsInt32 => this._valueIndex == 3; + + /// + /// Gets the current value as global::Thinktecture.Tests.NullValueClass?. + /// + /// If the current value is not of type global::Thinktecture.Tests.NullValueClass?. + public global::Thinktecture.Tests.NullValueClass? AsNullValueClass1 => IsNullValueClass1 ? default(global::Thinktecture.Tests.NullValueClass) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValueClass?'."); + + /// + /// Gets the current value as global::Thinktecture.Tests.NullValueClass?. + /// + /// If the current value is not of type global::Thinktecture.Tests.NullValueClass?. + public global::Thinktecture.Tests.NullValueClass? AsNullValueClass2 => IsNullValueClass2 ? default(global::Thinktecture.Tests.NullValueClass) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValueClass?'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + 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 => default(global::Thinktecture.Tests.NullValueClass), + 2 => default(global::Thinktecture.Tests.NullValueClass), + 3 => this._int32, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + private TestUnion(global::Thinktecture.Tests.NullValueClass? @value, int @valueIndex) + { + this._valueIndex = @valueIndex; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int @int32) + { + this._int32 = @int32; + this._valueIndex = 3; + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateNullValueClass1(global::Thinktecture.Tests.NullValueClass? @nullValueClass1) + { + return new TestUnion(@nullValueClass1, 1); + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateNullValueClass2(global::Thinktecture.Tests.NullValueClass? @nullValueClass2) + { + return new TestUnion(@nullValueClass2, 2); + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The action to execute if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValueClass1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValueClass2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32) + { + switch (this._valueIndex) + { + case 1: + @nullValueClass1(default(global::Thinktecture.Tests.NullValueClass)); + return; + case 2: + @nullValueClass2(default(global::Thinktecture.Tests.NullValueClass)); + return; + case 3: + @int32(this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The action to execute if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValueClass1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValueClass2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @nullValueClass1(@state, default(global::Thinktecture.Tests.NullValueClass)); + return; + case 2: + @nullValueClass2(@state, default(global::Thinktecture.Tests.NullValueClass)); + return; + case 3: + @int32(@state, this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The function to execute if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValueClass1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValueClass2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValueClass1(default(global::Thinktecture.Tests.NullValueClass)); + case 2: + return @nullValueClass2(default(global::Thinktecture.Tests.NullValueClass)); + case 3: + return @int32(this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The function to execute if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValueClass1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValueClass2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValueClass1(@state, default(global::Thinktecture.Tests.NullValueClass)); + case 2: + return @nullValueClass2(@state, default(global::Thinktecture.Tests.NullValueClass)); + case 3: + return @int32(@state, this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The instance to return if the current value is of type global::Thinktecture.Tests.NullValueClass?. + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValueClass1, + TResult @nullValueClass2, + TResult @int32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValueClass1; + case 2: + return @nullValueClass2; + case 3: + return @int32; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(int @int32) + { + return new global::Thinktecture.Tests.TestUnion(@int32); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + 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 => true, + 2 => true, + 3 => this._int32.Equals(other._int32), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => typeof(global::Thinktecture.Tests.NullValueClass)?.GetHashCode() ?? 0, + 2 => typeof(global::Thinktecture.Tests.NullValueClass)?.GetHashCode() ?? 0, + 3 => this._int32.GetHashCode(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValueClass)?.ToString(), + 2 => default(global::Thinktecture.Tests.NullValueClass)?.ToString(), + 3 => this._int32.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_regular_string_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_regular_string_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..8771d6f6 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_regular_string_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,478 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(string), + typeof(int), + typeof(string), + typeof(string), + typeof(int?) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly string? _string; + private readonly int _int32; + private readonly int? _nullableOfInt32; + + /// + /// Indication whether the current value is of type . + /// + public bool IsText => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsInt32 => this._valueIndex == 2; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString2 => this._valueIndex == 3; + + /// + /// Indication whether the current value is of type string?. + /// + public bool IsString3 => this._valueIndex == 4; + + /// + /// Indication whether the current value is of type int?. + /// + public bool IsNullableOfInt32 => this._valueIndex == 5; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsText => IsText ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + 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 . + /// + /// If the current value is not of type . + public string AsString2 => IsString2 ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as string?. + /// + /// If the current value is not of type string?. + public string? AsString3 => IsString3 ? this._string : 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? AsNullableOfInt32 => IsNullableOfInt32 ? this._nullableOfInt32 : 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._string, + 2 => this._int32, + 3 => this._string, + 4 => this._string, + 5 => this._nullableOfInt32, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + private TestUnion(string? @value, int @valueIndex) + { + this._string = @value; + this._valueIndex = @valueIndex; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int @int32) + { + this._int32 = @int32; + this._valueIndex = 2; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int? @nullableOfInt32) + { + this._nullableOfInt32 = @nullableOfInt32; + this._valueIndex = 5; + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateText(string @text) + { + return new TestUnion(@text, 1); + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateString2(string @string2) + { + return new TestUnion(@string2, 3); + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateString3(string? @string3) + { + return new TestUnion(@string3, 4); + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type string?. + /// The action to execute if the current value is of type int?. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @text, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string3, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullableOfInt32) + { + switch (this._valueIndex) + { + case 1: + @text(this._string!); + return; + case 2: + @int32(this._int32); + return; + case 3: + @string2(this._string!); + return; + case 4: + @string3(this._string); + return; + case 5: + @nullableOfInt32(this._nullableOfInt32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type string?. + /// The action to execute if the current value is of type int?. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @text, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string3, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullableOfInt32) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @text(@state, this._string!); + return; + case 2: + @int32(@state, this._int32); + return; + case 3: + @string2(@state, this._string!); + return; + case 4: + @string3(@state, this._string); + return; + case 5: + @nullableOfInt32(@state, this._nullableOfInt32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type string?. + /// The function to execute if the current value is of type int?. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @text, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string3, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullableOfInt32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @text(this._string!); + case 2: + return @int32(this._int32); + case 3: + return @string2(this._string!); + case 4: + return @string3(this._string); + case 5: + return @nullableOfInt32(this._nullableOfInt32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type string?. + /// The function to execute if the current value is of type int?. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @text, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string3, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullableOfInt32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @text(@state, this._string!); + case 2: + return @int32(@state, this._int32); + case 3: + return @string2(@state, this._string!); + case 4: + return @string3(@state, this._string); + case 5: + return @nullableOfInt32(@state, this._nullableOfInt32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// The instance to return if the current value is 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?. + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @text, + TResult @int32, + TResult @string2, + TResult @string3, + TResult @nullableOfInt32) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @text; + case 2: + return @int32; + case 3: + return @string2; + case 4: + return @string3; + case 5: + return @nullableOfInt32; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert 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 from type int?. + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(int? @nullableOfInt32) + { + return new global::Thinktecture.Tests.TestUnion(@nullableOfInt32); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator int(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsInt32; + } + + /// + /// Explicit conversion to type int?. + /// + /// Object to convert. + /// 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.AsNullableOfInt32; + } + + /// + /// 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._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + 2 => this._int32.Equals(other._int32), + 3 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + 4 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + 5 => this._nullableOfInt32.Equals(other._nullableOfInt32), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + 2 => this._int32.GetHashCode(), + 3 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + 4 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + 5 => this._nullableOfInt32.GetHashCode(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._string, + 2 => this._int32.ToString(), + 3 => this._string, + 4 => this._string, + 5 => this._nullableOfInt32.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_regular_types_requiring_factory_methods_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_regular_types_requiring_factory_methods_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..f7b57b95 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_regular_types_requiring_factory_methods_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,392 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(int), + typeof(string), + typeof(int), + typeof(string) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly int _int32; + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsValue1 => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString1 => this._valueIndex == 2; + + /// + /// Indication whether the current value is of type . + /// + public bool IsValue2 => this._valueIndex == 3; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString2 => this._valueIndex == 4; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public int AsValue1 => IsValue1 ? this._int32 : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'int'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString1 => IsString1 ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public int AsValue2 => IsValue2 ? this._int32 : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'int'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString2 => IsString2 ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => this._int32, + 2 => this._string!, + 3 => this._int32, + 4 => this._string!, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + private TestUnion(int @value, int @valueIndex) + { + this._int32 = @value; + this._valueIndex = @valueIndex; + } + + private TestUnion(string? @value, int @valueIndex) + { + this._string = @value; + this._valueIndex = @valueIndex; + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateValue1(int @value1) + { + return new TestUnion(@value1, 1); + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateString1(string @string1) + { + return new TestUnion(@string1, 2); + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateValue2(int @value2) + { + return new TestUnion(@value2, 3); + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateString2(string @string2) + { + return new TestUnion(@string2, 4); + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @value1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @value2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string2) + { + switch (this._valueIndex) + { + case 1: + @value1(this._int32); + return; + case 2: + @string1(this._string!); + return; + case 3: + @value2(this._int32); + return; + case 4: + @string2(this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @value1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @value2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string2) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @value1(@state, this._int32); + return; + case 2: + @string1(@state, this._string!); + return; + case 3: + @value2(@state, this._int32); + return; + case 4: + @string2(@state, this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @value1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @value2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string2) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @value1(this._int32); + case 2: + return @string1(this._string!); + case 3: + return @value2(this._int32); + case 4: + return @string2(this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @value1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @value2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string2) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @value1(@state, this._int32); + case 2: + return @string1(@state, this._string!); + case 3: + return @value2(@state, this._int32); + case 4: + return @string2(@state, this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @value1, + TResult @string1, + TResult @value2, + TResult @string2) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @value1; + case 2: + return @string1; + case 3: + return @value2; + case 4: + return @string2; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// 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._int32.Equals(other._int32), + 2 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + 3 => this._int32.Equals(other._int32), + 4 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => this._int32.GetHashCode(), + 2 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + 3 => this._int32.GetHashCode(), + 4 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._int32.ToString(), + 2 => this._string, + 3 => this._int32.ToString(), + 4 => this._string, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_stateless_types_T2_and_T3_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_stateless_types_T2_and_T3_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..fa4e41ed --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_stateless_types_T2_and_T3_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,359 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(string), + typeof(global::Thinktecture.Tests.EmptyState), + typeof(global::Thinktecture.Tests.EmptyState) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsEmptyState1 => this._valueIndex == 2; + + /// + /// Indication whether the current value is of type . + /// + public bool IsEmptyState2 => this._valueIndex == 3; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.EmptyState AsEmptyState1 => IsEmptyState1 ? default(global::Thinktecture.Tests.EmptyState) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'EmptyState'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.EmptyState AsEmptyState2 => IsEmptyState2 ? default(global::Thinktecture.Tests.EmptyState) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'EmptyState'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => this._string!, + 2 => default(global::Thinktecture.Tests.EmptyState), + 3 => default(global::Thinktecture.Tests.EmptyState), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 1; + } + + private TestUnion(global::Thinktecture.Tests.EmptyState @value, int @valueIndex) + { + this._valueIndex = @valueIndex; + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateEmptyState1(global::Thinktecture.Tests.EmptyState @emptyState1) + { + return new TestUnion(@emptyState1, 2); + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateEmptyState2(global::Thinktecture.Tests.EmptyState @emptyState2) + { + return new TestUnion(@emptyState2, 3); + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState2) + { + switch (this._valueIndex) + { + case 1: + @string(this._string!); + return; + case 2: + @emptyState1(default(global::Thinktecture.Tests.EmptyState)); + return; + case 3: + @emptyState2(default(global::Thinktecture.Tests.EmptyState)); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState2) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @string(@state, this._string!); + return; + case 2: + @emptyState1(@state, default(global::Thinktecture.Tests.EmptyState)); + return; + case 3: + @emptyState2(@state, default(global::Thinktecture.Tests.EmptyState)); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState2) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @string(this._string!); + case 2: + return @emptyState1(default(global::Thinktecture.Tests.EmptyState)); + case 3: + return @emptyState2(default(global::Thinktecture.Tests.EmptyState)); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState2) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @string(@state, this._string!); + case 2: + return @emptyState1(@state, default(global::Thinktecture.Tests.EmptyState)); + case 3: + return @emptyState2(@state, default(global::Thinktecture.Tests.EmptyState)); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @string, + TResult @emptyState1, + TResult @emptyState2) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @string; + case 2: + return @emptyState1; + case 3: + return @emptyState2; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(string @string) + { + return new global::Thinktecture.Tests.TestUnion(@string); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// 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._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + 2 => true, + 3 => true, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + 2 => typeof(global::Thinktecture.Tests.EmptyState).GetHashCode(), + 3 => typeof(global::Thinktecture.Tests.EmptyState).GetHashCode(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._string, + 2 => default(global::Thinktecture.Tests.EmptyState).ToString(), + 3 => default(global::Thinktecture.Tests.EmptyState).ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_value_struct_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_value_struct_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..04204c7b --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_duplicate_value_struct_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,359 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValue), + typeof(global::Thinktecture.Tests.NullValue), + typeof(string) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue1 => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue2 => this._valueIndex == 2; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString => this._valueIndex == 3; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue1 => IsNullValue1 ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue2 => IsNullValue2 ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue), + 2 => default(global::Thinktecture.Tests.NullValue), + 3 => this._string!, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + private TestUnion(global::Thinktecture.Tests.NullValue @value, int @valueIndex) + { + this._valueIndex = @valueIndex; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 3; + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateNullValue1(global::Thinktecture.Tests.NullValue @nullValue1) + { + return new TestUnion(@nullValue1, 1); + } + + /// + /// Creates new instance with . + /// + /// Value to create a new instance for. + public static TestUnion CreateNullValue2(global::Thinktecture.Tests.NullValue @nullValue2) + { + return new TestUnion(@nullValue2, 2); + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) + { + switch (this._valueIndex) + { + case 1: + @nullValue1(default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @nullValue2(default(global::Thinktecture.Tests.NullValue)); + return; + case 3: + @string(this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @nullValue1(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @nullValue2(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 3: + @string(@state, this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue1(default(global::Thinktecture.Tests.NullValue)); + case 2: + return @nullValue2(default(global::Thinktecture.Tests.NullValue)); + case 3: + return @string(this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue1, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue2, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue1(@state, default(global::Thinktecture.Tests.NullValue)); + case 2: + return @nullValue2(@state, default(global::Thinktecture.Tests.NullValue)); + case 3: + return @string(@state, this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValue1, + TResult @nullValue2, + TResult @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue1; + case 2: + return @nullValue2; + case 3: + return @string; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(string @string) + { + return new global::Thinktecture.Tests.TestUnion(@string); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// 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 => true, + 2 => true, + 3 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 2 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 3 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue).ToString(), + 2 => default(global::Thinktecture.Tests.NullValue).ToString(), + 3 => this._string, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_multiple_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_multiple_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..c11d8a4a --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_multiple_stateless_types_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,396 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValue), + typeof(global::Thinktecture.Tests.EmptyState), + typeof(string) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsEmptyState => this._valueIndex == 2; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString => this._valueIndex == 3; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue => IsNullValue ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.EmptyState AsEmptyState => IsEmptyState ? default(global::Thinktecture.Tests.EmptyState) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'EmptyState'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue), + 2 => default(global::Thinktecture.Tests.EmptyState), + 3 => this._string!, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.EmptyState @emptyState) + { + this._valueIndex = 2; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 3; + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) + { + switch (this._valueIndex) + { + case 1: + @nullValue(default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @emptyState(default(global::Thinktecture.Tests.EmptyState)); + return; + case 3: + @string(this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @emptyState, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @emptyState(@state, default(global::Thinktecture.Tests.EmptyState)); + return; + case 3: + @string(@state, this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(default(global::Thinktecture.Tests.NullValue)); + case 2: + return @emptyState(default(global::Thinktecture.Tests.EmptyState)); + case 3: + return @string(this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @emptyState, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + case 2: + return @emptyState(@state, default(global::Thinktecture.Tests.EmptyState)); + case 3: + return @string(@state, this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValue, + TResult @emptyState, + TResult @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue; + case 2: + return @emptyState; + case 3: + return @string; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + return new global::Thinktecture.Tests.TestUnion(@nullValue); + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.EmptyState @emptyState) + { + return new global::Thinktecture.Tests.TestUnion(@emptyState); + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(string @string) + { + return new global::Thinktecture.Tests.TestUnion(@string); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.NullValue(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsNullValue; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.EmptyState(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsEmptyState; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// 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 => true, + 2 => true, + 3 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 2 => typeof(global::Thinktecture.Tests.EmptyState).GetHashCode(), + 3 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue).ToString(), + 2 => default(global::Thinktecture.Tests.EmptyState).ToString(), + 3 => this._string, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_nullable_and_non_nullable_of_same_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_nullable_and_non_nullable_of_same_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..9e7feed0 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_nullable_and_non_nullable_of_same_type_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,400 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(int), + typeof(int?), + typeof(string) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly int _int32; + private readonly int? _nullableOfInt32; + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsInt32 => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type int?. + /// + public bool IsNullableOfInt32 => this._valueIndex == 2; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString => this._valueIndex == 3; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + 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 int?. + /// + /// If the current value is not of type int?. + public int? AsNullableOfInt32 => IsNullableOfInt32 ? this._nullableOfInt32 : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'int?'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + public object? Value => this._valueIndex switch + { + 1 => this._int32, + 2 => this._nullableOfInt32, + 3 => this._string, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int @int32) + { + this._int32 = @int32; + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int? @nullableOfInt32) + { + this._nullableOfInt32 = @nullableOfInt32; + this._valueIndex = 2; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 3; + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type int?. + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullableOfInt32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) + { + switch (this._valueIndex) + { + case 1: + @int32(this._int32); + return; + case 2: + @nullableOfInt32(this._nullableOfInt32); + return; + case 3: + @string(this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type int?. + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @int32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullableOfInt32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @int32(@state, this._int32); + return; + case 2: + @nullableOfInt32(@state, this._nullableOfInt32); + return; + case 3: + @string(@state, this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type int?. + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullableOfInt32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @int32(this._int32); + case 2: + return @nullableOfInt32(this._nullableOfInt32); + case 3: + return @string(this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type int?. + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @int32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullableOfInt32, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @int32(@state, this._int32); + case 2: + return @nullableOfInt32(@state, this._nullableOfInt32); + case 3: + return @string(@state, this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type int?. + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @int32, + TResult @nullableOfInt32, + TResult @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @int32; + case 2: + return @nullableOfInt32; + case 3: + return @string; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert 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 from type int?. + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(int? @nullableOfInt32) + { + return new global::Thinktecture.Tests.TestUnion(@nullableOfInt32); + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(string @string) + { + return new global::Thinktecture.Tests.TestUnion(@string); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator int(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsInt32; + } + + /// + /// Explicit conversion to type int?. + /// + /// Object to convert. + /// 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.AsNullableOfInt32; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// 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._int32.Equals(other._int32), + 2 => this._nullableOfInt32.Equals(other._nullableOfInt32), + 3 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => this._int32.GetHashCode(), + 2 => this._nullableOfInt32.GetHashCode(), + 3 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._int32.ToString(), + 2 => this._nullableOfInt32.ToString(), + 3 => this._string, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_stateless_type_and_conversion_operators_disabled_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_stateless_type_and_conversion_operators_disabled_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..bf7956c0 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_stateless_type_and_conversion_operators_disabled_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,308 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValue), + typeof(string) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString => this._valueIndex == 2; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue => IsNullValue ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue), + 2 => this._string!, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 2; + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) + { + switch (this._valueIndex) + { + case 1: + @nullValue(default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @string(this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @string(@state, this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(default(global::Thinktecture.Tests.NullValue)); + case 2: + return @string(this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + case 2: + return @string(@state, this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValue, + TResult @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue; + case 2: + return @string; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.NullValue(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsNullValue; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// 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 => true, + 2 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 2 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue).ToString(), + 2 => this._string, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_stateless_type_and_explicit_conversion_operators_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_stateless_type_and_explicit_conversion_operators_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..89e39ed6 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_stateless_type_and_explicit_conversion_operators_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,328 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValue), + typeof(string) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString => this._valueIndex == 2; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue => IsNullValue ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue), + 2 => this._string!, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 2; + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) + { + switch (this._valueIndex) + { + case 1: + @nullValue(default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @string(this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @string(@state, this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(default(global::Thinktecture.Tests.NullValue)); + case 2: + return @string(this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + case 2: + return @string(@state, this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValue, + TResult @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue; + case 2: + return @string; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Explicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static explicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + return new global::Thinktecture.Tests.TestUnion(@nullValue); + } + + /// + /// Explicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static explicit operator global::Thinktecture.Tests.TestUnion(string @string) + { + return new global::Thinktecture.Tests.TestUnion(@string); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.NullValue(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsNullValue; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// 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 => true, + 2 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 2 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue).ToString(), + 2 => this._string, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_stateless_type_using_AdHocUnionAttribute_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_stateless_type_using_AdHocUnionAttribute_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt new file mode 100644 index 00000000..d6b31234 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.Should_generate_union_with_stateless_type_using_AdHocUnionAttribute_file=Thinktecture.Tests.TestUnion.AdHocUnion.g.cs.verified.txt @@ -0,0 +1,328 @@ +// +#nullable enable + +namespace Thinktecture.Tests +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("ThinktectureRuntimeExtensionsAnalyzer", "TTRESG1000:Internal Thinktecture.Runtime.Extensions API usage")] + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators, + global::Thinktecture.IDisallowDefaultValue, + global::Thinktecture.Internal.IMetadataOwner + { + static global::Thinktecture.Internal.Metadata global::Thinktecture.Internal.IMetadataOwner.Metadata { get; } + = new global::Thinktecture.Internal.Metadata.AdHocUnion(typeof(global::Thinktecture.Tests.TestUnion)) + { + MemberTypes = new global::System.Collections.Generic.List + { + typeof(global::Thinktecture.Tests.NullValue), + typeof(string) + } + .AsReadOnly() + }; + + private readonly int _valueIndex; + + private readonly string? _string; + + /// + /// Indication whether the current value is of type . + /// + public bool IsNullValue => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type . + /// + public bool IsString => this._valueIndex == 2; + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public global::Thinktecture.Tests.NullValue AsNullValue => IsNullValue ? default(global::Thinktecture.Tests.NullValue) : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'NullValue'."); + + /// + /// Gets the current value as . + /// + /// If the current value is not of type . + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue), + 2 => this._string!, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 2; + } + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) + { + switch (this._valueIndex) + { + case 1: + @nullValue(default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @string(this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes an action depending on the current value. + /// + /// State to be passed to the callbacks. + /// The action to execute if the current value is of type . + /// The action to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public void Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Action @string) +#if NET9_0_OR_GREATER + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + return; + case 2: + @string(@state, this._string!); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(default(global::Thinktecture.Tests.NullValue)); + case 2: + return @string(this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + +#pragma warning disable CS0436 // InstantHandleAttribute may come from a different assembly + /// + /// Executes a function depending on the current value. + /// + /// State to be passed to the callbacks. + /// The function to execute if the current value is of type . + /// The function to execute if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Switch( + TState @state, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @nullValue, + [global::JetBrains.Annotations.InstantHandleAttribute] global::System.Func @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct + where TState : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue(@state, default(global::Thinktecture.Tests.NullValue)); + case 2: + return @string(@state, this._string!); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } +#pragma warning restore CS0436 // InstantHandleAttribute may come from a different assembly + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type . + /// The instance to return if the current value is of type . + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public TResult Map( + TResult @nullValue, + TResult @string) +#if NET9_0_OR_GREATER + where TResult : allows ref struct +#endif + { + switch (this._valueIndex) + { + case 1: + return @nullValue; + case 2: + return @string; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::Thinktecture.Tests.NullValue @nullValue) + { + return new global::Thinktecture.Tests.TestUnion(@nullValue); + } + + /// + /// Implicit conversion from type . + /// + /// Value to convert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(string @string) + { + return new global::Thinktecture.Tests.TestUnion(@string); + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator global::Thinktecture.Tests.NullValue(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsNullValue; + } + + /// + /// Explicit conversion to type . + /// + /// Object to convert. + /// Inner value of type . + /// If the inner value is not a . + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// 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 => true, + 2 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => typeof(global::Thinktecture.Tests.NullValue).GetHashCode(), + 2 => this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => default(global::Thinktecture.Tests.NullValue).ToString(), + 2 => this._string, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.cs index 89378667..ee71c2ca 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/AdHocUnionSourceGeneratorTests.cs @@ -923,4 +923,286 @@ public partial class TestUnion; await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); } + + [Fact] + public async Task Should_generate_union_with_T1_as_stateless_type() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct NullValue { } + + [Union(T1IsStateless = true)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_T2_as_stateless_type() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct EmptyState { } + + [Union(T2IsStateless = true)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_multiple_stateless_types() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct NullValue { } + public readonly struct EmptyState { } + + [Union(T1IsStateless = true, T2IsStateless = true)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_stateless_type_using_AdHocUnionAttribute() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct NullValue { } + + [AdHocUnion(typeof(NullValue), typeof(string), T1IsStateless = true)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_struct_union_with_stateless_type() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct NullValue { } + + [Union(T1IsStateless = true)] + public partial struct TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_all_three_types_as_stateless() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct NullValue { } + public readonly struct EmptyState { } + public readonly struct UndefinedValue { } + + [Union(T1IsStateless = true, T2IsStateless = true, T3IsStateless = true)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_stateless_type_and_conversion_operators_disabled() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct NullValue { } + + [Union(T1IsStateless = true, ConversionFromValue = ConversionOperatorsGeneration.None)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_stateless_type_and_explicit_conversion_operators() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct NullValue { } + + [Union(T1IsStateless = true, ConversionFromValue = ConversionOperatorsGeneration.Explicit)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_duplicate_value_struct_stateless_types() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct NullValue { } + + [Union(T1IsStateless = true, T2IsStateless = true, T1Name = "NullValue1", T2Name = "NullValue2")] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_duplicate_stateless_types_T2_and_T3() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct EmptyState { } + + [Union(T2IsStateless = true, T3IsStateless = true, T2Name = "EmptyState1", T3Name = "EmptyState2")] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_duplicate_reference_type_stateless_types() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public class NullValueClass { } + + [Union(T1IsStateless = true, T2IsStateless = true, T1Name = "NullValueClass1", T2Name = "NullValueClass2")] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_struct_union_with_duplicate_stateless_types() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + public readonly struct NullValue { } + + [Union(T1IsStateless = true, T2IsStateless = true, T1Name = "NullValue1", T2Name = "NullValue2")] + public partial struct TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_duplicate_regular_string_types() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(T1Name = "Text", T4IsNullableReferenceType = true)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_duplicate_regular_types_requiring_factory_methods() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(T1Name = "Value1", T3Name = "Value2")] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } + + [Fact] + public async Task Should_generate_union_with_nullable_and_non_nullable_of_same_type() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + + await VerifyAsync(outputs, "Thinktecture.Tests.TestUnion.AdHocUnion.g.cs"); + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/EmptyStateClass.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/EmptyStateClass.cs new file mode 100644 index 00000000..d351b42e --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/EmptyStateClass.cs @@ -0,0 +1,3 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +public class EmptyStateClass; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/EmptyStateStruct.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/EmptyStateStruct.cs new file mode 100644 index 00000000..dd46903b --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/EmptyStateStruct.cs @@ -0,0 +1,3 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +public readonly struct EmptyStateStruct; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/NullValueClass.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/NullValueClass.cs new file mode 100644 index 00000000..384ad4bd --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/NullValueClass.cs @@ -0,0 +1,3 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +public class NullValueClass; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/NullValueStruct.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/NullValueStruct.cs new file mode 100644 index 00000000..44551cd1 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/NullValueStruct.cs @@ -0,0 +1,3 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +public readonly struct NullValueStruct; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string.cs new file mode 100644 index 00000000..1c9abd46 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string.cs @@ -0,0 +1,9 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + T2IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.cs new file mode 100644 index 00000000..237e858c --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.cs @@ -0,0 +1,9 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + T2IsStateless = true, + T1Name = "NullValueClass1", + T2Name = "NullValueClass2")] +public partial class TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvalueclass_string.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvalueclass_string.cs new file mode 100644 index 00000000..cb1049e7 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvalueclass_string.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_stateless_nullvalueclass_string; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct.cs new file mode 100644 index 00000000..bfb47308 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct.cs @@ -0,0 +1,9 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + T2IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string.cs new file mode 100644 index 00000000..6ea93aa6 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string.cs @@ -0,0 +1,9 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + T2IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.cs new file mode 100644 index 00000000..1b3c41dd --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.cs @@ -0,0 +1,9 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + T2IsStateless = true, + T1Name = "NullValue1", + T2Name = "NullValue2")] +public partial class TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_string.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_string.cs new file mode 100644 index 00000000..f26b16be --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_stateless_nullvaluestruct_string.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_stateless_nullvaluestruct_string; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystateclass.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystateclass.cs new file mode 100644 index 00000000..e50d3ccb --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystateclass.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T2IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_string_stateless_emptystateclass; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystatestruct.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystatestruct.cs new file mode 100644 index 00000000..5b7b7079 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystatestruct.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T2IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_string_stateless_emptystatestruct; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystatestruct_nullable.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystatestruct_nullable.cs new file mode 100644 index 00000000..e00729d4 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystatestruct_nullable.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T2IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_string_stateless_emptystatestruct_nullable; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.cs new file mode 100644 index 00000000..4d536cca --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.cs @@ -0,0 +1,9 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T2IsStateless = true, + T3IsStateless = true, + T2Name = "EmptyState1", + T3Name = "EmptyState2")] +public partial class TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_with_same_types_case_sensitive.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_with_same_types_case_sensitive.cs deleted file mode 100644 index d26a0983..00000000 --- a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_class_with_same_types_case_sensitive.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Thinktecture.Runtime.Tests.TestAdHocUnions; - -// ReSharper disable once InconsistentNaming -[Union(T4IsNullableReferenceType = true, - T1Name = "Text", - DefaultStringComparison = StringComparison.Ordinal)] -public partial class TestUnion_class_with_same_types_case_sensitive; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_struct_stateless_nullvalueclass_int.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_struct_stateless_nullvalueclass_int.cs new file mode 100644 index 00000000..b419d02d --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_struct_stateless_nullvalueclass_int.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial struct TestUnion_struct_stateless_nullvalueclass_int; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_struct_stateless_nullvaluestruct_int.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_struct_stateless_nullvaluestruct_int.cs new file mode 100644 index 00000000..e08eab6a --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_struct_stateless_nullvaluestruct_int.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial struct TestUnion_struct_stateless_nullvaluestruct_int; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.cs new file mode 100644 index 00000000..19c98692 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestAdHocUnions/TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.cs @@ -0,0 +1,9 @@ +namespace Thinktecture.Runtime.Tests.TestAdHocUnions; + +// ReSharper disable once InconsistentNaming +[Union( + T1IsStateless = true, + T2IsStateless = true, + T1Name = "NullValue1", + T2Name = "NullValue2")] +public partial struct TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int; diff --git a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/AsValue.cs b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/AsValue.cs index d4fdd2a5..99a48c63 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/AsValue.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/AsValue.cs @@ -148,4 +148,157 @@ public void Should_return_correct_value_or_throw_exception_having_5_types_with_d new TestUnion_class_with_same_types((int?)1).Invoking(u => u.AsString3.Should()).Should().Throw().WithMessage("'TestUnion_class_with_same_types' is not of type 'string?'."); new TestUnion_class_with_same_types((int?)1).AsNullableOfInt32.Should().Be(1); } + + [Fact] + public void Should_return_default_for_stateless_type_T1() + { + // Marker type should return default(NullValueStruct) + var union = new TestUnion_class_stateless_nullvaluestruct_string(new NullValueStruct()); + union.AsNullValueStruct.Should().Be(default(NullValueStruct)); + + // Accessing wrong type should throw + union.Invoking(u => u.AsString.Should()).Should().Throw() + .WithMessage("'TestUnion_class_stateless_nullvaluestruct_string' is not of type 'string'."); + } + + [Fact] + public void Should_return_value_for_non_stateless_type() + { + var union = new TestUnion_class_stateless_nullvaluestruct_string("text"); + union.AsString.Should().Be("text"); + + // Accessing marker type should throw + union.Invoking(u => u.AsNullValueStruct.Should()).Should().Throw() + .WithMessage("'TestUnion_class_stateless_nullvaluestruct_string' is not of type 'NullValueStruct'."); + } + + [Fact] + public void Should_return_default_for_stateless_type_T2() + { + // Regular type should return its value + var union1 = new TestUnion_class_string_stateless_emptystatestruct("text"); + union1.AsString.Should().Be("text"); + + // Marker type should return default(EmptyStateStruct) + var union2 = new TestUnion_class_string_stateless_emptystatestruct(new EmptyStateStruct()); + union2.AsEmptyStateStruct.Should().Be(default(EmptyStateStruct)); + } + + [Fact] + public void Should_return_default_for_multiple_stateless_types() + { + var union1 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new NullValueStruct()); + union1.AsNullValueStruct.Should().Be(default(NullValueStruct)); + + var union2 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new EmptyStateStruct()); + union2.AsEmptyStateStruct.Should().Be(default(EmptyStateStruct)); + + var union3 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string("text"); + union3.AsString.Should().Be("text"); + } + + [Fact] + public void Should_return_default_for_stateless_type_in_struct_union() + { + var union1 = new TestUnion_struct_stateless_nullvaluestruct_int(new NullValueStruct()); + union1.AsNullValueStruct.Should().Be(default(NullValueStruct)); + + var union2 = new TestUnion_struct_stateless_nullvaluestruct_int(42); + union2.AsInt32.Should().Be(42); + } + + [Fact] + public void Should_return_default_for_nullable_struct_marker() + { + // Nullable struct marker should return null (default for nullable structs) + var union1 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)null); + union1.AsNullableOfEmptyStateStruct.Should().BeNull(); + + var union2 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)new EmptyStateStruct()); + union2.AsNullableOfEmptyStateStruct.Should().BeNull(); + } + + [Fact] + public void Should_return_null_for_reference_type_marker_T1() + { + // Reference type marker should return null (default for reference types) + var union = new TestUnion_class_stateless_nullvalueclass_string(new NullValueClass()); + union.AsNullValueClass.Should().BeNull(); + } + + [Fact] + public void Should_return_null_for_reference_type_marker_T2() + { + var union = new TestUnion_class_string_stateless_emptystateclass(new EmptyStateClass()); + union.AsEmptyStateClass.Should().BeNull(); + } + + [Fact] + public void Should_return_null_for_multiple_reference_type_stateless_1() + { + var union1 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string(new NullValueClass()); + union1.AsNullValueClass.Should().BeNull(); + + var union2 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string(new EmptyStateClass()); + union2.AsEmptyStateClass.Should().BeNull(); + + var union3 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string("text"); + union3.AsString.Should().Be("text"); + } + + [Fact] + public void Should_return_default_for_duplicate_value_struct_stateless() + { + // Both stateless should return default(NullValueStruct) + var union1 = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + union1.AsNullValue1.Should().Be(default(NullValueStruct)); + + var union2 = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + union2.AsNullValue2.Should().Be(default(NullValueStruct)); + + // Regular type should return its value + var union3 = new TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string("text"); + union3.AsString.Should().Be("text"); + } + + [Fact] + public void Should_return_default_for_duplicate_value_struct_markers_T2_and_T3() + { + var union1 = new TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct("text"); + union1.AsString.Should().Be("text"); + + var union2 = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState1(default); + union2.AsEmptyState1.Should().Be(default(EmptyStateStruct)); + + var union3 = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState2(default); + union3.AsEmptyState2.Should().Be(default(EmptyStateStruct)); + } + + [Fact] + public void Should_return_null_for_multiple_reference_type_stateless() + { + // Both markers should return null + var union1 = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + union1.AsNullValueClass1.Should().BeNull(); + + var union2 = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + union2.AsNullValueClass2.Should().BeNull(); + + // Regular type should return its value + var union3 = new TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int(42); + union3.AsInt32.Should().Be(42); + } + + [Fact] + public void Should_return_default_for_duplicate_markers_in_struct_union() + { + var union1 = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + union1.AsNullValue1.Should().Be(default(NullValueStruct)); + + var union2 = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + union2.AsNullValue2.Should().Be(default(NullValueStruct)); + + var union3 = new TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int(42); + union3.AsInt32.Should().Be(42); + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/EqualityOperator.cs b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/EqualityOperator.cs index 492259c0..b66bfb68 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/EqualityOperator.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/EqualityOperator.cs @@ -49,7 +49,7 @@ public void Should_compare_unions_with_5_types_with_duplicates() { Compare(TestUnion_class_with_same_types.CreateText, n => new TestUnion_class_with_same_types(n), - TestUnion_class_with_same_types_case_sensitive.CreateText); + TestAdHocUnions.NonGeneric.TestUnion_class_with_same_types_case_sensitive.CreateText); } private static void Compare( @@ -69,4 +69,80 @@ private static void Compare( (obj == stringFactory("other text")).Should().BeFalse(); (stringFactory("42") == intFactory(42)).Should().BeFalse(); } + + [Fact] + public void Should_use_equality_operator_with_duplicate_value_struct_stateless() + { + var union1a = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + var union1b = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + + // Same marker index should be equal + (union1a == union1b).Should().BeTrue(); + + var union2a = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + var union2b = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + + // Same marker index should be equal + (union2a == union2b).Should().BeTrue(); + + // Different marker indices should NOT be equal + (union1a == union2a).Should().BeFalse(); + (union1a != union2a).Should().BeTrue(); + } + + [Fact] + public void Should_use_equality_operator_with_duplicate_value_struct_stateless_T2_and_T3() + { + var union1a = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState1(default); + var union1b = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState1(default); + + (union1a == union1b).Should().BeTrue(); + + var union2a = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState2(default); + var union2b = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState2(default); + + (union2a == union2b).Should().BeTrue(); + + // Different marker indices should NOT be equal + (union1a == union2a).Should().BeFalse(); + (union1a != union2a).Should().BeTrue(); + } + + [Fact] + public void Should_use_equality_operator_with_duplicate_reference_type_stateless() + { + var union1a = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + var union1b = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + + // Same marker index should be equal + (union1a == union1b).Should().BeTrue(); + + var union2a = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + var union2b = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + + // Same marker index should be equal + (union2a == union2b).Should().BeTrue(); + + // Different marker indices should NOT be equal + (union1a == union2a).Should().BeFalse(); + (union1a != union2a).Should().BeTrue(); + } + + [Fact] + public void Should_use_equality_operator_with_duplicate_markers_in_struct_union() + { + var union1a = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + var union1b = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + + (union1a == union1b).Should().BeTrue(); + + var union2a = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + var union2b = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + + (union2a == union2b).Should().BeTrue(); + + // Different marker indices should NOT be equal + (union1a == union2a).Should().BeFalse(); + (union1a != union2a).Should().BeTrue(); + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Equals.cs b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Equals.cs index dc65c759..24d952c7 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Equals.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Equals.cs @@ -49,7 +49,7 @@ public void Should_compare_unions_with_5_types_with_duplicates() { Compare(TestUnion_class_with_same_types.CreateText, n => new TestUnion_class_with_same_types(n), - TestUnion_class_with_same_types_case_sensitive.CreateText); + TestAdHocUnions.NonGeneric.TestUnion_class_with_same_types_case_sensitive.CreateText); } private static void Compare( @@ -87,4 +87,183 @@ private static void Compare( stringFactory("42").Equals(caseSensitiveFactory("42")).Should().BeFalse(); stringFactory("42").Equals((object)caseSensitiveFactory("42")).Should().BeFalse(); } + + [Fact] + public void Should_consider_equal_when_both_are_same_stateless_type() + { + var union1 = new TestUnion_class_stateless_nullvaluestruct_string(new NullValueStruct()); + var union2 = new TestUnion_class_stateless_nullvaluestruct_string(new NullValueStruct()); + + union1.Equals(union2).Should().BeTrue(); + (union1 == union2).Should().BeTrue(); + (union1 != union2).Should().BeFalse(); + union1.GetHashCode().Should().Be(union2.GetHashCode()); + } + + [Fact] + public void Should_consider_not_equal_when_different_types() + { + var union1 = new TestUnion_class_stateless_nullvaluestruct_string(new NullValueStruct()); + var union2 = new TestUnion_class_stateless_nullvaluestruct_string("text"); + + union1.Equals(union2).Should().BeFalse(); + (union1 == union2).Should().BeFalse(); + (union1 != union2).Should().BeTrue(); + } + + [Fact] + public void Should_consider_equal_when_same_regular_type_value() + { + var union1 = new TestUnion_class_stateless_nullvaluestruct_string("text"); + var union2 = new TestUnion_class_stateless_nullvaluestruct_string("text"); + + union1.Equals(union2).Should().BeTrue(); + (union1 == union2).Should().BeTrue(); + (union1 != union2).Should().BeFalse(); + union1.GetHashCode().Should().Be(union2.GetHashCode()); + } + + [Fact] + public void Should_handle_equality_with_multiple_stateless_types() + { + var union1 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new NullValueStruct()); + var union2 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new NullValueStruct()); + union1.Equals(union2).Should().BeTrue(); + + var union3 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new EmptyStateStruct()); + var union4 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new EmptyStateStruct()); + union3.Equals(union4).Should().BeTrue(); + + union1.Equals(union3).Should().BeFalse(); + } + + [Fact] + public void Should_handle_equality_in_struct_union() + { + var union1 = new TestUnion_struct_stateless_nullvaluestruct_int(new NullValueStruct()); + var union2 = new TestUnion_struct_stateless_nullvaluestruct_int(new NullValueStruct()); + + union1.Equals(union2).Should().BeTrue(); + (union1 == union2).Should().BeTrue(); + (union1 != union2).Should().BeFalse(); + union1.GetHashCode().Should().Be(union2.GetHashCode()); + } + + [Fact] + public void Should_handle_equality_with_nullable_struct_marker() + { + var union1 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)null); + var union2 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)new EmptyStateStruct()); + + // Both are markers, so they should be equal + union1.Equals(union2).Should().BeTrue(); + (union1 == union2).Should().BeTrue(); + union1.GetHashCode().Should().Be(union2.GetHashCode()); + } + + [Fact] + public void Should_not_equal_when_marker_vs_regular_type_with_nullable_struct() + { + var union1 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)null); + var union2 = new TestUnion_class_string_stateless_emptystatestruct_nullable("text"); + + union1.Equals(union2).Should().BeFalse(); + (union1 == union2).Should().BeFalse(); + (union1 != union2).Should().BeTrue(); + } + + [Fact] + public void Should_handle_equality_with_reference_type_marker() + { + var union1 = new TestUnion_class_stateless_nullvalueclass_string(new NullValueClass()); + var union2 = new TestUnion_class_stateless_nullvalueclass_string(new NullValueClass()); + + // Both are markers, so they should be equal (both return null) + union1.Equals(union2).Should().BeTrue(); + (union1 == union2).Should().BeTrue(); + union1.GetHashCode().Should().Be(union2.GetHashCode()); + } + + [Fact] + public void Should_not_equal_when_reference_marker_vs_regular_type() + { + var union1 = new TestUnion_class_stateless_nullvalueclass_string(new NullValueClass()); + var union2 = new TestUnion_class_stateless_nullvalueclass_string("text"); + + union1.Equals(union2).Should().BeFalse(); + (union1 == union2).Should().BeFalse(); + (union1 != union2).Should().BeTrue(); + } + + [Fact] + public void Should_handle_equality_with_duplicate_value_struct_stateless() + { + var union1a = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + var union1b = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + + // Same marker index should be equal + union1a.Equals(union1b).Should().BeTrue(); + + var union2a = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + var union2b = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + + // Same marker index should be equal + union2a.Equals(union2b).Should().BeTrue(); + + // Different marker indices should NOT be equal (even though same underlying type) + union1a.Equals(union2a).Should().BeFalse(); + } + + [Fact] + public void Should_handle_equality_with_duplicate_value_struct_stateless_T2_and_T3() + { + var union1a = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState1(default); + var union1b = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState1(default); + + union1a.Equals(union1b).Should().BeTrue(); + + var union2a = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState2(default); + var union2b = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState2(default); + + union2a.Equals(union2b).Should().BeTrue(); + + // Different marker indices should NOT be equal + union1a.Equals(union2a).Should().BeFalse(); + } + + [Fact] + public void Should_handle_equality_with_duplicate_reference_type_stateless() + { + var union1a = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + var union1b = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + + // Same marker index should be equal (both return null) + union1a.Equals(union1b).Should().BeTrue(); + + var union2a = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + var union2b = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + + // Same marker index should be equal (both return null) + union2a.Equals(union2b).Should().BeTrue(); + + // Different marker indices should NOT be equal (even though both return null) + union1a.Equals(union2a).Should().BeFalse(); + } + + [Fact] + public void Should_handle_equality_with_duplicate_markers_in_struct_union() + { + var union1a = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + var union1b = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + + union1a.Equals(union1b).Should().BeTrue(); + + var union2a = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + var union2b = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + + union2a.Equals(union2b).Should().BeTrue(); + + // Different marker indices should NOT be equal + union1a.Equals(union2a).Should().BeFalse(); + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/GetHashCode.cs b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/GetHashCode.cs index 94bc0ecb..611c2a37 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/GetHashCode.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/GetHashCode.cs @@ -62,14 +62,14 @@ public void Should_return_hashcode_of_case_sensitive_union() ComputeHashCodeOrdinal(new TestUnion_class_string_int_bool_guid_char_case_sensitive("text"), "text", true); ComputeHashCodeOrdinal(new TestUnion_class_string_int_bool_guid_char_case_sensitive("text"), "TEXT", false); - ComputeHashCodeOrdinal(TestUnion_class_with_same_types_case_sensitive.CreateText("text"), "text", true); - ComputeHashCodeOrdinal(TestUnion_class_with_same_types_case_sensitive.CreateText("text"), "tExt", false); + ComputeHashCodeOrdinal(TestAdHocUnions.NonGeneric.TestUnion_class_with_same_types_case_sensitive.CreateText("text"), "text", true); + ComputeHashCodeOrdinal(TestAdHocUnions.NonGeneric.TestUnion_class_with_same_types_case_sensitive.CreateText("text"), "tExt", false); - ComputeHashCodeOrdinal(TestUnion_class_with_same_types_case_sensitive.CreateString2("text2"), "text2", true); - ComputeHashCodeOrdinal(TestUnion_class_with_same_types_case_sensitive.CreateString2("text2"), "TEXT2", false); + ComputeHashCodeOrdinal(TestAdHocUnions.NonGeneric.TestUnion_class_with_same_types_case_sensitive.CreateString2("text2"), "text2", true); + ComputeHashCodeOrdinal(TestAdHocUnions.NonGeneric.TestUnion_class_with_same_types_case_sensitive.CreateString2("text2"), "TEXT2", false); - ComputeHashCodeOrdinal(TestUnion_class_with_same_types_case_sensitive.CreateString3("text3"), "text3", true); - ComputeHashCodeOrdinal(TestUnion_class_with_same_types_case_sensitive.CreateString3("text3"), "Text3", false); + ComputeHashCodeOrdinal(TestAdHocUnions.NonGeneric.TestUnion_class_with_same_types_case_sensitive.CreateString3("text3"), "text3", true); + ComputeHashCodeOrdinal(TestAdHocUnions.NonGeneric.TestUnion_class_with_same_types_case_sensitive.CreateString3("text3"), "Text3", false); } private static void ComputeHashCodeOrdinal(T union, string value, bool equal) @@ -85,4 +85,46 @@ private static void ComputeHashCodeOrdinal(T union, string value, bool equal) union.GetHashCode().Should().NotBe(expected); } } + + [Fact] + public void Should_return_same_hashcode_for_duplicate_value_struct_stateless() + { + var union1a = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + var union1b = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + + union1a.GetHashCode().Should().Be(union1b.GetHashCode()); + + var union2a = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + var union2b = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + + union2a.GetHashCode().Should().Be(union2b.GetHashCode()); + } + + [Fact] + public void Should_return_same_hashcode_for_duplicate_reference_type_stateless() + { + var union1a = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + var union1b = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + + union1a.GetHashCode().Should().Be(union1b.GetHashCode()); + + var union2a = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + var union2b = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + + union2a.GetHashCode().Should().Be(union2b.GetHashCode()); + } + + [Fact] + public void Should_return_same_hashcode_for_duplicate_markers_in_struct_union() + { + var union1a = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + var union1b = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + + union1a.GetHashCode().Should().Be(union1b.GetHashCode()); + + var union2a = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + var union2b = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + + union2a.GetHashCode().Should().Be(union2b.GetHashCode()); + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/ImplicitCasts.cs b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/ImplicitCasts.cs index 2f6bc81a..af2d0228 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/ImplicitCasts.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/ImplicitCasts.cs @@ -81,4 +81,82 @@ public void Should_have_implicit_casts_from_value_having_5_values_with_duplicate TestUnion_class_with_same_types nullableIntUnion = (int?)42; nullableIntUnion.Value.Should().Be(42); } + + [Fact] + public void Should_support_implicit_conversion_from_stateless_type() + { + TestUnion_class_stateless_nullvaluestruct_string union = new NullValueStruct(); + union.IsNullValueStruct.Should().BeTrue(); + union.AsNullValueStruct.Should().Be(default(NullValueStruct)); + } + + [Fact] + public void Should_support_implicit_conversion_from_regular_type_with_marker() + { + TestUnion_class_stateless_nullvaluestruct_string union = "text"; + union.IsString.Should().BeTrue(); + union.AsString.Should().Be("text"); + } + + [Fact] + public void Should_support_conversion_with_stateless_type_T2() + { + TestUnion_class_string_stateless_emptystatestruct union1 = "text"; + union1.IsString.Should().BeTrue(); + + TestUnion_class_string_stateless_emptystatestruct union2 = new EmptyStateStruct(); + union2.IsEmptyStateStruct.Should().BeTrue(); + } + + [Fact] + public void Should_support_conversion_with_multiple_stateless_types() + { + TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string union1 = new NullValueStruct(); + union1.IsNullValueStruct.Should().BeTrue(); + + TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string union2 = new EmptyStateStruct(); + union2.IsEmptyStateStruct.Should().BeTrue(); + + TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string union3 = "text"; + union3.IsString.Should().BeTrue(); + } + + [Fact] + public void Should_support_conversion_in_struct_union() + { + TestUnion_struct_stateless_nullvaluestruct_int union1 = new NullValueStruct(); + union1.IsNullValueStruct.Should().BeTrue(); + + TestUnion_struct_stateless_nullvaluestruct_int union2 = 42; + union2.IsInt32.Should().BeTrue(); + } + + [Fact] + public void Should_support_conversion_from_nullable_struct_marker() + { + TestUnion_class_string_stateless_emptystatestruct_nullable union1 = (EmptyStateStruct?)null; + union1.IsNullableOfEmptyStateStruct.Should().BeTrue(); + + TestUnion_class_string_stateless_emptystatestruct_nullable union2 = (EmptyStateStruct?)new EmptyStateStruct(); + union2.IsNullableOfEmptyStateStruct.Should().BeTrue(); + + TestUnion_class_string_stateless_emptystatestruct_nullable union3 = "text"; + union3.IsString.Should().BeTrue(); + } + + [Fact] + public void Should_support_conversion_from_reference_type_marker() + { + TestUnion_class_stateless_nullvalueclass_string union = new NullValueClass(); + union.IsNullValueClass.Should().BeTrue(); + union.AsNullValueClass.Should().BeNull(); + } + + [Fact] + public void Should_support_conversion_from_regular_type_with_reference_marker() + { + TestUnion_class_stateless_nullvalueclass_string union = "text"; + union.IsString.Should().BeTrue(); + union.AsString.Should().Be("text"); + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/InequalityOperator.cs b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/InequalityOperator.cs index 3f3d512e..c6bbfa01 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/InequalityOperator.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/InequalityOperator.cs @@ -49,7 +49,7 @@ public void Should_compare_unions_with_5_types_with_duplicates() { Compare(TestUnion_class_with_same_types.CreateText, n => new TestUnion_class_with_same_types(n), - TestUnion_class_with_same_types_case_sensitive.CreateText); + TestAdHocUnions.NonGeneric.TestUnion_class_with_same_types_case_sensitive.CreateText); } private static void Compare( diff --git a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/IsValue.cs b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/IsValue.cs index 9ff189b0..555f5c07 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/IsValue.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/IsValue.cs @@ -148,4 +148,215 @@ public void Should_use_correct_index_having_5_types_with_duplicates() new TestUnion_class_with_same_types((int?)1).IsString3.Should().BeFalse(); new TestUnion_class_with_same_types((int?)1).IsNullableOfInt32.Should().BeTrue(); } + + [Fact] + public void Should_correctly_identify_stateless_type_T1() + { + // Marker type NullValue + var union1 = new TestUnion_class_stateless_nullvaluestruct_string(new NullValueStruct()); + union1.IsNullValueStruct.Should().BeTrue(); + union1.IsString.Should().BeFalse(); + + // Regular type string + var union2 = new TestUnion_class_stateless_nullvaluestruct_string("text"); + union2.IsNullValueStruct.Should().BeFalse(); + union2.IsString.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_stateless_type_T2() + { + // Regular type string + var union1 = new TestUnion_class_string_stateless_emptystatestruct("text"); + union1.IsString.Should().BeTrue(); + union1.IsEmptyStateStruct.Should().BeFalse(); + + // Marker type EmptyState + var union2 = new TestUnion_class_string_stateless_emptystatestruct(new EmptyStateStruct()); + union2.IsString.Should().BeFalse(); + union2.IsEmptyStateStruct.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_multiple_stateless_types() + { + // Marker type NullValue + var union1 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new NullValueStruct()); + union1.IsNullValueStruct.Should().BeTrue(); + union1.IsEmptyStateStruct.Should().BeFalse(); + union1.IsString.Should().BeFalse(); + + // Marker type EmptyState + var union2 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new EmptyStateStruct()); + union2.IsNullValueStruct.Should().BeFalse(); + union2.IsEmptyStateStruct.Should().BeTrue(); + union2.IsString.Should().BeFalse(); + + // Regular type string + var union3 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string("text"); + union3.IsNullValueStruct.Should().BeFalse(); + union3.IsEmptyStateStruct.Should().BeFalse(); + union3.IsString.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_stateless_type_in_struct_union() + { + // Marker type NullValue + var union1 = new TestUnion_struct_stateless_nullvaluestruct_int(new NullValueStruct()); + union1.IsNullValueStruct.Should().BeTrue(); + union1.IsInt32.Should().BeFalse(); + + // Regular type int + var union2 = new TestUnion_struct_stateless_nullvaluestruct_int(42); + union2.IsNullValueStruct.Should().BeFalse(); + union2.IsInt32.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_nullable_struct_marker() + { + var union1 = new TestUnion_class_string_stateless_emptystatestruct_nullable("text"); + union1.IsString.Should().BeTrue(); + union1.IsNullableOfEmptyStateStruct.Should().BeFalse(); + + var union2 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)null); + union2.IsString.Should().BeFalse(); + union2.IsNullableOfEmptyStateStruct.Should().BeTrue(); + + var union3 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)new EmptyStateStruct()); + union3.IsString.Should().BeFalse(); + union3.IsNullableOfEmptyStateStruct.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_reference_type_marker_T1() + { + var union1 = new TestUnion_class_stateless_nullvalueclass_string(new NullValueClass()); + union1.IsNullValueClass.Should().BeTrue(); + union1.IsString.Should().BeFalse(); + + var union2 = new TestUnion_class_stateless_nullvalueclass_string("text"); + union2.IsNullValueClass.Should().BeFalse(); + union2.IsString.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_reference_type_marker_T2() + { + var union1 = new TestUnion_class_string_stateless_emptystateclass("text"); + union1.IsString.Should().BeTrue(); + union1.IsEmptyStateClass.Should().BeFalse(); + + var union2 = new TestUnion_class_string_stateless_emptystateclass(new EmptyStateClass()); + union2.IsString.Should().BeFalse(); + union2.IsEmptyStateClass.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_multiple_reference_type_stateless() + { + var union1 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string(new NullValueClass()); + union1.IsNullValueClass.Should().BeTrue(); + union1.IsEmptyStateClass.Should().BeFalse(); + union1.IsString.Should().BeFalse(); + + var union2 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string(new EmptyStateClass()); + union2.IsNullValueClass.Should().BeFalse(); + union2.IsEmptyStateClass.Should().BeTrue(); + union2.IsString.Should().BeFalse(); + + var union3 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string("text"); + union3.IsNullValueClass.Should().BeFalse(); + union3.IsEmptyStateClass.Should().BeFalse(); + union3.IsString.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_duplicate_value_struct_stateless() + { + // First marker + var union1 = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + union1.IsNullValue1.Should().BeTrue(); + union1.IsNullValue2.Should().BeFalse(); + union1.IsString.Should().BeFalse(); + + // Second marker (same type as first) + var union2 = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + union2.IsNullValue1.Should().BeFalse(); + union2.IsNullValue2.Should().BeTrue(); + union2.IsString.Should().BeFalse(); + + // Regular type + var union3 = new TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string("text"); + union3.IsNullValue1.Should().BeFalse(); + union3.IsNullValue2.Should().BeFalse(); + union3.IsString.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_duplicate_value_struct_stateless_T2_and_T3() + { + // Regular type + var union1 = new TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct("text"); + union1.IsString.Should().BeTrue(); + union1.IsEmptyState1.Should().BeFalse(); + union1.IsEmptyState2.Should().BeFalse(); + + // First marker + var union2 = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState1(default); + union2.IsString.Should().BeFalse(); + union2.IsEmptyState1.Should().BeTrue(); + union2.IsEmptyState2.Should().BeFalse(); + + // Second marker (same type as first) + var union3 = TestUnion_class_string_stateless_emptystatestruct_stateless_emptystatestruct.CreateEmptyState2(default); + union3.IsString.Should().BeFalse(); + union3.IsEmptyState1.Should().BeFalse(); + union3.IsEmptyState2.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_duplicate_reference_type_stateless() + { + // First marker + var union1 = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + union1.IsNullValueClass1.Should().BeTrue(); + union1.IsNullValueClass2.Should().BeFalse(); + union1.IsInt32.Should().BeFalse(); + + // Second marker (same type as first) + var union2 = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + union2.IsNullValueClass1.Should().BeFalse(); + union2.IsNullValueClass2.Should().BeTrue(); + union2.IsInt32.Should().BeFalse(); + + // Regular type + var union3 = new TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int(42); + union3.IsNullValueClass1.Should().BeFalse(); + union3.IsNullValueClass2.Should().BeFalse(); + union3.IsInt32.Should().BeTrue(); + } + + [Fact] + public void Should_correctly_identify_duplicate_markers_in_struct_union() + { + // First marker + var union1 = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + union1.IsNullValue1.Should().BeTrue(); + union1.IsNullValue2.Should().BeFalse(); + union1.IsInt32.Should().BeFalse(); + + // Second marker + var union2 = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + union2.IsNullValue1.Should().BeFalse(); + union2.IsNullValue2.Should().BeTrue(); + union2.IsInt32.Should().BeFalse(); + + // Regular type + var union3 = new TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int(42); + union3.IsNullValue1.Should().BeFalse(); + union3.IsNullValue2.Should().BeFalse(); + union3.IsInt32.Should().BeTrue(); + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Map.cs b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Map.cs index a304fcff..061a9bb5 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Map.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Map.cs @@ -162,6 +162,176 @@ public void Should_use_correct_arg_having_5_values_with_duplicates(int index, ob calledActionOn.Should().Be(expected); } + + [Fact] + public void Should_map_correctly_with_stateless_type_T1() + { + var union1 = new TestUnion_class_stateless_nullvaluestruct_string(new NullValueStruct()); + var result1 = union1.Map( + nullValueStruct: "marker", + @string: "text"); + result1.Should().Be("marker"); + + var union2 = new TestUnion_class_stateless_nullvaluestruct_string("actual_text"); + var result2 = union2.Map( + nullValueStruct: "marker", + @string: "text"); + result2.Should().Be("text"); + } + + [Fact] + public void Should_map_correctly_with_stateless_type_T2() + { + var union1 = new TestUnion_class_string_stateless_emptystatestruct("actual_text"); + var result1 = union1.Map( + @string: "text", + emptyStateStruct: "marker"); + result1.Should().Be("text"); + + var union2 = new TestUnion_class_string_stateless_emptystatestruct(new EmptyStateStruct()); + var result2 = union2.Map( + @string: "text", + emptyStateStruct: "marker"); + result2.Should().Be("marker"); + } + + [Fact] + public void Should_map_correctly_with_multiple_stateless_types() + { + var union1 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new NullValueStruct()); + var result1 = union1.Map( + nullValueStruct: "null", + emptyStateStruct: "empty", + @string: "text"); + result1.Should().Be("null"); + + var union2 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new EmptyStateStruct()); + var result2 = union2.Map( + nullValueStruct: "null", + emptyStateStruct: "empty", + @string: "text"); + result2.Should().Be("empty"); + + var union3 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string("actual_text"); + var result3 = union3.Map( + nullValueStruct: "null", + emptyStateStruct: "empty", + @string: "text"); + result3.Should().Be("text"); + } + + [Fact] + public void Should_map_correctly_with_nullable_struct_marker() + { + var union1 = new TestUnion_class_string_stateless_emptystatestruct_nullable("actual"); + var result1 = union1.Map( + @string: "text", + nullableOfEmptyStateStruct: "marker"); + result1.Should().Be("text"); + + var union2 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)null); + var result2 = union2.Map( + @string: "text", + nullableOfEmptyStateStruct: "marker"); + result2.Should().Be("marker"); + + var union3 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)new EmptyStateStruct()); + var result3 = union3.Map( + @string: "text", + nullableOfEmptyStateStruct: "marker"); + result3.Should().Be("marker"); + } + + [Fact] + public void Should_map_correctly_with_reference_type_marker() + { + var union1 = new TestUnion_class_stateless_nullvalueclass_string(new NullValueClass()); + var result1 = union1.Map( + nullValueClass: "marker", + @string: "text"); + result1.Should().Be("marker"); + + var union2 = new TestUnion_class_stateless_nullvalueclass_string("actual"); + var result2 = union2.Map( + nullValueClass: "marker", + @string: "text"); + result2.Should().Be("text"); + } + + [Fact] + public void Should_map_correctly_with_multiple_reference_type_stateless() + { + var union1 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string(new NullValueClass()); + var result1 = union1.Map( + nullValueClass: "null", + emptyStateClass: "empty", + @string: "text"); + result1.Should().Be("null"); + + var union2 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string(new EmptyStateClass()); + var result2 = union2.Map( + nullValueClass: "null", + emptyStateClass: "empty", + @string: "text"); + result2.Should().Be("empty"); + + var union3 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string("text"); + var result3 = union3.Map( + nullValueClass: "null", + emptyStateClass: "empty", + @string: "text"); + result3.Should().Be("text"); + } + + [Fact] + public void Should_map_correctly_with_duplicate_value_struct_stateless() + { + var union1 = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + var result1 = union1.Map( + nullValue1: "marker1", + nullValue2: "marker2", + @string: "text"); + result1.Should().Be("marker1"); + + var union2 = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + var result2 = union2.Map( + nullValue1: "marker1", + nullValue2: "marker2", + @string: "text"); + result2.Should().Be("marker2"); + + var union3 = new TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string("actual"); + var result3 = union3.Map( + nullValue1: "marker1", + nullValue2: "marker2", + @string: "text"); + result3.Should().Be("text"); + } + + [Fact] + public void Should_map_correctly_with_duplicate_reference_type_stateless() + { + var union1 = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + var result1 = union1.Map( + nullValueClass1: "marker1", + nullValueClass2: "marker2", + int32: 100); + result1.Should().Be("marker1"); + + var union2 = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + var result2 = union2.Map( + nullValueClass1: "marker1", + nullValueClass2: "marker2", + int32: 100); + result2.Should().Be("marker2"); + + var union3 = new TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int(42); + var result3 = union3.Map( + nullValueClass1: "marker1", + nullValueClass2: "marker2", + int32: 100); + result3.Should().Be(100); + } } public class HavingStruct @@ -183,5 +353,46 @@ public void Should_use_correct_arg_having_2_values(int index, object expected) calledActionOn.Should().Be(expected); } + + [Fact] + public void Should_map_correctly_with_stateless_type_in_struct_union() + { + var union1 = new TestUnion_struct_stateless_nullvaluestruct_int(new NullValueStruct()); + var result1 = union1.Map( + nullValueStruct: 0, + int32: 100); + result1.Should().Be(0); + + var union2 = new TestUnion_struct_stateless_nullvaluestruct_int(42); + var result2 = union2.Map( + nullValueStruct: 0, + int32: 100); + result2.Should().Be(100); + } + + [Fact] + public void Should_map_correctly_with_duplicate_markers_in_struct_union() + { + var union1 = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + var result1 = union1.Map( + nullValue1: 0, + nullValue2: -1, + int32: 100); + result1.Should().Be(0); + + var union2 = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + var result2 = union2.Map( + nullValue1: 0, + nullValue2: -1, + int32: 100); + result2.Should().Be(-1); + + var union3 = new TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int(42); + var result3 = union3.Map( + nullValue1: 0, + nullValue2: -1, + int32: 100); + result3.Should().Be(100); + } } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Switch.cs b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Switch.cs index 0aa5000d..9dc4fdcb 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Switch.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/AdHocUnionTests/Switch.cs @@ -503,6 +503,176 @@ public void Should_call_correct_arg_having_5_types(int index, object expected) calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); } + + [Fact] + public void Should_call_correct_arg_with_stateless_type_T1() + { + var union1 = new TestUnion_class_stateless_nullvaluestruct_string(new NullValueStruct()); + var result1 = union1.Switch( + nullValueStruct: _ => "marker", + @string: v => v); + result1.Should().Be("marker"); + + var union2 = new TestUnion_class_stateless_nullvaluestruct_string("text"); + var result2 = union2.Switch( + nullValueStruct: _ => "marker", + @string: v => v); + result2.Should().Be("text"); + } + + [Fact] + public void Should_call_correct_arg_with_stateless_type_T2() + { + var union1 = new TestUnion_class_string_stateless_emptystatestruct("text"); + var result1 = union1.Switch( + @string: v => v, + emptyStateStruct: _ => "marker"); + result1.Should().Be("text"); + + var union2 = new TestUnion_class_string_stateless_emptystatestruct(new EmptyStateStruct()); + var result2 = union2.Switch( + @string: v => v, + emptyStateStruct: _ => "marker"); + result2.Should().Be("marker"); + } + + [Fact] + public void Should_call_correct_arg_with_multiple_stateless_types() + { + var union1 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new NullValueStruct()); + var result1 = union1.Switch( + nullValueStruct: _ => "null", + emptyStateStruct: _ => "empty", + @string: v => v); + result1.Should().Be("null"); + + var union2 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string(new EmptyStateStruct()); + var result2 = union2.Switch( + nullValueStruct: _ => "null", + emptyStateStruct: _ => "empty", + @string: v => v); + result2.Should().Be("empty"); + + var union3 = new TestUnion_class_stateless_nullvaluestruct_stateless_emptystatestruct_string("text"); + var result3 = union3.Switch( + nullValueStruct: _ => "null", + emptyStateStruct: _ => "empty", + @string: v => v); + result3.Should().Be("text"); + } + + [Fact] + public void Should_call_correct_arg_with_nullable_struct_stateless() + { + var union1 = new TestUnion_class_string_stateless_emptystatestruct_nullable("text"); + var result1 = union1.Switch( + @string: v => v, + nullableOfEmptyStateStruct: _ => "marker"); + result1.Should().Be("text"); + + var union2 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)null); + var result2 = union2.Switch( + @string: v => v, + nullableOfEmptyStateStruct: _ => "marker"); + result2.Should().Be("marker"); + + var union3 = new TestUnion_class_string_stateless_emptystatestruct_nullable((EmptyStateStruct?)new EmptyStateStruct()); + var result3 = union3.Switch( + @string: v => v, + nullableOfEmptyStateStruct: _ => "marker"); + result3.Should().Be("marker"); + } + + [Fact] + public void Should_call_correct_arg_with_reference_type_stateless() + { + var union1 = new TestUnion_class_stateless_nullvalueclass_string(new NullValueClass()); + var result1 = union1.Switch( + nullValueClass: _ => "marker", + @string: v => v); + result1.Should().Be("marker"); + + var union2 = new TestUnion_class_stateless_nullvalueclass_string("text"); + var result2 = union2.Switch( + nullValueClass: _ => "marker", + @string: v => v); + result2.Should().Be("text"); + } + + [Fact] + public void Should_call_correct_arg_with_multiple_reference_type_stateless() + { + var union1 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string(new NullValueClass()); + var result1 = union1.Switch( + nullValueClass: _ => "null", + emptyStateClass: _ => "empty", + @string: v => v); + result1.Should().Be("null"); + + var union2 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string(new EmptyStateClass()); + var result2 = union2.Switch( + nullValueClass: _ => "null", + emptyStateClass: _ => "empty", + @string: v => v); + result2.Should().Be("empty"); + + var union3 = new TestUnion_class_stateless_nullvalueclass_stateless_emptystateclass_string("text"); + var result3 = union3.Switch( + nullValueClass: _ => "null", + emptyStateClass: _ => "empty", + @string: v => v); + result3.Should().Be("text"); + } + + [Fact] + public void Should_switch_correctly_with_duplicate_value_struct_stateless() + { + var union1 = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue1(default); + var result1 = union1.Switch( + nullValue1: _ => "marker1", + nullValue2: _ => "marker2", + @string: _ => "text"); + result1.Should().Be("marker1"); + + var union2 = TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string.CreateNullValue2(default); + var result2 = union2.Switch( + nullValue1: _ => "marker1", + nullValue2: _ => "marker2", + @string: _ => "text"); + result2.Should().Be("marker2"); + + var union3 = new TestUnion_class_stateless_nullvaluestruct_stateless_nullvaluestruct_string("actual"); + var result3 = union3.Switch( + nullValue1: _ => "marker1", + nullValue2: _ => "marker2", + @string: _ => "text"); + result3.Should().Be("text"); + } + + [Fact] + public void Should_switch_correctly_with_duplicate_reference_type_stateless() + { + var union1 = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass1(null); + var result1 = union1.Switch( + nullValueClass1: _ => "marker1", + nullValueClass2: _ => "marker2", + int32: v => v.ToString()); + result1.Should().Be("marker1"); + + var union2 = TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int.CreateNullValueClass2(null); + var result2 = union2.Switch( + nullValueClass1: _ => "marker1", + nullValueClass2: _ => "marker2", + int32: v => v.ToString()); + result2.Should().Be("marker2"); + + var union3 = new TestUnion_class_stateless_nullvalueclass_stateless_nullvalueclass_int(42); + var result3 = union3.Switch( + nullValueClass1: _ => "marker1", + nullValueClass2: _ => "marker2", + int32: v => v.ToString()); + result3.Should().Be("42"); + } } public class WithFuncAndContext @@ -777,6 +947,47 @@ public void Should_call_correct_arg_having_2_types(int index, object expected) calledActionOn.Should().Be(expected); } + + [Fact] + public void Should_call_correct_arg_with_stateless_type_in_struct_union() + { + var union1 = new TestUnion_struct_stateless_nullvaluestruct_int(new NullValueStruct()); + var result1 = union1.Switch( + nullValueStruct: _ => 0, + int32: v => v); + result1.Should().Be(0); + + var union2 = new TestUnion_struct_stateless_nullvaluestruct_int(42); + var result2 = union2.Switch( + nullValueStruct: _ => 0, + int32: v => v); + result2.Should().Be(42); + } + + [Fact] + public void Should_switch_correctly_with_duplicate_markers_in_struct_union() + { + var union1 = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue1(default); + var result1 = union1.Switch( + nullValue1: _ => 0, + nullValue2: _ => -1, + int32: v => v); + result1.Should().Be(0); + + var union2 = TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int.CreateNullValue2(default); + var result2 = union2.Switch( + nullValue1: _ => 0, + nullValue2: _ => -1, + int32: v => v); + result2.Should().Be(-1); + + var union3 = new TestUnion_struct_stateless_nullvaluestruct_stateless_nullvaluestruct_int(42); + var result3 = union3.Switch( + nullValue1: _ => 0, + nullValue2: _ => -1, + int32: v => v); + result3.Should().Be(42); + } } public class WithFuncAndContext