【问题标题】:Create Roslyn C# analyzer that is aware of constructor argument types for class in assembly创建知道程序集中类的构造函数参数类型的 Roslyn C# 分析器
【发布时间】:2018-10-23 11:38:11
【问题描述】:

背景:

我有一个属性表明对象IsMagic 中的字段属性。我还有一个 Magician 类,它通过提取 IsMagic 的每个字段和属性并将其包装在 Magic 包装器中来运行在任何对象和 MakesMagic 上。

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

魔术师可以在anObjectMakeMagic 使用至少一个IsMagic 的字段或属性来生成List 的通用List Magic,如下所示:

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 属性没有构建时检查。我可以很高兴地在某个类IsMagic 中说出Dictionary&lt;string,bool&gt; 字段,并且直到运行时我才会被告知这是一个问题。更糟糕的是,我的神奇代码的用户将创建他们自己的普通类并使用IsMagic 属性装饰他们的属性和字段。我想帮助他们在问题变成问题之前发现问题。

建议的解决方案:

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

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

我需要帮助的地方:

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

理想的分析仪应该:

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

我会很感激有关如何编写一个“了解”Magic 中的构造函数的 Roslyn 分析器的任何指导。

【问题讨论】:

    标签: c# roslyn analyzer


    【解决方案1】:

    您需要使用 Roslyn 的语义模型 API 和 ITypeSymbol 重写 CanMakeMagicFromType()

    首先调用Compilation.GetTypeByMetadataName() 以获取INamedTypeSymbolMagic。然后您可以枚举其构造函数和参数并调用.ClassifyConversion 以查看它们是否与属性类型兼容。

    【讨论】:

    • 这个建议成功了!谢谢,斯拉克斯!为了完整起见,我将使用分析器的完整代码和代码修复创建另一个答案,但我会确保直接引用您的答案。
    【解决方案2】:

    根据 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 的帮助,我永远也不会想到这一点。

    【讨论】:

      猜你喜欢
      • 2021-10-11
      • 1970-01-01
      • 1970-01-01
      • 2014-05-27
      • 1970-01-01
      • 2015-06-30
      • 1970-01-01
      • 2012-07-03
      • 1970-01-01
      相关资源
      最近更新 更多