AST matchers 和 Clang refactoring tools

本文探讨Clang工具库中的AST匹配器和重构工具,介绍其与RecursiveASTVisitor的区别,展示如何使用`clang-query`测试AST匹配器,以及如何通过RefactoringTool实现代码重构。


本文为译文,点击 此处查看原文。

在过去的几年中,Clang tooling 受到了很多关注和开发重点。最后,给出了一个方便、准确、开源、支持良好的 C++ 代码程序化分析和重构框架;我觉得这很令人兴奋。

这种快速开发的一个重要结果是,新的 APIs 和 tools 不断涌现。例如,不久前,Clang tooling 开发人员发现,执行 AST 遍历的人员必须编写大量重复的代码才能找到感兴趣的 AST 节点,因此他们提出了一个很棒的新 API,称为 AST matchers,我将在这里讨论这个API。

1. Visitors vs. matchers

这里有一个激励人心的例子。假设我们正在寻找用于 if 比较的指针类型变量。为了更具体地说明这一点,假设我们正在寻找指针类型变量位于一个等式比较(==)左侧的情况。要在 RecursiveASTVisitor 中找到这样的节点,我们必须这样写:

bool VisitIfStmt(IfStmt *s) {
  if (const BinaryOperator *BinOP = llvm::dyn_cast<BinaryOperator>(s->getCond())) {
    if (BinOP->getOpcode() == BO_EQ) {
      const Expr *LHS = BinOP->getLHS();
      if (const ImplicitCastExpr *Cast = llvm::dyn_cast<ImplicitCastExpr>(LHS)) {
        LHS = Cast->getSubExpr();
      }

      if (const DeclRefExpr *DeclRef = llvm::dyn_cast<DeclRefExpr>(LHS)) {
        if (const VarDecl *Var = llvm::dyn_cast<VarDecl>(DeclRef->getDecl())) {
          if (Var->getType()->isPointerType()) {
            Var->dump();  // YAY found it!!
          }
        }
      }
    }
  }
  return true;
}

这是相当多的代码,但是如果您已经使用 Clang ASTs 一段时间,就不会有什么异常。也许可以将其简化为更短的形式,但主要的问题是,要编写这个函数,必须遍历大量文档和头文件,以确定要调用哪些方法以及它们返回什么类型的对象。

下面是等效的AST matcher

Finder.addMatcher(
    ifStmt(hasCondition(binaryOperator(
        hasOperatorName("=="),
        hasLHS(ignoringParenImpCasts(declRefExpr(
            to(varDecl(hasType(pointsTo(AnyType))).bind("lhs")))))))),
    &HandlerForIf);

有一些差异,对吧?matcher 定义的声明性使其非常容易阅读和映射到实际问题。HandlerForIf 是一个 MatchCallback 对象,可以直接访问此 matcher 的绑定节点:

struct IfStmtHandler : public MatchFinder::MatchCallback {
  virtual void run(const MatchFinder::MatchResult &Result) {
    const VarDecl *lhs = Result.Nodes.getNodeAs<VarDecl>("lhs");
    lhs->dump();   // YAY found it!!
  }
};

在 Clang 官方网站上有很多关于 AST matchers 的文档。对于可以在 LLVM 树外构建的一个完整示例,我重新编写了上一篇文章中的 tooling 示例,现在它使用 AST matchers(所有这些都可以在 llvm-clang-samples 存储库中获得)。

2. 使用 clang-query 来测试 matchers 并研究 AST

Clang-land 中一个有趣的新工具是 clang-query。它是一个用于 AST matchers 的交互式评估器,既可以用来测试 matchers,也可以对 AST 进行一些编程探索。下面是我们要处理的输入文件示例:

int foo(int* p, int v) {
  if (p == 0) {
    return v + 1;
  } else {
    return v - 1;
  }
}

让我们启动clang-query,看看它能做什么:

$ clang-query /tmp/iflhsptr.c --
clang-query> set output diag
clang-query> match functionDecl()

Match #1:

