2312llvm,09clang工具下

ClangQuery

LLVM3.5中引入ClangQuery工具,它可读入一个源文件,交互查询它所关联的ClangAST节点.是帮助查看并学习前端如何表达每行代码的很好工具.

然而,它的主要目标是,你不但可查看程序的AST,而且可测试AST匹配器.

编写重构工具时,你会对使用,包含匹配感兴趣的ClangAST片段的断定(predicate)AST匹配器库感兴趣.
ClangQuery工具可在开发时帮助你,因为它让你可查看哪个AST节点匹配具体AST匹配器.

你可在ASTMatchers.h中,查看可用的AST匹配器的列表,但是也可根据驼峰大小写的名字,猜测感兴趣的AST节点的类.
如,functionDecl会匹配所有表示函数声明FunctionDecl节点.
试验了哪个匹配器确切返回感兴趣的节点后,可在重构工具中用它们实现自动转换方法.

如下查看AST,会对上次PPTrace中用到的"helloworld"代码运行clang-query.ClangQuery期望有编译命令数据库.
如果查看没有编译命令数据库的文件,就在双短划线后给出编译命令,或空着它,如果不需要特殊编译器选项,如下行所示:

$ clang-query hello.c --

发出该命令后,clang-query会显示一个等待输入命令的交互提示.可输入match命令和任意AST匹配器的名字.
如,下面命令中,让clang-query显示所有CallExpr节点:

clang-query> match callExpr()
Match #1:
hello.c:12:5: note: "root" node binds here
write(1, "Hello, ", 7);
^~~~~~~~~~~~~~~~~~~~~~
...

该工具会高亮程序中与关联CallExprAST节点对应的第一个令牌确切位置.ClangQuery可接受的命令列表如下:
1,help:打印命令列表.
2,match<matchername>m<matchername>:该命令用请求的匹配器遍历AST.
3,set output<(diag|print|dump)>:该命令在成功匹配后,修改如何打印节点信息.第一个选项会打印一个Clang诊断消息,并高亮节点,这是默认选项.

第二个选项会简单地打印匹配到的对应源码的摘要,而最后选项会调用带复杂调试功能并显示所有子节点dump()类成员函数.

了解程序ClangAST的结构的一个重要方法,是修改dump输出,并匹配高级节点.试试:

lang-query> set output dump
clang-query> match functionDecl()

它会显示在C源码中,构成所有函数体语句和式的所有实例.另一方面,记住,用ClangCheck可更容易得到的该完整AST转存.

ClangQuery更适合造AST匹配器式并检查它们的结果.

ClangCheck

ClangCheck是个更易学习的只有几百行代码非常基础工具.然而,因为链接了LibTooling,它具有整个Clang解析能力.

ClangCheck可解析C/C++源码文件,转存ClangAST,及基础检查.还可应用Clang给出的"fixit"修改建议,利用为ClangModernizer建造的重写器设施.

如,假设想要打印program.cAST,就如下输入:

$ clang-check program.c -ast-dump --

注意,ClangCheckLibTooling读取源文件的方式,可用命令数据库文件,或在双短划线(--)后输入适当的参数.

ClangCheck是个小工具,编写自己工具时,可学习它.

去除c_str()调用

remove-cstr-calls工具是个简单的源到源转换(即重构)工具.工作时会识别冗余的调用std::string对象的c_str(),并重写代码来避免它.

有时会冗余调用,首先,用另一个string对象或c_str()的结果来建造新的string对象时,如,std::string(myString.c_str()).

这可简化为直接使用string拷贝构造器,如,str::string(myString).
其次,根据string对象来建造LLVM的具体的StringRefTwine类的实例时.

这时,最好用string对象自身,而不是其c_str()的结果,即用StringRef(myString),而不是StringRef(myString.c_str()).

可在单个C++文件里完整写出该工具,这是另一个优秀的易学习的演示如何使用LibTooling建造重构工具的示例.

编写自己的工具

Clang项目为用户提供了包括语法和语义分析的三个接口,以利用Clang的特性和它的解析能力.

首先,libclang是和Clang交互的主要方式,它提供了稳定的CAPI,允许外部项目嵌入它,并可高级访问整个框架.

