C++ `Clang LibTooling`:基于 Clang AST 的编译期代码分析与生成工具

哈喽,各位好!今天咱们来聊聊Clang LibTooling,这玩意儿听起来高大上,实际上就是个基于Clang AST(抽象语法树)的编译期代码分析和生成工具。说白了,就是让你能在编译的时候,像个超级侦探一样,窥探你的代码,还能动手改改它。

为啥要用LibTooling?

你可能会问,我代码都写完了,编译器也能跑,为啥还要搞这么个玩意儿?原因很简单:

  • 自动化重构: 想批量改名?想把循环改成并行?手动改?不存在的,LibTooling帮你搞定。
  • 静态代码分析: 想找出潜在的Bug?想遵守编码规范?LibTooling帮你检查。
  • 代码生成: 想根据现有代码生成新的代码?LibTooling帮你生成。
  • 自定义语言扩展: 想给C++加点新特性?LibTooling允许你魔改。

总之,有了LibTooling,你就能在编译阶段对代码进行各种骚操作,解放你的双手,提高你的代码质量。

Clang AST是啥?

要玩转LibTooling,首先要了解Clang AST。你可以把它想象成编译器对你代码的一种内部表示,就像X光片一样,能看到你代码的骨架。

// 源代码
int main() {
  int x = 10;
  int y = x + 5;
  return 0;
}

这段代码会被Clang解析成一个AST,大概长这样(简化版):

  • FunctionDecl: main
    • CompoundStmt
      • DeclStmt
        • VarDecl: x
          • IntegerLiteral: 10
      • DeclStmt
        • VarDecl: y
          • BinaryOperator: +
            • DeclRefExpr: x
            • IntegerLiteral: 5
      • ReturnStmt
        • IntegerLiteral: 0

这个AST就像一颗树,每个节点代表代码中的一个元素,比如函数声明、变量声明、表达式等等。

LibTooling的基本流程

使用LibTooling进行代码分析和生成,通常需要经过以下几个步骤:

  1. 创建ToolDriver: 这是LibTooling的入口,负责解析命令行参数,创建编译器实例。
  2. 创建FrontendAction: 这是你的代码分析逻辑的载体,告诉LibTooling你要做什么。
  3. 创建ASTConsumer: 这是实际访问AST的组件,你可以在这里遍历AST,分析代码,修改代码。
  4. 运行ToolDriver: 让ToolDriver驱动整个流程,开始编译和分析代码。

代码示例:查找所有函数调用

咱们先来个简单的例子,用LibTooling查找代码中所有的函数调用。

#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/Tooling.h"
#include <iostream>

using namespace clang;
using namespace clang::tooling;

// 1. 创建ASTConsumer
class FindFunctionCallConsumer : public ASTConsumer {
public:
  explicit FindFunctionCallConsumer(ASTContext *Context) : Visitor(Context) {}

  void HandleTranslationUnit(ASTContext &Context) override {
    Visitor.TraverseDecl(Context.getTranslationUnitDecl());
  }

private:
  class FindFunctionCallVisitor : public RecursiveASTVisitor<FindFunctionCallVisitor> {
  public:
    explicit FindFunctionCallVisitor(ASTContext *Context) : Context(Context) {}

    bool VisitCallExpr(CallExpr *Call) {
      if (FunctionDecl *Func = Call->getDirectCallee()) {
        llvm::outs() << "Function Call: " << Func->getNameInfo().getName().getAsString() << "n";
      }
      return true;
    }

  private:
    ASTContext *Context;
  };

  FindFunctionCallVisitor Visitor;
};

// 2. 创建FrontendAction
class FindFunctionCallAction : public ASTFrontendAction {
public:
  std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler, StringRef InFile) override {
    return std::make_unique<FindFunctionCallConsumer>(&Compiler.getASTContext());
  }
};

int main(int argc, const char **argv) {
  // 3. 创建ToolDriver
  CommonOptionsParser OptionsParser(argc, argv, "Find Function Calls");
  ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());

  // 4. 运行ToolDriver
  return Tool.run(newFrontendActionFactory<FindFunctionCallAction>().get());
}

