いつか使いたかったmetrix++、github actionsでC++コードメトリクス集計

# 1. はじめに

## 1.1. 動機

- c++コードを[lizard](https://github.com/terryyin/lizard)でメトリクス解析をしているとき、ネスト（max indent）が計測できないので、代わりのツールはないか探していました。
    - SonarQubeを使用することも考えましたが、もう少し手軽に使用できるツールを探しました。
- そのとき試使用して使い勝手が良かった[metrix++](https://metrixplusplus.github.io/metrixplusplus/)について、サンプルプログラムを用いて、マニュアル解析したり、Pull Requestを発行した際のgithub actionsで差分解析する例を紹介します。

## 1.2. 開発環境

- OS: Windows 11 Pro 21H2 (WSL Ubuntu22.04)
- CPU: Core(TM) i7-12700
- RAM: 32GB
- 利用ツール
  - VS Code
  - Docker (devcontainer利用)

## 1.3. リポジトリ

- https://github.com/tomoten-umino/my-cpp-cmake-sample

## 1.4. 参考資料

| No. | URL | 説明 |
| :-- | :-- | :-- |
| [1] | [metrix++ Doc](https://metrixplusplus.github.io/metrixplusplus/) | 本記事で紹介するC++メトリクス解析ツール（Doc） |
| [2] | [metrix++ Github](https://github.com/metrixplusplus/metrixplusplus) | 本記事で紹介するC++メトリクス解析ツール（Github） |
| [3] | [C/C++プロジェクトをCMakeでビルドする](https://qiita.com/Hiroya_W/items/049bfb4c6ad3dfe6ff0c) | 提示するサンプルプログラムのcmake環境は、こちらのQiita記事をもとに作成しました。

# 2. 準備：サンプルプログラム

devcontainer構築後の操作は、明示しない限りdevcontainer内で実施します。

## 2.1. 環境構築

- サンプルプログラムはdevcontainerで環境構築しているため、git cloneした後、VS Codeを開きます。
- VSCode左下にある「＞＜」みたいなボタンを押下して、「Reopen in Container」を押下します。押下すると、VSCodeがc++開発用のdockerコンテナを生成します。

```bash
$ git clone https://github.com/tomoten-umino/my-cpp-cmake-sample.git
$ cd my-cpp-cmake-sample
$ code .
```

## 2.2. metrix++のインストール

- metrix++はpipを用いてインストールすることができます。

```
$ pip3 install metrixpp
```

- サンプルプロジェクトには、プロジェクトルートディレクトリにrequirements.txtが格納されているので、ファイル指定でpipインストールしてください。

```
$ pip3 install -r requirements.txt
$ export PATH=$HOME/.local/bin:$PATH
```

## 2.3. サンプルプログラムのビルド

- cmakeのお作法にのっとってビルドします。詳細はリポジトリの[README.md](https://github.com/tomoten-umino/my-cpp-cmake-sample/blob/main/README.md)を参照願います。

```
$ mkdir build; cd build
$ cmake ..
$ make all
[ 50%] Building CXX object lib/mylib/CMakeFiles/mylib.dir/mylib.cpp.o
[100%] Linking CXX static library libmylib.a
[100%] Built target mylib
$ make examples
Consolidate compiler generated dependencies of target mylib
[ 50%] Built target mylib
[ 75%] Building CXX object examples/CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable main
[100%] Built target main
[100%] Built target examples
```

- 主なコマンドは以下の通りです。

```bash
# build lib
make
# build examples
make examples
# clean
make clean
# install
make install
# run cpplint
make cpplint
# run metrixpp
make metrixpp
```

# 3. ローカル環境でメトリクス解析

- metrix++のツール起動方法は、参照Docによると以下のように実施します。詳細なオプションについては参照Docを確認してください。

```
# メトリクス解析　（例）コード行数、ネスト、サイクロマティック複雑度、
# ファイルパスを指定しないと、カレントディレクトリ以下でC＋＋に該当するファイルをすべて解析する。
$ metrix++ collect --std.code.lines.code --std.code.complexity.maxindent --std.code.complexity.cyclomati ファイルのパスorファイル
# csv出力
$ metrix++ export --db-file=ファイル名.db > ファイル.csv
```

- サンプルプログラムでは、以下のようなcmake設定を実施しており、```make metrix++```を実行することで解析、CSV出力、上限確認を実施することができます。
    - ここでは、上限値として、ネスト：４、サイクロマティック複雑度：10と設定しています。

```cmake
# find command metrix++
find_program(METRIXPP metrix++)

# add make command
add_custom_target(
    metrixpp
    # analysis
    COMMAND cd ${CMAKE_CURRENT_SOURCE_DIR} &&
            ${METRIXPP} collect --log-level=WARNING
                                --std.code.lines.code 
                                --std.code.complexity.maxindent
                                --std.code.complexity.cyclomatic
                                --exclude-files=CMakeCXXCompilerId.cpp
                                --db-file=${PROJECT_BINARY_DIR}/metrixpp.db
    # export from db to csv
    COMMAND ${METRIXPP} export --db-file=./metrixpp.db > metrics-result.csv
    # check max indent
    COMMAND ${METRIXPP} limit --log-level=WARNING --db-file=metrixpp.db --max-limit=std.code.complexity:maxindent:4 | tee max_indent_error.txt
    # check max cyclomatic 
    COMMAND ${METRIXPP} limit --log-level=WARNING --db-file=metrixpp.db --max-limit=std.code.complexity:cyclomatic:10 | tee max_cyclomatic_error.txt
)
```

- buildディレクトリにて解析を実施すると以下のような結果となります。

```
$ make metrixpp
[LOG]: WARNING: Logging enabled with WARNING level
[LOG]: WARNING: Done (0.03 seconds). Exit code: 0
[LOG]: WARNING: Logging enabled with INFO level
[LOG]: INFO:    Processing: ./
[LOG]: WARNING: Done (0.01 seconds). Exit code: 0
[LOG]: WARNING: Logging enabled with WARNING level
[LOG]: WARNING: Done (0.0 seconds). Exit code: 0
./:: info: 0 regions exceeded the limit 'std.code.complexity:maxindent' > 4.0 [applied to 'any' region type(s)]

[LOG]: WARNING: Logging enabled with WARNING level
[LOG]: WARNING: Done (0.0 seconds). Exit code: 0
./:: info: 0 regions exceeded the limit 'std.code.complexity:cyclomatic' > 10.0 [applied to 'any' region type(s)]

Built target metrixpp
```

- buildディレクトリ内に解析結果として以下のファイルが出力されます。
    - metrixpp.db : sqlite3のDBファイル。
    - metrics-result.csv : dbファイルの解析結果をCSVファイルにダンプしたもの。
    - max_cyclomatic_error.txt, max_indent_error.txt 上限に引っかかった場合はエラーが記載される。
        - 実行時にteeコマンドを使用しているので標準出力にもログは表示されています。

```
# CSVファイルの中身
file,region,type,modified,line start,line end,std.code.complexity:cyclomatic,std.code.complexity:maxindent,std.code.lines:code
./examples/main.cpp,__global__,global,,1,13,,,0
./examples/main.cpp,main,function,,3,12,0,1,8
./examples/main.cpp,,file,,1,13,,,
./include/mylib/mylib.hpp,__global__,global,,1,11,,,1
./include/mylib/mylib.hpp,Mylib,class,,4,8,,,5
./include/mylib/mylib.hpp,,file,,1,11,,,
./lib/mylib/mylib.cpp,__global__,global,,1,21,,,0
./lib/mylib/mylib.cpp,hello,function,,4,6,0,1,3
./lib/mylib/mylib.cpp,nested_hello,function,,8,18,2,3,9
./lib/mylib/mylib.cpp,,file,,1,21,,,
```

# 4. Github ActionsでPull Request時に差分解析を実施する

## 4.1. CIのyamlファイル

※2023/4/29加筆

```yaml
name: metrics-analysis

on:
  # for debug
  workflow_dispatch:
  # for pull request check
  pull_request:
    branches:
      - 'main'

jobs:
  metrics-analysis:
    runs-on: ubuntu-22.04
    container:
      image: mcr.microsoft.com/vscode/devcontainers/cpp:0-bullseye
    
    steps:
      - name: update container package
        run: |
          apt-get update
          apt-get -y install python3-pip sqlite3

      - name: checkout code
        uses: actions/checkout@v3
        with:
          ref: ${{ github.head_ref }}

      # to diff between from_branch and to_branch
      - name: fetch repository
        if: ${{ success() }}
        run: |
          git config --global --add safe.directory /__w/${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}/${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}
          git remote set-url origin https://github-actions:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}
          git config --local user.name "${GITHUB_ACTOR}"
          git config --local user.email "${GITHUB_ACTOR}@users.noreply.github.com"
          git fetch

      - name: python package install
        if: ${{ success() }}
        run: |
          pip install -r requirements.txt

      - name: metrics analysis
        id: metrics_analysis
        run: |
          metrix++ collect --log-level=INFO \
          --std.code.lines.code \
          --std.code.complexity.maxindent \
          --std.code.complexity.cyclomatic \
          --exclude-files=CMakeCXXCompilerId.cpp \
          --db-file=metrixpp.db \
          $(git diff ${{ github.head_ref }} remotes/origin/${{ github.base_ref }} --name-only)

      - name: create comment header
        if: ${{ success() }}
        run: |
          echo "# Result of Max Indent analysis" >max_indent_error.txt
          echo "# Result of Cyclomatic Complexity analysis" >max_cyclomatic_error.txt

      - name: check max indent
        id: check_max_indent
        if: steps.metrics_analysis.outcome == 'success'
        run: | 
          # if all C/C++ files are not changed, count = 0
          if [ $(sqlite3 ./metrixpp.db 'SELECT COUNT(*) FROM "std.code.lines"') != "0" ]; then
            metrix++ limit --log-level=WARNING --db-file=metrixpp.db --max-limit=std.code.complexity:maxindent:4 >>max_indent_error.txt
          else
            echo "All C/C++ files are not changed." >>max_indent_error.txt
          fi
      
      - name: check max cyclomatic
        id: check_max_cyclomatic
        if: steps.metrics_analysis.outcome == 'success' && ( success() || failure() )
        run: |
          # if all C/C++ files are not changed, count = 0
          if [ $(sqlite3 ./metrixpp.db 'SELECT COUNT(*) FROM "std.code.complexity"') != "0" ]; then
            metrix++ limit --log-level=WARNING --db-file=metrixpp.db --max-limit=std.code.complexity:cyclomatic:10 >>max_cyclomatic_error.txt
          else
            echo "All C/C++ files are not changed." >>max_cyclomatic_error.txt
          fi

      # to convert % and \n to URL encodes
      - name: modify text result of indent
        id: result_of_indent
        if: ${{ always() }}
        run: |
          sed -i -z 's/%/%25/g' max_indent_error.txt
          sed -i -z 's/\n/%0A/g' max_indent_error.txt
          text_indent=`cat max_indent_error.txt`
          echo "::set-output name=message_body::$text_indent"

      # find comment for Indent
      - name: Find comment for Indent
        if: ${{ always() }}
        uses: peter-evans/find-comment@v2
        id: fc-indent
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-author: 'github-actions[bot]'
          body-includes: "Max Indent analysis"

      - name: Create or update comment for Max Indent
        if: ${{ always() }}
        uses: peter-evans/create-or-update-comment@v3
        with:
          comment-id: ${{ steps.fc-indent.outputs.comment-id }}
          issue-number: ${{ github.event.pull_request.number }}
          body: |
            ${{ steps.result_of_indent.outputs.message_body }}
          edit-mode: replace

      # to convert % and \n to URL encodes
      - name: modify text result of cyclomatic
        id: result_of_cyclomatic
        if: ${{ always() }}
        run: |
          sed -i -z 's/%/%25/g' max_cyclomatic_error.txt
          sed -i -z 's/\n/%0A/g' max_cyclomatic_error.txt
          text_cyclomatic=`cat max_cyclomatic_error.txt`
          echo "::set-output name=message_body::$text_cyclomatic"

      # find comment for Cyclomatic Complexity
      - name: Find comment for Cyclomatic Complexity
        if: ${{ always() }}
        uses: peter-evans/find-comment@v2
        id: fc-c-complexity
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-author: 'github-actions[bot]'
          body-includes: "Cyclomatic Complexity analysis"

      - name: Create or update comment for Cyclomatic Complexity
        if: ${{ always() }}
        uses: peter-evans/create-or-update-comment@v3
        with:
          comment-id: ${{ steps.fc-c-complexity.outputs.comment-id }}
          issue-number: ${{ github.event.pull_request.number }}
          body: |
            ${{ steps.result_of_cyclomatic.outputs.message_body }}
          edit-mode: replace
```

※ポイント

- メトリクス解析のstepにおいて、ブランチ間差分をmetrix++に与えることにより、修正コードのみを解析対象にしています。

```
          metrix++ collect --log-level=INFO \
          --std.code.lines.code \
          --std.code.complexity.maxindent \
          --std.code.complexity.cyclomatic \
          --exclude-files=CMakeCXXCompilerId.cpp \
          --db-file=metrixpp.db \
          $(git diff ${{ github.head_ref }} remotes/origin/${{ github.base_ref }} --name-only)
```

- metrix++ limitで上限値確認をしたとき、エラーテキストをPull Requestに表示するため、％や改行コードをURLエンコードを実施します。そうすることでPull Requestコメントに複数行コメントを投稿できます。

```
      - name: modify text result of indent
        id: result_of_indent
        if: ${{ always() }}
        run: |
          sed -i -z 's/%/%25/g' max_indent_error.txt
          sed -i -z 's/\n/%0A/g' max_indent_error.txt
          text_indent=`cat max_indent_error.txt`
          echo "::set-output name=message_body::$text_indent"
```

- (2023/4/29加筆)C/C++のコードを一切修正しないと、metrix++で解析をかけた結果のdbファイル（metrixpp.db）のデータ数が0となります。その結果、以下の処理が失敗します。
    - ```metrix++ limit --log-level=WARNING --db-file=metrixpp.db （以下略）```
- そのため、sqlite3でdbファイルのデータ数を確認し、ゼロかそれ以外の場合で処理を分岐させています。

```yaml
- name: check max indent
  id: check_max_indent
  if: steps.metrics_analysis.outcome == 'success'
  run: | 
    # if all C/C++ files are not changed, count = 0
    if [ $(sqlite3 ./metrixpp.db 'SELECT COUNT(*) FROM "std.code.lines"') != "0" ]; then
      metrix++ limit --log-level=WARNING --db-file=metrixpp.db --max-limit=std.code.complexity:maxindent:4 >>max_indent_error.txt
    else
      echo "All C/C++ files are not changed." >>max_indent_error.txt
    fi
```

## 4.2. ネストが深い関数をPull Requestして解析＆結果の確認

- サンプルリポジトリのブランチで、```deep-nested-hello```というブランチを作成し、以下のようなネストの深い関数```deep_nested_hello()```を作成しました。

```c++
// mylib.hpp
#ifndef INCLUDE_MYLIB_MYLIB_HPP_
#define INCLUDE_MYLIB_MYLIB_HPP_

class Mylib {
 public:
    void hello();
    void nested_hello();
    void deep_nested_hello();  // この関数
};

#endif  // INCLUDE_MYLIB_MYLIB_HPP_
```

```c++
(略)
void Mylib::deep_nested_hello() {
    // dummy val
    std::string tmp1("tmp1");
    std::string tmp2("tmp2");
    std::string tmp3("tmp3");
    std::string tmp4("tmp4");
    std::string tmp5("tmp5");

    if ( tmp1 == "tmp1" ) {
        if ( tmp2 == "tmp2" ) {
            if ( tmp3 == "tmp3" ) {
                if ( tmp4 == "tmp4" ) {
                    if ( tmp5 == "tmp5" ) {
                        std::cout << "Deep Nested Hello world!" << std::endl;
                    }
                }
            }
        }
    }
}
```

- deep-nested-helloブランチからmainブランチにPull Requestを発行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624259/9a5fae2b-4de8-6125-3b28-a7ac4cf11b9e.png)

- Pull RequestをトリガーにCIジョブが起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624259/0f8d2767-5967-c4ea-2708-361e28bbe54f.png)

- Pull Requestコメントにメトリクス結果がコメントされました。ネスト: 4を超えているためエラーとなり、Mergeできないです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624259/4bf42a5a-8110-265c-86ff-0bd6a80ee3b3.png)

# 5. おわりに

- metrix++はインストールも簡単にでき、それ単独で解析＆テキストベースの出力が得られるため非常にCI/CDと相性が良いです。
- また、metrix++のGithubリポジトリのIssueでは、[#15](https://github.com/metrixplusplus/metrixplusplus/issues/15)にて、htmlファイルでの出力する関連ツールが作られており、リポジトリ全体の解析結果の俯瞰もできそうです。