iOS 坑的集中营

OCLint自定义规则迁移至clang插件的做法

2020-10-09  本文已影响0人  9a957efaf40a

前提:基于OCLint,使用LLVM9.0.0版本源码,macOS系统

OCLint中已经自带了LLVM的源码,因此仅仅需要OCLint即可。

1.下载并修改OCLint

1.1 下载

OCLint源码,这里使用v0.15版本即可。

1.2 修改

在编译之前,我们需要对OCLint的代码进行修改。

进入oclint-core/include/oclint,修改RuleCarrier.h文件如下:

namespace clang
{
   …
   class TranslationUnitDecl;
   // 增加编译器类
   class CompilerInstance;
}
private:
   …
   // 增加私有编译器成员变量
   clang::CompilerInstance *_CI;
public:
   RuleCarrier(clang::ASTContext *astContext, ViolationSet *violationSet);
   // 增加编译器成员变量的setter和getter方法
   void setCompilerInstance(clang::CompilerInstance *Instance);
   clang::CompilerInstance* getCompilerInstance();
   ...

进入oclint-core/lib,修改RuleCarrier.cpp文件如下:

// 引入头文件
#include <clang/Frontend/CompilerInstance.h>
  …
  // 增加setter和getter方法
  void RuleCarrier::setCompilerInstance(clang::CompilerInstance *Instance) {
      _CI = Instance;
  }
  clang::CompilerInstance* RuleCarrier::getCompilerInstance() {
      return _CI;
  }

进入oclint-rules/include/oclint,修改AbstractASTVisitorRule.h文件如下:

    …
    if (startLocation.isValid())
    {
       if (isUserSourceCode(*it)) {
       (void) /* explicitly ignore the return of this function */
        clang::RecursiveASTVisitor<T>::TraverseDecl(*it);
       }
    }
  }
  tearDown();
}
bool isUserSourceCode (clang::Decl *decl)
{
    clang::SourceManager *sourceManager = &_carrier->getSourceManager();
    std::string filePath = sourceManager->getFilename(decl->getSourceRange().getBegin()).str();
    if (filePath.empty())
        return false;
    //非XCode中的源码都认为是用户源码
    if(filePath.find("/Applications/Xcode.app/") != filePath.npos)
        return false;
    return true;
 }

进入oclint-rules/include/oclint,修改AbstractASTRuleBase.h文件如下:

class AbstractASTRuleBase : public RuleBase
{
protected:
    void addViolation(clang::SourceLocation location, clang::SourceLocation startLocation,
        clang::SourceLocation endLocation, RuleBase *rule, const std::string& message = "");

