美文网首页移动开发技术前沿iOS程序犭袁iOS && Android
使用Xcode开发iOS语法检查的Clang插件

使用Xcode开发iOS语法检查的Clang插件

作者: 杰嗒嗒的阿杰 | 来源:发表于2017-03-24 11:08 被阅读2050次

    1. 前言

    Xcode编译依赖于Clang编译器,由于clang是LLVM的一部分,而LLVM(构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本)又是一个开源的项目,因此,使得Clang可定制成为了可能。而且Clang本来也支持插件,那么也说明了Xcode也支持这样的插件。所以下面的文章重点讲述如何在Xcode中实现Clang插件的编写。

    2. 获取Clang源码

    由于是要使用到Xcode中,因此最好还是从苹果官网中获取LLVM的源码,目前版本是Xcode8.1下的LLVM的源码,注意苹果的命名是clang-800.0.42.1,不要以为只是Clang部分的源码,一开始我就是被这样坑的。其中LLVM主要的子项目包括:

    名称 描述
    LLVM Core 包含一个现在的源代码/目标设备无关的优化器,一集一个针对很多主流(甚至于一些非主流)的CPU的汇编代码生成支持。包含一个现在的源代码/目标设备无关的优化器,一集一个针对很多主流(甚至于一些非主流)的CPU的汇编代码生成支持。
    Clang 一个C/C++/Objective-C编译器,致力于提供令人惊讶的快速编译,极其有用的错误和警告信息,提供一个可用于构建很棒的源代码级别的工具.
    dragonegg gcc插件,可将GCC的优化和代码生成器替换为LLVM的相应工具。
    LLDB 基于LLVM提供的库和Clang构建的优秀的本地调试器。
    libc++、libc++ ABI 符合标准的,高性能的C++标准库实现,以及对C++11的完整支持。
    compiler-rt 针对"__fixunsdfdi"和其他目标机器上没有一个核心IR(intermediate representation)对应的短原生指令序列时,提供高度调优过的底层代码生成支持。
    OpenMP Clang中对多平台并行编程的runtime支持。
    vmkit 基于LLVM的Java和.NET虚拟机实现
    polly 支持高级别的循环和数据本地化优化支持的LLVM框架。
    libclc OpenCL(开放运算语言)标准库的实现
    klee 基于LLVM编译基础设施的符号化虚拟机
    SAFECode 内存安全的C/C++编译器
    lld clang/llvm内置的链接器

    本文主要针对Clang项目进行讲述,要先使用Clang必须先对LLVM进行编译。

    3. 编译LLVM

    下载源码完成后解压目录,接下来就是要做编译LLVM的工作了。首先来对这些源码生成一个Xcode工程,源码项目的编译是由cmake管理(关于cmake详细资料请参考:cmake官方教程),因此生成Xcode工程非常方便。执行下面的shell命令:

    cd 解压llvm目录
    mkdir build && cd build
    cmake -G Xcode -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES:STRING=x86_64 -DLLVM_TARGETS_TO_BUILD=host -DLLVM_INCLUDE_TESTS=OFF -DCLANG_INCLUDE_TESTS=OFF -DLLVM_INCLUDE_UTILS=OFF -DLLVM_INCLUDE_DOCS=OFF -DLLVM_INCLUDE_EXAMPLES=OFF -DLLVM_BUILD_EXTERNAL_COMPILER_RT=ON -DLIBCXX_INCLUDE_TESTS=OFF -DCOMPILER_RT_INCLUDE_TESTS=OFF -DCOMPILER_RT_ENABLE_IOS=OFF ../src
    

    等待执行,提示成功后即可看到目录下多了一个build目录,点进去就可以看到一个Xcode的工程文件。双击打开项目,然后执行All_BUILD的scheme等待完成即可(这里面会有一个compiler_rt的编译报错,表示无法编译compiler_rt,由于这块不涉及插件编写所以可以暂时忽略)。

    4. 添加一个简单的插件项目

    找到src/tools/clang/example/目录,在里面新建一个目录如MyPlugin。然后修改example目录的CMakeLists.txt文件,添加一项:

    add_subdirectory(MyPlugin)
    

    然后进入创建的MyPlugin目录,生成三个文件,分别是:

    CMakeLists.txt
    MyPlugin.cpp
    MyPlugin.exports
    

    然后在新建的CMakeList.txt中加入下面内容:

    # If we don't need RTTI or EH, there's no reason to export anything
    # from the plugin.
    if( NOT MSVC ) # MSVC mangles symbols differently
      if( NOT LLVM_REQUIRES_RTTI )
        if( NOT LLVM_REQUIRES_EH )
          set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/MyPlugins.exports)
        endif()
      endif()
    endif()
    
    add_llvm_loadable_module(MyPlugin MyPlugin.cpp)
    
    if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
      target_link_libraries(MyPlugin ${cmake_2_8_12_PRIVATE}
        clangAST
        clangBasic
        clangFrontend
        LLVMSupport
        )
    endif()
    

    5. 开发插件

    然后在插件文件MyPlugin.cpp中,添加下面的内容:

    #include "clang/Frontend/FrontendPluginRegistry.h"
    #include "clang/AST/AST.h"
    #include "clang/AST/ASTConsumer.h"
    #include "clang/Frontend/CompilerInstance.h"
    
    using namespace clang;
    
    namespace
    {
        class MyPluginConsumer : public ASTConsumer
        {
        CompilerInstance &Instance;
        std::set<std::string> ParsedTemplates;
        public:
            MyPluginConsumer(CompilerInstance &Instance,
                                   std::set<std::string> ParsedTemplates)
            : Instance(Instance), ParsedTemplates(ParsedTemplates) {}
        };
        
        class MyPluginASTAction : public PluginASTAction
        {
        std::set<std::string> ParsedTemplates;
        protected:
            std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                           llvm::StringRef) override
            {
                return llvm::make_unique<MyPluginConsumer>(CI, ParsedTemplates);
            }
            
            bool ParseArgs(const CompilerInstance &CI,
                           const std::vector<std::string> &args) override {
                
                DiagnosticsEngine &D = CI.getDiagnostics();
                D.Report(D.getCustomDiagID(DiagnosticsEngine::Error,
                                           "My plugin Started..."));
                
                return true;
            }
        };
    }
    
    static clang::FrontendPluginRegistry::Add<MyPluginASTAction>
    X("MyPlugin", "My plugin");
    

    上面的代码中主要先看看MyPluginASTActionParseArgs方法,这是一个插件的入口函数,在这个方法里面调用了一个叫DiagnosticsEngine对象的Report方法,这段代码的主要功能是向编译器报告一个错误,而错误的描述就是“My plugin Started...”,下面会有具体的演示效果。关于其它部分的代码现在可以暂时不用理会,后续的章节会进行详细的说明。

    现在先回到源码根目录,使用同样的cmake语句来更新Xcode项目,更新完成后原来的项目会多出一个叫MyPlugin的插件项目,然后对这个插件项目进行编译。编译成功后会在Debug/lib目录中多出一个叫MyPlugin.dylib文件。

    6. 配置调用插件的Xcode项目

    打开要使用插件的Xcode项目,在build settings一栏中对Other C Flags一项进行编辑,调整为:

    -Xclang -load -Xclang /llvm/build/Debug/lib/MyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin
    

    注:最后一项-Xclang MyPlugin中的MyPlugin为插件名字,一定要是自己设置的插件名称,否则无法调用插件

    由于Clang插件需要对应的Clang版本来加载,如果版本不一致会导致编译错误,如下图所示:

    不一致的Clang版本错误

    为了解决这个问题需要调整Xcode中使用的Clang编译器,将默认的编译器改为我们自己编译出来的编译器。具体的方法是在build settings中再添加两项自定义项:

    CC = /clang-800.0.42.1/build/Debug/bin/clang
    CXX =/clang-800.0.42.1/build/Debug/bin/clang++
    

    目的用于指定Xcode的编译器从之前默认的,改为自定义的Clang编译器(注:CC和CXX中需要指定为你编译出来的Clang所在的绝对路径)。

    Common + B 编译则可以看到一个插件输出的错误提示。

    插件输出错误

    7. 抽象语法树AST

    在实现语法检测之前,需要了解一个叫AST(抽象语法树)的东西(以下内容选自http://blog.chinaunix.net/uid-26750235-id-3139100.html):

    抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无关文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。基于AST的不依赖具体文法和不依赖语言细节的特点,使得其在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器。

    来看一个程序转换的语法树实例,来帮助加深对AST的理解:

    while b != 0
    {
         if a > b
             a = a-b
         else
             b = b-a
    }
    
    return a
    

    上面的一个while循环,经过Clang分析所产生的AST如下图所示:

    抽象语法树

    通过上面的语法树可以看到其描述代码的具体结构,而在Clang对代码编译时会进入一个语法树的解析阶段,则这个阶段中语法树的每个节点都会被遍历到,因此借助此阶段可以检测程序中所有代码的书写格式是否符合规范,甚至是对代码编写的质量作出分析。

    8. 实现编译时语法检测

    回到上面所说的插件例子中的代码,先来了解一下clang::PluginASTActionclang::ASTConsumer这两个类。clang::PluginASTAction是一个基于consumer的AST前端Action抽象基类。clang::ASTConsumer则是用于客户读取AST的抽象基类。它们之间的关系是clang::PluginASTAction作为一个关于AST的插件,同时也是访问clang::ASTConsumer的入口;而clang::ASTConsumer则是用于定义如何取得AST相关内容。正如上面所说的,定义继承于clang::PluginASTActionclang::ASTConsumer类的子类后,通过static clang::FrontendPluginRegistry::Add<MyPluginASTAction> X("MyPlugin", "My plugin”);就可以把插件注册到Clang中。

    但是上面的例子是不完整的,因为是作为演示,所以在一开始执行时就让编译器报告错误了,并没有进行语法上面的检测。那么,接下来要对例子进行一番改造,让Clang在编译时执行一些编码格式与规范的检测。

    首先,先把MyPluginASTAction类的ParseArgs方法中的错误报告去掉,这样可以让编译工作能够继续进行下去。修改后如下:

    class MyPluginASTAction : public PluginASTAction
    {
        std::set<std::string> ParsedTemplates;
        protected:
            std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                           llvm::StringRef) override
            {
                return llvm::make_unique<MyPluginConsumer>(CI, ParsedTemplates);
            }
            
            bool ParseArgs(const CompilerInstance &CI,
                           const std::vector<std::string> &args) override {
                return true;
            }
    };
    

    接着,在改写`MyPluginConsumer`类前要引用一个叫`RecursiveASTVisitor`的类模版。该类型主要作用是前序或后续地深度优先搜索整个AST,并访问每一个节点的基类,主要利用它来遍历一些需要处理的节点。同样,需要创建一个实现`RecursiveASTVisitor`的模版类。如:

    class MyPluginVisitor : public RecursiveASTVisitor<MobCodeVisitor>
    {
        private:
            CompilerInstance &Instance;
            ASTContext *Context;
            
        public:
            
            void setASTContext (ASTContext &context)
            {
                this -> Context = &context;
            }
            
            MyPluginVisitor (CompilerInstance &Instance)
                :Instance(Instance)
            {
                
            }
    };
    

    然后在MyPluginConsumer加入MyPluginVisitor模版类的引用,修改如下:

    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
            {
                visitor.setASTContext(context);
                visitor.TraverseDecl(context.getTranslationUnitDecl());
            }
        private:
            MyPluginVisitor visitor;
    };
    

    这里要说明的是MyPluginConsumer::HandleTopLevelDecl方法表示每次分析到一个顶层定义时(Top level decl)就会回调到此方法。返回true表示处理该组定义,否则忽略该部分处理。而MyPluginConsumer::HandleTranslationUnit方法则为ASTConsumer的入口函数,当所有单元被解析成AST时会回调该方法。而方法中调用了visitorTraverseDecl方法来对已解析完成AST节点进行遍历。在遍历过程中只要在Visitor类中捕获不同的声明和定义即可对代码进行语法检测。

    8.1 检测ObjC中的类声明

    按照一般的类声明规范,类名需要驼峰式的拼写方法,并且类名第一个字母需要大写。如果不符合这个规范则需要在编译过程中提示警告信息。为了实现这样的效果,需要在MyPluginVisitor中加入VisitObjCInterfaceDecl方法来捕获OC的类声明节点。具体代码如下:

    bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
    {
          if (isUserSourceCode(declaration))
          {
               checkClassNameForLowercaseName(declaration);
               checkClassNameForUnderscoreInName(declaration);
          }
                
          return true;
    }
    
    /**
      判断是否为用户源码
    
      @param decl 声明
      @return true 为用户源码,false 非用户源码
     */
    bool isUserSourceCode (Decl *decl)
    {
          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();
            
          //类名称必须以大写字母开头
          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::Error, "Class name should not start with lowercase letter");
               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::Error, "Class name with `_` forbidden");
               SourceLocation location = decl->getLocation().getLocWithOffset(underscorePos);
               diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
           }
    }
    

    从上面代码可以看到,整个VisitObjCInterfaceDecl方法的处理过程是:先判断是否为自己项目的源码,然后再分别检查类名字是否小写开头和类名称存在下划线,如果有这些情况则报告警告并提供修改建议。

    其中的isUserSourceCode方法判断比较重要,如果不实现该判断,则所有经过编译的代码文件中的类型都会被检测,包括系统库中的类型定义。该方法的基本处理思路是通过获取定义(Decl)所在的源码文件路径,通过比对路径来区分哪些是项目引入代码,哪些是系统代码。

    checkClassNameForLowercaseNamecheckClassNameForUnderscoreInName方法处理逻辑基本相同,通过decl -> getName()来获取一个指向类名称的StringRef对象,然后通过比对类名中的字符来实现相关的检测。

    这里要说明的是关于编译的修正提示和诊断报告的实现,DiagnosticsEngineReport方法可以给Xcode一个编译的诊断报告,这个报告可以是警告(DiagnosticsEngine::Warning)也可以是错误(DiagnosticsEngine::Error),具体的报告类型可根据具体需求决定,但是不同的报告类型会导致不同的处理结果,如果是警告信息则不影响编译,编译工作会继续往下进行,如果是错误则编译工作会被终止。

    首先,需要从编译器实例(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);
    

    最后在报告后通过AddFixItHint方法将提示信息附加到报告上,如:

    diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
    

    然后,编译插件,接下来新建一个项目验证一下插件是否正常工作。新建一个类文件ClassDecl,如:

    @interface class_Decl : NSObject
    
    @end
    

    编译时就会弹出错误提示,如:

    检测语法插件演示

    8.2 检测ObjC中的方法声明

    方法的检测主要包括是方法名称首字母是否为大写,每个参数的名称首字母是否为大写以及每个方法的实现是否超过500行。具体实现如下:

    bool VisitObjCMethodDecl(ObjCMethodDecl *declaration)
    {
          if (isUserSourceCode(declaration))
          {
                checkMethodNameForUppercaseName(declaration);
                checkMethodParamsNameForUppercaseName(declaration);
                checkMethodBodyForOver500Lines(declaration);
          }
                
          return true;
    }
    
    /**
      检测方法名是否存在大写开头
    
      @param decl 方法声明
     */
    void checkMethodNameForUppercaseName(ObjCMethodDecl *decl)
    {
          //检查名称的每部分,都不允许以大写字母开头
          Selector sel = decl -> getSelector();
          int selectorPartCount = decl -> getNumSelectorLocs();
          for (int i = 0; i < selectorPartCount; i++)
          {
               StringRef selName = sel.getNameForSlot(i);
               char c = selName[0];
               if (isUppercase(c))
               {
                   //修正提示
                   std::string tempName = selName;
                   tempName[0] = toLowercase(c);
                   StringRef replacement(tempName);
                   SourceLocation nameStart = decl -> getSelectorLoc(i);
                   SourceLocation nameEnd = nameStart.getLocWithOffset(selName.size() - 1);
                   FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
                        
                   //报告警告
                   DiagnosticsEngine &D = Instance.getDiagnostics();
                   int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Selector name should not start with uppercase letter");
                   SourceLocation location = decl->getLocation();
                   D.Report(location, diagID).AddFixItHint(fixItHint);
               }
         }
    }
    
    /**
      检测方法中定义的参数名称是否存在大写开头
    
      @param decl 方法声明
     */
    void checkMethodParamsNameForUppercaseName(ObjCMethodDecl *decl)
    {
          for (ObjCMethodDecl::param_iterator it = decl -> param_begin(); it != decl -> param_end(); it++)
          {
               ParmVarDecl *parmVarDecl = *it;
               StringRef name = parmVarDecl -> getName();
               char c = name[0];
               if (isUppercase(c))
               {
                    //修正提示
                    std::string tempName = name;
                    tempName[0] = toLowercase(c);
                    StringRef replacement(tempName);
                    SourceLocation nameStart = parmVarDecl -> getLocation();
                    SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
                    FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
                        
                    //报告警告
                    DiagnosticsEngine &D = Instance.getDiagnostics();
                    int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Selector's param name should not start with uppercase letter");
                    SourceLocation location = decl->getLocation();
                    D.Report(location, diagID).AddFixItHint(fixItHint);
                }
            }
    }
    
    /**
      检测方法实现是否超过500行代码
    
      @param decl 方法声明
     */
    void checkMethodBodyForOver500Lines(ObjCMethodDecl *decl)
    {
          if (decl -> hasBody())
          {
               //存在方法体
               Stmt *methodBody = decl -> getBody();
                    
               string srcCode;
               srcCode.assign(Instance.getSourceManager().getCharacterData(methodBody->getSourceRange().getBegin()),
                              methodBody->getSourceRange().getEnd().getRawEncoding() - methodBody->getSourceRange().getBegin().getRawEncoding() + 1);
               vector<string> lines = split(srcCode, '\n');
               if(lines.size() > 500)
               {
                   DiagnosticsEngine &D = Instance.getDiagnostics();
                   unsigned diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Single method should not have body over 500 lines");
                   D.Report(decl -> getSourceRange().getBegin(), diagID);
               }
          }
    }
    

    上述代码中使用VisitObjCMethodDecl方法来处理每个ObjC方法的声明。

    checkMethodNameForUppercaseName方法主要是检测方法名是否以大写开头,如果存在则发出警告,与类名称的检测处理类似,唯一不同的是ObjC的方法由多个部分组成,如:method:param1Desc:param2Dec:这种形式,因此在检测的时候需要通过声明(decl)的getNumSelectorLocs方法取得方法有多少部分,然后通过遍历的形式,对每部分进行检测。要注意的是,声明(decl)是不能直接取到方法每部分的名字的,其需要通过getSelector方法取得一个Selector对象,然后使用Selector对象来获取每部分的名称,如:

    Selector sel = decl -> getSelector();
    StringRef selName = sel.getNameForSlot(i);
    

    checkMethodParamsNameForUppercaseName方法则是对参数进行检测,这里采用遍历声明(decl)中的每个参数声明(parmVarDecl),然后从参数声明中取得其参数名字的方式来实现命名规范的检测,如:

    for (ObjCMethodDecl::param_iterator it = decl -> param_begin(); it != decl -> param_end(); it++)
    {
          ParmVarDecl *parmVarDecl = *it;
          StringRef name = parmVarDecl -> getName();
          //进行名称检测
    }
    

    checkMethodBodyForOver500Lines方法是对方法体的实现行数的限制检测,先从声明(decl)中取得方法体对象(methodBody),然后将方法体中的所有字符都读取出来,最后根据换行符来切分计算行数,如果大于500行则发出警告。如:

    Stmt *methodBody = decl -> getBody();
    
    //读取方法体的内容
    string srcCode;
    srcCode.assign(Instance.getSourceManager().getCharacterData(methodBody->getSourceRange().getBegin()),
                   methodBody->getSourceRange().getEnd().getRawEncoding() - methodBody->getSourceRange().getBegin().getRawEncoding() + 1);
    
    //根据回车符切分行
    vector<string> lines = split(srcCode, '\n');
    if(lines.size() > 500)
    {
          //发出警告
    }
    

    8.3 检测ObjC中的属性声明

    ObjC属性声明检测包括:属性名称是否存在大写开头、是否包含下划线以及委托属性是否使用weak修饰。实现代码如下:

    bool VisitObjCPropertyDecl(ObjCPropertyDecl *declaration)
    {
          if (isUserSourceCode(declaration))
          {
               checkPropertyNameForUppercaseName(declaration);
               checkPropertyNameForUnderscoreInName(declaration);
               checkDelegatePropertyForUsageWeak(declaration);
          }
                
          return true;
    }
    
    /**
      检测属性名是否存在大写开头
    
      @param decl 属性声明
    */
    void checkPropertyNameForUppercaseName(ObjCPropertyDecl *decl)
    {
          bool checkUppercaseNameIndex = 0;
                
          StringRef name = decl -> getName();
                
          if (name.find('_') == 0)
          {
               //表示以下划线开头
               checkUppercaseNameIndex = 1;
          }
                
          //名称必须以小写字母开头
          char c = name[checkUppercaseNameIndex];
          if (isUppercase(c))
          {
               //修正提示
               std::string tempName = name;
               tempName[checkUppercaseNameIndex] = toLowercase(c);
               StringRef replacement(tempName);
               SourceLocation nameStart = decl->getLocation();
               SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
               FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
                    
               //报告错误
               DiagnosticsEngine &D = Instance.getDiagnostics();
               int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "Property name should not start with uppercase letter");
               SourceLocation location = decl->getLocation();
               D.Report(location, diagID).AddFixItHint(fixItHint);
           }
    }
    
    /**
      检测属性名是否包含下划线
             
      @param decl 属性声明
     */
    void checkPropertyNameForUnderscoreInName(ObjCPropertyDecl *decl)
    {
          StringRef name = decl -> getName();
                
          if (name.size() == 1)
          {
               //不需要检测
               return;
          }
                
          //类名不能包含下划线
          size_t underscorePos = name.find('_', 1);
          if (underscorePos != StringRef::npos)
          {
               //修正提示
               std::string tempName = name;
               std::string::iterator end_pos = std::remove(tempName.begin() + 1, tempName.end(), '_');
               tempName.erase(end_pos, tempName.end());
               StringRef replacement(tempName);
               SourceLocation nameStart = decl->getLocation();
               SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
               FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
                    
               //报告错误
               DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
               unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Property name with `_` forbidden");
               SourceLocation location = decl->getLocation().getLocWithOffset(underscorePos);
               diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
          }
    }
    
    /**
      检测委托属性是否有使用weak修饰
    
      @param decl 属性声明
     */
    void checkDelegatePropertyForUsageWeak (ObjCPropertyDecl *decl)
    {
         QualType type = decl -> getType();
         StringRef typeStr = type.getAsString();
                
         //Delegate
         if(typeStr.find("<") != string::npos && typeStr.find(">") != string::npos)
         {
              ObjCPropertyDecl::PropertyAttributeKind attrKind = decl -> getPropertyAttributes();
                    
              if(!(attrKind & ObjCPropertyDecl::OBJC_PR_weak))
              {
                   DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
                   unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Delegate should be declared as weak.");
                   diagEngine.Report(decl -> getLocation(), diagID);
              }
         }
    }
    

    上述代码中使用VisitObjCPropertyDecl方法来处理每个ObjC属性的声明。

    checkPropertyNameForUppercaseName与类检测中的checkClassNameForLowercaseName方法处理逻辑基本相同,只是一个判断大小写的区别。

    checkPropertyNameForUnderscoreInName与类检测中的checkClassNameForUnderscoreInName方法处理一致。

    checkDelegatePropertyForUsageWeak方法中直接取到属性类型的文本描述,然后判断类型是否声明协议(即两个尖括号定义,注:这里判断是否存在尖括号是会存在误判的情况,因为泛型同样使用尖括号表示),最后再判断属性的修饰是否存在weak,如果没有则报告警告。

    8.4 检测常量/变量声明

    变量声明检测主要是区别静态变量、常量和变量,并根据不同类型声明进行不同的检测。如下所示:

    /**
      观察变量声明
    
      @param declaration 声明对象
      @return 返回
     */
    bool VisitVarDecl(VarDecl *declaration)
    {
          if (isUserSourceCode(declaration))
          {
               checkVarName(declaration);
          }
                
          return true;
    }
    
    /**
      检测变量名称
    
      @param decl 变量声明
     */
    void checkVarName(VarDecl *decl)
    {
          if (decl -> isStaticLocal())
          {
               //静态变量
              if (decl -> getType().isConstant(*this -> Context))
              {
                   //常量
                   checkConstantNameForLowercaseName(decl);
              }
              else
              {
                   //非常量
                   checkVarNameForUppercaseName(decl);
              }
                    
          }
          else if (decl -> isLocalVarDecl())
          {
               //本地变量
               if (decl -> getType().isConstant(*this -> Context))
               {
                   //常量
                   checkConstantNameForLowercaseName(decl);
               }
               else
               {
                   //非常量
                   checkVarNameForUppercaseName(decl);
               }
           }
           else if (decl -> isFileVarDecl())
           {
               //文件定义变量
               if (decl -> getType().isConstant(*this -> Context))
               {
                    //常量
                    checkConstantNameForLowercaseName(decl);
               }
               else
               {
                    //非常量
                    checkVarNameForUppercaseName(decl);
               }
           }
    }
    

    VisitVarDec方法来捕获变量和常量的声明。checkVarName方法中分别通过isStaticLocalisLocalVarDeclisFileVarDecl方法来区分变量的作用域,isStaticLocal表示静态变量,isLocalVarDecl表示本地变量和isFileVarDecl表示文件域中的声明。然后再通过decl -> getType().isConstant(*this -> Context)来判断是否为常量,常量就检测是否以大写开头,变量则检测是否以小写开头。

    至此,已经把一些基本的OC规范检测进行了实现,如果需要定制更多更复杂的检测规则,可以根据自身的需要来进行实现。

    9. Clang的应用范围讨论

    Clang的开源和插件化结构让自定义化成为了可能。像上面说的语法规范与代码质量的检测,编译优化以及语言间的翻译等等它都可以实现。对于语言间的翻译最好的例子就是DynamicCocoa,虽然至今还没有开源,但是可以推测其思路是基于AST的文法无关性,来从OC得到的AST分析结果来输出JS文件的定义。这个例子很好地反映了Clang与AST的强大,我们都知道C/C++语言是能够实现源码级别的跨平台,那么,以后会不会存在任何一门语言都能够实现跨平台呢?这个需要大家往后的努力,这里笔者只是抛出了自己的个人观点。

    10. 推荐链接


    http://clang.llvm.org/
    http://codemacro.com/2013/02/12/using-clang/
    https://kangwang1988.github.io
    http://jszhujun2010.farbox.com/tags/LLVM&Clang

    相关文章

      网友评论

      • 西博尔:测试demo , 我看这样只能用模拟器编译, 如何改成可以用真机也能测试的pass呢
        杰嗒嗒的阿杰:@西博尔 我这边暂时没有试过真机编译,因为语法检测其实跟模拟器真机没有关系,如果你需要做编译插件,那么你就需要去了解一下相关的资料了
      • weiliang:作者 你好,请教一个问题
        class MyPluginVisitor : public RecursiveASTVisitor<MobCodeVisitor>
        “MobCodeVisitor”这个地方报错了,我搜了头文件也没有搜到,请问如何导入呢,感激不尽。
      • 老孟:你好, 作者.
        最近在学习使用Clang, 看了你的博客, 受益匪浅!
        有些问题想要请教你, 请问能留一下邮箱或者QQ么
        老孟:@老孟 MyPlugin.cpp文件中, 最后一行代码 标点符号用的中文字符. 编译会报错
        老孟:@杰嗒嗒的阿杰 文章中第4步, CMakeList.txt文件中, MyPlugin写成了MyPlugins, 找不到对应文件, 会报错!
        杰嗒嗒的阿杰:@老孟 有什么问题直接在这里聊吧,让大家也了解一下:blush:
      • 徐不同:原谅我想说你,文章问题太多了,如果一个新手能根据你的东西写出来,我服他。
        杰嗒嗒的阿杰:@徐不同 赞赞哒:+1:
        徐不同:@杰嗒嗒的阿杰 1.创建的三个名字第一个是CMakeLists.txt,第二,你的代码内容中英文符号中文符号的引号重用,必须修改3。MobCodeConsumer的类,找不到,无法编译过去。其他问题我调试着跟你说的
        杰嗒嗒的阿杰:@徐不同 :relieved: 我接受你的批评,但我不太清楚你说的问题究竟有哪些,最好是能说出来,否则我无法知道我写的文章里面存在一些什么问题。
      • 宿于松下:好多细节没说清楚
        杰嗒嗒的阿杰:@宿于松下 你想了解哪些细节呢?可以在这里说一下。
      • gavinxwang:你好,有个问题请教一下,我按照你的介绍设置了xcode,但是编译后并没有出现预期的错误,这是为什么呢?
        杰嗒嗒的阿杰:@suruiqiang 确认cmake执行成功了吗
        26dbff972f25:你好,安装文章里的介绍,创建了一个myplugin, 然后重新cmake, 工程里没有看到myplugin这个target呢?
        杰嗒嗒的阿杰:插件名字没有配置错吧?

      本文标题:使用Xcode开发iOS语法检查的Clang插件

      本文链接:https://www.haomeiwen.com/subject/cnvxottx.html