/tmp/iflhsptr.c:1:1: note: "root" binds here
int foo(int* p, int v) {
^~~~~~~~~~~~~~~~~~~~~~~~
1 match.

这是一个基本的 smoke 测试,以查看它如何匹配FunctionDecl。第一个命令中设置的 output 模式也可以要求工具转储或打印 AST,但是对于我们的目的来说,诊断输出是方便的。

下面是我们如何匹配更深层次的节点并绑定它们:

clang-query> match ifStmt(hasCondition(binaryOperator(hasOperatorName("==")).bind("op")))

Match #1:

/tmp/iflhsptr.c:2:7: note: "op" binds here
  if (p == 0) {
      ^~~~~~
/tmp/iflhsptr.c:2:3: note: "root" binds here
  if (p == 0) {
  ^~~~~~~~~~~~~
1 match.

如果我们打算提供自己的绑定,可以关闭根绑定:

clang-query> set bind-root false

让我们看看多个匹配:

clang-query> match varDecl().bind("var")

Match #1:

/tmp/iflhsptr.c:1:9: note: "var" binds here
int foo(int* p, int v) {
        ^~~~~~

Match #2:

/tmp/iflhsptr.c:1:17: note: "var" binds here
int foo(int* p, int v) {
                ^~~~~
2 matches.

在这一点上,我将停止,因为长 matchers 不方便格式化放在一篇博客文章上,但我相信您已经有了想法。很明显,这个工具对于开发 matchers 非常有用。虽然它是新的,有一些粗糙的边,但已经非常有用。

3. 重构工具和替换

随着 libTooling 的使用越来越多,它的开发人员不断提出更高级别的抽象,从而帮助开发者用越来越少的精力编写新工具,这一点也不奇怪。上面给出的 AST matchers 框架就是一个例子。另一个是 RefactoringTool,它是 ClangTool 的一个子类,可以用很少的代码创建新的工具。我将很快展示一个例子,但首先是关于一个单词的替换。

到目前为止,我所演示的工具使用一个 Rewriter 来更改底层源代码,以响应在 AST 中发现的感兴趣的东西。这是一个很好的方法,但是对于大型项目来说,它存在可伸缩性的问题。想象一下,在一个包含许多源文件和头文件的大型项目上运行一个工具。有些重写可能需要在头文件中发生,但是,考虑到同一头头文件可能包含在多个翻译单元中,如何管理这些重写呢?有些编辑可能会重复甚至冲突,这是一个问题。

Replacements 是解决方案。源代码转换任务分为两个不同的步骤:

  1. 自定义工具遍历源代码库,找到要应用的重构模式,并在文件中生成序列化的 replacement。可以将 replacement 看作类似补丁文件的东西(如何修改一个源文件的精确方向),但是格式要友好一些。
  2. 然后,clang-apply-replacement 可以在访问所有 replacement 的情况下运行,执行所需的重复删除和冲突解决,并将更改实际应用于源代码。

这种方法还允许在巨大的代码库上对重构进行良好的并行化,尽管世界上没有多少项目和公司拥有足够大的源代码来解决这个问题。

现在我们回到一个例子上。我从上一篇文章中获得了这个简单的示例工具(只是发现感兴趣的 if 节点并在其中添加了一些注释),然后再次使用 RefactoringToolreplacements 重写了它。示例项目中提供了完整的代码,我在这里展示了大部分代码。

这是完整的主函数。为了便于 hacking ,它只是转储 replacementsstdout,而不是序列化或应用它们:

int main(int argc, const char **argv) {
  CommonOptionsParser op(argc, argv, ToolingSampleCategory);
  RefactoringTool Tool(op.getCompilations(), op.getSourcePathList());

  // 设置 AST matcher callbacks
  IfStmtHandler HandlerForIf(&Tool.getReplacements());

  MatchFinder Finder;
  Finder.addMatcher(ifStmt().bind("ifStmt"), &HandlerForIf);

  // 运行该工具并收集 replacements 列表。我们可以调用 runAndSave,
  // 它将破坏性地使用新内容重写该文件。但是,出于演示的目的,显式 replacements 更好。
  if (int Result = Tool.run(newFrontendActionFactory(&Finder).get())) {
    return Result;
  }

  llvm::outs() << "Replacements collected by the tool:\n";
  for (auto &r : Tool.getReplacements()) {
    llvm::outs() << r.toString() << "\n";
  }

  return 0;
}

IfStmtHandler 只是一个 MatchCallback,它在 if 语句上触发:

class IfStmtHandler : public MatchFinder::MatchCallback {
public:
  IfStmtHandler(Replacements *Replace) : Replace(Replace) {}

  virtual void run(const MatchFinder::MatchResult &Result) {
    // 匹配的'if'语句被绑定到'ifStmt'。
    if (const IfStmt *IfS =
          Result.Nodes.getNodeAs<clang::IfStmt>("ifStmt")) {
      const Stmt *Then = IfS->getThen();
      Replacement Rep(*(Result.SourceManager), Then->getLocStart(), 0,
                      "// the 'if' part\n");
      Replace->insert(Rep);

      if (const Stmt *Else = IfS->getElse()) {
        Replacement Rep(*(Result.SourceManager), Else->getLocStart(), 0,
                        "// the 'else' part\n");
        Replace->insert(Rep);
      }
    }
  }

private:
  Replacements *Replace;
};

注意这段代码只包含了很少的样板文件。这个工具只需要几行代码就可以设置好,我的大多数代码都处理手头的实际重构。这无疑使编写工具比以前更快更容易。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值