该稳定接口保持旧版本兼容,避免因为发布新版的libclang而破坏你的软件.从其它语言使用libclang也是可能的,如,使用ClangPython绑定.
Apple的Xcode,如,它通过libclangClang交互.
其次,允许编译过程中添加自己的Clang插件,而不是由如Clang静态分析器等工具执行离线分析.

每次编译翻译单元都要执行它时,这是有用的.因此,要考虑期望执行该分析的时间,来决定是否适合频繁运行.

另一方面,与给编译器增加命令选项一样,整合分析构建系统,也很容易.

最后还可通过LibTooling利用Clang.以重构代码或检查语义为目标,它可轻松建造独立工具.

LibClang相比,LibTooling不与兼容妥协,可完全访问ClangAST结构.

问题:编写C++重构工具

假设创立新的叫IzzyC++C++IDE.
利用LibTooling制作简单而好用C++代码重构工具;它按C++成员函数,全名,和替换名接受参数.

任务是找到该成员函数定义,用替代名修改它,且相应修改所有对该函数的调用.

配置源码位置

第一步是决定在哪存放工具代码.在LLVM的源码目录中,将新建一个叫izzyrefactor的目录,在tools/clang/tools/extra中,保存项目的所有文件.

之后,扩展extra目录中的Makefile,以包含你的项目.简单,找到DIRS变量,并在其它Clang工具项目的旁边添加izzyrefactor名字.
或许还想编辑CMakeLists.txt文件,假如你使用CMake,添加新的一行:

add_subdirectory(izzyrefactor)

来到izzyrefactor目录,创建新的Makefile,以通知LLVM-build系统,要建造独立于其它二进制文件而存在的独立工具.如下:

CLANG_LEVEL := ../../..
TOOLNAME = izzyrefactor
TOOL_NO_EXPORTS = 1
include $(CLANG_LEVEL)/../../Makefile.config
LINK_COMPONENTS := $(TARGETS_TO_BUILD) asmparser bitreader support\
                   mc option
USEDLIBS = clangTooling.a clangFrontend.a clangSerialization.a \
           clangDriver.a clangRewriteFrontend.a clangRewriteCore.a \
           clangParse.a clangSema.a clangAnalysis.a clangAST.a \
           clangASTMatchers.a clangEdit.a clangLex.a clangBasic.a
include $(CLANG_LEVEL)/Makefile

这是指定所有需要你的代码链接到一起的库的重要文件,这样才能建造该工具.
可选地,如果不想运行make install时,和其它LLVM工具一样安装新工具,在设置TOOL_NO_EXPORTS这行之后,可添加一行NO_INSTALL=1.

设置TOOL_NO_EXPORTS=1,因为工具不会使用插件,因此,不需要导出符号,减小了最终程序动态符号表的大小,也减少了动态链接加载程序的时间.

注意包含了定义了期望的所有规则Clang总的Makefile编译该项目.

如果使用CMake而不是自动工具配置脚本,用如下内容,创建新CMakeLists.txt文件:

add_clang_executable(izzyrefactor
  IzzyRefactor.cpp
  )
target_link_libraries(izzyrefactor
     clangEdit clangTooling clangBasic clangAST clangASTMatchers)

此外,如果不想在Clang源码树中构建该工具,你也可按独立工具构建它.

注意,前面Makefile中用的库,在USEDLIBS变量中,及在CLANGLIBS变量中用的库.

它们引用了相同的库,除了USEDLIBS有包含LibToolingclangTooling.因此,在Makefile中,在-lclang这行之后,添加一行-lclangTooling,就大功告成了.

剖析工具样板代码

所有代码在IzzyRefactor.cpp中.新建该文件并开始添加初始样板代码,如下:

int main(int argc, char **argv) {
    
    
    cl::ParseCommandLineOptions(argc, argv);
    string ErrorMessage;
    OwningPtr<CompilationDatabase> Compilations (CompilationDatabase::loadFromDirectory(BuildPath, ErrorMessage));
    if (!Compilations)
        report_fatal_error(ErrorMessage);
    //...
}

主要代码从llvm::cl名字空间的解析argv中的每个选项的ParseCommandLineOptions函数开始.

注意,基于LibTooling的工具,典型会使用为所有重构工具共享的CommonOptionsParser对象来轻松解析通用选项.这里

