Skip to content

Commit 9603c2b

Browse files
committed
Implemented new method "Map" on smart enum that returns the provided instances.
1 parent 7a85bdb commit 9603c2b

File tree

12 files changed

+4770
-4003
lines changed

12 files changed

+4770
-4003
lines changed

samples/Thinktecture.Runtime.Extensions.Samples/SmartEnums/SmartEnumDemos.cs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@ private static void DemoForDailySalesCsvImporterType(ILogger logger)
2323
logger.Information("""
2424
2525
26-
==== Demo for Daily CSV-Importer-Type ====
26+
==== Demo for Daily CSV-Importer-Type ====
2727
28-
""");
28+
""");
2929

3030
var type = SalesCsvImporterType.Daily;
3131

3232
using var textReader = new StringReader("""
33-
id,datetime,volume
34-
1,20230425 10:45,345.67
35-
""");
33+
id,datetime,volume
34+
1,20230425 10:45,345.67
35+
""");
3636

3737
using var csvReader = new CsvReader(textReader, new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = true });
3838

@@ -54,20 +54,20 @@ private static void DemoForMonthlySalesCsvImporterType(ILogger logger)
5454
logger.Information("""
5555
5656
57-
==== Demo for Monthly CSV-Importer-Type ====
57+
==== Demo for Monthly CSV-Importer-Type ====
5858
59-
""");
59+
""");
6060

6161
var with_3_columns = true;
6262
var csvWith3Columns = """
63-
volume,datetime,id
64-
123.45,20230426 11:50,2
65-
""";
63+
volume,datetime,id
64+
123.45,20230426 11:50,2
65+
""";
6666

