探索叮当工具第3部分:用叮当工具重写代码

上一个职位 在这个系列中,我们使用 叮当声 检查简单源代码文件的抽象语法树。使用 叮当声 ,我们可以原型一个AST匹配器,我们可以使用在 叮当声 检查以批量重构代码。

null

这次,我们将完成源代码的重写。

图片[1]-探索叮当工具第3部分:用叮当工具重写代码-yiteyi-C++库

让我们回到生成的MyFirstCheck.cpp 早期的 并更新 寄存器匹配器 方法。首先,我们可以使用 callExpr() 被叫方() 我们在上一篇文章中使用的匹配器:

void MyFirstCheckCheck::registerMatchers(MatchFinder *Finder) {
    
  auto nonAwesomeFunction = functionDecl(
    unless(matchesName("^::awesome_"))
    );

  Finder->addMatcher(
    nonAwesomeFunction.bind("addAwesomePrefix")
    , this);

  Finder->addMatcher(
    callExpr(callee(nonAwesomeFunction)).bind("addAwesomePrefix")
    , this);
}

因为Matter实际上是C++代码,所以我们可以将它们提取成变量,并将它们组合成多个其他匹配器,如这里所做的那样。 非wesomefunction .

在本例中,我缩小了声明匹配器的范围,使其仅匹配不以开头的函数声明 太棒了_ . 然后,该匹配器与活页夹一起使用一次 addawesome前缀 ,然后再次指定 被叫方() callExpr() ,再次将相关表达式绑定到名称 addawesome前缀 .

由于大规模重构通常主要涉及更改特定表达式,因此通常可以分别定义要匹配的声明的匹配器和引用这些声明的表达式。根据我的经验,声明的匹配器可能会变得复杂,例如,由于反射系统的限制而导致的排除,或者关于具有特定返回类型或参数类型的函数的更多细节。集中这些案例有助于保持重构代码的可维护性。

我所做的另一个更改是,我将绑定重命名为 addawesome前缀 . 这是值得注意的,因为它使用动词来描述应该如何处理匹配项。通过读取匹配器绑定,应该可以清楚地知道调用修复程序的结果是什么。然后可以将绑定名称视为匹配器和替换代码之间基于字符串的弱类型语言接口。然后我们就可以实施 MyFirstCheckCheck::检查 使用绑定。一级近似可能如下所示:

void MyFirstCheckCheck::check(const MatchFinder::MatchResult &Result) {
  if (const auto MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("addAwesomePrefix"))
  {
    diag(MatchedDecl->getLocation(), "function is insufficiently awesome")
      << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
  }

  if (const auto MatchedExpr = Result.Nodes.getNodeAs<CallExpr>("addAwesomePrefix"))
  {
    diag(MatchedExpr->getExprLoc(), "code is insufficiently awesome")
      << FixItHint::CreateInsertion(MatchedExpr->getExprLoc(), "awesome_");
  }
}

也许更好的实施方式可以减少诊断代码的重复:

void MyFirstCheckCheck::check(const MatchFinder::MatchResult &Result) {
  SourceLocation insertionLocation;
  if (const auto MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("addAwesomePrefix"))
  {
    insertionLocation = MatchedDecl->getLocation();
  } else if (const auto MatchedExpr = Result.Nodes.getNodeAs<CallExpr>("addAwesomePrefix"))
  {
    insertionLocation = MatchedExpr->getExprLoc();
  }
  diag(insertionLocation, "code is insufficiently awesome")
      << FixItHint::CreateInsertion(insertionLocation, "awesome_");
}

因为 函数DECL 以及 呼叫者 不要共享继承层次结构,我们需要为每个继承层次结构提供单独的强制转换条件。即使它们共享继承层次结构,我们也需要调用 获取位置 在一个案例中 获取表达式 在另一个地方。原因是Clang为每个AST节点记录了许多相关的位置。clangtidy检查的开发人员需要知道哪种位置访问器方法对于每种情况是合适的或必需的。进一步的改进是改变类型转换以接受相关类型的 函数DECL 呼叫者 名称DECL 出口 分别。

if (const auto MatchedDecl = Result.Nodes.getNodeAs<NamedDecl>("addAwesomePrefix"))
{
  insertionLocation = MatchedDecl->getLocation();
} else if (const auto MatchedExpr = Result.Nodes.getNodeAs<Expr>("addAwesomePrefix"))
{
  insertionLocation = MatchedExpr->getExprLoc();
}

