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/