詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

C/C++のコードを静的解析する(clang-tidyのカスタムチェック)

C/C++のソースコードに対して、独自のlintを行いたくなるシチュエーションもときどき発生しますね。

この問題については、現代ではClangにLibToolingというライブラリがある。これによって、Clangの資産を使ったC/C++の静的解析・リファクタリングツールを作ることができる。とくにASTMatcherによって、ソースコードのAST(Abstract Syntax Tree、抽象構文木)に対する走査はわりと簡単に書ける。

インタフェースを考える

公式のチュートリアルとして、LibToolingでスタンドアロンのツールを書く方法が紹介されている。けどこれはLLVMのソースツリー内にファイルを置くのでちょっと不格好かな。別のソースツリーとしていちから書く方法は、How to create a C/C++ Static Code analysis tool が参考になった。

しかしlinterなんて入出力はフォーマットも含めて限られているものだし、まったく新しいツールとして作るよりは、標準的なものにあわせるのが賢明だろう。Clangsベースのlinterとしてclang-tidy があるので、これを拡張する方向で考えたい。

clang-tidyには標準的なチェック(ルール)が組み込まれているだけでなく、共有ライブラリを渡すことで新しいチェックを外部から追加することができる。

clang-tidy --load=mycheck.so ...

こんな感じ。今回はこの mycheck.so を作りたい。では早速いくぞよ!!!

書きはじめる

clang-tidyに新しいチェックを追加する方法は Writing a clang-tidy Check に書かれている。生成スクリプトがあって便利だ。LLVMのソースツリー内でPythonスクリプトを実行することで、必要なファイルが生成される。このスクリプトで生成される、awesome-function-names という、「関数名が awesome_ ではじまっていることのチェック」の実装をベースにして進めてみることにする。

# llvm/llvm-project で…
% ./clang-tools-extra/clang-tidy/add_new_check.py readability awesome-function-names
Updating ./clang-tools-extra/clang-tidy/readability/CMakeLists.txt...
Creating ./clang-tools-extra/clang-tidy/readability/AwesomeFunctionNamesCheck.h...
Creating ./clang-tools-extra/clang-tidy/readability/AwesomeFunctionNamesCheck.cpp...
Updating ./clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp...
Updating clang-tools-extra/docs/ReleaseNotes.rst...
Creating clang-tools-extra/test/clang-tidy/checkers/readability/awesome-function-names.cpp...
Creating clang-tools-extra/docs/clang-tidy/checks/readability/awesome-function-names.rst...
Updating clang-tools-extra/docs/clang-tidy/checks/list.rst...
Done. Now it's your turn!

けれどこれはやっぱりLLVM本体にコードを追加することになってるので、ソースツリー外でやりたい。

ここで生成されたファイルをベースに、新しいリポジトリを作ることにする。サンプルリポジトリを https://github.com/motemen/example-clang-tidy-check-outoftree に作っています。

まずは AwesomeFunctionNamesCheck.{cpp,h} をコピーしてきて、相対パスになっている#includeを書き換える(9ea0067)。

CMakeLists.txt は以下のように書いておく。

cmake_minimum_required(VERSION 3.0)
project(AwesomeFunctionNames VERSION 0.1.0)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Clang REQUIRED CONFIG)

add_library(AwesomeFunctionNames MODULE AwesomeFunctionNamesCheck.cpp)

set_target_properties(AwesomeFunctionNames PROPERTIES OUTPUT_NAME AwesomeFunctionNamesCheck_${LLVM_VERSION_MAJOR})

target_include_directories(AwesomeFunctionNames PRIVATE ${CLANG_INCLUDE_DIRS})

target_link_libraries(AwesomeFunctionNames PRIVATE
    clangTidy
    clangTidyUtils
)

成果物は libAwesomeFunctionNamesCheck_16.so みたいになる。この時点でビルドが通るはず。

テストも自動生成されているので追加する。clang-tidyチェックのテスト用のPythonスクリプト check_clang_tidy.py もあるので、これもコピーしてしまう。それなりに高度なことをしていて、これを再発明はしたくないので。

CMakeList.txt にテストを追加(d22beb8)。この時点ではテストは通りません。

@@ -16,3 +16,12 @@ target_link_libraries(AwesomeFunctionNames PRIVATE
     clangTidy
     clangTidyUtils
 )
+
+enable_testing()
+
+add_test(
+    NAME main
+    COMMAND python ${CMAKE_SOURCE_DIR}/test/check_clang_tidy.py ${CMAKE_SOURCE_DIR}/test/awesome-function-names.cpp readability-awesome-function-names tmp --
+        -load=${CMAKE_BINARY_DIR}/libAwesomeFunctionNamesCheck_${LLVM_VERSION_MAJOR}.so
+    WORKING_DIRECTORY ..
+)