    void addViolation(const clang::Decl *decl, RuleBase *rule, const std::string& message = "");
    void addViolation(const clang::Stmt *stmt, RuleBase *rule, const std::string& message = "");
...

进入oclint-rules/lib,修改AbstractASTRuleBase.cpp文件如下:

// 修改addViolation方法
void AbstractASTRuleBase::addViolation(clang::SourceLocation location, clang::SourceLocation startLocation,
clang::SourceLocation endLocation, RuleBase *rule, const std::string& message)
{
    clang::SourceManager *sourceManager = &_carrier->getSourceManager();
    // 获取编译器实例
    clang::CompilerInstance *CI = _carrier->getCompilerInstance();
    /* if it is a macro location return the expansion location or the spelling location */
    clang::SourceLocation startFileLoc = sourceManager->getFileLoc(startLocation);
    clang::SourceLocation endFileLoc = sourceManager->getFileLoc(endLocation);
    int beginLine = sourceManager->getPresumedLineNumber(startFileLoc);
    if (!shouldSuppress(beginLine, *_carrier->getASTContext()))
    {
        llvm::StringRef filename = sourceManager->getFilename(startFileLoc);
        // 输出错误
        clang::DiagnosticsEngine &D = CI->getDiagnostics();
        int diagID = D.getCustomDiagID(clang::DiagnosticsEngine::Error, "%0");
        D.Report(location, diagID) << message.c_str();
        return;
        _carrier->addViolation(filename.str(),
                               beginLine,
                               sourceManager->getPresumedColumnNumber(startFileLoc),
                               sourceManager->getPresumedLineNumber(endFileLoc),
                               sourceManager->getPresumedColumnNumber(endFileLoc),
                               rule,
                              message);
    }
}

void AbstractASTRuleBase::addViolation(const clang::Decl *decl,
    RuleBase *rule, const std::string& message)
{
    if (decl && !shouldSuppress(decl, *_carrier->getASTContext(), rule))
    {
        // 增加第一个参数的传递
        addViolation(decl->getLocation(), decl->getBeginLoc(), decl->getEndLoc(), rule, message);
    }
}

void AbstractASTRuleBase::addViolation(const clang::Stmt *stmt,
    RuleBase *rule, const std::string& message)
{
    if (stmt && !shouldSuppress(stmt, *_carrier->getASTContext(), rule))
    {
        // 增加第一个参数的传递
        addViolation(stmt->getBeginLoc(), stmt->getBeginLoc(), stmt->getEndLoc(), rule, message);
    }
}
...

2.编译OCLint源码

编译OCLint可以参考这篇文章:OCLint从安装到使用

⚠️:如果获取的OCLint源码是已经编译过的,也就是说在build文件夹下的所有.a和.dylib不包含我们已经修改的代码,那么需要重新编译一份并进行替换。

比如可以单独编译oclint-core为xcode工程,然后重新生成OCLintRuleSet.aOCLintCore.a并将build/oclint-core/lib中的两个.a文件替换。


目前,需要替换的有OCLintRuleSet.aOCLintCore.a以及所有的xxRule.dylib

3.编写可执行插件程序

在编写插件之前,我们需要在可调试的环境下,看看能否正常加载oclint的规则,因此需要基于llvm来编写可执行文件调用代码检测规则。

oclint本身已经下载好了llvm的源码并进行了编译,因此直接使用即可。

在oclint源码根目录下,新建MyPluginExe文件夹,该文件夹下包含MyPluginExe.cppCMakeLists.txt以及一个文件夹cmakecmake文件夹中包含一个OCLintConfig.cmake文件,它的内容是oclint-core中cmake文件夹下OCLintConfig.cmake文件的拷贝。

在MyPluginExe文件夹中,MyPluginExe.cpp内容如下:

#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
#include <dlfcn.h>
#include <dirent.h>
#include "oclint/RuleSet.h"
#include "oclint/RuleBase.h"
#include "oclint/AbstractASTRuleBase.h"
#include "oclint/RuleCarrier.h"
#include "oclint/RuleConfiguration.h"

static llvm::cl::OptionCategory MyToolCategory("my-tool options");
using namespace clang::tooling;
using namespace llvm;
using namespace clang;
using namespace oclint;

namespace
{   
    //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)
        {}
        bool HandleTopLevelDecl(DeclGroupRef DG) override
       {
            return true;
       }
       void HandleTranslationUnit(clang::ASTContext& context) override
       {
            dynamicLoadRules("/Users/58liqiang/Desktop/58Test/oclint-0.15/build/oclint-rules/rules.dl");
            oclint::RuleConfiguration::hasKey("oclint");
            for (int ruleIdx = 0, numRules = oclint::RuleSet::numberOfRules(); ruleIdx < numRules; ruleIdx++)
            {
                RuleBase *rule = RuleSet::getRuleAtIndex(ruleIdx);
                auto violationSet = new ViolationSet();
                auto carrier = new oclint::RuleCarrier(&context, violationSet);
                carrier->setCompilerInstance(&Instance);
                if (rule) {
                    rule->takeoff(carrier);
                }
             }
        }
        void dynamicLoadRules(std::string ruleDirPath)
        {
            DIR *pDir = opendir(ruleDirPath.c_str()); // 打开目录
            if (pDir != nullptr) // 如果不为空
            {
               struct dirent *dirp;
               while ((dirp = readdir(pDir))) // 读取目录实体,每次会读取目录中的一个文件
               {
                   if (dirp->d_name[0] == '.') // 如果目录名称.开头 (在linux中,目录第一个文件总是.,第二个目录总是..,其他的隐藏文件大多也是.开头,比如:.DS_Store)
                   {
                       continue; // 继续
                   }
                   std::string rulePath = ruleDirPath + "/" + std::string(dirp->d_name);
                   void *handler = dlopen(rulePath.c_str(), RTLD_LAZY);
                   if (handler == nullptr) // 加载dylib,RTLD_LAZY:在dlopen返回前,对于动态库中存在的未定义的变量(如外部变量extern,也可以是函数)不执行解析,就是不解析这个变量的地址。
                   {
                       char *error = dlerror();
                       llvm::errs() << "打开动态库失败:" << error << "\n";
                   }
               }
               closedir(pDir); // 关闭文件
            }
        }
     };

