冗余头文件检查(六):include-what-you-use 源码结构解析
冗余头文件检查(六):include-what-you-use 源码结构解析
前言
在了解了 IWYU 的原理、使用方法和局限性之后,现在是时候深入源码内部了。本文将详细分析 IWYU 的目录结构、关键模块、核心算法位置以及 include 处理路径。
这部分内容对于后续扩展 IWYU 功能至关重要,因为我们需要知道应该修改哪些文件、在哪些位置插入代码。
目录结构总览
IWYU 的源码目录结构如下(简化版):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
include-what-you-use/
├── include/ # 头文件
│ ├── iwyu_ast_util.h
│ ├── iwyu_cache.h
│ ├── iwyu_driver.h
│ ├── iwyu_file_info.h
│ ├── iwyu_getopt.h
│ ├── iwyu_include_picker.h
│ ├── iwyu_lexer.h
│ ├── iwyu_location_util.h
│ ├── iwyu_output.h
│ ├── iwyu_path_util.h
│ ├── iwyu_port.h
│ ├── iwyu_preprocessor.h
│ ├── iwyu_string_util.h
│ ├── iwyu_stl_util.h
│ └── iwyu_util.h
├── lib/ # 源文件实现
│ ├── iwyu_ast_util.cc
│ ├── iwyu_cache.cc
│ ├── iwyu_driver.cc
│ ├── iwyu_file_info.cc
│ ├── iwyu_getopt.cc
│ ├── iwyu_include_picker.cc
│ ├── iwyu_lexer.cc
│ ├── iwyu_location_util.cc
│ ├── iwyu_output.cc
│ ├── iwyu_path_util.cc
│ ├── iwyu_port.cc
│ ├── iwyu_preprocessor.cc
│ ├── iwyu_string_util.cc
│ ├── iwyu_stl_util.cc
│ └── iwyu_util.cc
├── tests/ # 测试文件
│ ├── ...
├── README.md
├── CMakeLists.txt
└── iwyu.cc # 主入口
核心入口分析
iwyu.cc - 主入口文件
这是 IWYU 的命令行入口点,主要功能包括:
- 解析命令行参数
- 配置 Clang Frontend
- 启动 IWYU 的分析流程
- 生成分析报告
关键代码片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// iwyu.cc
int main(int argc, char** argv) {
// 1. 解析命令行参数
if (!ParseArgv(argc, argv)) {
return 1;
}
// 2. 创建 Clang Frontend
clang::tooling::ClangTool Tool(...);
// 3. 创建 IWYU AST Consumer
factory.RegisterIWYUMatchers(...);
// 4. 运行分析
int result = Tool.run(&FrontendActionFactory);
// 5. 输出分析报告
OutputReport(...);
return result;
}
关键模块解析
1. iwyu_driver.cc - 驱动模块
这是 IWYU 的核心驱动模块,负责:
- 创建和管理 Clang AST Consumer
- 协调各个子模块的工作
- 处理单个文件的分析流程
主要类:IWYUDriver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class IWYUDriver {
public:
// 处理单个文件
void ProcessFile(const string& filename);
// 获取分析结果
const IncludePicker& GetIncludePicker() const;
private:
// 创建 AST Consumer
clang::ASTFrontendAction* CreateASTAction();
// 收集被使用的符号
void CollectUsedSymbols(clang::ASTContext* ctx);
// 构建依赖关系
void BuildDependencyGraph();
};
2. iwyu_ast_util.cc - AST 工具模块
这个模块提供了操作 Clang AST 的各种工具函数,是 IWYU 与 Clang AST 交互的主要接口。
主要功能:
| 函数 | 功能 |
|---|---|
GetIncludeLocInFile | 获取 #include 指令在文件中的位置 |
GetQualifiedNameAsString | 获取符号的完整限定名称 |
IsTemplateSpecialization | 判断是否为模板特化 |
GetDeclLoc | 获取声明的位置 |
GetDeclHeader | 获取声明所在的头文件 |
关键实现示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取符号的完整包含名称
string GetQualifiedNameAsString(const clang::Decl* decl) {
string name;
llvm::raw_string_ostream oss(name);
// 获取命名空间前缀
const clang::DeclContext* ctx = decl->getDeclContext();
while (ctx && !ctx->isTranslationUnit()) {
if (const auto* ns = dyn_cast<clang::NamespaceDecl>(ctx)) {
oss << ns->getName() << "::";
}
ctx = ctx->getParent();
}
// 添加符号本身的名字
oss << decl->getName();
return oss.str();
}
3. iwyu_include_picker.cc - 头文件选择模块
这个模块是 IWYU 的”大脑”,负责决定每个符号应该从哪个头文件引入。
核心类:IncludePicker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class IncludePicker {
public:
// 添加一个可用的头文件
void AddAvailableHeader(const string& header, const string& symbol);
// 为特定符号推荐头文件
string SuggestHeaderForSymbol(const string& symbol) const;
// 获取建议添加的头文件列表
vector<string> GetHeadersToAdd() const;
// 获取建议移除的头文件列表
vector<string> GetHeadersToRemove() const;
private:
// 符号到头文件的映射
map<string, vector<string>> symbol_to_headers_;
// 头文件到符号的映射
map<string, set<string>> header_to_symbols_;
};
头文件选择策略:
IncludePicker 使用以下策略选择最佳头文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
string IncludePicker::SelectBestHeader(const vector<string>& candidates) {
if (candidates.empty()) return "";
string best = candidates[0];
for (const auto& header : candidates) {
// 策略1: 优先选择系统头文件(<> 而非 "")
if (header[0] == '<' && best[0] == '"') {
best = header;
}
// 策略2: 优先选择头文件名更具体的
// 例如:优先选择 "specific_utils.h" 而非 "utils.h"
if (header.size() > best.size()) {
best = header;
}
// 策略3: 根据映射文件(mapping file)的配置
if (mapping_preferences_.count(header)) {
best = header;
break;
}
}
return best;
}
4. iwyu_file_info.cc - 文件信息模块
这个模块负责管理被分析文件的信息,包括:
- 文件中已有的 #include 列表
- 文件的路径处理
- 头文件 vs 源文件的判断
核心类:FileInfo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FileInfo {
public:
explicit FileInfo(const string& filepath);
// 是否为头文件(.h, .hpp 等)
bool IsHeader() const;
// 获取或添加 #include 指令
void AddInclude(const IncludeDirective& include);
const vector<IncludeDirective>& GetIncludes() const;
// 获取文件路径(包含相对路径处理)
string GetFilepath() const;
private:
string filepath_;
bool is_header_;
vector<IncludeDirective> includes_;
};
5. iwyu_preprocessor.cc - 预处理器模块
这个模块处理 C++ 预处理器相关的逻辑:
- 条件编译(
#ifdef,#if) - 宏定义
- 文件包含路径解析
关键功能:
1
2
3
4
5
6
7
// 判断 #include 是否在条件编译块中被跳过
bool IsIncludeInActiveBlock(const clang::Preprocessor& PP,
const clang::SourceLocation& loc);
// 解析 include 路径
string ResolveIncludePath(const string& header,
const vector<string>& include_paths);
6. iwyu_output.cc - 输出模块
这个模块负责生成 IWYU 的分析报告,包括:
- 人类可读的建议文本
- 机器可读的 fix_includes.py 格式
输出格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class OutputHandler {
public:
// 输出应该添加的头文件
void PrintHeaderToAdd(const string& filename, const string& header);
// 输出应该移除的头文件
void PrintHeaderToRemove(const string& filename,
const string& header,
int line_number);
// 输出完整的 include 列表
void PrintFullIncludeList(const string& filename,
const vector<string>& includes);
// 输出机器可读格式
void PrintMachineReadableFormat(const FileInfo& info);
};
核心算法位置
算法1:符号收集
位置:iwyu_driver.cc 中的 MarkUsedSymbols 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 遍历 AST,标记所有被使用的符号
void IWYUDriver::MarkUsedSymbols(clang::ASTContext* ctx) {
// 使用 Clang 的 AST Matcher 查找所有 DeclRefExpr
auto matcher = declRefExpr().bind("refs");
class RefHandler : public MatchFinder::MatchCallback {
public:
void run(const MatchFinder::MatchResult& result) override {
const auto* expr = result.Nodes.getNodeAs<DeclRefExpr>("refs");
string symbol = GetQualifiedNameAsString(expr->getDecl());
used_symbols_.insert(symbol);
}
};
RefHandler handler;
MatchFinder finder;
finder.addMatcher(matcher, &handler);
finder.matchAST(*ctx);
}
算法2:依赖推断
位置:iwyu_include_picker.cc 中的 DetermineDependencies 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void IncludePicker::DetermineDependencies() {
// 为每个使用过的符号找到对应的头文件
for (const auto& symbol : used_symbols_) {
// 查找符号声明所在的头文件
string header = FindHeaderForSymbol(symbol);
if (!header.empty()) {
symbol_to_header_[symbol] = header;
// 记录需要添加的头文件
headers_to_add_.insert(header);
} else {
// 未找到匹配的头文件,记录警告
missing_symbols_.insert(symbol);
}
}
// 查找未使用的头文件
for (const auto& include : existing_includes_) {
string header = include.GetHeader();
// 检查这个头文件是否有被使用的符号
bool has_used_symbol = false;
for (const auto& symbol : header_to_symbols_[header]) {
if (used_symbols_.count(symbol)) {
has_used_symbol = true;
break;
}
}
if (!has_used_symbol) {
headers_to_remove_.insert(header);
}
}
}
算法3:循环依赖检测
位置:iwyu_file_info.cc 中的 DetectCircularIncludes 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 深度优先搜索检测循环依赖
bool DetectCircularIncludes(const string& start_file,
const unordered_set<string>& visited,
const unordered_map<string, vector<string>>& graph,
vector<string>* cycle) {
if (visited.count(start_file)) {
// 找到循环
if (cycle) {
cycle->push_back(start_file);
}
return true;
}
unordered_set<string> new_visited = visited;
new_visited.insert(start_file);
auto it = graph.find(start_file);
if (it == graph.end()) {
return false;
}
for (const auto& dep : it->second) {
if (DetectCircularIncludes(dep, new_visited, graph, cycle)) {
if (cycle && cycle->empty()) {
cycle->push_back(start_file); // 闭合循环
}
return true;
}
}
return false;
}
Include 处理路径
从 IWYU 启动到生成建议的完整处理路径:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
用户输入
│
▼
[1] iwyu.cc::main()
│
▼
[2] iwyu_driver.cc::IWYUDriver::ProcessFile()
│
▼
[3] Clang Frontend 解析源文件,构建 AST
│
▼
[4] iwyu_ast_util.cc::MarkUsedSymbols()
│ 遍历 AST,收集所有被引用的符号
▼
[5] iwyu_include_picker.cc::DetermineDependencies()
│ 根据使用的符号确定需要的头文件
▼
[6] iwyu_file_info.cc::AnalyzeExistingIncludes()
│ 分析现有的 #include 列表
▼
[7] iwyu_include_picker.cc::GenerateSuggestions()
│ 对比需要的头文件和已有的头文件,生成建议
▼
[8] iwyu_output.cc::PrintReport()
│ 输出分析报告
▼
用户看到建议
数据结构关系图
IWYU 的核心数据结构关系如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────┐
│ IWYUDriver │
│ (驱动核心) │
└────────┬────────┘
│
├──> FileInfo (文件信息)
│ ├──> vector<IncludeDirective> includes_
│ └──> bool is_header_
│
├──> IncludePicker (头文件选择器)
│ ├──> map<string, vector<string>> symbol_to_headers_
│ ├──> map<string, set<string>> header_to_symbols_
│ ├──> set<string> headers_to_add_
│ └──> set<string> headers_to_remove_
│
└──> set<string> used_symbols_ (已使用符号)
修改点建议
当我们想要扩展 IWYU 功能时,需要关注的修改位置:
| 功能需求 | 主要修改文件 | 关键函数/类 |
|---|---|---|
| 支持非自包含头文件 | iwyu_driver.cc, iwyu_file_info.cc | ProcessFile, AnalyzeExistingIncludes |
| 识别隐式依赖 | iwyu_include_picker.cc | DetermineDependencies |
| 优化性能 | iwyu_driver.cc, iwyu_cache.cc | ProcessFile, Cache lookup |
| 改进输出格式 | iwyu_output.cc | PrintReport, PrintMachineReadableFormat |
小结
通过分析 IWYU 的源码结构,我们了解到:
- 模块化设计:IWYU 将功能划分为多个明确定义的模块,各司其职
- Clang 集成:通过 AST Consumer 和 Matcher 机制,深度集成 Clang Frontend
- 核心算法:符号收集、依赖推断、循环检测等算法的位置和实现方式
- 数据流路径:从输入到输出的完整处理流程
这些理解将为我们后续的功能扩展提供精确的导航。
本文由作者按照 CC BY 4.0 进行授权