Chintax Sugar

個人用技術メモとか

Roslyn のコード解析機能を試してみる

Roslyn とは

Visual Studio 2015 から追加された新機能の一つです。
Roslyn を使うことで、コードを解析してエラーや警告を出したりコードの修正提案を行う機能を自作して追加することができます。
なおこの記事を書くにあたって neue先生の記事 をめっちゃ参考にしました。いつもお世話になります。

今回の題材

時々見かけるイケてないコードの1つに

if (collection.Count() > 0) { ~ }

のようなものがあります。これはAny()でいいですね。
ReSharper が入っている環境であれば警告が出るのですが、ただの Visual Studio では何も出ません。
今回はこのコードに警告が出るようにし、さらに修正提案を出すところまでをやってみようと思います。
ReSharper って何よ!という方は 真雪先生のスライド あたりを参考にどうぞ。

手順

  1. テンプレートから「Diagnostic with Code Fix」を選択し、新規プロジェクトを作成。
  2. Analyzer(コード解析)用クラスを作成
  3. CodeFixProvider(修正提案)用クラスを作成
  4. コンパイルしできたものを配布

これだけです。簡単そうですね?
次から実際のコードを見ていきます。

Analyzer

プロジェクトを作成した時点で必要なプロジェクト、ファイルは揃っているので後はその中身を書き換えていくだけです。
まずは Analyzer、コード解析用のクラスの中身を見てみましょう。

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

namespace UseAnyMethod
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class UseAnyMethodAnalyzer : DiagnosticAnalyzer
    {
        public const string DiagnosticId = "UseAnyMethod";
        internal const string Title = "Use method Any()";
        internal const string MessageFormat = "Use method Any()";
        internal const string Category = "Refactoring";

        internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true);

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

        public override void Initialize(AnalysisContext context)
        {
            //SyntaxKindを今回検出したいものに変更
            context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.GreaterThanExpression);
        }

        private static void Analyze(SyntaxNodeAnalysisContext context)
        {
            var syntax = (BinaryExpressionSyntax)context.Node;

            //左辺がCountメソッドかどうかを判定
            var leftExpressionName = ((syntax.Left as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Name?.NormalizeWhitespace().ToFullString();
            if (leftExpressionName == null || leftExpressionName != "Count") return;

            //右辺が0かどうかを判定
            var rightValue = (syntax.Right as LiteralExpressionSyntax)?.Token.Value;
            if (rightValue == null || !rightValue.Equals(0)) return;

            var diagnostic = Diagnostic.Create(Rule, syntax.GetLocation());
            context.ReportDiagnostic(diagnostic);
        }
    }
}

重要なのはInitializeメソッドだけです。
ここで警告を出したい「~.Count() > 0」の検出を行うためのコードを書きます。
しかし書きます、といっても正直テンプレートにある記述だけでは何を書けばいいのか全くわかりません。
そこで役に立つのがneue先生の記事にもある「Roslyn Syntax Visualizer」です。
これを使えば構文をツリー上に可視化して見ることができます。神ツール

f:id:sunlight0915:20141208092915p:plain

構文を選択すると…

f:id:sunlight0915:20141208092947p:plain

こんな感じで表示されます。

ここまでで一度デバッグをしてみます。その結果、

f:id:sunlight0915:20141208093157p:plain

無事警告が表示されました。

CodeFixProvider

続いて CodeFixProvider、コードの修正提案用のクラスの中身を見ていきましょう。

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;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace UseAnyMethod
{
    [ExportCodeFixProvider("UseAnyMethodCodeFixProvider", LanguageNames.CSharp), Shared]
    public class UseAnyMethodCodeFixProvider : CodeFixProvider
    {
        public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
        {
            return ImmutableArray.Create(UseAnyMethodAnalyzer.DiagnosticId);
        }

        public sealed override FixAllProvider GetFixAllProvider()
        {
            return WellKnownFixAllProviders.BatchFixer;
        }

        public sealed override async Task ComputeFixesAsync(CodeFixContext context)
        {
            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

            var diagnostic = context.Diagnostics.First();
            var diagnosticSpan = diagnostic.Location.SourceSpan;

            //該当のNodeを検索
            var binaryExpression = root.FindNode(diagnosticSpan) as BinaryExpressionSyntax;

            //CodeActionを登録
            var codeAction = CodeAction.Create("ReplaceTo Any", c => ReplaceToAny(context.Document, root, binaryExpression, c));
            context.RegisterFix(codeAction, diagnostic);
        }

        private static Task<Document> ReplaceToAny(Document document, SyntaxNode root, BinaryExpressionSyntax binaryExpression, CancellationToken cancellationToken)
        {
            //Countメソッドを使用している個所を検索
            var countSyntax = binaryExpression.DescendantNodes().OfType<IdentifierNameSyntax>().First(x => x.ToFullString() == "Count");
            //CountをAnyに置き換え
            var newSyntax = binaryExpression.Left.ReplaceNode(countSyntax, SyntaxFactory.IdentifierName("Any"));
            //比較式を左辺だけに置き換え
            var newRoot = root.ReplaceNode(binaryExpression, newSyntax);

            var newDocument = document.WithSyntaxRoot(newRoot);
            return Task.FromResult(newDocument);
        }
    }
}

ここでも重要なのは1点だけ、ComputeFixesAsyncメソッドの実装だけです。
やりたいことは「~.Count() > 0」の「~.Any()」への置き換えです。
コードの実装はひたすらトライアンドエラーを繰り返すしかありません。。
これをデバッグしてみると、

f:id:sunlight0915:20141208093026p:plain

こうなって、

f:id:sunlight0915:20141208093208p:plain

こうじゃ。(不要なスペースが残るのは謎…)

課題

うまくいったように見えるのですが、このプログラムには致命的な欠陥があります。
実はコード解析時に Count() メソッドのクラスを特定できていません。
本当は System.Linq.Enumerable の Count() メソッドだけを検出したいのですが、現状適当に作った Hoge クラスの Count() メソッドであっても警告が出てしまいます。
どなたか解決策ご存知ないですかね…。

解決できたら追記します。

まとめ

今回作成したものはプロジェクトの参照に追加したり、VSの拡張(VSIX)として配布することができます。
プロジェクトに含めておけば開発メンバーのローカル環境に依存せずに警告を表示することができるので、なかなか使えそうな機能ではないでしょうか?

なお冒頭にも書きましたがこの機能は「Visual Studio 2015」から使用可能で、現在 VS2015 は Preview 版のみが公開されています。
Azure の仮想マシンを使うと VS2015 が入った開発環境がサクッと用意できるのでオススメです。Azureゴイスー。