2016年12月11日

RoslynのAanlyzerでC#のコードを分析するライブラリ作ってみたよ

これは、C# Advent Calendar 2016の12月11日(11日目)の記事なのだ。

昨日は、@tanaka_733さんのC# 7 on Linux
今日は、RoslynのAnalyzerを使ってみた話を書くよ。

☆☆☆コーディングスタイルを検証する仕組みへの不満☆☆☆

突然だけど、わたしのコーディングスタイルって、C#とJavaの中間みたいな書き方なんだよね。
例えばこんな感じ。

public class Project {
// フィールドだいたいは大文字ではじめるからC#風?
private string Field;
// メソッドはだいたい小文字ではじめるからJava風?
// 『{』は行末に書くからJava風?
public void doSomething() {
string f = this.Field;
}
}

で、VisualStudioってIntelliSenceが優秀だからか、
あまりコーディングスタイルを細かくチェックする仕組みがない気がする。
コーディングスタイルっていうか、静的解析?みたいな分類になるのかな?

いや、ほら、JavaのCheckstyleみたいに細かいチェックをするものって、
StyleCopとかFxCopとかだと思うんだけど、
これって、あんまり自由にカスタマイズする感じじゃない気がするの。

で、いろいろサイトめぐっていたら、
StyleCopがRoslyn使ってる〜とか言う話をとか見て、興味がわいてきた感じ。
ないなら、自分で作っちゃえば良いんだよね。

☆☆☆RoslynなAnalyzerの特徴☆☆☆

細かいこと書き出すと、ホントに書きたいことから遠ざかりそうだから、
基本的な作り方は、neueccさんの記事を参考にしてもらう感じで。
ここでは概要だけにとどめてさくさく具体的な話へいこ〜。

えっと、RoslynのDiagnosticAnalyzerってクラスを使うと、C#のコードの解析を簡単に作れる。
特に、次の点がとっても便利っ。もちろん、もっといっぱい便利な特徴はあるんだけど。
1.字句解析とか構文解析とか諸々省略して、SyntaxTreeがそろった状態から作業が始められる。
2.テンプレートプロジェクト?があるから、すぐ作るのをはじめられる。
3.作ったのは、NuGetで簡単に適用できる。
4.レポートした結果がVisualStudioと連動して見られる。

さぁ、じゃぁ、もう、プロジェクト作った前提で、
どのくらい簡単にかけるのか見てみよ〜。

☆☆☆具体的な例☆☆☆

Initializeメソッドで具体的な検証アクションを登録しておくんだけど、
解析するフェーズはいくつかあって、以下の4つのアクションから選んで解析コードを書く感じ。

public override void Initialize(AnalysisContext context) {
context.RegisterSyntaxTreeAction((c) => { });
context.RegisterSymbolAction((c) => { }, SymbolKind.Assembly);
context.RegisterSyntaxNodeAction((c) => { }, SyntaxKind.None);
context.RegisterSemanticModelAction((c) => { });
}

わたしの場合、使うのは、RegisterSyntaxNodeActionとRegisterSyntaxTreeActionかな。

RegisterSyntaxNodeActionは、名前の通り、読みたいNodeを指定して、そこから解析をしていく時に使うもの。
例えば、if文の書き方を確認したい!とかはこっちを使うといい感じ。

RegisterSyntaxTreeActionは、SyntaxTreeを自分で頭から読んでいくもの。
特に空白系を確認したい場合はこっちを使うといい感じ。

RegisterSyntaxNodeActionは、どうしてもNodeを基準にしないといけないから、Node無関係に空白を解析するのには向かないんだよね。

ちょっとサンプルを書くと、ブロックの手前にスペースが1個あるかどうかの確認はこんな感じ。

public override void Initialize(AnalysisContext context) {
context.RegisterSyntaxNodeAction((c) => { analyze(c); }, SyntaxKind.Block);
}
private static void analyze(SyntaxNodeAnalysisContext context) {
SyntaxNode node = context.Node;
SyntaxToken firstToken = node.GetFirstToken();
SyntaxToken previousToken = firstToken.GetPreviousToken();
SyntaxTriviaList trivias = previousToken.TrailingTrivia;
// 存在しない場合も0になる。
int triviasLength = trivias.FullSpan.End - trivias.FullSpan.Start;
if((triviasLength == 1) && trivias.First().IsKind(SyntaxKind.WhitespaceTrivia) &&
(previousToken.Parent.SyntaxTree.GetText().GetSubText(trivias.FullSpan).ToString() == " ")) {
// ブロックの直前にスペースが1個ある。
} else {
// Ruleはテンプレ生成したときにあるやつなので説明省略で。
Diagnostic diagnostic = Diagnostic.Create(Rule, node.GetLocation(), "メッセージ");
context.ReportDiagnostic(diagnostic);
}
}

う〜ん、簡単だね!
いや、まぁ、解析のロジック自体は、もちろん、みんなガンバレ!って話になるわけだけど、
構成は……簡単でしょ?

もう一種類も。
これは、行末に空白があったらダメだよっていうやつ。

