Skip to content

Commit

Permalink
added discriminated unions
Browse files Browse the repository at this point in the history
  • Loading branch information
PawelGerr committed Sep 8, 2024
1 parent c95052f commit 2f90db5
Show file tree
Hide file tree
Showing 66 changed files with 8,093 additions and 89 deletions.
52 changes: 26 additions & 26 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
<Project>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0"/>
<PackageVersion Include="CsvHelper" Version="33.0.1"/>
<PackageVersion Include="FluentAssertions" Version="6.12.0"/>
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.6"/>
<PackageVersion Include="MessagePack" Version="2.5.172"/>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.2"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.11.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.0"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0"/>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageVersion Include="NSubstitute" Version="5.1.0"/>
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17"/>
<PackageVersion Include="Serilog.Extensions.Logging" Version="8.0.0"/>
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageVersion Include="xunit" Version="2.9.0"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2"/>
</ItemGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="CsvHelper" Version="33.0.1" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.8" />
<PackageVersion Include="MessagePack" Version="2.5.172" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.11.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="xunit" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Thinktecture.DiscriminatedUnions;

[Union<string, int>(T1IsNullableReferenceType = true,
T1Name = "Text",
T2Name = "Number",
SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads,
MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)]
public sealed partial class TextOrNumber;
2 changes: 2 additions & 0 deletions samples/Thinktecture.Runtime.Extensions.Samples/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Serilog;
using Thinktecture.DiscriminatedUnions;
using Thinktecture.EmptyClass;
using Thinktecture.SmartEnums;
using Thinktecture.ValueObjects;
Expand All @@ -13,6 +14,7 @@ public static void Main()

SmartEnumDemos.Demo(logger);
ValueObjectDemos.Demo(logger);
DiscriminatedUnionsDemos.Demo(logger);
EmptyActionDemos.Demo();
EmptyCollectionsDemos.Demo();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
DiagnosticsDescriptors.PrimaryConstructorNotAllowed,
DiagnosticsDescriptors.CustomKeyMemberImplementationNotFound,
DiagnosticsDescriptors.CustomKeyMemberImplementationTypeMismatch,
DiagnosticsDescriptors.IndexBasedSwitchAndMapMustUseNamedParameters);
DiagnosticsDescriptors.IndexBasedSwitchAndMapMustUseNamedParameters,
DiagnosticsDescriptors.TypeMustBeClass);

/// <inheritdoc />
public override void Initialize(AnalysisContext context)
Expand All @@ -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);
}
Expand Down Expand Up @@ -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<InstanceMemberInfo>? assignableMembers,
INamedTypeSymbol type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;

public sealed class AllUnionSettings : IEquatable<AllUnionSettings>
{
public bool SkipToString { get; }
public SwitchMapMethodsGeneration SwitchMethods { get; }
public SwitchMapMethodsGeneration MapMethods { get; }
public IReadOnlyList<MemberTypeSetting> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Thinktecture.CodeAnalysis.DiscriminatedUnions;

public readonly struct MemberTypeSetting : IEquatable<MemberTypeSetting>, 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);
}
}
}
Loading

0 comments on commit 2f90db5

Please sign in to comment.