本例中,用低级的ParseCommandLineOptions()函数来确切说明要解析的参数.
所有的LLVM工具都会使用cl名字空间提供的功能,在命令行中,用工具识别定义的参数,实在是很简单.为此,声明新的opt模板类型和list全局变量:

cl::opt<string> BuildPath(
  cl::Positional,
  cl::desc("<build-path>"));
cl::list<string> SourcePaths(
  cl::Positional,
  cl::desc("<source0> [... <sourceN>]"),
  cl::OneOrMore);
cl::opt<string> OriginalMethodName("method",
  cl::desc("Method name to replace"),
  cl::ValueRequired);
cl::opt<string> ClassName("class",
  cl::desc("Name of the class that has this method"),
  cl::ValueRequired);
cl::opt<string> NewMethodName("newname",
  cl::desc("New method name"),
  cl::ValueRequired);

定义main函数前声明这五个全局变量.根据期望按参数读取的数据,指定了opt类型.如,如果需要读取数字,你会声明一个新的cl::opt<int>全局变量.

为了读取这些参数的数值,先要调用ParseCommandLineOptions.之后,只需要在期望关联数据类型的代码位置,引用与参数关联全局变量名.

如,如果代码像std::out<<NewMethodName期望串,NewMethodName会为该参数求值用户提供的串.

因为opt<>opt_storage<>父类模板,这工作,它定义了从所管理的数据类型(此处为string)继承的.

通过继承,opt<string>变量也是个.
如果opt<>类模板不能从被包装的数据类型(如,不存在的int类)继承,它会定义个类型转换符号,如为int数据类型,定义operator int().

代码中,效果是一样的;引用cl::opt<int>变量时,它会自动转换为一个整数,并返回它所存储的数字,即用户在命令行提供的数字.

还可为参数指定不同特征.示例中,通过指定cl::Positional使用了位置参数,即用户不会显示按名字指定参数,而是会根据在命令行中的相对位置推导出来.

还给opt构造器传递了一个desc对象,它定义了一段,用户在命令行中输入-help参数时,展示给用户的描述信息.

还有个使用cl::list类型的参数,不同于opt,它允许传递多个参数,这样可处理一堆源文件.要求包含下面头文件:

#include "llvm/Support/CommandLine.h"

注意,作为LLVM编码标准的一部分,include语句要,先包含本地头文件,随后包含ClangLLVMAPI头文件.

两个头文件属于相同分类时,按字母顺序排序.写独立工具,则自动为你整理头文件顺序.

最后三个全局变量设置用本重构工具的要求选项.第一个是-method名字的参数.第一个串参数指定没有短线参数名字,而cl::RequiredValues会通知命令行解析器,该值运行该程序所必需的.

该参数会给出方法名字,然后工具会去找该方法,然后用-newname给出的名字更改它的名字.-class参数给出有此方法类名.

下一段代码管理新的CompilationDatabase对象.首先,要包含定义OwningPtr类的头文件.

#include "llvm/ADT/OwningPtr.h"

注意Clang版本,从Clang/LLVM版本3.5开始,人们弃用了OwningPtr<>模板,而是转向C++标准的std::unique_ptr<>模板.

其次,要包含第一次用到的正式属于LibToolingCompilationDatabase类的头文件:

#include "clang/Tooling/CompilationDatabase.h"

该类负责管理编译数据库.
为了初化该对象,用到叫loadFromDirectory的工厂方法,它会从指定的构建目录加载编译数据库文件.

这就是按输入工具参数,声明构建路径的目的;用户要指定源文件及编译数据库文件的路径.

注意,给该工厂成员函数输入两个参数:代表命令行的cl::opt对象的BuildPath对象,及最近声明的ErrorMessage串.

假如引擎加载编译数据库失败了,会用一个消息填充ErrorMessage串,即工厂成员函数没有返回CompilationDatabase对象,这时会马上显示该消息.

llvm::report_fatal_error()函数会触发已配置的任意LLVM错误处理例程,并以1错误码退出工具.它要求包含下面头文件:

#include "llvm/Support/ErrorHandling.h"

示例中,缩写了很多类全名,因此还要在全局域中添加若干个using声明,但是只要你喜欢,也可用全名:

using namespace clang;
using namespace std;
using namespace llvm;
using clang::tooling::RefactoringTool;
using clang::tooling::Replacement;
using clang::tooling::CompilationDatabase;
using clang::tooling::newFrontendActionFactory;

使用AST匹配器

对编写基于Clang的代码重构工具,AST匹配器非常重要.

AST匹配器库让用户可轻松匹配符合特定条件的ClangAST的子树,如,表示所命名为calloc,且有两个参数的函数调用的AST节点.
查找指定的ClangAST节点并修改它们,这是每个代码重构工具共同的基本任务,利用该库,极大地方便了编写此类工具.

为了帮助找到正确的匹配器,依靠ClangQueryAST匹配器文档,文档在此.

先为工具编写叫wildlifesim.cpp的测试案例.这是一个复杂的可沿着直线任意方向遍历的一维动物模拟器:

class Animal {
    
    
  int position;
public:
  Animal(int pos) : position(pos) {
    
    }
  //返回新位置
  int walk(int quantity) {
    
    
    return position += quantity;
  }
};
class Cat : public Animal {
    
    
public:
  Cat(int pos) : Animal(pos) {
    
    }
  void meow() {
    
    }
  void destroySofa() {
    
    }
  bool wildMood() {
    
    return true;}
};
int main() {
    
    
  Cat c(50); c.meow();
  if (c.wildMood())
    c.destroySofa();
  c.walk(2);
  return 0;
}

要求工具可将成员函数比如walk重命名为run.运行ClangQuery,研究此例中的AST.这里用recordDecl匹配器,转存所有表示C结构C++类的RecordDeclAST节点的内容:

$ clang-query wildanimal-sim.cpp --
clang-query> set output dump
clang-query> match recordDecl()
(...)
|-CXXMethodDecl 0x(...) <line:6:3, line 8:3> line 6:7 walk 'int (int)'
(...)

在表示Animal类的RecordDecl对象的内部,观察到按CXXMethodDeclAST节点表示walk.查看AST匹配器文档,发现它由methodDeclAST匹配器匹配.

组合匹配器

AST匹配器的强大在于可组合性.如果只想要声明了叫walk成员函数的MethodDecl节点,可先匹配所有叫walk的命名声明,然后细化匹配同时是方法声明的节点.

hasName("input")匹配器返回所有叫"input"的命名声明.可在ClangQuery中,测试methodDeclhasName的组合:

clang-query> match methodDecl(hasName("walk"))

可见它只返回了一个walk的声明,而不是代码中存在的所有八个不同方法的声明.太好了!

尽管如此,发现仅修改Animal类的walk方法的定义是不够的,因为继承类可能重载它.不想重构工具重写了基类一个方法而不重写继承类中重载方法.

要找到所有定义了walk方法的类,即Animal类或其继承类.为了找到所有Animal类或其继承类,使用期望按NamedDecl参数的isSameOrDerivedFrom()匹配器.

通过带选择所有叫hasName()NamedDecl的匹配器的组合来提供该参数.因此,如下查询:

clang-query> match recordDecl(isSameOrDerivedFrom(hasName("Animal")))

还要选择那些重载walk方法的继承类.hasMethod()断定返回包含指定方法类声明.和第一个查询组合成如下查询:

clang-query> match recordDecl(hasMethod(methodDecl(hasName("walk"))))

为了用and符号语义(满足所有断定)连结两个断定,使用allOf()匹配器.它要求所有传入匹配器必须成立.
此时就可建造最终查询,以找到要重写所有声明:

clang-query> match recordDecl(allOf(hasMethod(methodDecl(hasName("walk"))), isSameOrDerivedFrom(hasName("Animal"))))

用该查询,可精确地找到Animal类或其继承类的所有walk方法的声明.

这样允许修改所有这些声明名字,但是还需要修改方法调用.为此,先观察CXXMemberCallExpr节点和它的memberCallExpr匹配器.试试:

clang-query> match memberCallExpr()

因为代码确实有四个方法调用,ClangQuery返回四个匹配:meow,wildMood,destroySofa,和walk.只对定位最后一个感兴趣.

