程序员

利用 oclint 做静态代码分析

2018-06-02  本文已影响0人  ck2016

场景

有些情况下代码有问题,但编译器不会报警告,也不报错,运行期也不崩溃,但程序执行就会有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官网有安装配置方法,

安装完了之后,就可以编写自定义规则来做代码检查了,还是拿category举例。
新建一个规则后,会得到一个 cpp 文件,这里面就可以用 C++ 编写自己的规则
oclint 会调用 clang 把代码建立好抽象语法树,然后在遍历树的时候,每遇到一个节点就会产生一个回调,在 cpp 文件中就有各种回调方法,比如 property 回调,interface 回调,category 回调等等。

那么要检查category,需要在 interface 的回调中分析。为什么不在 category 中,因为 interface 的子节点有很多 category,但是到了一个具体的 category 就不能保证他的 interface 是统一的,假如 interface 不一样的,那方法完全可以重名。

遇到一个 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。当然运行期的问题静态分析是很难发现的。

上一篇下一篇

猜你喜欢

热点阅读