这一变化强化了这样一种想法,即绑定节点的名称在匹配程序代码和重写程序代码之间形成弱类型接口。因为重写器代码现在期望 addawesome前缀 与基类型一起使用 名称DECL 出口 ,其他匹配器代码可以利用这一点。我们现在可以重新使用 addawesome前缀 绑定名称向字段声明或成员表达式添加前缀,例如,因为它们对应的Clang AST类也继承了 名称DECL :

auto nonAwesomeField = fieldDecl(unless(hasName("::awesome_")));
Finder->addMatcher(
  nonAwesomeField.bind("addAwesomePrefix")
  , this);

Finder->addMatcher(
  memberExpr(member(nonAwesomeField)).bind("addAwesomePrefix")
  , this);

请注意,此代码与我们为 函数DECL / 呼叫者 配对。利用绑定名称接口,我们可以继续将匹配器代码扩展到端口变量声明,而无需更改该接口的重写器端:

void MyFirstCheckCheck::registerMatchers(MatchFinder *Finder) {
  
  auto nonAwesome = namedDecl(
    unless(matchesName("::awesome_.*"))
    );

  auto nonAwesomeFunction = functionDecl(nonAwesome);
  // void foo(); 
  Finder->addMatcher(
    nonAwesomeFunction.bind("addAwesomePrefix")
    , this);

  // foo();
  Finder->addMatcher(
    callExpr(callee(nonAwesomeFunction)).bind("addAwesomePrefix")
    , this);

  auto nonAwesomeVar = varDecl(nonAwesome);
  // int foo;
  Finder->addMatcher(
    nonAwesomeVar.bind("addAwesomePrefix")
    , this);

  // foo = 7;
  Finder->addMatcher(
    declRefExpr(to(nonAwesomeVar)).bind("addAwesomePrefix")
    , this);

  auto nonAwesomeField = fieldDecl(nonAwesome);
  // int m_foo;
  Finder->addMatcher(
    nonAwesomeField.bind("addAwesomePrefix")
    , this);

  // m_foo = 42;
  Finder->addMatcher(
    memberExpr(member(nonAwesomeField)).bind("addAwesomePrefix")
    , this);
}

位置位置

让我们回到 检查 实施和检查。此方法负责实现由匹配器及其绑定节点描述的源代码重写。在本例中,我们在 源位置 由任何一方返回 获取位置() getExprLoc() 属于 名称DECL 出口 分别。clangast类有许多返回的方法 源位置 指源代码中与特定AST节点相关的各个位置。例如 呼叫者 源位置 访问器 getBeginLoc公司 , getEndLoc公司 获取表达式 . 目前很难发现源代码中的某个特定位置与某个特定位置的关系 源位置 存取器。

叮当声:瓦尔代克 表示Clang AST中的变量声明。 叮当声:帕姆瓦德克尔 继承 叮当声:瓦尔代克 表示参数声明。注意,在所有情况下, 结束 位置表示最后一个标记的开始,而不是结束。另请注意,在下面的第二个示例中,用于初始化变量的调用的源位置不是变量的一部分。必须遍历初始化表达式才能访问这些表达式。

图片[2]-探索叮当工具第3部分:用叮当工具重写代码-yiteyi-C++库

叮当声::函数decl 表示clangast中的函数声明。 叮当声:CXX方法 继承 叮当声::函数decl 表示方法声明。注意,返回类型的位置并不总是由 getBeginLoc公司 在C++中。

图片[3]-探索叮当工具第3部分:用叮当工具重写代码-yiteyi-C++库

铿锵::CallExpr 表示Clang AST中的函数调用。 CXXMemberCallExpr 继承 铿锵::CallExpr 表示方法调用。请注意,在调用自由函数(由 铿锵::CallExpr ),的 获取表达式 以及 getBeginLoc公司 会是一样的。总是选择语义正确的位置访问器,而不是显示正确位置的位置。

图片[4]-探索叮当工具第3部分:用叮当工具重写代码-yiteyi-C++库