    class MyPluginASTAction : public ASTFrontendAction
    {
        std::set<std::string> ParsedTemplates;
      public:
        // 重写CreateASTConsumer方法,返回自定义的consumer,自己去解析AST
        virtual std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, llvm::StringRef) override
        {
           return llvm::make_unique<MyPluginConsumer>(CI, ParsedTemplates);
        }
    };
}

int main(int argc, const char **argv) {
   CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
   ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());
   return Tool.run(newFrontendActionFactory<MyPluginASTAction>().get());
}

CMakeLists.txt文件内容为:

CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

SET(LLVM_ROOT "/Users/58liqiang/Desktop/58Test/oclint-0.15/build/llvm-install")

SET(CMAKE_MODULE_PATH
 ${CMAKE_MODULE_PATH}
 "${CMAKE_CURRENT_SOURCE_DIR}/cmake"
 )

INCLUDE(OCLintConfig)

INCLUDE_DIRECTORIES(
 "/Users/58liqiang/Desktop/58Test/oclint-0.15/oclint-core/include"
 "/Users/58liqiang/Desktop/58Test/oclint-0.15/oclint-rules/include"
 )

LINK_DIRECTORIES(
 "/Users/58liqiang/Desktop/58Test/oclint-0.15/build/oclint-core/lib"
 )

ADD_EXECUTABLE(MyPluginExe
 MyPluginExe.cpp
 )

TARGET_LINK_LIBRARIES(MyPluginExe
 OCLintRuleSet
 OCLintCore
 clangStaticAnalyzerFrontend
 clangStaticAnalyzerCheckers
 clangStaticAnalyzerCore
 clangRewriteFrontend
 clangRewrite
 clangCrossTU
 clangIndex
 ${CLANG_LIBRARIES}
 ${REQ_LLVM_LIBRARIES}
 ${CMAKE_DL_LIBS}
 )

现在,在oclint根目录下,创建MyPluginExe-xcode文件夹,并且进入该文件夹,执行命令:

bogon:MyPluginExe-xcode 58liqiang$ cmake -G Xcode ../MyPluginExe

最终会在MyPluginExe-xcode文件夹中生成xcode工程,打开xcode工程,编译MyPluginExe可执行文件,添加六个参数:

/Users/58liqiang/xxxx/xxx/Person.m
--
-isysroot
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.2.sdk
-isystem
/Users/58liqiang/Desktop/58Test/oclint-0.15/build/llvm-install/lib/clang/9.0.0/include

注意,你需要替换为自己本机的对应路径。

运行项目,即可对Person.m进行编译,并应用oclint的检测规则。

4.插件化

在oclint根目录下,新建文件夹MyPlugin,其中创建CMakeLists.txtMyPlugin.cppcmake文件夹三项。cmake文件夹中的内容和上面的相同.

CMakeLists.txt内容为:

CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

SET(LLVM_ROOT "/Users/58liqiang/Desktop/58Test/oclint-0.15/build/llvm-install")

SET(CMAKE_MODULE_PATH
 ${CMAKE_MODULE_PATH}
 "${CMAKE_CURRENT_SOURCE_DIR}/cmake"
 )

INCLUDE(OCLintConfig)

INCLUDE_DIRECTORIES(
 "/Users/58liqiang/Desktop/58Test/oclint-0.15/oclint-core/include"
 "/Users/58liqiang/Desktop/58Test/oclint-0.15/oclint-rules/include"
 )

LINK_DIRECTORIES(
 "/Users/58liqiang/Desktop/58Test/oclint-0.15/build/oclint-core/lib"
 )

SET(CMAKE_SHARED_LINKER_FLAGS "-undefined dynamic_lookup")

add_library(MyPlugin SHARED MyPlugin.cpp)

TARGET_LINK_LIBRARIES(MyPlugin
 OCLintRuleSet
 OCLintCore
)

MyPlugin.cpp内容如下:

#include "clang/Frontend/FrontendActions.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
#include <dlfcn.h>
#include <dirent.h>
#include "oclint/RuleSet.h"
#include "oclint/RuleBase.h"
#include "oclint/AbstractASTRuleBase.h"
#include "oclint/RuleCarrier.h"
#include "oclint/RuleConfiguration.h"
#include "oclint/AbstractASTVisitorRule.h"

