Unityエディタ上で循環的複雑度(CCN)を計測する

[PONOS Advent Calendar 2022](https://qiita.com/advent-calendar/2022/ponos)の1日目の記事です。

# はじめに

ポノスではソースコードの保守性が担保されているか判断するために、**循環的複雑度(以下、CCN)** の数値を参考にしています。([循環的複雑度 - Wikipedia](https://ja.wikipedia.org/wiki/%E5%BE%AA%E7%92%B0%E7%9A%84%E8%A4%87%E9%9B%91%E5%BA%A6))
この数値はソースコードがどれだけ複雑に構成されているかを表現しており、処理の分岐（if、for、switch等)が増えるに従って上昇していきます。
一般的に、CCNが高いソースコードほど保守しづらくバグが混入しやすい傾向があります。
CCNの数値が高いメソッドを発見したら、リファクタリングを行って分岐を減らしたり処理を分散させたりして、数値を下げるようにしましょう。
（なお、ポノスではCCNが30を超えるコードがないことがリリース要件とされています）

今回は、Unity開発においてCCNを確認しやすくする方法を紹介したいと思います。

# lizardでCCNを計測する

今回の記事ではCCNを計測するためのプログラムとして[lizard](https://github.com/terryyin/lizard)を利用します。
lizardはPython製のプログラムとなりますので、pipを使用してインストールしてください。

```shell
$ pip install lizard
```

計測対象のフォルダへ移動し、lizardコマンドを実行することで計測が実施されます。

```shell
$ cd Assets
$ lizard -l csharp
```

`-l`は計測対象のプログラミング言語を指定するオプションです。
今回はUnityプロジェクトのC#スクリプトファイルが対象となるので、`csharp`を指定します。

試しに、Unityで`Create/C# Script`メニューから作成されるC#スクリプトファイルを対象に計測してみます。

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Sample : MonoBehaviour
{
    void Start()
    {
    }

    void Update()
    {   
    }
}
```
このC#スクリプトファイルに対して、lizardを実行してみます。
```shell
$ lizard -l csharp
================================================
  NLOC    CCN   token  PARAM  length  location
------------------------------------------------
       3      1      5      0       4 Sample::Start@8-11@./Sample.cs
       3      1      5      0       4 Sample::Update@14-17@./Sample.cs
1 file analyzed.
==============================================================
NLOC    Avg.NLOC  AvgCCN  Avg.token  function_cnt    file
--------------------------------------------------------------
     12       3.0     1.0        5.0         2     ./Sample.cs

===============================================================================================================
No thresholds exceeded (cyclomatic_complexity > 15 or length > 1000 or nloc > 1000000 or parameter_count > 100)
==========================================================================================
Total nloc   Avg.NLOC  AvgCCN  Avg.token   Fun Cnt  Warning cnt   Fun Rt   nloc Rt
------------------------------------------------------------------------------------------
        12       3.0     1.0        5.0        2            0      0.00    0.00
```

結果出力の中の「CCN」の数値が循環的複雑度を指しています。
`Start()`と`Update()`の2つのメソッドについて計測していることがわかりますが、どちらもCCNの値は1となっています。
これはメソッド内にifやforなどの分岐が存在していないためです。

このままだとちゃんとCCNが計測されているのかが分かりづらいため、`Start()`メソッドに分岐を増やしてみます。

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Sample : MonoBehaviour
{
    void Start()
    {
        int number = 1;
        if(0 < number) // まったく意味のない分岐
        {
            Debug.Log("numberは0よりも大きい！");
        }
    }

    void Update()
    {
    }
}
```
再度、lizardの計測を実施してみましょう。
```shell
$ lizard -l csharp
================================================
  NLOC    CCN   token  PARAM  length  location
------------------------------------------------
       8      2     24      0       8 Sample::Start@7-14@./Sample.cs
       3      1      5      0       3 Sample::Update@16-18@./Sample.cs
1 file analyzed.
==============================================================
NLOC    Avg.NLOC  AvgCCN  Avg.token  function_cnt    file
--------------------------------------------------------------
     17       5.5     1.5       14.5         2     ./Sample.cs

===============================================================================================================
No thresholds exceeded (cyclomatic_complexity > 15 or length > 1000 or nloc > 1000000 or parameter_count > 100)
==========================================================================================
Total nloc   Avg.NLOC  AvgCCN  Avg.token   Fun Cnt  Warning cnt   Fun Rt   nloc Rt
------------------------------------------------------------------------------------------
        17       5.5     1.5       14.5        2            0      0.00    0.00
```

if分の追加により、処理の分岐が増えたため`Start()`メソッドのCCNの値が1から2へ上昇していることがわかります。

lizardのプログラムを使用すれば、非常に手軽にメソッドのCCNを取得することができました。

# Unityエディタ上で循環的複雑度を計測する

では、このlizardのプログラムをUnityから呼び出し、Unity上で計測結果を確認できるようにしてみましょう。
Unityから外部のプログラムを実行するためには[Processクラス](https://learn.microsoft.com/ja-jp/dotnet/api/system.diagnostics.process?view=net-7.0)を使用します。

Processを介してlizardのプロジェクトを実行し結果をConsoleウィンドウへ出力するメソッドを作成します。

```csharp
using System.Diagnostics;
using UnityEditor;

public static class CyclomaticComplexityInstrument
{
    [MenuItem("循環的複雑度/計測")]
    public static void Perform()
    {
        var process = new Process();
        // lizardのプログラム指定にはフルパスが必要でした。
        process.StartInfo.FileName = "/Library/Frameworks/Python.framework/Versions/Current/bin/lizard";
        process.StartInfo.Arguments = "-l csharp";
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.CreateNoWindow = true;
        process.Start();

        var output = process.StandardOutput.ReadToEnd();
        process.WaitForExit();
        process.Close();

        UnityEngine.Debug.Log(output);
    }
}

```
メニューからこのプログラムを実行すると、Consoleウィンドウへ以下のような結果が出力されます。
（新規プロジェクトを作成したときに自動でインストールされたパッケージ内のC#スクリプトファイルが計測されている模様です）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/7c5a6783-1ae6-b2f0-1e7a-81c5dfd2b3c7.png)
Unity上でCCNの計測結果を出力することができました。

# Projectウィンドウで選択されたC#スクリプトファイルに対してのみCCNを計測する

ここまでで、UntiyプロジェクトのC#スクリプトファイルのCCNを計測して出力することができました。
しかし、現在のソースコードはプロジェクト内のすべてのC#スクリプトファイルを計測対象としているため、プロジェクト内のC#スクリプトファイルの数が多くなると、目的のC#スクリプトファイルの結果を確認しづらくなってしまいます。

そこで、Projectウィンドウで選択中のC#スクリプトファイルに対してのみ計測が実行されるに改修し、目的のC#スクリプトファイルの計測結果がすぐに確認できるようにしてみましょう。
lizardは実行時の引数で計測対象のファイルを指定することができるため、Projectウィンドウで選択中のアセットのパスを取得し、lizardの引数に設定します。

```csharp
using System.Diagnostics;
using System.Linq;
using UnityEditor;

public static class CyclomaticComplexityInstrument
{
    [MenuItem("Assets/循環的複雑度/計測")]
    public static void Perform()
    {
        var assetPaths = Selection.assetGUIDs.Select(e => AssetDatabase.GUIDToAssetPath(e));
        var targets = assetPaths.Aggregate((a, b) => a + " " + b);
       
        var process = new Process();
        process.StartInfo.FileName = "/Library/Frameworks/Python.framework/Versions/Current/bin/lizard";
        process.StartInfo.Arguments = "-l csharp " + targets;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.CreateNoWindow = true;
        process.Start();

        var output = process.StandardOutput.ReadToEnd();
        process.WaitForExit();
        process.Close();

        UnityEngine.Debug.Log(output);
    }
}
```
機能が完成したら先ほど計測したSampleのC#スクリプトファイルに対して計測を実施してみましょう。
Projectウィンドウ上で対象となるC#スクリプトファイルを選択し、右クリックから`循環的複雑度/計測`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/fc583be4-b685-9078-b9b2-29075e951dd6.png)
以下のように、選択中のC#スクリプトファイル内に定義されているメソッドの情報だけが出力されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/59ba5d7d-b5e1-4f31-02ac-dc2eb01144d5.png)

これでUnityエディタ上から気軽に目的のC#スクリプトファイルのCCNが計測できるようになりました。
プルリクエストを作成する前などにこの機能を使い、CCNの高いコードを書いていないかを確認すると良いかと思います。

# まとめ

CCNが大きい数値にならないように心がけ、ソースコードの保守性を保っていきたいですね！

明日は@blockさんです！