1.抽象语法树AST
在实现语法检测之前,需要了解一个叫AST(抽象语法树)的东西
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,看个例子:
语法树是编译器对我们所书写的代码的“理解”,如上图中的x = a + b;语句,编译器会先将operator =作为节点,将语句拆分为左节点和右节点,随后继续分析其子节点,直到叶子节点为止。对于一个基本的运算表达式,我想我们都能很轻松的写出它的 AST,但我们在日常业务开发时所写的代码,可不都是简单而基础的表达式而已,诸如
- (void)viewDidLoad{
[self doSomething];
}
这样的代码,其 AST 是什么样的呢?好消息是 Clang 提供了对应的命令,让我们能够输出 Clang 对特定文件编译所输出的 AST,先创建一个简单的 CommandLine 示例工程,在main函数之后如下代码:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
@interface HelloAST:NSObject
@property (nonatomic,strong) NSArray *list;
@property (nonatomic,assign) NSInteger count;
@end
@implementation HelloAST
- (void)hello{
[self print:@"hello!"];
}
- (void)print:(NSString*)msg{
NSLog(@"%@",msg);
}
- (void)execute{
[self instanceMethod];
[self performSelector:@selector(selectorMethod) withObject:nil afterDelay:0];
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(selectorMethod) userInfo:nil repeats:NO];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onNotification:) name:NSUserDefaultsDidChangeNotification object:nil];
}
- (void)instanceMethod{}
- (void)selectorMethod{}
- (void)timerMethod{}
- (void)onNotification:(NSNotification*)notification{}
- (void)protocolMethod{}
@end
随后,在 Terminal 中进入 main.m 所在文件夹,执行如下指令:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
可看到一个清晰的树状结构,如类定义、方法定义、方法调用在 AST 中所对应的节点
比如我们定义的HelloAST类
ObjCInterfaceDecl:该类型节点为 objc 类定义(声明)。
ObjCPropertyDecl:属性定义,下面包括了
-ObjCMethodDecl 0x7fa99d272db0 <line:21:39> col:39 implicit - list 'NSArray *'
-ObjCMethodDecl 0x7fa99d272e38 <col:39> col:39 implicit - setList: 'void'
ObjCMethodDecl:该节点定义了一个 objc 方法(包含类、实例方法,包含普通方法和协议方法),这里为list属性的get/set方法
ObjCMessageExpr:说明该节点是一个标准的 objc 消息发送表达式([obj foo])
这些名称对应的都是 Clang 中定义的类,其中所包含的信息为我们的分析提供了可能。Clang 提供的各种类信息,可以在这里进行进一步查阅。
同时,我们也看到在函数定义的时候,ImplicitParamDecl节点声明了隐式参数self和_cmd,这正是函数体内self关键字的来源。
从以上可分析出,
在一个 oc 的程序中,几乎所有代码都可以被划分为两类:Decl(声明),Stmt(语句),上述各个ObjCXXXDecl类都是Decl的子类,ObjCXXXExpr也是Stmt的子类,根据RecursiveASTVisitor中声明的方法,我们可以看到对应的入口方法:bool VisitDecl (Decl *D)以及bool VisitStmt (Stmt *S)
2.语法检查
首先,先把MyPluginASTAction类的ParseArgs方法中的错误报告去掉,这样可以让编译工作能够继续进行下去。修改后如下:
//基于consumer的AST前端Action抽象基类
class MyPluginASTAction : public PluginASTAction
{
std::set<std::string> ParsedTemplates;
protected:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
llvm::StringRef) override
{
return llvm::make_unique<MobCodeConsumer>(CI, ParsedTemplates);
}
bool ParseArgs(const CompilerInstance &CI,
const std::vector<std::string> &args) override {
return true;
}
};
自定义ASTConsumer:
//用于客户读取AST的抽象基类
class MyPluginConsumer : public ASTConsumer
{
CompilerInstance &Instance;
std::set<std::string> ParsedTemplates;
public:
MyPluginConsumer(CompilerInstance &Instance,
std::set<std::string> ParsedTemplates)
: Instance(Instance), ParsedTemplates(ParsedTemplates) ,visitor(Instance) {}
bool HandleTopLevelDecl(DeclGroupRef DG) override
{
return true;
}
void HandleTranslationUnit(ASTContext& context) override
{
this->visitor.setASTContext(context);
this->visitor.TraverseDecl(context.getTranslationUnitDecl());
this->visitor.logResult();
}
private:
MyPluginVisitor visitor;
};
这里需要引用一个叫`RecursiveASTVisitor`的类模版,该类型主要作用是前序或后续地深度优先搜索整个AST,并访问每一个节点的基类,主要利用它来遍历一些需要处理的节点。同样,需要创建一个实现`RecursiveASTVisitor`的模版类。如:
//前序或后续地深度优先搜索整个AST,并访问每一个节点的基类)等基类
class MyPluginVisitor : public RecursiveASTVisitor<MyPluginVisitor>
{
private:
CompilerInstance &Instance;
ASTContext *Context;
public:
void setASTContext (ASTContext &context)
{
this -> Context = &context;
}
MyPluginVisitor (CompilerInstance &Instance):Instance(Instance)
{
}
}
这里要说明的是MyPluginConsumer::HandleTopLevelDecl方法表示每次分析到一个顶层定义时(Top level decl)就会回调到此方法。返回true表示处理该组定义,否则忽略该部分处理。而MyPluginConsumer::HandleTranslationUnit方法则为ASTConsumer的入口函数,当所有单元被解析成AST时会回调该方法。而方法中调用了visitor的TraverseDecl方法来对已解析完成AST节点进行遍历。在遍历过程中只要在Visitor类中捕获不同的声明和定义即可对代码进行语法检测。
3.例子:
a.类名检查
//前序或后续地深度优先搜索整个AST,并访问每一个节点的基类)等基类
class MyPluginVisitor : public RecursiveASTVisitor<MyPluginVisitor>
{
private:
CompilerInstance &Instance;
ASTContext *Context;
public:
void setASTContext (ASTContext &context)
{
this -> Context = &context;
}
MyPluginVisitor (CompilerInstance &Instance):Instance(Instance)
{
}
//类名检查
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
if (isUserSourceCode(declaration))
{
checkClassNameForLowercaseName(declaration);
checkClassNameForUnderscoreInName(declaration);
}
return true;
}
/**
判断是否为用户源码
@param decl 声明
@return true 为用户源码,false 非用户源码
*/
bool isUserSourceCode (Decl *decl)
{
std::string filename = Instance.getSourceManager().getFilename(decl->getSourceRange().getBegin()).str();
if (filename.empty())
return false;
//非XCode中的源码都认为是用户源码
if(filename.find("/Applications/Xcode.app/") == 0)
return false;
return true;
}
/**
检测类名是否存在小写开头
@param decl 类声明
*/
void checkClassNameForLowercaseName(ObjCInterfaceDecl *decl)
{
StringRef className = decl -> getName();
printf("类名:%s",className);
//类名称必须以大写字母开头
char c = className[0];
if (isLowercase(c))
{
//修正提示
std::string tempName = className;
tempName[0] = toUppercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//报告警告
DiagnosticsEngine &D = Instance.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "类名不能小写开头");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
/**
检测类名是否包含下划线
@param decl 类声明
*/
void checkClassNameForUnderscoreInName(ObjCInterfaceDecl *decl)
{
StringRef className = decl -> getName();
//类名不能包含下划线
size_t underscorePos = className.find('_');
if (underscorePos != StringRef::npos)
{
//修正提示
std::string tempName = className;
std::string::iterator end_pos = std::remove(tempName.begin(), tempName.end(), '_');
tempName.erase(end_pos, tempName.end());
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//报告错误
DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Class name with `_` forbidden");
SourceLocation location = decl->getLocation().getLocWithOffset(underscorePos);
diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
}
}
}
接着,cmd+B重新编译出MyPlugin.dylib,然后回到使用插件的工程(testPlugin),clear一下再buid,可见如下:
从上面代码可以看到,整个VisitObjCInterfaceDecl方法的处理过程是:先判断是否为自己项目的源码,然后再分别检查类名字是否小写开头和类名称存在下划线,如果有这些情况则报告警告并提供修改建议。
其中的isUserSourceCode方法判断比较重要,如果不实现该判断,则所有经过编译的代码文件中的类型都会被检测,包括系统库中的类型定义。该方法的基本处理思路是通过获取定义(Decl)所在的源码文件路径,通过比对路径来区分哪些是项目引入代码,哪些是系统代码。
checkClassNameForLowercaseName和checkClassNameForUnderscoreInName方法处理逻辑基本相同,通过decl -> getName()来获取一个指向类名称的StringRef对象,然后通过比对类名中的字符来实现相关的检测。
首先,需要从编译器实例(CompilerInstance)中取得诊断器(DiagnosticsEngine),由于是一个自定义诊断报告,因此诊断标识需要通过诊断器的getCustomDiagID方法取得,方法中需要传入报告类型和报告说明。然后调用诊断器的Report方法,把有问题的源码位置和诊断标识传进去。如:
DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Class name with `_` forbidden");
SourceLocation location = decl->getLocation().getLocWithOffset(underscorePos);
diagEngine.Report(location, diagID);
至于修正提示则是在诊断报告的基础上进行的,其通过FixItHint对象来包含一个修改提示行为,主要描述了某段源码需要修改成指定的内容。如:
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
b.查找无用方法
记录所有定义的方法以及所有被调用的方法,再取差集即可,有两个关键点:
方法所属的对象类型(Interface)
方法的选择子(Selector)
我们需要记录所有定义的方法以及所有被调用过的方法,并在扫描完整个 AST 之后对它们进行比较,我所采用的方式是以类名作为 key,以ObjCMethodDecl数组作为 value,构造一个 Map 来存储这些信息:
typedef std::vector<ObjCMethodDecl *> MethodVector;
typedef std::map<StringRef ,MethodVector> InterfaceMethodsMap;
typedef std::vector<Selector> SelectorVector;
在MyPluginVisitor定义成员变量
InterfaceMethodsMap definedMethodsMap;
InterfaceMethodsMap usedMethodsMap;
SelectorVector usedSelectors;
添加方法,访问所有的消息调用,如[obj sendMsg],并以类名作为 key记录下来
bool VisitObjCMessageExpr(ObjCMessageExpr *expr){
ObjCInterfaceDecl *interfaceDecl = expr -> getReceiverInterface();
StringRef clsName = interfaceDecl->getName();
MethodVector methodVec;
if(usedMethodsMap.find(clsName) != usedMethodsMap.end()) {
methodVec = usedMethodsMap.at(clsName);
}else{
methodVec = MethodVector();
usedMethodsMap.insert(std::make_pair(clsName, methodVec));
}
methodVec.push_back(expr->getMethodDecl());
InterfaceMethodsMap::iterator it = usedMethodsMap.find(clsName);
it->second = methodVec;
return true;
}
记录使用@selector()的方法
bool VisitObjCSelectorExpr(ObjCSelectorExpr *expr){
usedSelectors.push_back(expr->getSelector());
return true;
}
记录所有的方法定义:
//declaration
bool VisitObjCMethodDecl(ObjCMethodDecl *methDecl){// 包括了 protocol 方法的定义
if(!isUserSourceCode(methDecl)){
return true;
}
ObjCInterfaceDecl *interfaceDecl = methDecl->getClassInterface();
if(!interfaceDecl || interfaceHasProtocolMethod(interfaceDecl, methDecl)){
return true;
}
StringRef clsName = interfaceDecl->getName();
MethodVector methodVec;
if(definedMethodsMap.find(clsName) != definedMethodsMap.end()) {
methodVec = definedMethodsMap.at(clsName);
}else{
methodVec = MethodVector();
definedMethodsMap.insert(std::make_pair(clsName, methodVec));
}
methodVec.push_back(methDecl);
InterfaceMethodsMap::iterator it = definedMethodsMap.find(clsName);
it->second = methodVec;
return true;
}
//
bool interfaceHasProtocolMethod(ObjCInterfaceDecl *interfaceDecl ,ObjCMethodDecl *methDecl){
for(auto*protocolDecl : interfaceDecl->all_referenced_protocols()){
if(protocolDecl->lookupMethod(methDecl->getSelector(), methDecl->isInstanceMethod())) {
return true;
}
}
return false;
}
以上,在ObjCInterfaceDecl的文档中,我们可以找到all_referenced_protocols()方法,可以让我们拿到当前类遵循的所有协议,而其中的ObjCProtocolDecl类则有lookUpMethod()方法,可以用于检索协议定义中是否有某个方法。也就是说,当我们遇到一个方法定义时,我们需要多做一步判断:若该方法是协议方法,则忽略,否则记录下来,用于后续判断是否被使用
最后:
//查找无用方法
void logResult(){
DiagnosticsEngine &D = Instance.getDiagnostics();
for(InterfaceMethodsMap::iterator definedIt = definedMethodsMap.begin(); definedIt != definedMethodsMap.end(); ++definedIt){
StringRef clsName = definedIt->first;
MethodVector definedMethods = definedIt->second;
if(usedMethodsMap.find(clsName) == usedMethodsMap.end()) {
// the class could not be found ,all of its method is unused.
for(auto*methDecl : definedMethods){
int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning,"无用方法定义 : %0 ");
D.Report(methDecl->getLocStart(), diagID) << methDecl->getSelector().getAsString();
outfile << "无用方法定义" << std::endl;
}
continue;
}
MethodVector usedMethods = usedMethodsMap.at(clsName);
for(auto*defined : definedMethods){
bool found =false;
for(auto*used : usedMethods){
if(defined->getSelector() == used->getSelector()){// find used method
found =true;
break;
}
}
if(!found) {
for(auto sel : usedSelectors){
if(defined->getSelector() == sel){// or find @selector
found =true;
break;
}
}
}
if(!found){
int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning,"Method Defined ,but never used. SEL : %0 ");
D.Report(defined->getLocStart(), diagID) << defined->getSelector().getAsString();
}
}
}
}
logResult方法在ASTConsumer自定义类中调用,
void HandleTranslationUnit(ASTContext& context) override
{
this->visitor.setASTContext(context);
this->visitor.TraverseDecl(context.getTranslationUnitDecl());
this->visitor.logResult();
}
摘录:
https://my.oschina.net/vimfung/blog/866109
https://github.com/LiuShulong/SLClangTutorial/blob/master/%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E7%BC%96%E5%86%99clang%E6%8F%92%E4%BB%B6%E5%92%8Clibtool.md
http://kangwang1988.github.io/tech/2016/10/31/check-code-style-using-clang-plugin.html
http://blog.gocy.tech/