Clang の libTooling を利用して C/C++ のソースコードを字句解析するための調査

# はじめに

いろいろと調べて理解した(つもりの)ことをメモしておきます。

内容は、非効率だったり間違いだったりする可能性があります。

# 環境

* Ubuntu Server 20.04.2 LTS
* Clang 10.0.0
* CMake 3.16.3
* Ninja 1.10.0

# 参照情報

* https://github.com/llvm/llvm-project/tree/llvmorg-10.0.0
* https://clang.llvm.org/doxygen/ (※)
* https://llvm.org/doxygen/ (※)
* https://cmake.org/cmake/help/v3.16/

※2021/03/08 現在は "13.0.0git" の表記があり、実行環境とバージョンが異なる点に留意。

# 準備

以下のようにパッケージをインストールしました。

```
sudo apt-get install clang
sudo apt-get install libclang-dev
sudo apt-get install cmake
sudo apt-get install ninja-build
```

以下のようにディレクトリを作成しました。また AddClang.cmake をダウンロードしました。

```
mkdir -p ~/workspace/sample
cd ~/workspace/sample
mkdir build src
mkdir -p cmake/modules
cd cmake/modules
curl -L -O https://github.com/llvm/llvm-project/raw/llvmorg-10.0.0/clang/cmake/modules/AddClang.cmake
```

AddClang.cmake は後述の CMakeLists.txt の中で使用したいのですが、Clang のパッケージに含まれていないようだったのでダウンロードしました。

以下のリンク先のやりとりから、AddClang.cmake は今後の Clang のバージョンのパッケージには含まれるような気がします。

* https://reviews.llvm.org/D94533

# ソースファイルの作成

Sample.cpp というファイル名で作成しました。

```
cd ~/workspace/sample/src
code Sample.cpp
```

内容はチュートリアルにあるサンプルコード (以下のリンク先) をそのまま使用しました。

* https://github.com/llvm/llvm-project/blob/llvmorg-10.0.0/clang/docs/LibTooling.rst#putting-it-together-----the-first-tool

# CMakeLists.txt の作成

Sample.cpp と同じディレクトリに作成しました。

```
cd ~/workspace/sample/src
code CMakeLists.txt
```

内容は以下のようにしました。

```cmake
cmake_minimum_required(VERSION 3.16.3)

project(sample)

# LLVMConfig.cmake を読み込む。
find_package(LLVM REQUIRED CONFIG)
# ClangConfig.cmake を読み込む。
find_package(Clang REQUIRED CONFIG)

# LLVM_LINK_LLVM_DYLIB は LLVMConfig.cmake の中で ON に設定されている。
# CLANG_LINK_CLANG_DYLIB も ON に設定する。
set(CLANG_LINK_CLANG_DYLIB ${LLVM_LINK_LLVM_DYLIB})

# add_clang_executable の中で使用される add_llvm_executable のために AddLLVM.cmake を読み込む。
list(APPEND CMAKE_MODULE_PATH ${LLVM_DIR})
include(AddLLVM)

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/modules")
include(AddClang)

include_directories(${LLVM_INCLUDE_DIRS})
include_directories(${CLANG_INCLUDE_DIRS})

# https://github.com/llvm/llvm-project/blob/llvmorg-10.0.0/clang/cmake/modules/AddClang.cmake#L139
add_clang_executable(sample Sample.cpp)

# https://github.com/llvm/llvm-project/blob/llvmorg-10.0.0/clang/cmake/modules/AddClang.cmake#L181
# CLANG_LINK_CLANG_DYLIB があると動的リンク (clang-cpp とリンク) になる。
clang_target_link_libraries(sample PRIVATE clangFrontend clangTooling)
```

# ビルド

以下のようにビルドしました。compile_commands.json も生成するようにしました。

```
cd ~/workspace/sample/build
cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ../src
ninja
```

# サンプルをそのまま実行

以下のように SyntaxOnlyAction は何も表示しない Action でした。

```
$ cd ~/workspace/sample/build
$ ./sample -p compile_commands.json ../src/Sample.cpp
(表示なし)
```

# Action を書き換えて実行

以下のように SyntaxOnlyAction を DumpRawTokensAction に書き換えて実行してみました。

```
$ cd ~/workspace/sample/src
$ cp Sample.cpp Sample.cpp.old
$ code Sample.cpp
$ diff Sample.cpp.old Sample.cpp
27c27
<   return Tool.run(newFrontendActionFactory<clang::SyntaxOnlyAction>().get());
---
>   return Tool.run(newFrontendActionFactory<clang::DumpRawTokensAction>().get());
$ cd ~/workspace/sample/build
$ ninja
$ ./sample -p compile_commands.json ../src/Sample.cpp
comment '// Declares clang::SyntaxOnlyAction.'   [StartOfLine]  Loc=</home/ayweak/workspace/sample/src/Sample.cpp:1:1>
unknown '
'               Loc=</home/ayweak/workspace/sample/src/Sample.cpp:1:37>
hash '#'         [StartOfLine]  Loc=</home/ayweak/workspace/sample/src/Sample.cpp:2:1>
...省略...
```

字句解析して得られたトークンの情報を表示してくれました。

