clang 开发应用xcode 编译检查的插件 二:开发篇
来源:互联网 发布:淘宝业务流程分析 编辑:程序博客网 时间:2024/06/05 20:59
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/
- clang 开发应用xcode 编译检查的插件 二:开发篇
- clang 开发应用xcode 编译检查的插件 一:构建篇
- Xcode clang-omp openmp开发
- 手把手教你开发 clang 插件
- Xcode插件开发入门
- 如何开发Xcode插件
- iOS开发 Xcode插件
- xcode 插件开发
- Xcode 增强开发效率的插件
- ios开发不能错过的xcode插件
- iOS开发进阶 - 常用的Xcode插件
- iOS开发中常用的Xcode插件
- iOS开发大神必备的Xcode插件
- 个人iOS 开发使用的Xcode插件
- 自己开发的一个Xcode插件
- iOS开发大神必备的Xcode插件
- iOS 开发必备的Xcode插件(快捷键)
- iOS开发大神必备的Xcode插件
- 【Ionic】value和placeholder比较
- 详谈判断点在多边形内的七种方法(最全面) hdu1756 hrbust1429 为例
- uWSGI教程-----使用uWSGI和nginx配置你的web服务器
- 如何高效利用github提升自己
- JSP(2)EL表达式入门
- clang 开发应用xcode 编译检查的插件 二:开发篇
- js 获取字符串长度
- Python 装饰,重载
- SQL Server怎样更改时间 sql 语句
- 基于用户画像的精准营销
- 背影
- 欢迎使用CSDN-markdown编辑器
- MATLAB带通滤波器开始端和结尾端数据异常(解决的小技巧)
- jmeter源码---JmeterUtils