Skip to content

Commit 8f95739

Browse files
committed
A union can be a struct or a ref struct.
1 parent 3a59b84 commit 8f95739

36 files changed

+3298
-1908
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,12 +474,21 @@ Features:
474474
* Renaming of properties
475475
* Definition of nullable reference types
476476

477-
Definition of a basic union with 2 types:
477+
Definition of a basic union with 2 types using a `class`, a `struct` or `ref struct`:
478478

479479
```csharp
480+
// class
480481
[Union<string, int>]
481482
public partial class TextOrNumber;
482483

484+
// struct
485+
[Union<string, int>]
486+
public partial struct TextOrNumber;
487+
488+
// ref struct
489+
[Union<string, int>]
490+
public ref partial struct TextOrNumber;
491+
483492
// Up to 5 types
484493
[Union<string, int, bool, Guid, char>]
485494
public partial class MyUnion;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace Thinktecture.DiscriminatedUnions;
2+
3+
[Union<string, int>]
4+
public ref partial struct TextOrNumberRefStruct;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace Thinktecture.DiscriminatedUnions;
2+
3+
[Union<string, int>]
4+
public partial struct TextOrNumberStruct;

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
3030
DiagnosticsDescriptors.TypeCannotBeNestedClass,
3131
DiagnosticsDescriptors.KeyMemberShouldNotBeNullable,
3232
DiagnosticsDescriptors.StaticPropertiesAreNotConsideredItems,
33-
DiagnosticsDescriptors.EnumsAndValueObjectsMustNotBeGeneric,
33+
DiagnosticsDescriptors.EnumsValueObjectsAndUnionsMustNotBeGeneric,
3434
DiagnosticsDescriptors.BaseClassFieldMustBeReadOnly,
3535
DiagnosticsDescriptors.BaseClassPropertyMustBeReadOnly,
3636
DiagnosticsDescriptors.EnumKeyShouldNotBeNullable,
@@ -42,7 +42,7 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
4242
DiagnosticsDescriptors.CustomKeyMemberImplementationNotFound,
4343
DiagnosticsDescriptors.CustomKeyMemberImplementationTypeMismatch,
4444
DiagnosticsDescriptors.IndexBasedSwitchAndMapMustUseNamedParameters,
45-
DiagnosticsDescriptors.TypeMustBeClass);
45+
DiagnosticsDescriptors.VariableMustBeInitializedWithNonDefaultValue);
4646

4747
/// <inheritdoc />
4848
public override void Initialize(AnalysisContext context)
@@ -55,6 +55,37 @@ public override void Initialize(AnalysisContext context)
5555
context.RegisterOperationAction(AnalyzeUnion, OperationKind.Attribute);
5656

5757
context.RegisterOperationAction(AnalyzeMethodCall, OperationKind.Invocation);
58+
context.RegisterOperationAction(AnalyzeDefaultValueAssignment, OperationKind.DefaultValue);
59+
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
60+
}
61+
62+
private void AnalyzeObjectCreation(OperationAnalysisContext context)
63+
{
64+
var operation = (IObjectCreationOperation)context.Operation;
65+
66+
if (operation.Type is null)
67+
return;
68+
69+
if (!operation.Type.IsReferenceType
70+
&& operation.Arguments.Length == 0
71+
&& operation.Type.IsUnionType(out _))
72+
{
73+
ReportDiagnostic(context, DiagnosticsDescriptors.VariableMustBeInitializedWithNonDefaultValue, operation.Syntax.GetLocation(), operation.Type);
74+
}
75+
}
76+
77+
private void AnalyzeDefaultValueAssignment(OperationAnalysisContext context)
78+
{
79+
var operation = (IDefaultValueOperation)context.Operation;
80+
81+
if (operation.Type is null)
82+
return;
83+
84+
if (!operation.Type.IsReferenceType
85+
&& operation.Type.IsUnionType(out _))
86+
{
87+
ReportDiagnostic(context, DiagnosticsDescriptors.VariableMustBeInitializedWithNonDefaultValue, operation.Syntax.GetLocation(), operation.Type);
88+
}
5889
}
5990

6091
private static void AnalyzeMethodCall(OperationAnalysisContext context)
@@ -84,7 +115,7 @@ private static void AnalyzeMethodCall(OperationAnalysisContext context)
84115
operation,
85116
isValidatable);
86117
}
87-
else if (operation.Instance.Type.IsUnionAttribute(out attribute)
118+
else if (operation.Instance.Type.IsUnionType(out attribute)
88119
&& attribute.AttributeClass is not null)
89120
{
90121
AnalyzeUnionSwitchMap(context,
@@ -244,9 +275,9 @@ private static void ValidateUnion(OperationAnalysisContext context,
244275
IObjectCreationOperation attribute,
245276
Location locationOfFirstDeclaration)
246277
{
247-
if (type.IsRecord || type.TypeKind is not TypeKind.Class)
278+
if (type.IsRecord || type.TypeKind is not (TypeKind.Class or TypeKind.Struct))
248279
{
249-
ReportDiagnostic(context, DiagnosticsDescriptors.TypeMustBeClass, locationOfFirstDeclaration, type);
280+
ReportDiagnostic(context, DiagnosticsDescriptors.TypeMustBeClassOrStruct, locationOfFirstDeclaration, type);
250281
return;
251282
}
252283

@@ -533,7 +564,7 @@ private static void ValidateKeyedSmartEnum(
533564
private static void TypeMustNotBeGeneric(OperationAnalysisContext context, INamedTypeSymbol type, Location locationOfFirstDeclaration, string typeKind)
534565
{
535566
if (!type.TypeParameters.IsDefaultOrEmpty)
536-
ReportDiagnostic(context, DiagnosticsDescriptors.EnumsAndValueObjectsMustNotBeGeneric, locationOfFirstDeclaration, typeKind, BuildTypeName(type));
567+
ReportDiagnostic(context, DiagnosticsDescriptors.EnumsValueObjectsAndUnionsMustNotBeGeneric, locationOfFirstDeclaration, typeKind, BuildTypeName(type));
537568
}
538569

539570
private static void Check_ItemLike_StaticProperties(

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal static class DiagnosticsDescriptors
1818
public static readonly DiagnosticDescriptor InnerEnumOnNonFirstLevelMustBePublic = new("TTRESG015", "Non-first-level inner enumerations must be public", "Derived inner enumeration '{0}' on non-first-level must be public", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
1919
public static readonly DiagnosticDescriptor TypeCannotBeNestedClass = new("TTRESG016", "The type cannot be a nested class", "The type '{0}' cannot be a nested class", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
2020
public static readonly DiagnosticDescriptor KeyMemberShouldNotBeNullable = new("TTRESG017", "The key member must not be nullable", "A key member must not be nullable", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
21-
public static readonly DiagnosticDescriptor EnumsAndValueObjectsMustNotBeGeneric = new("TTRESG033", "Enumerations and value objects must not be generic", "{0} '{1}' must not be generic", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
21+
public static readonly DiagnosticDescriptor EnumsValueObjectsAndUnionsMustNotBeGeneric = new("TTRESG033", "Enumerations, value objects and unions must not be generic", "{0} '{1}' must not be generic", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
2222
public static readonly DiagnosticDescriptor BaseClassFieldMustBeReadOnly = new("TTRESG034", "Field of the base class must be read-only", "The field '{0}' of the base class '{1}' must be read-only", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
2323
public static readonly DiagnosticDescriptor BaseClassPropertyMustBeReadOnly = new("TTRESG035", "Property of the base class must be read-only", "The property '{0}' of the base class '{1}' must be read-only", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
2424
public static readonly DiagnosticDescriptor EnumKeyShouldNotBeNullable = new("TTRESG036", "The key must not be nullable", "The generic type T of SmartEnumAttribute<T> must not be nullable", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
@@ -29,7 +29,7 @@ internal static class DiagnosticsDescriptors
2929
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);
3030
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);
3131
public static readonly DiagnosticDescriptor IndexBasedSwitchAndMapMustUseNamedParameters = new("TTRESG046", "The arguments of \"Switch\" and \"Map\" must named", "Not all arguments of \"Switch/Map\" on type '{0}' are named", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
32-
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);
32+
public static readonly DiagnosticDescriptor VariableMustBeInitializedWithNonDefaultValue = new("TTRESG047", "Variable must be initialed with non-default value", "Variable of type '{0}' must be initialized with non default value", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3333

3434
public static readonly DiagnosticDescriptor ErrorDuringCodeAnalysis = new("TTRESG098", "Error during code analysis", "Error during code analysis of '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
3535
public static readonly DiagnosticDescriptor ErrorDuringGeneration = new("TTRESG099", "Error during code generation", "Error during code generation for '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,16 @@ private void GenerateUnion(CancellationToken cancellationToken)
5050
_sb.Append(@"
5151
");
5252

53-
_sb.Append(_state.IsReferenceType ? "sealed " : "readonly ").Append("partial ").Append(_state.IsReferenceType ? "class" : "struct").Append(" ").Append(_state.Name).Append(" :")
54-
.Append(@"
53+
_sb.Append(_state.IsReferenceType ? "sealed " : "readonly ").Append("partial ").Append(_state.IsReferenceType ? "class" : "struct").Append(" ").Append(_state.Name);
54+
55+
if (!_state.IsRefStruct)
56+
{
57+
_sb.Append(@" :
5558
global::System.IEquatable<").AppendTypeFullyQualified(_state).Append(@">,
56-
global::System.Numerics.IEqualityOperators<").AppendTypeFullyQualified(_state).Append(@", ").AppendTypeFullyQualified(_state).Append(@", bool>
59+
global::System.Numerics.IEqualityOperators<").AppendTypeFullyQualified(_state).Append(@", ").AppendTypeFullyQualified(_state).Append(", bool>");
60+
}
61+
62+
_sb.Append(@"
5763
{
5864
private static readonly int _typeHashCode = typeof(").AppendTypeFullyQualified(_state).Append(@").GetHashCode();
5965
@@ -267,8 +273,20 @@ private void GenerateEquals()
267273
268274
/// <inheritdoc />
269275
public override bool Equals(object? other)
276+
{");
277+
278+
if (_state.IsRefStruct)
270279
{
271-
return other is ").AppendTypeFullyQualified(_state).Append(@" obj && Equals(obj);
280+
_sb.Append(@"
281+
return false;");
282+
}
283+
else
284+
{
285+
_sb.Append(@"
286+
return other is ").AppendTypeFullyQualified(_state).Append(" obj && Equals(obj);");
287+
}
288+
289+
_sb.Append(@"
272290
}
273291
274292
/// <inheritdoc />

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenState.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ public sealed class UnionSourceGenState : ITypeInformation, IEquatable<UnionSour
77
public string TypeFullyQualified { get; }
88
public string TypeMinimallyQualified { get; }
99
public bool IsReferenceType { get; }
10-
public NullableAnnotation NullableAnnotation { get; set; }
11-
public bool IsNullableStruct { get; set; }
10+
public NullableAnnotation NullableAnnotation { get; }
11+
public bool IsNullableStruct { get; }
12+
public bool IsRefStruct { get; }
1213
public bool IsEqualWithReferenceEquality => false;
1314

1415
public ImmutableArray<MemberTypeState> MemberTypes { get; }
@@ -28,6 +29,7 @@ public UnionSourceGenState(
2829
IsReferenceType = type.IsReferenceType;
2930
NullableAnnotation = type.NullableAnnotation;
3031
IsNullableStruct = type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
32+
IsRefStruct = type is { IsRefLikeType: true, IsReferenceType: false };
3133
}
3234

3335
public override bool Equals(object? obj)
@@ -44,6 +46,7 @@ public bool Equals(UnionSourceGenState? other)
4446

4547
return TypeFullyQualified == other.TypeFullyQualified
4648
&& IsReferenceType == other.IsReferenceType
49+
&& IsRefStruct == other.IsRefStruct
4750
&& Settings.Equals(other.Settings)
4851
&& MemberTypes.SequenceEqual(other.MemberTypes);
4952
}
@@ -54,6 +57,7 @@ public override int GetHashCode()
5457
{
5558
var hashCode = TypeFullyQualified.GetHashCode();
5659
hashCode = (hashCode * 397) ^ IsReferenceType.GetHashCode();
60+
hashCode = (hashCode * 397) ^ IsRefStruct.GetHashCode();
5761
hashCode = (hashCode * 397) ^ Settings.GetHashCode();
5862
hashCode = (hashCode * 397) ^ MemberTypes.ComputeHashCode();
5963

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
66
public class UnionSourceGenerator : ThinktectureSourceGeneratorBase, IIncrementalGenerator
77
{
88
public UnionSourceGenerator()
9-
: base(10_000)
9+
: base(15_000)
1010
{
1111
}
1212

@@ -59,6 +59,7 @@ private bool IsCandidate(SyntaxNode syntaxNode, CancellationToken cancellationTo
5959
return syntaxNode switch
6060
{
6161
ClassDeclarationSyntax classDeclaration when IsUnionCandidate(classDeclaration) => true,
62+
StructDeclarationSyntax structDeclaration when IsUnionCandidate(structDeclaration) => true,
6263
_ => false
6364
};
6465
}

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/SmartEnumSourceGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Thinktecture.CodeAnalysis.SmartEnums;
66
public sealed class SmartEnumSourceGenerator : ThinktectureSourceGeneratorBase, IIncrementalGenerator
77
{
88
public SmartEnumSourceGenerator()
9-
: base(17_000)
9+
: base(25_000)
1010
{
1111
}
1212

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypedMemberStateFactory.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace Thinktecture.CodeAnalysis;
33
public class TypedMemberStateFactory
44
{
55
private const string _SYSTEM_RUNTIME_DLL = "System.Runtime.dll";
6+
private const string _SYSTEM_CORELIB_DLL = "System.Private.CoreLib.dll";
67

78
private readonly TypedMemberStates _boolean;
89
private readonly TypedMemberStates _char;
@@ -46,6 +47,7 @@ private TypedMemberStateFactory(Compilation compilation)
4647
CreateAndAddStatesForSystemRuntime(compilation, "System.DateOnly", lookup);
4748
CreateAndAddStatesForSystemRuntime(compilation, "System.TimeOnly", lookup);
4849
CreateAndAddStatesForSystemRuntime(compilation, "System.TimeSpan", lookup);
50+
CreateAndAddStatesForSystemRuntime(compilation, "System.Guid", lookup);
4951
}
5052

5153
private static TypedMemberStates CreateStates(Compilation compilation, SpecialType specialType)
@@ -67,7 +69,7 @@ private static void CreateAndAddStatesForSystemRuntime(Compilation compilation,
6769
{
6870
type = types[0];
6971

70-
if (type.ContainingModule.MetadataName != _SYSTEM_RUNTIME_DLL)
72+
if (type.ContainingModule.MetadataName is not (_SYSTEM_RUNTIME_DLL or _SYSTEM_CORELIB_DLL))
7173
return;
7274
}
7375
else
@@ -76,7 +78,7 @@ private static void CreateAndAddStatesForSystemRuntime(Compilation compilation,
7678
{
7779
var candidate = types[i];
7880

79-
if (candidate.ContainingModule.MetadataName != _SYSTEM_RUNTIME_DLL)
81+
if (candidate.ContainingModule.MetadataName is not (_SYSTEM_RUNTIME_DLL or _SYSTEM_CORELIB_DLL))
8082
continue;
8183

8284
// duplicate?
@@ -87,7 +89,7 @@ private static void CreateAndAddStatesForSystemRuntime(Compilation compilation,
8789
}
8890
}
8991

90-
if (type is null)
92+
if (type is null || type.TypeKind == TypeKind.Error)
9193
return;
9294

9395
lookup.Add((type.ContainingModule.MetadataName, type.MetadataToken), CreateStates(type));

0 commit comments

Comments
 (0)