Skip to content

Commit

Permalink
A union can be a struct or a ref struct.
Browse files Browse the repository at this point in the history
  • Loading branch information
PawelGerr committed Sep 19, 2024
1 parent 3a59b84 commit 8f95739
Show file tree
Hide file tree
Showing 36 changed files with 3,298 additions and 1,908 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,21 @@ Features:
* Renaming of properties
* Definition of nullable reference types

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

```csharp
// class
[Union<string, int>]
public partial class TextOrNumber;

// struct
[Union<string, int>]
public partial struct TextOrNumber;

// ref struct
[Union<string, int>]
public ref partial struct TextOrNumber;

// Up to 5 types
[Union<string, int, bool, Guid, char>]
public partial class MyUnion;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Thinktecture.DiscriminatedUnions;

[Union<string, int>]
public ref partial struct TextOrNumberRefStruct;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Thinktecture.DiscriminatedUnions;

[Union<string, int>]
public partial struct TextOrNumberStruct;
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
DiagnosticsDescriptors.TypeCannotBeNestedClass,
DiagnosticsDescriptors.KeyMemberShouldNotBeNullable,
DiagnosticsDescriptors.StaticPropertiesAreNotConsideredItems,
DiagnosticsDescriptors.EnumsAndValueObjectsMustNotBeGeneric,
DiagnosticsDescriptors.EnumsValueObjectsAndUnionsMustNotBeGeneric,
DiagnosticsDescriptors.BaseClassFieldMustBeReadOnly,
DiagnosticsDescriptors.BaseClassPropertyMustBeReadOnly,
DiagnosticsDescriptors.EnumKeyShouldNotBeNullable,
Expand All @@ -42,7 +42,7 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
DiagnosticsDescriptors.CustomKeyMemberImplementationNotFound,
DiagnosticsDescriptors.CustomKeyMemberImplementationTypeMismatch,
DiagnosticsDescriptors.IndexBasedSwitchAndMapMustUseNamedParameters,
DiagnosticsDescriptors.TypeMustBeClass);
DiagnosticsDescriptors.VariableMustBeInitializedWithNonDefaultValue);

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

context.RegisterOperationAction(AnalyzeMethodCall, OperationKind.Invocation);
context.RegisterOperationAction(AnalyzeDefaultValueAssignment, OperationKind.DefaultValue);
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
}

private void AnalyzeObjectCreation(OperationAnalysisContext context)
{
var operation = (IObjectCreationOperation)context.Operation;

if (operation.Type is null)
return;

if (!operation.Type.IsReferenceType
&& operation.Arguments.Length == 0
&& operation.Type.IsUnionType(out _))
{
ReportDiagnostic(context, DiagnosticsDescriptors.VariableMustBeInitializedWithNonDefaultValue, operation.Syntax.GetLocation(), operation.Type);
}
}

private void AnalyzeDefaultValueAssignment(OperationAnalysisContext context)
{
var operation = (IDefaultValueOperation)context.Operation;

if (operation.Type is null)
return;

if (!operation.Type.IsReferenceType
&& operation.Type.IsUnionType(out _))
{
ReportDiagnostic(context, DiagnosticsDescriptors.VariableMustBeInitializedWithNonDefaultValue, operation.Syntax.GetLocation(), operation.Type);
}
}

private static void AnalyzeMethodCall(OperationAnalysisContext context)
Expand Down Expand Up @@ -84,7 +115,7 @@ private static void AnalyzeMethodCall(OperationAnalysisContext context)
operation,
isValidatable);
}
else if (operation.Instance.Type.IsUnionAttribute(out attribute)
else if (operation.Instance.Type.IsUnionType(out attribute)
&& attribute.AttributeClass is not null)
{
AnalyzeUnionSwitchMap(context,
Expand Down Expand Up @@ -244,9 +275,9 @@ private static void ValidateUnion(OperationAnalysisContext context,
IObjectCreationOperation attribute,
Location locationOfFirstDeclaration)
{
if (type.IsRecord || type.TypeKind is not TypeKind.Class)
if (type.IsRecord || type.TypeKind is not (TypeKind.Class or TypeKind.Struct))
{
ReportDiagnostic(context, DiagnosticsDescriptors.TypeMustBeClass, locationOfFirstDeclaration, type);
ReportDiagnostic(context, DiagnosticsDescriptors.TypeMustBeClassOrStruct, locationOfFirstDeclaration, type);
return;
}

Expand Down Expand Up @@ -533,7 +564,7 @@ private static void ValidateKeyedSmartEnum(
private static void TypeMustNotBeGeneric(OperationAnalysisContext context, INamedTypeSymbol type, Location locationOfFirstDeclaration, string typeKind)
{
if (!type.TypeParameters.IsDefaultOrEmpty)
ReportDiagnostic(context, DiagnosticsDescriptors.EnumsAndValueObjectsMustNotBeGeneric, locationOfFirstDeclaration, typeKind, BuildTypeName(type));
ReportDiagnostic(context, DiagnosticsDescriptors.EnumsValueObjectsAndUnionsMustNotBeGeneric, locationOfFirstDeclaration, typeKind, BuildTypeName(type));
}

private static void Check_ItemLike_StaticProperties(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal static class DiagnosticsDescriptors
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);
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);
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);
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);
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);
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);
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);
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);
Expand All @@ -29,7 +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 type '{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 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);

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,16 @@ private void GenerateUnion(CancellationToken cancellationToken)
_sb.Append(@"
");

_sb.Append(_state.IsReferenceType ? "sealed " : "readonly ").Append("partial ").Append(_state.IsReferenceType ? "class" : "struct").Append(" ").Append(_state.Name).Append(" :")
.Append(@"
_sb.Append(_state.IsReferenceType ? "sealed " : "readonly ").Append("partial ").Append(_state.IsReferenceType ? "class" : "struct").Append(" ").Append(_state.Name);

if (!_state.IsRefStruct)
{
_sb.Append(@" :
global::System.IEquatable<").AppendTypeFullyQualified(_state).Append(@">,
global::System.Numerics.IEqualityOperators<").AppendTypeFullyQualified(_state).Append(@", ").AppendTypeFullyQualified(_state).Append(@", bool>
global::System.Numerics.IEqualityOperators<").AppendTypeFullyQualified(_state).Append(@", ").AppendTypeFullyQualified(_state).Append(", bool>");
}

_sb.Append(@"
{
private static readonly int _typeHashCode = typeof(").AppendTypeFullyQualified(_state).Append(@").GetHashCode();
Expand Down Expand Up @@ -267,8 +273,20 @@ private void GenerateEquals()
/// <inheritdoc />
public override bool Equals(object? other)
{");

if (_state.IsRefStruct)
{
return other is ").AppendTypeFullyQualified(_state).Append(@" obj && Equals(obj);
_sb.Append(@"
return false;");
}
else
{
_sb.Append(@"
return other is ").AppendTypeFullyQualified(_state).Append(" obj && Equals(obj);");
}

_sb.Append(@"
}
/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ public sealed class UnionSourceGenState : ITypeInformation, IEquatable<UnionSour
public string TypeFullyQualified { get; }
public string TypeMinimallyQualified { get; }
public bool IsReferenceType { get; }
public NullableAnnotation NullableAnnotation { get; set; }
public bool IsNullableStruct { get; set; }
public NullableAnnotation NullableAnnotation { get; }
public bool IsNullableStruct { get; }
public bool IsRefStruct { get; }
public bool IsEqualWithReferenceEquality => false;

public ImmutableArray<MemberTypeState> MemberTypes { get; }
Expand All @@ -28,6 +29,7 @@ public UnionSourceGenState(
IsReferenceType = type.IsReferenceType;
NullableAnnotation = type.NullableAnnotation;
IsNullableStruct = type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
IsRefStruct = type is { IsRefLikeType: true, IsReferenceType: false };
}

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

return TypeFullyQualified == other.TypeFullyQualified
&& IsReferenceType == other.IsReferenceType
&& IsRefStruct == other.IsRefStruct
&& Settings.Equals(other.Settings)
&& MemberTypes.SequenceEqual(other.MemberTypes);
}
Expand All @@ -54,6 +57,7 @@ public override int GetHashCode()
{
var hashCode = TypeFullyQualified.GetHashCode();
hashCode = (hashCode * 397) ^ IsReferenceType.GetHashCode();
hashCode = (hashCode * 397) ^ IsRefStruct.GetHashCode();
hashCode = (hashCode * 397) ^ Settings.GetHashCode();
hashCode = (hashCode * 397) ^ MemberTypes.ComputeHashCode();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;
public class UnionSourceGenerator : ThinktectureSourceGeneratorBase, IIncrementalGenerator
{
public UnionSourceGenerator()
: base(10_000)
: base(15_000)
{
}

Expand Down Expand Up @@ -59,6 +59,7 @@ private bool IsCandidate(SyntaxNode syntaxNode, CancellationToken cancellationTo
return syntaxNode switch
{
ClassDeclarationSyntax classDeclaration when IsUnionCandidate(classDeclaration) => true,
StructDeclarationSyntax structDeclaration when IsUnionCandidate(structDeclaration) => true,
_ => false
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Thinktecture.CodeAnalysis.SmartEnums;
public sealed class SmartEnumSourceGenerator : ThinktectureSourceGeneratorBase, IIncrementalGenerator
{
public SmartEnumSourceGenerator()
: base(17_000)
: base(25_000)
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Thinktecture.CodeAnalysis;
public class TypedMemberStateFactory
{
private const string _SYSTEM_RUNTIME_DLL = "System.Runtime.dll";
private const string _SYSTEM_CORELIB_DLL = "System.Private.CoreLib.dll";

private readonly TypedMemberStates _boolean;
private readonly TypedMemberStates _char;
Expand Down Expand Up @@ -46,6 +47,7 @@ private TypedMemberStateFactory(Compilation compilation)
CreateAndAddStatesForSystemRuntime(compilation, "System.DateOnly", lookup);
CreateAndAddStatesForSystemRuntime(compilation, "System.TimeOnly", lookup);
CreateAndAddStatesForSystemRuntime(compilation, "System.TimeSpan", lookup);
CreateAndAddStatesForSystemRuntime(compilation, "System.Guid", lookup);
}

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

if (type.ContainingModule.MetadataName != _SYSTEM_RUNTIME_DLL)
if (type.ContainingModule.MetadataName is not (_SYSTEM_RUNTIME_DLL or _SYSTEM_CORELIB_DLL))
return;
}
else
Expand All @@ -76,7 +78,7 @@ private static void CreateAndAddStatesForSystemRuntime(Compilation compilation,
{
var candidate = types[i];

if (candidate.ContainingModule.MetadataName != _SYSTEM_RUNTIME_DLL)
if (candidate.ContainingModule.MetadataName is not (_SYSTEM_RUNTIME_DLL or _SYSTEM_CORELIB_DLL))
continue;

// duplicate?
Expand All @@ -87,7 +89,7 @@ private static void CreateAndAddStatesForSystemRuntime(Compilation compilation,
}
}

if (type is null)
if (type is null || type.TypeKind == TypeKind.Error)
return;

lookup.Add((type.ContainingModule.MetadataName, type.MetadataToken), CreateStates(type));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,17 @@ public static bool IsUnionAttribute(this ITypeSymbol? attributeType)
return attributeType is { Name: Constants.Attributes.Union.NAME, ContainingNamespace: { Name: Constants.Attributes.Union.NAMESPACE, ContainingNamespace.IsGlobalNamespace: true } };
}

public static bool IsUnionAttribute(
public static bool IsUnionType(
[NotNullWhen(true)] this ITypeSymbol? unionType,
[NotNullWhen(true)] out AttributeData? unionAttribute)
{
unionAttribute = unionType?.FindAttribute(static attributeClass => attributeClass.IsUnionAttribute());
if (unionType is null || unionType.SpecialType != SpecialType.None)
{
unionAttribute = null;
return false;
}

unionAttribute = unionType.FindAttribute(static attributeClass => attributeClass.IsUnionAttribute());

return unionAttribute is not null;
}
Expand Down Expand Up @@ -88,7 +94,13 @@ public static bool IsEnum(
[NotNullWhen(true)] this ITypeSymbol? enumType,
[NotNullWhen(true)] out AttributeData? smartEnumAttribute)
{
smartEnumAttribute = enumType?.FindAttribute(static attributeClass => attributeClass.IsSmartEnumAttribute());
if (enumType is null || enumType.SpecialType != SpecialType.None)
{
smartEnumAttribute = null;
return false;
}

smartEnumAttribute = enumType.FindAttribute(static attributeClass => attributeClass.IsSmartEnumAttribute());

return smartEnumAttribute is not null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Thinktecture.Runtime.Extensions/UnionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Thinktecture;
/// </summary>
/// <typeparam name="T1">One of the types of the discriminated union.</typeparam>
/// <typeparam name="T2">One of the types of the discriminated union.</typeparam>
[AttributeUsage(AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class UnionAttribute<T1, T2> : UnionAttributeBase
{
/// <summary>
Expand Down
Loading

0 comments on commit 8f95739

Please sign in to comment.