文章

冗余头文件检查(八):扩展 IWYU 以支持未自包含头文件检测

冗余头文件检查(八):扩展 IWYU 以支持未自包含头文件检测

前言

这是本系列文章的最后一篇,也是高潮部分。我们将基于前面分析的所有内容,实现一个实际的功能扩展:让 IWYU 能够检测和处理未自包含的头文件

回顾之前的问题:

  • IWYU 假设所有头文件都是自包含的
  • 当头文件不自包含时,IWYU 可能错误地建议移除必要的 #include
  • 这导致 IWYU 在某些真实工程项目中无法直接使用

我们的目标是:通过分析头文件的使用上下文,检测出未自包含的头文件,并给出更有针对性的建议。

设计思路

核心思想

未自包含头文件问题的本质是:存在隐式依赖关系

1
2
3
4
5
6
// print.h - 未自包含
void print(const std::string& s);

// main.cpp
#include <string>      // 使用者必须知道包含这个
#include "print.h"

IWYU 分析 main.cpp 时,它看到:

  • std::stringprint.h 的函数签名中被引用
  • print.h 本身不包含 <string>
  • 如果 <string> 也在 main.cpp 中未被其他代码直接使用
  • IWYU 会错误地建议移除 #include <string>

解决方案设计

我们采用”消费者-生产者”上下文分析的方法:

  1. 第一阶段:标记候选未自包含头文件
    • 对于某个头文件 H,检查是否有符号在其函数签名/类型定义中引用了来自头文件 S 的符号
    • 如果 H 没有包含 S,但 H 的”消费者”包含了 S,则标记 H 为候选
  2. 第二阶段:反向验证
    • 对于每个候选头文件 H,收集所有包合 H 的源文件列表
    • 检查这些源文件是否都包含了 S
    • 如果是,说明 S 是 H 的隐式依赖,H 未自包含
  3. 第三阶段:生成建议
    • 不建议移除用户为 H 加的 S
    • 建议修改 H,添加对 S 的包含

实现步骤

步骤1:修改 FileInfo 类,记录隐式依赖

文件include/iwyu_file_info.hlib/iwyu_file_info.cc

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
// include/iwyu_file_info.h

class FileInfo {
public:
    // ... 现有成员 ...

    // 新增:隐式依赖信息
    struct ImplicitDependency {
        string required_header;    // 该文件在语义上需要的头文件
        set<string> source_files; // 包含该文件的源文件,它们都包含了 required_header
        bool verified;            // 是否经过验证
    };

    // 添加一个隐式依赖
    void AddImplicitDependency(const string& header, const string& source_file);

    // 获取隐式依赖列表
    const vector<ImplicitDependency>& GetImplicitDependencies() const;

    // 判断是否存在验证过的隐式依赖
    bool HasVerifiedImplicitDependencies() const;

private:
    vector<ImplicitDependency> implicit_dependencies_;
};
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
// lib/iwyu_file_info.cc

void FileInfo::AddImplicitDependency(const string& header,
                                 const string& source_file) {
    // 查找是否已存在该头文件的隐式依赖记录
    for (auto& dep : implicit_dependencies_) {
        if (dep.required_header == header) {
            dep.source_files.insert(source_file);
            return;
        }
    }

    // 创建新的隐式依赖记录
    ImplicitDependency dep;
    dep.required_header = header;
    dep.source_files.insert(source_file);
    dep.verified = false;
    implicit_dependencies_.push_back(dep);
}

const vector<FileInfo::ImplicitDependency>&
FileInfo::GetImplicitDependencies() const {
    return implicit_dependencies_;
}

bool FileInfo::HasVerifiedImplicitDependencies() const {
    for (const auto& dep : implicit_dependencies_) {
        if (dep.verified) {
            return true;
        }
    }
    return false;
}

步骤2:扩展 AST 分析,检测未自包含的头文件

文件include/iwyu_ast_util.hlib/iwyu_ast_util.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// include/iwyu_ast_util.h

// 扫描头文件中的声明,查找对外部符号的引用
struct ExternalSymbolUsage {
    string symbol_name;       // 符号名称(如 std::string)
    string required_header;   // 推测需要的头文件
    SourceLocation location;  // 引用位置
};

vector<ExternalSymbolUsage> FindExternalSymbolUsagesInHeader(
    clang::ASTContext* ctx, const FileInfo* header_info);

// 分析头文件是否自包含
bool IsHeaderSelfContained(const vector<ExternalSymbolUsage>& usages,
                         const FileInfo* header_info);
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// lib/iwyu_ast_util.cc

