文章

冗余头文件检查(六):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 的命令行入口点,主要功能包括:

  1. 解析命令行参数
  2. 配置 Clang Frontend
  3. 启动 IWYU 的分析流程
  4. 生成分析报告

关键代码片段:

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.ccProcessFile, AnalyzeExistingIncludes
识别隐式依赖iwyu_include_picker.ccDetermineDependencies
优化性能iwyu_driver.cc, iwyu_cache.ccProcessFile, Cache lookup
改进输出格式iwyu_output.ccPrintReport, PrintMachineReadableFormat

小结

通过分析 IWYU 的源码结构,我们了解到:

  1. 模块化设计:IWYU 将功能划分为多个明确定义的模块,各司其职
  2. Clang 集成:通过 AST Consumer 和 Matcher 机制,深度集成 Clang Frontend
  3. 核心算法:符号收集、依赖推断、循环检测等算法的位置和实现方式
  4. 数据流路径:从输入到输出的完整处理流程

这些理解将为我们后续的功能扩展提供精确的导航。

本文由作者按照 CC BY 4.0 进行授权