こんにちは!開発環境アーキテクトの私です。
毎日のコードレビュー、大変ですよね。「また同じ指摘をしちゃったな」「なぜこのチームメンバーは、この危険な関数(例えば `strcpy` や `malloc` の戻り値未チェックなど)を使い続けてしまうんだろう……」と、夜な夜なプルリクエストと睨めっこしている方も多いのではないでしょうか。
人間の目によるコードレビューには限界があります。だからこそ、機械的にルール違反を検知する「静的解析ツール」の導入が不可欠なのです。
しかし、既存の静的解析ツール(FlawfinderやCppcheckなど)では、「うちのチーム独自の、この特殊なマクロや関数構造のルールを検知させたい!」という要望を満たせないことがよくあります。かといって、C++で本格的なClangプラグインやLLVMパスを書くとなると、膨大なボイラープレートコードと難解なテンプレート地獄に心が折れてしまいますよね。
そこで今回は、「ClangのAST(抽象構文木)をPythonから手軽に叩いて、自分専用の静的解析ツールを爆速で試作する方法」を伝授します。
これをマスターすれば、たった数十行のスクリプトで世界に一つだけのカスタムLinterが作れるようになり、毎日のコーディングライフが劇的に楽になりますよ。さあ、一緒に低レイヤの世界へ踏み出しましょう!
—
なぜC++ではなく「Python (libclang)」なのか?
通常、Clangを使ってソースコードを解析しようとすると、C++でLLVMの巨大なエコシステムに依存したプラグインを書く必要があります。これはビルド時間も長く、開発フィードバックループが遅いため、ちょっとしたルールの試作には重すぎます。
ここで登場するのが、ClangのコアライブラリをPythonから安全に呼び出せる公式バインディング `libclang` です。
内部で何が起きているのか?(アーキテクトの視点)
1. 字句解析(Lexer): ソースコードの文字列を意味のある最小単位(トークン)に分割します。
2. 構文解析(Parser): トークン群を組み立て、文法規則に基づいた木構造である AST(Abstract Syntax Tree) をメモリ上に構築します。
3. libclang API: Pythonスクリプトはこの `libclang` を通じて、C++の複雑なメモリ管理を意識することなく、C言語の関数ポインタ経由でASTのノード群を安全に「巡回(Traversal)」できます。
つまり、Pythonの柔軟なデータ構造と記述力を活かしながら、世界最高峰のC/C++コンパイラフロントエンドの解析能力をそのまま手に入れられるというわけです。
—
1. 基礎セットアップ:環境を整える
まずは、LLVM/Clangのライブラリと、Pythonからそれを扱うためのパッケージをインストールします。ここではmacOSまたはUbuntuを前提に話を進めますが、Windowsでもパスの通し方が違うだけで基本は同じです。
必要なパッケージのインストール (Ubuntuの場合)
システムに最新のClangとlibclang開発用ライブラリをインストールします
sudo apt-get update
sudo apt-get install -y clang libclang-dev python3-pip
Pythonからlibclangを操作するためのラッパーライブラリをインストールします
pip3 install clang
ここで重要なポイントがあります。Pythonの `clang` パッケージのバージョンと、システムにインストールされている `libclang`(共有ライブラリ)のバージョンが乖離していると、セグメンテーション違反(Segfault)でPythonが突然クラッシュすることがあります。バージョンは可能な限り揃えておいてください。
—
2. 動作確認:ASTの構造を覗き見る「Hello World」
まずは、対象となる小さなC言語のソースコードと、それを読み込んでASTの構造をツリー状に出力するPythonスクリプトを作成しましょう。
対象のC言語コード (`sample.c`)
// 検査対象となるシンプルなCソースコード
include
int add(int a, int b) {
int result = a + b;
return result;
}
int main() {
// 禁止したい危険な関数をあえて使ってみる
int sum = add(5, 10);
return 0;
}
AST可視化スクリプト (`ast_viewer.py`)
import sys
import clang.cindex
def print_ast(node, depth=0):
“””
再帰的にASTのノードを走査し、ノードの種類(kind)と名前(spelling)をインデント付きで表示する関数
“””
# インデントを作成して階層構造を視覚化しやすくする
indent = ” ” depth
print(f”{indent}- Node: {node.kind} (Name: {node.spelling}, Location: {node.location.file})”)
# 子ノードを再帰的に巡回
for child in node.get_children():
print_ast(child, depth + 1)
if __name__ == “__main__”:
if len(sys.argv) < 2:
print("Usage: python3 ast_viewer.py
sys.exit(1)
target_file = sys.argv[1]
# ClangのIndexを作成(第2引数Trueでパースエラーなどを無視せず詳細に取得)
index = clang.cindex.Index.create()
# ソースコードをパースしてTranslationUnit(翻訳単位=ASTのルート)を取得
# 引数にはコンパイラに渡すオプション(インクルードパスなど)をリストで渡せます
try:
translation_unit = index.parse(target_file, args=[‘-std=c99’])
except Exception as e:
print(f”Failed to parse: {e}”)
sys.exit(1)
print(f”=== AST Dump for {target_file} ===”)
# ルートノードからASTの走査を開始
print_ast(translation_unit.cursor)
実行と確認
以下のコマンドでスクリプトを実行してみてください。
python3 ast_viewer.py sample.c
実行すると、コンソールに膨大な構文木のデータが流れます。これがまさにClangが理解しているあなたのコードの世界です。`FUNCTION_DECL`(関数の宣言)や `BINARY_OPERATOR`(二項演算子)など、すべての構造がオブジェクトとして表現されていることがわかります。
—
3. 実践:独自のコーディングルール違反を検知するカスタムLinterの作成
ここからが本番です。
「プロジェクト内では、生の値や安全性の低い加算関数ではなく、特定のビルトイン関数やカスタム関数を使わなければならない」というチーム規約があると仮定しましょう。
今回は、「`add` という名前の関数を定義すること、あるいは特定の式を使っている箇所を自動検知して警告を出すスクリプト」を実装します。
カスタムLinterスクリプト (`custom_linter.py`)
import sys
import clang.cindex
def analyze_ast(node, file_name):
“””
ASTを走査し、特定のルール違反がないかをチェックする関数
“””
# ノードが解析対象のファイル自身のものであるか確認(ヘッダファイルの標準ライブラリ等は除外するため)
if node.location.file and node.location.file.name == file_name:
# ルール1: 「add」という名前の関数定義を見つけたら警告する(例としてのカスタムルール)
if node.kind == clang.cindex.CursorKind.FUNCTION_DECL and node.spelling == “add”:
print(f”[警告] 独自ルール違反 (Line {node.location.line}): ”
f”関数 ‘add’ の直接定義は禁止されています。共通ライブラリを使用してください。”)
# ルール2: 二項演算子 ‘+’ (足し算)が使われている場所を検出する
elif node.kind == clang.cindex.CursorKind.BINARY_OPERATOR:
# トークンを走査して ‘+’ 演算子が含まれているか確認
tokens = list(node.get_tokens())
token_spelling = [t.spelling for t in tokens]
if ‘+’ in token_spelling:
print(f”[情報] コードインスペクション (Line {node.location.line}): ”
f”生のカプセル化されていない加算演算子が検出されました。”)
# 子ノードを再帰的に巡回してチェックを継続
for child in node.get_children():
analyze_ast(child, file_name)
if __name__ == “__main__”:
if len(sys.argv) < 2:
print("Usage: python3 custom_linter.py
sys.exit(1)
target_file = sys.argv[1]
index = clang.cindex.Index.create()
translation_unit = index.parse(target_file, args=[‘-std=c99’])
print(f”Running custom static analysis on {target_file}…”)
analyze_ast(translation_unit.cursor, target_file)
print(“Analysis finished.”)
Linterの実行結果
python3 custom_linter.py sample.c
出力結果:
Running custom static analysis on sample.c…
[警告] 独自ルール違反 (Line 4): 関数 ‘add’ の直接定義は禁止されています。共通ライブラリを使用してください。
[情報] コードインスペクション (Line 5): 生のカプセル化されていない加算演算子が検出されました。
Analysis finished.
どうでしょうか?たったこれだけのコードで、自分たちが定義した「プロジェクト独自のアーキテクチャ規約」に違反しているコードを正確にピンポイントで射抜くことができました。
—
4. CI/CDパイプラインへの組み込み
この強力なスクリプトを、ローカルのチェックだけでなく GitHub Actions などのCIパイプラインに組み込むことで、レビューの自動化を完全に担保できます。
以下に、GitHub Actionsでこの静的解析を走らせるワークフローの例を提示します。
`.github/workflows/static_analysis.yml`
name: Custom Static Analysis
プルリクエストやメインブランチへのプッシュ時に発動
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
# リポジトリのソースコードをチェックアウト
- name: Checkout Repository
uses: actions/checkout@v4
# Pythonと必要なパッケージのセットアップ
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ‘3.10’
# システムのClangとPython用libclangをインストール
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang libclang-dev
pip install clang
# 自作の静的解析スクリプトを実行(違反が見つかった場合は終了コードを非ゼロにしてCIを落とすことも可能)
- name: Run Custom Linter
run: |
python3 custom_linter.py sample.c
このように、CI環境に `libclang` をインストールし、PRが作成されたタイミングでスクリプトを実行するようにしておけば、チームメンバー全員がルールを忘れていても、マージ前に機械的にガードすることができます。
—
先輩エンジニアからのエール
今回は、ClangのASTマッチングをPythonで手軽に試し、独自の静的解析ツールを構築してCIに組み込むまでのステップを解説しました。
既存のツールに縛られる必要はありません。「自分たちのコードベースの品質は、自分たちの手で定義する」。この自律性こそが、優秀なエンジニアリングチームを支える最大の武器になります。
Pythonとlibclangの組み合わせなら、思いついたルールを数分でコードに落とし込み、即座に検証が可能です。「毎日の面倒な指摘」を自動化し、あなたとチームのエネルギーを、もっとクリエイティブで本質的な設計・実装に注ぎ込みましょう。
あなたの開発ライフが、このツールによってさらに快適でエキサイティングなものになることを心から応援しています!