OCLint自定义规则迁移至clang插件的做法
前提:基于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.a
和OCLintCore.a
并将build/oclint-core/lib
中的两个.a文件替换。
目前,需要替换的有OCLintRuleSet.a
、OCLintCore.a
以及所有的xxRule.dylib
。
3.编写可执行插件程序
在编写插件之前,我们需要在可调试的环境下,看看能否正常加载oclint的规则,因此需要基于llvm来编写可执行文件调用代码检测规则。
oclint本身已经下载好了llvm的源码并进行了编译,因此直接使用即可。
在oclint源码根目录下,新建MyPluginExe文件夹,该文件夹下包含MyPluginExe.cpp
、CMakeLists.txt
以及一个文件夹cmake
,cmake
文件夹中包含一个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.txt
、MyPlugin.cpp
和cmake
文件夹三项。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
来保存传递进来的文件夹名称,加载规则的时候只会加载对应文件夹下的规则。

这样,在项目的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
。现在,就只会使用p0
,p1
目录下的规则了。
8.现存的问题
- 错误提示
在我们修改的AbstractASTRuleBase.cpp
文件中,报错的信息来自于addViolation
函数。然而,OCLint已经存在的规则,很多调用addViolation
的时候,并没有将message
传递过来。这导致虽然有错误,但是没有任何文本性的提示。
比如一个关于循环的语法检测:
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();