已知道如何利用hasName()匹配器,来选择指定命名声明,但是如何把命名声明映射到成员函数调用式呢?
答案是用member()匹配器,来只选择命名且和一个方法名字链接声明,然后用callee()匹配器用调用式链接它.

完整式如下:

clang-query> match memberCallExpr(callee(memberExpr(member(hasName("walk")))))

然而,这里,盲目选择了所有调用walk()的方法.这里只想选择那些Animal类或其继承类walk调用.
memberCallExpr()匹配器按参数接受第二个匹配器.会使用thisPointerType()匹配器以仅选择调用对象指定类方法调用.

利用该规则,构建完整表达式:

clang-query>matchmemberCallExpr(callee(memberExpr(member(hasName("walk")))),thisPointerType(recordDecl(isSameOrDerivedFrom(hasName("Animal")))))

在代码中用AST匹配器断定

已决定了用哪些断定来抓正确的AST节点,现在可在工具代码中运用它们了.首先,为了使用AST匹配器,需要添加新的include指令:

#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"

还要添加新的using指令,使得更易引用这些类(在其它指令后面):

using namespace clang::ast_matchers;

第二个头文件,对实际查找机制是必须的.继续编写main函数,开始添加剩余代码:

RefactoringTool Tool(*Compilations, SourcePaths);
ast_matchers::MatchFinder Finder;
ChangeMemberDecl DeclCallback(&Tool.getReplacements());
ChangeMemberCall CallCallback(&Tool.getReplacements()));
Finder.addMatcher(recordDecl(allOf(hasMethod(id("methodDecl", methodDecl(hasName(OriginalMethodName)))), isSameOrDerivedFrom(hasName(ClassName)))), &DeclCallback);
Finder.addMatcher(memberCallExpr(callee(id("member", memberExpr(hasName(OriginalMethodName))))),
    thisPointerType(recordDecl(isSameOrDerivedFrom(hasName(ClassName)))), &CallCallback);
return Tool.runAndSave(newFrontendActionFactory(&Finder));

3.5版本中,要修改以上代码最后一行

return Tool.runAndSave(newFrontendActionFactory(&Finder).get());

这要就完成了main函数.

第一行代码实例化了个新的RefactoringTool对象.这是用到的LibTooling的需要额外包含语句的第二个类:

#include "clang/Tooling/Refactoring.h"

RefactoringTool类为你的工具实现了协调基本任务的所有逻辑:
打开源文件,解析它们,运行AST匹配器,匹配时调用回调函数,按工具要求修改源码等.

因此,在初化要求对象后,要调用RefactoringTool::runAndSave()结束main函数.

转移控制该类,让它执行所有基本任务.

接着,声明一个已包含其头文件MatchFinder对象.你用ClangQuery练习过的,该类负责匹配ClangAST.
MatchFinder要求用AST匹配器和回调函数配置,提供的AST匹配器匹配一个AST节点时,就会调用回调函数.
回调函数中,可修改源码.按MatchCallback的子类实现回调函数.

接着,声明回调函数对象,并用MatchFinder::addFinder()方法,用回调关联具体的AST匹配器.

声明两个单独回调函数,一个重写方法声明,另一个重写方法调用.这两个回调函数DeclCallbackCallCallback.

把前面设计的两个AST匹配器组合,但是用,用户按命令行参数提供的要重构的类名替换Animal类名.
同样,用也是命令行参数OriginalMethodName替换walk.

战略性地引入了新的叫id()匹配器,它不修改表达式所匹配的节点,只是用具体节点绑定一个名字.为让回调函数可产生替换内容,这很重要.

id()匹配器接受两个参数,第一个是用它取节点节名,第二个是用来抓命名AST匹配器.

第一个AST组合负责定位成员方法声明,命名确定方法的MethodDecl节点.第二个AST组合负责定位调用成员函数,命名与调用成员函数链接CXXMemberExpr节点.

编写回调函数

要定义匹配AST节点时,要执行的动作.为此,创建两个从MatchCallback继承的新类,每个匹配各一个类.