public override void Initialize(AnalysisContext context) {
context.RegisterSyntaxTreeAction((c) => { analyze(c); });
}
private static void analyze(SyntaxTreeAnalysisContext context) {
SyntaxNode root = context.Tree.GetCompilationUnitRoot(context.CancellationToken);
IEnumerable trivias = root.DescendantTrivia();
{
SyntaxTrivia previousTrivia = default(SyntaxTrivia);
foreach(SyntaxTrivia trivia in trivias) {
if(trivia.IsKind(SyntaxKind.EndOfLineTrivia) && previousTrivia.IsKind(SyntaxKind.WhitespaceTrivia)) {
if(trivia.FullSpan.Start == previousTrivia.FullSpan.End) {
Diagnostic diagnostic = Diagnostic.Create(Rule, trivia.GetLocation(), "メッセージ");
context.ReportDiagnostic(diagnostic);
}
}
previousTrivia = trivia;
}
{
SyntaxTrivia trivia = trivias.LastOrDefault();
if(trivia.IsKind(SyntaxKind.WhitespaceTrivia) && (trivia.FullSpan.End == root.FullSpan.End)) {
Diagnostic diagnostic = Diagnostic.Create(Rule, trivia.GetLocation(), "メッセージ");
context.ReportDiagnostic(diagnostic);
}
}
}
}

いける!いける!
StyleCopとか、何を検証しているのかわからないけど、結構カオスなコード書いてあるように見えるんだけど、
ちょっとしたの書くなら、検証する内容がはっきりイメージできれば、結構、簡単にかけるよ!
……はっきり……イメージできれば……ね……。

☆☆☆作ってみたライブラリ☆☆☆

ということで、作ったライブラリは、NuGetにおいてあるから、良ければ試してみてね。
『ECheckStyle』で登録してあるから検索すれば出てくると思うよ。
NuGetパッケージのサイト
プロジェクトサイト
折角だから、今日アップデートしておいたっ。

☆☆☆注意点☆☆☆

作る上での注意点とかを簡単にあげておくと、
1.解析でできるSyntaxTreeが一意ではない。
  解析ロジック考える時に、いろいろなサイトでSyntax Visualizer見ると良いよって書いてあるんだけど、
  これ、実は、エディタ開いた状態で行われる解析と、開いてない状態で行われる解析で、作られるSyntaxTreeが違うの。
  あ、エディタ上の色分けもSyntax Visualizerのとは違うッぽいからこれも参考にするとダメみたいだよ。
  理由は知らないけど、そのせいでだいぶ苦労したっ。特にコメント回りっ!
2.CodeFixを作る難易度が高い。
  Roslynの良いところは、実は、解析の実装が簡単なだけじゃなくて、CodeFixも準備できることにもあると思うの。
  おかしいところをまとめてぽちぽち直せるのは凄いと思う。でも、これ、作るの結構辛いっ。
  解析のロジックを考えるのは、まぁ、変な構文が入ってきても、スルーするだけだと思うんだけど、
  CodeFixはそれもまとめて直してあげないといけないからね。CodeFix作り込むのはかなり難易度高いね。
3.空白の作成で改行あたりが消える。
  CodeFixと言えば、空白の調整とか、自前でやろうとすると、まぁ、空白を作成することになるんだけど、
  SyntaxFactory.ParseLeadingTriviaとSyntaxFactory.ParseTrailingTriviaってあって、
  後者は改行を作れないっぽいって言うか、作ろうとすると静かに消えるから注意なのだ。
  どこにもそのあたりの説明書いてない感じで微妙なんだけど。
4.NuGetで入れたAnalyzerの削除で依存ライブラリが消える。
  StyleCopもそうなんだけど、設定とかの読み込みとかに、Json.NETかな、使っているケースが多いみたい。
  EcheckStyleもそうなんだけどね。
  で、これ、NuGetで入れるときは自動で依存しているライブラリを入れるScriptが動くみたいなんだけど、
  削除する時には、削除するScript勝手に動くみたいで、どうも依存関係が確認されないで消されるみたい。
  うーんと、つまり、StyleCop入れて、ECheckStyle入れて、StyleCop消すと、
  Json.NETのライブラリが消えて、ECheckStyleが動かなくなる。っていうこと。
  この辺は、どうするのがいいのかな……。
5.ファイルの除外について
  これは、ECheckStyleだと設定ファイルでサポートしているから、別に注意って言うわけじゃないんだけど、
  この辺で解析対象の除外について議論されているみたいなんだよね。
  どうなるのかわからないけど、
  解析するライブラリは複数あるわけで、それぞれ、かけたい範囲かけたくない範囲っていうのはあるはずで、
  Roslyn側で全体まとめてかけるかけないって言うのは、ちょっと違うんじゃないかなぁ?とか思ったりしている。
  まぁ、ここでは、ライブラリ作る側でその辺も考えようねって言うだけなんだけどね。

☆☆☆まとめ☆☆☆

いろいろ微妙な点も書いたけど、やっぱり便利だよ。ホントに。
ただ、対象がC#とVBだけなのが淋しいかな。

アプリつくってたらさ、
Webアプリケーションだったら、HTMLとかTypeScriptも書くし、
クライアントだったら、XAMLとかも書くわけじゃない?

この辺もサポートしてくれたら、とっても良いと思うんだけどなぁ(゜▽、゜

posted by すふぃあ at 12:50| Comment(0) | TrackBack(0) | 雁字