代码解释:

  • FindFunctionCallConsumer 这个类继承自ASTConsumer,负责处理AST。HandleTranslationUnit函数会在遍历整个翻译单元(一个源文件)之前被调用,我们在这里启动AST的遍历。
  • FindFunctionCallVisitor 这个类继承自RecursiveASTVisitor,负责递归遍历AST。VisitCallExpr函数会在每次遇到函数调用表达式时被调用,我们在这里输出函数名。
  • FindFunctionCallAction 这个类继承自ASTFrontendAction,负责创建ASTConsumer
  • main函数:
    • CommonOptionsParser:解析命令行参数。
    • ClangTool:创建Clang工具实例,需要编译数据库和源文件路径。
    • Tool.run:运行Clang工具,传入一个FrontendActionFactory,用于创建FrontendAction

编译和运行:

  1. 创建compile_commands.json LibTooling需要编译数据库才能正确解析代码。你可以使用CMake或者Bear等工具生成compile_commands.json

    # 使用CMake
    mkdir build
    cd build
    cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..
    make

    或者

    # 使用Bear
    bear -- make
  2. 编译代码:

    clang++ -std=c++17 find_function_calls.cpp -o find_function_calls `llvm-config --cxxflags --ldflags --system-libs --libs core` -I /path/to/llvm/include -I /path/to/clang/include

    注意: 你需要根据你的系统配置修改/path/to/llvm/include/path/to/clang/include

  3. 运行代码:

    ./find_function_calls your_source_file.cpp --

    your_source_file.cpp是你要分析的源文件。

示例代码:

// your_source_file.cpp
#include <iostream>

void foo() {
  std::cout << "Hello from foo!n";
}

int main() {
  foo();
  std::cout << "Hello from main!n";
  return 0;
}

预期输出:

Function Call: foo
Function Call: operator<<

代码示例:自动添加代码注释

接下来,咱们来个稍微复杂点的例子,用LibTooling自动给函数添加代码注释。

#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/Tooling.h"
#include <iostream>

using namespace clang;
using namespace clang::tooling;

// 1. 创建ASTConsumer
class AddCommentConsumer : public ASTConsumer {
public:
  explicit AddCommentConsumer(ASTContext *Context, Rewriter &TheRewriter) : Visitor(Context, TheRewriter), TheRewriter(TheRewriter) {}

  void HandleTranslationUnit(ASTContext &Context) override {
    Visitor.TraverseDecl(Context.getTranslationUnitDecl());
    TheRewriter.overwriteChangedFiles();
  }

private:
  class AddCommentVisitor : public RecursiveASTVisitor<AddCommentVisitor> {
  public:
    explicit AddCommentVisitor(ASTContext *Context, Rewriter &TheRewriter) : Context(Context), TheRewriter(TheRewriter) {}

    bool VisitFunctionDecl(FunctionDecl *Func) {
      SourceLocation StartLoc = Func->getSourceRange().getBegin();

      // 检查是否已经有注释
      if (Func->getBeginLoc().isMacroID() || TheRewriter.getSourceMgr().getPresumedLoc(StartLoc).getColumn() != 1) {
        return true; // 跳过宏定义和非行首的函数
      }

      std::string Comment = "// This function does something.n";
      TheRewriter.InsertTextBefore(StartLoc, Comment);
      return true;
    }

  private:
    ASTContext *Context;
    Rewriter &TheRewriter;
  };

  AddCommentVisitor Visitor;
  Rewriter &TheRewriter;
};

// 2. 创建FrontendAction
class AddCommentAction : public ASTFrontendAction {
public:
  AddCommentAction() {}

  void setRewriter(Rewriter &TheRewriter) {
    this->TheRewriter = &TheRewriter;
  }

  std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler, StringRef InFile) override {
    TheRewriter->setSourceMgr(Compiler.getSourceManager(), Compiler.getLangOpts());
    return std::make_unique<AddCommentConsumer>(&Compiler.getASTContext(), *TheRewriter);
  }