最後に、共有モジュールがロードされたときにチェックが追加されるようなコードを追加する(6f239a13)。

--- a/AwesomeFunctionNamesCheck.cpp
+++ b/AwesomeFunctionNamesCheck.cpp
@@ -31,3 +34,18 @@ void AwesomeFunctionNamesCheck::check(const MatchFinder::MatchResult &Result) {
 }
 
 } // namespace clang::tidy::readability
+
+namespace clang::tidy {
+
+class ExtraModule : public ClangTidyModule {
+public:
+  void addCheckFactories(ClangTidyCheckFactories &CheckFactories) override {
+    CheckFactories.registerCheck<readability::AwesomeFunctionNamesCheck>(
+        "readability-awesome-function-names");
+  }
+};
+
+static ClangTidyModuleRegistry::Add<ExtraModule> X("extra-module",
+                                                   "Adds extra lint checks.");
+
+} // namespace clang::tidy

これでやっと雛形が完成した! テストファイルは以下のようになっていて、コメントの通り編集すれば意図は実現できると思う。

// FIXME: Add something that triggers the check here.
void f();
// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [readability-awesome-function-names]

// FIXME: Verify the applied fix.
//   * Make the CHECK patterns specific enough and try to make verified lines
//     unique to avoid incorrect matches.
//   * Use {{}} for regular expressions.
// CHECK-FIXES: {{^}}void awesome_f();{{$}}

// FIXME: Add something that doesn't trigger the check here.
void awesome_f2();

できたものは clang-check に渡して clang-tidy -load=./build/libAwesomeFunctionNamesCheck_16.so -checks='-*,readability-awesome-function-names' <file> のような形で利用できる。とくに -list-checks を使えば、ロードがちゃんとできているかの確認もできる。

% clang-tidy --load=./build/libAwesomeFunctionNamesCheck_16.so -checks='-*,readability-awesome-function-names' -list-checks
Enabled checks:
    readability-awesome-function-names

ASTの探索

ここまでできれば、メインの仕事は、これをベースに探索したいAST向けのコードを書くことになる。AST Matcher Reference とかを見ながら雰囲気で書ける……と思う。registerMatchers の中でASTMatcherを登録して、check でそこから情報を取り出していく流れ。

ひとつ注意しておきたいのが、デフォルトではASTMatcherは暗黙的に解釈生成されたノードも操作対象とするため、コードとしてに実際に書かれたものよりたくさんのものが見えているらしいこと。コード上では直接の子になっているようにしかみえないノードが実はそうでなかったりする。そういう現象に出会った場合は TK_IgnoreUnlessSpelledInSource というフラグを有効にしてあげるとよい。Traverse Mode を参照。

実際にどう解析されているか、どんなmatcherがヒットするのか知りたくなった場合は clang-query コマンドが使えた。

% clang-query file.cpp
...
clang-query> set output dump # ASTのダンプを有効にする
clang-query> match functionDecl() # matcherを適用してみる

Match #1:

Binding for "root":
FunctionDecl 0x13d0f0640 <path/to/file.cpp:1:1, line:4:1> line:1:5 foo 'int ()'
`-CompoundStmt 0x13d0f09e0 <col:11, line:4:1>
  |-DeclStmt 0x13d0f0980 <line:2:3, col:17>
  | `-VarDecl 0x13d0f07a0 <col:3, col:16> col:8 used n 'int':'int' cinit
  |   `-BinaryOperator 0x13d0f0848 <col:12, col:16> 'int' '+'
  |     |-IntegerLiteral 0x13d0f0808 <col:12> 'int' 1
  |     `-IntegerLiteral 0x13d0f0828 <col:16> 'int' 2
  `-ReturnStmt 0x13d0f09d0 <line:3:3, col:10>
    `-ImplicitCastExpr 0x13d0f09b8 <col:10> 'int':'int' <LValueToRValue>
      `-DeclRefExpr 0x13d0f0998 <col:10> 'int':'int' lvalue Var 0x13d0f07a0 'n' 'int':'int'

1 match.

便利ですね。


これを使って実際に作ってみたものとして、文字列が(gettext的な意味で)国際化されているかどうかをチェックする clang-tidy-check-extra-i18n-string ってのがあります。C++はほぼ書いたことないので微妙かもしれないけど、オプションを渡したりとかそういうことをしたい場合の例として。

おまけ: yak-shavingマップ

おまけで、今回のこの内容をやるに至ったyak-shavingマップを有料部分に書いておきます。 自分がやってる趣味プロジェクトとかの話が書いてありますが、とくに有意義なことはないので、興味があればどうぞ。

最初の一行だけ載せておきます。

この続きはcodocで購入

はてなで一緒に働きませんか?