知道AST类上的位置在所有情况下都指向令牌的开始是很重要的。在检查端点位置时,这一点最初可能会令人困惑。有时要到达所需的位置,就必须使用 getLocWithOffset() 前进或后退 源位置 . 推进到令牌的末尾可以通过 Lexer::getLocForEndOfToken .

函数调用的参数的源代码位置无法从 呼叫者 ,但必须通过参数本身的AST节点访问。

// Get the zeroth argument:
Expr* arg0 = someCallExpr->getArg(0);
SourceLocation arg0Loc = arg0->getExprLoc();

每个AST节点都有访问器 getBeginLoc公司 getEndLoc公司 . 表达式节点还有一个 获取表达式 ,并且声明节点有一个附加的 获取位置 存取器。更具体的子类具有与它们所代表的C++结构相关的位置的更具体的访问器。Clang中的源代码位置是全面的,但是随着需求的不断提高,访问它们会变得越来越复杂。如果读者感兴趣,未来的博客文章可能会更详细地探讨这个话题。

一旦我们获得了感兴趣的位置,我们就需要在这些位置插入、删除或替换源代码片段。

让我们返回MyFirstCheck.cpp:

diag(insertionLocation, "code is insufficiently awesome")
    << FixItHint::CreateInsertion(insertionLocation, "awesome_");

诊断 是上的方法 叮当作响 基类 . 其目的是向用户发布诊断和消息。可以仅使用源位置和消息调用它,从而在指定位置发出诊断:

diag(insertionLocation, "code is insufficiently awesome");

导致:

    testfile.cpp:19:5: warning: code is insufficiently awesome [misc-my-first-check]
    int addTwo(int num)
        ^

这个 诊断 方法返回 诊断生成器 我们可以使用 固定剂 .

这个 创建删除 方法创建 修复 用于删除一系列源代码。它的核心是 源范围 只是一双 源位置 s。如果我们想移除 太棒了_ 从包含前缀的函数中,我们可能希望编写如下内容:

void MyFirstCheckCheck::registerMatchers(MatchFinder *Finder) {
  
  Finder->addMatcher(
    functionDecl(
      matchesName("::awesome_.*")
      ).bind("removeAwesomePrefix")
    , this);
}

void MyFirstCheckCheck::check(const MatchFinder::MatchResult &Result) {

  if (const auto MatchedDecl = Result.Nodes.getNodeAs<NamedDecl>("removeAwesomePrefix"))
  {
      auto removalStartLocation = MatchedDecl->getLocation();
      auto removalEndLocation = removalStartLocation.getLocWithOffset(sizeof("awesome_") - 1);
      auto removalRange = SourceRange(removalStartLocation, removalEndLocation);

      diag(removalStartLocation, "code is too awesome")
          << FixItHint::CreateRemoval(removalRange);
  }
}

这段代码的matcher部分很好,但是当我们运行clangtidy时,我们发现删除操作应用于整个函数名,而不仅仅是 太棒了_ 前缀。问题是,Clang将移除范围的末端扩展到了末端指向的令牌的末端。这与AST节点 getEndLoc() 指向最后一个标记开始的方法。通常,目的是移除或替换整个令牌。

要在扩展到令牌中间的源代码中进行替换或删除,我们需要使用 CharSourceRange::getCharRange :

auto removalRange = CharSourceRange::getCharRange(removalStartLocation, removalEndLocation);

结论

关于写作的小系列到此结束 叮当声 检查。这个系列是一个测试兴趣的实验,如果读者有兴趣的话,还有更多的内容可以在以后的文章中报道。

进一步的主题可以涵盖现实世界中发生的主题,例如

  • 创建编译数据库
  • 创建一个独立的buildsystem来进行整洁的检查
  • 了解和探索震源位置
  • 完成更复杂的任务
  • 使用自定义匹配器扩展匹配器系统
  • 测试重构
  • 来自战壕的更多提示和技巧。

这将涵盖您需要知道的一切,以便在您的代码库上快速有效地创建和使用自定义重构工具。

你想看更多吗!请在下面的评论中告诉我们,或通过电子邮件直接联系作者 stkelly@microsoft.com ,或在Twitter上 @斯蒂维尔 .

我将展示更多的新的和未来的发展 叮当声 叮当声 代码::潜水 明天,包括上面列为未来主题的许多项目。如果你参加的是code::dive,一定要在你的日程表上安排!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享