Unity でコーディングスタイルを機械的にチェックする

この記事は [KLab Advent Calendar](https://qiita.com/advent-calendar/2017/klab) の22日目の記事です。

## はじめに
私が所属するチームでは Unity での開発におけるコーディングスタイルチェックに [StyleCop](https://github.com/StyleCop/StyleCop) というツールを使っています。
この記事では Unity と StyleCop をうまく連携して使う方法について書きたいと思います。

Unity で使える外部エディタは MonoDevelop 以外にもあります。MonoDevelop ではエディタ上から StyleCop の実行結果を確認することができました。しかし、 MonoDevelop 以外の Rider
 などのエディタを使おうと思うと、簡単に導入できる StyleCop プラグインはありません。（今のところ）
そこで、Unity から StyleCop を実行することができれば、エディタに関わらず StyleCop の出力結果を確認できると思い、外部アプリケーションとして Editor 拡張から呼び出す方法を検討しました。

## StyleCop とは
StyleCopはC#向けのコーディングスタイルやルールを静的解析するツールです。
コードレビューをしてもらう前に実装者自身が修正することができるので、インデントのスペースの数が違ったり、変数がキャメルケースになっていなかったりコードの本質ではない部分をレビュワーが指摘する必要がなくなります。
すべてのルールを適用すると、かなり厳しいものとなるため、使うチームで適用するルールを選定することをオススメします。


## Unity で実行するには
Unity の Editor 拡張を使ってStyleCopを実行します。
記事のおわりに、 StyleCop を実行するためのコードを記載しています。
その中から重要と思われる箇所について、該当コードと一緒に解説していきます。

### 呼び出しタイミング
スクリプトのコンパイル時に呼ばれる `DidReloadScripts` のタイミングで外部アプリケーションとして StyleCop を呼び出します。
`DidReloadScripts` は実行順が設定できること以外は `InitializeOnLoad` とそこまで変わらないようですが、別の拡張との兼ね合いで、実行順を指定しています。

```C#
[DidReloadScripts(1)]
static void DidReloadScripts()
{
    // ここでStyleCopを呼ぶ
}
```

### StyleCopの実行
次に `RunStyleCop` というメソッドを分解して、StyleCop の実行について解説します。

StyleCop の実行ファイルがない場合は、 XBuild を使ってビルドします。
こちらも外部アプリケーションとして実行しています。

```C#
if (!File.Exists(StyleCopExePath))
{
    RunXBuild();
}
```

外部アプリケーションとして実行するために、`ProcessStartInfo` を設定していきます。
macOS では、 mono を使って `StyleCop.exe` を実行します。 Windows の場合はそのまま実行できるので、実行する `FileName` と `Arguments` を実行環境によって分ける必要があります。
また、 StyleCop の出力ログを取得するために、`RedirectStandardOutput` を有効にしておく必要があります。

```C#
var info = new ProcessStartInfo();
info.WindowStyle = ProcessWindowStyle.Normal;
info.UseShellExecute = false;
info.CreateNoWindow = true;
info.Arguments = Args + targetPath;
info.RedirectStandardOutput = true;
            
#if UNITY_EDITOR_WIN
info.FileName = StyleCopExePath;
#else
info.FileName = MonoCommand;
#endif
```

実際に StyleCop を実行するために `Process` クラスを使います。
ログの収集を行う `dataReceivedEventHandler` と実行終了後のイベント `exitedHandler` を登録します。
`Start` で実行を開始し、`BeginOutputReadLine` で StyleCop の出力読み取りを開始します。


```C#
var process = new Process();
process.StartInfo = info;
process.EnableRaisingEvents = true;
process.OutputDataReceived += dataReceivedEventHandler;
process.Exited += exitedHandler;

process.Start();
process.BeginOutputReadLine();
```

### ログの整形
終了時のイベントに登録した `OnFinished` でログの整形をしています。
ログの出力形式は、見やすいように色とか必要な情報だけサマっています。
また、 UnityEditor のログに出力する場合、あまり長すぎるとすべて表示されないので注意が必要です。
Warning などのログレベルの設定やカラータグが使えるので、 StyleCop の出力が他のログに埋もれないようにする工夫はできると思います。

## StyleCop を実行する Editor 拡張のコード
今回の説明外の拡張コードは省いていますが、対象ディレクトリや StyleCop のパスを正しく設定すれば実行できるように載せています。

```C#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;

namespace StyleCop
{
    /// <summary>
    /// StyleCopを実行する
    /// </summary>
    static class StyleCopRunner
    {
        /// <summary>
        /// StyleCopの対象としたいディレクトリを指定してください。
        /// </summary>
        static readonly List<string> TargetPathList = new List<string>
        {
            Application.dataPath + "/Scripts/"
        };

        /// <summary>
        /// monoの場所
        /// "brew install mono" でmonoをインストールするか、パスを変更してください。
        /// </summary>
#if !UNITY_EDITOR_WIN
        const string MonoCommand = "/usr/local/bin/mono";
#endif

#if UNITY_EDITOR_WIN
        /// <summary>xbuildの場所</summary>
        const string XBuildCommand = @"C:\Program Files\Unity\Editor\Data\MonoBleedingEdge\bin\xbuild.bat";

        /// <summary>StyleCopビルド時の引数</summary>
        const string XBuildArgs = " /p:Platform=\"Any CPU\" ";
#else
        /// <summary>xbuildの場所</summary>
        const string XBuildCommand = "/usr/local/bin/xbuild";

        /// <summary>StyleCopビルド時の引数</summary>
        const string XBuildArgs = "";
#endif

        /// <summary>
        /// StyleCopのディレクトリ
        /// https://github.com/Nylle/StyleCop.Console を利用しています。
        /// こちらのリポジトリをCloneしたフォルダを指定してください。
        /// </summary>
        static readonly string StyleCopDirectory = Application.dataPath + "/StyleCop.Console/";

        /// <summary>
        /// StyleCop実行ファイルのパス
        /// </summary>
        static readonly string StyleCopExePath = StyleCopDirectory + "StyleCop.Console/bin/Debug/StyleCop.Console.exe";

        /// <summary>
        /// 設定ファイルとオプションをArgsに詰める
        /// </summary>
#if UNITY_EDITOR_WIN
        static readonly string Args = "-s Settings.StyleCop -p ";
#else
        static readonly string Args = StyleCopExePath + " -s Settings.StyleCop -p ";
#endif

        /// <summary>
        /// ログ収集用
        /// </summary>
        static List<string> logs;

        /// <summary>
        /// 出力受け取り時のイベント
        /// </summary>
        static void OnReceiveOutputData(object sender, DataReceivedEventArgs e)
        {
            logs.Add(e.Data + '\n');
        }

        /// <summary>
        /// 終了時のイベント
        /// </summary>
        static void OnFinished()
        {
            var violationCount = logs.Count(c => c.StartsWith("  Line"));

            // 全ファイルを表示するとログ出力がTruncateされるので違反が出たファイルと箇所だけ表示するように整形
            var tmpLogs = new List<string>();
            for (int i = 0; i < logs.Count; i++)
            {
                if (logs[i].StartsWith("Pass") && logs[i + 1].StartsWith("  Line"))
                {
                    tmpLogs.Add(logs[i]);
                }

                if (logs[i].StartsWith("  Line"))
                {
                    tmpLogs.Add(logs[i]);
                }
            }
            
            logs.Clear();

            // 結果をまとめる
            bool existsViolation = violationCount > 0;
            var logTitle = string.Format(
                "<color='{0}'>----- Run StyleCop -----</color>\n",
                existsViolation ? "magenta" : "cyan");

            tmpLogs.Insert(0, logTitle);
            tmpLogs.Insert(1, string.Format("<color='yellow'>{0} Violations</color>\n", violationCount));

            string logText = string.Join("", tmpLogs.ToArray());
            if (existsViolation)
            {
                UnityEngine.Debug.LogWarning(logText);
                return;
            }

            UnityEngine.Debug.Log(logText);
        }

        /// <summary>
        /// スクリプト読み込み時にStyleCopが実行される
        /// </summary>
        [DidReloadScripts(1)]
        static void DidReloadScripts()
        {
            // DidReloadSciptsは再生直後にも発火するのでガードしておく
            if (EditorApplication.isPlayingOrWillChangePlaymode)
            {
                return;
            }
            
            if (logs == null)
            {
                logs = new List<string>();
            }

            var pathes = string.Join(" ", TargetPathList.ToArray());
            RunStyleCop(pathes, OnReceiveOutputData, (_, __) => OnFinished());
        }

        /// <summary>
        /// StyleCopを別プロセスで起動する
        /// </summary>
        /// <param name="targetPath">StyleCop対象のパス</param>
        /// <param name="dataReceivedEventHandler">ログ出力ハンドラ</param>
        /// <param name="exitedHandler">プロセス終了ハンドラ</param>
        public static void RunStyleCop(
            string targetPath,
            DataReceivedEventHandler dataReceivedEventHandler,
            EventHandler exitedHandler)
        {
            if (!File.Exists(StyleCopExePath))
            {
                RunXBuild();
            }

            var info = new ProcessStartInfo();
            info.WindowStyle = ProcessWindowStyle.Normal;
            info.UseShellExecute = false;
            info.CreateNoWindow = true;
            info.Arguments = Args + targetPath;
            info.RedirectStandardOutput = true;
            
#if UNITY_EDITOR_WIN
            info.FileName = StyleCopExePath;
#else
            info.FileName = MonoCommand;
#endif

            var process = new Process();
            process.StartInfo = info;
            process.EnableRaisingEvents = true;
            process.OutputDataReceived += dataReceivedEventHandler;
            process.Exited += exitedHandler;

            process.Start();
            process.BeginOutputReadLine();
        }

        /// <summary>
        /// StyleCopのビルド
        /// </summary>
        static void RunXBuild()
        {
            var info = new ProcessStartInfo();
            info.WindowStyle = ProcessWindowStyle.Normal;
            info.UseShellExecute = false;
            info.FileName = XBuildCommand;
            info.Arguments = StyleCopDirectory + "StyleCop.sln" + XBuildArgs;
            info.CreateNoWindow = true;
            info.RedirectStandardError = true;

            var process = new Process();
            process.StartInfo = info;

            process.Start();
            var error = process.StandardError.ReadToEnd();
            if (!string.IsNullOrEmpty(error))
            {
                UnityEngine.Debug.Log(error);
            }

            process.WaitForExit();
        }
    }
}

```

##おわりに
この記事では、Unity 上で StyleCop を実行する方法を説明しました。
チーム内では MonoDevelop はもう使われていないため、この方法で StyleCop が利用されています。
コードの可読性が上がるので、StyleCop を利用することをオススメします。