private:
  Rewriter *TheRewriter = nullptr;
};

int main(int argc, const char **argv) {
  // 3. 创建ToolDriver
  CommonOptionsParser OptionsParser(argc, argv, "Add Comments");
  ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());

  // 创建Rewriter
  Rewriter TheRewriter;

  // 4. 运行ToolDriver
  std::unique_ptr<AddCommentAction> action = std::make_unique<AddCommentAction>();
  action->setRewriter(TheRewriter);
  int result = Tool.run(std::unique_ptr<FrontendActionFactory>(new FrontendActionFactoryFromProto(std::move(action))).get());

  return result;
}

代码解释:

  • AddCommentConsumer 这个类继承自ASTConsumer,负责处理AST。HandleTranslationUnit函数会在遍历完整个翻译单元之后被调用,我们在这里调用TheRewriter.overwriteChangedFiles(),将修改后的代码写回文件。
  • AddCommentVisitor 这个类继承自RecursiveASTVisitor,负责递归遍历AST。VisitFunctionDecl函数会在每次遇到函数声明时被调用,我们在这里判断函数是否已经有注释,如果没有,则添加注释。
  • AddCommentAction 这个类继承自ASTFrontendAction,负责创建ASTConsumer
  • Rewriter 这个类是Clang提供的用于修改代码的工具。
  • main函数:
    • 创建Rewriter实例。
    • Rewriter实例传递给AddCommentAction
    • 运行Clang工具。

编译和运行:

编译步骤与前面的例子类似。

示例代码:

// your_source_file.cpp
#include <iostream>

void foo() {
  std::cout << "Hello from foo!n";
}

int main() {
  foo();
  std::cout << "Hello from main!n";
  return 0;
}

修改后的代码:

// your_source_file.cpp
#include <iostream>

// This function does something.
void foo() {
  std::cout << "Hello from foo!n";
}

// This function does something.
int main() {
  foo();
  std::cout << "Hello from main!n";
  return 0;
}

需要注意的点:

  • 编译数据库: LibTooling依赖编译数据库,所以一定要确保compile_commands.json文件是正确的。
  • AST遍历: AST的结构比较复杂,需要仔细研究Clang的文档,才能正确地遍历AST。
  • 代码修改: 使用Rewriter修改代码时,需要注意SourceLocation的正确性,否则可能会导致代码错乱。
  • 头文件包含: 根据你的需求,可能需要包含更多的Clang头文件。

更高级的应用

LibTooling的用途远不止这些,你可以用它来做更多的事情:

  • 自动生成代码: 根据现有代码生成新的类、函数等。
  • 代码格式化: 自动调整代码的缩进、空格等,使其符合编码规范。
  • 代码安全检查: 检查代码中是否存在潜在的安全漏洞。
  • 自定义语言扩展: 给C++添加新的语法特性。

表格总结

组件 功能
ToolDriver LibTooling的入口,负责解析命令行参数,创建编译器实例。
FrontendAction 代码分析逻辑的载体,告诉LibTooling你要做什么。
ASTConsumer 实际访问AST的组件,你可以在这里遍历AST,分析代码,修改代码。
RecursiveASTVisitor 递归遍历AST的工具类,提供了方便的接口用于访问AST节点。
Rewriter 修改源代码的工具类,可以插入、删除、替换代码。
ASTContext 存储AST信息的上下文,包含了类型信息、声明信息等。
SourceManager 管理源代码的工具类,可以获取源代码的文本、位置等信息。
LangOptions 语言选项,例如C++标准、是否启用扩展等。

学习资源

总结

Clang LibTooling是一个强大的工具,可以让你在编译阶段对代码进行各种骚操作。虽然学习曲线比较陡峭,但是一旦掌握,就能极大地提高你的开发效率和代码质量。希望今天的分享能帮助你入门LibTooling,开启你的代码魔改之旅!

最后,别忘了多看文档,多写代码,实践才是检验真理的唯一标准! 加油!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注