Clang单元测试的类重命名


文档摘要

我们将创建一个Clang工具来帮助我们为用于单元测试的类重命名方法。首先,描述一下 clang::Rewriter 类——这是用于代码修改的基本类。 clang::Rewriter 是Clang库中的一个类,为在一个编译单元内的源代码提供重写操作的支持。提供了在源代码的抽象语法树(AST)中插入、删除和替换代码的方法。开发人员可以使用 clang::Rewriter 进行复杂的代码修改,例如重构或生成新的代码结构。可以应用于代码生成和代码重构任务,对于各种代码转换目的都非常有用。 该类有几个文本插入的方法;例如,clang::Rewriter::InsertText 在指定的源位置插入文本,并且 clang::SourceLocation 用来指定缓冲区中的确切位置,具体参见第4.4.

我们将创建一个Clang工具来帮助我们为用于单元测试的类重命名方法。首先,描述一下
clang::Rewriter 类——这是用于代码修改的基本类。

clang::Rewriter
是Clang库中的一个类,为在一个编译单元内的源代码提供重写操作的支持。提供了在源代码的抽象语法树(AST)中插入、删除和替换代码的方法。开发人员可以使用
clang::Rewriter
进行复杂的代码修改,例如重构或生成新的代码结构。可以应用于代码生成和代码重构任务,对于各种代码转换目的都非常有用。

该类有几个文本插入的方法;例如,clang::Rewriter::InsertText
在指定的源位置插入文本,并且 clang::SourceLocation
用来指定缓冲区中的确切位置,具体参见第4.4.1节《源管理器与源位置》。除了文本插入外,还可以使用
clang::Rewriter::RemoveText 删除文本,或者使用
clang::Rewriter::ReplaceText
替换文本。后两种方法使用源范围(clang::SourceRange)来指定要删除或替换的文本的位置。

clang::Rewriter 使用 clang::SourceManager
访问需要修改的源代码,这在第4.4.1节《源管理器与源位置》中有详细说明。来看看
Rewriter 如何在实际项目中使用。

假设有一个用于测试的类。类名以 "Test" 开头(例如
TestClass),但该类的公共方法没有使用 ’test_’
前缀。例如,该类有一个名为 pos 的公共方法(TestClass::pos),而不是
test_pos(TestClass::test_pos())。我们想要创建一个工具,为这些类的方法添加这样的前缀。

class TestClass

public: TestClass(); void pos();

private: void private_pos();

;

原始代码

class TestClass

public: TestClass(); void test_pos();

private: void private_pos();

;

修改后的代码

图 7.1: 对 TestClass 的代码转换

因此,我们希望将方法 TestClass::pos(参见图 7.1)替换为
TestClass::test_pos 在类声明中。

如果有一个调用该方法的代码,应该进行以下替换:

TestClass test; test.pos();

原始代码

TestClass test; test.test_pos();

修改后的代码

图 7.2: 对 TestClass 方法调用的代码转换

该工具还应忽略所有已经应用了所需修改的公共方法,无论是手动还是自动完成的。换句话说,如果一个方法已经有了所需的
’test_’ 前缀,则工具就不应该对其进行修改。

我们将创建一个名为 methodrename
的Clang工具,执行所有必需的代码修改。这个工具将利用第3.4节《递归AST访问者》中讨论的递归AST访问者。最重要的部分是实现
Visitor 类。

我们的 Visitor 类应处理以下特定的 AST 节点:

clang::CXXRecordDecl:这涉及处理名称以 "Test" 开头的 C++
类定义。对于这类类,所有用户定义的公共方法都应加上 "test_" 前缀。

clang::CXXMemberCallExpr:此外,需要识别所有使用修改后方法的地方,并根据类定义中的方法重命名进行相应的更改。

对于 clang::CXXRecordDecl 节点的处理如下:

bool VisitCXXRecordDecl(clang::CXXRecordDecl *Class) if
(!Class->isClass()) return true; if
(!Class->isThisDeclarationADefinition()) return true; if
(!Class->getName().starts_with("Test")) return true; for (const
clang::CXXMethodDecl *Method : Class->methods()) clang::SourceLocation
StartLoc = Method->getLocation(); if (!processMethod(Method, StartLoc,
"Renamed method")) return false; return true;

图 7.3: CXXRecordDecl 访问者的实现

图 7.3 中的第 11 至 16 行代表了对所检查节点的要求。相应的类名应该以
"Test" 开头(参见图 7.3 中的第 15 至 16 行),这里使用了 llvm::StringRef
类的 starts_with() 方法。

验证这些条件后,继续检查找到的类中的方法。

验证过程实现在 Visitor::processMethod 方法中:

bool processMethod(const clang::CXXMethodDecl *Method,
clang::SourceLocation StartLoc, const char *LogMessage)

if (Method->getAccess() != clang::AS_public) return true; if
(llvm::isa<clang::CXXConstructorDecl>(Method)) return true; if
(!Method->getIdentifier() ||
Method->getName().starts_with("test_")) return true;