その他にもいくつかの Action が用意されているようです。以下のリンク先で確認できました。

* https://clang.llvm.org/doxygen/classclang_1_1FrontendAction.html

## 同じ表示は clang -cc1 -dump-raw-tokens で可能

今回の調査の後に気づいたのですが、clang の -dump-raw-tokens オプションで同じ表示が可能でした。

```
$ clang -cc1 --help
...省略...
  -dump-raw-tokens        Lex file in raw mode and dump raw tokens
...省略...
$ cd ~/workspace/sample
$ clang -cc1 -dump-raw-tokens src/Sample.cpp
comment '// Declares clang::SyntaxOnlyAction.'   [StartOfLine]  Loc=<src/Sample.cpp:1:1>
unknown '
'               Loc=<src/Sample.cpp:1:37>
hash '#'         [StartOfLine]  Loc=<src/Sample.cpp:2:1>
...省略...
```

# 独自の Action を作成して実行

トークンの情報を JSON 形式で表示したいと考えて、以下の処理を参考に JSONDumpRawTokensAction を作成しました。

* https://github.com/llvm/llvm-project/blob/llvmorg-10.0.0/clang/lib/Frontend/FrontendActions.cpp#L741
* https://github.com/llvm/llvm-project/blob/llvmorg-10.0.0/clang/lib/Lex/Preprocessor.cpp#L233
* https://github.com/llvm/llvm-project/blob/llvmorg-10.0.0/llvm/include/llvm/Support/JSON.h#L703

JSONDumpRawTokensAction.h は以下のように作成しました。src 内に配置しました。

```c++
#ifndef JSON_DUMP_RAW_TOKENS_ACTION_H
#define JSON_DUMP_RAW_TOKENS_ACTION_H

#include "clang/Frontend/FrontendAction.h"

class JSONDumpRawTokensAction : public clang::PreprocessorFrontendAction {
protected:
  void ExecuteAction() override;
};

#endif
```

JSONDumpRawTokensAction.cpp は以下のように作成しました。src 内に配置しました。

```c++
#include "JSONDumpRawTokensAction.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Lex/Token.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/raw_ostream.h"

using namespace clang;

void JSONDumpRawTokensAction::ExecuteAction() {
  Preprocessor &PP = getCompilerInstance().getPreprocessor();
  SourceManager &SM = PP.getSourceManager();
  llvm::json::OStream J(llvm::errs());

  const llvm::MemoryBuffer *FromFile = SM.getBuffer(SM.getMainFileID());
  Lexer RawLex(SM.getMainFileID(), FromFile, SM, PP.getLangOpts());
  RawLex.SetKeepWhitespaceMode(true);

  Token RawTok;
  RawLex.LexFromRawLexer(RawTok);

  J.object([&]{
    if (RawTok.is(tok::eof)) {
        return;
    }
    J.attribute("file", SM.getFilename(RawTok.getLocation()));
    J.attributeArray("tokens", [&]{
      while (RawTok.isNot(tok::eof)) {
        J.object([&]{
          J.attribute("kind", tok::getTokenName(RawTok.getKind()));
          J.attribute("spelling", PP.getSpelling(RawTok));
          if (RawTok.isAtStartOfLine()) {
            J.attribute("start_of_line", true);
          }
          if (RawTok.hasLeadingSpace()) {
            J.attribute("leading_space", true);
          }
          if (RawTok.isExpandDisabled()) {
            J.attribute("expand_disabled", true);
          }
          if (RawTok.needsCleaning()) {
            const char *Start = SM.getCharacterData(RawTok.getLocation());
            J.attribute("unclean", StringRef(Start, RawTok.getLength()));
          }
          J.attribute("line", SM.getSpellingLineNumber(RawTok.getLocation()));
          J.attribute("column", SM.getSpellingColumnNumber(RawTok.getLocation()));
        });
        RawLex.LexFromRawLexer(RawTok);
      }
    });
  });
}
```

Sample.cpp は以下のように変更しました。

```
$ diff Sample.cpp.old Sample.cpp
0a1
> #include "JSONDumpRawTokensAction.h"
27c28
<   return Tool.run(newFrontendActionFactory<clang::SyntaxOnlyAction>().get());
---
>   return Tool.run(newFrontendActionFactory<JSONDumpRawTokensAction>().get());
```

CMakeLists.txt は以下のように変更しました。

```
$ diff CMakeLists.txt.old CMakeLists.txt
19c19
< add_clang_executable(sample Sample.cpp)
---
> add_clang_executable(sample Sample.cpp JSONDumpRawTokensAction.cpp)
```

ビルドして実行してみると JSON 形式で表示することを確認できました。

```
$ cd ~/workspace/sample/build
$ cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ../src
$ ninja
$ ./sample -p compile_commands.json ../src/Sample.cpp 2>&1 | python3 -mjson.tool
{
    "file": "/home/ayweak/workspace/sample/src/Sample.cpp",
    "tokens": [
        {
            "kind": "hash",
            "spelling": "#",
            "start_of_line": true,
            "line": 1,
            "column": 1
        },
...省略...
```

# まとめ

ある程度の方法は理解できたような気がします。