PythonのlibclangでC++のヘッダーファイルから情報を抽出する

# 概要

Pythonのlibclangを使用してC++のヘッダーファイルから関数宣言の情報を抽出する方法の例です。

具体例のリポジトリは以下で公開しています。
https://github.com/ishioka0222/libclang_example

この記事には、以下のようなヘッダーファイル（説明のため、わざと意味のないincludeをしています）をパースし、
```cpp:header.h
#include <iostream>

int my_add(int a, int b); // addの宣言と同じ行にあるコメント

int my_sub(int a, int b); // subの宣言と同じ行にあるコメント
```
以下のようなJSONファイルを出力するサンプルコードを載せています。
```json
[
    {
        "name": "my_add",
        "return_type": "int",
        "params": [
            {
                "name": "a",
                "type": "int"
            },
            {
                "name": "b",
                "type": "int"
            }
        ],
        "filename": "header.h",
        "line_number": 3,
        "comment": "// addの宣言と同じ行にあるコメント"
    },
    {
        "name": "my_sub",
        "return_type": "int",
        "params": [
            {
                "name": "a",
                "type": "int"
            },
            {
                "name": "b",
                "type": "int"
            }
        ],
        "filename": "header.h",
        "line_number": 5,
        "comment": "// subの宣言と同じ行にあるコメント"
    }
]
```

# 関数宣言の情報を抽出する

まずはlibclangをインストールします。
```sh
pip install libclang
```

`main.py`という名前で以下のようなファイルを作成します。
```py:main.py
import argparse
import json
import os

from clang.cindex import CursorKind, Index, TokenKind


def main():
    # コマンドライン引数をパースする。
    parser = argparse.ArgumentParser()
    parser.add_argument("filename", help="Input file")
    args = parser.parse_args()

    # 入力ファイル名を取得する。
    input_filename = os.path.basename(args.filename)

    # libclang の Index オブジェクトを作成する。
    index = Index.create()
    # 入力ファイルをパースする。
    # -x c++ オプションを指定することで、C++ のソースコードとしてパースする。
    translation_unit = index.parse(args.filename, args=["-x", "c++"])

    # コメントの行番号からコメント文字列へのディクショナリを作成する。
    comments = {}
    for token in translation_unit.cursor.get_tokens():
        if token.kind == TokenKind.COMMENT:
            line_number = token.location.line
            comment = token.spelling

            comments[line_number] = comment

    # 関数宣言の情報からなるリストを作成する。
    functions = []
    for child in translation_unit.cursor.get_children():
        # 関数宣言以外は無視する。
        if child.kind != CursorKind.FUNCTION_DECL:
            continue

        # ファイル名がない場合は無視する。
        if child.location.file is None:
            continue

        # ファイル名を取得する。
        filepath = child.location.file.name
        filename = os.path.basename(filepath)

        # 関数宣言があるファイルが入力ファイルに一致しない場合は無視する。
        # これがないと #include で読み込んだヘッダファイルの関数宣言も出力されてしまう。
        if filename != input_filename:
            continue

        # 各種情報を取得する。
        function_name = child.spelling
        return_type = child.result_type.spelling

        line_number = child.location.line
        comment = comments.get(line_number, "")

        params = []
        for param in child.get_arguments():
            param_name = param.spelling
            param_type = param.type.spelling

            params.append({
                "name": param_name,
                "type": param_type
            })

        functions.append({
            "name": function_name,
            "return_type": return_type,
            "params": params,
            "filename": filename,
            "line_number": line_number,
            "comment": comment
        })

    # JSON 形式で出力する。
    print(json.dumps(functions, indent=4, ensure_ascii=False))


if __name__ == "__main__":
    main()
```

`main.py`の引数としてC++のヘッダーファイルを渡すことで、最初の例のような出力が得られます。
```sh
$ python main.py header.h
[
    {
        "name": "my_add",
        "return_type": "int",
        "params": [
            {
                "name": "a",
                "type": "int"
            },
            {
                "name": "b",
                "type": "int"
            }
        ],
        "filename": "header.h",
        "line_number": 3,
        "comment": "// addの宣言と同じ行にあるコメント"
    },
    {
        "name": "my_sub",
        "return_type": "int",
        "params": [
            {
                "name": "a",
                "type": "int"
            },
            {
                "name": "b",
                "type": "int"
            }
        ],
        "filename": "header.h",
        "line_number": 5,
        "comment": "// subの宣言と同じ行にあるコメント"
    }
]
```

# 備考

関数宣言程度の情報であれば上の例のようにデフォルトで取得できますが、プリプロセッサなどの詳細な情報はデフォルトでは取得できないようです。
こういった詳細な情報も取得したい場合は、`parse`を呼び出すときに`PARSE_DETAILED_PROCESSING_RECORD`というオプションを指定する必要があります。

```py
from clang.cindex import CursorKind, Index, TokenKind, TranslationUnit

# （略）

translation_unit = index.parse(
        args.filename, options=TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD)
```