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

Create Roslyn C# analyzer that is aware of constructor argument types for class in assembly

背景:

我有一个属性指示对象 IsMagic 中的 属性 字段。我还有一个 Magician class 运行 覆盖任何对象,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;
        }
    }
}

魔术师可以 MakeMagicanObject 上使用至少一个字段或 属性 IsMagic 来生成 [=17= 的通用 List ],像这样:

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();
        }
    }
}

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

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

问题描述:

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

建议的解决方案:

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

但是,当 IsMagic 被放置在字段上或 属性 具有 Type 时,似乎应该可以使用 Roslyn 来显示错误包裹在 Magic.

我需要帮助的地方:

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

理想的分析仪应该是:

  1. 不需要我保留可以包含在 Magic 中的允许类型的列表。毕竟,Magic 有一个用于此目的的构造函数列表。
  2. 允许类型的自然转换。例如,如果 Magic 有一个接受 IEnumerable<bool> 的构造函数,那么 Roslyn 应该允许 IsMagic 附加到 属性 类型 List<bool>bool[]。这种魔法的施放对魔术师的功能至关重要。

对于如何编写 Magic 中构造函数的 "aware" Roslyn 分析器的任何指导,我将不胜感激。

您需要使用 Roslyn 的语义模型 API 和 ITypeSymbol.

重写 CanMakeMagicFromType()

首先调用 Compilation.GetTypeByMetadataName() to get the INamedTypeSymbol for Magic. You can then enumerate its constructors & parameters and call .ClassifyConversion 以查看它们是否与 属性 类型兼容。

根据 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;
        }
    }
}

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;
        }
    }
}

这里唯一需要的技巧是有时删除单个属性,有时删除整个属性列表。

我将此标记为已接受的答案;但是,为了全面披露,如果没有 SLaks 的帮助,我永远不会想出这个。