You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于Clang开发废弃代码块宏缺失检测及自动转换工具的技术问询

基于Clang开发废弃代码块宏缺失检测及自动转换工具的技术问询

我之前帮团队做过类似的Clang静态检查工具,正好可以给你梳理下最落地的实现路径——直接用Clang-Tidy框架来做是最高效的,不用从零搭LibTooling的轮子,它本身就封装了AST遍历、诊断输出、自动修复这些核心能力,完全适配你的需求。

下面是具体的实现步骤和细节:

一、核心思路:用Clang-Tidy做自定义检查器

Clang-Tidy是Clang官方的静态分析和自动修复工具,支持自定义检查器。我们要做的就是写一个专属检查器,实现三个核心逻辑:

  1. 跟踪代码中#ifdef MYLIB_ENABLE_DEPRECATED_CODE的预处理块范围
  2. 在这些块内检查是否存在MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_PUSH()MYLIB_IMPL_DISABLE_DEPRECATED_WARNINGS_POP()
  3. 缺失时输出清晰诊断,甚至自动插入缺失的宏

二、具体实现步骤

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 --
    

三、避坑细节

  1. 嵌套#ifdef块处理:如果目标#ifdef块里还有其他#ifdef,要在状态变量里加个计数器,比如BlockDepth,进入目标块时+1,离开时-1,避免误判嵌套的块
  2. 空块处理:如果目标#ifdef块里没有任何代码,可以加个判断,跳过空块的检查,避免无意义的修复
  3. 宏的多种定义形式:如果你的宏是对象式宏(不带括号),要把匹配器换成objectLikeMacroExpansion
  4. 跨文件包含:如果#ifdef块跨头文件和源文件,Clang-Tidy会自动处理,因为它会遍历整个翻译单元的AST

四、调试技巧

  • clang-check -ast-dump your-source.cpp查看代码的AST结构,确认匹配器是否能命中目标宏
  • 用Clang-Tidy的-v参数查看加载自定义检查器的日志,排查加载失败的问题
  • 先在本地小范围测试,比如找一个缺失宏的测试文件,运行检查器看诊断和修复是否符合预期

这样做出来的工具,完全可以集成到你的CI流程里,在构建前自动检查并修复,再也不用等编译失败才发现问题了!

火山引擎 最新活动