Создайте анализатор Roslyn C #, который знает типы аргументов конструктора для класса в сборке - PullRequest
0 голосов
/ 18 октября 2018

Фон:

У меня есть атрибут, который указывает, что свойство поля в объекте 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;
        }
    }
}

Маг может MakeMagic на anObject с хотя бы одним полем или свойством, которое IsMagic для создания общего 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.Я могу с радостью сказать поле Dictionary<string,bool> в каком-то классе IsMagic, и мне не скажут, что это проблема до времени выполнения.Хуже того, пользователи моего магического кода будут создавать свои собственные мирские классы и украшать свои свойства и поля атрибутом IsMagic.Я хотел бы помочь им увидеть проблемы до того, как они станут проблемами.

Предлагаемое решение:

В идеале я мог бы установить какой-нибудь флаг AttributeUsage на свой IsMagicатрибут для указания Visual Studio использовать метод Magic.CanMakeMagicFromType() для проверки свойства или типа поля, к которому присоединяется атрибут IsMagic.К сожалению, такого атрибута, по-видимому, нет.

Однако, похоже, что можно использовать Roslyn для отображения ошибки, когда IsMagic помещается в поле или свойство, которое имеет Type, которые нельзя обернуть в Magic.

Где мне нужна помощь:

У меня проблемы с проектированием анализатора Roslyn.Суть проблемы в том, что Magic.CanMakeMagicFromType принимает System.Type, но Рослин использует ITypeSymbol для представления типов объектов.

Идеальный анализатор будет:

  1. Не требует от менявести список разрешенных типов, которые можно заключить в Magic.В конце концов, Magic имеет список конструкторов, которые служат этой цели.
  2. Разрешить естественное приведение типов.Например, если Magic имеет конструктор, который принимает IEnumerable<bool>, то Roslyn должен разрешить IsMagic присоединяться к свойству с типом List<bool> или bool[].Этот каст Магии имеет решающее значение для функциональности Мага.

Буду признателен за любые указания о том, как кодировать анализатор Roslyn, который «знает» о конструкторах в Magic.

Ответы [ 2 ]

0 голосов
/ 23 октября 2018

Основываясь на превосходных советах 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.

0 голосов
/ 18 октября 2018

Вам нужно переписать CanMakeMagicFromType() с помощью API семантической модели Рослина и ITypeSymbol.

Начните с вызова Compilation.GetTypeByMetadataName(), чтобы получить INamedTypeSymbol для Magic.Затем вы можете перечислить его конструкторы и параметры и вызвать .ClassifyConversion, чтобы узнать, совместимы ли они с типом свойства.

...