class ChangeMemberDecl : public ast_matchers::MatchFinder::MatchCallback {
    
    
    tooling::Replacements *Replace;
public:
    ChangeMemberDecl(tooling::Replacements *Replace) : Replace(Replace) {
    
    }
    virtual void run(const ast_matchers::MatchFinder::MatchResult &Result) {
    
    
        const CXXMethodDecl *method = Result.Nodes.getNodeAs<CXXMethodDecl>("methodDecl");
        Replace->insert(Replacement(*Result.SourceManager, CharSourceRange::getTokenRange(SourceRange(method->getLocation())), NewMethodName));
    }
};
    class ChangeMemberCall : public ast_matchers::MatchFinder::MatchCallback {
    
    
        tooling::Replacements *Replace;
public:
    ChangeMemberCall(tooling::Replacements *Replace) : Replace(Replace) {
    
    }
    virtual void run(const ast_matchers::MatchFinder::MatchResult &Result) {
    
    
        const MemberExpr *member = Result.Nodes.getNodeAs<MemberExpr>("member");
        Replace->insert(Replacement(*Result.SourceManager, CharSourceRange::getTokenRange(SourceRange(member->getMemberLoc())), NewMethodName));
    }
};

两个类都私下存储了,只是std::set<Replacement>typedefReplacements对象的引用.Replacement类存储的信息有,用什么文本,在哪个文件的哪些行要打补丁.

RafactoringTool类内部管理Replacement对象集合,因而在main函数中用RefactoringTool::getReplacements()方法取得该集合,并初化回调函数.

用存储它来以后使用的Replacements对象指针定义了个基本构造器.通过重载run()方法,来实现回调的动作,又一次,代码相当简单.

函数按参数接受一个MatchResult对象.对给定匹配,MatchResult类存储了,如id()匹配器请求的按名字绑定的所有节点.

MatchResult对象中可通过节点名公开访问的BoundNodes管理这些节点.因此,在run()函数中的第一个动作是调用特化的BoundNodes::getNodeAs<CXXMethodDecl>方法取感兴趣的节点.

结果,就得到了CXXMethodDeclAST节点的只读版本引用.
访问该节点后,为了决定如何给代码打补丁,需要一个,告诉在源文件中,关联令牌的确切行和列SourceLocation对象.

CXXMethodDecl从表示通用声明的Decl基类(a)继承.它(a)提供了返回想要的SourceLocation对象的Decl::getLocation()方法.

有了它,就可创建第一个Replacement对象,并把它插入工具建议的源码修改列表中.

用到的Replacement构造器需要三个参数:一个SourceManager对象的引用,一个CharSourceRange对象的引用,和包含在头两个参数指定位置要写的新文本串.

SourceManager类是个通用的管理加载内存的源码的Clang组件.CharSourceRange类包含有用的解析器,它分析令牌并推导出组成该令牌的源码区间(文件中的两个点),从而决定要从源码文件中删除的确切符,并为新文本空出位置.

该信息创建新的Replacement对象,并在由RefactoringTool管理的集合存储它,就完成任务了.RefactoringTool应用这些补丁,或去除冲突.

记得在匿名名字空间里,声明所有本地包装,这样避免,翻译单元导出本地符号.

测试你的新重构工具

野生模拟器代码示例,作为测试用例,来测试新创建的工具.现在应该运行make,然后等待LLVM编译并链接你的新工具.

生成工具后,试用一番.看看在命令行接口中,按cl::opt对象声明的参数:

$ izzyrefactor -help

为了使用该工具,还需要编译命令数据库.为了避免创建并运行一个CMake配置文件,手动创建一个.

命名为compile_commands.json,并有如下内容.把<FULLPATHTOFILE>标签替换为野生模拟器源码目录的完整路径:

[
{
    
    
"directory": "<FULLPATHTOFILE>",
"command": "/usr/bin/c++ -o wildlifesim.cpp.o -c <FULLPATHTOFILE>/ wildlifesim.cpp",
"file": "<FULLPATHTOFILE>/wildlifesim.cpp"
}
]

保存该编译命令数据库后,就可测试工具了:

$ izzyrefactor -class=Animal -method=walk -newname=run ./ wildfilesim.cpp

现在可检查野生模拟器源码,会看到该工具重命名了所有方法定义和调用.

更多

设置命令数据库指令.
C++模块
AST匹配器和LibTooling的教程.
ClangTidy用户手册
ClangFormat的用户手册.
llvm博客

猜你喜欢

转载自blog.csdn.net/fqbqrr/article/details/135187255