プルリクを出した際のコード規約自動チェッカーをRoslynで試作してみる

本記事は[Craft Egg Advent Calendar 2021](https://qiita.com/advent-calendar/2021/craftegg)の12/17の記事です。
12/16の記事は@ishiguro_takuyaさんの記事でした。

## はじめに
株式会社Craft EggでUnityクライアントエンジニアをしている豊田です。
12/3には「[新卒2年目までに学んだ、コーディングで意識すること](https://qiita.com/Tomy_0331/items/d580aa4537edcda29c4d)」、12/6には「[Toggl Trackを使ってみた](https://qiita.com/Tomy_0331/items/967ec008e7703be5bdc5)」について書きました。

今回は、コード規約のチェックを自動化するため、試作した仕組みを紹介させていただこうと思います。

### コード規約とは
まず、コード規約とは、
>　・修飾子は付いているか、それによる命名は正しいか
　・クラス、フィールド、メソッドの宣言にサマリコメントがあるか
　・ブロックの改行フォーマットは正しいか

などの、コードのしきたりのことです。
開発に携わるメンバー全体でルールを共有・従うことでチームの誰が見てもわかりやすいコードになります。

例えば、「メソッドの命名の先頭は大文字にしなければならない」という規約をチームで定めている場合は以下のコードは違反していると言えます。

```cs
public void hogeMethod()
{
}
```

### Roslynとは
コード規約の判定を行うにあたって、Roslynを利用しましたが、
Roslynの概要につきましてはこちらのサイトの解説がわかりやすかったです。
[Visual Studio 2015の新機能“Roslyn”とは
](https://www.buildinsider.net/enterprise/roslyn/01)

私達が普段IDEでコードを書いていると、補完や色つけ、リファクタなど便利な機能でアシストしてくれますが、
これはIDEが持つ静的解析機能のおかげです。
また、コードのコンパイルにおいても、IDEとは独立して構文解析・意味解析の機能が存在します。

かつては双方で保守コストがかかっていましたが、Microsoftが中間情報をAPIで取得可能なC#コンパイラを開発しまして、それが「Roslyn」です。

RoslynのAPIは一般の開発者も利用可能で、リファクタリングのツールやコード規約に関したツールを開発することが可能です。

## Roslynを使う
### 環境構築

1. VisualStudioでプロジェクトを作成
2. nugetで Microsoft.CodeAnalysis のパッケージを追加
<img width="500px" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146248/66d99b1e-1920-89f0-990e-71264081b50c.png">
3. 以下のようにusingすれば利用可能になります

```cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
```

### コードを書いてみる
コードを解析するためのコードを書いていきます。

```cs:解析の準備
// 外部ライブラリ定義
private readonly PortableExecutableReference[] References = new[] {
    // microlib.dll
    MetadataReference.CreateFromFile(typeof(object).Assembly.Location),

    // System.dll
    MetadataReference.CreateFromFile(typeof(ObservableCollection<>).Assembly.Location),

    // System.Core.dll
    MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),

    // その他のlibrary
    // MetadataReference.CreateFromFile("library path"),
};

// コードを解析する（引数はコード）
public void Main(string code)
{
    // 構文木の構築
    SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
    CompilationUnitSyntax unit = tree.GetCompilationUnitRoot();
    CSharpCompilation compilation = CreateCompilation(tree, References);

    // 解析結果を得る
    SemanticModel semanticModel = compilation.GetSemanticModel(tree);

    // 「unit」や「semanticModel」を使って実装していく...
}

// 構文木のノードから型を解析するための、
// アセンブリ参照や他のソースコード由来の構文木とかを抱え込むcompilationを作成
private CSharpCompilation CreateCompilation(SyntaxTree tree, PortableExecutableReference[] references)
{
    CSharpCompilation compilation = CSharpCompilation.Create("Resolver");
    compilation = compilation.AddReferences(references);
    compilation = compilation.AddSyntaxTrees(tree);
    return compilation;
}

```

CreateCompilationというメソッドを作りましたが、ここでは外部ライブラリのクラスを解析するため、System系のアセンブリの登録をし、解析用コンパイラを生成しています。

これを行わないと、正しい情報を取得することができず、
例えば、`List<string>`と記述していると`List<string>`として、`Generic.List<string>`と書いていると`Generic.List<string>`として別々に取得されてしまいます。
ただの名前情報しかないため、これ以上解析できなくなります。

### 解析結果を見る
先程取得したCompilationUnitSyntaxから子ノード郡を見てみます

```cs
IEnumerable<SyntaxNode> nodes = unit.DescendantNodes();
```
IDEでブレイクポイントを置いて、nodesの中身を見ると、色々な情報が入っています。
![スクリーンショット 2021-12-12 22.51.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146248/574da68e-42c7-8269-41a0-73d4ccc9170a.png)

`ClassDeclarationSyntax`、`FieldDeclarationSyntax`、`VariableDeclarationSyntax`というように要素ごとに、クラス、フィールドとデータが分類されていました。

### 解析結果を使ってコード規約の判定をする
材料は揃いましたので、後はどんどん実装を進めます

```cs:要素ごとに実装していくイメージ
nodes.OfType<ClassDeclarationSyntax>()
    .ToList()
    .ForEach(syntax => クラスの判定(syntax, semanticModel));

nodes.OfType<MethodDeclarationSyntax>()
    .ToList()
    .ForEach(syntax => メソッドの判定(syntax, semanticModel));

nodes.OfType<PropertyDeclarationSyntax>()
    .ToList()
    .ForEach(syntax => プロパティの判定(syntax, semanticModel));

nodes.OfType<FieldDeclarationSyntax>()
    .ToList()
    .ForEach(syntax => フィールドの判定(syntax, semanticModel));
```

syntaxには、修飾子や命名、参照などコードの様々な情報が含まれています。
例えば、それらを文字列で処理して規約と一致しているかなど、用途に合わせて固有の実装を行えます。

```cs:フィールドの判定例
// int hoge; なら"hoge"という文字列を取得できる
string fieldName = syntax.Declaration.Variables.First()?.Identifier.ValueText;

// アクセシビリティ（"public"や"private"）を取得できる
string accessibility = syntax.Modifiers.First().Text

// 大文字や小文字など、プロジェクトの規約に合わせて判定...
```

### 実装したコード規約チェッカーを実行してみる
先述したような解析情報を使って、簡易的なコード規約チェッカーを作成しました。
メソッドやプロパティの宣言において、修飾子に対して命名の先頭が大文字小文字になっているか、という簡単な判定を実装しています。

まずは違反していないサンプルコードに対して動かしてみます。
![スクリーンショット 2021-12-12 23.22.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146248/62b33b1c-e2b0-e25f-f61c-b4a08e9563bf.png)
どの宣言も問題なしとして、OKのログを出しました。 

次に、先程のサンプルコードからあえて規約に違反するように書き換え、再度実行してみます。

![スクリーンショット 2021-12-12 23.27.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146248/0b2d5c43-9558-90a9-d252-72985bde3c3d.png)
命名や修飾子の有無などで違反していることを検知できました。

ちなみに、「(5,4)」というようにコードのどの行かも一緒に出力しています。

```cs:LinePosの取り方
node.GetLocation().GetMappedLineSpan().StartLinePosition
```


## さいごに：Jenkinsを使ってPullRequestに対して自動で判定させる
ここまでで、簡易的なコード規約チェッカーの作成を紹介しました。
まだ実際に動かして自動化までは出来ていませんが、今後はGitLabでプルリクを出すと自動で結果を通知する仕組みを作っていく予定です。

構成イメージは以下の図の通りです。
![スクリーンショット 2021-12-12 23.33.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146248/e52c5607-6de4-1376-1723-a3cb0101736e.png)

GitLabでプルリクをだすと、Webhockでjenkinsジョブへ通知し、シェルスクを実行させます。
シェルスクでは、Webhockで得たプルリクの対象ブランチやファイル情報を元に、必要なソースのみ解析を行うように絞った上で、コンソールアプリ化したコード規約チェッカーを実行します。

その結果出力を元にGitLaAPIを叩き、結果をGitLab上の画面で知らせることが出来ます。

```sh:jenkinsで実行するシェルの例
resultFile=result.txt

git fetch
git checkout $gitlabBranch

# PRの差分は、マージベースとの差分になるため、トリプルドットで取得する
diff="`git diff $gitlabTargetBranch...$gitlabBranch --name-only '*.cs'`"

# 空白区切りで差分ファイルが格納押されるためカンマ区切りにする
param=`echo $diff | sed -e "s/ /,/g"`

mono PRCodeAnalyzer.exe $param > resultFile

# ※コード規約チェッカーでは違反しているときのみログを出力するようにしています
if [ -s "$resultFile" ]; then
# 結果出力メッセージ
body=$(cat << EOS
コード規約の違反を検知しました

ビルド番号: [${BUILD_DISPLAY_NAME}](${BUILD_URL})
メッセージ:
\`\`\`
$(cat $resultFile)
\`\`\`
EOS
)

# GitLabAPIを叩く
curl -XPOST -H"PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
  --data-urlencode "body=${body}" \
  https://web-gitlab.xxxxxxxxx/api/v4/projects/${projectId}/merge_requests/${gitlabMergeRequestIid}/notes >/dev/null
fi
```

明日のアドベントカレンダーは @arumani さんの記事です！

## 参考文献
https://www.buildinsider.net/enterprise/roslyn/01

https://www.peyarogu.com/entry/roslyn-csharp

https://aonasuzutsuki.hatenablog.jp/entry/2019/05/07/104305

https://developers.cyberagent.co.jp/blog/archives/4257/