oclint - 检测子类是否重写父类私有方法

最近做项目的时候遇到一个问题,子类写了一个和父类的私有方法同名的方法,导致父类方法被子类该方法重写,出现异常。比如下面这种情况:

TestA.h:

#import <Foundation/Foundation.h>

@interface TestA : NSObject

- (void)sayHello;

@end

TestA.m:

#import "TestA.h"

@implementation TestA

- (void)myWord {
    NSLog(@"it is good");
}

- (void)sayHello {
    [self myWord];
}

@end

TestB.h:

#import "TestA.h"

@interface TestB : TestA

@end


TestB.m:

#import "TestB.h"

@implementation TestB

- (void)myWord {
    NSLog(@"it is not good");
}

@end

然后我创建一个 TestB 对象,并调用 sayHello 方法:

TestB *testB = [TestB new];
[testB sayHello];

printf:
it is not good

结果打印的是 it is not good。TestB 写了一个方法,刚好和父类 TestA 的私有方法重名了,覆盖了父类的私有方法,导致了错误的执行结果。其原理和OC运行时寻找方法的逻辑有关,不是本文重点在此不做详述。在项目越来越大的时候,类的继承往往会有很多层,此时我们有可能会在子类写了某个方法覆盖了某个父类的私有方法而又难以发现,因此需要有一个方法或者工具去排查这种潜在风险。经过调研发现,oclint这个第三方库可以通过自定义规则去实现这种检查,什么是oclint呢?

oclint是一个静态代码分析工具,它通过clang把代码生成为AST语法树,可以用于检查代码规范,发现代码中潜在的问题。原理可看下这两篇文章:
oclint的原理
了解ClangAST

oclint有提供默认的规则,如果只是要使用默认的规则,可以直接用 brew 安装 oclint,具体步骤如下:

# 安装 xcpretty
gem install xcpretty
# 安装 oclint
brew tap oclint/formulae
brew install oclint

gem install xcpretty 这一步可能会安装失败,可以尝试添加源:

gem sources --add [https://gems.ruby-china.com/](https://gems.ruby-china.com/)

然后去这里下载一个脚本并拖到自己项目的根目录,将脚本中最下方的 myworkspace 和 myscheme 改为自己的项目的 workspace 和 scheme。然后执行脚本,经过编译和oclint的分析,会生成一份分析报告,报告的格式也可以在脚本最下方的 reportType 填写(有html,xml 和 xcode),报告中就会列出项目代码中不符合默认规则的代码。

但是,我要检查的是 “子类是否重写父类私有方法”,oclint 没有这样的规则,所以此时就要走自定义规则的安装方式了。
github下载oclint的最新源码,然后安装以下工具:

brew install cmake ninja
gem install xcpretty

可能会遇到提醒安装其他的工具,按提示安装就好。
然后 cd 到oclint源码目录的oclint-scripts文件夹,执行

./make

之后就是漫长的下载,编译,我这边执行完需要1个小时以上,大概就是从svn下载LLVM,clang的源码,编译LLVM,clang和oclint的默认规则。编译的时候网络一定要好,不然中间连接svn超时又要重新开始了。
编译时我遇到的几个问题:
1.目前github最新版oclint0.15是无法编译的,会一直卡在下载LLVM那里,而oclint0.14下载的LLVM是8.0版本的,在Xocde11会报错,因为Xcode11是使用LLVM9.0。我的解决方法是手动把oclint0.14的LLVM下载版本改成9.0,issue那里已经有人提出来,看这里
这个问题的补充:后来在github上提了个issue,原来是作者把log取消掉了,我误以为卡住了,恢复log看这里
2.编译完成后,需要把 /Users/linzhesheng/Desktop/oclint-0.14/build/oclint-release/bin 里的 oclint-0.14 改成 oclint,不过该文件夹已经有一个文件叫oclint,需要把原来叫oclint的那个文件的名字也改了,改完之后就是这样子:

image.png

不改的话在后面执行 oclint-json-compilation-database(具体请看上述下载的脚本) 会报 Error: OCLint executable file not found. 的错误。

上面步骤执行完之后,cd 到 oclint-0.14 目录,执行:

oclint-scripts/scaffoldRule OverrideSuperClassPrivateMethodRule -t ASTVisitor

OverrideSuperClassPrivateMethodRule 是你自定义规则的名字,执行完之后会生成目录:


image.png

接下来就可以在 cpp 这个文件写你的自定义规则了,写完之后重新执行上面的make命令,重新编译一次。然而根据上面体验极差的编译过程每次调试重新编译是不可能的,需要一个本地调试的方式:
cd 到 oclint-0.14 目录

mkdir oclint-xcoderules
cd 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工程,这个工程包含了所有的oclint规则,也包含上面自己创建的规则,打开这个工程,选择自己的规则,编译成功之后就能在Products里看到这个规则的动态库了:


image.png

那么如何在Xcode里面调试这个动态库呢,需要配置一个主程序来引导启动,把oclint复制一个出来,并改名为oclintExe:


image.png

然后工程里面选择这个可执行文件:
image.png

接下来,配置输入的用于被检查的文件:


image.png

这里填入的内容是:
2011574924479_.pic_hd.jpg

1里填入的内容就是oclint这个Debug文件的路径,2里填入的内容是用于被检查的文件的路径,后面那一长串的内容,是你编译这个被检查的文件时得到的编译依赖,在这里可以看到:
image.png

把 从 “-target” 到结尾的内容复制过来就行了。

接下来把工程跑起来,就能用你自定义的规则检查这个文件了,还能打断点,简直不要太方便:


image.png

不过这里有个问题,就是只能输入一个被检查的文件,多个就不行。(如果有谁知道的话可以留言告诉我下,感谢)那如何只输入一个.m文件就能检查多个类呢,我的解决方法是把多个类的实现放在同一个.m文件里面,像这样:


image.png

这样就能在Xcode调试的时候检查多个类了。
准备工作做完了,接下来就是编码实现自定义规则。oclint实现代码静态检查的原理,就是调用clang的API把一个个源文件生成一个个AST,然后遍历AST中的每个节点传入各个规则,并在遍历到每个节点时提供回调,这些便是oclint提供的回调函数:
image.png

例如这个 VisitObjCImplementationDecl 回调,是在遍历的类的实现节点的时候就会回调,VisitObjCInterfaceDecl在遍历到类的声明的时候会回调,那一个文件被clang生成的完整的节点树是什么呢,用这个命令可以打印出来:

clang -Xclang -ast-dump -fsyntax-only /Users/linzhesheng/Desktop/Goodman/Goodman/ViewController2.m

即使一个简单的文件,也会生成老长的一串:


image.png

这里就能显示各个节点的名字,至于各个节点代表什么,可以直接“节点名 clang”谷歌搜索出来。

回到我要实现的规则,我要实现的检查具体就是“子类的某个方法是否和其某个父类的某个私有方法重名”。要实现这个规则,我需要的数据是:子类的方法,子类的所有父类,所有父类的所有私有方法。通过查阅文档并实际调试发现,ObjCImplementationDecl代表一个类的实现,通过该节点可以获取该类的所有数据(包括方法,父类)。而这个 ObjCImplementationDecl节点会在 VisitObjCImplementationDecl 这个回调返回,那么应该在这个回调里面去实现检查的逻辑,基本思路就是遍历这个类.m文件的每一个方法,然后再判断方法在这个类的所有父类的.m文件是否存在,如果存在再判断方法是否有声明,如果方法没有声明就是重写了父类私有方法,具体代码如下(C++):

bool VisitObjCImplementationDecl(ObjCImplementationDecl *node)
    { 
        // 遍历.m文件所有的方法
        for (auto method = node->meth_begin(), methodEnd = node->meth_end(); method != methodEnd; method++) {
            ObjCMethodDecl *methodDecl = (ObjCMethodDecl *)*method;
            // 方法在本类的所有父类的私有方法里是否存在
            ObjCMethodDecl *superMethodDecl = this->methodIsExistInSuperClassPrivateMethodsK(methodDecl, node);
            if (superMethodDecl) {
                // 将信息写进 report 里面,就是执行 oclint-json-compilation-database 得到的分析报告
                addViolation(methodDecl, this, "method oveeride superClass private method");
            }
        }
        
        return true;
    }

    // 方法在本类的所有父类的私有方法里是否存在
    ObjCMethodDecl* methodIsExistInSuperClassPrivateMethodsK(ObjCMethodDecl *method, ObjCImplementationDecl *node) {
        if (!method || !node) {
            return NULL;
        }
        
        // 取到父类的.h文件
        ObjCInterfaceDecl *superInterface = node->getSuperClass();
        // 取到父类的.m文件
        ObjCImplementationDecl *superNode = superInterface->getImplementation();
        
        // 如果是系统类或者库,无法取到.m文件,已经遍历到头,返回NULL
        if (!superNode) {
            return NULL;
        }
        
        // 尝试从父类的.m文件取到该方法
        ObjCMethodDecl *superMethodInImplementation = superNode->getMethod(method->getSelector(), method->isInstanceMethod());
        // 如果能从父类的.m文件取到该方法,则需要判断该方法在所有父类的.h文件里是否存在,如果不存在,则该方法为私有方法,返回该方法
        if (superMethodInImplementation && !this->methodIsExistInInterfaceAndAllSuperInterfaceK(method, superInterface)) {
            return method;
        }
        
        // 递归往更上层的父类去执行
        return this->methodIsExistInSuperClassPrivateMethodsK(method, superNode);
    }
    
    // 方法在.h文件以及所有父类的.h文件是否存在
    ObjCMethodDecl* methodIsExistInInterfaceAndAllSuperInterfaceK(ObjCMethodDecl *method, ObjCInterfaceDecl *interface) {
        if (!method || !interface) {
            return NULL;
        }
        
        // 尝试从父类的.h文件取到该方法
        ObjCMethodDecl *methodInInterface = interface->getMethod(method->getSelector(), method->isInstanceMethod());
        if (methodInInterface) {
            return methodInInterface;
        }
        
        // 递归往更上层的父类去执行
        return this->methodIsExistInInterfaceAndAllSuperInterfaceK(method, interface->getSuperClass());
    }

写完之后,直接运行调试,嗯木有问题,接下来就是把这个规则应用到oclint-json-compilation-database命令中,一开始我是再执行make命令编译一次,后面发现直接把规则对应的动态库拖到这里就能生效了:


image.png

并且我还把系统规则对应的动态库都移除掉了。

本来开开心心以后实现功能了,结果使用 oclint-json-compilation-database 跑起来之后,发现这个规则并没有生效。Xcode调试的时候是有效的,oclint-json-compilation-database执行无效,这两个的区别就是Xcode调试时我是把所有类的实现写到一个.m文件里面,而 oclint-json-compilation-database执行时类的实现我是分散在各自的.m文件。那是不是类的实现分散在各自的.m文件,检测代码就会因为某种原因失效呢?于是我再次把所有类的实现都写到一个.m文件,不过这次是执行 oclint-json-compilation-database 去检查,结果发现规则又生效了。后面通过添加 addViolation 把调试信息写进report发现,在类的实现分散在各自的.m文件时,ObjCImplementationDecl *superNode = superInterface->getImplementation() 取出来的 superNode 为 NULL,也就是无法获取获取到父类.m文件的信息。
于是找有没有其他方法可以获取父类的 ObjCImplementationDecl,发现文档中还有这两个方法:


image.png

,第二个方法的unsigned ID找不到获取方式, 使用第一个方法:


image.png

其他的参数看起来可以从 node 获取到父类的信息,只有最后一个参数,找不到获取的方式。尝试填入node自己的startLoc,获取到的 superNode 还是 NULL。网上也搜索不到相关资料,思路卡在这里了。。。
是不是 oclint 本身的实现方式就不支持获取不同.m文件的信息呢?oclint-json-compilation-database处理的是

compile_commands.json文件,compile_commands.json文件是通过 xcpretty 生成的,见脚本:

xcodebuild -workspace ${myworkspace} -scheme ${myscheme} -sdk iphonesimulator -derivedDataPath ./build/derivedData -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json

打开这个 compile_commands.json 文件,发现是一个如下数据结构组成的数组:


image.png

每条数据都对应一个.m文件。command是这个.m文件的编译依赖,file 和 directory 组成.m文件的路径。因此猜想oclint是依次根据这个数据结构去拿到每一个.m文件进行分析,不同.m文件之间不存在联系。由于对编译原理相关的知识不是很熟,这里我没有深究下去。

那么既然无法获取到另一个.m文件的信息,我能不能先把项目里所有的.m文件的内容合到一个.m文件,再用oclint分析。因为在Xcode调试的时候就是多个类的实现写在同一个.m文件里面,所以我首先想到的是这种实现方式。
不过因为会对项目产生影响,需要把项目copy一份出来。在当前项目根目录执行:

mkdir ../copy
cp -af ./ ../copy
cd ../copy
find ./Goodman -name "*.m" | xargs sed 'a\' > ./allFile.m
mv ./allFile.m ./Goodman/allFile.m

这只是创建了一个包含其他所有.m文件内容的.m文件,还要把这个文件添加到项目的引用中。具体来说,我们平时把文件拖入到项目中,是会出现这个东西的:

image.png

勾选了 Add to targets 之后,文件才会出现在左边目录以及这个编译选项中:
image.png

而如果直接把文件拖到项目的文件夹中,这个文件跟你项目是没有任何联系的,不会参与到编译之中。那么如何用代码实现这个拖动文件到项目的过程呢,这两篇文章有详细的解释:
用脚本来修改Xcode工程
使用代码为Xcode工程添加文件
ruby脚本实现如下:

require 'xcodeproj'
project_path = './Goodman.xcodeproj'
project = Xcodeproj::Project.open(project_path)
target = project.targets.first
group = project.main_group.find_subpath(File.join('Goodman'), true)
group.set_source_tree('SOURCE_ROOT')
# 删除文件引用
group.files.each do |file|
    # print file
    target.source_build_phase.remove_file_reference(file)
end
# 添加文件引用
file_ref = group.new_reference('./allFile.m')
target.add_file_references([file_ref])

合成.m文件完成了,编译通过,接下来执行 oclint 的脚本,嗯,自定义的规则又可以生效了。
然而事情还是没有那么顺利滴,当我把合成.m文件的脚本在我们实际项目中执行时,合成的.m文件出现了大量的编译错误。之前的Demo项目没出现错误时因为文件比较少,且实现方式比较简单。而我们的项目有大量的.m文件,各种各样的实现,合成之后就出现很多错误了。
那么看看是什么错误,有没有办法逐个解决,我只需要想办法规避编译错误,不需要管代码具体逻辑对不对。
(1)Reimplementation of class 'XXX' 和 'XXX.h' file not found
类重复定义,出现的原因是我们项目的文件夹里有一些已经无用的文件,没有添加到项目引用,静静的躺在项目目录之中,而我是把项目文件夹里所有的.m文件合成,这样这些无用的文件就都参与到编译中了。有些是和项目已有的实现重复了,有些是.h文件找不到(因为我只合成了.m)。总之,把这些无用的文件删掉就行了,这也算是一个意外的收获。
(2)Duplicate protocol definition of 'XXX' is ignored
如果一个文件里引入了同名的协议,这些同名的协议会全部失效,用到这些协议的时候也会报错。这个也是之前代码的漏洞,解决方式是在同名协议里加上条件编译。

#ifndef TestProtocol_h
#define TestProtocol_h
@protocol TestProtocol <TestProtocol2>

- (void)good;

@end
#endif

#ifndef TestProtocol_h
#define TestProtocol_h
@protocol TestProtocol <TestProtocol2>

- (void)good;


@end
#endif

(3)Redefinition of 'XXX'
重复定义了变量,出现这个问题的原因是项目中在.m文件里定义了在这个文件内可访问的静态变量,合成之后,有些静态变量名重复了。解决方式和第二点一样,也是为每个静态变量添加条件编译,不过是使用脚本处理

brew install grep sed
# 将项目所有.m文件内容写到一个.m文件
find ./EasiPass -name "*.m" | xargs sed 'a\' > ./temporaryAllFile.m
# 将 temporaryAllFile.m 同时包含 static const = ; 的行输出到constDefine.m文件
ggrep -Pio '^(?=.*?static )(?=.*?const )(?=.*?=)(?=.*?;).+$' ./temporaryAllFile.m > ./constDefine.m
# 将 temporaryAllFile.m 包含 static const = ; 的行删除
gsed -i '/static /{/const /{/=/{/;/d}}}' ./temporaryAllFile.m
# 为 constDefine.m 符合条件的行添加条件编译,并生成 allFile.m 文件
ruby addConditional.rb
cat ./temporaryAllFile.m >> ./allFile.m

ruby 脚本如下

# addConditional.rb
# 为静态变量添加条件编译
File.open("./constDefine.m", "r").each_line do |line|
    # 项目中静态变量名以k或K开头,匹配出变量名(去除开头的k或K)
    $nameArray = line.match /(?<= k).*?(?= =)|(?<= K).*?(?= =)/
    f = open("./temporaryAllFile.m","a")
    if $nameArray then
        # 取到变量名
        $name = $nameArray[0]
        # 宏定义不能以数字开头,需要作处理
        if $name =~ /^[0-9]+/ then 
            $name = "number" + $name
        end
        $string1 = "#ifndef " + $name + "\n" + "#define " + $name + "\n"
        $string2 = $string1 + line + "#endif" + "\n\n"
        f.puts $string2
    else
        f.puts line
    end
end

(4)变量和宏定义重名
类似这样:


image.png

这种想不到什么好办法处理了。
(5)Property 'XXX' attempting to use instance variable '_XXX' declared in super class 'XXX'
类似这样:


image.png

userInfo 这个属性是写在 TestB 的父类的.h文件中,如果要在 TestB 中使用 _userInfo,就要使用 @synthesize userInfo = _userInfo; 为 TestB 生成这个成员变量,这样写在正常写法中没有问题,合在同一个.m文件就不知为何报错了。这种也不好解决。
所有.m文件合成到同一个.m文件,我的项目就报如上5个错误,第4,第5点不好解决,而且也不能保证后面不会出现其他的错误。看来这种解决方式是行不通了。
于是重新回到原点,思考其他的解决方式。在 oclint 回调 VisitObjCImplementationDecl 函数时,是可以拿到一个类的.m文件信息的,那么在拿到每个类的.m文件信息时,用集合把它保存起来,等到 oclint 检查最后一个类,回调 VisitObjCImplementationDecl 函数时,遍历集合中每个类去执行检查的逻辑,根据父类的类名去集合中取到父类信息,这样不就解决父类.m文件取不到的问题了吗?

按照这种思路,一开始我是直接把 VisitObjCImplementationDecl 返回的 ObjCImplementationDecl 保存起来,后面根据类名取出来,调用 ObjCImplementationDecl 的方法的时候,却报了内存访问错误,可能 oclint 在走完整个类的检查之后直接调用 delete 把 ObjCImplementationDecl 对象释放了。考虑用智能指针也不一定能解决对象被释放的问题,实现拷贝构造函数, ObjCImplementationDecl 有很多属性指向了其他的对象,貌似更加复杂,因此还是决定自定义类,从 ObjCImplementationDecl 取出必要的信息,用自定义类保存起来。

在上文“子类的某个方法是否和其某个父类的某个私有方法重名”的检查逻辑中,是检查到本类的一个方法如果能在父类的.m文件中找到,就去从该父类开始到NSObject的.h文件中找是否有这个方法,如果没有找到则报错。实际上还应该把寻找的范围扩大到从该父类开始到NSObject的所有分类的头文件,.h文件遵循的所有协议,分类遵循的协议,协议遵循的协议,因为在这些范围里声明的方法,子类重写没有问题。因此自定义类的数据结构如下:

// 方法
class MyMethod {
public:
    string name;
    bool isInstance;
};

// 协议
class MyProtocolDecl  {
public:
    string name; // 协议名字
    vector<MyMethod> instanceMethodArray; // 实例方法数组
    vector<MyMethod> classMethodArray; // 类方法数组
    vector<MyProtocolDecl *> protocolArray; // 该协议继承的协议数组
    
    MyMethod getMethod(string name, bool isInstance) {
        if (isInstance) {
            for (MyMethod myMethod : instanceMethodArray) {
                if (myMethod.name == name) {
                    return myMethod;
                }
            }
        } else {
            for (MyMethod myMethod : classMethodArray) {
                if (myMethod.name == name) {
                    return myMethod;
                }
            }
        }
        
        // 递归往父协议寻找
        for (MyProtocolDecl *protocol : protocolArray) {
            MyMethod method = protocol->getMethod(name, isInstance);
            if (method.name.length()) {
                return method;
            }
        }
        
        MyMethod method;
        return method;
    }
};

// 分类
class MyCategoryDecl {
public:
    string name;
    vector<MyMethod> instanceMethodArray;
    vector<MyMethod> classMethodArray;
    vector<MyProtocolDecl *> protocolArray; // 该分类继承的协议数组
    
    MyMethod getMethod(string name, bool isInstance) {
        if (isInstance) {
            for (MyMethod myMethod : instanceMethodArray) {
                if (myMethod.name == name) {
                    return myMethod;
                }
            }
        } else {
            for (MyMethod myMethod : classMethodArray) {
                if (myMethod.name == name) {
                    return myMethod;
                }
            }
        }
        
        // 递归往父协议寻找
        for (MyProtocolDecl *protocol : protocolArray) {
            MyMethod method = protocol->getMethod(name, isInstance);
            if (method.name.length()) {
                return method;
            }
        }
        
        MyMethod method;
        return method;
    }
};

// .h文件
class MyInterfaceDecl {
public:
    string name;
    string superName; // 父类名字
    vector<MyMethod> instanceMethodArray;
    vector<MyMethod> classMethodArray;
    vector<MyCategoryDecl *> categoryDeclArray;
    vector<MyProtocolDecl *> protocolArray;
    
    MyMethod getMethod(string name, bool isInstance) {
        MyMethod result;
        if (isInstance) {
            for (MyMethod myMethod : instanceMethodArray) {
                if (myMethod.name == name) {
                    result = myMethod;
                    break;
                }
            }
        } else {
            for (MyMethod myMethod : classMethodArray) {
                if (myMethod.name == name) {
                    result = myMethod;
                    break;
                }
            }
        }
        return result;
    }
};

// .m文件
class MyImplementationDecl {
public:
    string name;
    string superName;
//    MyInterfaceDecl *interfaceDel;
    vector<MyMethod> instanceMethodArray;
    vector<MyMethod> classMethodArray;
    
    MyMethod getMethod(string name, bool isInstance) {
        MyMethod result;
        if (isInstance) {
            for (MyMethod myMethod : instanceMethodArray) {
                if (myMethod.name == name) {
                    result = myMethod;
                    break;
                }
            }
        } else {
            for (MyMethod myMethod : classMethodArray) {
                if (myMethod.name == name) {
                    result = myMethod;
                    break;
                }
            }
        }
        return result;
    }
};

接下来,要从 clang 相关的类中获取必要信息,生成自己的类的对象

// 创建方法
    MyMethod createMyMethod(clang::ObjCMethodDecl *methodDecl) {
        MyMethod myMethod;
        if (methodDecl) {
            myMethod.name = methodDecl->getNameAsString();
            myMethod.isInstance = methodDecl->isInstanceMethod();
        }
        return myMethod;
    }
    
    // 创建分类
    MyCategoryDecl* createMyCategoryDecl(ObjCInterfaceDecl::known_categories_iterator categories_iterator) {
        MyCategoryDecl *categoryDecl = new MyCategoryDecl();
        categoryDecl->name = categories_iterator->getNameAsString();
        vector<MyMethod> instanceMethodArray;
        vector<MyMethod> classMethodArray;
        for (auto method = categories_iterator->meth_begin(), methodEnd = categories_iterator->meth_end(); method != methodEnd; method++) {
            clang::ObjCMethodDecl *methodDecl = (clang::ObjCMethodDecl *)*method;
            if (methodDecl->isInstanceMethod()) {
                instanceMethodArray.emplace_back(this->createMyMethod(methodDecl));
            } else {
                classMethodArray.emplace_back(this->createMyMethod(methodDecl));
            }
        }
        categoryDecl->instanceMethodArray = instanceMethodArray;
        categoryDecl->classMethodArray = classMethodArray;
        
        vector<MyProtocolDecl *> protocolArray;
        for (ObjCProtocolDecl *protocol : categories_iterator->getReferencedProtocols()) {
            protocolArray.emplace_back(this->createMyProtocolDecl(protocol));
        }
        categoryDecl->protocolArray = protocolArray;
        
        return categoryDecl;
    }
    
    // 创建协议
    MyProtocolDecl* createMyProtocolDecl(ObjCProtocolDecl *protocol) {
        MyProtocolDecl *protocolDecl = new MyProtocolDecl();
        protocolDecl->name = protocol->getNameAsString();
        vector<MyMethod> instanceMethodArray;
        vector<MyMethod> classMethodArray;
        
        for (auto method = protocol->meth_begin(), methodEnd = protocol->meth_end(); method != methodEnd; method++) {
            clang::ObjCMethodDecl *methodDecl = (clang::ObjCMethodDecl *)*method;
            if (methodDecl->isInstanceMethod()) {
                instanceMethodArray.emplace_back(this->createMyMethod(methodDecl));
            } else {
                classMethodArray.emplace_back(this->createMyMethod(methodDecl));
            }
        }
        protocolDecl->instanceMethodArray = instanceMethodArray;
        protocolDecl->classMethodArray = classMethodArray;
        
        vector<MyProtocolDecl *> protocolArray;
        for (ObjCProtocolDecl *protocol : protocol->getReferencedProtocols()) {
            //            string protocolnName = protocol->getNameAsString();
            protocolArray.emplace_back(this->createMyProtocolDecl(protocol));
        }
        protocolDecl->protocolArray = protocolArray;
        
        return protocolDecl;
    }
    
    // 创建类的声明
    MyInterfaceDecl* createMyInterfaceDecl(ObjCInterfaceDecl *interfaceNode) {
        MyInterfaceDecl *interfaceDecl = new MyInterfaceDecl();
        interfaceDecl->name = interfaceNode->getNameAsString();
        if (interfaceNode->getSuperClass()) {
            interfaceDecl->superName = interfaceNode->getSuperClass()->getNameAsString();
        }
        vector<MyMethod> instanceMethodArray;
        vector<MyMethod> classMethodArray;
        for (auto method = interfaceNode->meth_begin(), methodEnd = interfaceNode->meth_end(); method != methodEnd; method++) {
            clang::ObjCMethodDecl *methodDecl = (clang::ObjCMethodDecl *)*method;
            if (methodDecl->isInstanceMethod()) {
                instanceMethodArray.emplace_back(this->createMyMethod(methodDecl));
            } else {
                classMethodArray.emplace_back(this->createMyMethod(methodDecl));
            }
        }
        interfaceDecl->instanceMethodArray = instanceMethodArray;
        interfaceDecl->classMethodArray = classMethodArray;
        
        if (interfaceNode->known_categories_empty() == false) {
            vector<MyCategoryDecl *> categoryDeclArray;
            ObjCInterfaceDecl::known_categories_iterator categories_iterator = interfaceNode->known_categories_begin();
            while (categories_iterator != interfaceNode->known_categories_end()) {
                //            string cateName = categories_iterator->getNameAsString();
                categoryDeclArray.emplace_back(this->createMyCategoryDecl(categories_iterator));
                categories_iterator++;
            }
            interfaceDecl->categoryDeclArray = categoryDeclArray;
        }
        
        vector<MyProtocolDecl *> protocolArray;
        
        for (ObjCProtocolDecl *protocol : interfaceNode->getReferencedProtocols()) {
            //            string protocolName = protocol->getNameAsString();
            protocolArray.emplace_back(this->createMyProtocolDecl(protocol));
            
        }
        interfaceDecl->protocolArray = protocolArray;
        
        return interfaceDecl;
    }

// 创建类的实现
    MyImplementationDecl* createMyImplementationDecl(ObjCImplementationDecl *node) {
        // 创建 MyImplementationDecl
        MyImplementationDecl *implementationDecl = new MyImplementationDecl();
        implementationDecl->name = node->getNameAsString();
        ObjCInterfaceDecl *interfaceNode = node->getClassInterface();
        if (interfaceNode) {
            implementationDecl->superName = interfaceNode->getSuperClass()->getNameAsString();
        }
        vector<MyMethod> instanceMethodArray;
        vector<MyMethod> classMethodArray;
        for (auto method = node->meth_begin(), methodEnd = node->meth_end(); method != methodEnd; method++) {
            clang::ObjCMethodDecl *methodDecl = (clang::ObjCMethodDecl *)*method;
            if (methodDecl->isInstanceMethod()) {
                instanceMethodArray.emplace_back(this->createMyMethod(methodDecl));
            } else {
                classMethodArray.emplace_back(this->createMyMethod(methodDecl));
            }
        }
        implementationDecl->instanceMethodArray = instanceMethodArray;
        implementationDecl->classMethodArray = classMethodArray;
        //        implementationDecl->interfaceDel = this->createMyInterfaceDecl(node->getClassInterface());
        return implementationDecl;
    }

然后每次回调 VisitObjCImplementationDecl 函数时都将类的信息保存起来:

class OverrideSuperClassPrivateMethodRule : public AbstractASTVisitorRule<OverrideSuperClassPrivateMethodRule>
{
private:
    map<string, MyImplementationDecl *> allClassImplementationDeclMap; // 所有类的实现
    map<string, MyInterfaceDecl *> allClassInterfaceDeclMap; // 所有类的声明
    vector<string> checkClassNameArray; // 接受检查的类

...

bool VisitObjCImplementationDecl(ObjCImplementationDecl *node)
    {
        this->checkOmyGa(node);
        return true;
    }

void checkOmyGa(ObjCImplementationDecl *node) {
        // 创建类的实现
        MyImplementationDecl *implementationDecl = this->createMyImplementationDecl(node);
        string nodeName = implementationDecl->name;
        allClassImplementationDeclMap[nodeName] = implementationDecl;
        checkClassNameArray.emplace_back(nodeName);
        
        this->cycleAddMyInterfaceDecl(node->getClassInterface());
    }

// 递归添加类的声明
    void cycleAddMyInterfaceDecl(ObjCInterfaceDecl *interfaceNode) {
        if (!interfaceNode) {
            return;
        }
        
        // 如果 allClassInterfaceDeclMap 已经有 nodeName 相关的信息,说明其所有父类的相关信息都有了,可直接 return
        string nodeName = interfaceNode->getNameAsString();
        if (allClassInterfaceDeclMap[nodeName]) {
            return;
        }
        
        MyInterfaceDecl *interfaceDecl = this->createMyInterfaceDecl(interfaceNode);
        allClassInterfaceDeclMap[nodeName] = interfaceDecl;
        this->cycleAddMyInterfaceDecl(interfaceNode->getSuperClass());
    }

这里有两点要注意:
(1)map在取一个不存在的key时,取完之后会把key添加到map里,导致map集合发生变化,因此把 所有类的实现 和 接受检查的类 使用两个集合区分开来。
(2)单独搞一个 所有类的声明 的集合,而不是根据 类的实现 去取到 类的声明。一个原因是因为假如使用“类的实现 取 类的声明 取 父类的声明 ”(clang就是这样实现的)这种方式,在创建自定义的 类的实现 的时候就要根据这个链条一直创建下去,而很多类具有共同的父类,这样共同的这些父类都是单独的对象,没有复用起来,而通过集合把所有的声明存储起来可以解决这个问题。另一个原因是很多类只能取到声明取不到实现,比如系统类,各种库。

还有一个要解决的问题,就是上文提到的 “等到 oclint 检查最后一个类,回调 VisitObjCImplementationDecl 函数时,遍历集合中每个类去执行检查的逻辑”,怎么知道 oclint 检查到了最后一个类呢, oclint 可没有提供这样的回调。
一开始的想法是将项目中所有的.h文件写到一个文件,在通过脚本计算这个文件包含了 "@interface : " 的行数,后面发现不准,有的项目的.h文件并没有参与编译。最后解决方案是跑一次oclint,每次执行 VisitObjCImplementationDecl 方法计数+1,写到一个文件中,等跑完之后就能得到oclint总共检查了多少个类。然后再跑一次oclint,去文件中取这个值就行了。这种方案检查时间会大大增加,不过能取到准确的类的总数:

class OverrideSuperClassPrivateMethodRule : public AbstractASTVisitorRule<OverrideSuperClassPrivateMethodRule>
{
private:
    int currentClassNumber = 0; // 当前执行到的类
    int maxClassNumber = 0; // 接受检查的类的最大数量
    bool isInitMaxClassNumber = false; // 是否已经初始化maxClassNumber
    string classNumberPath = "/Users/jz-linzhesheng/Desktop/oclint-release/classNumber.txt";
...

bool VisitObjCImplementationDecl(ObjCImplementationDecl *node)
    {
        this->checkOmyGa(node);
        return true;
    }

void checkOmyGa(ObjCImplementationDecl *node) {
        // 初始化接受检查的类的最大数量
        if (!isInitMaxClassNumber) {
            ifstream ifile(classNumberPath);
            string str((istreambuf_iterator<char>(ifile)), istreambuf_iterator<char>());
            maxClassNumber = atoi(str.c_str());
            isInitMaxClassNumber = true;
            addViolation(node, this, "the number of class is " + to_string(maxClassNumber));
        }
        
        currentClassNumber++;
        
        // 如果maxClassNumber初始化之后为0,则开始计算类的总数
        if (maxClassNumber == 0) {
            this->write_string_to_file(classNumberPath, to_string(currentClassNumber));
            return;
        }
        
        if (currentClassNumber == maxClassNumber) {
            // 执行检查逻辑
        }
    }

int write_string_to_file(string & file_string, string str )
    {
        ofstream OsWrite(file_string);
        OsWrite<<str;
        OsWrite<<endl;
        OsWrite.close();
        return 0;
    }

最后,执行检查的逻辑:

void checkOmyGa(ObjCImplementationDecl *node) {
       ...

        if (currentClassNumber == maxClassNumber) {
            for (string nodeName : checkClassNameArray) {
                MyImplementationDecl *implementationDecl = allClassImplementationDeclMap[nodeName];
                this->checkObjCImplementationDecl(implementationDecl, node);
            }
        }
    }

// 检查类中的方法是否重写了父类的私有方法
    void checkObjCImplementationDecl(MyImplementationDecl *implementationDecl, ObjCImplementationDecl *node) {
        // 遍历.m文件的所有方法(实例方法和类方法)
        for (MyMethod method : implementationDecl->instanceMethodArray) {
            MyImplementationDecl *superImplementationDecl = this->methodIsExistInSuperClassPrivateMethod(method, implementationDecl);
            if (superImplementationDecl) {
                string description = "\"" + implementationDecl->name + "\" have a method \"" + method.name + "\" override superClass \"" + superImplementationDecl->name + "\" privite method " ;
                addViolation(node, this, description);
            }
        }
        for (MyMethod method : implementationDecl->classMethodArray) {
            MyImplementationDecl *superImplementationDecl = this->methodIsExistInSuperClassPrivateMethod(method, implementationDecl);
            if (superImplementationDecl) {
                string description = "\"" + implementationDecl->name + "\" have a method \"" + method.name + "\" override superClass \"" + superImplementationDecl->name + "\" privite method " ;
                addViolation(node, this, description);
            }
        }
    }
    
    // 检查类的一个方法是否重写了父类的私有方法
    MyImplementationDecl* methodIsExistInSuperClassPrivateMethod(MyMethod method, MyImplementationDecl *implementationDecl) {
        
        string methodName = method.name;
        string nodeName = implementationDecl->name;
        
        // 得到当前类.h文件
        //        MyInterfaceDecl *interfaceDel = implementationDecl->interfaceDel;
        // 父类类名
        string superNodeName = implementationDecl->superName;
        // 得到父类.m文件
        MyImplementationDecl *superImplementationDecl = allClassImplementationDeclMap[superNodeName];
        
        
        // 如果是系统类或者动态库,无法取到.m文件,已经遍历到头,返回false
        if (!superImplementationDecl) {
            return NULL;
        }
        
        // 得到父类.h文件
        MyInterfaceDecl *superInterfaceDel = allClassInterfaceDeclMap[superNodeName];
        
        MyMethod superMethod = superImplementationDecl->getMethod(method.name, method.isInstance);
        if (superMethod.name.length() && !this->methodIsExistInInterfaceAndAllSuperInterface(method, superInterfaceDel)) {
            return superImplementationDecl;
        }
        
        return this->methodIsExistInSuperClassPrivateMethod(method, superImplementationDecl);
    }
    
    // 判断方法在本类以及本类所有的父类中是否有声明
    bool methodIsExistInInterfaceAndAllSuperInterface(MyMethod method, MyInterfaceDecl *interfaceDel) {
        if (!method.name.length() || !interfaceDel) {
            return false;
        }
        
        MyMethod interfaceMethod = interfaceDel->getMethod(method.name, method.isInstance);
        if (interfaceMethod.name.length()) {
            return true;
        }
        
        if (this->methodIsExistInCategory(method, interfaceDel)) {
            return true;
        }
        
        if (this->methodIsExistInProtocol(method, interfaceDel)) {
            return true;
        }
        
        MyInterfaceDecl *superInterfaceDel = allClassInterfaceDeclMap[interfaceDel->superName];
        return this->methodIsExistInInterfaceAndAllSuperInterface(method, superInterfaceDel);
    }
    
    // 判断方法在类的分类中是否有声明
    bool methodIsExistInCategory(MyMethod method, MyInterfaceDecl *interfaceDel) {
        if (!method.name.length() || !interfaceDel) {
            return false;
        }
        
        if (!interfaceDel->categoryDeclArray.size()) {
            return false;
        }
        
        for (MyCategoryDecl *categoryDel : interfaceDel->categoryDeclArray) {
            MyMethod categoryMethod = categoryDel->getMethod(method.name, method.isInstance);
            
            if (categoryMethod.name.length()) {
                return true;
            }
        }
        
        return false;
    }
    
    // 判断方法在类的协议中是否有声明
    bool methodIsExistInProtocol(MyMethod method, MyInterfaceDecl *interfaceDel) {
        if (!method.name.length() || !interfaceDel) {
            return false;
        }
        
        if (!interfaceDel->protocolArray.size()) {
            return false;
        }
        
        for (MyProtocolDecl *protocolDel : interfaceDel->protocolArray) {
            MyMethod protocolMethod = protocolDel->getMethod(method.name, method.isInstance);
            
            if (protocolMethod.name.length()) {
                return true;
            }
        }
        
        return false;
    }

写完之后跑到项目中,规则总算是生效了。
不过,还有一个未解决的问题:
这个 compile_commands.json文件,如果包含的要分析的文件实在是太多(具体数量不明,我的是2000文件以内就不会报错),执行 oclint-json-compilation-database 时,会报 "OSError: [Errno 7] Argument list too long" 的错误,参数太多了,不能分析。这位老哥写了一个python脚本去解决,大致就是把 compile_commands.json 文件拆分成几个小的文件,再依次输入给 oclint-json-compilation-database,再把生成的 report 合成一个 report。但是,对于我的规则这个解决方案是无效的,因为拆分之后,我就不能获取不同oclint分析过程中的.m文件的信息了。
这个问题后面再解决了,对于我的项目来说,排除Pod目录之后,就不会报这个错误。
总算解决了,中间走了很多弯路,但是最后能解决还是很有成就感的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342