using namespace clang;
using namespace oclint;

namespace
{
   //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)
        {}  
        bool HandleTopLevelDecl(DeclGroupRef DG) override
        {
           return true;
        }
        void HandleTranslationUnit(clang::ASTContext& context) override
        {
             dynamicLoadRules("/Users/58liqiang/Desktop/58Test/oclint-0.15/build/oclint-rules/rules.dl");
             oclint::RuleConfiguration::hasKey("oclint");
             for (int ruleIdx = 0, numRules = oclint::RuleSet::numberOfRules(); ruleIdx < numRules; ruleIdx++)
             {
                  RuleBase *rule = RuleSet::getRuleAtIndex(ruleIdx);
                  auto violationSet = new ViolationSet();
                  auto carrier = new oclint::RuleCarrier(&context, violationSet);
                  carrier->setCompilerInstance(&Instance);
                  if (rule) {  
                      rule->takeoff(carrier);
                  }
              }
         }
         void dynamicLoadRules(std::string ruleDirPath)
         {
              DIR *pDir = opendir(ruleDirPath.c_str()); // 打开目录
              if (pDir != nullptr) // 如果不为空
              {
                  struct dirent *dirp;
                  while ((dirp = readdir(pDir))) // 读取目录实体,每次会读取目录中的一个文件
                  {
                      if (dirp->d_name[0] == '.') // 如果目录名称.开头 (在linux中,目录第一个文件总是.,第二个目录总是..,其他的隐藏文件大多也是.开头,比如:.DS_Store)
                      {
                          continue; // 继续
                      }
                      std::string rulePath = ruleDirPath + "/" + std::string(dirp->d_name);
                      void *handler = dlopen(rulePath.c_str(), RTLD_LAZY);
                      if (handler == nullptr) // 加载dylib,RTLD_LAZY:在dlopen返回前,对于动态库中存在的未定义的变量(如外部变量extern,也可以是函数)不执行解析,就是不解析这个变量的地址。
                      {
                          char *error = dlerror();
                          llvm::errs() << "打开动态库失败:" << error << "\n";
                      }
                  }
                  closedir(pDir); // 关闭文件
              }
          }
    };
    class MyPluginASTAction : public PluginASTAction
    {
         std::set<std::string> ParsedTemplates;
       public:
           // 重写CreateASTConsumer方法,返回自定义的consumer,自己去解析AST
           virtual std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, llvm::StringRef) override
           {
               return llvm::make_unique<MyPluginConsumer>(CI, ParsedTemplates);
           }
           /*
            插件的入口函数,返回false则不执行任何插件的操作args是命令行参数,一般情况下为空
            clang -cc1 -load /Users/xx/Desktop/MyPlugin.dylib -plugin <plugin_name> -plugin-arg-<plugin_name> <arg> ../test.c
           */
           bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args) override {
               return true;
           }
    };
}

//clang:FrontendPluginRegistry::Add<自定义PluginASTAction> X("插件名称","插件描述")
static clang::FrontendPluginRegistry::Add<MyPluginASTAction>
X("MyPlugin", "My plugin");

现在,在oclint源码根目录下创建MyPlugin-xcode文件夹,进入文件夹,执行命令:

bogon:MyPlugin-xcode 58liqiang$ cmake -G Xcode ../MyPlugin

同样,打开生成的xcode工程,编译MyPlugin,会生成MyPlugin.dylib。

5. 调试插件(可跳过)

本质上,编译源文件需要使用clang可执行文件,而LLVM自带clang,因此我们可以直接使用clang程序来运行我们的插件。
要达到这个目的,需要将llvm项目编译成xcode工程,编译其中的clang即可,然后在clang的main函数中加载我们的插件(使用dlopen)。

6.使用插件

可以参考使用clang编写XCode代码检测插件的插件化配置部分内容。

7. 优化

OCLint将规范分为3个等级,使用priority属性来区分(分别为1、2、3,1为严重)。如果一次性加载所有的规则,无疑会导致编译时长的增加。

对于严重性低的,或者仅仅是推荐需要修改的代码,我们可以后置,在开发时期先忽略,放到完成后统一使用OCLint来检测(可忽略的修改,在改动的时候应该也会容易许多)。

如此,我们需要将规则分级,放入不同的文件夹下,选择性的加载规则从而提高编译速度。

修改插件代码:

