Background:
I have an attribute that indicates that a property of field in an object IsMagic
. I also have a Magician
class that runs over any object and MakesMagic
by extracting each field and property that IsMagic
and wraps it in a Magic
wrapper.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace MagicTest
{
/// <summary>
/// An attribute that allows us to decorate a class with information that identifies which member is magic.
/// </summary>
[AttributeUsage(AttributeTargets.Property|AttributeTargets.Field, AllowMultiple = false)]
class IsMagic : Attribute { }
public class Magic
{
// Internal data storage
readonly public dynamic value;
#region My ever-growing list of constructors
public Magic(int input) { value = input; }
public Magic(string input) { value = input; }
public Magic(IEnumerable<bool> input) { value = input; }
// ...
#endregion
public bool CanMakeMagicFromType(Type targetType)
{
if (targetType == null) return false;
ConstructorInfo publicConstructor = typeof(Magic).GetConstructor(new[] { targetType });
if (publicConstructor != null) return true; // We can make Magic from this input type!!!
return false;
}
public override string ToString()
{
return value.ToString();
}
}
public static class Magician
{
/// <summary>
/// A method that returns the members of anObject that have been marked with an IsMagic attribute.
/// Each member will be wrapped in Magic.
/// </summary>
/// <param name="anObject"></param>
/// <returns></returns>
public static List<Magic> MakeMagic(object anObject)
{
Type type = anObject?.GetType() ?? null;
if (type == null) return null; // Sanity check
List<Magic> returnList = new List<Magic>();
// Any field or property of the class that IsMagic gets added to the returnList in a Magic wrapper
MemberInfo[] objectMembers = type.GetMembers();
foreach (MemberInfo mi in objectMembers)
{
bool isMagic = (mi.GetCustomAttributes<IsMagic>().Count() > 0);
if (isMagic)
{
dynamic memberValue = null;
if (mi.MemberType == MemberTypes.Property) memberValue = ((PropertyInfo)mi).GetValue(anObject);
else if (mi.MemberType == MemberTypes.Field) memberValue = ((FieldInfo)mi).GetValue(anObject);
if (memberValue == null) continue;
returnList.Add(new Magic(memberValue)); // This could fail at run-time!!!
}
}
return returnList;
}
}
}
The Magician can MakeMagic
on anObject
with at least one field or property that IsMagic
to produce a generic List
of Magic
, like so:
using System;
using System.Collections.Generic;
namespace MagicTest
{
class Program
{
class Mundane
{
[IsMagic] public string foo;
[IsMagic] public int feep;
public float zorp; // If this [IsMagic], we'll have a run-time error
}
static void Main(string[] args)
{
Mundane anObject = new Mundane
{
foo = "this is foo",
feep = -10,
zorp = 1.3f
};
Console.WriteLine("Magic:");
List<Magic> myMagics = Magician.MakeMagic(anObject);
foreach (Magic aMagic in myMagics) Console.WriteLine(" {0}",aMagic.ToString());
Console.WriteLine("More Magic: {0}", new Magic("this works!"));
//Console.WriteLine("More Magic: {0}", new Magic(Mundane)); // build-time error!
Console.WriteLine("\nPress Enter to continue");
Console.ReadLine();
}
}
}
Notice that Magic
wrappers can only go around properties or fields of certain types. This means that only property or field that contains data of specific types should be marked as IsMagic
. To make matters more complicated, I expect the list of specific types to change as business needs evolve (since programming Magic is in such high demand).
The good news is that the Magic
has some build time safety. If I try to add code like new Magic(true)
Visual Studio will tell me it's wrong, since there is no constructor for Magic
that takes a bool
. There is also some run-time checking, since the Magic.CanMakeMagicFromType
method can be used to catch problems with dynamic variables.
Problem Description:
The bad news is that there's no build-time checking on the IsMagic
attribute. I can happily say a Dictionary<string,bool>
field in some class IsMagic
, and I won't be told that it's a problem until run-time. Even worse, the users of my magical code will be creating their own mundane classes and decorating their properties and fields with the IsMagic
attribute. I'd like to help them see problems before they become problems.
Proposed Solution:
Ideally, I could put some kind of AttributeUsage flag on my IsMagic
attribute to tell Visual Studio to use the Magic.CanMakeMagicFromType()
method to check the property or field type that the IsMagic
attribute is being attached to. Unfortunately, there doesn't seem to be such an attribute.
However, it seems like it should be possible to use Roslyn to present an error when IsMagic
is placed on a field or property that has a Type
that can't be wrapped in Magic
.
Where I need help:
I am having trouble designing the Roslyn analyser. The heart of the problem is that Magic.CanMakeMagicFromType
takes in System.Type
, but Roslyn uses ITypeSymbol
to represent object types.
The ideal analyzer would:
Magic
. After all, Magic
has a list of constructors that serve this purpose.Magic
has a constructor that takes in IEnumerable<bool>
, then Roslyn should allow IsMagic
to be attached to a property with type List<bool>
or bool[]
. This casting of Magic is critical to the Magician's functionality.I'd appreciate any direction on how to code a Roslyn analyzer that is "aware" of the constructors in Magic
.
You need to rewrite CanMakeMagicFromType()
using Roslyn's semantic model APIs and ITypeSymbol
.
Start by calling Compilation.GetTypeByMetadataName()
to get the INamedTypeSymbol
for Magic
. You can then enumerate its constructors & parameters and call .ClassifyConversion
to see whether they're compatible with the property type.
Based on the excellent advice from SLaks, I was able to code up a complete solution.
The code analyzer that spots mis-applied attributes looks like this:
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
namespace AttributeAnalyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AttributeAnalyzerAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "AttributeAnalyzer";
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
id: DiagnosticId,
title: "Magic cannot be constructed from Type",
messageFormat: "Magic cannot be built from Type '{0}'.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The IsMagic attribue needs to be attached to Types that can be rendered as Magic."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(
AnalyzeSyntax,
SyntaxKind.PropertyDeclaration, SyntaxKind.FieldDeclaration
);
}
private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
ITypeSymbol memberTypeSymbol = null;
if (context.ContainingSymbol is IPropertySymbol)
{
memberTypeSymbol = (context.ContainingSymbol as IPropertySymbol)?.GetMethod?.ReturnType;
}
else if (context.ContainingSymbol is IFieldSymbol)
{
memberTypeSymbol = (context.ContainingSymbol as IFieldSymbol)?.Type;
}
else throw new InvalidOperationException("Can only analyze property and field declarations.");
// Check if this property of field is decorated with the IsMagic attribute
INamedTypeSymbol isMagicAttribute = context.SemanticModel.Compilation.GetTypeByMetadataName("MagicTest.IsMagic");
ISymbol thisSymbol = context.ContainingSymbol;
ImmutableArray<AttributeData> attributes = thisSymbol.GetAttributes();
bool hasMagic = false;
Location attributeLocation = null;
foreach (AttributeData attribute in attributes)
{
if (attribute.AttributeClass != isMagicAttribute) continue;
hasMagic = true;
attributeLocation = attribute.ApplicationSyntaxReference.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span);
break;
}
if (!hasMagic) return;
// Check if we can make Magic using the current property or field type
if (!CanMakeMagic(context,memberTypeSymbol))
{
var diagnostic = Diagnostic.Create(Rule, attributeLocation, memberTypeSymbol.Name);
context.ReportDiagnostic(diagnostic);
}
}
/// <summary>
/// Check if a given type can be wrapped in Magic in the current context.
/// </summary>
/// <param name="context"></param>
/// <param name="sourceTypeSymbol"></param>
/// <returns></returns>
private static bool CanMakeMagic(SyntaxNodeAnalysisContext context, ITypeSymbol sourceTypeSymbol)
{
INamedTypeSymbol magic = context.SemanticModel.Compilation.GetTypeByMetadataName("MagicTest.Magic");
ImmutableArray<IMethodSymbol> constructors = magic.Constructors;
foreach (IMethodSymbol methodSymbol in constructors)
{
ImmutableArray<IParameterSymbol> parameters = methodSymbol.Parameters;
IParameterSymbol param = parameters[0]; // All Magic constructors take one parameter
ITypeSymbol paramType = param.Type;
Conversion conversion = context.Compilation.ClassifyConversion(sourceTypeSymbol, paramType);
if (conversion.Exists && conversion.IsImplicit) return true; // We've found at least one way to make Magic
}
return false;
}
}
}
The CanMakeMagic function has the magic solution that SLaks spelled out for me.
The code fix provider looks like this:
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace AttributeAnalyzer
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AttributeAnalyzerCodeFixProvider)), Shared]
public class AttributeAnalyzerCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(AttributeAnalyzerAnalyzer.DiagnosticId); }
}
public sealed override FixAllProvider GetFixAllProvider()
{
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
return WellKnownFixAllProviders.BatchFixer;
}
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Diagnostic diagnostic = context.Diagnostics.First();
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
context.RegisterCodeFix(
CodeAction.Create(
title: "Remove attribute",
createChangedDocument: c => RemoveAttributeAsync(context.Document, diagnosticSpan, context.CancellationToken),
equivalenceKey: "Remove_Attribute"
),
diagnostic
);
}
private async Task<Document> RemoveAttributeAsync(Document document, TextSpan diagnosticSpan, CancellationToken cancellation)
{
SyntaxNode root = await document.GetSyntaxRootAsync(cancellation).ConfigureAwait(false);
AttributeListSyntax attributeListDeclaration = root.FindNode(diagnosticSpan).FirstAncestorOrSelf<AttributeListSyntax>();
SeparatedSyntaxList<AttributeSyntax> attributes = attributeListDeclaration.Attributes;
if (attributes.Count > 1)
{
AttributeSyntax targetAttribute = root.FindNode(diagnosticSpan).FirstAncestorOrSelf<AttributeSyntax>();
return document.WithSyntaxRoot(
root.RemoveNode(targetAttribute,
SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.KeepEndOfLine | SyntaxRemoveOptions.KeepDirectives)
);
}
if (attributes.Count==1)
{
return document.WithSyntaxRoot(
root.RemoveNode(attributeListDeclaration,
SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.KeepEndOfLine | SyntaxRemoveOptions.KeepDirectives)
);
}
return document;
}
}
}
The only cleverness required here is sometimes removing a single attribute, and other times removing an entire attribute list.
I'm marking this as the accepted answer; but, in the interest of full disclosure, I would never have figured this out without SLaks help.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With