clang 开发应用xcode 编译检查的插件 二:开发篇

版权声明:未经博主同意不得转载 https://blog.csdn.net/bluefish89/article/details/77994531

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/

猜你喜欢

转载自blog.csdn.net/bluefish89/article/details/77994531