CIでC#(Unity)のコードフォーマット・静的コード解析・スクリプトビルドを行う方法

本稿は[KLab Engineer Advent Calendar 2020](https://qiita.com/advent-calendar/2020/klab)の6日目の記事です。

# はじめに

PullRequest時のCI環境上でC#(Unity)のコードフォーマット・静的コード解析・スクリプトビルドを行う仕組みを作ったので、内容を紹介したいと思います。

# 使用したツール
| ツール名 | 説明 |
|:-----------|:------------|
| [CleanupCode Command-Line Tool](https://www.jetbrains.com/help/rider/CleanupCode.html) | Rider(ReShaper)内蔵のコードフォーマッターをコマンドラインで実行できるようにしたツールで、Riderがなくても単独で実行できるようになっているスタンドアロンツールです |
| [InspectCode Command-Line Tool](https://www.jetbrains.com/help/rider/InspectCode.html) | Rider(ReShaper)内蔵の静的コード解析をコマンドラインで実行できるようにしたツールで、Riderがなくても単独で実行できるようになっているスタンドアロンツールです |

上記のツールはRider(ReShaper)内蔵のコードフォーマッターと静的コード解析をコマンドラインで実行できるようにしたライセンスフリーのツールです。
このツールを使うだけであればRider(ReShaper)のライセンスは不要なので、
Rider以外のIDEを使用している開発者はコマンドラインツールだけローカル環境にセットアップして使用する事もできます。
Window,Mac,Linuxに対応しているのとコマンドラインツールなので各種IDEとの連携もやりやすいんじゃないでしょうか。

このコマンドラインツールは[Rider(ReShaper)のバージョンごとに用意されている](https://www.jetbrains.com/resharper/download/previous.html)ので、IDEとコマンドラインツールのバージョンは開発者全員で統一するようにして下さい。

# Unityの.slnファイル・.csprojファイルを作成する
[コマンドラインツール](#使用したツール)の実行するには.slnファイルと.csprojファイルが必要です。
CI環境ではgithubからソースコードをcloneして利用する事が多いと思いますが、私のプロジェクトではgithubには.slnと.csprojをコミットしていないためCI環境で作成する必要がありました。

Unityの.slnファイルと.csprojファイルはUnityEditorによって自動生成されますが、作成するためのAPI等は提供されていません。
色々調べたところ、UnityEditorのメニューのAssets → `Open C# Project`を実行すると.slnファイルと.csprojファイルが作成される事がわかりましたが、同時にUnityEditorに関連付けられたIDEも起動してしまうためCI環境で実行する事を考えるとIDEの起動は避けたい所です。

そこで、`Open C# Project`の内部実装を確認するために、UnityEditorの[レポジトリ](https://github.com/Unity-Technologies/UnityCsReference/blob/2018.4/Editor/Mono/SyncProject.cs#L180-L185)を確認してみたところ、以下のような実装になっていました。
※Unity2018.4のコードです

```c#
internal class SyncVS : AssetPostprocessor
{
    // -------省略--------
    [MenuItem("Assets/Open C# Project")]
    static void SyncAndOpenSolution()
    {
        SyncSolution();
        OpenProjectFileUnlessInBatchMode();
    }
    // -------省略--------
```

実装を見た感じ`SyncVS.SyncSolution();`を実行すれば.slnファイルの作成だけを行ってくれそうですが、SyncVSクラスはinternal classなので別アセンブリからは実行できません。
そこで以下のようにリフレクションを使って呼び出すようにしました。

```c#
var syncVs = Type.GetType("UnityEditor.SyncVS,UnityEditor");  
var syncSolution = syncVs.GetMethod("SyncSolution", BindingFlags.Public | BindingFlags.Static);  
syncSolution.Invoke(null, null);
```

上記の処理をUnityのbatchmodeを使ってコマンドラインから実行する事で、CI環境上で.slnファイル・.csprojファイルを作成する事ができるようになりました。


*[Tips]
このようなリフレクションを使ったhackを覚えておくと、たまに役に立つ事があるので覚えておくといいと思います。
注意点としては今回のような非公開の関数を呼び出す場合は、Unityのバージョンが変わった際に実装内容が変わって使えなくなる可能性がある事も頭に入れておいて下さい。*


# コードフォーマット
### コードフォーマットの定義ファイルを作成する
Rider(or ReShaper)でコードフォーマットルールが定義された.sln.DotSettingsファイルを作成します。
作成方法は、Riderの設定画面でコードフォーマット設定を行い、Saveボタンの右の▼ボタンから「Solution "xxxxx" team-shared」ボタンを押す事で、プロジェクトルートに.sln.DotSettingsファイルが作成されます。
<img width="300" alt="Solution "xxxxx" team-shared" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216057/c22c134a-6a72-b0eb-b5ee-c5d49376c78b.png">
※[参考：.sln.DotSettingsファイルの作り方](https://pleiades.io/help/rider/Sharing_Configuration_Options.html#solution-team-shared-layer)

### コマンドラインでコードフォーマッターを実行する
[コマンドラインツール](#使用したツール)に含まれるcleanupcode.shスクリプトを使用します。
パラメータには.slnファイルと、
--profileパラメータには.sln.DotSettinsファイルの拡張子(.sln.DotSettins)を除いたファイル名を指定して下さい。
外部ライブラリ等をコードフォーマット対象から外したい場合は--excludeパラメーターで除外する事もできます。

```shell
./cleanupcode.sh \
--toolset-path=/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/msbuild/15.0/bin/MSBuild.dll \
--exclude=Assets/LineSDK/**/* \
--profile=Sample \  # .sln.DotSettinsファイルの拡張子を除いたファイル名
Sample.sln
```
*※Windows環境の場合はcleanupcode.exe*を使用してください

# 静的コード解析
### コマンドラインで静的コード解析ツールを実行する
[コマンドラインツール](#使用したツール)に含まれるinspectcode.shスクリプトを使用します。
パラメータには.slnファイルと.sln.DotSettingsファイル（拡張子を含める）と解析結果を出力するファイル名を指定して実行して下さい。
外部ライブラリ等を対象から外したい場合は--excludeパラメーターで除外する事もできます。

```shell
./inspectcode.sh \
--toolset-path=/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/msbuild/15.0/bin/MSBuild.dll \
--exclude=Assets/LineSDK/**/* \
--profile=Sample.sln.DotSettings \
Sample.sln \
-o=report.xml # 解析結果の内容を出力するファイル
```
*※Windows環境の場合はinspectcode.exe*を使用してください


### 解析結果ページの生成
解析結果はxmlで出力されるためHTMLに整形してCI環境上から確認しやすくしました。
以下のコードではxsltprocコマンドを使って、xmlにxslを適用する事で整形したhtmlを出力しています。

```shell
# report.xmlにreport.xslを適用してreport.htmlを出力
xsltproc --output report.html report.xsl report.xml
```

report.xsl

```xml
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:key name="IssueSearch" match="IssueType" use="@Id"/>
  <xsl:template match="Report">
    <html>
      <head>
        <title>Inspect Code Report</title>
        <style type="text/css">
        table {
            font-size: small;
        }
        table, tr, th, td {
            border-collapse: collapse;
            border: 1px solid #000000;
        }
        th {
            background-color: #3333ff;
            color: #ffffff;
        }
        </style>
      </head>
      <body>
        <h1>Inspect Code Report</h1>
        <xsl:apply-templates select="Issues"/>
      </body>
    </html>
  </xsl:template>
  <xsl:template match="Project">
    <h2><xsl:value-of select="@Name"/></h2>
    <table>
      <tr>
        <th>TypeId</th>
        <th>Severity</th>
        <th>File</th>
        <th>Offset</th>
        <th>Line</th>
        <th>Message</th>
        <th>TypeDescription</th>
      </tr>
      <xsl:apply-templates/>
    </table>
  </xsl:template>
  <xsl:template match="Issue">
    <xsl:variable name="typeId" select="@TypeId" />
    <tr>
      <td><xsl:value-of select="@TypeId"/></td>
      <td><xsl:for-each select="key('IssueSearch', $typeId)" ><xsl:value-of select="@Severity"/></xsl:for-each></td>
      <td><xsl:value-of select="@File"/></td>
      <td><xsl:value-of select="@Offset"/></td>
      <td><xsl:value-of select="@Line"/></td>
      <td><xsl:value-of select="@Message"/></td>
      <td><xsl:for-each select="key('IssueSearch', $typeId)" ><xsl:value-of select="@Description"/></xsl:for-each></td>
    </tr>
  </xsl:template>
</xsl:stylesheet>
```

## スクリプトビルド
UnityEditorを使ったスクリプトビルドは遅いので、xbuildコマンドを使ってUnityEditorを使わずにスクリプトビルドを行うようにしました。
UnityEditorを使ったビルドとは完全に同じ挙動にはなりませんが、目的がスクリプトのチェックだけであれば問題になる事は殆どないでしょう。

```shell
/Applications/Unity/Unity.app/Contents/MonoBleedingEdge/bin/xbuild Sample.sln
```

## おわりに
同じような事ができるツールはいくつかあると思うので、皆様の環境にあった方法を選択してもらえばいいかと思います。
今回紹介した内容が、その時の一つの選択肢になれば幸いです。