基于Clang开发废弃代码块宏缺失检测及自动转换工具的技术问询
基于Clang开发废弃代码块宏缺失检测及自动转换工具的技术问询
我之前帮团队做过类似的Clang静态检查工具,正好可以给你梳理下最落地的实现路径——直接用Clang-Tidy框架来做是最高效的,不用从零搭LibTooling的轮子,它本身就封装了AST遍历、诊断输出、自动修复这些核心能力,完全适配你的需求。
下面是具体的实现步骤和细节:
一、核心思路:用Clang-Tidy做自定义检查器
Clang-Tidy是Clang官方的静态分析和自动修复工具,支持自定义检查器。我们要做的就是写一个专属检查器,实现三个核心逻辑:
- 跟踪代码中
#ifdef MYLIB_ENABLE_DEPRECATED_CODE的预处理块范围 - 在这些块内检查是否存在
MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_PUSH()和MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_POP() - 缺失时输出清晰诊断,甚至自动插入缺失的宏
二、具体实现步骤
1. 搭建自定义检查器的基础框架
新建一个继承自ClangTidyCheck的类,比如DeprecatedCodeWarningMacroCheck,然后在类里维护几个状态变量:
CurrentDeprecatedBlockStart:记录当前正在跟踪的目标#ifdef块的起始位置HasPushMacro/HasPopMacro:标记当前块内是否已检测到对应的两个宏
然后重写三个关键方法:
registerPPCallbacks:处理预处理指令(#ifdef/#endif),跟踪目标块的开始和结束registerMatchers:用AST匹配器定位块内的两个自定义宏check:处理匹配到的宏,更新状态变量;在块结束时触发缺失检查和修复
2. 跟踪#ifdef块的范围
因为#ifdef属于预处理阶段的指令,我们需要通过PreprocessorCallbacks来监听这些指令的触发:
class DeprecatedCodePPCallbacks : public PPCallbacks { public: DeprecatedCodePPCallbacks(DeprecatedCodeWarningMacroCheck& Check, const SourceManager& SM) : Check(Check), SM(SM) {} // 监听#ifdef指令,匹配到目标宏时初始化状态 void Ifdef(SourceLocation Loc, const Token& MacroNameTok, const MacroDefinition& MD) override { StringRef MacroName = MacroNameTok.getIdentifierInfo()->getName(); if (MacroName == "MYLIB_ENABLE_DEPRECATED_CODE") { Check.CurrentDeprecatedBlockStart = Loc; Check.HasPushMacro = false; Check.HasPopMacro = false; } } // 监听#endif指令,结束目标块时检查宏是否缺失 void Endif(SourceLocation Loc, SourceLocation IfLoc) override { if (!Check.CurrentDeprecatedBlockStart.isValid()) return; // 确保当前#endif对应我们跟踪#ifdef块 if (SM.isInSameFile(Check.CurrentDeprecatedBlockStart, IfLoc)) { // 检查并输出诊断、生成修复提示 if (!Check.HasPushMacro) { // 在#ifdef下一行插入PUSH宏,自动对齐缩进 auto InsertLoc = SM.getLocForEndOfToken(Check.CurrentDeprecatedBlockStart); StringRef Indent = getCurrentIndent(SM, Check.CurrentDeprecatedBlockStart); Check.diag(Check.CurrentDeprecatedBlockStart, "废弃代码块开头缺失MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_PUSH()") << FixItHint::CreateInsertion(InsertLoc, "\n" + Indent + "MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_PUSH()"); } if (!Check.HasPopMacro) { // 在#endif上一行插入POP宏 auto InsertLoc = SM.getLocForStartOfLine(Loc); StringRef Indent = getCurrentIndent(SM, Loc); Check.diag(Loc, "废弃代码块结尾缺失MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_POP()") << FixItHint::CreateInsertion(InsertLoc, Indent + "MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_POP()\n"); } // 重置状态,准备跟踪下一个块 Check.CurrentDeprecatedBlockStart = SourceLocation(); } } private: // 辅助函数:获取当前行的缩进字符串 StringRef getCurrentIndent(const SourceManager& SM, SourceLocation Loc) { auto LineStart = SM.getLocForStartOfLine(Loc); return StringRef(SM.getCharacterData(LineStart), Loc.getRawEncoding() - LineStart.getRawEncoding()); } DeprecatedCodeWarningMacroCheck& Check; const SourceManager& SM; };
然后在检查器的registerPPCallbacks方法里注册这个回调:
void registerPPCallbacks(const SourceManager& SM, Preprocessor* PP, Preprocessor*) override { PP->addPPCallbacks(std::make_unique<DeprecatedCodePPCallbacks>(*this, SM)); }
3. 检测块内的自定义宏
用Clang的AST匹配器定位块内的两个宏,这里要注意:如果你的宏是函数式宏(带括号的),要用functionLikeMacroExpansion匹配,比匹配函数调用更准确:
void registerMatchers(MatchFinder* Finder) override { // 匹配PUSH宏的展开 Finder->addMatcher( functionLikeMacroExpansion(hasName("MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_PUSH")) .bind("push_macro"), this); // 匹配POP宏的展开 Finder->addMatcher( functionLikeMacroExpansion(hasName("MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_POP")) .bind("pop_macro"), this); } // 处理匹配到的宏,更新状态 void check(const MatchFinder::MatchResult& Result) override { if (!CurrentDeprecatedBlockStart.isValid()) return; if (Result.Nodes.getNodeAs<MacroExpansion>("push_macro")) { HasPushMacro = true; } if (Result.Nodes.getNodeAs<MacroExpansion>("pop_macro")) { HasPopMacro = true; } }
4. 编译和使用自定义检查器
- 把你的检查器代码编译成动态库(需要链接Clang和Clang-Tidy的开发库,具体编译脚本可以参考Clang-Tidy官方的自定义检查器示例)
- 运行检查时,用Clang-Tidy加载这个动态库:
# 只运行我们的自定义检查,输出诊断 clang-tidy -checks='-*,mylib-deprecated-code-missing-warning-macros' your-source.cpp -- # 加上-fix参数自动修复缺失的宏 clang-tidy -checks='-*,mylib-deprecated-code-missing-warning-macros' -fix your-source.cpp --
三、避坑细节
- 嵌套#ifdef块处理:如果目标#ifdef块里还有其他#ifdef,要在状态变量里加个计数器,比如
BlockDepth,进入目标块时+1,离开时-1,避免误判嵌套的块 - 空块处理:如果目标#ifdef块里没有任何代码,可以加个判断,跳过空块的检查,避免无意义的修复
- 宏的多种定义形式:如果你的宏是对象式宏(不带括号),要把匹配器换成
objectLikeMacroExpansion - 跨文件包含:如果#ifdef块跨头文件和源文件,Clang-Tidy会自动处理,因为它会遍历整个翻译单元的AST
四、调试技巧
- 用
clang-check -ast-dump your-source.cpp查看代码的AST结构,确认匹配器是否能命中目标宏 - 用Clang-Tidy的
-v参数查看加载自定义检查器的日志,排查加载失败的问题 - 先在本地小范围测试,比如找一个缺失宏的测试文件,运行检查器看诊断和修复是否符合预期
这样做出来的工具,完全可以集成到你的CI流程里,在构建前自动检查并修复,再也不用等编译失败才发现问题了!




