创建可识别程序集中类的构造函数参数类型的Roslyn C#分析器

Nei*_*der 5 c# analyzer roslyn

背景:

我有一个属性,指示对象中字段的属性IsMagic。我还有一个Magician类可以运行在任何对象上,并MakesMagic提取每个字段和属性IsMagic并将其包装在Magic包装器中。

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

魔术师罐MakeMagicanObject具有至少一个字段或属性,IsMagic以产生一个通用ListMagic,如下所示:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,Magic包装器只能绕过某些类型的属性或字段。这意味着仅将包含特定类型数据的属性或字段标记为IsMagic。更复杂的是,我希望随着业务需求的发展,特定类型的列表也会发生变化(因为对Magic的编程需求如此之高)。

好消息是它Magic具有一定的构建时间安全性。如果我尝试添加代码,例如new Magic(true)Visual Studio会告诉我这是错误的,因为没有构造函数Magic需要使用bool。还存在一些运行时检查,因为该Magic.CanMakeMagicFromType方法可用于捕获动态变量的问题。

问题描述:

坏消息是没有对IsMagic属性进行构建时检查。我可以很高兴地说出Dictionary<string,bool>某个类中的一个字段,IsMagic直到运行时我都不会被告知这是一个问题。更糟糕的是,我的神奇代码的用户将创建自己的普通类,并使用该IsMagic属性装饰其属性和字段。我想帮助他们在问题变成问题之前就发现问题。

建议的解决方案:

理想情况下,我可以在属性上放置某种AttributeUsage标志,IsMagic以告诉Visual Studio使用该Magic.CanMakeMagicFromType()方法来检查IsMagic附加属性的属性或字段类型。不幸的是,似乎没有这样的属性。

但是,当IsMagic将Roslyn 放在具有Type不能包装在的字段或属性上时,似乎应该可以使用Roslyn来显示错误Magic

我需要帮助的地方:

我在设计Roslyn分析器时遇到麻烦。问题的核心是Magic.CanMakeMagicFromTypein System.Type,但是Roslyn ITypeSymbol用来表示对象类型。

理想的分析仪将:

  1. 不需要我保留可以包装的允许类型的列表Magic。毕竟,Magic有满足此目的的构造函数列表。
  2. 允许自然类型转换。例如,如果Magic有一个接受的构造函数IEnumerable<bool>,则Roslyn应该允许IsMagic附加到类型为List<bool>或的属性bool[]。魔术的投射对于魔术师的功能至关重要。

对于如何编写“了解”中的构造函数的Roslyn分析器的任何指导,我将不胜感激Magic

Nei*_*der 4

根据 SLAks 的出色建议,我能够编写出完整的解决方案。

发现错误应用属性的代码分析器如下所示:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

CanMakeMagic 函数具有 SLaks 为我阐明的神奇解决方案。

代码修复提供程序如下所示:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这里唯一需要的聪明之处是有时删除单个属性,有时删除整个属性列表。

我将此标记为已接受的答案;但是,为了充分披露,如果没有 SLAks 的帮助,我永远不会弄清楚这一点。