1. はじめに
1.1. 動機
- c++コードをlizardでメトリクス解析をしているとき、ネスト(max indent)が計測できないので、代わりのツールはないか探していました。
- SonarQubeを使用することも考えましたが、もう少し手軽に使用できるツールを探しました。
- そのとき試使用して使い勝手が良かったmetrix++について、サンプルプログラムを用いて、マニュアル解析したり、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. リポジトリ
1.4. 参考資料
No. | URL | 説明 |
---|---|---|
[1] | metrix++ Doc | 本記事で紹介するC++メトリクス解析ツール(Doc) |
[2] | metrix++ Github | 本記事で紹介するC++メトリクス解析ツール(Github) |
[3] | C/C++プロジェクトをCMakeでビルドする | 提示するサンプルプログラムのcmake環境は、こちらのQiita記事をもとに作成しました。 |
2. 準備:サンプルプログラム
devcontainer構築後の操作は、明示しない限りdevcontainer内で実施します。
2.1. 環境構築
- サンプルプログラムはdevcontainerで環境構築しているため、git cloneした後、VS Codeを開きます。
- VSCode左下にある「><」みたいなボタンを押下して、「Reopen in Container」を押下します。押下すると、VSCodeがc++開発用のdockerコンテナを生成します。
$ 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を参照願います。
$ 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
- 主なコマンドは以下の通りです。
# 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出力、上限確認を実施することができます。- ここでは、上限値として、ネスト:4、サイクロマティック複雑度:10と設定しています。
# 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加筆
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ファイルのデータ数を確認し、ゼロかそれ以外の場合で処理を分岐させています。
- 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()
を作成しました。
// 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_
(略)
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を発行します。
- Pull RequestをトリガーにCIジョブが起動します。
- Pull Requestコメントにメトリクス結果がコメントされました。ネスト: 4を超えているためエラーとなり、Mergeできないです。
5. おわりに
- metrix++はインストールも簡単にでき、それ単独で解析&テキストベースの出力が得られるため非常にCI/CDと相性が良いです。
- また、metrix++のGithubリポジトリのIssueでは、#15にて、htmlファイルでの出力する関連ツールが作られており、リポジトリ全体の解析結果の俯瞰もできそうです。