6767
var csvWith4Columns = """
68-
volume,quantity,id,datetime
69-
123.45,42,2,2023-04-25
70-
""";
68+
volume,quantity,id,datetime
69+
123.45,42,2,2023-04-25
70+
""";
7171

7272
var type = SalesCsvImporterType.Monthly;
7373
var csv = with_3_columns ? csvWith3Columns : csvWith4Columns;
@@ -144,6 +144,11 @@ private static void DemoForNonValidatableEnum(ILogger logger)
144144

145145
logger.Information(returnValue);
146146

147+
returnValue = productType.Map(ProductType.Groceries, "Map: Groceries",
148+
ProductType.Housewares, "Map: Housewares");
149+
150+
logger.Information(returnValue);
151+
147152
var parsed = ProductType.TryParse("Groceries", null, out var parsedProductType);
148153
logger.Information("Success: {Success} Parsed: {Parsed}", parsed, parsedProductType);
149154

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
3737
DiagnosticsDescriptors.EnumKeyShouldNotBeNullable,
3838
DiagnosticsDescriptors.EnumWithoutDerivedTypesMustBeSealed,
3939
DiagnosticsDescriptors.ValueObjectMustBeSealed,
40-
DiagnosticsDescriptors.SwitchMustCoverAllItems,
40+
DiagnosticsDescriptors.SwitchAndMapMustCoverAllItems,
4141
DiagnosticsDescriptors.DontImplementEnumInterfaceWithTwoGenerics,
4242
DiagnosticsDescriptors.ComparerTypeMustMatchMemberType,
4343
DiagnosticsDescriptors.ErrorDuringCodeAnalysis,
@@ -63,7 +63,7 @@ private static void AnalyzeMethodCall(OperationAnalysisContext context)
6363
|| operation.Arguments.IsDefaultOrEmpty
6464
|| operation.Arguments.Length % 2 != 0
6565
|| operation.TargetMethod.IsStatic
66-
|| operation.TargetMethod.Name != "Switch")
66+
|| (operation.TargetMethod.Name != "Switch" && operation.TargetMethod.Name != "Map"))
6767
{
6868
return;
6969
}
@@ -101,7 +101,7 @@ private static void AnalyzeMethodCall(OperationAnalysisContext context)
101101
}
102102

103103
if (!missingItemNames.IsDefaultOrEmpty)
104-
ReportDiagnostic(context, DiagnosticsDescriptors.SwitchMustCoverAllItems, operation.Syntax.GetLocation(), operation.Instance.Type, String.Join(", ", missingItemNames));
104+
ReportDiagnostic(context, DiagnosticsDescriptors.SwitchAndMapMustCoverAllItems, operation.Syntax.GetLocation(), operation.Instance.Type, String.Join(", ", missingItemNames));
105105
}
106106

107107
private static void AnalyzeSmartEnum(SymbolAnalysisContext context)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal static class DiagnosticsDescriptors
2929
public static readonly DiagnosticDescriptor EnumKeyShouldNotBeNullable = new("TTRESG036", "The key must not be nullable", "The generic type T of IEnum<T> and IValidatableEnum<T> must not be nullable", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3030
public static readonly DiagnosticDescriptor EnumWithoutDerivedTypesMustBeSealed = new("TTRESG037", "Enumeration without derived types must be sealed", "Enumeration '{0}' without derived types must be sealed", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3131
public static readonly DiagnosticDescriptor ValueObjectMustBeSealed = new("TTRESG038", "Value objects must be sealed", "Value object '{0}' must be sealed", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
32-
public static readonly DiagnosticDescriptor SwitchMustCoverAllItems = new("TTRESG039", "Switch must cover all items", "The switch-case on the enumeration '{0}' does not cover all items. Following items are not covered: {1}.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
32+
public static readonly DiagnosticDescriptor SwitchAndMapMustCoverAllItems = new("TTRESG039", "The methods \"Switch\" and \"Map\" must cover all items", "The method \"Switch/Map\" on the enumeration '{0}' does not cover all items. Following items are not covered: {1}.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3333
public static readonly DiagnosticDescriptor DontImplementEnumInterfaceWithTwoGenerics = new("TTRESG040", "Don't implement \"IEnum<TKey, T>\", implement \"IEnum<TKey>\" only", "The interface \"IEnum<{0}, {1}>\" will be implemented by Source Generator, implement \"IEnum<{0}>\" only", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3434
public static readonly DiagnosticDescriptor ComparerTypeMustMatchMemberType = new("TTRESG041", "The type of the comparer doesn't match the type of the member", "A comparer '{0}' doesn't match the type of the member", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3535
public static readonly DiagnosticDescriptor InitAccessorMustBePrivate = new("TTRESG042", "Property 'init' accessor must be private", "The 'init' accessor of the property '{0}' of the type '{1}' must be private", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public sealed class EnumSettings : IEquatable<EnumSettings>
1212
public bool SkipIFormattable { get; }
1313
public bool SkipToString { get; }
1414
public bool SkipSwitchMethods { get; }
15+
public bool SkipMapMethods { get; }
1516

1617
public EnumSettings(AttributeData? attribute)
1718
{
@@ -23,6 +24,7 @@ public EnumSettings(AttributeData? attribute)
2324
SkipIFormattable = attribute?.FindSkipIFormattable() ?? false;
2425
SkipToString = attribute?.FindSkipToString() ?? false;
2526
SkipSwitchMethods = attribute?.FindSkipSwitchMethods() ?? false;
27+
SkipMapMethods = attribute?.FindSkipMapMethods() ?? false;
2628

2729
// Comparison operators depend on the equality comparison operators
2830
if (ComparisonOperators > EqualityComparisonOperators)
@@ -48,7 +50,8 @@ public bool Equals(EnumSettings? other)
4850
&& EqualityComparisonOperators == other.EqualityComparisonOperators
4951
&& SkipIFormattable == other.SkipIFormattable
5052
&& SkipToString == other.SkipToString
51-
&& SkipSwitchMethods == other.SkipSwitchMethods;
53+
&& SkipSwitchMethods == other.SkipSwitchMethods
54+
&& SkipMapMethods == other.SkipMapMethods;
5255
}
5356

5457
public override int GetHashCode()
@@ -63,6 +66,7 @@ public override int GetHashCode()
6366
hashCode = (hashCode * 397) ^ SkipIFormattable.GetHashCode();
6467
hashCode = (hashCode * 397) ^ SkipToString.GetHashCode();
6568
hashCode = (hashCode * 397) ^ SkipSwitchMethods.GetHashCode();
69+
hashCode = (hashCode * 397) ^ SkipMapMethods.GetHashCode();
6670

6771
return hashCode;
6872
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public sealed class EnumSourceGeneratorState : ITypeInformation, IEquatable<Enum
1616
public BaseTypeState? BaseType { get; }
1717
public bool SkipToString { get; }
1818
public bool SkipSwitchMethods { get; }
19+
public bool SkipMapMethods { get; }
1920

2021
public bool HasCreateInvalidItemImplementation { get; }
2122
public bool HasKeyComparerImplementation { get; }
@@ -35,6 +36,7 @@ public EnumSourceGeneratorState(
3536
IMemberState keyProperty,
3637
bool skipToString,
3738
bool skipSwitchMethods,
39+
bool skipMapMethods,
3840
bool isValidatable,
3941
bool hasCreateInvalidItemImplementation,
4042
bool hasStructLayoutAttribute,
@@ -43,6 +45,7 @@ public EnumSourceGeneratorState(
4345
KeyProperty = keyProperty;
4446
SkipToString = skipToString;
4547
SkipSwitchMethods = skipSwitchMethods;
48+
SkipMapMethods = skipMapMethods;
4649
IsValidatable = isValidatable;
4750
HasCreateInvalidItemImplementation = hasCreateInvalidItemImplementation;
4851
HasStructLayoutAttribute = hasStructLayoutAttribute;

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ public override int GetHashCode()
155155
GenerateSwitchForFunc(true);
156156
}
157157

158+
if (!_state.SkipMapMethods)
159+
GenerateMap();
160+
158161
GenerateGetLookup();
159162

160163
_sb.Append(@"
@@ -319,7 +322,7 @@ public T Switch<TContext, T>(
319322

320323
_sb.Append("if (this == ").Append(itemNamePrefix).Append(i + 1).Append(@")
321324
{
322-
return ").Append(itemNamePrefix).Append("Func").Append(i + 1).Append(@"(");
325+
return ").Append(itemNamePrefix).Append("Func").Append(i + 1).Append("(");
323326

324327
if (withContext)
325328
_sb.Append("context");
@@ -336,6 +339,63 @@ public T Switch<TContext, T>(
336339
}");
337340
}
338341

342+
private void GenerateMap()
343+
{
344+
if (_state.ItemNames.Count == 0)
345+
return;
346+
347+
_sb.Append(@"
348+
349+
/// <summary>
350+
/// Maps an item to an instance of type <typeparamref name=""T""/>.
351+
/// </summary>");
352+
353+
var itemNamePrefix = _state.ArgumentName;
354+
355+
for (var i = 0; i < _state.ItemNames.Count; i++)
356+
{
357+
_sb.Append(@"
358+
/// <param name=""").Append(itemNamePrefix).Append(i + 1).Append(@""">The item to compare to.</param>
359+
/// <param name=""other").Append(i + 1).Append(@""">The instance to return if the current item is equal to <paramref name=""").Append(itemNamePrefix).Append(i + 1).Append(@"""/>.</param>");
360+
}
361+
362+
_sb.Append(@"
363+
public T Map<T>(");
364+
365+
for (var i = 0; i < _state.ItemNames.Count; i++)
366+
{
367+
if (i != 0)
368+
_sb.Append(",");
369+
370+
_sb.Append(@"
371+
").Append(_state.Name).Append(" ").Append(itemNamePrefix).Append(i + 1)
372+
.Append(", T ").Append("other").Append(i + 1);
373+
}
374+
375+
_sb.Append(@")
376+
{
377+
");
378+
379+
for (var i = 0; i < _state.ItemNames.Count; i++)
380+
{
381+
if (i != 0)
382+
_sb.Append(@"
383+
else ");
384+
385+
_sb.Append("if (this == ").Append(itemNamePrefix).Append(i + 1).Append(@")
386+
{
387+
return other").Append(i + 1).Append(@";
388+
}");
389+
}
390+
391+
_sb.Append(@"
392+
else
393+
{
394+
throw new global::System.ArgumentOutOfRangeException($""No instance provided for the item '{this}'."");
395+
}
396+
}");
397+
}
398+
339399
private void GenerateModuleInitializer(IMemberState keyMember)
340400
{
341401
var enumType = _state.TypeFullyQualified;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,8 @@ private bool IsEnumCandidate(TypeDeclarationSyntax typeDeclaration)
317317

318318
var attributeInfo = new AttributeInfo(type);
319319

320-
var enumState = new EnumSourceGeneratorState(factory, type, keyProperty, settings.SkipToString, settings.SkipSwitchMethods, isValidatable, hasCreateInvalidItemImplementation, attributeInfo.HasStructLayoutAttribute, cancellationToken);
320+
var enumState = new EnumSourceGeneratorState(factory, type, keyProperty, settings.SkipToString, settings.SkipSwitchMethods, settings.SkipMapMethods,
321+
isValidatable, hasCreateInvalidItemImplementation, attributeInfo.HasStructLayoutAttribute, cancellationToken);
321322
var derivedTypes = new SmartEnumDerivedTypes(enumState.Namespace, enumState.Name, enumState.TypeFullyQualified, enumState.IsReferenceType, FindDerivedTypes(type));
322323

323324
Logger.LogDebug("The type declaration is a valid smart enum", namespaceAndName: enumState);

src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ public static OperatorsGeneration FindEqualityComparisonOperators(this Attribute
8585
return GetBooleanParameterValue(attributeData, "SkipSwitchMethods");
8686
}
8787

88+
public static bool? FindSkipMapMethods(this AttributeData attributeData)
89+
{
90+
return GetBooleanParameterValue(attributeData, "SkipMapMethods");
91+
}
92+
8893
public static (ITypeSymbol ComparerType, ITypeSymbol ItemType)? GetComparerTypes(this AttributeData attributeData)
8994
{
9095
if (attributeData.AttributeClass is not { } attributeClass || attributeClass.TypeKind == TypeKind.Error)

src/Thinktecture.Runtime.Extensions/EnumGenerationAttribute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,9 @@ public OperatorsGeneration EqualityComparisonOperators
8080
/// Indication whether the generator should skip the implementation of the methods <code>Switch</code>.
8181
/// </summary>
8282
public bool SkipSwitchMethods { get; set; }
83+
84+
/// <summary>
85+
/// Indication whether the generator should skip the implementation of the methods <code>Map</code>.
86+
/// </summary>
87+
public bool SkipMapMethods { get; set; }
8388
}

test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG039_SwitchMustCoverAllItems.cs

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
namespace Thinktecture.Runtime.Tests.AnalyzerAndCodeFixTests;
66

77
// ReSharper disable InconsistentNaming
8-
public class TTRESG033_SwitchMustCoverAllItems
8+
public class TTRESG033_SwitchAndMapMustCoverAllItems
99
{
1010
private const string _DIAGNOSTIC_ID = "TTRESG039";
1111

1212
[Fact]
13-
public async Task Should_trigger_on_missing_items_having_action()
13+
public async Task Should_trigger_on_Switch_missing_items_having_action()
1414
{
1515
var code = @"
1616
using System;
@@ -36,7 +36,7 @@ public void Do()
3636
}
3737

3838
[Fact]
39-
public async Task Should_not_trigger_when_all_items_are_covered_having_action()
39+
public async Task Should_not_trigger_on_Switch_when_all_items_are_covered_having_action()
4040
{
4141
var code = @"
4242
using System;
@@ -61,7 +61,7 @@ public void Do()
6161
}
6262

6363
[Fact]
64-
public async Task Should_trigger_on_missing_items_having_func()
64+
public async Task Should_trigger_on_Switch_missing_items_having_func()
6565
{
6666
var code = @"
6767
using System;
@@ -87,7 +87,7 @@ public void Do()
8787
}
8888

8989
[Fact]
90-
public async Task Should_not_trigger_when_all_items_are_covered_having_func()
90+
public async Task Should_not_trigger_on_Switch_when_all_items_are_covered_having_func()
9191
{
9292
var code = @"
9393
using System;
@@ -110,4 +110,55 @@ public void Do()
110110

111111
await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(ValueObjectAttribute).Assembly, typeof(TestEnum).Assembly });
112112
}
113+
114+
[Fact]
115+
public async Task Should_trigger_on_Map_missing_items()
116+
{
117+
var code = @"
118+
using System;
119+
using Thinktecture;
120+
using Thinktecture.Runtime.Tests.TestEnums;
121+
122+
namespace TestNamespace
123+
{
124+
public class Test
125+
{
126+
public void Do()
127+
{
128+
var testEnum = TestEnum.Item1;
129+
130+
var returnValue = {|#0:testEnum.Map(TestEnum.Item1, 1,
131+
TestEnum.Item1, 2)|};
132+
}
133+
}
134+
}";
135+
136+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestEnum", "Item2");
137+
await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(ValueObjectAttribute).Assembly, typeof(TestEnum).Assembly }, expected);
138+
}
139+
140+
[Fact]
141+
public async Task Should_not_trigger_on_Map_when_all_items_are_covered()
142+
{
143+
var code = @"
144+
using System;
145+
using Thinktecture;
146+
using Thinktecture.Runtime.Tests.TestEnums;
147+
148+
namespace TestNamespace
149+
{
150+
public class Test
151+
{
152+
public void Do()
153+
{
154+
var testEnum = TestEnum.Item1;
155+
156+
var returnValue = {|#0:testEnum.Map(TestEnum.Item1, 1,
157+
TestEnum.Item2, 2)|};
158+
}
159+
}
160+
}";
161+
162+
await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(ValueObjectAttribute).Assembly, typeof(TestEnum).Assembly });
163+
}
113164
}

0 commit comments

Comments
 (0)