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



背景:

我有一个属性,指示对象中字段的属性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产生Magic通用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属性进行构建时检查。 我可以很高兴地说Dictionary<string,bool>在某个类IsMagic,直到运行时我才会被告知这是一个问题。 更糟糕的是,我的魔法代码的用户将创建自己的平凡类,并使用IsMagic属性装饰他们的属性和字段。 我想帮助他们在问题成为问题之前看到问题。

建议的解决方案:

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

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

我需要帮助的地方:

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

理想的分析仪应:

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

我将不胜感激有关如何编写"知道"Magic中构造函数的 Roslyn 分析器的任何方向。

基于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的帮助,我永远不会弄清楚这一点。

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

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

最新更新