The Clang AST
AST(Abstract Syntax Tree)抽象语法树
抽象语法树(Abstract Syntax Tree, AST)是源代码的抽象表示,广泛用于编译器和分析工具中。 AST将源代码的语法结构转换为树形结构,其中每个节点表示代码中的一个语法元素。与源代码的文本表示相比,AST更关心代码的结构和语义。
词法分析(Lexical Analysis):将源代码分解为一系列词法单元(Tokens),如关键词、变量名、操作符等。
语法分析(Syntax Analysis): 根据语言的语法规则,将Token序列组织成语法树(Syntax Tree),也就是初步的结构化表示。
**抽象语法树(Abstract Syntax Tree):**在语法分析的基础上构建AST,这是一种高层次、结构化的代码表示方式。
代码生成(Code Generation): 将AST转化为中间表示(IR,Intermediate Representation),通常为LLVM IR
1. 抽象性:AST不包含源代码的所有信息,例如具体的空格、注释等,但它保留了语言的结构信息。
2. 层次结构:AST的每个节点代表源代码中的一个构造,例如表达式、语句、变量声明等。树的每个层次对应着不同的语法结构。
3. 语法语义结合:AST不仅体现代码的语法结构,还能结合上下文信息捕获部分语义内容(如变量作用域、类型信息等)。
4. 可扩展性:AST可以被修改和扩展,用于实现代码重构、代码生成以及语言特性的新增。
- 简单实例
借助AST explorer,一个强大的在线工具,用于解析代码并展示其抽象语法树(AST)。它支持多种编程语言和解析器。
Python代码
def func(a, b):result = a + breturn result
获取对应的AST
AST 从顶层的 Program
节点开始,表示整个程序。其子节点为 FunctionDeclaration
,表示定义了一个函数 func
,包含两个参数 a
和 b
,函数体由一个 BlockStatement
表示。在函数体中,首先是一个 VariableDeclaration
节点,声明变量 result
并将 a + b
的结果赋值给它,该表达式由 BinaryExpression
节点表示,操作数为参数 a
和 b
。最后是一个 ReturnStatement
节点,返回变量 result
。
基于不同的工具可以获取到不同的 AST,获取到的 AST 的内容取决于工具的 LR 文法文件。例如 Clang 是 LLVM 提供的 C/C++/OC 的编译器前端,内部包含了成熟通用的文法文件,获取到的 AST 和编译过程中产生的 AST 相同。
AST 以树状的形式表现源代码的语法结构,非常有利于分析遍历,且包含了足够多的源代码信息,因此基于 AST 可以做很多语法分析相关的工作:
- 语法检查
- 自动修复编译错误
- 自动修改代码格式
- 代码高亮
- 语法折叠
- 代码结构分析
- …
基于 Clang 获取分析 AST
Clang 是一个开源的编译器前端,主要用于 C、C++、Objective-C 和 Objective-C++ 的编译。它是 LLVM 项目的一部分,目标是提供一个高效、可移植、易于扩展且用户友好的编译器框架。Clang 不仅提供了编译器功能,还为静态分析、源代码工具、重构工具等提供了基础。
LLVM的三段式设计:Front-end、Middle-end、Back-end
Clang 是 OC、C++、C 语言的 Front-end,主要负责预处理、词法分析、语法分析、语义分析等,生成 AST、并把 AST 转换为 IR,作为 Middle-end 的输入。
Clang 的 AST 分析工具在生成 AST 之前需要预处理阶段,而预处理依赖于 完整的编译参数(如头文件路径、宏定义、目标架构等)和 编译环境配置。
1、使用 Clang 命令行工具查看 AST
Clang 提供了命令行工具,可以快速查看源代码生成的抽象语法树(AST)。 这也是 Clang 最基础的用法。
命令格式:
clang -Xclang -ast-dump -fsyntax-only <source_file>
-Xclang -ast-dump
:打印抽象语法树。-fsyntax-only
:仅检查语法,不进行实际的编译。<source_file>
:输入的源代码文件。
ASTContext
ASTContext是 Clang 用于管理抽象语法树(AST)的核心类。它封装了一个翻译单元的所有 AST 相关信息。
它允许从 getTranslationUnitDecl
(TranslationUnitDecl
是 AST 的顶级节点,代表整个源文件)开始遍历整个翻译单元,或者对于已解析的翻译单元,访问 Clang 的标识符表(该表记录了源代码中所有的标识符及其对应的语法信息 )。
Clang AST节点
Clang 的 AST 节点是 Clang 抽象语法树的基础结构,每个节点表示源代码中的某种语法元素。
- AST 节点的层次结构:Clang 的 AST 节点不是基于单一的祖先类,而是根据节点的语法和语义意义划分为多个主要类别
- Decl(声明) :表示所有声明相关的语法元素,例如变量声明、函数声明、类声明等
- Stmt(语句) :表示所有的语句,例如条件语句、循环语句、表达式语句等
- Type(类型) :表示代码中的类型信息,例如
int
、float
、class
类型等
简单示例
example.c
int main() {printf("Hello, world!\n");return 0;
}
运行指令:clang -Xclang -ast-dump -fsyntax-only example.c
输出(部分):
TranslationUnitDecl 0x5608c0 <example.c:1:1, line:4:1>
|-FunctionDecl 0x560960 <line:1:1, line:4:1> line:1:5 main 'int ()'
| `-CompoundStmt 0x560b60 <line:2:1, line:4:1>
| |-CallExpr 0x560aa8 <line:2:5, line:2:28> 'int'
| | `-DeclRefExpr 0x560a90 <line:2:5> 'int (const char *)' Function 'printf'
| `-ReturnStmt 0x560b40 <line:3:5, line:3:12>
......
TranslationUnitDecl
:表示整个翻译单元(源文件)。FunctionDecl
:表示main
函数的定义。CompoundStmt
:表示复合语句(函数体)。CallExpr
:表示函数调用(printf
)。ReturnStmt
:表示返回语句。
优缺点分析
优点:简单易用
缺点:输出的信息难以阅读、无法直接操作AST、仅支持静态结构
2、使用编程接口查看分析 AST
Clang LibTooling 是 Clang 提供的一套强大的编程接口,用于构建自定义的编译器工具。这些工具可以解析源代码的抽象语法树(AST),并执行静态分析、代码重构、定制化的代码生成等任务。相比于通过命令行直接查看 AST 的方法,LibTooling 提供了更多的灵活性,允许开发者以编程方式操作 AST。
LibTooling 是 Clang 的 API,用于构建工具链程序。它可以直接访问 Clang 的编译器基础设施(如 AST、语义分析、诊断系统等),开发者可以通过它来实现以下功能:
- 遍历和分析 AST。
- 匹配特定的代码模式(使用 LibASTMatchers)。
- 对源代码进行静态分析和诊断。
- 修改和生成代码(代码重构工具)。
命令行方式存在固有的局限:(1)无法筛选或操作 AST;(2)难以自动化和扩展;(3)缺乏上下文信息,无法处理复杂的依赖关系或跨文件分析。
在 LibTooling 中,核心类包括:
ClangTool
:管理工具的执行。FrontendAction
:定义工具的行为。ASTConsumer
:定义对 AST 的处理逻辑。RecursiveASTVisitor
:用于便利遍历 AST 节点。
简单示例
以下是一个使用 LibTooling 遍历 AST 并打印函数定义的代码示例:
#include "clang/AST/AST.h"
// Clang 的 AST 核心头文件,用于访问 AST 节点的接口
#include "clang/AST/RecursiveASTVisitor.h"
// 提供 RecursiveASTVisitor 类,用于遍历 AST 节点
#include "clang/Frontend/FrontendAction.h"
// 定义 FrontendAction 类,作为工具的入口点
#include "clang/Frontend/CompilerInstance.h"
// 提供 CompilerInstance 类,管理编译器的上下文
#include "clang/Tooling/Tooling.h" // Clang 工具开发的核心接口
#include "clang/Tooling/CommonOptionsParser.h" // 用于解析命令行参数和编译数据库
#include "clang/ASTMatchers/ASTMatchers.h" // 提供 ASTMatchers 库,用于匹配 AST 节点
#include "clang/ASTMatchers/ASTMatchFinder.h"
// 提供 ASTMatchFinder 类,用于查找 AST 节点
#include <iostream> // 标准 C++ 输入输出库using namespace clang; // 简化 Clang 命名空间的使用
using namespace clang::tooling; // 简化 Clang 工具库命名空间的使用
using namespace clang::ast_matchers; // 简化 ASTMatchers 库命名空间的使用// 自定义 AST 访问器,用于遍历 AST
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:// 构造函数,接受一个 ASTContext 指针,保存为成员变量explicit MyASTVisitor(ASTContext *Context) : Context(Context) {}// 钩子函数:访问函数声明节点 (FunctionDecl)bool VisitFunctionDecl(FunctionDecl *FD) {// 获取函数名称并打印到标准错误流llvm::errs() << "Function found: " << FD->getNameInfo().getName().getAsString() << "\n";return true; // 返回 true 表示继续遍历其他节点}private:ASTContext *Context; // 保存 AST 的上下文信息,供其他方法使用
};// 自定义 AST Consumer,用于处理 AST
class MyASTConsumer : public ASTConsumer {
public:// 构造函数,接受一个 ASTContext 指针,传递给自定义的 AST 访问器explicit MyASTConsumer(ASTContext *Context) : Visitor(Context) {}// 重写 HandleTranslationUnit 方法,当翻译单元解析完成后调用void HandleTranslationUnit(ASTContext &Context) override {// 调用访问器的 TraverseDecl 方法,遍历翻译单元的根节点 (TranslationUnitDecl)Visitor.TraverseDecl(Context.getTranslationUnitDecl());}private:MyASTVisitor Visitor; // 自定义的 AST 访问器,用于遍历和分析 AST
};// 自定义 FrontendAction,定义工具的入口点
class MyFrontendAction : public ASTFrontendAction {
public:// 重写 CreateASTConsumer 方法,返回一个自定义的 ASTConsumer 实例std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef file) override {// 创建并返回 MyASTConsumer 对象,传递 AST 上下文return std::make_unique<MyASTConsumer>(&CI.getASTContext());}
};// 主函数,工具的入口
int main(int argc, const char **argv) {// 使用 CommonOptionsParser 解析命令行参数和编译数据库// 第一个参数:命令行参数数量 (argc)// 第二个参数:命令行参数内容 (argv)// 第三个参数:命令行选项的分类(这里使用默认分类)CommonOptionsParser OptionsParser(argc, argv, llvm::cl::GeneralCategory);// 创建 ClangTool 对象,用于管理工具的运行// 参数 1:编译数据库,包含编译参数 (compile_commands.json)// 参数 2:源文件路径列表ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());// 运行工具,传递自定义的 FrontendAction 工厂return Tool.run(newFrontendActionFactory<MyFrontendAction>().get());
}
优缺点分析
优点:灵活性和可编程性、丰富的API支持
缺点:复杂性较高、依赖正确的头文件和编译参数