std::string OldMethodName = Method->getNameAsString(); std::string
NewMethodName = "test_" + OldMethodName; clang::SourceManager &SM =
Context.getSourceManager(); clang::tooling::Replacement Replace(SM,
StartLoc, OldMethodName.length(), NewMethodName);
Replaces.push_back(Replace); llvm::outs() << LogMessage << ": " <<
OldMethodName << " to " << NewMethodName << ""; return true;

图 7.4: processMethod 实现

图 7.4 中的第 46 至 51 行包含了所需条件的检查。在第 46 至 47
行,验证该方法是否为公共方法。第 48 至 49 行用于排除构造函数的处理,而第
50 至 51 行则用于排除已有所需前缀的方法。

主要的替换逻辑实现在第 53 至 58 行。第 56 至 57 行,创建了一个特殊的
clang::tooling::Replacement
对象,作为所需代码修改的包装器。该对象的参数如下:

  1. clang::SourceManager:第 55 行从 clang::ASTContext 获取源管理器。

  2. clang::SourceLocation:源位置指定了替换的起始位置。该位置作为
    processMethod 方法的第二个参数传递,如第 45 行所示。

  3. unsigned: 替换文本的长度。

  4. clang::StringRef: 替换文本,第 54 行创建。

Visitor类中存储替换操作,这是一个私有成员:

private: clang::ASTContext &Context;
std::vector<clang::tooling::Replacement> Replaces; There is a special
getter to access the object outside the Visitor class: const
std::vector<clang::tooling::Replacement> &getReplacements() return
Replaces;

第59-60行记录操作,使用LogMessage作为日志消息的前缀。不同的AST节点使用不同的日志消息;对于clang::CXXRecordDecl,使用"Renamed
method"(见图7.3,第19行)。

对于方法调用,日志消息将有所不同。相应的处理如下所示:

bool VisitCXXMemberCallExpr(clang::CXXMemberCallExpr *Call) if
(clang::CXXMethodDecl *Method = Call->getMethodDecl())
clang::CXXRecordDecl *Class = Method->getParent(); if
(!Class->getName().starts_with("Test")) return true;
clang::SourceLocation StartLoc = Call->getExprLoc(); return
processMethod(Method, StartLoc, "Renamed method call"); return true;

图7.5:CXXMemberCallExpr访问者实现

第27-29行验证持有测试方法的类名是否以’Test’前缀开头。在第30行获取替换的源位置。在第31行,调用processMethod函数来处理找到的方法,将"Renamed
method call"作为日志消息传递给调用。

Consumer类初始化Visitor,并在HandleTranslationUnit方法中开始AST遍历:

图7.6:Consumer类实现

初始化Visitor并在第9-10行开始遍历(见图7.6)。第13行创建Rewriter,并在第14-19行应用替换。最后,第22-24行将结果存储在原始文件中。

Visitor和Consumer类被包装在clangbook::methodrename命名空间中。Consumer实例在FrontendAction类中创建。这个类的实现与图3.8中详细描述的RecursiveVisitor和DeclVisitor相似,唯一的区别是在新工具中使用clangbook::methodrename命名空间。

我们的工具的主函数与图3.21中定义的递归访问者类似:

int main(int argc, const char **argv)
llvm::Expected<clang::tooling::CommonOptionsParser> OptionsParser =
clang::tooling::CommonOptionsParser::create(argc, argv, TestCategory);
if (!OptionsParser) llvm::errs() << OptionsParser.takeError(); return
1; clang::tooling::ClangTool Tool(OptionsParser->getCompilations(),
OptionsParser->getSourcePathList()); return
Tool.run(clang::tooling::newFrontendActionFactory<
clangbook::methodrename::FrontendAction>() .get());

图7.7:’methodrename’测试工具的主函数

只在第23行更改了自定义前端动作的命名空间名称。

构建配置如下:

图7.8:构建’methodrename’测试工具的配置

相比于图3.20中的代码,最显著的变化是在第23和24行,添加了两个新的库来支持代码修改:clangToolingCore和clangRewrite。其他变化包括工具的新名称(第2行)以及包含主函数的源文件(第14行)。

完成了代码,就该构建并运行我们的工具了。

程序可以使用第3.3节AST遍历中使用的相同命令序列进行编译,见图3.11:

export LLVM_HOME=<...>/llvm-project/install mkdir build cd build cmake
-G Ninja -DCMAKE_BUILD_TYPE=Debug ... ninja

图7.9:’methodrename’工具的配置和构建命令

可以对以下测试文件(TestClass.cpp)运行创建的工具:

class TestClass public: TestClass(); void pos(); ;

int main() TestClass test; test.pos(); return 0;

图7.10:原始的TestClass.cpp

可以按以下方式运行工具:

图7.11:在TestClass.cpp上运行methodrename Clang工具

方法TestClass::pos重命名为TestClass::test_pos,方法调用也有更新:

class TestClass public: TestClass(); void test_pos(); ;

int main() TestClass test; test.test_pos(); return 0;

图7.12:修改后的TestClass.cpp

提供的示例演示了Clang如何协助创建重构工具。创建的Clang工具使用递归访问者来设置所需的代码转换。另一个可能的选项是,使用第5章Clang-Tidy检查框架中研究过的Clang-Tidy。


发布者: 作者: 转发
评论区 (0)
U