关键词
oclint, cmake, xcode, rule
预计阅读时间
20-30 min
准备开发环境
本文基于以下环境完成
macOS Big Sur 11.1
Xcode 12.0.1
Apple clang version 12.0.0 (clang-1200.0.32.2)
cmake 3.19.2
OCLint 20.11
下载 oclint 代码
在自定义目录下克隆仓库
$ git clone https://github.com/oclint/oclint.git
克隆完成后, 进入oclint-scripts 目录下 ./make 即可开始编译,当然在这个过程中,也会下载依赖库的代码。 具体的时间看网络的情况(建议自备梯子)。 编译成功后就可以准备规则的编写了!
创建规则——scaffoldRule 脚本
这是由 oclint 提供的一个脚手架。 相关介绍如下 docs.oclint.org/en/stable/d…
可以使用该脚本可以方便的创建自定义规则。
下面我们来看一个实例,在 oclint-scripts 目录下
./scaffoldRule KirinzerTest -c controversial -t ASTVisitor
// 生成一个名为 KirinzerTestRule 类型是 ASTVisitor 的规则模板
如果有错误提示,例如如下的提示 FileNotFoundError: [Errno 2] No such file or directory: '/Users/developer/Projects/oclint/oclint-> scripts/../llvm/tools/clang/include/clang/Basic/StmtNodes.td' 那么,需要将 llvm 拷贝至相应的目录下面。
我们通过他传入要生成的规则名,级别,类型,脚本就会在目录oclint-rules/rules/controversial/自动帮我们生成一个模板代码,并且加入编译路径中。
在 oclint-rules/rules/controversial 目录下,会生成两个文件
- KirinzerTestRule.cpp
- CMakeLists.txt
// CMakeLists.txt 是对规则 KirinzerTestRule 的编译描述,由make程序在编译时使用。KirinzerTestRule.cpp 的内容之后再分析
编写规则
通过阅读 oclint 的官方文档,以及阅读 clang AST 的介绍。现在我们已经知道了,oclint 的大致工作方式。首先通过调用 clang 的 api 把源文件一个个的生成对应的 AST;其次遍历 AST 中的每个节点,并根据相应的规则将违例情况写入违例结果集;最后根据配置的报告类型,将违例结果输出成指定的报告格式。
先上一个 oclint 编写思路的脑图,有个初步的印象即可。
按照上文,我们现在已经得到了一个 xcodeproj 工程。现在可以打开我们创建的规则的 cpp 源文件。
首先我们可以看到,使用脚手架生成的规则,模板代码有近 2000 行,是不是有点慌? 不用担心。这些模板里,大多都是 Visit 开头的方法,这是 oclint 提供给我们的回调方法, 也就是说在访问到 AST 上相应的节点时就会触发的方法。
下面我们来看一个实际的案例,已经用在 iOS 组的代码检查中的一个规则。 这个规则所做的工作大致如下,按照 cocoa 的规范要求来检查 if else 条件分支的格式。 具体的格式要求是这样的,if else 和后面跟着的括号以及花括号要分割开,可以使用空格和换行符。 示例代码如下:
void example()
{
int a = 1;
if(a > 0) { // (左侧无空格或换行不合规
a = 10;
}
if (a > 0){ // )右侧无空格或换行不合规
a = 10;
}
if (a > 0)
{
a = 10;
}else { // }右侧无空格或换行不合规
a = -1;
}
if (a > 0)
{
a = 10;
} else{ // {左侧无空格或换行不合规
a = -1;
}
}
- 首先,在终端中使用 dump 查看 AST(第一部分已经介绍了如何查看 AST,如果没看过建议先看看第一部分)
屏幕上一连串花花绿绿的字符闪过,最后停在了这里!
没错,这就是正是我们需要找的。
可以很清楚的看到,最上方的变量声明 VarDecl,以及下方的条件语句 IfStmt。 2. 需要检验的节点名称已经确定,就是 IfStmt 3. 接下来,在已经生成的规则模板中找对应的回调方法 我猜,应该叫做 VisitXXIfStmt 之类的 果然不出所料,我们找到了!VisitIfStmt 这个方法,看起来正是我们所需要的。 4. 紧接着,我们需要获取节点名称和节点描述。(详细的代码可以参看下方提供的完整规则文件) 5. 最后是判断这里的方法名是否符合规则。(可以使用 llvm,clang,以及 std 提供的各种函数,如果有你需要的) 6. 如果检测出来的方法名是不符合规范的,将节点及描述信息加入 violationSet。
到这里,整体的编写流程已经完成了。相信你看完下方的实例代码,以及再多读几个官方提供的规则代码之后,很快就可以举一反三的写出自己的规则了。
这里直接给出上文规则的完整实现
#include "oclint/AbstractASTVisitorRule.h"
#include "oclint/RuleSet.h"
using namespace std;
using namespace clang;
using namespace oclint;
class KirinzerTestRule : public AbstractASTVisitorRule<KirinzerTestRule>
{
public:
virtual const string name() const override
{
return "if else format";
}
virtual int priority() const override
{
return 2;
}
virtual const string category() const override
{
return "controversial";
}
#ifdef DOCGEN
virtual const std::string since() const override
{
return "20.11";
}
virtual const std::string description() const override
{
return "用于检查 if else 条件分支中的括号是否符合编码规范";
}
virtual const std::string example() const override
{
return R"rst(
.. code-block:: cpp
void example()
{
int a = 1;
if(a > 0) { // (左侧无空格或换行不合规
a = 10;
}
if (a > 0){ // )右侧无空格或换行不合规
a = 10;
}
if (a > 0)
{
a = 10;
}else { // }右侧无空格或换行不合规
a = -1;
}
if (a > 0)
{
a = 10;
} else{ // {左侧无空格或换行不合规
a = -1;
}
}
)rst";
}
#endif
bool VisitIfStmt(IfStmt *node)
{
clang::SourceManager *sourceManager = &_carrier->getSourceManager();
SourceLocation begin = node->getIfLoc();
SourceLocation elseLoc = node->getElseLoc();
SourceLocation end = node->getEndLoc();
int length = sourceManager->getFileOffset(end) - sourceManager->getFileOffset(begin) + 1; // 计算该节点源码的长度
string sourceCode = StringRef(sourceManager->getCharacterData(begin), length).str(); // 从起始位置按指定长度读取字符数据
// printf("%s\n", sourceCode.c_str());
// 检查 if 左括号
std::size_t found = sourceCode.find("if (");
if (found==std::string::npos) {
// printf("if ( 格式不正确\n");
AppendToViolationSet(node, Description());
}
// 检查 if 右括号
found = sourceCode.find(") {");
if (found==std::string::npos) {
found = sourceCode.find(")\n");
if (found ==std::string::npos) {
// printf("if 右括号 格式不正确\n");
AppendToViolationSet(node, Description());
}
}
// 没有 else 分支就不再进行检查
if (!elseLoc.isValid()) {
return true;
}
// 检查 else 左括号
found = sourceCode.find("} else");
if (found==std::string::npos) {
found = sourceCode.find("}\n");
if (found==std::string::npos) {
// printf("} else 格式不正确\n");
AppendToViolationSet(node, Description());
}
}
// 检查 else 右括号
found = sourceCode.find("else {");
if (found==std::string::npos) {
found = sourceCode.find("else\n");
if (found==std::string::npos) {
// printf("else { 格式不正确\n");
AppendToViolationSet(node, Description());
}
}
return true;
}
// 将违例信息追加进结果集
bool AppendToViolationSet(IfStmt *node, string description) {
addViolation(node, this, description);
}
string Description() {
return "格式不正确";
}
};
static RuleSet rules(new KirinzerTestRule());
调试规则
根据前面的所学到的内容,我们知道了规则的实际体现形式为 dylib 文件。那么如果编写 cpp 的时候没办法调试,那真的是噩梦一般的体验。将我们现在遇到的问题,如何调试 oclint 规则?
- 首先需要一个 Xcode 工程。
oclint 工程使用 CMakeLists 来维护依赖关系。我们也可利用 CMake 来将 CMakeLists 生成 xcodeproj。你可以对每个文件夹生成一个 Xcode 工程,在这里我们对 oclint-rules 生成对应的 Xcode 工程。
// 在OCLint源码目录下建立一个文件夹,我这里命名为oclint-xcoderules
mkdir oclint-xcoderules
cd oclint-xcoderules
// 执行如下命令
cmake -G Xcode -D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++ -D CMAKE_C_COMPILER=../build/llvm-install/bin/clang -D OCLINT_BUILD_DIR=../build/oclint-core -D OCLINT_SOURCE_DIR=../oclint-core -D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics -D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics -D LLVM_ROOT=../build/llvm-install/ ../oclint-rules
- Xcode 工程创建好之后,我们需要对指定的 Scheme 添加启动参数。并且在 Scheme 的 Info 一栏选择 Executable ,选择上文中编译完成的 oclint 可执行文件。
tip: 编译生成的oclint可执行文件在根目录下 build/oclint-release/bin 目录下,以最新版的 oclint 20.11 为例,生成的文件名为 oclint-20.11,会被 Finder 识别为 Document 类型。(.11被识别为了后缀),虽然并不影响在终端的直接调用,但是我们后续的调试中会需要在 xcode 中通过 Finder 来选取这个可执行文件,但是由于类型被识别错误,会导致无法点击选中。所以在这里我们就删除小数点,修改可执行文件名为 oclint-2011 并且没有任何后缀即可。(注意修改的时候,右键getInfo,在文件名和扩展名那一栏来修改,还有注意是否隐藏了拓展名)。
启动参数如下, (第一个参数是规则加载路径,第二个是测试规则用文件)
>-R=/Users/developer/TempData/oclint/oclint-xcoderules/rules.dl/Debug /Users/developer/TempData/oclint/oclint-xcoderules/test2.m -- -x objective-c -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
准备完成后即可运行规则,在控制台中可以输出你的规则运行的结果以及调试信息。
使用规则
使用xcode 编写的规则完成编译后,可以在 xcode 的 Products group 中找到相应的 dylib 文件。
默认情况下,规则将从$(/path/to/bin/oclint)/../lib/oclint/rules目录中加载,我们将其命名为“ 规则搜索路径”或“ 规则加载路径”。规则搜索路径由一组动态库组成,这些库在Linux,macOS和 Windows中具有扩展名 so, dylib 以及 dll。
通过将新规则拖放到规则加载路径中,可以立即使用它们。 因此,只需要将我们自定义规则生成的 dylib 放入默认的规则加载目录即可。当然这里的规则目录也是可以配置的。一个项目可以使用多个规则搜索路径,可以为不同的项目指定不同的规则加载路径。
更多详细的配置参考这里的官方文档: 选择OCLint检查规则
接下来该做什么
本文已经是 OCLint 系列的最后一篇了,相信看完这 3 篇文章后,你对于 oclint 已经有了一定程度的了解。
使用静态代码检查工具,可以高效的检查出代码中的潜在问题。在做持续的业务交付过程中,提高开发同学们对于编码规范的重视,防止代码的劣化,减少一些由于粗心导致的错误。
希望本文提及的静态检查工具,以及自定义规则的编写的说明,能帮助大家写出更高质量,更优雅,更美观的代码。