c#静的解析によるコーディング規約チェッカーを作った話
はじめに
はじめまして。 「ボーイフレンド(仮)きらめき☆ノート」(以降、ボイきら)でUnityエンジニアをしております、田村です。
ボイきらでは、コードがコーディング規約に準拠しているかを自動でチェックするツールを自作し、導入しております。 今回は、チェックツールを作った経緯から、具体的なチェックツールの実装方法まで紹介したいと思います。 ボイきらでは、ゲーム開発エンジンとしてUnity、開発言語としてc#が採用されておりますので、今回の話の対象となる言語はc#となります。
コーディング規約チェッカーを作った経緯
コーディング規約とは
コーディング規約とは、プログラムの際の決め事です。 実際ボイきらでは、
- 命名規則は正しいか
- 全てのクラス、フィールド、メソッドの宣言にコメントが有るか
- フォーマットされているか(MonoDevelopの自動フォーマット機能を利用)
といったコーディング規約を定めています。 このようなコーディング規約を作り、それに従うことによって、チーム内の誰が見てもすぐ理解できるコードを書くことができます。
一方、コーディング規約は、プロジェクトの初期は浸透しないことが多かったり、コードレビューのコストが増加したりという問題があります。 中途半端にコーディング規約が導入されると、コーディング規約のメリットが十分に得られず、余計なコストだけ発生するという状態になってしまいます。 実際、ボイきらの開発の初期の頃は、このような問題が発生していました。 コーディング規約に従っているかどうかのチェックを自動的に行う ことができれば、レビューのコストが削減され、全てのメンバーにコーディング規約が浸透するだろうと考えました。
自動チェックの要件
自動チェックの要件としては、以下のようなものが挙げられれます。
- ボイきらの既存のコーディング規約をできる限りチェック可能
- macで動く(メンバーの開発環境がmacのため)
- コマンドラインから使える
- 対象ファイルを柔軟に指定可能
c#のコーディングスタイルを検証可能なソフトは世の中にいくつかありますが、これらの要件を満たすものを見つけることはできませんでした。 そこで、ボイきら開発のためのチェックツールを自作することにしました。
静的解析の必要性
正規表現の限界
手始めに、変数の命名が正しいかをチェックする方法を考えます。 例えば、ローカル変数の命名規則がlowerCamelCaseだとします。 下図のように、HogeClassのコードが与えられたとき、変数宣言のみを列挙することができれば、その変数名を調べることで、命名規則に従っているかを判断することができます。
変数名が命名規則に従っているかどうかのチェックは、lowerCamelCaseなら
"^[a-z][A-Za-z\d]*$"
といった具合の正規表現を用意し、これにマッチするかどうかで判断可能なので簡単です。
それでは、変数宣言の列挙はどうでしょうか。 まず思いつくのは、やはり正規表現です。 しかし、
class HogeClass
{
int field;
void Func()
{
int local;
}
}
のようなHogeClassを考えたとき、変数もしくはフィールドだけにマッチする正規表現は、存在するでしょうか。 実は、c#を含むほとんどのプログラミング言語の言語体系は文脈自由言語というカテゴリ(またはそれ以上)に属しており、正規表現で表現可能なカテゴリを超えています。 そのため、c#コードの特定の箇所にマッチする正規表現を自由に作り出すことは、理論的に不可能なはずです。
構文解析の力
そこで、正規表現による検索ではなく、構文解析によってコードの構造を解釈することを考えます。 コードの構造を厳密に解釈できれば、欲しい情報を自由に抽出することができます。 ざっくりとしたイメージですが、構文解析を行うと図のような抽象構文木(AST)と言われる木構造を得ることができます。
構文解析など、コードを実行せずに行う検証を静的解析と呼びます。 以上から、静的解析の力を用いて、コーディング規約チェッカーを作ることにしました。
コーディング規約チェッカーのための静的解析
NRefactory
ボイきら用コーディング規約チェッカーでは、c#を解析するためのライブラリとしてNRefactoryを採用しています。 NRefactoryはNuGetで簡単にc#プロジェクトに導入することができます。 NRefactoryは、MonoDevelop、Xamarin、Omnisharpで使われている(もしくは、使われていた)ライブラリです。 最近”Roslyn”という似たような機能を持つライブラリに統合されたので、これからc#の静的解析を行いたい場合はそちらを採用するべきかもしれません。
システムの概観
システムの概観は下図のとおりです。
チェッカーはUnityとは別プロジェクトとして作成し、Unityのエディタ拡張によって連携します。 チェッカーから受け取ったチェック結果は、エディタ拡張を通してGUIで表示するようにしており、開発者が迅速にコードを修正できるようにしています。
構文解析の基本
NRefactoryを用いた構文解析は、以下のように行います。
using ICSharpCode.NRefactory.CSharp;
class MainClass
{
public static void Main(string[] args)
{
string code = "class HogeClass { private int field; }";
CSharpParser parser = new CSharpParser();
SyntaxTree root = parser.Parse(code, "");
}
}
これだけで、c#コードのSyntaxTree(ASTのルートを表すようなクラス)を得ることができます。 以下、コードのサンプルをいくつか載せていますが、using等は簡潔のために省略していますのでご了承下さい。
NRefactoryのASTには構文解析で分かるありとあらゆる情報が詰まっています。
- フィールドか?メソッドか?クラスか?文か?コメントか?
- 修飾子
- 名前
- 前/後/親/子のノードは何か?
これらの情報を用いることで、命名が規約に従っているかや、コメントがついているかを判断することができます。
命名規則チェックへの応用
例えば、privateフィールドの命名をチェックしたい場合は、以下のように行います。
// 全てのフィールド宣言列挙
foreach (FieldDeclaration field in root.Descendants.OfType())
{
// プライベートのみ
if (field.HasModifier(Modifiers.Private))
{
// 全ての変数初期化列挙
foreach (VariableInitializer variable in field.Variables)
{
// 正規表現にマッチするかをチェック
if (!PrivateFieldNameRegex.IsMatch(variable.Name))
{
// プライベートフィールドが命名規則に従っていない
// 場所は variable.StartLocation で参照可能
}
}
}
}
このように、命名規則をチェックする基本的なフローは前述したとおりで、
- 対象となる宣言箇所を全て列挙
- 名前が正規表現にマッチするかをチェック
となります。
コメントチェックへの応用
続いて、コメントが各種宣言に付いているかをチェックする方法を紹介します。 例えば、メソッドにコメントがついているかどうかのチェックは、以下のように行います。
foreach (MethodDeclaration method in root.Descendants.OfType())
{
if (method.FirstChild is Comment) // 最初の子供のノードがコメント
{
// ドキュメントコメントあり
}
else if (method.PrevSibling is NewLineNode // 前のノードが改行
&& method.PrevSibling.PrevSibling is Comment) // 2つ前のノードがコメント
{
// 通常コメントあり
}
else
{
// コメントなし
}
}
1つ目のifは、スラッシュを3つ書いたドキュメントコメントがある場合に対応します。 メソッド宣言にドキュメントコメントが付いている場合、メソッド宣言のノードの最初の子供が、コメントのノードとなるようです。 2つ目のifは、スラッシュを2つ書いた通常コメントがある場合に対応します。 メソッド宣言のノードの前のノードが改行ノードで、さらにその前のノードがコメントであるとき、メソッド宣言にコメントが付いていると判断することができます。
フォーマットチェックへの応用
NRefactoryには、与えられたコードをフォーマットする機能が備わっています。 この機能を用いると、MonoDevelopやXamarinと完全に同等のフォーマットが可能です。 (※ただし、MonoDevelopやXamarinのバージョンによると思います。) 具体的には、以下のようにしてコードフォーマットを行います。
var policy = FormattingOptionsFactory.CreateMono();
policy.IndentSwitchBody = true;
// 略
policy.IndexerClosingBracketOnNewLine = NewLinePlacement.NewLine;
var options = new TextEditorOptions();
options.WrapLineLength = 80;
// 略
options.IndentBlankLines = false;
var formatter = new CSharpFormatter(policy, options);
var formattedCode = formatter.Format(code);
上のコード上では省略していますが、フォーマットには多数のオプションがあり、自由に設定することができます。 既存のc#プロジェクトで、どんなオプションが採用されているかは、”mdpolicy”ファイルや”csproj”ファイルを見ると分かるので、設定の参考になります。
フォーマットされた結果が、入力されたコードと等しいかどうかによって、コードが正しくフォーマットされているかを判断することができます。
エディタ拡張への統合
前述のような方法で、コーディング規約のチェッカーを実装し、エディタ拡張を用いてUnity経由で実行できるようにします。 Unityからコーディング規約チェックするフローは以下のとおりです。
- エディタ拡張で実装されたチェッカーWindow(下図)を開く
- rangeを指定し、”git diff”を行う
- 差分のあるファイルが列挙される(自分が編集したファイルだけチェックするため)
- チェック実行
- チェッカーから返ってきた結果が、問題のリストとして表示される
- 問題のある箇所を修正する
ここで、チェック結果のリスト上のボタンをクリックするだけで、対応箇所をエディタで開くことができるようにしているので、開発者は即座に問題を修正することができます。 この機能は、
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(filePath, lineNumber);
によって実現しています。
プロジェクトへの導入
今まで紹介したようなコーディング規約チェッカーを、ボイきらプロジェクトに導入しました。
その結果として、まず1つ目は、コーディング規約が浸透しました。 人がレビューするだけでは、どうしても見落としが起きてしまいますが、チェッカーは機械的に全ての問題を指摘するので、全員のコードが厳密にコーディング規約に沿ったものになります。 さらに、チェッカーの結果を見れば、プロジェクトのコーディング規約がどのようなものかが分かるため、コーディング規約を覚えるスピードが上がります。
もう1つの結果として、コードレビューの時間が削減されました。 プルリクのレビューを行うときは、既にチェッカーを通っているはずなので、設計やロジックなど、より高度なレビューに集中することができます。 また、もし、コーディング規約に関して問題があったら、「チェッカーをかけてください」と言うだけで済みます。
おわりに
本記事では、静的解析を用いたコー ディング規約チェッカーを紹介しました。 何故チェッカーを作り、導入した結果どうなったかについても合わせて紹介しました。
今回用いた静的解析は構文解析とコードフォーマットだけでしたが、NRefactoryが提供する静的解析には、他にも意味解析やコードの書き換えがあります。 意味解析を用いると、どのクラスのメソッドが呼ばれているかを把握できたりするので、解析の幅がぐんと広がります。 コードの書き換えは、例えば命名を機械的に修正したり、特定のメソッド呼び出しを消したり追加したりしたいときに役立ちます。 この機能を用いると、何かしらの機械的なリファクタリングを自動で行うことができるかもしれません。
静的解析を用いたコーディング規約チェッカーというアイディアは、Unityやc#に限った話ではなく、あらゆる開発プロジェクトに応用できるものだと思っています。 プロジェクトでコーディング規約が徹底されておらず困っているという方は、導入を検討されてみてはいかがでしょうか。