diff --git a/Thinktecture.Runtime.Extensions.sln b/Thinktecture.Runtime.Extensions.sln index 6056e3d1..403792d2 100644 --- a/Thinktecture.Runtime.Extensions.sln +++ b/Thinktecture.Runtime.Extensions.sln @@ -68,6 +68,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thinktecture.Runtime.Extens EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thinktecture.Runtime.Extensions.AspNetCore.Tests", "test\Thinktecture.Runtime.Extensions.AspNetCore.Tests\Thinktecture.Runtime.Extensions.AspNetCore.Tests.csproj", "{EF07F98E-9914-4419-A1B4-AB264F496121}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thinktecture.Runtime.Extensions.Tests.Shared", "test\Thinktecture.Runtime.Extensions.Tests.Shared\Thinktecture.Runtime.Extensions.Tests.Shared.csproj", "{E654E2B6-9396-4B8D-917D-EAD8FAA0DBF2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -158,6 +160,10 @@ Global {EF07F98E-9914-4419-A1B4-AB264F496121}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF07F98E-9914-4419-A1B4-AB264F496121}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF07F98E-9914-4419-A1B4-AB264F496121}.Release|Any CPU.Build.0 = Release|Any CPU + {E654E2B6-9396-4B8D-917D-EAD8FAA0DBF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E654E2B6-9396-4B8D-917D-EAD8FAA0DBF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E654E2B6-9396-4B8D-917D-EAD8FAA0DBF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E654E2B6-9396-4B8D-917D-EAD8FAA0DBF2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -184,6 +190,7 @@ Global {CA1D0C43-10C9-46E2-A893-4E93FB4A1511} = {8F117684-7943-4DCE-8861-F2B854924837} {6328DC16-6E31-42B9-B0E5-6A98A072CACC} = {8F117684-7943-4DCE-8861-F2B854924837} {EF07F98E-9914-4419-A1B4-AB264F496121} = {AE711F89-41F2-4519-B20D-BA1FAB0EB364} + {E654E2B6-9396-4B8D-917D-EAD8FAA0DBF2} = {AE711F89-41F2-4519-B20D-BA1FAB0EB364} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1C34F508-A60B-4C0E-AFA0-0F4CFFB23603} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/BaseEnumState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/BaseEnumState.cs new file mode 100644 index 00000000..da5aecb4 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/BaseEnumState.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Thinktecture.CodeAnalysis +{ + public class BaseEnumState : IBaseEnumState + { + public bool IsSameAssembly => false; + public INamedTypeSymbol Type { get; } + public string? NullableQuestionMark => Type.IsReferenceType ? "?" : null; + + private IReadOnlyList? _items; + + public IReadOnlyList Items => _items ??= Type.EnumerateEnumItems().Select(DefaultSymbolState.CreateFrom).ToList(); + + private IReadOnlyList? _ctorExtraArgs; + + public IReadOnlyList ConstructorArguments + { + get + { + if (_ctorExtraArgs is null) + { + var ctor = Type.Constructors + .Where(c => c.MethodKind == MethodKind.Constructor && c.DeclaredAccessibility == Accessibility.Protected) + .OrderBy(c => c.Parameters.Length) + .FirstOrDefault(); + + if (ctor is null) + throw new Exception($"'{Type.Name}' doesn't have a protected constructor."); + + var ctorAttrArgs = GetCtorParameterNames(ctor); + + _ctorExtraArgs = ctor.Parameters + .Select((p, i) => + { + var memberName = ctorAttrArgs[i]; + + if (memberName.Value is not string name) + throw new Exception($"The parameter '{memberName.Value}' of the 'EnumConstructorAttribute' of '{Type.Name}' at index {i} must be a string."); + + return new DefaultSymbolState(name, p.Type, p.Name, false); + }) + .ToList(); + } + + return _ctorExtraArgs; + } + } + + private IReadOnlyList GetCtorParameterNames(IMethodSymbol ctor) + { + var ctorAttr = Type.FindEnumConstructorAttribute(); + + if (ctorAttr is null) + throw new Exception($"'{Type.Name}' doesn't have an 'EnumConstructorAttribute'."); + + if (ctorAttr.ConstructorArguments.Length != 1) + throw new Exception($"'EnumConstructorAttribute' of '{Type.Name}' must have exactly 1 argument."); + + var ctorAttrArgs = ctorAttr.ConstructorArguments[0].Values; + + if (ctorAttrArgs.Length != ctor.Parameters.Length) + throw new Exception($"'EnumConstructorAttribute' of '{Type.Name}' specifies {ctorAttrArgs.Length} parameters but the constructor takes {ctor.Parameters.Length} arguments."); + + return ctorAttrArgs; + } + + public BaseEnumState(INamedTypeSymbol type) + { + Type = type; + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/CodeFixes/ThinktectureRuntimeExtensionsCodeFixProvider.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/CodeFixes/ThinktectureRuntimeExtensionsCodeFixProvider.cs index 64b3e3de..3257b3e4 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/CodeFixes/ThinktectureRuntimeExtensionsCodeFixProvider.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/CodeFixes/ThinktectureRuntimeExtensionsCodeFixProvider.cs @@ -18,7 +18,7 @@ public class ThinktectureRuntimeExtensionsCodeFixProvider : CodeFixProvider { private const string _MAKE_PARTIAL = "Make the type partial"; private const string _MAKE_STRUCT_READONLY = "Make the type read-only"; - private const string _MAKE_FIELD_PUBLIC = "Make the field public"; + private const string _MAKE_MEMBER_PUBLIC = "Make the member public"; private const string _MAKE_FIELD_READONLY = "Make the field read-only"; private const string _REMOVE_PROPERTY_SETTER = "Remove property setter"; private const string _IMPLEMENT_CREATE_INVALID = "Implement 'CreateInvalidItem'"; @@ -48,67 +48,47 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (root is not null) { - var typeDeclaration = GetDeclaration(context, root); - FieldDeclarationSyntax? fieldDeclaration = null; - PropertyDeclarationSyntax? propertyDeclaration = null; + var ctx = new CodeFixesContext(context, root); - if (typeDeclaration is not null) - { - var makePartialDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.TypeMustBePartial); - - if (makePartialDiagnostic is not null) - context.RegisterCodeFix(CodeAction.Create(_MAKE_PARTIAL, _ => AddTypeModifierAsync(context.Document, root, typeDeclaration, SyntaxKind.PartialKeyword), _MAKE_PARTIAL), makePartialDiagnostic); - - var makeStructReadOnlyDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.StructMustBeReadOnly); - - if (makeStructReadOnlyDiagnostic is not null) - context.RegisterCodeFix(CodeAction.Create(_MAKE_STRUCT_READONLY, _ => AddTypeModifierAsync(context.Document, root, typeDeclaration, SyntaxKind.ReadOnlyKeyword), _MAKE_STRUCT_READONLY), makeStructReadOnlyDiagnostic); + var makePartialDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.TypeMustBePartial); - var makeFieldReadOnlyDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.FieldMustBeReadOnly); + if (makePartialDiagnostic is not null) + context.RegisterCodeFix(CodeAction.Create(_MAKE_PARTIAL, _ => AddTypeModifierAsync(context.Document, root, ctx.TypeDeclaration, SyntaxKind.PartialKeyword), _MAKE_PARTIAL), makePartialDiagnostic); - if (makeFieldReadOnlyDiagnostic is not null) - { - fieldDeclaration ??= GetDeclaration(context, root); + var makeStructReadOnlyDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.StructMustBeReadOnly); - if (fieldDeclaration is not null) - context.RegisterCodeFix(CodeAction.Create(_MAKE_FIELD_READONLY, _ => AddTypeModifierAsync(context.Document, root, fieldDeclaration, SyntaxKind.ReadOnlyKeyword), _MAKE_FIELD_READONLY), makeFieldReadOnlyDiagnostic); - } + if (makeStructReadOnlyDiagnostic is not null) + context.RegisterCodeFix(CodeAction.Create(_MAKE_STRUCT_READONLY, _ => AddTypeModifierAsync(context.Document, root, ctx.TypeDeclaration, SyntaxKind.ReadOnlyKeyword), _MAKE_STRUCT_READONLY), makeStructReadOnlyDiagnostic); - var makeFieldPublicDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.FieldMustBePublic); + var makeFieldReadOnlyDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.FieldMustBeReadOnly); - if (makeFieldPublicDiagnostic is not null) - { - fieldDeclaration ??= GetDeclaration(context, root); + if (makeFieldReadOnlyDiagnostic is not null) + context.RegisterCodeFix(CodeAction.Create(_MAKE_FIELD_READONLY, _ => AddTypeModifierAsync(context.Document, root, ctx.FieldDeclaration, SyntaxKind.ReadOnlyKeyword), _MAKE_FIELD_READONLY), makeFieldReadOnlyDiagnostic); - if (fieldDeclaration is not null) - context.RegisterCodeFix(CodeAction.Create(_MAKE_FIELD_PUBLIC, _ => ChangeAccessibilityAsync(context.Document, root, fieldDeclaration, SyntaxKind.PublicKeyword), _MAKE_FIELD_PUBLIC), makeFieldPublicDiagnostic); - } + var makeFieldPublicDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.FieldMustBePublic); - var makePropertyReadOnlyDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.PropertyMustBeReadOnly); + if (makeFieldPublicDiagnostic is not null) + context.RegisterCodeFix(CodeAction.Create(_MAKE_MEMBER_PUBLIC, _ => ChangeAccessibilityAsync(context.Document, root, ctx.FieldDeclaration, SyntaxKind.PublicKeyword), _MAKE_MEMBER_PUBLIC), makeFieldPublicDiagnostic); - if (makePropertyReadOnlyDiagnostic is not null) - { - propertyDeclaration ??= GetDeclaration(context, root); + var makePropertyReadOnlyDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.PropertyMustBeReadOnly); - if (propertyDeclaration is not null) - context.RegisterCodeFix(CodeAction.Create(_REMOVE_PROPERTY_SETTER, _ => RemovePropertySetterAsync(context.Document, root, propertyDeclaration), _REMOVE_PROPERTY_SETTER), makePropertyReadOnlyDiagnostic); - } + if (makePropertyReadOnlyDiagnostic is not null) + context.RegisterCodeFix(CodeAction.Create(_REMOVE_PROPERTY_SETTER, _ => RemovePropertySetterAsync(context.Document, root, ctx.PropertyDeclaration), _REMOVE_PROPERTY_SETTER), makePropertyReadOnlyDiagnostic); - var needsCreateInvalidDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.AbstractEnumNeedsCreateInvalidItemImplementation); + var needsCreateInvalidDiagnostic = FindDiagnostics(context, DiagnosticsDescriptors.AbstractEnumNeedsCreateInvalidItemImplementation); - if (needsCreateInvalidDiagnostic is not null) - context.RegisterCodeFix(CodeAction.Create(_IMPLEMENT_CREATE_INVALID, t => AddCreateInvalidItemAsync(context.Document, root, typeDeclaration, t), _IMPLEMENT_CREATE_INVALID), needsCreateInvalidDiagnostic); + if (needsCreateInvalidDiagnostic is not null) + context.RegisterCodeFix(CodeAction.Create(_IMPLEMENT_CREATE_INVALID, t => AddCreateInvalidItemAsync(context.Document, root, ctx.TypeDeclaration, t), _IMPLEMENT_CREATE_INVALID), needsCreateInvalidDiagnostic); - var makeDerivedTypePrivate = FindDiagnostics(context, DiagnosticsDescriptors.FirstLevelInnerTypeMustBePrivate); + var makeDerivedTypePrivate = FindDiagnostics(context, DiagnosticsDescriptors.FirstLevelInnerTypeMustBePrivate); - if (makeDerivedTypePrivate is not null) - context.RegisterCodeFix(CodeAction.Create(_MAKE_TYPE_PRIVATE, _ => ChangeAccessibilityAsync(context.Document, root, typeDeclaration, SyntaxKind.PrivateKeyword), _MAKE_TYPE_PRIVATE), makeDerivedTypePrivate); + if (makeDerivedTypePrivate is not null) + context.RegisterCodeFix(CodeAction.Create(_MAKE_TYPE_PRIVATE, _ => ChangeAccessibilityAsync(context.Document, root, ctx.TypeDeclaration, SyntaxKind.PrivateKeyword), _MAKE_TYPE_PRIVATE), makeDerivedTypePrivate); - var makeDerivedTypePublic = FindDiagnostics(context, DiagnosticsDescriptors.NonFirstLevelInnerTypeMustBePublic); + var makeDerivedTypePublic = FindDiagnostics(context, DiagnosticsDescriptors.NonFirstLevelInnerTypeMustBePublic); - if (makeDerivedTypePublic is not null) - context.RegisterCodeFix(CodeAction.Create(_MAKE_TYPE_PUBLIC, _ => ChangeAccessibilityAsync(context.Document, root, typeDeclaration, SyntaxKind.PublicKeyword), _MAKE_TYPE_PUBLIC), makeDerivedTypePublic); - } + if (makeDerivedTypePublic is not null) + context.RegisterCodeFix(CodeAction.Create(_MAKE_TYPE_PUBLIC, _ => ChangeAccessibilityAsync(context.Document, root, ctx.TypeDeclaration, SyntaxKind.PublicKeyword), _MAKE_TYPE_PUBLIC), makeDerivedTypePublic); } } @@ -117,19 +97,15 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) return context.Diagnostics.FirstOrDefault(d => d.Id == diagnostic.Id); } - private static T? GetDeclaration(CodeFixContext context, SyntaxNode root) - where T : MemberDeclarationSyntax - { - var diagnosticSpan = context.Diagnostics.First().Location.SourceSpan; - return root.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); - } - private static Task AddTypeModifierAsync( Document document, SyntaxNode root, - MemberDeclarationSyntax declaration, + MemberDeclarationSyntax? declaration, SyntaxKind modifier) { + if (declaration is null) + return Task.FromResult(document); + var newModifier = SyntaxFactory.Token(modifier); var indexOfPartialKeyword = declaration.Modifiers.IndexOf(SyntaxKind.PartialKeyword); var newDeclaration = indexOfPartialKeyword < 0 ? declaration.AddModifiers(newModifier) : declaration.WithModifiers(declaration.Modifiers.Insert(indexOfPartialKeyword, newModifier)); @@ -142,16 +118,19 @@ private static Task AddTypeModifierAsync( private static Task ChangeAccessibilityAsync( Document document, SyntaxNode root, - MemberDeclarationSyntax declaration, - SyntaxKind accessbility) + MemberDeclarationSyntax? declaration, + SyntaxKind accessibility) { + if (declaration is null) + return Task.FromResult(document); + var firstModifier = declaration.Modifiers.FirstOrDefault(); var newModifiers = declaration.Modifiers; var isFirstModiferRemoved = false; foreach (var currentModifier in newModifiers) { - if (currentModifier.Kind() is SyntaxKind.PrivateKeyword or SyntaxKind.ProtectedKeyword or SyntaxKind.InternalKeyword or SyntaxKind.PublicKeyword && !currentModifier.IsKind(accessbility)) + if (currentModifier.Kind() is SyntaxKind.PrivateKeyword or SyntaxKind.ProtectedKeyword or SyntaxKind.InternalKeyword or SyntaxKind.PublicKeyword && !currentModifier.IsKind(accessibility)) { newModifiers = newModifiers.Remove(currentModifier); @@ -163,7 +142,7 @@ private static Task ChangeAccessibilityAsync( if (!isFirstModiferRemoved && firstModifier.HasLeadingTrivia) newModifiers = newModifiers.RemoveAt(0).Insert(0, firstModifier.WithLeadingTrivia(SyntaxTriviaList.Empty)); - var publicSyntax = SyntaxFactory.Token(accessbility); + var publicSyntax = SyntaxFactory.Token(accessibility); if (firstModifier.HasLeadingTrivia) publicSyntax = publicSyntax.WithLeadingTrivia(declaration.Modifiers.FirstOrDefault().LeadingTrivia); @@ -183,8 +162,11 @@ private static Task ChangeAccessibilityAsync( private static Task RemovePropertySetterAsync( Document document, SyntaxNode root, - PropertyDeclarationSyntax declaration) + PropertyDeclarationSyntax? declaration) { + if (declaration is null) + return Task.FromResult(document); + var setter = declaration.AccessorList?.Accessors.FirstOrDefault(a => a.IsKind(SyntaxKind.SetAccessorDeclaration)); if (setter is not null) @@ -201,12 +183,15 @@ private static Task RemovePropertySetterAsync( return Task.FromResult(document); } - private async Task AddCreateInvalidItemAsync( + private static async Task AddCreateInvalidItemAsync( Document document, SyntaxNode root, - TypeDeclarationSyntax declaration, + TypeDeclarationSyntax? declaration, CancellationToken cancellationToken) { + if (declaration is null) + return document; + var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); if (model is null) @@ -246,5 +231,33 @@ private static ThrowStatementSyntax BuildThrowNotImplementedException() var throwStatement = SyntaxFactory.ThrowStatement(newNotImplementedException); return throwStatement; } + + private class CodeFixesContext + { + private readonly CodeFixContext _context; + private readonly SyntaxNode _root; + + private TypeDeclarationSyntax? _typeDeclaration; + public TypeDeclarationSyntax? TypeDeclaration => _typeDeclaration ??= GetDeclaration(); + + private FieldDeclarationSyntax? _fieldDeclaration; + public FieldDeclarationSyntax? FieldDeclaration => _fieldDeclaration ??= GetDeclaration(); + + private PropertyDeclarationSyntax? _propertyDeclaration; + public PropertyDeclarationSyntax? PropertyDeclaration => _propertyDeclaration ??= GetDeclaration(); + + public CodeFixesContext(CodeFixContext context, SyntaxNode root) + { + _context = context; + _root = root; + } + + private T? GetDeclaration() + where T : MemberDeclarationSyntax + { + var diagnosticSpan = _context.Diagnostics.First().Location.SourceSpan; + return _root.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); + } + } } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DefaultSymbolState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DefaultSymbolState.cs new file mode 100644 index 00000000..78faa4fa --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DefaultSymbolState.cs @@ -0,0 +1,30 @@ +using Microsoft.CodeAnalysis; + +namespace Thinktecture.CodeAnalysis +{ + public class DefaultSymbolState : ISymbolState + { + public string Identifier { get; } + public ITypeSymbol Type { get; } + public string ArgumentName { get; } + public bool IsStatic { get; } + + public DefaultSymbolState(string identifier, ITypeSymbol type, string argumentName, bool isStatic) + { + Identifier = identifier; + Type = type; + ArgumentName = argumentName; + IsStatic = isStatic; + } + + public static DefaultSymbolState CreateFrom(IFieldSymbol field) + { + return new(field.Name, field.Type, field.Name.MakeArgumentName(), field.IsStatic); + } + + public static DefaultSymbolState CreateFrom(IPropertySymbol property) + { + return new(property.Name, property.Type, property.Name.MakeArgumentName(), property.IsStatic); + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs index a47efaf4..6a1d013c 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs @@ -30,7 +30,21 @@ public class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer DiagnosticsDescriptors.FirstLevelInnerTypeMustBePrivate, DiagnosticsDescriptors.NonFirstLevelInnerTypeMustBePublic, DiagnosticsDescriptors.TypeCannotBeNestedClass, - DiagnosticsDescriptors.KeyMemberShouldNotBeNullable); + DiagnosticsDescriptors.KeyMemberShouldNotBeNullable, + DiagnosticsDescriptors.ExtensibleMemberMustBePublicOrHaveMapping, + DiagnosticsDescriptors.MemberNotFound, + DiagnosticsDescriptors.MultipleMembersWithSameName, + DiagnosticsDescriptors.MappedMemberMustBePublic, + DiagnosticsDescriptors.MappedMethodMustBeNotBeGeneric, + DiagnosticsDescriptors.EnumAndBaseClassMustBeBothEnumOrValidatableEnum, + DiagnosticsDescriptors.DerivedEnumMustNotBeExtensible, + DiagnosticsDescriptors.BaseEnumMustBeExtensible, + DiagnosticsDescriptors.ExtensibleEnumCannotBeStruct, + DiagnosticsDescriptors.ExtensibleEnumCannotBeAbstract, + DiagnosticsDescriptors.ExtensibleEnumMustNotHaveVirtualMembers, + DiagnosticsDescriptors.StaticPropertiesAreNotConsideredItems, + DiagnosticsDescriptors.KeyComparerMustBeStaticFieldOrProperty, + DiagnosticsDescriptors.KeyComparerOfExtensibleEnumMustBeProtectedOrPublic); /// public override void Initialize(AnalysisContext context) @@ -129,21 +143,232 @@ private static void ValidateEnum(SymbolAnalysisContext context, SyntaxNode decla if (items.Count == 0) context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.NoItemsWarning, tds.Identifier.GetLocation(), tds.Identifier)); + Check_ItemLike_StaticProperties(context, enumType); FieldsMustBePublic(context, enumType, items); if (isValidatable) ValidateCreateInvalidItem(context, tds, enumType, validEnumInterface); - enumType.GetAssignableFieldsAndPropertiesAndCheckForReadOnly(false, context.ReportDiagnostic); + var assignableMembers = enumType.GetAssignableFieldsAndPropertiesAndCheckForReadOnly(false, context.ReportDiagnostic); - var enumSettingsAttr = enumType.FindEnumGenerationAttribute(); + var hasBaseEnum = ValidateBaseEnum(context, tds, enumType, isValidatable); - if (enumSettingsAttr is not null) - EnumKeyPropertyNameMustNotBeItem(context, tds, enumSettingsAttr); + var enumAttr = enumType.FindEnumGenerationAttribute(); + + if (enumAttr is not null) + { + EnumKeyPropertyNameMustNotBeItem(context, tds, enumAttr); + + var comparer = enumAttr.FindKeyComparer(); + var comparerMembers = comparer is null ? (IReadOnlyList)Array.Empty() : enumType.GetMembers(comparer); + + var isExtensible = enumAttr.IsExtensible() ?? false; + + CheckKeyComparer(context, comparerMembers, isExtensible); + + if (isExtensible) + { + AssignableMembersMustBePublicOrBeMapped(context, tds, assignableMembers); + InstanceMembersMustNotBeVirtual(context, tds); + + if (hasBaseEnum) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.DerivedEnumMustNotBeExtensible, + enumAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? tds.Identifier.GetLocation(), + tds.Identifier)); + } + + if (enumType.IsValueType) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.ExtensibleEnumCannotBeStruct, + enumAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? tds.Identifier.GetLocation(), + tds.Identifier)); + } + + if (enumType.IsAbstract) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.ExtensibleEnumCannotBeAbstract, + enumAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? tds.Identifier.GetLocation(), + tds.Identifier)); + } + } + } ValidateDerivedTypes(context, enumType); } + private static void CheckKeyComparer(SymbolAnalysisContext context, IReadOnlyList comparerMembers, bool isExtensible) + { + foreach (var comparerMember in comparerMembers) + { + switch (comparerMember) + { + case IFieldSymbol field: + + if (!field.IsStatic) + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.KeyComparerMustBeStaticFieldOrProperty, field.GetIdentifier().GetLocation(), field.Name)); + + if (isExtensible && !field.DeclaredAccessibility.IsAtLeastProtected()) + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.KeyComparerOfExtensibleEnumMustBeProtectedOrPublic, field.GetIdentifier().GetLocation(), field.Name)); + + break; + + case IPropertySymbol property: + + if (!property.IsStatic) + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.KeyComparerMustBeStaticFieldOrProperty, property.GetIdentifier().GetLocation(), property.Name)); + + if (isExtensible && !property.DeclaredAccessibility.IsAtLeastProtected()) + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.KeyComparerOfExtensibleEnumMustBeProtectedOrPublic, property.GetIdentifier().GetLocation(), property.Name)); + + break; + + default: + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.KeyComparerMustBeStaticFieldOrProperty, comparerMember.DeclaringSyntaxReferences.First().GetSyntax().GetLocation(), comparerMember.Name)); + break; + } + } + } + + private static void Check_ItemLike_StaticProperties(SymbolAnalysisContext context, INamedTypeSymbol enumType) + { + foreach (var member in enumType.GetMembers()) + { + if (member.IsStatic && member is IPropertySymbol property && SymbolEqualityComparer.Default.Equals(property.Type, enumType)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.StaticPropertiesAreNotConsideredItems, property.GetIdentifier().GetLocation(), property.Name)); + } + } + } + + private static void InstanceMembersMustNotBeVirtual(SymbolAnalysisContext context, TypeDeclarationSyntax tds) + { + foreach (var member in tds.Members) + { + var virtualKeyword = member.Modifiers.FirstOrDefault(m => m.IsKind(SyntaxKind.VirtualKeyword)); + + if (virtualKeyword != default) + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.ExtensibleEnumMustNotHaveVirtualMembers, virtualKeyword.GetLocation(), tds.Identifier)); + } + } + + private static bool ValidateBaseEnum( + SymbolAnalysisContext context, + TypeDeclarationSyntax enumDeclaration, + INamedTypeSymbol enumType, + bool isValidatable) + { + if (enumType.BaseType is null) + return false; + + if (enumType.ContainingType is not null) // inner enum + return false; + + if (!enumType.BaseType.IsEnum(out var baseEnumInterfaces)) + return false; + + var isBaseEnumValidatable = baseEnumInterfaces.GetValidEnumInterface(enumType.BaseType)?.IsValidatableEnumInterface() ?? false; + + if (isValidatable != isBaseEnumValidatable) + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.EnumAndBaseClassMustBeBothEnumOrValidatableEnum, enumDeclaration.Identifier.GetLocation(), enumDeclaration.Identifier)); + + var baseEnumAttr = enumType.BaseType.FindEnumGenerationAttribute(); + var isBaseEnumExtensible = baseEnumAttr?.IsExtensible() ?? false; + + if (!isBaseEnumExtensible) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.BaseEnumMustBeExtensible, + enumDeclaration.Identifier.GetLocation(), + enumType.BaseType.Name)); + } + + return true; + } + + private static void AssignableMembersMustBePublicOrBeMapped( + SymbolAnalysisContext context, + TypeDeclarationSyntax enumDeclaration, + IReadOnlyList assignableMembers) + { + foreach (var memberInfo in assignableMembers) + { + if (memberInfo.ReadAccessibility == Accessibility.Public || memberInfo.IsStatic) + continue; + + if (HasMemberMapping(context, memberInfo, enumDeclaration)) + continue; + + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.ExtensibleMemberMustBePublicOrHaveMapping, + memberInfo.Identifier.GetLocation(), + memberInfo.Identifier)); + } + } + + private static bool HasMemberMapping( + SymbolAnalysisContext context, + InstanceMemberInfo memberInfo, + TypeDeclarationSyntax enumDeclaration) + { + var enumMemberAttr = memberInfo.Symbol.FindEnumGenerationMemberAttribute(); + + if (enumMemberAttr is null) + return false; + + var mappedMemberName = enumMemberAttr.FindMapsToMember(); + + if (mappedMemberName is null) + return false; + + var mappedMembers = enumDeclaration.Members.Where(m => + { + if (m is FieldDeclarationSyntax field) + return field.Declaration.Variables[0].Identifier.ToString() == mappedMemberName; + + if (m is PropertyDeclarationSyntax property) + return property.Identifier.ToString() == mappedMemberName; + + if (m is MethodDeclarationSyntax method) + return method.Identifier.ToString() == mappedMemberName; + + return false; + }) + .ToList(); + + if (mappedMembers.Count == 0) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.MemberNotFound, + enumMemberAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? memberInfo.Identifier.GetLocation(), + mappedMemberName)); + } + else if (mappedMembers.Count > 2) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.MultipleMembersWithSameName, + enumMemberAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? memberInfo.Identifier.GetLocation(), + mappedMemberName)); + } + else + { + var mappedMember = mappedMembers[0]; + + if (!mappedMember.Modifiers.Any(SyntaxKind.PublicKeyword)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.MappedMemberMustBePublic, + enumMemberAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? memberInfo.Identifier.GetLocation(), + mappedMemberName)); + } + + if (mappedMember is MethodDeclarationSyntax method && + method.TypeParameterList?.Parameters.Count > 0) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.MappedMethodMustBeNotBeGeneric, + enumMemberAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? memberInfo.Identifier.GetLocation(), + mappedMemberName)); + } + } + + return true; + } + private static void ValidateDerivedTypes(SymbolAnalysisContext context, INamedTypeSymbol enumType) { var derivedTypes = enumType.FindDerivedInnerTypes(); diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs index e0b50a18..09b6e614 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs @@ -22,7 +22,23 @@ internal static class DiagnosticsDescriptors public static readonly DiagnosticDescriptor NonFirstLevelInnerTypeMustBePublic = new("TTRESG015", "Non-first-level inner types must be public", "Derived type '{0}' 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 should not be nullable", "The key member '{0}' should not be nullable", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor ExtensibleMemberMustBePublicOrHaveMapping = new("TTRESG018", "The members of an extensible enumeration must be public or be mapped to another public member with same value/behavior.", "The member '{0}' of an extensible enumeration must be public or be mapped to another public member with same value/behavior (use [EnumGenerationMember(MapsToMember = nameof(PublicMember))] for mapping).", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor MemberNotFound = new("TTRESG019", "Member not found.", "The member with the name '{0}' not found.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor MultipleMembersWithSameName = new("TTRESG020", "Multiple member with same name found.", "Multiple members with the name '{0}' found.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor MappedMemberMustBePublic = new("TTRESG021", "Mapped member must be public.", "Mapped member with the name '{0}' must be public.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor MappedMethodMustBeNotBeGeneric = new("TTRESG022", "Mapped method must not be generic.", "Mapped method with the name '{0}' must not be generic.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor EnumAndBaseClassMustBeBothEnumOrValidatableEnum = new("TTRESG023", "Type and the base class must be either IEnum or IValidatableEnum but not both.", "Type '{0}' and the base class must be either IEnum or IValidatableEnum but not both.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor DerivedEnumMustNotBeExtensible = new("TTRESG024", "Derived enumeration must not be extensible.", "Derived enumeration '{0}' cannot be extensible.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor BaseEnumMustBeExtensible = new("TTRESG025", "Base enumeration must not extensible.", "Base enumeration '{0}' must be extensible to be able to extend it, i.e. to derive from it.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor ExtensibleEnumCannotBeStruct = new("TTRESG026", "An extensible enumeration cannot be a struct.", "A extensible enumeration '{0}' cannot be a struct.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor ExtensibleEnumCannotBeAbstract = new("TTRESG027", "An extensible enumeration cannot be a abstract.", "A extensible enumeration '{0}' cannot be abstract.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor ExtensibleEnumMustNotHaveVirtualMembers = new("TTRESG028", "An extensible enumeration must not have virtual members.", "A extensible enumeration '{0}' must not have virtual members.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor KeyComparerOfExtensibleEnumMustBeProtectedOrPublic = new("TTRESG029", "The key comparer of an extensible enumeration must be protected or public.", "The key comparer '{0}' of an extensible enumeration must be protected or public.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor KeyComparerMustBeStaticFieldOrProperty = new("TTRESG030", "The key comparer must a static field or property.", "The key comparer '{0}' must be a static field or property.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ErrorDuringGeneration = new("TTRESG099", "Error during code generation.", "Error during code generation for '{0}': '{1}'.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); public static readonly DiagnosticDescriptor NoItemsWarning = new("TTRESG100", "The enumeration has no items", "The enumeration '{0}' has no items", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true); + public static readonly DiagnosticDescriptor StaticPropertiesAreNotConsideredItems = new("TTRESG101", "Static properties are not considered enumeration items, use a field instead.", "The static property '{0}' is not considered a enumeration item, use a field instead.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true); } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EnumSourceGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EnumSourceGenerator.cs index b2c19907..3ce80b50 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EnumSourceGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EnumSourceGenerator.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text; namespace Thinktecture.CodeAnalysis @@ -43,15 +45,31 @@ private void GenerateEnum() { var derivedTypes = _state.EnumType.FindDerivedInnerTypes(); var needCreateInvalidImplementation = _state.IsValidatable && !_state.EnumType.HasCreateInvalidImplementation(_state.KeyType); + var newKeyword = _state.HasBaseEnum ? "new " : null; _sb.GenerateStructLayoutAttributeIfRequired(_state.EnumType); + if (_state.IsExtensible) + { + _sb.Append($@" + [Thinktecture.EnumConstructor(nameof({_state.KeyPropertyName})"); + + foreach (var member in _state.AssignableInstanceFieldsAndProperties) + { + var memberName = member.Symbol.FindEnumGenerationMemberAttribute()?.FindMapsToMember() ?? member.Identifier.ToString(); + + _sb.Append($@", nameof({memberName})"); + } + + _sb.Append(")]"); + } + _sb.Append($@" [System.ComponentModel.TypeConverter(typeof({_state.EnumIdentifier}_EnumTypeConverter))] partial {(_state.EnumType.IsValueType ? "struct" : "class")} {_state.EnumIdentifier} : IEquatable<{_state.EnumIdentifier}{_state.NullableQuestionMarkEnum}> {{ [System.Runtime.CompilerServices.ModuleInitializer] - internal static void ModuleInit() + internal {(_state.HasBaseEnum && _state.BaseEnum.IsSameAssembly ? newKeyword : null)}static void ModuleInit() {{ var convertFromKey = new Func<{_state.KeyType}{_state.NullableQuestionMarkKey}, {_state.EnumIdentifier}{_state.NullableQuestionMarkEnum}>({_state.EnumIdentifier}.Get); Expression> convertFromKeyExpression = {_state.KeyArgumentName} => {_state.EnumIdentifier}.Get({_state.KeyArgumentName}); @@ -82,7 +100,7 @@ internal static void ModuleInit() var defaultComparer = _state.KeyType.IsString() ? "StringComparer.OrdinalIgnoreCase" : $"EqualityComparer<{_state.KeyType}>.Default"; _sb.Append($@" - private static readonly IEqualityComparer<{_state.KeyType}{_state.NullableQuestionMarkKey}> _defaultKeyComparerMember = {defaultComparer};"); + {(_state.IsExtensible ? "protected" : "private")} static readonly IEqualityComparer<{_state.KeyType}{_state.NullableQuestionMarkKey}> _defaultKeyComparerMember = {defaultComparer};"); } _sb.Append($@" @@ -95,7 +113,11 @@ internal static void ModuleInit() /// /// Gets all valid items. /// - public static IReadOnlyList<{_state.EnumIdentifier}> Items => _items ??= ItemsLookup.Values.ToList().AsReadOnly(); + public {newKeyword}static IReadOnlyList<{_state.EnumIdentifier}> Items => _items ??= ItemsLookup.Values.ToList().AsReadOnly();"); + + if (!_state.HasBaseEnum) + { + _sb.Append($@" /// /// The identifier of the item. @@ -103,19 +125,26 @@ internal static void ModuleInit() [NotNull] public {_state.KeyType} {_state.KeyPropertyName} {{ get; }}"); - if (_state.IsValidatable) - { - _sb.Append($@" + if (_state.IsValidatable) + { + _sb.Append($@" /// public bool IsValid {{ get; }}"); - GenerateEnsureValid(); + GenerateEnsureValid(); + } } + if (_state.HasBaseEnum) + GenerateBaseItems(_state.BaseEnum); + GenerateConstructors(); - GenerateGetKey(); - GeneratedGet(needCreateInvalidImplementation); + + if (!_state.HasBaseEnum) + GenerateGetKey(); + + GenerateGet(needCreateInvalidImplementation); if (needCreateInvalidImplementation && !_state.EnumType.IsAbstract) GenerateCreateInvalidItem(); @@ -139,17 +168,48 @@ public override bool Equals(object? other) public override int GetHashCode() {{ return _typeHashCode ^ {_state.KeyComparerMember}.GetHashCode(this.{_state.KeyPropertyName}); - }} + }}"); + + if (!_state.HasBaseEnum) + { + _sb.Append($@" /// public override string? ToString() {{ return this.{_state.KeyPropertyName}{(_state.EnumType.IsValueType && _state.KeyType.IsReferenceType ? "?" : null)}.ToString(); }}"); + } GenerateGetLookup(); } + private void GenerateBaseItems(IBaseEnumState baseEnum) + { + if(baseEnum.Items.Count == 0) + return; + + _sb.Append(@" +"); + + foreach (var item in baseEnum.Items) + { + _sb.Append($@" + public new static readonly {_state.EnumType} {item.Identifier} = new {_state.EnumType}("); + + for (var i = 0; i < baseEnum.ConstructorArguments.Count; i++) + { + if (i > 0) + _sb.Append($@", "); + + var arg = baseEnum.ConstructorArguments[i]; + _sb.Append($@"{baseEnum.Type}.{item.Identifier}.{arg.Identifier}"); + } + + _sb.Append($@");"); + } + } + private void GenerateTryGet() { _sb.Append($@" @@ -242,7 +302,7 @@ private void GenerateImplicitConversion() /// 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: NotNullIfNotNull(""item"")] public static implicit operator {_state.KeyType}{_state.NullableQuestionMarkKey}({_state.EnumIdentifier}{_state.NullableQuestionMarkEnum} item) {{"); @@ -360,10 +420,19 @@ void AddItem({_state.EnumIdentifier} item, string itemName) }} "); + if (_state.HasBaseEnum) + { + foreach (var item in _state.BaseEnum.Items) + { + _sb.Append($@" + AddItem({item.Identifier}, nameof({item.Identifier}));"); + } + } + foreach (var item in _state.Items) { _sb.Append($@" - AddItem({item.Name}, ""{item.Name}"");"); + AddItem({item.Name}, nameof({item.Name}));"); } } @@ -385,7 +454,7 @@ private void GenerateEnsureValid() public void EnsureValid() {{ if (!IsValid) - throw new InvalidOperationException($""The current enumeration item of type '{_state.EnumIdentifier}' with identifier '{{this.{_state.KeyPropertyName}}}' is not valid.""); + throw new InvalidOperationException($""The current enumeration item of type '{_state.RuntimeTypeName}' with identifier '{{this.{_state.KeyPropertyName}}}' is not valid.""); }}"); } @@ -402,7 +471,7 @@ private void GenerateGetKey() }}"); } - private void GeneratedGet(bool needCreateInvalidImplementation) + private void GenerateGet(bool needCreateInvalidImplementation) { _sb.Append($@" @@ -420,7 +489,7 @@ private void GeneratedGet(bool needCreateInvalidImplementation) _sb.Append($@" [return: NotNullIfNotNull(""{_state.KeyArgumentName}"")] - public static {_state.EnumIdentifier}{(_state.KeyType.IsReferenceType ? _state.NullableQuestionMarkEnum : null)} Get({_state.KeyType}{_state.NullableQuestionMarkKey} {_state.KeyArgumentName}) + public {(_state.HasBaseEnum ? "new " : null)}static {_state.EnumIdentifier}{(_state.KeyType.IsReferenceType ? _state.NullableQuestionMarkEnum : null)} Get({_state.KeyType}{_state.NullableQuestionMarkKey} {_state.KeyArgumentName}) {{"); if (_state.KeyType.IsReferenceType) @@ -482,29 +551,43 @@ private void GenerateCreateInvalidItem() _sb.Append('!'); } + if (_state.HasBaseEnum) + { + foreach (var arg in _state.BaseEnum.ConstructorArguments.Skip(1)) + { + _sb.Append(", default"); + + if (arg.Type.IsReferenceType) + _sb.Append('!'); + } + } + _sb.Append($@"); }}"); } private void GenerateConstructors() { - var fieldsAndProperties = _state.AssignableInstanceFieldsAndProperties; + var baseCtorArgs = _state.BaseEnum?.ConstructorArguments.Skip(1) ?? Array.Empty(); + var ctorArgs = _state.AssignableInstanceFieldsAndProperties + .Concat(baseCtorArgs); + var accessibilityModifier = _state.IsExtensible ? "protected" : "private"; if (_state.IsValidatable) { _sb.Append($@" - private {_state.EnumIdentifier}({_state.KeyType} {_state.KeyArgumentName}"); + {accessibilityModifier} {_state.EnumIdentifier}({_state.KeyType} {_state.KeyArgumentName}"); - foreach (var members in fieldsAndProperties) + foreach (var member in ctorArgs) { - _sb.Append($@", {members.Type} {members.ArgumentName}"); + _sb.Append($@", {member.Type} {member.ArgumentName}"); } _sb.Append($@") : this({_state.KeyArgumentName}, true"); - foreach (var members in fieldsAndProperties) + foreach (var members in ctorArgs) { _sb.Append($@", {members.ArgumentName}"); } @@ -516,20 +599,38 @@ private void GenerateConstructors() _sb.Append($@" - private {_state.EnumIdentifier}({_state.KeyType} {_state.KeyArgumentName}"); + {accessibilityModifier} {_state.EnumIdentifier}({_state.KeyType} {_state.KeyArgumentName}"); if (_state.IsValidatable) _sb.Append(", bool isValid"); - foreach (var members in fieldsAndProperties) + foreach (var member in ctorArgs) + { + _sb.Append($@", {member.Type} {member.ArgumentName}"); + } + + _sb.Append($@")"); + + if (_state.HasBaseEnum) { - _sb.Append($@", {members.Type} {members.ArgumentName}"); + _sb.Append($@" + : base({_state.KeyArgumentName}"); + + if (_state.IsValidatable) + _sb.Append($@", isValid"); + + foreach (var baseArg in baseCtorArgs) + { + _sb.Append($@", {baseArg.ArgumentName}"); + } + + _sb.Append($@")"); } - _sb.Append($@") + _sb.Append($@" {{"); - if (_state.KeyType.IsReferenceType) + if (!_state.HasBaseEnum && _state.KeyType.IsReferenceType) { _sb.Append($@" if ({_state.KeyArgumentName} is null) @@ -543,38 +644,41 @@ private void GenerateConstructors() if (_state.IsValidatable) _sb.Append(", isValid"); - foreach (var members in fieldsAndProperties) + foreach (var members in ctorArgs) { _sb.Append($@", ref {members.ArgumentName}"); } _sb.Append($@"); +"); + if (!_state.HasBaseEnum) + { + _sb.Append($@" this.{_state.KeyPropertyName} = {_state.KeyArgumentName};"); - if (_state.IsValidatable) - { - _sb.Append(@" + if (_state.IsValidatable) + { + _sb.Append(@" this.IsValid = isValid;"); + } } - foreach (var memberInfo in fieldsAndProperties) + foreach (var memberInfo in _state.AssignableInstanceFieldsAndProperties) { _sb.Append($@" this.{memberInfo.Identifier} = {memberInfo.ArgumentName};"); } _sb.Append($@" - }}"); - - _sb.Append($@" + }} static partial void ValidateConstructorArguments({_state.KeyType} {_state.KeyArgumentName}"); if (_state.IsValidatable) _sb.Append(", bool isValid"); - foreach (var members in fieldsAndProperties) + foreach (var members in ctorArgs) { _sb.Append($@", ref {members.Type} {members.ArgumentName}"); } @@ -611,7 +715,7 @@ public class {_state.EnumIdentifier}_EnumTypeConverter : Thinktecture.ValueTypeC if({_state.EnumIdentifier}.TryGet({_state.KeyArgumentName}, out var item)) return item; - throw new FormatException($""There is no item of type '{_state.EnumIdentifier}' with the identifier '{{{_state.KeyArgumentName}}}'."");"); + throw new FormatException($""There is no item of type '{_state.RuntimeTypeName}' with the identifier '{{{_state.KeyArgumentName}}}'."");"); } _sb.Append($@" diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EnumSourceGeneratorState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EnumSourceGeneratorState.cs index 29716bb4..86860386 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EnumSourceGeneratorState.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EnumSourceGeneratorState.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -8,21 +9,43 @@ namespace Thinktecture.CodeAnalysis { public class EnumSourceGeneratorState { - private readonly TypeDeclarationSyntax _declaration; - + public TypeDeclarationSyntax Declaration { get; } public SemanticModel Model { get; } public string? Namespace { get; } public INamedTypeSymbol EnumType { get; } public ITypeSymbol KeyType { get; } - public SyntaxToken EnumIdentifier => _declaration.Identifier; + public SyntaxToken EnumIdentifier => Declaration.Identifier; + + public string RuntimeTypeName { get; } + + public string KeyPropertyName { get; private set; } + public string KeyArgumentName { get; private set; } + public string KeyComparerMember { get; private set; } + public bool NeedsDefaultComparer { get; private set; } + public bool IsExtensible { get; private set; } - public string KeyPropertyName { get; } - public string KeyArgumentName { get; } - public string KeyComparerMember { get; } - public bool NeedsDefaultComparer { get; } public bool IsValidatable { get; } + private bool _isBaseItemDetermined; + private IBaseEnumState? _baseEnum; + + public IBaseEnumState? BaseEnum + { + get + { + if (_isBaseItemDetermined) + return _baseEnum; + + DetermineBaseEnum(); + + return _baseEnum; + } + } + + [MemberNotNullWhen(true, nameof(BaseEnum))] + public bool HasBaseEnum => BaseEnum is not null; + public string? NullableQuestionMarkEnum { get; } public string? NullableQuestionMarkKey { get; } @@ -43,7 +66,7 @@ public EnumSourceGeneratorState( Model = model ?? throw new ArgumentNullException(nameof(model)); - _declaration = enumDeclaration ?? throw new ArgumentNullException(nameof(enumDeclaration)); + Declaration = enumDeclaration ?? throw new ArgumentNullException(nameof(enumDeclaration)); EnumType = enumType ?? throw new ArgumentNullException(nameof(enumType)); Namespace = enumType.ContainingNamespace.ToString(); @@ -55,10 +78,19 @@ public EnumSourceGeneratorState( var enumSettings = enumType.FindEnumGenerationAttribute(); + InitializeFromSettings(enumSettings, false); + IsExtensible = enumType.IsReferenceType && (enumSettings?.IsExtensible() ?? false); + + RuntimeTypeName = IsExtensible ? "{GetType().Name}" : Declaration.Identifier.ToString(); + } + + [MemberNotNull(nameof(KeyComparerMember), nameof(KeyPropertyName), nameof(KeyArgumentName))] + private void InitializeFromSettings(AttributeData? enumSettings, bool isFromBaseEnum) + { KeyComparerMember = GetKeyComparerMember(enumSettings, out var needsDefaultComparer); KeyPropertyName = GetKeyPropertyName(enumSettings); KeyArgumentName = KeyPropertyName.MakeArgumentName(); - NeedsDefaultComparer = needsDefaultComparer; + NeedsDefaultComparer = !isFromBaseEnum && needsDefaultComparer; } private static string GetKeyComparerMember(AttributeData? enumSettingsAttribute, out bool needsDefaultComparer) @@ -81,5 +113,42 @@ private static string GetKeyPropertyName(AttributeData? enumSettingsAttribute) return "Key"; } + + public void SetBaseType(EnumSourceGeneratorState other) + { + if (_baseEnum is SameAssemblyBaseEnumState) + return; + + SetBaseTypeInternal(new SameAssemblyBaseEnumState(other)); + } + + private void DetermineBaseEnum() + { + _isBaseItemDetermined = true; + + if (EnumType.BaseType is null) + return; + + if (!EnumType.BaseType.IsEnum(out var enumInterfaces)) + return; + + var baseInterface = enumInterfaces.GetValidEnumInterface(EnumType.BaseType); + + if (baseInterface is null) + return; + + SetBaseTypeInternal(new BaseEnumState(EnumType.BaseType)); + } + + private void SetBaseTypeInternal(IBaseEnumState other) + { + _baseEnum = other; + _isBaseItemDetermined = true; + + var enumSettings = other.Type.FindEnumGenerationAttribute(); + InitializeFromSettings(enumSettings, true); + + IsExtensible = false; + } } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/IBaseEnumState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/IBaseEnumState.cs new file mode 100644 index 00000000..372a4064 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/IBaseEnumState.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Thinktecture.CodeAnalysis +{ + public interface IBaseEnumState + { + bool IsSameAssembly { get; } + INamedTypeSymbol Type { get; } + string? NullableQuestionMark { get; } + IReadOnlyList Items { get; } + IReadOnlyList ConstructorArguments { get; } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ISymbolState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ISymbolState.cs new file mode 100644 index 00000000..9b317a36 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ISymbolState.cs @@ -0,0 +1,12 @@ +using Microsoft.CodeAnalysis; + +namespace Thinktecture.CodeAnalysis +{ + public interface ISymbolState + { + string Identifier { get; } + ITypeSymbol Type { get; } + string ArgumentName { get; } + bool IsStatic { get; } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/InstanceMemberInfo.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/InstanceMemberInfo.cs index 4dd308d1..345bfc98 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/InstanceMemberInfo.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/InstanceMemberInfo.cs @@ -2,22 +2,43 @@ namespace Thinktecture.CodeAnalysis { - public class InstanceMemberInfo + public class InstanceMemberInfo : ISymbolState { public ISymbol Symbol { get; } public ITypeSymbol Type { get; } public SyntaxToken Identifier { get; } + public Accessibility ReadAccessibility { get; } public string ArgumentName { get; } + public bool IsStatic { get; } public bool IsReferenceTypeOrNullableStruct => Type.IsReferenceType || Type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; public bool IsNullableStruct => Type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; public string? NullableQuestionMark => Type.IsReferenceType ? "?" : null; - public InstanceMemberInfo(ISymbol symbol, ITypeSymbol type, SyntaxToken identifier) + string ISymbolState.Identifier => Identifier.ToString(); + + public InstanceMemberInfo( + ISymbol symbol, + ITypeSymbol type, + SyntaxToken identifier, + Accessibility readAccessibility, + bool isStatic) { Symbol = symbol; Type = type; Identifier = identifier; + ReadAccessibility = readAccessibility; + IsStatic = isStatic; ArgumentName = identifier.Text.MakeArgumentName(); } + + public static InstanceMemberInfo CreateFrom(IFieldSymbol isStatic) + { + return new(isStatic, isStatic.Type, isStatic.GetIdentifier(), isStatic.DeclaredAccessibility, isStatic.IsStatic); + } + + public static InstanceMemberInfo CreateFrom(IPropertySymbol property) + { + return new(property, property.Type, property.GetIdentifier(), property.DeclaredAccessibility, property.IsStatic); + } } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SameAssemblyBaseEnumState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SameAssemblyBaseEnumState.cs new file mode 100644 index 00000000..843f2a9f --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SameAssemblyBaseEnumState.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using System.Linq; + +namespace Thinktecture.CodeAnalysis +{ + public class SameAssemblyBaseEnumState : IBaseEnumState + { + private readonly EnumSourceGeneratorState _baseState; + + public bool IsSameAssembly => true; + public INamedTypeSymbol Type => _baseState.EnumType; + public string? NullableQuestionMark => Type.IsReferenceType ? "?" : null; + + private IReadOnlyList? _ctorArgs; + + public IReadOnlyList ConstructorArguments + { + get + { + if (_ctorArgs is null) + { + var args = new List + { + new DefaultSymbolState(_baseState.KeyPropertyName, _baseState.KeyType, _baseState.KeyArgumentName, false) + }; + + foreach (var member in _baseState.AssignableInstanceFieldsAndProperties) + { + var memberAttr = member.Symbol.FindEnumGenerationMemberAttribute(); + var mappedMemberName = memberAttr?.FindMapsToMember(); + + if (mappedMemberName is not null) + { + args.Add(new DefaultSymbolState(mappedMemberName, member.Type, mappedMemberName.MakeArgumentName(), member.IsStatic)); + } + else + { + args.Add(member); + } + } + + _ctorArgs = args; + } + + return _ctorArgs; + } + } + + private IReadOnlyList? _items; + public IReadOnlyList Items => _items ??= Type.EnumerateEnumItems().Select(InstanceMemberInfo.CreateFrom).ToList(); + + public SameAssemblyBaseEnumState(EnumSourceGeneratorState baseState) + { + _baseState = baseState; + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ThinktectureRuntimeExtensionsSourceGeneratorBase.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ThinktectureRuntimeExtensionsSourceGeneratorBase.cs index a3839df6..41e89428 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ThinktectureRuntimeExtensionsSourceGeneratorBase.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ThinktectureRuntimeExtensionsSourceGeneratorBase.cs @@ -30,60 +30,116 @@ public void Execute(GeneratorExecutionContext context) { var receiver = (SyntaxReceiver)(context.SyntaxReceiver ?? throw new Exception($"Syntax receiver must be of type '{nameof(SyntaxReceiver)}' but found '{context.SyntaxReceiver?.GetType().Name}'.")); - foreach (var declaration in receiver.Enums) + foreach (var state in PrepareEnums(context.Compilation, receiver.Enums)) { - GenerateEnum(context, declaration); + try + { + var generatedCode = GenerateEnum(state); + EmitFile(context, state.Declaration, generatedCode); + } + catch (Exception ex) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.ErrorDuringGeneration, state.Declaration.GetLocation(), state.EnumIdentifier, ex.Message)); + } } foreach (var declaration in receiver.ValueTypes) { - GenerateValueType(context, declaration); + try + { + var generatedCode = GenerateValueType(context.Compilation, declaration); + EmitFile(context, declaration, generatedCode); + } + catch (Exception ex) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.ErrorDuringGeneration, declaration.GetLocation(), declaration.Identifier, ex.Message)); + } } } - private void GenerateValueType(GeneratorExecutionContext context, TypeDeclarationSyntax declaration) + private static IReadOnlyList PrepareEnums( + Compilation compilation, + IReadOnlyList enums) { - var model = context.Compilation.GetSemanticModel(declaration.SyntaxTree, true); + if (enums.Count == 0) + return Array.Empty(); + + var states = new List(); + + foreach (var tds in enums) + { + var state = GetEnumState(compilation, tds); + + if (state is not null) + states.Add(state); + } + + foreach (var state in states) + { + if (state.EnumType.BaseType is null || state.EnumType.BaseType.SpecialType == SpecialType.System_Object) + continue; + + var baseEnum = states.FirstOrDefault(s => SymbolEqualityComparer.Default.Equals(state.EnumType.BaseType, s.EnumType)); + + if (baseEnum is not null) + state.SetBaseType(baseEnum); + } + + states.Sort((state, other) => + { + if (SymbolEqualityComparer.Default.Equals(state.EnumType.BaseType, other.EnumType)) + return 1; + + if (SymbolEqualityComparer.Default.Equals(other.EnumType.BaseType, state.EnumType)) + return -1; + + if (state.EnumType.BaseType is null) + return other.EnumType.BaseType is null ? 0 : -1; + + return other.EnumType.BaseType is null ? 1 : 0; + }); + + return states; + } + + private string? GenerateValueType(Compilation compilation, TypeDeclarationSyntax declaration) + { + var model = compilation.GetSemanticModel(declaration.SyntaxTree, true); var type = model.GetDeclaredSymbol(declaration); if (type is null) - return; + return null; if (!type.HasValueTypeAttribute(out var valueTypeAttribute)) - return; + return null; if (type.ContainingType is not null) - return; + return null; var state = new ValueTypeSourceGeneratorState(model, declaration, type, valueTypeAttribute); - var generatedCode = GenerateValueType(state); - - EmitFile(context, declaration, generatedCode); + return GenerateValueType(state); } - private void GenerateEnum(GeneratorExecutionContext context, TypeDeclarationSyntax declaration) + private static EnumSourceGeneratorState? GetEnumState(Compilation compilation, TypeDeclarationSyntax declaration) { - var model = context.Compilation.GetSemanticModel(declaration.SyntaxTree, true); + var model = compilation.GetSemanticModel(declaration.SyntaxTree, true); var type = model.GetDeclaredSymbol(declaration); if (type is null) - return; + return null; if (!type.IsEnum(out var enumInterfaces)) - return; + return null; if (type.ContainingType is not null) - return; + return null; var enumInterface = enumInterfaces.GetValidEnumInterface(type); if (enumInterface is null) - return; - - var state = new EnumSourceGeneratorState(model, declaration, type, enumInterface); - var generatedCode = GenerateEnum(state); + return null; - EmitFile(context, declaration, generatedCode); + return new EnumSourceGeneratorState(model, declaration, type, enumInterface); } private void EmitFile( diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueTypeSourceGeneratorState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueTypeSourceGeneratorState.cs index 732b3ef4..1f2b95f9 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueTypeSourceGeneratorState.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueTypeSourceGeneratorState.cs @@ -61,8 +61,8 @@ private IReadOnlyList GetEqualityMembers() if (attribute is not null) { - var equalityComparer = attribute.FindEqualityComparer().TrimmAndNullify(); - var comparer = attribute.FindComparer().TrimmAndNullify(); + var equalityComparer = attribute.FindEqualityComparer().TrimAndNullify(); + var comparer = attribute.FindComparer().TrimAndNullify(); var equalityMember = new EqualityInstanceMemberInfo(member, equalityComparer, comparer); (equalityMembers ??= new List()).Add(equalityMember); @@ -71,5 +71,10 @@ private IReadOnlyList GetEqualityMembers() return equalityMembers ?? members.Select(m => new EqualityInstanceMemberInfo(m, null, null)).ToList(); } + + public void SetBaseType(ValueTypeSourceGeneratorState baseTypeState) + { + + } } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AccessibilityExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AccessibilityExtensions.cs new file mode 100644 index 00000000..58921d34 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AccessibilityExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; + +// ReSharper disable once CheckNamespace +namespace Thinktecture +{ + public static class AccessibilityExtensions + { + public static bool IsAtLeastProtected(this Accessibility accessibility) + { + return accessibility == Accessibility.Protected || + accessibility == Accessibility.ProtectedAndFriend || + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + accessibility == Accessibility.ProtectedAndInternal || + accessibility == Accessibility.Public; + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs index 6fba4f93..4ae9e2e7 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -27,6 +27,16 @@ public static class AttributeDataExtensions return GetStringParameterValue(attributeData, "KeyComparer"); } + public static string? FindMapsToMember(this AttributeData attributeData) + { + return GetStringParameterValue(attributeData, "MapsToMember"); + } + + public static bool? IsExtensible(this AttributeData attributeData) + { + return GetBooleanParameterValue(attributeData, "IsExtensible"); + } + public static bool? FindSkipFactoryMethods(this AttributeData attributeData) { return GetBooleanParameterValue(attributeData, "SkipFactoryMethods"); diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/FieldSymbolExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/FieldSymbolExtensions.cs index a1eaec80..1b563fc6 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/FieldSymbolExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/FieldSymbolExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -12,5 +13,17 @@ public static SyntaxToken GetIdentifier(this IFieldSymbol field) var syntax = (VariableDeclaratorSyntax)field.DeclaringSyntaxReferences.First().GetSyntax(); return syntax.Identifier; } + + public static bool IsPropertyBackingField(this IFieldSymbol field, [MaybeNullWhen(false)] out IPropertySymbol property) + { + if (field.AssociatedSymbol is IPropertySymbol prop) + { + property = prop; + return true; + } + + property = null; + return false; + } } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringExtensions.cs index 2162cdbf..e32a60d3 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringExtensions.cs @@ -7,10 +7,15 @@ public static class StringExtensions { public static string MakeArgumentName(this string name) { - return $"{Char.ToLowerInvariant(name[0])}{name.Substring(1)}"; + if (name.Length == 1) + return name.ToLowerInvariant(); + + return name.StartsWith("_", StringComparison.Ordinal) + ? $"{Char.ToLowerInvariant(name[1])}{name.Substring(2)}" + : $"{Char.ToLowerInvariant(name[0])}{name.Substring(1)}"; } - public static string? TrimmAndNullify(this string? text) + public static string? TrimAndNullify(this string? text) { if (String.IsNullOrWhiteSpace(text)) return null; diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/SymbolExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/SymbolExtensions.cs index 69faafdf..b6d81512 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/SymbolExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/SymbolExtensions.cs @@ -29,5 +29,15 @@ public static bool HasAttribute(this ISymbol symbol, string attributeType) { return symbol.FindAttribute("Thinktecture.ValueTypeEqualityMemberAttribute"); } + + public static AttributeData? FindEnumGenerationMemberAttribute(this ISymbol symbol) + { + return symbol.FindAttribute("Thinktecture.EnumGenerationMemberAttribute"); + } + + public static AttributeData? FindEnumConstructorAttribute(this ISymbol symbol) + { + return symbol.FindAttribute("Thinktecture.EnumConstructorAttribute"); + } } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeDeclarationSyntaxExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeDeclarationSyntaxExtensions.cs index ccd42755..b6346140 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeDeclarationSyntaxExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeDeclarationSyntaxExtensions.cs @@ -24,28 +24,37 @@ public static bool IsValueTypeCandidate(this TypeDeclarationSyntax tds) } private static bool CouldBeValueType(TypeSyntax? type) + { + var typeName = ExtractTypeName(type); + return typeName == "ValueTypeAttribute" || typeName == "ValueType"; + } + + private static string? ExtractTypeName(TypeSyntax? type) { while (type is not null) { switch (type) { case IdentifierNameSyntax ins: - return ins.Identifier.Text == "ValueTypeAttribute" || ins.Identifier.Text == "ValueType"; + return ins.Identifier.Text; case QualifiedNameSyntax qns: type = qns.Right; break; default: - return false; + return null; } } - return false; + return null; } public static bool IsEnumCandidate(this TypeDeclarationSyntax tds) { + if (tds.AttributeLists.Any(l => l.Attributes.Any(a => CouldBeEnumType(a.Name)))) + return true; + if (tds.BaseList?.Types.Count > 0) { foreach (var baseType in tds.BaseList.Types) @@ -58,6 +67,12 @@ public static bool IsEnumCandidate(this TypeDeclarationSyntax tds) return false; } + private static bool CouldBeEnumType(TypeSyntax? type) + { + var typeName = ExtractTypeName(type); + return typeName == "EnumGeneration" || typeName == "EnumGeneration"; + } + private static bool CouldBeEnumInterface(BaseTypeSyntax? baseType) { var type = baseType?.Type; diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs index ef66aa2d..56454b83 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs @@ -49,7 +49,29 @@ public static bool IsEnum(this ITypeSymbol enumType, out IReadOnlyList 0; + if (implementedInterfaces.Count > 0) + { + if (enumType.BaseType is not null) + { + foreach (var @interface in enumType.BaseType.AllInterfaces) + { + if (@interface.IsNonValidatableEnumInterface() || @interface.IsValidatableEnumInterface()) + implementedInterfaces.Add(@interface); + } + } + + return true; + } + + var enumGenerationAttr = enumType.FindEnumGenerationAttribute(); + + if (enumGenerationAttr is null) + return false; + + if (enumType.BaseType is null) + return false; + + return enumType.BaseType.IsEnum(out enumInterfaces); } public static INamedTypeSymbol? GetValidEnumInterface( @@ -106,11 +128,16 @@ public static bool IsValidatableEnumInterface(this ITypeSymbol type) } public static IReadOnlyList GetEnumItems(this ITypeSymbol enumType) + { + return enumType.EnumerateEnumItems().ToList()!; + } + + public static IEnumerable EnumerateEnumItems(this ITypeSymbol enumType) { return enumType.GetMembers() .Select(m => { - if (!m.IsStatic || m is not IFieldSymbol field) + if (!m.IsStatic || m is not IFieldSymbol field || field.IsPropertyBackingField(out _)) return null; if (SymbolEqualityComparer.Default.Equals(field.Type, enumType)) @@ -118,8 +145,7 @@ public static IReadOnlyList GetEnumItems(this ITypeSymbol enumType return null; }) - .Where(field => field is not null) - .ToList()!; + .Where(field => field is not null)!; } public static bool IsFormattable(this ITypeSymbol type) @@ -213,7 +239,7 @@ public static IReadOnlyList GetAssignableFieldsAndProperties type.Name)); } - return new InstanceMemberInfo(fds, fds.Type, identifier); + return InstanceMemberInfo.CreateFrom(fds); } if (m is IPropertySymbol pds) @@ -243,7 +269,7 @@ public static IReadOnlyList GetAssignableFieldsAndProperties type.Name)); } - return new InstanceMemberInfo(pds, pds.Type, identifier); + return InstanceMemberInfo.CreateFrom(pds); } return null; @@ -263,12 +289,12 @@ public static IReadOnlyList GetReadableInstanceFieldsAndProp switch (m) { case IFieldSymbol fds: - return new InstanceMemberInfo(fds, fds.Type, fds.GetIdentifier()); + return InstanceMemberInfo.CreateFrom(fds); case IPropertySymbol pds: { if (!pds.IsWriteOnly) - return new InstanceMemberInfo(pds, pds.Type, pds.GetIdentifier()); + return InstanceMemberInfo.CreateFrom(pds); break; } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/NullableAttributes.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/NullableAttributes.cs index ca062bc3..16b384a2 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/NullableAttributes.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/NullableAttributes.cs @@ -30,4 +30,20 @@ public MemberNotNullWhenAttribute(bool returnValue, params string[] members) Members = members; } } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + public string[] Members { get; } + + public MemberNotNullAttribute(string member) + { + Members = new[] { member }; + } + + public MemberNotNullAttribute(params string[] members) + { + Members = members; + } + } } diff --git a/src/Thinktecture.Runtime.Extensions/EnumConstructorAttribute.cs b/src/Thinktecture.Runtime.Extensions/EnumConstructorAttribute.cs new file mode 100644 index 00000000..d2ae218b --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions/EnumConstructorAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Thinktecture +{ + /// + /// For internal use only. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] + public class EnumConstructorAttribute : Attribute + { + /// + /// The names of the members of the constructor. + /// + public string[] Members { get; set; } + + /// + /// Initializes new instance of . + /// + /// Member names of the constructor. + public EnumConstructorAttribute(params string[] members) + { + Members = members; + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions/EnumGenerationAttribute.cs b/src/Thinktecture.Runtime.Extensions/EnumGenerationAttribute.cs index 0fe60a02..67d55e67 100644 --- a/src/Thinktecture.Runtime.Extensions/EnumGenerationAttribute.cs +++ b/src/Thinktecture.Runtime.Extensions/EnumGenerationAttribute.cs @@ -23,5 +23,11 @@ public string KeyPropertyName get => _keyPropertyName ?? "Key"; set => _keyPropertyName = value; } + + /// + /// Indication whether the enumeration should be to derive from. + /// This feature comes with multiple limitations, use it only if necessary! + /// + public bool IsExtensible { get; set; } } } diff --git a/src/Thinktecture.Runtime.Extensions/EnumGenerationMember.cs b/src/Thinktecture.Runtime.Extensions/EnumGenerationMember.cs new file mode 100644 index 00000000..d89d92a8 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions/EnumGenerationMember.cs @@ -0,0 +1,16 @@ +using System; + +namespace Thinktecture +{ + /// + /// Settings to be used by the enum source generator. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public class EnumGenerationMemberAttribute : Attribute + { + /// + /// The name of other member which is public and yields the same value/behavior as the current member. + /// + public string? MapsToMember { get; set; } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.AspNetCore.Tests/Thinktecture.Runtime.Extensions.AspNetCore.Tests.csproj b/test/Thinktecture.Runtime.Extensions.AspNetCore.Tests/Thinktecture.Runtime.Extensions.AspNetCore.Tests.csproj index 1f5f4137..6c18f008 100644 --- a/test/Thinktecture.Runtime.Extensions.AspNetCore.Tests/Thinktecture.Runtime.Extensions.AspNetCore.Tests.csproj +++ b/test/Thinktecture.Runtime.Extensions.AspNetCore.Tests/Thinktecture.Runtime.Extensions.AspNetCore.Tests.csproj @@ -4,12 +4,7 @@ - - - - - %(RecursiveDir)TestValueTypes/%(FileName)%(Extension) - + diff --git a/test/Thinktecture.Runtime.Extensions.Json.Tests/Thinktecture.Runtime.Extensions.Json.Tests.csproj b/test/Thinktecture.Runtime.Extensions.Json.Tests/Thinktecture.Runtime.Extensions.Json.Tests.csproj index 3dc36ad2..d75c1c49 100644 --- a/test/Thinktecture.Runtime.Extensions.Json.Tests/Thinktecture.Runtime.Extensions.Json.Tests.csproj +++ b/test/Thinktecture.Runtime.Extensions.Json.Tests/Thinktecture.Runtime.Extensions.Json.Tests.csproj @@ -4,12 +4,7 @@ - - - - - %(RecursiveDir)TestValueTypes/%(FileName)%(Extension) - + diff --git a/test/Thinktecture.Runtime.Extensions.MessagePack.Tests/Thinktecture.Runtime.Extensions.MessagePack.Tests.csproj b/test/Thinktecture.Runtime.Extensions.MessagePack.Tests/Thinktecture.Runtime.Extensions.MessagePack.Tests.csproj index ff87b9e7..4a50e604 100644 --- a/test/Thinktecture.Runtime.Extensions.MessagePack.Tests/Thinktecture.Runtime.Extensions.MessagePack.Tests.csproj +++ b/test/Thinktecture.Runtime.Extensions.MessagePack.Tests/Thinktecture.Runtime.Extensions.MessagePack.Tests.csproj @@ -4,12 +4,7 @@ - - - - - %(RecursiveDir)TestValueTypes/%(FileName)%(Extension) - + diff --git a/test/Thinktecture.Runtime.Extensions.Newtonsoft.Json.Tests/Thinktecture.Runtime.Extensions.Newtonsoft.Json.Tests.csproj b/test/Thinktecture.Runtime.Extensions.Newtonsoft.Json.Tests/Thinktecture.Runtime.Extensions.Newtonsoft.Json.Tests.csproj index b0298f1a..6c8bf832 100644 --- a/test/Thinktecture.Runtime.Extensions.Newtonsoft.Json.Tests/Thinktecture.Runtime.Extensions.Newtonsoft.Json.Tests.csproj +++ b/test/Thinktecture.Runtime.Extensions.Newtonsoft.Json.Tests/Thinktecture.Runtime.Extensions.Newtonsoft.Json.Tests.csproj @@ -4,12 +4,7 @@ - - - - - %(RecursiveDir)TestValueTypes/%(FileName)%(Extension) - + diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Extensions/SolutionExtensions.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Extensions/SolutionExtensions.cs index 9d049d17..c657c2f1 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Extensions/SolutionExtensions.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Extensions/SolutionExtensions.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Immutable; +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Testing; @@ -15,9 +15,9 @@ public static class SolutionExtensions /// related to nullability mapped to , which is then used to enable all /// of these warnings for default validation during analyzer and code fix tests. /// - private static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); + private static IReadOnlyDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); - private static ImmutableDictionary GetNullableWarningsFromCompiler() + private static IReadOnlyDictionary GetNullableWarningsFromCompiler() { string[] args = { "/warnaserror:nullable" }; var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, Environment.CurrentDirectory, Environment.CurrentDirectory); diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/EnumSourceGeneratorTests.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/EnumSourceGeneratorTests.cs index 9feb8c2d..def986f7 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/EnumSourceGeneratorTests.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/EnumSourceGeneratorTests.cs @@ -7,6 +7,7 @@ using Thinktecture.AspNetCore.ModelBinding; using Thinktecture.CodeAnalysis; using Thinktecture.Formatters; +using Thinktecture.Runtime.Tests.TestEnums; using Xunit; using Xunit.Abstractions; @@ -168,7 +169,7 @@ public static bool TryGet([AllowNull] string key, [MaybeNullWhen(false)] out Tes /// 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: NotNullIfNotNull(""item"")] public static implicit operator string?(TestEnum? item) { @@ -262,8 +263,8 @@ void AddItem(TestEnum item, string itemName) lookup.Add(item.Key, item); } - AddItem(Item1, ""Item1""); - AddItem(Item2, ""Item2""); + AddItem(Item1, nameof(Item1)); + AddItem(Item2, nameof(Item2)); return lookup; } @@ -309,6 +310,526 @@ public partial class TestEnum : Thinktecture.IEnum output.Should().Be(_OUTPUT_OF_SIMPLE_ENUM); } + [Fact] + public void Should_generate_simple_extensible_class_which_implements_IEnum() + { + var source = @" +using System; + +namespace Thinktecture.Tests +{ + [EnumGeneration(IsExtensible = true)] + public partial class TestEnum : IEnum + { + public static readonly TestEnum Item1 = new(""Item1""); + public static readonly TestEnum Item2 = new(""Item2""); + } +} +"; + var output = GetGeneratedOutput(source, typeof(IEnum<>).Assembly); + output.Should().Be(@"// +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Linq.Expressions; +using Thinktecture; + +namespace Thinktecture.Tests +{ + public class TestEnum_EnumTypeConverter : Thinktecture.ValueTypeConverter + { + /// + [return: NotNullIfNotNull(""key"")] + protected override TestEnum? ConvertFrom(string? key) + { + if(key is null) + return default; + + if(TestEnum.TryGet(key, out var item)) + return item; + + throw new FormatException($""There is no item of type '{GetType().Name}' with the identifier '{key}'.""); + } + + /// + protected override string GetKeyValue(TestEnum item) + { + return item.Key; + } + } + + [Thinktecture.EnumConstructor(nameof(Key))] + [System.ComponentModel.TypeConverter(typeof(TestEnum_EnumTypeConverter))] + partial class TestEnum : IEquatable + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void ModuleInit() + { + var convertFromKey = new Func(TestEnum.Get); + Expression> convertFromKeyExpression = key => TestEnum.Get(key); + + var convertToKey = new Func(item => item.Key); + Expression> convertToKeyExpression = item => item.Key; + + var validate = new Thinktecture.Internal.Validate(TestEnum.Validate); + + var enumType = typeof(TestEnum); + var metadata = new ValueTypeMetadata(enumType, typeof(string), false, convertFromKey, convertFromKeyExpression, convertToKey, convertToKeyExpression, validate); + + ValueTypeMetadataLookup.AddMetadata(enumType, metadata); + } + + private static readonly int _typeHashCode = typeof(TestEnum).GetHashCode() * 397; + protected static readonly IEqualityComparer _defaultKeyComparerMember = StringComparer.OrdinalIgnoreCase; + + private static IReadOnlyDictionary? _itemsLookup; + private static IReadOnlyDictionary ItemsLookup => _itemsLookup ??= GetLookup(); + + private static IReadOnlyList? _items; + + /// + /// Gets all valid items. + /// + public static IReadOnlyList Items => _items ??= ItemsLookup.Values.ToList().AsReadOnly(); + + /// + /// The identifier of the item. + /// + [NotNull] + public string Key { get; } + + protected TestEnum(string key) + { + if (key is null) + throw new ArgumentNullException(nameof(key)); + + ValidateConstructorArguments(key); + + this.Key = key; + } + + static partial void ValidateConstructorArguments(string key); + + /// + /// Gets the identifier of the item. + /// + string IEnum.GetKey() + { + return this.Key; + } + + /// + /// Gets an enumeration item for provided . + /// + /// The identifier to return an enumeration item for. + /// An instance of if is not null; otherwise null. + /// If there is no item with the provided . + [return: NotNullIfNotNull(""key"")] + public static TestEnum? Get(string? key) + { + if (key is null) + return default; + + if (!ItemsLookup.TryGetValue(key, out var item)) + { + throw new KeyNotFoundException($""There is no item of type 'TestEnum' with the identifier '{key}'.""); + } + + return item; + } + + /// + /// Gets a valid enumeration item for provided if a valid item exists. + /// + /// The identifier to return an enumeration item for. + /// A valid instance of ; otherwise null. + /// true if a valid item with provided exists; false otherwise. + public static bool TryGet([AllowNull] string key, [MaybeNullWhen(false)] out TestEnum item) + { + if (key is null) + { + item = default; + return false; + } + + return ItemsLookup.TryGetValue(key, out item); + } + + /// + /// Validates the provided and returns a valid enumeration item if found. + /// + /// The identifier to return an enumeration item for. + /// A valid instance of ; otherwise null. + /// if a valid item with provided exists; with an error message otherwise. + public static ValidationResult? Validate(string key, [MaybeNull] out TestEnum item) + { + return TestEnum.TryGet(key, out item) + ? ValidationResult.Success + : new ValidationResult($""The enumeration item of type 'TestEnum' with identifier '{key}' is not valid.""); + } + + /// + /// Implicit conversion to the type . + /// + /// Item to covert. + /// The of provided or default if is null. + [return: NotNullIfNotNull(""item"")] + public static implicit operator string?(TestEnum? item) + { + return item is null ? default : item.Key; + } + + /// + /// Explicit conversion from the type . + /// + /// Value to covert. + /// An instance of if the is a known item or implements . + [return: NotNullIfNotNull(""key"")] + public static explicit operator TestEnum?(string? key) + { + return TestEnum.Get(key); + } + + /// + /// Compares to instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// true if items are equal; otherwise false. + public static bool operator ==(TestEnum? item1, TestEnum? item2) + { + if (item1 is null) + return item2 is null; + + return item1.Equals(item2); + } + + /// + /// Compares to instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// false if items are equal; otherwise true. + public static bool operator !=(TestEnum? item1, TestEnum? item2) + { + return !(item1 == item2); + } + + /// + public bool Equals(TestEnum? other) + { + if (other is null) + return false; + + if (!ReferenceEquals(GetType(), other.GetType())) + return false; + + if (ReferenceEquals(this, other)) + return true; + + return _defaultKeyComparerMember.Equals(this.Key, other.Key); + } + + /// + public override bool Equals(object? other) + { + return other is TestEnum item && Equals(item); + } + + /// + public override int GetHashCode() + { + return _typeHashCode ^ _defaultKeyComparerMember.GetHashCode(this.Key); + } + + /// + public override string? ToString() + { + return this.Key.ToString(); + } + + private static IReadOnlyDictionary GetLookup() + { + var lookup = new Dictionary(_defaultKeyComparerMember); + + void AddItem(TestEnum item, string itemName) + { + if(item is null) + throw new ArgumentNullException($""The item \""{itemName}\"" of type \""TestEnum\"" must not be null.""); + + if(item.Key is null) + throw new ArgumentException($""The \""Key\"" of the item \""{itemName}\"" of type \""TestEnum\"" must not be null.""); + + if (lookup.ContainsKey(item.Key)) + throw new ArgumentException($""The type \""TestEnum\"" has multiple items with the identifier \""{item.Key}\"".""); + + lookup.Add(item.Key, item); + } + + AddItem(Item1, nameof(Item1)); + AddItem(Item2, nameof(Item2)); + + return lookup; + } + } +} +"); + } + + [Fact] + public void Should_generate_simple_extended_class_which_implements_IEnum() + { + var source = @" +using System; + +namespace Thinktecture.Tests +{ + [EnumGeneration] + public partial class ExtendedTestEnum : Thinktecture.Runtime.Tests.TestEnums.ExtensibleTestEnum + { + public static readonly ExtendedTestEnum Item2 = new(""Item2""); + } +} +"; + var output = GetGeneratedOutput(source, typeof(IEnum<>).Assembly, typeof(ExtensibleTestEnum).Assembly); + output.Should().Be(@"// +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Linq.Expressions; +using Thinktecture; + +namespace Thinktecture.Tests +{ + public class ExtendedTestEnum_EnumTypeConverter : Thinktecture.ValueTypeConverter + { + /// + [return: NotNullIfNotNull(""key"")] + protected override ExtendedTestEnum? ConvertFrom(string? key) + { + if(key is null) + return default; + + if(ExtendedTestEnum.TryGet(key, out var item)) + return item; + + throw new FormatException($""There is no item of type 'ExtendedTestEnum' with the identifier '{key}'.""); + } + + /// + protected override string GetKeyValue(ExtendedTestEnum item) + { + return item.Key; + } + } + + [System.ComponentModel.TypeConverter(typeof(ExtendedTestEnum_EnumTypeConverter))] + partial class ExtendedTestEnum : IEquatable + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void ModuleInit() + { + var convertFromKey = new Func(ExtendedTestEnum.Get); + Expression> convertFromKeyExpression = key => ExtendedTestEnum.Get(key); + + var convertToKey = new Func(item => item.Key); + Expression> convertToKeyExpression = item => item.Key; + + var validate = new Thinktecture.Internal.Validate(ExtendedTestEnum.Validate); + + var enumType = typeof(ExtendedTestEnum); + var metadata = new ValueTypeMetadata(enumType, typeof(string), false, convertFromKey, convertFromKeyExpression, convertToKey, convertToKeyExpression, validate); + + ValueTypeMetadataLookup.AddMetadata(enumType, metadata); + } + + private static readonly int _typeHashCode = typeof(ExtendedTestEnum).GetHashCode() * 397; + + private static IReadOnlyDictionary? _itemsLookup; + private static IReadOnlyDictionary ItemsLookup => _itemsLookup ??= GetLookup(); + + private static IReadOnlyList? _items; + + /// + /// Gets all valid items. + /// + public new static IReadOnlyList Items => _items ??= ItemsLookup.Values.ToList().AsReadOnly(); + + public new static readonly Thinktecture.Tests.ExtendedTestEnum DerivedItem = new Thinktecture.Tests.ExtendedTestEnum(Thinktecture.Runtime.Tests.TestEnums.ExtensibleTestEnum.DerivedItem.Key, Thinktecture.Runtime.Tests.TestEnums.ExtensibleTestEnum.DerivedItem.Foo); + public new static readonly Thinktecture.Tests.ExtendedTestEnum Item1 = new Thinktecture.Tests.ExtendedTestEnum(Thinktecture.Runtime.Tests.TestEnums.ExtensibleTestEnum.Item1.Key, Thinktecture.Runtime.Tests.TestEnums.ExtensibleTestEnum.Item1.Foo); + + private ExtendedTestEnum(string key, System.Action foo) + : base(key, foo) + { + ValidateConstructorArguments(key, ref foo); + + } + + static partial void ValidateConstructorArguments(string key, ref System.Action foo); + + /// + /// Gets an enumeration item for provided . + /// + /// The identifier to return an enumeration item for. + /// An instance of if is not null; otherwise null. + /// If there is no item with the provided . + [return: NotNullIfNotNull(""key"")] + public new static ExtendedTestEnum? Get(string? key) + { + if (key is null) + return default; + + if (!ItemsLookup.TryGetValue(key, out var item)) + { + throw new KeyNotFoundException($""There is no item of type 'ExtendedTestEnum' with the identifier '{key}'.""); + } + + return item; + } + + /// + /// Gets a valid enumeration item for provided if a valid item exists. + /// + /// The identifier to return an enumeration item for. + /// A valid instance of ; otherwise null. + /// true if a valid item with provided exists; false otherwise. + public static bool TryGet([AllowNull] string key, [MaybeNullWhen(false)] out ExtendedTestEnum item) + { + if (key is null) + { + item = default; + return false; + } + + return ItemsLookup.TryGetValue(key, out item); + } + + /// + /// Validates the provided and returns a valid enumeration item if found. + /// + /// The identifier to return an enumeration item for. + /// A valid instance of ; otherwise null. + /// if a valid item with provided exists; with an error message otherwise. + public static ValidationResult? Validate(string key, [MaybeNull] out ExtendedTestEnum item) + { + return ExtendedTestEnum.TryGet(key, out item) + ? ValidationResult.Success + : new ValidationResult($""The enumeration item of type 'ExtendedTestEnum' with identifier '{key}' is not valid.""); + } + + /// + /// Implicit conversion to the type . + /// + /// Item to covert. + /// The of provided or default if is null. + [return: NotNullIfNotNull(""item"")] + public static implicit operator string?(ExtendedTestEnum? item) + { + return item is null ? default : item.Key; + } + + /// + /// Explicit conversion from the type . + /// + /// Value to covert. + /// An instance of if the is a known item or implements . + [return: NotNullIfNotNull(""key"")] + public static explicit operator ExtendedTestEnum?(string? key) + { + return ExtendedTestEnum.Get(key); + } + + /// + /// Compares to instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// true if items are equal; otherwise false. + public static bool operator ==(ExtendedTestEnum? item1, ExtendedTestEnum? item2) + { + if (item1 is null) + return item2 is null; + + return item1.Equals(item2); + } + + /// + /// Compares to instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// false if items are equal; otherwise true. + public static bool operator !=(ExtendedTestEnum? item1, ExtendedTestEnum? item2) + { + return !(item1 == item2); + } + + /// + public bool Equals(ExtendedTestEnum? other) + { + if (other is null) + return false; + + if (!ReferenceEquals(GetType(), other.GetType())) + return false; + + if (ReferenceEquals(this, other)) + return true; + + return EqualityComparer.Equals(this.Key, other.Key); + } + + /// + public override bool Equals(object? other) + { + return other is ExtendedTestEnum item && Equals(item); + } + + /// + public override int GetHashCode() + { + return _typeHashCode ^ EqualityComparer.GetHashCode(this.Key); + } + + private static IReadOnlyDictionary GetLookup() + { + var lookup = new Dictionary(EqualityComparer); + + void AddItem(ExtendedTestEnum item, string itemName) + { + if(item is null) + throw new ArgumentNullException($""The item \""{itemName}\"" of type \""ExtendedTestEnum\"" must not be null.""); + + if(item.Key is null) + throw new ArgumentException($""The \""Key\"" of the item \""{itemName}\"" of type \""ExtendedTestEnum\"" must not be null.""); + + if (lookup.ContainsKey(item.Key)) + throw new ArgumentException($""The type \""ExtendedTestEnum\"" has multiple items with the identifier \""{item.Key}\"".""); + + lookup.Add(item.Key, item); + } + + AddItem(DerivedItem, nameof(DerivedItem)); + AddItem(Item1, nameof(Item1)); + AddItem(Item2, nameof(Item2)); + + return lookup; + } + } +} +"); + } + [Fact] public void Should_generate_simple_class_which_implements_IValidatableEnum() { @@ -498,7 +1019,7 @@ public static bool TryGet([AllowNull] string key, [MaybeNullWhen(false)] out Tes /// 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: NotNullIfNotNull(""item"")] public static implicit operator string?(TestEnum? item) { @@ -598,8 +1119,8 @@ void AddItem(TestEnum item, string itemName) lookup.Add(item.Key, item); } - AddItem(Item1, ""Item1""); - AddItem(Item2, ""Item2""); + AddItem(Item1, nameof(Item1)); + AddItem(Item2, nameof(Item2)); return lookup; } @@ -795,7 +1316,7 @@ public static bool TryGet([AllowNull] string key, [MaybeNullWhen(false)] out Tes /// 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: NotNullIfNotNull(""item"")] public static implicit operator string?(TestEnum item) { @@ -880,8 +1401,8 @@ void AddItem(TestEnum item, string itemName) lookup.Add(item.Key, item); } - AddItem(Item1, ""Item1""); - AddItem(Item2, ""Item2""); + AddItem(Item1, nameof(Item1)); + AddItem(Item2, nameof(Item2)); return lookup; } @@ -1114,7 +1635,7 @@ public static bool TryGet([AllowNull] string name, [MaybeNullWhen(false)] out Te /// 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: NotNullIfNotNull(""item"")] public static implicit operator string?(TestEnum? item) { @@ -1214,8 +1735,8 @@ void AddItem(TestEnum item, string itemName) lookup.Add(item.Name, item); } - AddItem(Item1, ""Item1""); - AddItem(Item2, ""Item2""); + AddItem(Item1, nameof(Item1)); + AddItem(Item2, nameof(Item2)); return lookup; } diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Thinktecture.Runtime.Extensions.SourceGenerator.Tests.csproj b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Thinktecture.Runtime.Extensions.SourceGenerator.Tests.csproj index e0012224..62b9b972 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Thinktecture.Runtime.Extensions.SourceGenerator.Tests.csproj +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/Thinktecture.Runtime.Extensions.SourceGenerator.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/AbstractEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/AbstractEnum.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/AbstractEnum.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/AbstractEnum.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/EmptyEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/EmptyEnum.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/EmptyEnum.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/EmptyEnum.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/EnumWithDerivedType.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/EnumWithDerivedType.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/EnumWithDerivedType.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/EnumWithDerivedType.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/EnumWithDuplicateKey.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/EnumWithDuplicateKey.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/EnumWithDuplicateKey.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/EnumWithDuplicateKey.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtendedTestEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtendedTestEnum.cs new file mode 100644 index 00000000..dbfc6f4a --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtendedTestEnum.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.Runtime.Tests.TestEnums +{ + [EnumGeneration] + public partial class ExtendedTestEnum : ExtensibleTestEnum + { + public static readonly ExtendedTestEnum Item2 = new("Item2", Empty.Action); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtendedTestValidatableEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtendedTestValidatableEnum.cs new file mode 100644 index 00000000..7c4d1cac --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtendedTestValidatableEnum.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.Runtime.Tests.TestEnums +{ + [EnumGeneration] + public partial class ExtendedTestValidatableEnum : ExtensibleTestValidatableEnum + { + public static readonly ExtendedTestValidatableEnum Item2 = new("Item2", Empty.Action); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtensibleTestEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtensibleTestEnum.cs new file mode 100644 index 00000000..83a90aec --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtensibleTestEnum.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace Thinktecture.Runtime.Tests.TestEnums +{ + [EnumGeneration(IsExtensible = true, KeyComparer = nameof(EqualityComparer))] + public partial class ExtensibleTestEnum : IEnum + { + protected static readonly IEqualityComparer EqualityComparer = StringComparer.OrdinalIgnoreCase; + + public static readonly ExtensibleTestEnum DerivedItem = new ExtensibleTestEnumImpl("DerivedItem", Empty.Action); + public static readonly ExtensibleTestEnum Item1 = new("Item1", Empty.Action); + + [EnumGenerationMember(MapsToMember = nameof(Foo))] + private readonly Action _foo; + + public void Foo() + { + _foo(); + } + + private class ExtensibleTestEnumImpl : ExtensibleTestEnum + { + public ExtensibleTestEnumImpl(string key, Action foo) + : base(key, foo) + { + } + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtensibleTestValidatableEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtensibleTestValidatableEnum.cs new file mode 100644 index 00000000..9a11cda1 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ExtensibleTestValidatableEnum.cs @@ -0,0 +1,18 @@ +using System; + +namespace Thinktecture.Runtime.Tests.TestEnums +{ + [EnumGeneration(IsExtensible = true)] + public partial class ExtensibleTestValidatableEnum : IValidatableEnum + { + public static readonly ExtensibleTestValidatableEnum Item1 = new("Item1", Empty.Action); + + [EnumGenerationMember(MapsToMember = nameof(Foo))] + private readonly Action _foo; + + public void Foo() + { + _foo(); + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/IntegerEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/IntegerEnum.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/IntegerEnum.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/IntegerEnum.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/StructIntegerEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/StructIntegerEnum.cs similarity index 71% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/StructIntegerEnum.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/StructIntegerEnum.cs index f33e616a..de2cb045 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/StructIntegerEnum.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/StructIntegerEnum.cs @@ -7,10 +7,13 @@ namespace Thinktecture.Runtime.Tests.TestEnums public static readonly StructIntegerEnum Item1 = new(1, 42, 100); public static readonly StructIntegerEnum Item2 = new(2, 43, 200); + // ReSharper disable once UnusedMember.Global public int Property1 => Field; + // ReSharper disable once UnusedMember.Global public int Property2 { + // ReSharper disable once ArrangeAccessorOwnerBody get { return Field; } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/StructIntegerEnumWithZero.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/StructIntegerEnumWithZero.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/StructIntegerEnumWithZero.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/StructIntegerEnumWithZero.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/StructStringEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/StructStringEnum.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/StructStringEnum.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/StructStringEnum.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/TestEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/TestEnum.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/TestEnum.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/TestEnum.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/TestEnumWithNonDefaultComparer.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/TestEnumWithNonDefaultComparer.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/TestEnumWithNonDefaultComparer.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/TestEnumWithNonDefaultComparer.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/ValidTestEnum.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ValidTestEnum.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestEnums/ValidTestEnum.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestEnums/ValidTestEnum.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestValueTypes/IntBasedReferenceValueType.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/IntBasedReferenceValueType.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestValueTypes/IntBasedReferenceValueType.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/IntBasedReferenceValueType.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestValueTypes/IntBasedStructValueType.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/IntBasedStructValueType.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestValueTypes/IntBasedStructValueType.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/IntBasedStructValueType.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestValueTypes/StringBasedReferenceValueType.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/StringBasedReferenceValueType.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestValueTypes/StringBasedReferenceValueType.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/StringBasedReferenceValueType.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestValueTypes/StringBasedStructValueType.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/StringBasedStructValueType.cs similarity index 100% rename from test/Thinktecture.Runtime.Extensions.Tests/TestValueTypes/StringBasedStructValueType.cs rename to test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/StringBasedStructValueType.cs diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/Thinktecture.Runtime.Extensions.Tests.Shared.csproj b/test/Thinktecture.Runtime.Extensions.Tests.Shared/Thinktecture.Runtime.Extensions.Tests.Shared.csproj new file mode 100644 index 00000000..f34c3ef8 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/Thinktecture.Runtime.Extensions.Tests.Shared.csproj @@ -0,0 +1,16 @@ + + + + $(NoWarn);CS1591;CA1052;CA1716;CA1801;CA1052;CA1707;CS1718;CA1062;CA1806;CA1822;CA1825;CA2000;CA2007;CA2234 + + + + + + + + + + + + diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EnsureValid.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EnsureValid.cs index 124f9679..7cb94e62 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EnsureValid.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EnsureValid.cs @@ -14,6 +14,8 @@ public void Should_not_throw_if_item_is_valid() StructIntegerEnum.Item1.EnsureValid(); StructIntegerEnumWithZero.Item0.EnsureValid(); StructStringEnum.Item1.EnsureValid(); + ExtensibleTestValidatableEnum.Item1.EnsureValid(); + ExtendedTestValidatableEnum.Item2.EnsureValid(); } [Fact] @@ -59,6 +61,12 @@ public void Should_throw_if_item_is_invalid() // we cannot prevent construction of a struct new StructIntegerEnumWithZero().Invoking(e => e.EnsureValid()) .Should().Throw().WithMessage($"The current enumeration item of type '{nameof(StructIntegerEnumWithZero)}' with identifier '0' is not valid."); + + ExtensibleTestValidatableEnum.Get("invalid").Invoking(e => e.EnsureValid()) + .Should().Throw().WithMessage($"The current enumeration item of type '{nameof(ExtensibleTestValidatableEnum)}' with identifier 'invalid' is not valid."); + + ExtendedTestValidatableEnum.Get("invalid").Invoking(e => e.EnsureValid()) + .Should().Throw().WithMessage($"The current enumeration item of type '{nameof(ExtendedTestValidatableEnum)}' with identifier 'invalid' is not valid."); } } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EqualityOperator.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EqualityOperator.cs index 3c058c5d..cc4ce095 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EqualityOperator.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EqualityOperator.cs @@ -10,6 +10,11 @@ public class EqualityOperator public void Should_return_false_if_item_is_null() { (TestEnum.Item1 is null).Should().BeFalse(); + + (ExtensibleTestEnum.Item1 is null).Should().BeFalse(); + (ExtendedTestEnum.DerivedItem is null).Should().BeFalse(); + (ExtendedTestEnum.Item1 is null).Should().BeFalse(); + (ExtendedTestEnum.Item2 is null).Should().BeFalse(); } [Fact] @@ -17,13 +22,23 @@ public void Should_return_false_if_item_is_of_different_type() { // ReSharper disable once SuspiciousTypeConversion.Global (TestEnum.Item1 == TestEnumWithNonDefaultComparer.Item).Should().BeFalse(); + + (ExtensibleTestEnum.Item1 == ExtendedTestEnum.Item1).Should().BeFalse(); + (ExtensibleTestEnum.DerivedItem == ExtendedTestEnum.DerivedItem).Should().BeFalse(); + (ExtensibleTestValidatableEnum.Item1 == ExtendedTestValidatableEnum.Item1).Should().BeFalse(); } [Fact] public void Should_return_true_on_reference_equality() { - // ReSharper disable once EqualExpressionComparison + // ReSharper disable EqualExpressionComparison (TestEnum.Item1 == TestEnum.Item1).Should().BeTrue(); + + (ExtensibleTestEnum.Item1 == ExtensibleTestEnum.Item1).Should().BeTrue(); + (ExtensibleTestEnum.DerivedItem == ExtensibleTestEnum.DerivedItem).Should().BeTrue(); + (ExtendedTestEnum.Item1 == ExtendedTestEnum.Item1).Should().BeTrue(); + (ExtendedTestEnum.Item2 == ExtendedTestEnum.Item2).Should().BeTrue(); + (ExtendedTestEnum.DerivedItem == ExtendedTestEnum.DerivedItem).Should().BeTrue(); } [Fact] @@ -66,12 +81,18 @@ public void Should_return_false_for_invalid_structs_on_inequality() public void Should_return_true_if_both_items_are_invalid_and_have_same_key() { (TestEnum.Get("unknown") == TestEnum.Get("Unknown")).Should().BeTrue(); + + (ExtensibleTestValidatableEnum.Get("unknown") == ExtensibleTestValidatableEnum.Get("Unknown")).Should().BeTrue(); + (ExtendedTestValidatableEnum.Get("unknown") == ExtendedTestValidatableEnum.Get("Unknown")).Should().BeTrue(); } [Fact] public void Should_return_false_if_both_items_are_invalid_and_have_different_keys() { (TestEnum.Get("unknown") == TestEnum.Get("other")).Should().BeFalse(); + + (ExtensibleTestValidatableEnum.Get("unknown") == ExtensibleTestValidatableEnum.Get("other")).Should().BeFalse(); + (ExtendedTestValidatableEnum.Get("unknown") == ExtendedTestValidatableEnum.Get("other")).Should().BeFalse(); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Equals.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Equals.cs index 1a2fa0d3..6837ebd6 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Equals.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Equals.cs @@ -10,6 +10,13 @@ public class Equals public void Should_return_false_if_item_is_null() { TestEnum.Item1.Equals(null).Should().BeFalse(); + + ExtensibleTestEnum.Item1.Equals(null).Should().BeFalse(); + ExtensibleTestEnum.DerivedItem.Equals(null).Should().BeFalse(); + ExtendedTestEnum.Item1.Equals(null).Should().BeFalse(); + ExtendedTestEnum.DerivedItem.Equals(null).Should().BeFalse(); + ExtensibleTestValidatableEnum.Item1.Equals(null).Should().BeFalse(); + ExtendedTestValidatableEnum.Item1.Equals(null).Should().BeFalse(); } [Fact] @@ -17,24 +24,40 @@ public void Should_return_false_if_item_is_of_different_type() { // ReSharper disable once SuspiciousTypeConversion.Global TestEnum.Item1.Equals(TestEnumWithNonDefaultComparer.Item).Should().BeFalse(); + + ExtensibleTestEnum.Item1.Equals(ExtendedTestEnum.Item1).Should().BeFalse(); + ExtensibleTestEnum.DerivedItem.Equals(ExtendedTestEnum.DerivedItem).Should().BeFalse(); + ExtensibleTestValidatableEnum.Item1.Equals(ExtendedTestValidatableEnum.Item1).Should().BeFalse(); } [Fact] public void Should_return_true_on_reference_equality() { TestEnum.Item1.Equals(TestEnum.Item1).Should().BeTrue(); + + ExtensibleTestEnum.Item1.Equals(ExtensibleTestEnum.Item1).Should().BeTrue(); + ExtensibleTestEnum.DerivedItem.Equals(ExtensibleTestEnum.DerivedItem).Should().BeTrue(); + ExtendedTestEnum.Item1.Equals(ExtendedTestEnum.Item1).Should().BeTrue(); + ExtendedTestEnum.Item2.Equals(ExtendedTestEnum.Item2).Should().BeTrue(); + ExtendedTestEnum.DerivedItem.Equals(ExtendedTestEnum.DerivedItem).Should().BeTrue(); } [Fact] public void Should_return_true_if_both_items_are_invalid_and_have_same_key() { TestEnum.Get("unknown").Equals(TestEnum.Get("Unknown")).Should().BeTrue(); + + ExtensibleTestValidatableEnum.Get("unknown").Equals(ExtensibleTestValidatableEnum.Get("Unknown")).Should().BeTrue(); + ExtendedTestValidatableEnum.Get("unknown").Equals(ExtendedTestValidatableEnum.Get("Unknown")).Should().BeTrue(); } [Fact] public void Should_return_false_if_both_items_are_invalid_and_have_different_keys() { TestEnum.Get("unknown").Equals(TestEnum.Get("other")).Should().BeFalse(); + + ExtensibleTestValidatableEnum.Get("unknown").Equals(ExtensibleTestValidatableEnum.Get("other")).Should().BeFalse(); + ExtendedTestValidatableEnum.Get("unknown").Equals(ExtendedTestValidatableEnum.Get("other")).Should().BeFalse(); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ExplicitConversionFromKey.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ExplicitConversionFromKey.cs new file mode 100644 index 00000000..1c8cdebd --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ExplicitConversionFromKey.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Thinktecture.Runtime.Tests.TestEnums; +using Xunit; + +namespace Thinktecture.Runtime.Tests.EnumTests +{ + public class ExplicitConversionFromKey + { + [Fact] + public void Should_return_null_if_key_is_null() + { + string key = null; + var item = (TestEnum)key; + + item.Should().BeNull(); + } + + [Fact] + public void Should_return_invalid_item_if_struct_is_default_and_there_are_no_items_for_default_value() + { + int key = default; + var item = (StructIntegerEnum)key; + + item.Should().Be(new StructIntegerEnum()); + } + + [Fact] + public void Should_return_item() + { + var testEnum = (TestEnum)"item1"; + testEnum.Should().Be(TestEnum.Item1); + + var extensibleItem = (ExtensibleTestEnum)"Item1"; + extensibleItem.Should().Be(ExtensibleTestEnum.Item1); + + var extendedItem = (ExtendedTestEnum)"Item1"; + extendedItem.Should().Be(ExtendedTestEnum.Item1); + + extendedItem = (ExtendedTestEnum)"Item2"; + extendedItem.Should().Be(ExtendedTestEnum.Item2); + } + + [Fact] + public void Should_return_invalid_item_if_enum_has_no_such_key() + { + var item = TestEnum.Get("invalid"); + item.Key.Should().Be("invalid"); + item.IsValid.Should().BeFalse(); + + var extensibleItem = ExtensibleTestValidatableEnum.Get("invalid"); + extensibleItem.Key.Should().Be("invalid"); + extensibleItem.IsValid.Should().BeFalse(); + + var extendedItem = ExtendedTestValidatableEnum.Get("invalid"); + extendedItem.Key.Should().Be("invalid"); + extendedItem.IsValid.Should().BeFalse(); + } + + [Fact] + public void Should_throw_if_non_validable_enum_has_no_such_key() + { + Action action = () => + { + var item = (ValidTestEnum)"invalid"; + }; + action.Should().Throw().WithMessage("There is no item of type 'ValidTestEnum' with the identifier 'invalid'."); + + action = () => + { + var item = (ExtensibleTestEnum)"invalid"; + }; + action.Should().Throw().WithMessage("There is no item of type 'ExtensibleTestEnum' with the identifier 'invalid'."); + + action = () => + { + var item = (ExtendedTestEnum)"invalid"; + }; + action.Should().Throw().WithMessage("There is no item of type 'ExtendedTestEnum' with the identifier 'invalid'."); + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Get.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Get.cs index 3bc6af78..0fe70ba3 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Get.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Get.cs @@ -3,7 +3,6 @@ using System.Reflection; using FluentAssertions; using Thinktecture.Runtime.Tests.TestEnums; -using Thinktecture.Runtime.Tests.TestEnums.Isolated; using Xunit; namespace Thinktecture.Runtime.Tests.EnumTests @@ -15,6 +14,11 @@ public void Should_return_null_if_null_is_provided() { TestEnum.Get(null).Should().BeNull(); ValidTestEnum.Get(null).Should().BeNull(); + + ExtensibleTestEnum.Get(null).Should().BeNull(); + ExtendedTestEnum.Get(null).Should().BeNull(); + ExtensibleTestValidatableEnum.Get(null).Should().BeNull(); + ExtendedTestValidatableEnum.Get(null).Should().BeNull(); } [Fact] @@ -41,21 +45,39 @@ public void Should_return_invalid_item_via_reflection_if_enum_doesnt_have_any_it public void Should_return_invalid_item_if_enum_doesnt_have_item_with_provided_key() { var item = TestEnum.Get("unknown"); - item.IsValid.Should().BeFalse(); item.Key.Should().Be("unknown"); + + var extensibleItem = ExtensibleTestValidatableEnum.Get("unknown"); + extensibleItem.IsValid.Should().BeFalse(); + extensibleItem.Key.Should().Be("unknown"); + + var extendedItem = ExtendedTestValidatableEnum.Get("unknown"); + extendedItem.IsValid.Should().BeFalse(); + extendedItem.Key.Should().Be("unknown"); } [Fact] public void Should_return_item_with_provided_key() { TestEnum.Get("item2").Should().Be(TestEnum.Item2); + + ValidTestEnum.Get("item1").Should().Be(ValidTestEnum.Item1); + + ExtensibleTestEnum.Get("Item1").Should().Be(ExtensibleTestEnum.Item1); + ExtendedTestEnum.Get("Item1").Should().Be(ExtendedTestEnum.Item1); + ExtendedTestEnum.Get("Item2").Should().Be(ExtendedTestEnum.Item2); + + ExtensibleTestValidatableEnum.Get("Item1").Should().Be(ExtensibleTestValidatableEnum.Item1); + ExtendedTestValidatableEnum.Get("Item1").Should().Be(ExtendedTestValidatableEnum.Item1); + ExtendedTestValidatableEnum.Get("Item2").Should().Be(ExtendedTestValidatableEnum.Item2); } [Fact] public void Should_return_item_with_provided_key_ignoring_casing() { - StaticCtorTestEnum_Get.Get("Item").Should().Be(StaticCtorTestEnum_Get.Item); + TestEnum.Get("Item1").Should().Be(TestEnum.Item1); + TestEnum.Get("item1").Should().Be(TestEnum.Item1); } [Fact] @@ -72,12 +94,9 @@ public void Should_return_derived_type() EnumWithDerivedType.Get(2).Should().Be(EnumWithDerivedType.ItemOfDerivedType); AbstractEnum.Get(1).Should().Be(AbstractEnum.Item); - } - [Fact] - public void Should_return_valid_item_of_non_validatable_enum() - { - ValidTestEnum.Get("item1").Should().Be(ValidTestEnum.Item1); + ExtensibleTestEnum.Get("DerivedItem").Should().Be(ExtensibleTestEnum.DerivedItem); + ExtendedTestEnum.Get("DerivedItem").Should().Be(ExtendedTestEnum.DerivedItem); } [Fact] @@ -85,6 +104,12 @@ public void Should_throw_if_key_is_unknown_to_non_validatable_enum() { Action action = () => ValidTestEnum.Get("invalid"); action.Should().Throw().WithMessage("There is no item of type 'ValidTestEnum' with the identifier 'invalid'."); + + action = () => ExtensibleTestEnum.Get("invalid"); + action.Should().Throw().WithMessage("There is no item of type 'ExtensibleTestEnum' with the identifier 'invalid'."); + + action = () => ExtendedTestEnum.Get("invalid"); + action.Should().Throw().WithMessage("There is no item of type 'ExtendedTestEnum' with the identifier 'invalid'."); } } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/GetHashCode.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/GetHashCode.cs index 83048060..bc70080d 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/GetHashCode.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/GetHashCode.cs @@ -5,26 +5,35 @@ namespace Thinktecture.Runtime.Tests.EnumTests { - public class GetHashCode - { - [Fact] - public void Should_return_hashcode_of_the_type_plus_key() - { - var hashCode = TestEnum.Item1.GetHashCode(); + public class GetHashCode + { + [Fact] + public void Should_return_hashcode_of_the_type_plus_key() + { + var hashCode = TestEnum.Item1.GetHashCode(); + var typeHashCode = typeof(TestEnum).GetHashCode(); + var keyHashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(TestEnum.Item1.Key); + hashCode.Should().Be((typeHashCode * 397) ^ keyHashCode); + + hashCode = ExtensibleTestEnum.Item1.GetHashCode(); + typeHashCode = typeof(ExtensibleTestEnum).GetHashCode(); + keyHashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(ExtensibleTestEnum.Item1.Key); + hashCode.Should().Be((typeHashCode * 397) ^ keyHashCode); - var typeHashCode = typeof(TestEnum).GetHashCode(); - var keyHashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(TestEnum.Item1.Key); - hashCode.Should().Be((typeHashCode * 397) ^ keyHashCode); - } + hashCode = ExtendedTestEnum.Item1.GetHashCode(); + typeHashCode = typeof(ExtendedTestEnum).GetHashCode(); + keyHashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(ExtendedTestEnum.Item1.Key); + hashCode.Should().Be((typeHashCode * 397) ^ keyHashCode); + } [Fact] - public void Should_return_hashcode_of_the_type_plus_key_for_structs() - { - var hashCode = StructIntegerEnum.Item1.GetHashCode(); + public void Should_return_hashcode_of_the_type_plus_key_for_structs() + { + var hashCode = StructIntegerEnum.Item1.GetHashCode(); - var typeHashCode = typeof(StructIntegerEnum).GetHashCode(); - var keyHashCode = StructIntegerEnum.Item1.Key.GetHashCode(); - hashCode.Should().Be((typeHashCode * 397) ^ keyHashCode); - } - } + var typeHashCode = typeof(StructIntegerEnum).GetHashCode(); + var keyHashCode = StructIntegerEnum.Item1.Key.GetHashCode(); + hashCode.Should().Be((typeHashCode * 397) ^ keyHashCode); + } + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ImplicitConversionToKey.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ImplicitConversionToKey.cs index 93896031..5ccc3ea1 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ImplicitConversionToKey.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ImplicitConversionToKey.cs @@ -4,31 +4,39 @@ namespace Thinktecture.Runtime.Tests.EnumTests { - public class ImplicitConversionToKey - { - [Fact] - public void Should_return_null_if_item_is_null() - { - string key = (TestEnum)null; + public class ImplicitConversionToKey + { + [Fact] + public void Should_return_null_if_item_is_null() + { + string key = (TestEnum)null; - key.Should().BeNull(); - } + key.Should().BeNull(); + } [Fact] - public void Should_return_default_if_struct_is_default() + public void Should_return_default_if_struct_is_default() { StructIntegerEnum item = default; - int key = item; + int key = item; + + key.Should().Be(0); + } + + [Fact] + public void Should_return_key() + { + string key = TestEnum.Item1; + key.Should().Be(TestEnum.Item1.Key); - key.Should().Be(0); - } + key = ExtensibleTestEnum.Item1; + key.Should().Be(ExtensibleTestEnum.Item1.Key); - [Fact] - public void Should_return_key() - { - string key = TestEnum.Item1; + key = ExtendedTestEnum.Item1; + key.Should().Be(ExtendedTestEnum.Item1.Key); - key.Should().Be(TestEnum.Item1.Key); - } - } + key = ExtendedTestEnum.Item2; + key.Should().Be(ExtendedTestEnum.Item2.Key); + } + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Items.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Items.cs index e651512a..fe1f5708 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Items.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Items.cs @@ -22,6 +22,17 @@ public void Should_return_public_fields_only() enums.Should().HaveCount(2); enums.Should().Contain(TestEnum.Item1); enums.Should().Contain(TestEnum.Item2); + + var extensibleItems = ExtensibleTestEnum.Items; + extensibleItems.Should().HaveCount(2); + extensibleItems.Should().Contain(ExtensibleTestEnum.Item1); + extensibleItems.Should().Contain(ExtensibleTestEnum.DerivedItem); + + var extendedItems = ExtendedTestEnum.Items; + extendedItems.Should().HaveCount(3); + extendedItems.Should().Contain(ExtendedTestEnum.Item1); + extendedItems.Should().Contain(ExtendedTestEnum.Item2); + extendedItems.Should().Contain(ExtendedTestEnum.DerivedItem); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/NotEqualityOperator.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/NotEqualityOperator.cs index 11dab96e..c8c8f8e0 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/NotEqualityOperator.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/NotEqualityOperator.cs @@ -10,6 +10,11 @@ public class NotEqualityOperator public void Should_return_true_if_item_is_null() { (TestEnum.Item1 is not null).Should().BeTrue(); + + (ExtensibleTestEnum.Item1 is not null).Should().BeTrue(); + (ExtendedTestEnum.DerivedItem is not null).Should().BeTrue(); + (ExtendedTestEnum.Item1 is not null).Should().BeTrue(); + (ExtendedTestEnum.Item2 is not null).Should().BeTrue(); } [Fact] @@ -17,6 +22,10 @@ public void Should_return_true_if_item_is_of_different_type() { // ReSharper disable once SuspiciousTypeConversion.Global (TestEnum.Item1 != TestEnumWithNonDefaultComparer.Item).Should().BeTrue(); + + (ExtensibleTestEnum.Item1 != ExtendedTestEnum.Item1).Should().BeTrue(); + (ExtensibleTestEnum.DerivedItem != ExtendedTestEnum.DerivedItem).Should().BeTrue(); + (ExtensibleTestValidatableEnum.Item1 != ExtendedTestValidatableEnum.Item1).Should().BeTrue(); } [Fact] @@ -24,18 +33,30 @@ public void Should_return_false_on_reference_equality() { // ReSharper disable once EqualExpressionComparison (TestEnum.Item1 != TestEnum.Item1).Should().BeFalse(); + + (ExtensibleTestEnum.Item1 != ExtensibleTestEnum.Item1).Should().BeFalse(); + (ExtensibleTestEnum.DerivedItem != ExtensibleTestEnum.DerivedItem).Should().BeFalse(); + (ExtendedTestEnum.Item1 != ExtendedTestEnum.Item1).Should().BeFalse(); + (ExtendedTestEnum.Item2 != ExtendedTestEnum.Item2).Should().BeFalse(); + (ExtendedTestEnum.DerivedItem != ExtendedTestEnum.DerivedItem).Should().BeFalse(); } [Fact] public void Should_return_false_if_both_items_are_invalid_and_have_same_key() { (TestEnum.Get("unknown") != TestEnum.Get("Unknown")).Should().BeFalse(); + + (ExtensibleTestValidatableEnum.Get("unknown") != ExtensibleTestValidatableEnum.Get("Unknown")).Should().BeFalse(); + (ExtendedTestValidatableEnum.Get("unknown") != ExtendedTestValidatableEnum.Get("Unknown")).Should().BeFalse(); } [Fact] public void Should_return_true_if_both_items_are_invalid_and_have_different_keys() { (TestEnum.Get("unknown") != TestEnum.Get("other")).Should().BeTrue(); + + (ExtensibleTestValidatableEnum.Get("unknown") != ExtensibleTestValidatableEnum.Get("other")).Should().BeTrue(); + (ExtendedTestValidatableEnum.Get("unknown") != ExtendedTestValidatableEnum.Get("other")).Should().BeTrue(); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ToString.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ToString.cs index 9a225443..2f165f55 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ToString.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/ToString.cs @@ -10,6 +10,9 @@ public class ToString public void Should_return_string_representation_of_the_key() { TestEnum.Item1.ToString().Should().Be("item1"); + + ExtensibleTestEnum.Item1.ToString().Should().Be("Item1"); + ExtendedTestEnum.Item1.ToString().Should().Be("Item1"); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/TryGet.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/TryGet.cs index 475250d0..c17ce016 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/TryGet.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/TryGet.cs @@ -1,73 +1,82 @@ using FluentAssertions; using Thinktecture.Runtime.Tests.TestEnums; -using Thinktecture.Runtime.Tests.TestEnums.Isolated; using Xunit; namespace Thinktecture.Runtime.Tests.EnumTests { - public class TryGet - { - [Fact] - public void Should_return_false_if_null_is_provided() - { - EmptyEnum.TryGet(null, out var item) - .Should().BeFalse(); + public class TryGet + { + [Fact] + public void Should_return_false_if_null_is_provided() + { + EmptyEnum.TryGet(null, out var item).Should().BeFalse(); + item.Should().BeNull(); - item.Should().BeNull(); - } + ExtensibleTestEnum.TryGet(null, out var extensibleItem).Should().BeFalse(); + extensibleItem.Should().BeNull(); - [Fact] - public void Should_return_false_if_enum_dont_have_any_items() - { - EmptyEnum.TryGet("unknown", out var item) - .Should().BeFalse(); + ExtendedTestEnum.TryGet(null, out var extendedItem).Should().BeFalse(); + extendedItem.Should().BeNull(); + } - item.Should().BeNull(); - } + [Fact] + public void Should_return_false_if_enum_dont_have_any_items() + { + EmptyEnum.TryGet("unknown", out var item).Should().BeFalse(); + item.Should().BeNull(); + } - [Fact] - public void Should_return_false_if_struct_dont_have_any_items() - { - StructIntegerEnum.TryGet(42, out var item) - .Should().BeFalse(); + [Fact] + public void Should_return_false_if_struct_dont_have_any_items() + { + StructIntegerEnum.TryGet(42, out var item).Should().BeFalse(); + item.Should().Be(new StructIntegerEnum()); + } - item.Should().Be(new StructIntegerEnum()); - } + [Fact] + public void Should_return_false_if_enum_dont_have_item_with_provided_key() + { + TestEnum.TryGet("unknown", out var item).Should().BeFalse(); + item.Should().BeNull(); - [Fact] - public void Should_return_false_if_enum_dont_have_item_with_provided_key() - { - TestEnum.TryGet("unknown", out var item) - .Should().BeFalse(); + ExtensibleTestEnum.TryGet("unknown", out var extensibleItem).Should().BeFalse(); + extensibleItem.Should().BeNull(); - item.Should().BeNull(); - } + ExtendedTestEnum.TryGet("unknown", out var extendedItem).Should().BeFalse(); + extendedItem.Should().BeNull(); + } - [Fact] - public void Should_return_true_if_item_with_provided_key_exists() - { - TestEnum.TryGet("item2", out var item) - .Should().BeTrue(); + [Fact] + public void Should_return_true_if_item_with_provided_key_exists() + { + TestEnum.TryGet("item2", out var item).Should().BeTrue(); + item.Should().Be(TestEnum.Item2); - item.Should().Be(TestEnum.Item2); - } + ExtensibleTestEnum.TryGet("Item1", out var extensibleItem).Should().BeTrue(); + extensibleItem.Should().Be(ExtensibleTestEnum.Item1); - [Fact] - public void Should_return_true_if_item_with_provided_key_exists_ignoring_casing() - { - StaticCtorTestEnum_TryGet.TryGet("Item", out var item) - .Should().BeTrue(); + ExtendedTestEnum.TryGet("Item1", out var extendedItem).Should().BeTrue(); + extendedItem.Should().Be(ExtendedTestEnum.Item1); - item.Should().Be(StaticCtorTestEnum_TryGet.Item); - } + ExtendedTestEnum.TryGet("Item2", out extendedItem).Should().BeTrue(); + extendedItem.Should().Be(ExtendedTestEnum.Item2); + } - [Fact] - public void Should_return_false_if_the_casing_does_not_match_accoring_to_comparer() - { - TestEnumWithNonDefaultComparer.TryGet("Item2", out var item) - .Should().BeFalse(); + [Fact] + public void Should_return_true_if_item_with_provided_key_exists_ignoring_casing() + { + TestEnum.TryGet("Item1", out var item).Should().BeTrue(); + item.Should().Be(TestEnum.Item1); - item.Should().BeNull(); - } - } + TestEnum.TryGet("item1", out item).Should().BeTrue(); + item.Should().Be(TestEnum.Item1); + } + + [Fact] + public void Should_return_false_if_the_casing_does_not_match_according_to_comparer() + { + TestEnumWithNonDefaultComparer.TryGet("Item2", out var item).Should().BeFalse(); + item.Should().BeNull(); + } + } } diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/Isolated/StaticCtorTestEnum_Get.cs b/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/Isolated/StaticCtorTestEnum_Get.cs deleted file mode 100644 index 0f8039c1..00000000 --- a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/Isolated/StaticCtorTestEnum_Get.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Thinktecture.Runtime.Tests.TestEnums.Isolated -{ - /// - /// This enum may be used in 1 test only. - /// Otherwise it is initialized and the test is invalid. - /// - // ReSharper disable once InconsistentNaming - public partial class StaticCtorTestEnum_Get : IValidatableEnum - { - // ReSharper disable once UnusedMember.Global - public static readonly StaticCtorTestEnum_Get Item = new("item"); - } -} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/Isolated/StaticCtorTestEnum_TryGet.cs b/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/Isolated/StaticCtorTestEnum_TryGet.cs deleted file mode 100644 index 7ea2e6db..00000000 --- a/test/Thinktecture.Runtime.Extensions.Tests/TestEnums/Isolated/StaticCtorTestEnum_TryGet.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Thinktecture.Runtime.Tests.TestEnums.Isolated -{ - /// - /// This enum may be used in 1 test only. - /// Otherwise it is initialized and the test is invalid. - /// - // ReSharper disable once InconsistentNaming - public partial class StaticCtorTestEnum_TryGet : IValidatableEnum - { - // ReSharper disable once UnusedMember.Global - public static readonly StaticCtorTestEnum_TryGet Item = new("item"); - } -} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/Thinktecture.Runtime.Extensions.Tests.csproj b/test/Thinktecture.Runtime.Extensions.Tests/Thinktecture.Runtime.Extensions.Tests.csproj index 1751789f..c12d0b14 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/Thinktecture.Runtime.Extensions.Tests.csproj +++ b/test/Thinktecture.Runtime.Extensions.Tests/Thinktecture.Runtime.Extensions.Tests.csproj @@ -7,6 +7,11 @@ + + + + +