利用 oclint 做静态代码分析
场景
有些情况下代码有问题,但编译器不会报警告,也不报错,运行期也不崩溃,但程序执行就会有bug。
举个例子:两个不同的category下有一个同名的方法,xcode9.2不会报警告,运行期不确定会调用哪个,导致bug出现。因为category会把自己的方法加入到主类的方法链表中,出现同名的话不确定是加入失败还是覆盖同名的。
代码如下,可以试试运行结果,编译器是不警告不报错的。
// 主类
@interface OCCategoryMethodTest : NSObject
@end
@implementation OCCategoryMethodTest
@end
// CategoryA
@interface OCCategoryMethodTest (CategoryA)
- (void)funcA;
@end
@implementation OCCategoryMethodTest (CategoryA)
- (void)funcA {
NSLog(@"Category A");
}
@end
// CategoryB
@interface OCCategoryMethodTest (CategoryB)
- (void)funcA;
@end
@implementation OCCategoryMethodTest (CategoryB)
- (void)funcA {
NSLog(@"Category B");
}
@end
// 在另一个类中测试调用代码
OCCategoryMethodTest *test = [[OCCategoryMethodTest alloc] init];
[test funcA]; // 此处代码会打印什么结果呢?我这是 “Category B”
假设 CategoryA 的 funcA,CategoryB 的 funcA 都是很重要的业务代码必须要执行到,不然就会出bug,CategoryA 和 CategoryB 是两个业务团队写的,彼此不知道对方怎么命名,此时就会出bug了,还非常难调试。
此时就有必要通过 oclint 静态代码检查来事先发现这种风险,提前解决。
oclint的使用
- oclint 安装配置和调试,
上网搜索一下 “oclint 自定义规则" 就有,或者上oclint官网有安装配置方法,
安装完了之后,就可以编写自定义规则来做代码检查了,还是拿category举例。
新建一个规则后,会得到一个 cpp 文件,这里面就可以用 C++ 编写自己的规则
oclint 会调用 clang 把代码建立好抽象语法树,然后在遍历树的时候,每遇到一个节点就会产生一个回调,在 cpp 文件中就有各种回调方法,比如 property 回调,interface 回调,category 回调等等。
那么要检查category,需要在 interface 的回调中分析。为什么不在 category 中,因为 interface 的子节点有很多 category,但是到了一个具体的 category 就不能保证他的 interface 是统一的,假如 interface 不一样的,那方法完全可以重名。
- category方法重名检测算法大致如下:
遇到一个 interface 回调,去遍历其下面的所有 category,的所有 method,把方法名的字符串作为 key,方法对象 ObjCMethodDecl 加入数组,数组作为 value,插入哈希表中。插入时,如果key存在,那么value数组中追加一个 ObjCMethodDecl,不存在则直接插入。
category 遍历完了之后,遍历哈希表,如果表中存在一个方法,他有2个以上的 ObjCMethodDecl,那么都弹出来,调用报错函数。
具体代码如下:
class MyCategoryMethodConflictRule : public AbstractASTVisitorRule<MTCategoryMethodConflictRule> {
// 省略部分无关代码
private:
// 方法名, ObjCMethodDecl 数组的 map
unordered_map<string, vector<ObjCMethodDecl *> > umap;
unordered_map<string, vector<ObjCMethodDecl *> >::iterator map_it;
public:
// InterfaceDecl 的回调方法
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *node)
{
umap.clear(); // 初始化
// 遍历主类 impl 的方法,因为有些函数没在 interface 中
ObjCImplementationDecl *impl = node->getImplementation();
ObjCContainerDecl::method_iterator met_it = impl->meth_begin();
while (met_it != impl->meth_end()) {
insert_map(met_it->getNameAsString(), met_it->getCanonicalDecl());
met_it++;
}
// 遍历分类
if (node->known_categories_empty() == false) {
ObjCInterfaceDecl::known_categories_iterator cate_it = node->known_categories_begin();
while (cate_it != node->known_categories_end()) {
// 获取分类 impl, 遍历分类方法
ObjCCategoryImplDecl *ca_impl = cate_it->getImplementation();
ObjCContainerDecl::method_iterator came_it = ca_impl->meth_begin();
while (came_it != ca_impl->meth_end()) {
insert_map(came_it->getNameAsString(), came_it->getCanonicalDecl());
came_it++;
}
cate_it++;
}
}
// 出错处理和清空map
if (umap.size() > 0) {
for (map_it = umap.begin(); map_it != umap.end(); map_it++) {
// 找出出现次数大于2的方法, 报错
if (map_it->second.size() > 1) {
string msg = "Method \"" + map_it->first + "\" also be implemented by other category or primary class";
vector<ObjCMethodDecl *> &vec = map_it->second;
for (unsigned int i=0; i<vec.size(); i++) {
addViolation(vec[i], this, msg); // 出错处理
}
}
}
umap.clear();
}
return true;
}
// 向 map 中加入一个元素
void insert_map(string name, ObjCMethodDecl *methodDec) {
// 如果存在,数组追加一,不存在,则插入
map_it = umap.find(name);
if (map_it == umap.end()) {
vector<ObjCMethodDecl *> vec;
vec.emplace_back(methodDec);
umap[name] = vec;
} else {
map_it->second.emplace_back(methodDec);
}
}
// 省略部分无关代码
}
写完后会生成动态库 dylib,oclint 会调用这个动态库去分析测试代码,然后就能得到很多警告了。
这里只拿一个例子做说明 - category 方法重名
此外还有很多有风险的代码规则可以检查。
比如 delegate 写成了 assign 的,分类中实现了 +initialize 方法的,block 中写了 self 的,等等。
静态代码分析能发现一部分代码有风险的地方,提前解决掉,避免线上出bug。当然运行期的问题静态分析是很难发现的。