diff --git a/Directory.Packages.props b/Directory.Packages.props
index d61e99d6..73fe33c6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,28 +1,28 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/DiscriminatedUnionsDemos.cs b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/DiscriminatedUnionsDemos.cs
new file mode 100644
index 00000000..dcbf97ff
--- /dev/null
+++ b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/DiscriminatedUnionsDemos.cs
@@ -0,0 +1,53 @@
+using Serilog;
+
+namespace Thinktecture.DiscriminatedUnions;
+
+public class DiscriminatedUnionsDemos
+{
+ public static void Demo(ILogger logger)
+ {
+ logger.Information("""
+
+
+ ==== Demo for Union ====
+
+ """);
+
+ TextOrNumber textOrNumberFromString = "text";
+ logger.Information("Implicitly casted from string: {TextOrNumberFromString}", textOrNumberFromString);
+
+ TextOrNumber textOrNumberFromInt = 42;
+ logger.Information("Implicitly casted from int: {TextOrNumberFromInt}", textOrNumberFromInt);
+
+ logger.Information("TextOrNumber from string: IsText = {IsText}", textOrNumberFromString.IsText);
+ logger.Information("TextOrNumber from string: IsNumber = {IsNumber}", textOrNumberFromString.IsNumber);
+ logger.Information("TextOrNumber from int: IsText = {IsText}", textOrNumberFromInt.IsText);
+ logger.Information("TextOrNumber from int: IsNumber = {IsNumber}", textOrNumberFromInt.IsNumber);
+
+ logger.Information("TextOrNumber from string: AsText = {AsText}", textOrNumberFromString.AsText);
+ logger.Information("TextOrNumber from int: AsNumber = {AsNumber}", textOrNumberFromInt.AsNumber);
+
+ logger.Information("TextOrNumber from string: Value = {Value}", textOrNumberFromString.Value);
+ logger.Information("TextOrNumber from int: Value = {Value}", textOrNumberFromInt.Value);
+
+ textOrNumberFromString.Switch(text: s => logger.Information("String Action: {Text}", s),
+ number: i => logger.Information("Int Action: {Number}", i));
+
+ textOrNumberFromString.Switch(logger,
+ text: static (l, s) => l.Information("String Action with context: {Text}", s),
+ number: static (l, i) => l.Information("Int Action with context: {Number}", i));
+
+ var switchResponse = textOrNumberFromInt.Switch(text: static s => $"String Func: {s}",
+ number: static i => $"Int Func: {i}");
+ logger.Information("{Response}", switchResponse);
+
+ var switchResponseWithContext = textOrNumberFromInt.Switch(123.45,
+ text: static (ctx, s) => $"String Func with context: {ctx} | {s}",
+ number: static (ctx, i) => $"Int Func with context: {ctx} | {i}");
+ logger.Information("{Response}", switchResponseWithContext);
+
+ var mapResponse = textOrNumberFromString.Map(text: "Mapped string",
+ number: "Mapped int");
+ logger.Information("{Response}", mapResponse);
+ }
+}
diff --git a/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumber.cs b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumber.cs
new file mode 100644
index 00000000..a7627f1d
--- /dev/null
+++ b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumber.cs
@@ -0,0 +1,8 @@
+namespace Thinktecture.DiscriminatedUnions;
+
+[Union(T1IsNullableReferenceType = true,
+ T1Name = "Text",
+ T2Name = "Number",
+ SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads,
+ MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)]
+public sealed partial class TextOrNumber;
diff --git a/samples/Thinktecture.Runtime.Extensions.Samples/Program.cs b/samples/Thinktecture.Runtime.Extensions.Samples/Program.cs
index 5b050e6d..9bd83e16 100644
--- a/samples/Thinktecture.Runtime.Extensions.Samples/Program.cs
+++ b/samples/Thinktecture.Runtime.Extensions.Samples/Program.cs
@@ -1,4 +1,5 @@
using Serilog;
+using Thinktecture.DiscriminatedUnions;
using Thinktecture.EmptyClass;
using Thinktecture.SmartEnums;
using Thinktecture.ValueObjects;
@@ -13,6 +14,7 @@ public static void Main()
SmartEnumDemos.Demo(logger);
ValueObjectDemos.Demo(logger);
+ DiscriminatedUnionsDemos.Demo(logger);
EmptyActionDemos.Demo();
EmptyCollectionsDemos.Demo();
}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ComparableCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ComparableCodeGenerator.cs
index 5155f93a..9dff877a 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ComparableCodeGenerator.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ComparableCodeGenerator.cs
@@ -33,7 +33,7 @@ public int CompareTo(object? obj)
return 1;
if(obj is not ").AppendTypeFullyQualified(state.Type).Append(@" item)
- throw new global::System.ArgumentException(""Argument must be of type \""").Append(state.Type.TypeMinimallyQualified).Append(@"\""."", nameof(obj));
+ throw new global::System.ArgumentException(""Argument must be of type \""").AppendTypeMinimallyQualified(state.Type).Append(@"\""."", nameof(obj));
return this.CompareTo(item);
}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Constants.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Constants.cs
index bd77d9c1..3b13e30d 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Constants.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Constants.cs
@@ -70,5 +70,15 @@ public static class SmartEnum
public const string KEYED_FULL_NAME = "Thinktecture.SmartEnumAttribute`1";
public const string KEYLESS_FULL_NAME = "Thinktecture.SmartEnumAttribute";
}
+
+ public static class Union
+ {
+ public const string NAMESPACE = "Thinktecture";
+ public const string NAME = "UnionAttribute";
+ public const string FULL_NAME_2_TYPES = "Thinktecture.UnionAttribute`2";
+ public const string FULL_NAME_3_TYPES = "Thinktecture.UnionAttribute`3";
+ public const string FULL_NAME_4_TYPES = "Thinktecture.UnionAttribute`4";
+ public const string FULL_NAME_5_TYPES = "Thinktecture.UnionAttribute`5";
+ }
}
}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs
index 0f7ab4e9..06ec648f 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs
@@ -41,7 +41,8 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
DiagnosticsDescriptors.PrimaryConstructorNotAllowed,
DiagnosticsDescriptors.CustomKeyMemberImplementationNotFound,
DiagnosticsDescriptors.CustomKeyMemberImplementationTypeMismatch,
- DiagnosticsDescriptors.IndexBasedSwitchAndMapMustUseNamedParameters);
+ DiagnosticsDescriptors.IndexBasedSwitchAndMapMustUseNamedParameters,
+ DiagnosticsDescriptors.TypeMustBeClass);
///
public override void Initialize(AnalysisContext context)
@@ -51,6 +52,7 @@ public override void Initialize(AnalysisContext context)
context.RegisterOperationAction(AnalyzeSmartEnum, OperationKind.Attribute);
context.RegisterOperationAction(AnalyzeValueObject, OperationKind.Attribute);
+ context.RegisterOperationAction(AnalyzeUnion, OperationKind.Attribute);
context.RegisterOperationAction(AnalyzeMethodCall, OperationKind.Invocation);
}
@@ -177,6 +179,56 @@ private static void AnalyzeValueObject(OperationAnalysisContext context)
}
}
+ private static void AnalyzeUnion(OperationAnalysisContext context)
+ {
+ if (context.ContainingSymbol.Kind != SymbolKind.NamedType
+ || context.Operation is not IAttributeOperation { Operation: IObjectCreationOperation attrCreation }
+ || context.ContainingSymbol is not INamedTypeSymbol type
+ || type.TypeKind == TypeKind.Error
+ || type.DeclaringSyntaxReferences.IsDefaultOrEmpty)
+ {
+ return;
+ }
+
+ try
+ {
+ if (!attrCreation.Type.IsUnionAttribute())
+ return;
+
+ var locationOfFirstDeclaration = type.Locations.IsDefaultOrEmpty ? Location.None : type.Locations[0]; // a representative for all
+
+ ValidateUnion(context, type, attrCreation, locationOfFirstDeclaration);
+ }
+ catch (Exception ex)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.ErrorDuringCodeAnalysis,
+ Location.None,
+ type.ToFullyQualifiedDisplayString(), ex.Message));
+ }
+ }
+
+ private static void ValidateUnion(OperationAnalysisContext context,
+ INamedTypeSymbol type,
+ IObjectCreationOperation attribute,
+ Location locationOfFirstDeclaration)
+ {
+ if (type.IsRecord || type.TypeKind is not TypeKind.Class)
+ {
+ ReportDiagnostic(context, DiagnosticsDescriptors.TypeMustBeClass, locationOfFirstDeclaration, type);
+ return;
+ }
+
+ if (type.ContainingType is not null) // is nested class
+ {
+ ReportDiagnostic(context, DiagnosticsDescriptors.TypeCannotBeNestedClass, locationOfFirstDeclaration, type);
+ return;
+ }
+
+ EnsureNoPrimaryConstructor(context, type);
+ TypeMustBePartial(context, type);
+ TypeMustNotBeGeneric(context, type, locationOfFirstDeclaration, "Union");
+ }
+
private static void ValidateKeyedValueObject(OperationAnalysisContext context,
IReadOnlyList? assignableMembers,
INamedTypeSymbol type,
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs
index f63a89ad..b51fa34e 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs
@@ -29,6 +29,7 @@ internal static class DiagnosticsDescriptors
public static readonly DiagnosticDescriptor CustomKeyMemberImplementationNotFound = new("TTRESG044", "Custom implementation of the key member not found", $"Provide a custom implementation of the key member. Implement a field or property '{{0}}'. Use '{Constants.Attributes.Properties.KEY_MEMBER_NAME}' to change the name.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor CustomKeyMemberImplementationTypeMismatch = new("TTRESG045", "Key member type mismatch", "The type of the key member '{0}' must be '{2}' instead of '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor IndexBasedSwitchAndMapMustUseNamedParameters = new("TTRESG046", "The arguments of \"Switch\" and \"Map\" must named", "Not all arguments of \"Switch/Map\" on the enumeration '{0}' are named", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
+ public static readonly DiagnosticDescriptor TypeMustBeClass = new("TTRESG047", "The type must be a class", "The type '{0}' must be a class", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ErrorDuringCodeAnalysis = new("TTRESG098", "Error during code analysis", "Error during code analysis of '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
public static readonly DiagnosticDescriptor ErrorDuringGeneration = new("TTRESG099", "Error during code generation", "Error during code generation for '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs
new file mode 100644
index 00000000..67c45cda
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs
@@ -0,0 +1,60 @@
+namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
+
+public sealed class AllUnionSettings : IEquatable
+{
+ public bool SkipToString { get; }
+ public SwitchMapMethodsGeneration SwitchMethods { get; }
+ public SwitchMapMethodsGeneration MapMethods { get; }
+ public IReadOnlyList MemberTypeSettings { get; }
+ public StringComparison DefaultStringComparison { get; }
+
+ public AllUnionSettings(AttributeData attribute, int numberOfMemberTypes)
+ {
+ SkipToString = attribute.FindSkipToString() ?? false;
+ SwitchMethods = attribute.FindSwitchMethods();
+ MapMethods = attribute.FindMapMethods();
+ DefaultStringComparison = attribute.FindDefaultStringComparison();
+
+ var memberTypeSettings = new MemberTypeSetting[numberOfMemberTypes];
+ MemberTypeSettings = memberTypeSettings;
+
+ for (var i = 0; i < numberOfMemberTypes; i++)
+ {
+ memberTypeSettings[i] = new MemberTypeSetting(attribute.FindTxIsNullableReferenceType(i + 1),
+ attribute.FindTxName(i + 1));
+ }
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is AllUnionSettings enumSettings && Equals(enumSettings);
+ }
+
+ public bool Equals(AllUnionSettings? other)
+ {
+ if (other is null)
+ return false;
+ if (ReferenceEquals(this, other))
+ return true;
+
+ return SkipToString == other.SkipToString
+ && SwitchMethods == other.SwitchMethods
+ && MapMethods == other.MapMethods
+ && DefaultStringComparison == other.DefaultStringComparison
+ && MemberTypeSettings.SequenceEqual(other.MemberTypeSettings);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = SkipToString.GetHashCode();
+ hashCode = (hashCode * 397) ^ SwitchMethods.GetHashCode();
+ hashCode = (hashCode * 397) ^ MapMethods.GetHashCode();
+ hashCode = (hashCode * 397) ^ (int)DefaultStringComparison;
+ hashCode = (hashCode * 397) ^ MemberTypeSettings.ComputeHashCode();
+
+ return hashCode;
+ }
+ }
+}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeSetting.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeSetting.cs
new file mode 100644
index 00000000..b62795ea
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeSetting.cs
@@ -0,0 +1,34 @@
+namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
+
+public readonly struct MemberTypeSetting : IEquatable, IHashCodeComputable
+{
+ public bool IsNullableReferenceType { get; }
+ public string? Name { get; }
+
+ public MemberTypeSetting(
+ bool isNullableReferenceType,
+ string? name)
+ {
+ IsNullableReferenceType = isNullableReferenceType;
+ Name = name;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is MemberTypeSetting other && Equals(other);
+ }
+
+ public bool Equals(MemberTypeSetting other)
+ {
+ return IsNullableReferenceType == other.IsNullableReferenceType
+ && Name == other.Name;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (IsNullableReferenceType.GetHashCode() * 397) ^ (Name?.GetHashCode() ?? 0);
+ }
+ }
+}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs
new file mode 100644
index 00000000..f550a2ad
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs
@@ -0,0 +1,63 @@
+namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
+
+public sealed class MemberTypeState : IEquatable, IMemberInformation, ITypeMinimallyQualified, IHashCodeComputable
+{
+ public string TypeFullyQualified { get; }
+ public string TypeMinimallyQualified { get; }
+ public string Name { get; }
+ public bool IsReferenceType { get; }
+ public NullableAnnotation NullableAnnotation { get; }
+ public bool IsNullableStruct { get; }
+ public SpecialType SpecialType { get; }
+
+ public ArgumentName ArgumentName { get; }
+ public MemberTypeSetting Setting { get; }
+
+ public MemberTypeState(
+ INamedTypeSymbol type,
+ ITypedMemberState typeState,
+ MemberTypeSetting setting)
+ {
+ Name = setting.Name ?? (typeState.IsNullableStruct ? $"Nullable{type.TypeArguments[0].Name}" : type.Name);
+ TypeFullyQualified = typeState.TypeFullyQualified;
+ TypeMinimallyQualified = typeState.TypeMinimallyQualified;
+ IsReferenceType = typeState.IsReferenceType;
+ NullableAnnotation = typeState.NullableAnnotation;
+ IsNullableStruct = typeState.IsNullableStruct;
+ SpecialType = typeState.SpecialType;
+
+ ArgumentName = Name.MakeArgumentName();
+ Setting = setting;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is MemberTypeState other && Equals(other);
+ }
+
+ public bool Equals(MemberTypeState? other)
+ {
+ if (other is null)
+ return false;
+ if (ReferenceEquals(this, other))
+ return true;
+
+ return TypeFullyQualified == other.TypeFullyQualified
+ && IsReferenceType == other.IsReferenceType
+ && SpecialType == other.SpecialType
+ && Setting.Equals(other.Setting);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = TypeFullyQualified.GetHashCode();
+ hashCode = (hashCode * 397) ^ IsReferenceType.GetHashCode();
+ hashCode = (hashCode * 397) ^ (int)SpecialType;
+ hashCode = (hashCode * 397) ^ Setting.GetHashCode();
+
+ return hashCode;
+ }
+ }
+}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs
new file mode 100644
index 00000000..0a7ecff5
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs
@@ -0,0 +1,772 @@
+using System.Text;
+
+namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
+
+public class UnionCodeGenerator : CodeGeneratorBase
+{
+ public override string CodeGeneratorName => "Union-CodeGenerator";
+ public override string? FileNameSuffix => null;
+
+ private readonly UnionSourceGenState _state;
+ private readonly StringBuilder _sb;
+
+ public UnionCodeGenerator(
+ UnionSourceGenState state,
+ StringBuilder sb)
+ {
+ _state = state;
+ _sb = sb;
+ }
+
+ public override void Generate(CancellationToken cancellationToken)
+ {
+ _sb.AppendLine(GENERATED_CODE_PREFIX);
+
+ var hasNamespace = _state.Namespace is not null;
+
+ if (hasNamespace)
+ {
+ _sb.Append(@"
+namespace ").Append(_state.Namespace).Append(@"
+{");
+ }
+
+ GenerateUnion(cancellationToken);
+
+ if (hasNamespace)
+ {
+ _sb.Append(@"
+}");
+ }
+
+ _sb.Append(@"
+");
+ }
+
+ private void GenerateUnion(CancellationToken cancellationToken)
+ {
+ _sb.GenerateStructLayoutAttributeIfRequired(_state.IsReferenceType, _state.Settings.HasStructLayoutAttribute);
+
+ _sb.Append(@"
+ ");
+
+ _sb.Append(_state.IsReferenceType ? "sealed " : "readonly ").Append("partial ").Append(_state.IsReferenceType ? "class" : "struct").Append(" ").Append(_state.Name).Append(" :")
+ .Append(@"
+ global::System.IEquatable<").AppendTypeFullyQualified(_state).Append(@">,
+ global::System.Numerics.IEqualityOperators<").AppendTypeFullyQualified(_state).Append(@", ").AppendTypeFullyQualified(_state).Append(@", bool>
+ {
+ private static readonly int _typeHashCode = typeof(").AppendTypeFullyQualified(_state).Append(@").GetHashCode();
+
+ private readonly int _valueIndex;
+"); // index is 1-based
+
+ GenerateMemberTypeFieldsAndProps();
+ GenerateRawValueGetter();
+ GenerateConstructors();
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (_state.Settings.SwitchMethods != SwitchMapMethodsGeneration.None)
+ {
+ GenerateSwitchForAction(false, false);
+
+ if (_state.Settings.SwitchMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads)
+ GenerateSwitchForAction(false, true);
+
+ GenerateSwitchForAction(true, false);
+
+ if (_state.Settings.SwitchMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads)
+ GenerateSwitchForAction(true, true);
+
+ GenerateSwitchForFunc(false, false);
+
+ if (_state.Settings.SwitchMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads)
+ GenerateSwitchForFunc(false, true);
+
+ GenerateSwitchForFunc(true, false);
+
+ if (_state.Settings.SwitchMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads)
+ GenerateSwitchForFunc(true, true);
+ }
+
+ if (_state.Settings.MapMethods != SwitchMapMethodsGeneration.None)
+ {
+ GenerateMap(false);
+
+ if (_state.Settings.MapMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads)
+ GenerateMap(true);
+ }
+
+ GenerateImplicitConversions();
+ GenerateExplicitConversions();
+ GenerateEqualityOperators();
+ GenerateEquals();
+ GenerateGetHashCode();
+
+ if (!_state.Settings.SkipToString)
+ GenerateToString();
+
+ _sb.Append(@"
+ }");
+ }
+
+ private void GenerateImplicitConversions()
+ {
+ foreach (var memberType in _state.MemberTypes)
+ {
+ _sb.Append(@"
+
+ ///
+ /// Implicit conversion from type ").AppendTypeMinimallyQualified(memberType).Append(@".
+ ///
+ /// Value to covert from.
+ /// A new instance of converted from .
+ public static implicit operator ").AppendTypeFullyQualified(_state).Append("(").AppendTypeFullyQualified(memberType).Append(@" value)
+ {
+ return new ").AppendTypeFullyQualified(_state).Append(@"(value);
+ }");
+ }
+ }
+
+ private void GenerateExplicitConversions()
+ {
+ foreach (var memberType in _state.MemberTypes)
+ {
+ _sb.Append(@"
+
+ ///
+ /// Implicit conversion to type ").AppendTypeMinimallyQualified(memberType).Append(@".
+ ///
+ /// Object to covert.
+ /// Inner value of type ").AppendTypeMinimallyQualified(memberType).Append(@".
+ /// If the inner value is not a ").AppendTypeMinimallyQualified(memberType).Append(@".
+ public static explicit operator ").AppendTypeFullyQualified(memberType).Append("(").AppendTypeFullyQualified(_state).Append(@" obj)
+ {
+ return obj.As").Append(memberType.Name).Append(@";
+ }");
+ }
+ }
+
+ private void GenerateEqualityOperators()
+ {
+ _sb.Append(@"
+
+ ///
+ /// Compares two instances of .
+ ///
+ /// Instance to compare.
+ /// Another instance to compare.
+ /// true if objects are equal; otherwise false.
+ public static bool operator ==(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(" obj, ").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" other)
+ {");
+
+ if (_state.IsReferenceType)
+ {
+ _sb.Append(@"
+ if (obj is null)
+ return other is null;
+");
+ }
+
+ _sb.Append(@"
+ 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 !=(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(" obj, ").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" other)
+ {
+ return !(obj == other);
+ }");
+ }
+
+ private void GenerateToString()
+ {
+ _sb.Append(@"
+
+ ///
+ public override string? ToString()
+ {
+ return this._valueIndex switch
+ {");
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ _sb.Append(@"
+ ").Append(i + 1).Append(" => this._").Append(memberType.ArgumentName.Raw);
+
+ if (memberType.SpecialType != SpecialType.System_String)
+ {
+ if (memberType.IsReferenceType)
+ _sb.Append("?");
+
+ _sb.Append(".ToString()");
+ }
+
+ _sb.Append(",");
+ }
+
+ _sb.Append(@"
+ _ => throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."")
+ };
+ }");
+ }
+
+ private void GenerateGetHashCode()
+ {
+ _sb.Append(@"
+
+ ///
+ public override int GetHashCode()
+ {
+ return this._valueIndex switch
+ {");
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ _sb.Append(@"
+ ").Append(i + 1).Append(" => ");
+
+ _sb.Append("global::System.HashCode.Combine(").AppendTypeFullyQualified(_state).Append("._typeHashCode, this._").Append(memberType.ArgumentName.Raw);
+
+ if (memberType.IsReferenceType)
+ _sb.Append("?");
+
+ _sb.Append(".GetHashCode(");
+
+ if (memberType.SpecialType == SpecialType.System_String)
+ _sb.Append("global::System.StringComparison.").Append(Enum.GetName(typeof(StringComparison), _state.Settings.DefaultStringComparison));
+
+ _sb.Append(")");
+
+ if (memberType.IsReferenceType)
+ _sb.Append(" ?? 0");
+
+ _sb.Append("),");
+ }
+
+ _sb.Append(@"
+ _ => throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."")
+ };
+ }");
+ }
+
+ private void GenerateEquals()
+ {
+ _sb.Append(@"
+
+ ///
+ public override bool Equals(object? other)
+ {
+ return other is ").AppendTypeFullyQualified(_state).Append(@" obj && Equals(obj);
+ }
+
+ ///
+ public bool Equals(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" other)
+ {");
+
+ if (_state.IsReferenceType)
+ {
+ _sb.Append(@"
+ if (other is null)
+ return false;
+
+ if (ReferenceEquals(this, other))
+ return true;
+");
+ }
+
+ _sb.Append(@"
+ if (this._valueIndex != other._valueIndex)
+ return false;
+
+ return this._valueIndex switch
+ {");
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ _sb.Append(@"
+ ").Append(i + 1).Append(" => ");
+
+ if (memberType.IsReferenceType)
+ _sb.Append("this._").Append(memberType.ArgumentName.Raw).Append(" is null ? other._").Append(memberType.ArgumentName.Raw).Append(" is null : ");
+
+ _sb.Append("this._").Append(memberType.ArgumentName.Raw).Append(".Equals(other._").Append(memberType.ArgumentName.Raw);
+
+ if (memberType.SpecialType == SpecialType.System_String)
+ _sb.Append(", global::System.StringComparison.").Append(Enum.GetName(typeof(StringComparison), _state.Settings.DefaultStringComparison));
+
+ _sb.Append("),");
+ }
+
+ _sb.Append(@"
+ _ => throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."")
+ };
+ }");
+ }
+
+ private void GenerateSwitchForAction(bool withContext, bool isPartially)
+ {
+ _sb.Append(@"
+
+ ///
+ /// Executes an action depending on the current value.
+ /// ");
+
+ if (withContext)
+ {
+ _sb.Append(@"
+ /// Context to be passed to the callbacks.");
+ }
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+ /// The action to execute if no value-specific action is provided.");
+ }
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ _sb.Append(@"
+ /// The action to execute if the current value is of type ").AppendTypeMinimallyQualified(memberType).Append(".");
+ }
+
+ var methodName = isPartially ? "SwitchPartially" : "Switch";
+
+ if (withContext)
+ {
+ _sb.Append(@"
+ public void ").Append(methodName).Append(@"(
+ TContext context,");
+ }
+ else
+ {
+ _sb.Append(@"
+ public void ").Append(methodName).Append("(");
+ }
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+ global::System.Action<");
+
+ if (withContext)
+ _sb.Append("TContext, ");
+
+ _sb.Append("object?>? @default = null,");
+ }
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ if (i != 0)
+ _sb.Append(",");
+
+ _sb.Append(@"
+ global::System.Action<");
+
+ if (withContext)
+ _sb.Append("TContext, ");
+
+ _sb.AppendTypeFullyQualified(memberType).Append(">");
+
+ if (isPartially)
+ _sb.Append('?');
+
+ _sb.Append(' ').Append(memberType.ArgumentName.Escaped);
+
+ if (isPartially)
+ _sb.Append(" = null");
+ }
+
+ _sb.Append(@")
+ {");
+
+ GenerateIndexBasedActionSwitchBody(withContext, isPartially);
+
+ _sb.Append(@"
+ }");
+ }
+
+ private void GenerateIndexBasedActionSwitchBody(bool withContext, bool isPartially)
+ {
+ _sb.Append(@"
+ switch (this._valueIndex)
+ {");
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ _sb.Append(@"
+ case ").Append(i + 1).Append(":");
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+ if (").Append(memberType.ArgumentName.Escaped).Append(@" is null)
+ break;
+");
+ }
+
+ _sb.Append(@"
+ ").Append(memberType.ArgumentName.Escaped).Append("(");
+
+ if (withContext)
+ _sb.Append("context, ");
+
+ _sb.Append("this._").Append(memberType.ArgumentName.Raw).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null).Append(@");
+ return;");
+ }
+
+ _sb.Append(@"
+ default:
+ throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."");
+ }");
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+
+ @default?.Invoke(");
+
+ if (withContext)
+ _sb.Append("context, ");
+
+ _sb.Append("this.Value);");
+ }
+ }
+
+ private void GenerateSwitchForFunc(bool withContext, bool isPartially)
+ {
+ _sb.Append(@"
+
+ ///
+ /// Executes a function depending on the current value.
+ /// ");
+
+ if (withContext)
+ {
+ _sb.Append(@"
+ /// Context to be passed to the callbacks.");
+ }
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+ /// The function to execute if no value-specific action is provided.");
+ }
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ _sb.Append(@"
+ /// The function to execute if the current value is of type ").AppendTypeMinimallyQualified(memberType).Append(".");
+ }
+
+ var methodName = isPartially ? "SwitchPartially" : "Switch";
+
+ if (withContext)
+ {
+ _sb.Append(@"
+ public TResult ").Append(methodName).Append(@"(
+ TContext context,");
+ }
+ else
+ {
+ _sb.Append(@"
+ public TResult ").Append(methodName).Append("(");
+ }
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+ global::System.Func<");
+
+ if (withContext)
+ _sb.Append("TContext, ");
+
+ _sb.Append("object?, TResult> @default,");
+ }
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ if (i != 0)
+ _sb.Append(",");
+
+ _sb.Append(@"
+ ");
+
+ _sb.Append("global::System.Func<");
+
+ if (withContext)
+ _sb.Append("TContext, ");
+
+ _sb.AppendTypeFullyQualified(memberType).Append(", TResult>");
+
+ if (isPartially)
+ _sb.Append('?');
+
+ _sb.Append(' ').Append(memberType.ArgumentName.Escaped);
+
+ if (isPartially)
+ _sb.Append(" = null");
+ }
+
+ _sb.Append(@")
+ {");
+
+ GenerateIndexBasedFuncSwitchBody(withContext, isPartially);
+
+ _sb.Append(@"
+ }");
+ }
+
+ private void GenerateIndexBasedFuncSwitchBody(bool withContext, bool isPartially)
+ {
+ _sb.Append(@"
+ switch (this._valueIndex)
+ {");
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ _sb.Append(@"
+ case ").Append(i + 1).Append(":");
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+ if (").Append(memberType.ArgumentName.Escaped).Append(@" is null)
+ break;
+");
+ }
+
+ _sb.Append(@"
+ return ").Append(memberType.ArgumentName.Escaped).Append("(");
+
+ if (withContext)
+ _sb.Append("context, ");
+
+ _sb.Append("this._").Append(memberType.ArgumentName.Raw).Append(memberType is { IsReferenceType: true, Setting.IsNullableReferenceType: false } ? "!" : null).Append(");");
+ }
+
+ _sb.Append(@"
+ default:
+ throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."");
+ }");
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+
+ return @default(");
+
+ if (withContext)
+ _sb.Append("context, ");
+
+ _sb.Append("this.Value);");
+ }
+ }
+
+ private void GenerateMap(bool isPartially)
+ {
+ _sb.Append(@"
+
+ ///
+ /// Maps current value to an instance of type .
+ /// ");
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+ /// The instance to return if no value is provided for the current value.");
+ }
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ _sb.Append(@"
+ /// The instance to return if the current value is of type ").AppendTypeMinimallyQualified(memberType).Append(".");
+ }
+
+ var methodName = isPartially ? "MapPartially" : "Map";
+
+ _sb.Append(@"
+ public TResult ").Append(methodName).Append("(");
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+ TResult @default,");
+ }
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ if (i != 0)
+ _sb.Append(",");
+
+ _sb.Append(@"
+ ");
+
+ if (isPartially)
+ _sb.Append("global::Thinktecture.Argument<");
+
+ _sb.Append("TResult");
+
+ if (isPartially)
+ _sb.Append(">");
+
+ _sb.Append(" ").Append(_state.MemberTypes[i].ArgumentName.Escaped);
+
+ if (isPartially)
+ _sb.Append(" = default");
+ }
+
+ _sb.Append(@")
+ {");
+
+ GenerateIndexBasedMapSwitchBody(isPartially);
+
+ _sb.Append(@"
+ }");
+ }
+
+ private void GenerateIndexBasedMapSwitchBody(bool isPartially)
+ {
+ _sb.Append(@"
+ switch (this._valueIndex)
+ {");
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ _sb.Append(@"
+ case ").Append(i + 1).Append(":");
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+ if (!").Append(_state.MemberTypes[i].ArgumentName.Escaped).Append(@".IsSet)
+ break;
+");
+ }
+
+ _sb.Append(@"
+ return ").Append(_state.MemberTypes[i].ArgumentName.Escaped);
+
+ if (isPartially)
+ _sb.Append(".Value");
+
+ _sb.Append(";");
+ }
+
+ _sb.Append(@"
+ default:
+ throw new global::System.ArgumentOutOfRangeException($""Unexpected value index '{this._valueIndex}'."");
+ }");
+
+ if (isPartially)
+ {
+ _sb.Append(@"
+
+ return @default;");
+ }
+ }
+
+ private void GenerateConstructors()
+ {
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+ _sb.Append(@"
+
+ ///
+ /// Initializes new instance with .
+ ///
+ /// Value to create a new instance for.
+ public ").Append(_state.Name).Append("(").AppendTypeFullyQualified(memberType).Append(" ").Append(memberType.ArgumentName.Escaped).Append(@")
+ {
+ this._").Append(memberType.ArgumentName.Raw).Append(" = ").Append(memberType.ArgumentName.Escaped).Append(@";
+ this._valueIndex = ").Append(i + 1).Append(@";
+ }");
+ }
+ }
+
+ private void GenerateMemberTypeFieldsAndProps()
+ {
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+ _sb.Append(@"
+ private readonly ").AppendTypeFullyQualifiedNullAnnotated(memberType).Append(" _").Append(memberType.ArgumentName.Raw).Append(";");
+ }
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+ _sb.Append(@"
+
+ ///
+ /// Indication whether the current value is of type ").AppendTypeMinimallyQualified(memberType).Append(@".
+ ///
+ public bool Is").Append(memberType.Name).Append(" => this._valueIndex == ").Append(i + 1).Append(";");
+ }
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+ _sb.Append(@"
+
+ ///
+ /// Gets the current value as ").AppendTypeMinimallyQualified(memberType).Append(@".
+ ///
+ /// If the current value is not of type ").AppendTypeMinimallyQualified(memberType).Append(@".
+ public ").AppendTypeFullyQualified(memberType).Append(" As").Append(memberType.Name).Append(" => Is").Append(memberType.Name)
+ .Append(" ? this._").Append(memberType.ArgumentName.Raw).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("'.\");");
+ }
+ }
+
+ private void GenerateRawValueGetter()
+ {
+ var hasNullableTypes = _state.MemberTypes.Any(t => t.IsNullableStruct || t.NullableAnnotation == NullableAnnotation.Annotated);
+
+ _sb.Append(@"
+
+ ///
+ /// Gets the current value as .
+ ///
+ public object").Append(hasNullableTypes ? "?" : null).Append(@" Value => this._valueIndex switch
+ {");
+
+ for (var i = 0; i < _state.MemberTypes.Length; i++)
+ {
+ var memberType = _state.MemberTypes[i];
+
+ _sb.Append(@"
+ ").Append(i + 1).Append(" => this._").Append(memberType.ArgumentName.Raw).Append(memberType.IsReferenceType && !hasNullableTypes ? "!" : null).Append(",");
+ }
+
+ _sb.Append(@"
+ _ => throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."")
+ };");
+ }
+}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGeneratorFactory.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGeneratorFactory.cs
new file mode 100644
index 00000000..4af6dda3
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGeneratorFactory.cs
@@ -0,0 +1,24 @@
+using System.Text;
+
+namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
+
+public class UnionCodeGeneratorFactory : ICodeGeneratorFactory
+{
+ public static readonly ICodeGeneratorFactory Instance = new UnionCodeGeneratorFactory();
+
+ public string CodeGeneratorName => "Union-CodeGenerator";
+
+ private UnionCodeGeneratorFactory()
+ {
+ }
+
+ public CodeGeneratorBase Create(UnionSourceGenState state, StringBuilder stringBuilder)
+ {
+ return new UnionCodeGenerator(state, stringBuilder);
+ }
+
+ public bool Equals(ICodeGeneratorFactory other)
+ {
+ return ReferenceEquals(this, other);
+ }
+}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs
new file mode 100644
index 00000000..0a18c0b9
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs
@@ -0,0 +1,47 @@
+namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
+
+public readonly struct UnionSettings : IEquatable
+{
+ private readonly AllUnionSettings _settings;
+ private readonly AttributeInfo _attributeInfo;
+
+ public bool SkipToString => _settings.SkipToString;
+ public SwitchMapMethodsGeneration SwitchMethods => _settings.SwitchMethods;
+ public SwitchMapMethodsGeneration MapMethods => _settings.MapMethods;
+ public StringComparison DefaultStringComparison => _settings.DefaultStringComparison;
+ public bool HasStructLayoutAttribute => _attributeInfo.HasStructLayoutAttribute;
+
+ public UnionSettings(AllUnionSettings settings, AttributeInfo attributeInfo)
+ {
+ _settings = settings;
+ _attributeInfo = attributeInfo;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is UnionSettings enumSettings && Equals(enumSettings);
+ }
+
+ public bool Equals(UnionSettings other)
+ {
+ return SkipToString == other.SkipToString
+ && SwitchMethods == other.SwitchMethods
+ && MapMethods == other.MapMethods
+ && DefaultStringComparison == other.DefaultStringComparison
+ && HasStructLayoutAttribute == other.HasStructLayoutAttribute;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = SkipToString.GetHashCode();
+ hashCode = (hashCode * 397) ^ SwitchMethods.GetHashCode();
+ hashCode = (hashCode * 397) ^ MapMethods.GetHashCode();
+ hashCode = (hashCode * 397) ^ (int)DefaultStringComparison;
+ hashCode = (hashCode * 397) ^ HasStructLayoutAttribute.GetHashCode();
+
+ return hashCode;
+ }
+ }
+}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenState.cs
new file mode 100644
index 00000000..f15285d5
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenState.cs
@@ -0,0 +1,63 @@
+namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
+
+public sealed class UnionSourceGenState : ITypeInformation, IEquatable
+{
+ public string? Namespace { get; }
+ public string Name { get; }
+ public string TypeFullyQualified { get; }
+ public string TypeMinimallyQualified { get; }
+ public bool IsReferenceType { get; }
+ public NullableAnnotation NullableAnnotation { get; set; }
+ public bool IsNullableStruct { get; set; }
+ public bool IsEqualWithReferenceEquality => false;
+
+ public ImmutableArray MemberTypes { get; }
+ public UnionSettings Settings { get; }
+
+ public UnionSourceGenState(
+ INamedTypeSymbol type,
+ ImmutableArray memberTypes,
+ UnionSettings settings)
+ {
+ MemberTypes = memberTypes;
+ Settings = settings;
+ Name = type.Name;
+ Namespace = type.ContainingNamespace?.IsGlobalNamespace == true ? null : type.ContainingNamespace?.ToString();
+ TypeFullyQualified = type.ToFullyQualifiedDisplayString();
+ TypeMinimallyQualified = type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
+ IsReferenceType = type.IsReferenceType;
+ NullableAnnotation = type.NullableAnnotation;
+ IsNullableStruct = type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is UnionSourceGenState other && Equals(other);
+ }
+
+ public bool Equals(UnionSourceGenState? other)
+ {
+ if (other is null)
+ return false;
+ if (ReferenceEquals(this, other))
+ return true;
+
+ return TypeFullyQualified == other.TypeFullyQualified
+ && IsReferenceType == other.IsReferenceType
+ && Settings.Equals(other.Settings)
+ && MemberTypes.SequenceEqual(other.MemberTypes);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = TypeFullyQualified.GetHashCode();
+ hashCode = (hashCode * 397) ^ IsReferenceType.GetHashCode();
+ hashCode = (hashCode * 397) ^ Settings.GetHashCode();
+ hashCode = (hashCode * 397) ^ MemberTypes.ComputeHashCode();
+
+ return hashCode;
+ }
+ }
+}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs
new file mode 100644
index 00000000..f5b89bae
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs
@@ -0,0 +1,248 @@
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
+
+[Generator]
+public class UnionSourceGenerator : ThinktectureSourceGeneratorBase, IIncrementalGenerator
+{
+ public UnionSourceGenerator()
+ : base(10_000)
+ {
+ }
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var options = GetGeneratorOptions(context);
+
+ SetupLogger(context, options);
+
+ InitializeUnionSourceGen(context, options);
+ }
+
+ private void InitializeUnionSourceGen(
+ IncrementalGeneratorInitializationContext context,
+ IncrementalValueProvider options)
+ {
+ InitializeUnionSourceGen(context, options, Constants.Attributes.Union.FULL_NAME_2_TYPES);
+ InitializeUnionSourceGen(context, options, Constants.Attributes.Union.FULL_NAME_3_TYPES);
+ InitializeUnionSourceGen(context, options, Constants.Attributes.Union.FULL_NAME_4_TYPES);
+ InitializeUnionSourceGen(context, options, Constants.Attributes.Union.FULL_NAME_5_TYPES);
+ }
+
+ private void InitializeUnionSourceGen(
+ IncrementalGeneratorInitializationContext context,
+ IncrementalValueProvider options,
+ string fullyQualifiedMetadataName)
+ {
+ var unionTypeOrError = context.SyntaxProvider
+ .ForAttributeWithMetadataName(fullyQualifiedMetadataName,
+ IsCandidate,
+ GetSourceGenContextOrNull)
+ .SelectMany(static (state, _) => state.HasValue
+ ? [state.Value]
+ : ImmutableArray.Empty);
+
+ var validStates = unionTypeOrError.SelectMany(static (state, _) => state.ValidState is not null
+ ? [state.ValidState]
+ : ImmutableArray.Empty);
+
+ InitializeUnionTypeGeneration(context, validStates, options);
+
+ InitializeErrorReporting(context, unionTypeOrError);
+ InitializeExceptionReporting(context, unionTypeOrError);
+ }
+
+ private bool IsCandidate(SyntaxNode syntaxNode, CancellationToken cancellationToken)
+ {
+ try
+ {
+ return syntaxNode switch
+ {
+ ClassDeclarationSyntax classDeclaration when IsUnionCandidate(classDeclaration) => true,
+ _ => false
+ };
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error during checking whether a syntax node is a discriminated union candidate", exception: ex);
+ return false;
+ }
+ }
+
+ private bool IsUnionCandidate(TypeDeclarationSyntax typeDeclaration)
+ {
+ var isCandidate = typeDeclaration.IsPartial()
+ && !typeDeclaration.IsGeneric();
+
+ if (isCandidate)
+ {
+ Logger.LogDebug("The type declaration is a discriminated union candidate", typeDeclaration);
+ }
+ else
+ {
+ Logger.LogTrace("The type declaration is not a discriminated union candidate", typeDeclaration);
+ }
+
+ return isCandidate;
+ }
+
+ private SourceGenContext? GetSourceGenContextOrNull(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
+ {
+ var tds = (TypeDeclarationSyntax)context.TargetNode;
+
+ try
+ {
+ var type = (INamedTypeSymbol)context.TargetSymbol;
+
+ if (type.TypeKind == TypeKind.Error)
+ {
+ Logger.LogDebug("Type from semantic model is erroneous", tds);
+ return null;
+ }
+
+ if (type.ContainingType is not null)
+ {
+ Logger.LogDebug("Nested types are not supported", tds);
+ return null;
+ }
+
+ if (context.Attributes.Length > 1)
+ {
+ Logger.LogDebug($"Type has more than 1 '{Constants.Attributes.Union.NAME}'", tds);
+ return null;
+ }
+
+ var attributetype = context.Attributes[0].AttributeClass;
+
+ if (attributetype is null)
+ {
+ Logger.LogDebug("The attribute type is null", tds);
+ return null;
+ }
+
+ if (attributetype.TypeKind == TypeKind.Error)
+ {
+ Logger.LogDebug("The attribute type is erroneous", tds);
+ return null;
+ }
+
+ if (attributetype.TypeArguments.Length < 2)
+ {
+ Logger.LogDebug($"Expected the attribute type to have at least 2 type arguments but found {attributetype.TypeArguments.Length.ToString()}", tds);
+ return null;
+ }
+
+ var errorMessage = AttributeInfo.TryCreate(type, out var attributeInfo);
+
+ if (errorMessage is not null)
+ {
+ Logger.LogDebug(errorMessage, tds);
+ return null;
+ }
+
+ var factory = TypedMemberStateFactoryProvider.GetFactoryOrNull(context.SemanticModel.Compilation, Logger);
+
+ if (factory is null)
+ return new SourceGenContext(new SourceGenError("Could not fetch type information for code generation of a discriminated union", tds));
+
+ var settings = new AllUnionSettings(context.Attributes[0], attributetype.TypeArguments.Length);
+ var memberTypeStates = ImmutableArray.Empty;
+
+ for (var i = 0; i < attributetype.TypeArguments.Length; i++)
+ {
+ var memberType = attributetype.TypeArguments[i];
+
+ if (memberType.TypeKind == TypeKind.Error)
+ {
+ Logger.LogDebug("Type of the member is erroneous", tds);
+ return null;
+ }
+
+ if (memberType is not INamedTypeSymbol namedMemberType)
+ {
+ Logger.LogDebug("Type of the member must be a named type", tds);
+ return null;
+ }
+
+ var memberTypeSettings = settings.MemberTypeSettings[i];
+ memberType = memberType.IsReferenceType && memberTypeSettings.IsNullableReferenceType ? memberType.WithNullableAnnotation(NullableAnnotation.Annotated) : memberType;
+ var typeState = factory.Create(memberType);
+
+ var memberTypeState = new MemberTypeState(namedMemberType, typeState, memberTypeSettings);
+ memberTypeStates = memberTypeStates.Add(memberTypeState);
+ }
+
+ var unionState = new UnionSourceGenState(type,
+ memberTypeStates,
+ new UnionSettings(settings, attributeInfo));
+
+ Logger.LogDebug("The type declaration is a valid union", null, unionState);
+
+ return new SourceGenContext(unionState);
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error during extraction of relevant information out of semantic model for generation of a discriminated union", tds, ex);
+
+ return new SourceGenContext(new SourceGenException(ex, tds));
+ }
+ }
+
+ private void InitializeUnionTypeGeneration(
+ IncrementalGeneratorInitializationContext context,
+ IncrementalValuesProvider validStates,
+ IncrementalValueProvider options)
+ {
+ var unionTypes = validStates
+ .Collect()
+ .Select(static (states, _) => states.IsDefaultOrEmpty
+ ? ImmutableArray.Empty
+ : states.Distinct(TypeOnlyComparer.Instance))
+ .WithComparer(new SetComparer())
+ .SelectMany((states, _) => states);
+
+ context.RegisterSourceOutput(unionTypes.Combine(options), (ctx, tuple) => GenerateCode(ctx, tuple.Left, tuple.Right, UnionCodeGeneratorFactory.Instance));
+ }
+
+ private void InitializeErrorReporting(
+ IncrementalGeneratorInitializationContext context,
+ IncrementalValuesProvider unionTypeOrException)
+ {
+ var exceptions = unionTypeOrException.SelectMany(static (state, _) => state.Error is not null
+ ? [state.Error.Value]
+ : ImmutableArray.Empty);
+ context.RegisterSourceOutput(exceptions, ReportError);
+ }
+
+ private void InitializeExceptionReporting(
+ IncrementalGeneratorInitializationContext context,
+ IncrementalValuesProvider unionTypeOrException)
+ {
+ var exceptions = unionTypeOrException.SelectMany(static (state, _) => state.Exception is not null
+ ? [state.Exception.Value]
+ : ImmutableArray.Empty);
+ context.RegisterSourceOutput(exceptions, ReportException);
+ }
+
+ private readonly record struct SourceGenContext(UnionSourceGenState? ValidState, SourceGenException? Exception, SourceGenError? Error)
+ {
+ public SourceGenContext(UnionSourceGenState validState)
+ : this(validState, null, null)
+ {
+ }
+
+ public SourceGenContext(SourceGenException exception)
+ : this(null, exception, null)
+ {
+ }
+
+ public SourceGenContext(SourceGenError errorMessage)
+ : this(null, null, errorMessage)
+ {
+ }
+ }
+}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EqualityComparisonOperatorsCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EqualityComparisonOperatorsCodeGenerator.cs
index ca920757..4c37e9a5 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EqualityComparisonOperatorsCodeGenerator.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EqualityComparisonOperatorsCodeGenerator.cs
@@ -71,7 +71,7 @@ private static void GenerateUsingEquals(StringBuilder sb, EqualityComparisonOper
{
sb.Append(@"
///
- /// Compares two instances of .
+ /// Compares two instances of .
///
/// Instance to compare.
/// Another instance to compare.
@@ -105,7 +105,7 @@ private static void GenerateUsingEquals(StringBuilder sb, EqualityComparisonOper
}
///
- /// Compares two instances of .
+ /// Compares two instances of .
///
/// Instance to compare.
/// Another instance to compare.
@@ -175,7 +175,7 @@ private static bool Equals(").AppendTypeFullyQualifiedNullAnnotated(state.Type).
}
///
- /// Compares an instance of with .
+ /// Compares an instance of with .
///
/// Instance to compare.
/// Value to compare with.
@@ -186,7 +186,7 @@ private static bool Equals(").AppendTypeFullyQualifiedNullAnnotated(state.Type).
}
///
- /// Compares an instance of with .
+ /// Compares an instance of with .
///
/// Value to compare.
/// Instance to compare with.
@@ -197,7 +197,7 @@ private static bool Equals(").AppendTypeFullyQualifiedNullAnnotated(state.Type).
}
///
- /// Compares an instance of with .
+ /// Compares an instance of with .
///
/// Instance to compare.
/// Value to compare with.
@@ -208,7 +208,7 @@ private static bool Equals(").AppendTypeFullyQualifiedNullAnnotated(state.Type).
}
///
- /// Compares an instance of with .
+ /// Compares an instance of with .
///
/// Value to compare.
/// Instance to compare with.
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeInformation.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeInformation.cs
index 781a5e00..09ba21b8 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeInformation.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeInformation.cs
@@ -1,7 +1,6 @@
namespace Thinktecture.CodeAnalysis;
-public interface ITypeInformation : INamespaceAndName, ITypeInformationWithNullability
+public interface ITypeInformation : INamespaceAndName, ITypeInformationWithNullability, ITypeMinimallyQualified
{
- string TypeMinimallyQualified { get; }
bool IsEqualWithReferenceEquality { get; }
}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeMinimallyQualified.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeMinimallyQualified.cs
new file mode 100644
index 00000000..bb683ca9
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeMinimallyQualified.cs
@@ -0,0 +1,6 @@
+namespace Thinktecture.CodeAnalysis;
+
+public interface ITypeMinimallyQualified
+{
+ string TypeMinimallyQualified { get; }
+}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/SmartEnumCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/SmartEnumCodeGenerator.cs
index 38de7da3..0a9073a9 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/SmartEnumCodeGenerator.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/SmartEnumCodeGenerator.cs
@@ -809,7 +809,7 @@ private void GenerateTryGet(IMemberState keyProperty)
/// Gets a valid enumeration item for provided if a valid item exists.
///
/// The identifier to return an enumeration item for.
- /// An instance of .
+ /// An instance of .
/// true if a valid item with provided exists; false otherwise.
public static bool TryGet([global::System.Diagnostics.CodeAnalysis.AllowNull] ").AppendTypeFullyQualified(keyProperty).Append(" ").Append(keyProperty.ArgumentName.Escaped).Append(", [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out ").AppendTypeFullyQualified(_state).Append(@" item)
{");
@@ -855,7 +855,7 @@ private void GenerateValidate(IMemberState keyProperty)
///
/// The identifier to return an enumeration item for.
/// An object that provides culture-specific formatting information.
- /// An instance of .
+ /// An instance of .
/// null if a valid item with provided exists; with an error message otherwise.
public static ").AppendTypeFullyQualified(_state.ValidationError).Append("? Validate([global::System.Diagnostics.CodeAnalysis.AllowNull] ").AppendTypeFullyQualified(keyProperty).Append(" ").Append(keyProperty.ArgumentName.Escaped).Append(", global::System.IFormatProvider? ").Append(providerArgumentName).Append(", [global::System.Diagnostics.CodeAnalysis.MaybeNull] out ").AppendTypeFullyQualified(_state).Append(@" item)
{
@@ -882,7 +882,7 @@ private void GenerateValidate(IMemberState keyProperty)
}
_sb.Append(@"
- return global::Thinktecture.Internal.ValidationErrorCreator.CreateValidationError<").AppendTypeFullyQualified(_state.ValidationError).Append(@">($""There is no item of type '").Append(_state.TypeMinimallyQualified).Append("' with the identifier '{").Append(keyProperty.ArgumentName.Escaped).Append(@"}'."");
+ return global::Thinktecture.Internal.ValidationErrorCreator.CreateValidationError<").AppendTypeFullyQualified(_state.ValidationError).Append(@">($""There is no item of type '").AppendTypeMinimallyQualified(_state).Append("' with the identifier '{").Append(keyProperty.ArgumentName.Escaped).Append(@"}'."");
}
}");
}
@@ -895,7 +895,7 @@ private void GenerateImplicitConversion(IMemberState keyProperty)
/// Implicit conversion to the type .
///
/// Item to covert.
- /// The of provided or default if is null.
+ /// The of provided or default if is null.
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(""item"")]
public static implicit operator ").AppendTypeFullyQualifiedNullAnnotated(keyProperty).Append("(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" item)
{");
@@ -923,7 +923,7 @@ private void GenerateExplicitConversion(IMemberState keyProperty)
/// Explicit conversion from the type .
///
/// Value to covert.
- /// An instance of if the is a known item or implements .
+ /// An instance of if the is a known item or implements .
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(""").Append(keyProperty.ArgumentName.Escaped).Append(@""")]
public static explicit operator ").AppendTypeFullyQualifiedNullAnnotated(_state).Append("(").AppendTypeFullyQualifiedNullAnnotated(keyProperty).Append(" ").Append(keyProperty.ArgumentName.Escaped).Append(@")
{
@@ -1022,7 +1022,7 @@ void AddItem(").AppendTypeFullyQualified(_state).Append(@" item, string itemName
{
_sb.Append(@"
if (item is null)
- throw new global::System.ArgumentNullException($""The item \""{itemName}\"" of type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" must not be null."");
+ throw new global::System.ArgumentNullException($""The item \""{itemName}\"" of type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" must not be null."");
");
}
@@ -1030,7 +1030,7 @@ void AddItem(").AppendTypeFullyQualified(_state).Append(@" item, string itemName
{
_sb.Append(@"
if (item.").Append(keyMember.Name).Append(@" is null)
- throw new global::System.ArgumentException($""The \""").Append(keyMember.Name).Append(@"\"" of the item \""{itemName}\"" of type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" must not be null."");
+ throw new global::System.ArgumentException($""The \""").Append(keyMember.Name).Append(@"\"" of the item \""{itemName}\"" of type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" must not be null."");
");
}
@@ -1038,13 +1038,13 @@ void AddItem(").AppendTypeFullyQualified(_state).Append(@" item, string itemName
{
_sb.Append(@"
if (!item.IsValid)
- throw new global::System.ArgumentException($""All \""public static readonly\"" fields of type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" must be valid but the item \""{itemName}\"" with the identifier \""{item.").Append(keyMember.Name).Append(@"}\"" is not."");
+ throw new global::System.ArgumentException($""All \""public static readonly\"" fields of type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" must be valid but the item \""{itemName}\"" with the identifier \""{item.").Append(keyMember.Name).Append(@"}\"" is not."");
");
}
_sb.Append(@"
if (lookup.ContainsKey(item.").Append(keyMember.Name).Append(@"))
- throw new global::System.ArgumentException($""The type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" has multiple items with the identifier \""{item.").Append(keyMember.Name).Append(@"}\""."");
+ throw new global::System.ArgumentException($""The type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" has multiple items with the identifier \""{item.").Append(keyMember.Name).Append(@"}\""."");
lookup.Add(item.").Append(keyMember.Name).Append(@", item);
}
@@ -1097,7 +1097,7 @@ private void GenerateGetItems()
void AddItem(").AppendTypeFullyQualified(_state).Append(@" item, string itemName)
{
if (item is null)
- throw new global::System.ArgumentNullException($""The item \""{itemName}\"" of type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" must not be null."");
+ throw new global::System.ArgumentNullException($""The item \""{itemName}\"" of type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" must not be null."");
list.Add(item);
}
@@ -1155,7 +1155,7 @@ private void GenerateGet(IMemberState keyProperty)
/// Gets an enumeration item for provided .
///
/// The identifier to return an enumeration item for.
- /// An instance of if is not null; otherwise null.");
+ /// An instance of if is not null; otherwise null.");
if (!_state.Settings.IsValidatable)
{
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeInformationComparer.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeInformationComparer.cs
index 49fb23cd..6da965cc 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeInformationComparer.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeInformationComparer.cs
@@ -12,7 +12,7 @@ public bool Equals(ITypeInformation? x, ITypeInformation? y)
if (y is null)
return false;
- return x.TypeMinimallyQualified == y.TypeMinimallyQualified
+ return x.TypeFullyQualified == y.TypeFullyQualified
&& x.IsReferenceType == y.IsReferenceType;
}
@@ -20,7 +20,7 @@ public int GetHashCode(ITypeInformation obj)
{
unchecked
{
- var hashCode = obj.TypeMinimallyQualified.GetHashCode();
+ var hashCode = obj.TypeFullyQualified.GetHashCode();
hashCode = (hashCode * 397) ^ obj.IsReferenceType.GetHashCode();
return hashCode;
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeOnlyComparer.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeOnlyComparer.cs
index 6bba5218..7c7e836b 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeOnlyComparer.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeOnlyComparer.cs
@@ -1,3 +1,4 @@
+using Thinktecture.CodeAnalysis.DiscriminatedUnions;
using Thinktecture.CodeAnalysis.SmartEnums;
using Thinktecture.CodeAnalysis.ValueObjects;
@@ -13,7 +14,8 @@ public class TypeOnlyComparer
IEqualityComparer,
IEqualityComparer,
IEqualityComparer,
- IEqualityComparer
+ IEqualityComparer,
+ IEqualityComparer
{
public static readonly TypeOnlyComparer Instance = new();
@@ -27,6 +29,7 @@ public class TypeOnlyComparer
public bool Equals(SmartEnumDerivedTypes x, SmartEnumDerivedTypes y) => x.TypeFullyQualified == y.TypeFullyQualified;
public bool Equals(KeyedValueObjectSourceGeneratorState x, KeyedValueObjectSourceGeneratorState y) => x.TypeFullyQualified == y.TypeFullyQualified;
public bool Equals(ComplexValueObjectSourceGeneratorState x, ComplexValueObjectSourceGeneratorState y) => x.TypeFullyQualified == y.TypeFullyQualified;
+ public bool Equals(UnionSourceGenState x, UnionSourceGenState y) => x.TypeFullyQualified == y.TypeFullyQualified;
public int GetHashCode(FormattableGeneratorState obj) => obj.Type.TypeFullyQualified.GetHashCode();
public int GetHashCode(ComparableGeneratorState obj) => obj.Type.TypeFullyQualified.GetHashCode();
@@ -38,6 +41,7 @@ public class TypeOnlyComparer
public int GetHashCode(SmartEnumDerivedTypes obj) => obj.TypeFullyQualified.GetHashCode();
public int GetHashCode(KeyedValueObjectSourceGeneratorState obj) => obj.TypeFullyQualified.GetHashCode();
public int GetHashCode(ComplexValueObjectSourceGeneratorState obj) => obj.TypeFullyQualified.GetHashCode();
+ public int GetHashCode(UnionSourceGenState obj) => obj.TypeFullyQualified.GetHashCode();
private TypeOnlyComparer()
{
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectCodeGenerator.cs
index 47985b86..dc4a3d69 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectCodeGenerator.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectCodeGenerator.cs
@@ -304,7 +304,7 @@ private void GenerateEqualityOperators()
_sb.Append(@"
///
- /// Compares to instances of .
+ /// Compares two instances of .
///
/// Instance to compare.
/// Another instance to compare.
@@ -325,7 +325,7 @@ private void GenerateEqualityOperators()
}
///
- /// Compares to instances of .
+ /// Compares two instances of .
///
/// Instance to compare.
/// Another instance to compare.
@@ -567,7 +567,7 @@ public override string ToString()
else
{
_sb.Append(@"
- return """).Append(_state.TypeMinimallyQualified).Append(@""";");
+ return """).AppendTypeMinimallyQualified(_state).Append(@""";");
}
_sb.Append(@"
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectJsonCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectJsonCodeGenerator.cs
index 21d710ab..496a4922 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectJsonCodeGenerator.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectJsonCodeGenerator.cs
@@ -80,7 +80,7 @@ public ValueObjectJsonConverter(global::System.Text.Json.JsonSerializerOptions o
return default;
if (reader.TokenType != global::System.Text.Json.JsonTokenType.StartObject)
- throw new global::System.Text.Json.JsonException($""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"". Expected token: \""{(global::System.Text.Json.JsonTokenType.StartObject)}\""."");
+ throw new global::System.Text.Json.JsonException($""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"". Expected token: \""{(global::System.Text.Json.JsonTokenType.StartObject)}\""."");
");
cancellationToken.ThrowIfCancellationRequested();
@@ -103,12 +103,12 @@ public ValueObjectJsonConverter(global::System.Text.Json.JsonSerializerOptions o
break;
if (reader.TokenType != global::System.Text.Json.JsonTokenType.PropertyName)
- throw new global::System.Text.Json.JsonException($""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"". Expected token: \""{(global::System.Text.Json.JsonTokenType.PropertyName)}\""."");
+ throw new global::System.Text.Json.JsonException($""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"". Expected token: \""{(global::System.Text.Json.JsonTokenType.PropertyName)}\""."");
var propName = reader.GetString();
if(!reader.Read())
- throw new global::System.Text.Json.JsonException($""Unexpected end of the JSON message when trying the read the value of \""{propName}\"" during deserialization of \""").Append(_type.TypeMinimallyQualified).Append(@"\""."");
+ throw new global::System.Text.Json.JsonException($""Unexpected end of the JSON message when trying the read the value of \""{propName}\"" during deserialization of \""").AppendTypeMinimallyQualified(_type).Append(@"\""."");
");
cancellationToken.ThrowIfCancellationRequested();
@@ -139,7 +139,7 @@ public ValueObjectJsonConverter(global::System.Text.Json.JsonSerializerOptions o
_sb.Append(@"
else
{
- throw new global::System.Text.Json.JsonException($""Unknown member \""{propName}\"" encountered when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\""."");
+ throw new global::System.Text.Json.JsonException($""Unknown member \""{propName}\"" encountered when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\""."");
}");
}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectMessagePackCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectMessagePackCodeGenerator.cs
index bdad21a0..f33d6468 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectMessagePackCodeGenerator.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectMessagePackCodeGenerator.cs
@@ -87,7 +87,7 @@ public sealed class ValueObjectMessagePackFormatter : global::MessagePack.Format
out var obj);
if (validationError is not null)
- throw new global::MessagePack.MessagePackSerializationException(validationError.ToString() ?? ""Unable to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\""."");
+ throw new global::MessagePack.MessagePackSerializationException(validationError.ToString() ?? ""Unable to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\""."");
return obj;
}
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectNewtonsoftJsonCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectNewtonsoftJsonCodeGenerator.cs
index 24cfed9a..a3f8423e 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectNewtonsoftJsonCodeGenerator.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectNewtonsoftJsonCodeGenerator.cs
@@ -68,7 +68,7 @@ public override bool CanConvert(global::System.Type objectType)
var (lineNumber, linePosition) = GetLineInfo(reader);
throw new global::Newtonsoft.Json.JsonReaderException(
- $""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"". Expected token: \""{(global::Newtonsoft.Json.JsonToken.StartObject)}\""."",
+ $""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"". Expected token: \""{(global::Newtonsoft.Json.JsonToken.StartObject)}\""."",
reader.Path,
lineNumber,
linePosition,
@@ -98,7 +98,7 @@ public override bool CanConvert(global::System.Type objectType)
var (lineNumber, linePosition) = GetLineInfo(reader);
throw new global::Newtonsoft.Json.JsonReaderException(
- $""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"". Expected token: \""{(global::Newtonsoft.Json.JsonToken.PropertyName)}\""."",
+ $""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"". Expected token: \""{(global::Newtonsoft.Json.JsonToken.PropertyName)}\""."",
reader.Path,
lineNumber,
linePosition,
@@ -112,7 +112,7 @@ public override bool CanConvert(global::System.Type objectType)
var (lineNumber, linePosition) = GetLineInfo(reader);
throw new global::Newtonsoft.Json.JsonReaderException(
- $""Unexpected end of the JSON message when trying the read the value of \""{propName}\"" during deserialization of \""").Append(_type.TypeMinimallyQualified).Append(@"\""."",
+ $""Unexpected end of the JSON message when trying the read the value of \""{propName}\"" during deserialization of \""").AppendTypeMinimallyQualified(_type).Append(@"\""."",
reader.Path,
lineNumber,
linePosition,
@@ -151,7 +151,7 @@ public override bool CanConvert(global::System.Type objectType)
var (lineNumber, linePosition) = GetLineInfo(reader);
throw new global::Newtonsoft.Json.JsonReaderException(
- $""Unknown member \""{propName}\"" encountered when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\""."",
+ $""Unknown member \""{propName}\"" encountered when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\""."",
reader.Path,
lineNumber,
linePosition,
@@ -182,7 +182,7 @@ public override bool CanConvert(global::System.Type objectType)
var (lineNumber, linePosition) = GetLineInfo(reader);
throw new global::Newtonsoft.Json.JsonSerializationException(
- validationError.ToString() ?? ""Unable to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\""."",
+ validationError.ToString() ?? ""Unable to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\""."",
reader.Path,
lineNumber,
linePosition,
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/KeyedValueObjectCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/KeyedValueObjectCodeGenerator.cs
index 2f66c8a9..380436bf 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/KeyedValueObjectCodeGenerator.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/KeyedValueObjectCodeGenerator.cs
@@ -264,7 +264,7 @@ private void GenerateExplicitConversion(bool emptyStringYieldsNull)
/// Explicit conversion from the type .
///
/// Value to covert.
- /// An instance of .");
+ /// An instance of .");
if (bothAreReferenceTypes && !emptyStringYieldsNull)
{
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs
index b3f7b836..5403065b 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs
@@ -149,6 +149,26 @@ public static SerializationFrameworks FindUseForSerialization(this AttributeData
return frameworks.Value;
}
+ public static StringComparison FindDefaultStringComparison(this AttributeData attributeData)
+ {
+ var frameworks = (StringComparison?)GetIntegerParameterValue(attributeData, "DefaultStringComparison");
+
+ if (frameworks is null || !frameworks.Value.IsValid())
+ return StringComparison.OrdinalIgnoreCase;
+
+ return frameworks.Value;
+ }
+
+ public static bool FindTxIsNullableReferenceType(this AttributeData attributeData, int index)
+ {
+ return GetBooleanParameterValue(attributeData, $"T{index}IsNullableReferenceType") ?? false;
+ }
+
+ public static string? FindTxName(this AttributeData attributeData, int index)
+ {
+ return GetStringParameterValue(attributeData, $"T{index}Name");
+ }
+
public static (ITypeSymbol ComparerType, ITypeSymbol ItemType)? GetComparerTypes(this AttributeData attributeData)
{
if (attributeData.AttributeClass is not { } attributeClass || attributeClass.TypeKind == TypeKind.Error)
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs
index a6281807..f9844703 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs
@@ -121,6 +121,13 @@ public static StringBuilder AppendCast(
return sb;
}
+ public static StringBuilder AppendTypeMinimallyQualified(
+ this StringBuilder sb,
+ ITypeMinimallyQualified type)
+ {
+ return sb.Append(type.TypeMinimallyQualified);
+ }
+
public static StringBuilder AppendTypeFullyQualified(
this StringBuilder sb,
ITypeFullyQualified type)
diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs
index 256f3f1a..dfd4c51f 100644
--- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs
+++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs
@@ -45,6 +45,14 @@ public static bool IsSmartEnumAttribute(this ITypeSymbol? attributeType)
return attributeType is { Name: Constants.Attributes.SmartEnum.NAME, ContainingNamespace: { Name: Constants.Attributes.SmartEnum.NAMESPACE, ContainingNamespace.IsGlobalNamespace: true } };
}
+ public static bool IsUnionAttribute(this ITypeSymbol? attributeType)
+ {
+ if (attributeType is null || attributeType.TypeKind == TypeKind.Error)
+ return false;
+
+ return attributeType is { Name: Constants.Attributes.Union.NAME, ContainingNamespace: { Name: Constants.Attributes.Union.NAMESPACE, ContainingNamespace.IsGlobalNamespace: true } };
+ }
+
public static bool IsValueObjectMemberEqualityComparerAttribute(this ITypeSymbol? attributeType)
{
if (attributeType is null || attributeType.TypeKind == TypeKind.Error)
diff --git a/src/Thinktecture.Runtime.Extensions/UnionAttribute.cs b/src/Thinktecture.Runtime.Extensions/UnionAttribute.cs
new file mode 100644
index 00000000..0c6c6585
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions/UnionAttribute.cs
@@ -0,0 +1,211 @@
+namespace Thinktecture;
+
+///
+/// Marks a type as a discriminated union.
+///
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+[AttributeUsage(AttributeTargets.Class)]
+public class UnionAttribute : UnionAttributeBase
+{
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T1Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T1IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T2Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T2IsNullableReferenceType { get; set; }
+}
+
+///
+/// Marks a type as a discriminated union.
+///
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
+public class UnionAttribute : UnionAttributeBase
+{
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T1Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T1IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T2Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T2IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T3Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T3IsNullableReferenceType { get; set; }
+}
+
+///
+/// Marks a type as a discriminated union.
+///
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
+public class UnionAttribute : UnionAttributeBase
+{
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T1Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T1IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T2Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T2IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T3Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T3IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T4Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T4IsNullableReferenceType { get; set; }
+}
+
+///
+/// Marks a type as a discriminated union.
+///
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+/// One of the types of the discriminated union.
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
+public class UnionAttribute : UnionAttributeBase
+{
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T1Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T1IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T2Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T2IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T3Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T3IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T4Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T4IsNullableReferenceType { get; set; }
+
+ ///
+ /// Changes the name of all members regarding .
+ /// By default, the type name is used.
+ ///
+ public string? T5Name { get; set; }
+
+ ///
+ /// Makes the type argument a nullable reference type.
+ /// This setting has no effect if is a struct.
+ ///
+ public bool T5IsNullableReferenceType { get; set; }
+}
diff --git a/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs b/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs
new file mode 100644
index 00000000..c959dbee
--- /dev/null
+++ b/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs
@@ -0,0 +1,28 @@
+namespace Thinktecture;
+
+///
+/// Base class for marking a type as a discriminated union.
+///
+public abstract class UnionAttributeBase : Attribute
+{
+ ///
+ /// Defines the .
+ /// Default is .
+ ///
+ public StringComparison DefaultStringComparison { get; set; } = StringComparison.OrdinalIgnoreCase;
+
+ ///
+ /// Indication whether the generator should skip the implementation of the method or not.
+ ///
+ public bool SkipToString { get; set; }
+
+ ///
+ /// Indication whether and how the generator should generate the methods Switch.
+ ///
+ public SwitchMapMethodsGeneration SwitchMethods { get; set; }
+
+ ///
+ /// Indication whether and how the generator should generate the methods Map.
+ ///
+ public SwitchMapMethodsGeneration MapMethods { get; set; }
+}
diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG006_TypeMustBePartial.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG006_TypeMustBePartial.cs
index 41b8666a..a6334543 100644
--- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG006_TypeMustBePartial.cs
+++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG006_TypeMustBePartial.cs
@@ -352,4 +352,54 @@ public partial struct {|#0:TestValueObject|}
await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(ComplexValueObjectAttribute).Assembly });
}
}
+
+ public class Union_must_be_partial
+ {
+ [Fact]
+ public async Task Should_trigger_on_non_partial_class()
+ {
+ var code = """
+ using System;
+ using Thinktecture;
+
+ namespace TestNamespace
+ {
+ [Union]
+ public class {|#0:TestUnion|};
+ }
+ """;
+
+ var expectedCode = """
+ using System;
+ using Thinktecture;
+
+ namespace TestNamespace
+ {
+ [Union]
+ public partial class {|#0:TestUnion|};
+ }
+ """;
+
+ var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestUnion");
+ await Verifier.VerifyCodeFixAsync(code, expectedCode, new[] { typeof(ComplexValueObjectAttribute).Assembly }, expected);
+ }
+
+ [Fact]
+ public async Task Should_not_trigger_on_partial_class()
+ {
+ var code = """
+
+ using System;
+ using Thinktecture;
+
+ namespace TestNamespace
+ {
+ [Union]
+ public partial class {|#0:TestUnion|};
+ }
+ """;
+
+ await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(ComplexValueObjectAttribute).Assembly });
+ }
+ }
}
diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG016_TypeCannotBeNestedClass.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG016_TypeCannotBeNestedClass.cs
index 8eabfe9f..ff6312dc 100644
--- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG016_TypeCannotBeNestedClass.cs
+++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG016_TypeCannotBeNestedClass.cs
@@ -89,4 +89,29 @@ public partial class {|#0:TestValueObject|}
await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(ComplexValueObjectAttribute).Assembly }, expected);
}
}
+
+ public class Union_cannot_be_nested_class
+ {
+ [Fact]
+ public async Task Should_trigger_if_enum_is_nested_class()
+ {
+ var code = """
+
+ using System;
+ using Thinktecture;
+
+ namespace TestNamespace
+ {
+ public class SomeClass
+ {
+ [Union]
+ public partial class {|#0:TestUnion|};
+ }
+ }
+ """;
+
+ var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestUnion");
+ await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected);
+ }
+ }
}
diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG017_KeyMemberShouldNotBeNullable.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG017_KeyMemberShouldNotBeNullable.cs
index 3252e687..698dcd8b 100644
--- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG017_KeyMemberShouldNotBeNullable.cs
+++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG017_KeyMemberShouldNotBeNullable.cs
@@ -1,5 +1,4 @@
using System.Threading.Tasks;
-using Microsoft.CodeAnalysis;
using Verifier = Thinktecture.Runtime.Tests.Verifiers.CodeFixVerifier;
namespace Thinktecture.Runtime.Tests.AnalyzerAndCodeFixTests;
diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG033_EnumsAndValueObjectsMustNotBeGeneric.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG033_EnumsAndValueObjectsMustNotBeGeneric.cs
index 4037496d..8dc99421 100644
--- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG033_EnumsAndValueObjectsMustNotBeGeneric.cs
+++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG033_EnumsAndValueObjectsMustNotBeGeneric.cs
@@ -146,4 +146,26 @@ public partial struct {|#0:TestValueObject|}
await CodeFixVerifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected);
}
}
+
+ public class Union_must_not_be_generic
+ {
+ [Fact]
+ public async Task Should_trigger_on_generic_class()
+ {
+ var code = """
+
+ using System;
+ using Thinktecture;
+
+ namespace TestNamespace
+ {
+ [Union]
+ public partial class {|#0:TestUnion|};
+ }
+ """;
+
+ var expected = CodeFixVerifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("Union", "TestUnion");
+ await CodeFixVerifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected);
+ }
+ }
}
diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG043_PrimaryConstructorNotAllowed.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG043_PrimaryConstructorNotAllowed.cs
index aa2705e8..8f5c1440 100644
--- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG043_PrimaryConstructorNotAllowed.cs
+++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG043_PrimaryConstructorNotAllowed.cs
@@ -144,4 +144,26 @@ public partial struct {|#0:ValueObject|}()
await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected);
}
}
+
+ public class Union_cannot_have_a_primary_constructor
+ {
+ [Fact]
+ public async Task Should_trigger_if_union_is_class_and_has_primary_constructor()
+ {
+ var code = """
+
+ using System;
+ using Thinktecture;
+
+ namespace TestNamespace
+ {
+ [Union]
+ public partial class {|#0:TestUnion|}();
+ }
+ """;
+
+ var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestUnion");
+ await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected);
+ }
+ }
}
diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG047_TypeMustBeClass.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG047_TypeMustBeClass.cs
new file mode 100644
index 00000000..eab28114
--- /dev/null
+++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG047_TypeMustBeClass.cs
@@ -0,0 +1,50 @@
+using System.Threading.Tasks;
+using Verifier = Thinktecture.Runtime.Tests.Verifiers.CodeFixVerifier;
+
+namespace Thinktecture.Runtime.Tests.AnalyzerAndCodeFixTests;
+
+// ReSharper disable InconsistentNaming
+public class TTRESG047_TypeMustBeClassOrStruct
+{
+ private const string _DIAGNOSTIC_ID = "TTRESG047";
+
+ public class Union_must_be_class
+ {
+ [Fact]
+ public async Task Should_trigger_on_record()
+ {
+ var code = """
+
+ using System;
+ using Thinktecture;
+
+ namespace TestNamespace
+ {
+ [Union]
+ public partial record {|#0:TestUnion|};
+ }
+ """;
+
+ var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestUnion");
+ await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(UnionAttribute<,>).Assembly }, expected);
+ }
+
+ [Fact]
+ public async Task Should_not_trigger_on_class()
+ {
+ var code = """
+
+ using System;
+ using Thinktecture;
+
+ namespace TestNamespace
+ {
+ [Union]
+ public partial class {|#0:TestUnion|};
+ }
+ """;
+
+ await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(UnionAttribute<,>).Assembly });
+ }
+ }
+}
diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs
new file mode 100644
index 00000000..d21b452b
--- /dev/null
+++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs
@@ -0,0 +1,3376 @@
+using System.Linq;
+using Thinktecture.CodeAnalysis.DiscriminatedUnions;
+using Xunit.Abstractions;
+
+namespace Thinktecture.Runtime.Tests.SourceGeneratorTests;
+
+public class UnionSourceGeneratorTests : SourceGeneratorTestsBase
+{
+ private const string _GENERATED_HEADER = """
+ //
+ #nullable enable
+
+
+ """;
+
+ public UnionSourceGeneratorTests(ITestOutputHelper output)
+ : base(output)
+ {
+ }
+
+ [Fact]
+ public void Should_generate_class_with_string_and_int()
+ {
+ var source = """
+ using System;
+
+ namespace Thinktecture.Tests
+ {
+ [Union]
+ public partial class TestUnion;
+ }
+ """;
+ var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly);
+ outputs.Should().HaveCount(1);
+
+ var mainOutput = outputs.Single(kvp => kvp.Key.Contains("Thinktecture.Tests.TestUnion.g.cs")).Value;
+
+ AssertOutput(mainOutput, _GENERATED_HEADER + """
+ namespace Thinktecture.Tests
+ {
+ sealed partial class TestUnion :
+ global::System.IEquatable,
+ global::System.Numerics.IEqualityOperators
+ {
+ private static readonly int _typeHashCode = typeof(global::Thinktecture.Tests.TestUnion).GetHashCode();
+
+ private readonly int _valueIndex;
+
+ private readonly string? _string;
+ private readonly int _int32;
+
+ ///
+ /// Indication whether the current value is of type string.
+ ///
+ public bool IsString => this._valueIndex == 1;
+
+ ///
+ /// Indication whether the current value is of type int.
+ ///
+ public bool IsInt32 => this._valueIndex == 2;
+
+ ///
+ /// Gets the current value as string.
+ ///
+ /// If the current value is not of type string.
+ public string AsString => IsString ? 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 AsInt32 => IsInt32 ? this._int32 : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'int'.");
+
+ ///
+ /// Gets the current value as .
+ ///
+ public object Value => this._valueIndex switch
+ {
+ 1 => this._string!,
+ 2 => this._int32,
+ _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.")
+ };
+
+ ///
+ /// Initializes new instance with .
+ ///
+ /// Value to create a new instance for.
+ public TestUnion(string @string)
+ {
+ this._string = @string;
+ this._valueIndex = 1;
+ }
+
+ ///
+ /// Initializes new instance with .
+ ///
+ /// Value to create a new instance for.
+ public TestUnion(int int32)
+ {
+ this._int32 = int32;
+ this._valueIndex = 2;
+ }
+
+ ///
+ /// Executes an action depending on the current value.
+ ///
+ /// The action to execute if the current value is of type string.
+ /// The action to execute if the current value is of type int.
+ public void Switch(
+ global::System.Action @string,
+ global::System.Action int32)
+ {
+ switch (this._valueIndex)
+ {
+ case 1:
+ @string(this._string!);
+ return;
+ case 2:
+ int32(this._int32);
+ return;
+ default:
+ throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.");
+ }
+ }
+
+ ///
+ /// Executes an action depending on the current value.
+ ///
+ /// Context to be passed to the callbacks.
+ /// The action to execute if the current value is of type string.
+ /// The action to execute if the current value is of type int.
+ public void Switch(
+ TContext context,
+ global::System.Action @string,
+ global::System.Action int32)
+ {
+ switch (this._valueIndex)
+ {
+ case 1:
+ @string(context, this._string!);
+ return;
+ case 2:
+ int32(context, this._int32);
+ return;
+ default:
+ throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.");
+ }
+ }
+
+ ///
+ /// Executes a function depending on the current value.
+ ///
+ /// The function to execute if the current value is of type string.
+ /// The function to execute if the current value is of type int.
+ public TResult Switch(
+ global::System.Func @string,
+ global::System.Func int32)
+ {
+ switch (this._valueIndex)
+ {
+ case 1:
+ return @string(this._string!);
+ case 2:
+ return int32(this._int32);
+ default:
+ throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.");
+ }
+ }
+
+ ///
+ /// Executes a function depending on the current value.
+ ///
+ /// Context to be passed to the callbacks.
+ /// The function to execute if the current value is of type string.
+ /// The function to execute if the current value is of type int.
+ public TResult Switch(
+ TContext context,
+ global::System.Func @string,
+ global::System.Func int32)
+ {
+ switch (this._valueIndex)
+ {
+ case 1:
+ return @string(context, this._string!);
+ case 2:
+ return int32(context, this._int32);
+ default:
+ throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.");
+ }
+ }
+
+ ///
+ /// Maps current value to an instance of type .
+ ///
+ /// The instance to return if the current value is of type string.
+ /// The instance to return if the current value is of type int.
+ public TResult Map(
+ TResult @string,
+ TResult int32)
+ {
+ switch (this._valueIndex)
+ {
+ case 1:
+ return @string;
+ case 2:
+ return int32;
+ default:
+ throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'.");
+ }
+ }
+
+ ///
+ /// Implicit conversion from type string.
+ ///
+ /// Value to covert from.
+ /// A new instance of converted from .
+ public static implicit operator global::Thinktecture.Tests.TestUnion(string value)
+ {
+ return new global::Thinktecture.Tests.TestUnion(value);
+ }
+
+ ///
+ /// Implicit conversion from type int.
+ ///
+ /// Value to covert from.
+ /// A new instance of converted from .
+ public static implicit operator global::Thinktecture.Tests.TestUnion(int value)
+ {
+ return new global::Thinktecture.Tests.TestUnion(value);
+ }
+
+ ///
+ /// Implicit conversion to type string.
+ ///
+ /// Object to covert.
+ /// Inner value of type string.
+ /// If the inner value is not a string.
+ public static explicit operator string(global::Thinktecture.Tests.TestUnion obj)
+ {
+ return obj.AsString;
+ }
+
+ ///
+ /// Implicit conversion to type int.
+ ///
+ /// Object to covert.
+ /// Inner value of type int.
+ /// If the inner value is not a int.
+ public static explicit operator int(global::Thinktecture.Tests.TestUnion obj)
+ {
+ return obj.AsInt32;
+ }
+
+ ///
+ /// Compares two instances of .
+ ///
+ /// Instance to compare.
+ /// Another instance to compare.
+ /// true if objects are equal; otherwise false.
+ public static bool operator ==(global::Thinktecture.Tests.TestUnion? obj, global::Thinktecture.Tests.TestUnion? other)
+ {
+ if (obj is null)
+ return other is null;
+
+ return obj.Equals(other);
+ }
+
+ ///
+ /// Compares two instances of .
+ ///
+ /// Instance to compare.
+ /// Another instance to compare.
+ /// false if objects are equal; otherwise true.
+ public static bool operator !=(global::Thinktecture.Tests.TestUnion? obj, global::Thinktecture.Tests.TestUnion? other)
+ {
+ return !(obj == other);
+ }
+
+ ///
+ public override bool Equals(object? other)
+ {
+ return other is global::Thinktecture.Tests.TestUnion obj && Equals(obj);
+ }
+
+ ///
+ public bool Equals(global::Thinktecture.Tests.TestUnion? other)
+ {
+ if (other is null)
+ return false;
+
+ if (ReferenceEquals(this, other))
+ return true;
+
+ if (this._valueIndex != other._valueIndex)
+ return false;
+
+ return this._valueIndex switch
+ {
+ 1 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase),
+ 2 => this._int32.Equals(other._int32),
+ _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.")
+ };
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return this._valueIndex switch
+ {
+ 1 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0),
+ 2 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._int32.GetHashCode()),
+ _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.")
+ };
+ }
+
+ ///
+ public override string? ToString()
+ {
+ return this._valueIndex switch
+ {
+ 1 => this._string,
+ 2 => this._int32.ToString(),
+ _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.")
+ };
+ }
+ }
+ }
+
+ """);
+ }
+
+ [Fact]
+ public void Should_generate_class_with_string_and_int_with_SwitchPartially()
+ {
+ var source = """
+ using System;
+
+ namespace Thinktecture.Tests
+ {
+ [Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)]
+ public partial class TestUnion;
+ }
+ """;
+ var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly);
+ outputs.Should().HaveCount(1);
+
+ var mainOutput = outputs.Single(kvp => kvp.Key.Contains("Thinktecture.Tests.TestUnion.g.cs")).Value;
+
+ AssertOutput(mainOutput, _GENERATED_HEADER + """
+ namespace Thinktecture.Tests
+ {
+ sealed partial class TestUnion :
+ global::System.IEquatable,
+ global::System.Numerics.IEqualityOperators
+ {
+ private static readonly int _typeHashCode = typeof(global::Thinktecture.Tests.TestUnion).GetHashCode();
+
+ private readonly int _valueIndex;
+
+ private readonly string? _string;
+ private readonly int _int32;
+
+ ///
+ /// Indication whether the current value is of type string.
+ ///
+ public bool IsString => this._valueIndex == 1;
+
+ ///
+ /// Indication whether the current value is of type int.
+ ///
+ public bool IsInt32 => this._valueIndex == 2;
+
+ ///
+ /// Gets the current value as string.
+ ///
+ /// If the current value is not of type string.
+ public string AsString => IsString ? 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 AsInt32 => IsInt32 ? this._int32 : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'int'.");
+
+ ///
+ /// Gets the current value as .
+ ///
+ public object Value => this._valueIndex switch
+ {
+ 1 => this._string!,
+ 2 => this._int32,
+ _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.")
+ };
+
+ ///
+ /// Initializes new instance with .
+ ///
+ /// Value to create a new instance for.
+ public TestUnion(string @string)
+ {
+ this._string = @string;
+ this._valueIndex = 1;
+ }
+
+ ///
+ /// Initializes new instance with .
+ ///
+ /// Value to create a new instance for.
+ public TestUnion(int int32)
+ {
+ this._int32 = int32;
+ this._valueIndex = 2;
+ }
+
+ ///
+ /// Executes an action depending on the current value.
+ ///
+ /// The action to execute if the current value is of type string.
+ /// The action to execute if the current value is of type int.
+ public void Switch(
+ global::System.Action @string,
+ global::System.Action int32)
+ {
+ switch (this._valueIndex)
+ {
+ case 1:
+ @string(this._string!);
+ return;
+ case 2:
+ int32(this._int32);
+ return;
+ default:
+ throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.");
+ }
+ }
+
+ ///
+ /// Executes an action depending on the current value.
+ ///
+ /// The action to execute if no value-specific action is provided.
+ /// The action to execute if the current value is of type string.
+ /// The action to execute if the current value is of type int.
+ public void SwitchPartially(
+ global::System.Action