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

# 概要
特定のクラス及びその派生クラスには特定の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](https://visualstudiogallery.msdn.microsoft.com/2ddb7240-5249-4c8c-969e-5d05823bcb89)をインストールしましょう。  
  これは、`Diagnostic with Code Fix`のプロジェクトテンプレートや`Syntax Visualizer`が同梱されているSDKです。

## プロジェクトを作る
- 新しいプロジェクトの作成から、 テンプレート - Visual C# - Extensibility - `Analyzer with Code Fix (NuGet + VSIX)`を選びましょう。
![image](https://qiita-image-store.s3.amazonaws.com/0/47699/00dd9594-c76c-e343-153d-2385a565fbc0.png)

## 動作テストをしてみる
- {ソリューション名}.Vsixというプロジェクトをスタートアッププロジェクトに指定します。(デフォルト)
- F5を押すと、サンドボックスとなるVisualStudioがもう一つ起動し、そこで実際に動かしながらテストを行うことができます。
    - もちろん、ブレークポイントも設定することが可能です。

## Analyzerの作成
### まずはソースファイルを作成します。
この時、テンプレートに同梱されている`DiagnosticAnalyzer.cs`を参考にすると楽です。

### Initializeメソッドを作成します。
`AnalysisContext`を引数に取るメソッドです。  
こちらはどのタイミングでAnalyzeを実行するかを登録します。
今回の例では、`context.RegisterSyntaxNodeAction(AnalyzeSymbol, SyntaxKind.ClassDeclaration);`としてみます。

- ちなみに、このあたりもコード補完を頼りに手探り状態で進めています。何かいいドキュメントがあれば教えてください。
- 第一引数はアナライザ本体となるメソッドを渡します。お好みの名前でかまいません。

### `AnalyzeSymbol`メソッドを作成します。
`Initialize`で指定したのは`RegisterSyntaxNodeAction`なので、`SyntaxNodeAnalysisContext`が引数です。
このContextにコンパイル対象になっているクラスの情報が詰まっています。
今回の例ではここから型情報をを取り出します。
ちなみに、エラーではない場合、無視する場合はそのまま`return;`すれば問題ないです。

- まず、`SyntaxNodeAnalysisContext`から`SemanticModel`を取得します。  このクラスには様々なデータが入っているため、何はともあれこのクラスから探していくといいのではないかと思います。
- 次に、`ITypeSymbol`を取得します。  このクラスは何らかの型を表すシンボルを表すものです。  

```csharp
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`を辿っていけば、派生クラスかどうかを判定できます。
以下がその判定メソッドです。

```csharp
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>`が取得できるので、それを使いましょう。
以下がその判定メソッドです。

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

### エラー情報を出す
ここは決めうちでいいと思います。
エラーを出す場所と、内容を設定し、contextへ報告します。
`Diagnostic.Create`時に`MessageFormat`へ引数を渡すことができるので、より説明的な表示をすることが可能です。

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

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

```csharp
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
- どんな情報をどう辿ったらいいのかわからない
    - ブレークポイントを置いて、変数の内容からみたいものを探しましょう。
    - ブレークポイントを置いた状態であれば、コードも自由に編集し、即時反映できるので活用しましょう。
        - ただし、大きすぎる変更は除く
- プロジェクト固有の設定どかどうするの？
    - つ [Roslyn Analyzerでコンフィグを読み込ませて挙動を変更する](http://neue.cc/2016/03/30_527.html)

# 参考記事
- [VS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)](http://neue.cc/2014/11/20_485.html)
- [VS2015+RoslynによるCodeRefactoringProviderの作り方と活用法](http://neue.cc/2014/12/08_496.html)
- [UniRxでの空呼び出し検出、或いはRoslynによるCode Aware Libraries時代の到来について](http://neue.cc/2015/05/11_512.html)