class MyPluginConsumer : public ASTConsumer
    {
        CompilerInstance &Instance;
        std::set<std::string> ParsedTemplates;
        std::vector<std::string> IncludePath;
    public:
        MyPluginConsumer(CompilerInstance &Instance,
                         std::set<std::string> ParsedTemplates, std::vector<std::string> IncludePath) : Instance(Instance), ParsedTemplates(ParsedTemplates), IncludePath(IncludePath)
        {}
        ...
        void HandleTranslationUnit(clang::ASTContext& context) override
        {
            loadDirectory(IncludePath);
            oclint::RuleConfiguration::hasKey("oclint");
            for (int ruleIdx = 0, numRules = oclint::RuleSet::numberOfRules(); ruleIdx < numRules; ruleIdx++)
            {
                RuleBase *rule = RuleSet::getRuleAtIndex(ruleIdx);
                auto violationSet = new ViolationSet();
                auto carrier = new oclint::RuleCarrier(&context, violationSet);
                carrier->setCompilerInstance(&Instance);
                if (rule) {
                    rule->takeoff(carrier);
                }
            }
        }
        void loadDirectory(std::vector<std::string> includeStrings) {
            for (auto path : includeStrings) {
                std::string directoryPath = "/Users/58liqiang/Desktop/58Test/C/cmake使用/013.oclint测试/oclint-xcoderules/rules/custom/Debug";
                dynamicLoadRules(directoryPath + "/" + path);
            }
        }
        ...
    };
class MyPluginASTAction : public PluginASTAction
    {
        std::set<std::string> ParsedTemplates;
        std::vector<std::string> includePath;
    public:
        // 重写CreateASTConsumer方法,返回自定义的consumer,自己去解析AST
        virtual std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, llvm::StringRef) override
        {
            return llvm::make_unique<MyPluginConsumer>(CI, ParsedTemplates, includePath);
        }
        /*
         插件的入口函数,返回false则不执行任何插件的操作args是命令行参数,一般情况下为空
         clang -cc1 -load /Users/xx/Desktop/MyPlugin.dylib -plugin <plugin_name> -plugin-arg-<plugin_name> <arg> ../test.c
         */
        bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args) override {
            includePath = args;
            return true;
        }
    };

代码的修改部分,仅仅是增加了数组IncludePath来保存传递进来的文件夹名称,加载规则的时候只会加载对应文件夹下的规则。

WeChat84194cd4272898298f877c5a757cdcdf.png

这样,在项目的Other C Flags中,需要修改参数为: -Xclang -load -Xclang /Users/58liqiang/Desktop/libMyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin -Xclang -plugin-arg-MyPlugin -Xclang p0 -Xclang -plugin-arg-MyPlugin -Xclang p1。现在,就只会使用p0p1目录下的规则了。

8.现存的问题

比如一个关于循环的语法检测:

virtual const string name() const override
    {
        return "for loop should be while loop";
    }

    virtual int priority() const override
    {
        return 3;
    }

    virtual const string category() const override
    {
        return "basic";
    }

#ifdef DOCGEN
    virtual const std::string since() const override
    {
        return "0.6";
    }

    virtual const std::string description() const override
    {
        return "Under certain circumstances, some ``for`` loops can be simplified to while "
            "loops to make code more concise.";
    }

    virtual const std::string example() const override
    {
        return R"rst(
.. code-block:: cpp

    void example(int a)
    {
        for (; a < 100;)
        {
            foo(a);
        }
    }
    )rst";
    }


bool VisitForStmt(ForStmt *forStmt)
    {
        Stmt *initStmt = forStmt->getInit();
        Expr *condExpr = forStmt->getCond();
        Expr *incExpr = forStmt->getInc();
        if (!initStmt && !incExpr && condExpr && !isa<NullStmt>(condExpr))
        {
            addViolation(forStmt, this);
        }

        return true;
    }
#endif

最详细的信息其实是记录在description中的。然而并不是所有的规则都实现了该属性,并且该属性是由宏定义来控制的。如果打开宏定义,则必须手动为没有实现的规则添加该属性(否则会报符号错误)。

目前,简单一点的解决方法是当message没有值的时候,直接取规则的name属性的值。

// AbstractASTRuleBase.cpp
std::string errorMsg = message;
if (errorMsg == "") {
    errorMsg = rule->name();
}
D.Report(startLocation, diagID) << errorMsg.c_str();
上一篇 下一篇

猜你喜欢

热点阅读