Skip to content

Commit

Permalink
Enums can be extended, i.e. derived from - which comes with some limi…
Browse files Browse the repository at this point in the history
…tations
  • Loading branch information
PawelGerr committed Feb 7, 2021
1 parent 9702c3d commit ebca5d7
Show file tree
Hide file tree
Showing 66 changed files with 1,962 additions and 312 deletions.
7 changes: 7 additions & 0 deletions Thinktecture.Runtime.Extensions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ISymbolState>? _items;

public IReadOnlyList<ISymbolState> Items => _items ??= Type.EnumerateEnumItems().Select(DefaultSymbolState.CreateFrom).ToList();

private IReadOnlyList<ISymbolState>? _ctorExtraArgs;

public IReadOnlyList<ISymbolState> 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<TypedConstant> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'";
Expand Down Expand Up @@ -48,67 +48,47 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)

if (root is not null)
{
var typeDeclaration = GetDeclaration<TypeDeclarationSyntax>(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<FieldDeclarationSyntax>(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<FieldDeclarationSyntax>(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<PropertyDeclarationSyntax>(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);
}
}

Expand All @@ -117,19 +97,15 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
return context.Diagnostics.FirstOrDefault(d => d.Id == diagnostic.Id);
}

private static T? GetDeclaration<T>(CodeFixContext context, SyntaxNode root)
where T : MemberDeclarationSyntax
{
var diagnosticSpan = context.Diagnostics.First().Location.SourceSpan;
return root.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<T>().First();
}

private static Task<Document> 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));
Expand All @@ -142,16 +118,19 @@ private static Task<Document> AddTypeModifierAsync(
private static Task<Document> 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);

Expand All @@ -163,7 +142,7 @@ private static Task<Document> 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);
Expand All @@ -183,8 +162,11 @@ private static Task<Document> ChangeAccessibilityAsync(
private static Task<Document> 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)
Expand All @@ -201,12 +183,15 @@ private static Task<Document> RemovePropertySetterAsync(
return Task.FromResult(document);
}

private async Task<Document> AddCreateInvalidItemAsync(
private static async Task<Document> 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)
Expand Down Expand Up @@ -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<TypeDeclarationSyntax>();

private FieldDeclarationSyntax? _fieldDeclaration;
public FieldDeclarationSyntax? FieldDeclaration => _fieldDeclaration ??= GetDeclaration<FieldDeclarationSyntax>();

private PropertyDeclarationSyntax? _propertyDeclaration;
public PropertyDeclarationSyntax? PropertyDeclaration => _propertyDeclaration ??= GetDeclaration<PropertyDeclarationSyntax>();

public CodeFixesContext(CodeFixContext context, SyntaxNode root)
{
_context = context;
_root = root;
}

private T? GetDeclaration<T>()
where T : MemberDeclarationSyntax
{
var diagnosticSpan = _context.Diagnostics.First().Location.SourceSpan;
return _root.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<T>().First();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading

0 comments on commit ebca5d7

Please sign in to comment.