Edited at

VisualStudioで特定のクラスには特定のAttributeが付いていない場合はエラー扱いにする

More than 3 years have passed since last update.


概要

特定のクラス及びその派生クラスには特定のAttributeが必要だとか、

.Where(...).Any().Any(...)でいいじゃんとか、

コンパイルエラーはでないけど、エラーが出てほしいような箇所というのが多々あると思います。

そこで、Roslynの機能を使って、エラーを検出するための方法がAnalyzer with Code Fixです!

・・・が、日本語のドキュメントもろくにないし、どう手を出せばわからないといった方の道しるべになればと思います。

今回は特定のクラスには特定のAttributeが必要というAnalyzerを作ってみたいと思います。


環境

Visual Studio Enterprise 2015 Version 14.0.24720.00 Update 1 を使用しています。


手順


SDKをインストール



  • .NET Compiler Platform SDKをインストールしましょう。

    これは、Diagnostic with Code FixのプロジェクトテンプレートやSyntax Visualizerが同梱されているSDKです。


プロジェクトを作る


  • 新しいプロジェクトの作成から、 テンプレート - Visual C# - Extensibility - Analyzer with Code Fix (NuGet + VSIX)を選びましょう。
    image


動作テストをしてみる


  • {ソリューション名}.Vsixというプロジェクトをスタートアッププロジェクトに指定します。(デフォルト)

  • F5を押すと、サンドボックスとなるVisualStudioがもう一つ起動し、そこで実際に動かしながらテストを行うことができます。


    • もちろん、ブレークポイントも設定することが可能です。




Analyzerの作成


まずはソースファイルを作成します。

この時、テンプレートに同梱されているDiagnosticAnalyzer.csを参考にすると楽です。


Initializeメソッドを作成します。

AnalysisContextを引数に取るメソッドです。


こちらはどのタイミングでAnalyzeを実行するかを登録します。

今回の例では、context.RegisterSyntaxNodeAction(AnalyzeSymbol, SyntaxKind.ClassDeclaration);としてみます。


  • ちなみに、このあたりもコード補完を頼りに手探り状態で進めています。何かいいドキュメントがあれば教えてください。

  • 第一引数はアナライザ本体となるメソッドを渡します。お好みの名前でかまいません。


AnalyzeSymbolメソッドを作成します。

Initializeで指定したのはRegisterSyntaxNodeActionなので、SyntaxNodeAnalysisContextが引数です。

このContextにコンパイル対象になっているクラスの情報が詰まっています。

今回の例ではここから型情報をを取り出します。

ちなみに、エラーではない場合、無視する場合はそのままreturn;すれば問題ないです。


  • まず、SyntaxNodeAnalysisContextからSemanticModelを取得します。 このクラスには様々なデータが入っているため、何はともあれこのクラスから探していくといいのではないかと思います。

  • 次に、ITypeSymbolを取得します。 このクラスは何らかの型を表すシンボルを表すものです。


var semanticModel = context.SemanticModel;

var classSyntax = context.Node as ClassDeclarationSyntax;
if (null == classSyntax) return;

var type = semanticModel.GetDeclaredSymbol(classSyntax);
if (null == type) return;


正直、ITypeSymbolを取得するまでがわかりづらいだけで、あとはこいつから好きな情報を引き出すだけなので、

以下は読み飛ばしてしまっても問題ありません。



指定のクラスまたは、その派生クラスかどうかを判定します。

ITypeSymbolにはBaseTypeというプロパティがあり、それが親クラスを表しています。

そのため、再帰的にこのITypeSymbolを辿っていけば、派生クラスかどうかを判定できます。

以下がその判定メソッドです。

private static bool ExtendsClass(ITypeSymbol type, string targetName)

{
while (type != null)
{
if (type.Name == targetName) return true;
type = type.BaseType;
}
return false;
}


指定のAttributeを含んでいるかどうかを判定します。

ITypeSymbol.GetAttributes()ImmutableArray<AttributeData>が取得できるので、それを使いましょう。

以下がその判定メソッドです。

private static bool ContainsAttribute(ImmutableArray<AttributeData> attributes, string targetName)

{
return attributes.Any(x => x.AttributeClass.Name == targetName);
}


エラー情報を出す

ここは決めうちでいいと思います。

エラーを出す場所と、内容を設定し、contextへ報告します。

Diagnostic.Create時にMessageFormatへ引数を渡すことができるので、より説明的な表示をすることが可能です。

var location = Location.Create(semanticModel.SyntaxTree, context.Node.Span);

var diagnostic = Diagnostic.Create(Rule, location, type.Name, "TargetAttribute");
context.ReportDiagnostic(diagnostic);


完成

以下にAnalyzerのコード全文を載せます。

using System.Linq;

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Analyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class RequireAttributeAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "RequireAttributeAnalyzer";

private static readonly LocalizableString Title = "Attribute漏れチェッカー";
private static readonly LocalizableString MessageFormat = "{0}には{1}が必要です";
private static readonly LocalizableString Description = Title;
private const string Category = "Attribute";

private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeSymbol, SyntaxKind.ClassDeclaration);
}

private void AnalyzeSymbol(SyntaxNodeAnalysisContext context)
{
var semanticModel = context.SemanticModel;

var classSyntax = context.Node as ClassDeclarationSyntax;
if (null == classSyntax) return;

var type = semanticModel.GetDeclaredSymbol(classSyntax);
if (null == type) return;

if (!ContainsNamespace(type.ContainingNamespace, "TargetNamespace")) return;

if (!ExtendsClass(type, "TargetClass")) return;

// 目的のAttributeが含まれていればエラーではない
if (ContainsAttribute(type.GetAttributes(), "TargetAttribute")) return;

var location = Location.Create(semanticModel.SyntaxTree, context.Node.Span);
var diagnostic = Diagnostic.Create(Rule, location, type.Name, "TargetAttribute");
context.ReportDiagnostic(diagnostic);
}

private static bool ContainsNamespace(INamespaceSymbol nameSpace, string targetName)
{
while (nameSpace != null)
{
if (nameSpace.Name == targetName) return true;
nameSpace = nameSpace.ContainingNamespace;
}
return false;
}

private static bool ExtendsClass(ITypeSymbol type, string targetName)
{
while (type != null)
{
if (type.Name == targetName) return true;
type = type.BaseType;
}
return false;
}

private static bool ContainsAttribute(ImmutableArray<AttributeData> attributes, string targetName)
{
return attributes.Any(x => x.AttributeClass.Name == targetName);
}
}
}


蛇足的FAQ


  • どんな情報をどう辿ったらいいのかわからない


    • ブレークポイントを置いて、変数の内容からみたいものを探しましょう。

    • ブレークポイントを置いた状態であれば、コードも自由に編集し、即時反映できるので活用しましょう。


      • ただし、大きすぎる変更は除く





  • プロジェクト固有の設定どかどうするの?




参考記事