vector<ExternalSymbolUsage> FindExternalSymbolUsagesInHeader(
    clang::ASTContext* ctx, const FileInfo* header_info) {

    vector<ExternalSymbolUsage> usages;

    // 使用 Clang AST Matcher 查找函数签名中的类型引用
    auto func_matcher = functionDecl(
        hasAnyParameter(hasType(
            qualType(hasDeclaration(namedDecl().bind("type_decl")))
        )).bind("func");

    class Handler : public MatchFinder::MatchCallback {
    public:
        Handler(clang::ASTContext* c, vector<ExternalSymbolUsage>* u)
            : ctx(c), usages(u) {}

        void run(const MatchFinder::MatchResult& result) override {
            if (const auto* type_decl = result.Nodes.getNodeAs<NamedDecl>("type_decl")) {
                string symbol = GetQualifiedNameAsString(type_decl);
                string header = DetermineRequiredHeaderForSymbol(symbol);

                ExternalSymbolUsage usage;
                usage.symbol_name = symbol;
                usage.required_header = header;
                // 获取位置信息...

                usages->push_back(usage);
            }
        }

    private:
        clang::ASTContext* ctx;
        vector<ExternalSymbolUsage>* usages;
    };

    Handler handler(ctx, &usages);
    MatchFinder finder;
    finder.addMatcher(func_matcher, &handler);
    finder.matchAST(*ctx);

    return usages;
}

bool IsHeaderSelfContained(const vector<ExternalSymbolUsage>& usages,
                         const FileInfo* header_info) {
    // 检查每个外部符号使用是否有对应的 #include
    auto existing_includes = header_info->GetIncludes();
    set<string> included_headers;

    for (const auto& inc : existing_includes) {
        included_headers.insert(NormalizeHeader(inc.GetHeader()));
    }

    for (const auto& usage : usages) {
        if (!included_headers.count(usage.required_header)) {
            return false;  // 发现缺少的 #include
        }
    }

    return true;
}

步骤3:修改 IWYUDriver,执行两阶段分析

文件include/iwyu_driver.hlib/iwyu_driver.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// include/iwyu_driver.h

class IWYUDriver {
public:
    // ... 现有声明 ...

    // 新增:两阶段分析
    void TwoPhaseAnalysis();

private:
    // 第一阶段:标记候选未自包含头文件
    void Phase1_MarkPossibleNonSelfContainedHeaders();

    // 第二阶段:验证隐式依赖
    void Phase2_VerifyImplicitDependencies();

    // 记录头文件到使用它的源文件的映射
    map<string, vector<string>> header_to_source_files_;
};
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// lib/iwyu_driver.cc

void IWYUDriver::TwoPhaseAnalysis() {
    // 执行原始分析(标记使用的符号等)
    ProcessSingleFile(...);

    // 第一阶段:标记候选
    Phase1_MarkPossibleNonSelfContainedHeaders();

    // 第二阶段:验证
    Phase2_VerifyImplicitDependencies();
}

void IWYUDriver::Phase1_MarkPossibleNonSelfContainedHeaders() {
    // 遍历所有头文件
    for (auto& file_info_pair : file_infos_) {
        const string& filepath = file_info_pair.first;
        FileInfo* file_info = file_info_pair.second.get();

        if (!file_info->IsHeader()) continue;

        // 分析该头文件中的外部符号使用
        auto external_usages = FindExternalSymbolUsagesInHeader(ast_context_.get(), file_info);

        // 检查是否自包含
        if (!IsHeaderSelfContained(external_usages, file_info)) {
            // 标记为候选
            for (const auto& usage : external_usages) {
                file_info->AddImplicitDependency(usage.required_header, "");
            }
        }
    }
}

void IWYUDriver::Phase2_VerifyImplicitDependencies() {
    // 构建头文件 -> 源文件映射
    for (auto& file_info_pair : file_infos_) {
        const string& header_path = file_info_pair.first;
        if (!file_info_pair.second->IsHeader()) {
            // 这个是源文件,记录它包含的头文件
            FileInfo* source_info = file_info_pair.second.get();
            for (const auto& inc : source_info->GetIncludes()) {
                string included_header = ResolveIncludePath(inc.GetHeader());
                header_to_source_files_[included_header].push_back(header_path);
            }
        }
    }

    // 验证隐式依赖
    for (auto& file_info_pair : file_infos_) {
        const string& header_path = file_info_pair.first;
        FileInfo* header_info = file_info_pair.second.get();

        if (!header_info->IsHeader()) continue;

        auto& deps = header_info->GetImplicitDependencies();
        for (auto& dep : deps) {
            // 查找所有包含这个头文件的源文件
            if (header_to_source_files_.count(header_path)) {
                const auto& sources = header_to_source_files_[header_path];

                // 检查这些源文件是否都包含所需的头文件
                bool all_contain = true;
                for (const auto& source : sources) {
                    FileInfo* source_info = GetFileInfo(source);
                    if (!source_info->ContainsHeader(dep.required_header)) {
                        all_contain = false;
                        break;
                    }
                }

                // 验证通过
                if (all_contain) {
                    dep.verified = true;
                    for (const auto& source : sources) {
                        dep.source_files.insert(source);
                    }
                }
            }
        }
    }
}

步骤4:修改输出模块,显示关于未自包含头文件的建议

文件include/iwyu_output.hlib/iwyu_output.cc

1
2
3
4
5
6
7
8
9
// include/iwyu_output.h

class OutputHandler {
public:
    // ... 现有方法 ...

    // 新增:输出未自包含头文件的建议
    void PrintNonSelfContainedWarnings(const FileInfo* file_info);
};
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
// lib/iwyu_output.cc

void OutputHandler::PrintNonSelfContainedWarnings(const FileInfo* file_info) {
    if (!file_info->HasVerifiedImplicitDependencies()) {
        return;
    }

    const auto& deps = file_info->GetImplicitDependencies();

    cout << "WARNING: " << file_info->GetFilepath()
         << " appears to be non-self-contained!\n";

    for (const auto& dep : deps) {
        if (dep.verified) {
            cout << "  " << file_info->GetFilepath()
                 << " uses symbols from " << dep.required_header
                 << " but does not include it.\n";
            cout << "  Consumers of this header (" << dep.source_files.size() << " files) all "
                 << "explicitly include " << dep.required_header << ".\n";
            cout << "  Recommendation: consider adding #include "
                 << dep.required_header << " to "
                 << file_info->GetFilepath() << "\n\n";
        }
    }
}

步骤5:集成到主流程

文件lib/iwyu_driver.cc 中的 ProcessFile 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在生成建议之前,执行两阶段分析
void IWYUDriver::ProcessFile(const string& filename) {
    // ... 原有的单文件分析代码 ...

    // 执行两阶段分析,检测未自包含头文件
    TwoPhaseAnalysis();

    // 生成原始建议
    GenerateSuggestions(...);

    // 输出警告和建议
    OutputHandler output;
    for (const auto& file_info_pair : file_infos_) {
        const FileInfo* file_info = file_info_pair.second.get();

        // 输出原始建议
        output.PrintHeaderToAddSuggestions(file_info);
        output.PrintHeaderToRemoveSuggestions(file_info);

        // 输出未自包含头文件警告
        output.PrintNonSelfContainedWarnings(file_info);
    }
}

测试方法

测试用例 1:基本未自包含检测

1
2
3
4
5
6
7
8
9
10
11
12
13
// test_bad.h
#include <vector>  // 需要 <string> 但未包含
void process_strings(const std::vector<std::string>& items);

// test_main.cpp
#include <string>
#include "test_bad.h"

int main() {
    std::vector<std::string> items;
    process_strings(items);
    return 0;
}

预期输出:

1
2
3
4
WARNING: test_bad.h appears to be non-self-contained!
  test_bad.h uses symbols from <string> but does not include it.
  Consumers of this header (1 files) all explicitly include <string>.
  Recommendation: consider adding #include <string> to test_bad.h

测试用例 2:正确的自包含(不应报错)

1
2
3
4
// test_good.h
#include <vector>
#include <string>
void process_strings(const std::vector<std::string>& items);

预期输出:无警告。

局限与改进方向

当前实现的局限

  1. 误报风险:某些符号可能由多个头文件提供,我们选择的”所需头文件”可能不准确
  2. 性能开销:两阶段分析增加了额外的 AST 遍历和验证步骤
  3. 宏依赖:仍然受限,无法完全处理条件编译中的宏依赖

可能的改进

  1. 改进猜测量:结合项目编译数据库(compile_commands.json)获取更准确的头文件信息
  2. 缓存机制:缓存隐式依赖的分析结果,避免重复计算
  3. 机器学习:使用机器学习模型预测头文件的隐式依赖

验证扩展效果

在 Docker 环境中测试

1
2
3
4
5
6
7
8
9
10
# 启动 Docker 容器
docker run -it --rm -v /path/to/your/project:/workspace/project iwyu-dev:latest bash

# 重新编译修改后的 IWYU
cd /workspace/include-what-you-use/build-debug
ninja

# 运行测试
/workspace/include-what-you-use/build-debug/bin/include-what-you-use \
    /workspace/project/test_main.cpp -I/workspace/project

使用 VSCode 调试

在模拟 container 环境中,设置断点在:

  • IWYUDriver::Phase1_MarkPossibleNonSelfContainedHeaders
  • IWYUDriver::Phase2_VerifyImplicitDependencies
  • OutputHandler::PrintNonSelfContainedWarnings

观察两阶段分析的执行过程。

这个插件展示了静态分析工具如何通过细致的上下文分析来处理复杂的工程现实问题。虽然我们的解决方案可能不是完美的,但它提供了一种可行的改进思路,也为进一步的优化和探索奠定了基础。

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