最近做項(xiàng)目的時(shí)候遇到一個(gè)問題娃胆,子類寫了一個(gè)和父類的私有方法同名的方法哩罪,導(dǎo)致父類方法被子類該方法重寫,出現(xiàn)異常锁摔。比如下面這種情況:
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
然后我創(chuàng)建一個(gè) TestB 對(duì)象廓旬,并調(diào)用 sayHello 方法:
TestB *testB = [TestB new];
[testB sayHello];
printf:
it is not good
結(jié)果打印的是 it is not good。TestB 寫了一個(gè)方法鄙漏,剛好和父類 TestA 的私有方法重名了嗤谚,覆蓋了父類的私有方法棺蛛,導(dǎo)致了錯(cuò)誤的執(zhí)行結(jié)果。其原理和OC運(yùn)行時(shí)尋找方法的邏輯有關(guān)巩步,不是本文重點(diǎn)在此不做詳述旁赊。在項(xiàng)目越來越大的時(shí)候,類的繼承往往會(huì)有很多層椅野,此時(shí)我們有可能會(huì)在子類寫了某個(gè)方法覆蓋了某個(gè)父類的私有方法而又難以發(fā)現(xiàn)终畅,因此需要有一個(gè)方法或者工具去排查這種潛在風(fēng)險(xiǎn)。經(jīng)過調(diào)研發(fā)現(xiàn)竟闪,oclint這個(gè)第三方庫可以通過自定義規(guī)則去實(shí)現(xiàn)這種檢查离福,什么是oclint呢?
oclint是一個(gè)靜態(tài)代碼分析工具炼蛤,它通過clang把代碼生成為AST語法樹妖爷,可以用于檢查代碼規(guī)范,發(fā)現(xiàn)代碼中潛在的問題理朋。原理可看下這兩篇文章:
oclint的原理
了解ClangAST
oclint有提供默認(rèn)的規(guī)則絮识,如果只是要使用默認(rèn)的規(guī)則,可以直接用 brew 安裝 oclint嗽上,具體步驟如下:
# 安裝 xcpretty
gem install xcpretty
# 安裝 oclint
brew tap oclint/formulae
brew install oclint
gem install xcpretty 這一步可能會(huì)安裝失敗次舌,可以嘗試添加源:
gem sources --add [https://gems.ruby-china.com/](https://gems.ruby-china.com/)
然后去這里下載一個(gè)腳本并拖到自己項(xiàng)目的根目錄,將腳本中最下方的 myworkspace 和 myscheme 改為自己的項(xiàng)目的 workspace 和 scheme兽愤。然后執(zhí)行腳本彼念,經(jīng)過編譯和oclint的分析,會(huì)生成一份分析報(bào)告浅萧,報(bào)告的格式也可以在腳本最下方的 reportType 填寫(有html逐沙,xml 和 xcode),報(bào)告中就會(huì)列出項(xiàng)目代碼中不符合默認(rèn)規(guī)則的代碼惯殊。
但是酱吝,我要檢查的是 “子類是否重寫父類私有方法”也殖,oclint 沒有這樣的規(guī)則土思,所以此時(shí)就要走自定義規(guī)則的安裝方式了。
去github下載oclint的最新源碼忆嗜,然后安裝以下工具:
brew install cmake ninja
gem install xcpretty
可能會(huì)遇到提醒安裝其他的工具己儒,按提示安裝就好。
然后 cd 到oclint源碼目錄的oclint-scripts文件夾捆毫,執(zhí)行
./make
之后就是漫長的下載闪湾,編譯,我這邊執(zhí)行完需要1個(gè)小時(shí)以上绩卤,大概就是從svn下載LLVM途样,clang的源碼江醇,編譯LLVM,clang和oclint的默認(rèn)規(guī)則何暇。編譯的時(shí)候網(wǎng)絡(luò)一定要好陶夜,不然中間連接svn超時(shí)又要重新開始了。
編譯時(shí)我遇到的幾個(gè)問題:
1.目前github最新版oclint0.15是無法編譯的裆站,會(huì)一直卡在下載LLVM那里条辟,而oclint0.14下載的LLVM是8.0版本的,在Xocde11會(huì)報(bào)錯(cuò)宏胯,因?yàn)閄code11是使用LLVM9.0羽嫡。我的解決方法是手動(dòng)把oclint0.14的LLVM下載版本改成9.0,issue那里已經(jīng)有人提出來肩袍,看這里
這個(gè)問題的補(bǔ)充:后來在github上提了個(gè)issue杭棵,原來是作者把log取消掉了,我誤以為卡住了氛赐,恢復(fù)log看這里
2.編譯完成后颜屠,需要把 /Users/linzhesheng/Desktop/oclint-0.14/build/oclint-release/bin 里的 oclint-0.14 改成 oclint,不過該文件夾已經(jīng)有一個(gè)文件叫oclint鹰祸,需要把原來叫oclint的那個(gè)文件的名字也改了甫窟,改完之后就是這樣子:
不改的話在后面執(zhí)行 oclint-json-compilation-database(具體請(qǐng)看上述下載的腳本) 會(huì)報(bào) Error: OCLint executable file not found. 的錯(cuò)誤。
上面步驟執(zhí)行完之后蛙婴,cd 到 oclint-0.14 目錄粗井,執(zhí)行:
oclint-scripts/scaffoldRule OverrideSuperClassPrivateMethodRule -t ASTVisitor
OverrideSuperClassPrivateMethodRule 是你自定義規(guī)則的名字,執(zhí)行完之后會(huì)生成目錄:
接下來就可以在 cpp 這個(gè)文件寫你的自定義規(guī)則了街图,寫完之后重新執(zhí)行上面的make命令浇衬,重新編譯一次。然而根據(jù)上面體驗(yàn)極差的編譯過程每次調(diào)試重新編譯是不可能的餐济,需要一個(gè)本地調(diào)試的方式:
cd 到 oclint-0.14 目錄
mkdir oclint-xcoderules
cd oclint-xcoderules
然后 cd 到 oclint-xcoderules 目錄耘擂,執(zhí)行
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
得到一個(gè)Xcode工程,這個(gè)工程包含了所有的oclint規(guī)則絮姆,也包含上面自己創(chuàng)建的規(guī)則醉冤,打開這個(gè)工程,選擇自己的規(guī)則篙悯,編譯成功之后就能在Products里看到這個(gè)規(guī)則的動(dòng)態(tài)庫了:
那么如何在Xcode里面調(diào)試這個(gè)動(dòng)態(tài)庫呢蚁阳,需要配置一個(gè)主程序來引導(dǎo)啟動(dòng),把oclint復(fù)制一個(gè)出來鸽照,并改名為oclintExe:
然后工程里面選擇這個(gè)可執(zhí)行文件:
接下來螺捐,配置輸入的用于被檢查的文件:
這里填入的內(nèi)容是:
1里填入的內(nèi)容就是oclint這個(gè)Debug文件的路徑,2里填入的內(nèi)容是用于被檢查的文件的路徑,后面那一長串的內(nèi)容定血,是你編譯這個(gè)被檢查的文件時(shí)得到的編譯依賴赔癌,在這里可以看到:
把 從 “-target” 到結(jié)尾的內(nèi)容復(fù)制過來就行了。
接下來把工程跑起來澜沟,就能用你自定義的規(guī)則檢查這個(gè)文件了届榄,還能打斷點(diǎn),簡(jiǎn)直不要太方便:
不過這里有個(gè)問題倔喂,就是只能輸入一個(gè)被檢查的文件铝条,多個(gè)就不行。(如果有誰知道的話可以留言告訴我下席噩,感謝)那如何只輸入一個(gè).m文件就能檢查多個(gè)類呢班缰,我的解決方法是把多個(gè)類的實(shí)現(xiàn)放在同一個(gè).m文件里面,像這樣:
這樣就能在Xcode調(diào)試的時(shí)候檢查多個(gè)類了悼枢。
準(zhǔn)備工作做完了埠忘,接下來就是編碼實(shí)現(xiàn)自定義規(guī)則。oclint實(shí)現(xiàn)代碼靜態(tài)檢查的原理馒索,就是調(diào)用clang的API把一個(gè)個(gè)源文件生成一個(gè)個(gè)AST莹妒,然后遍歷AST中的每個(gè)節(jié)點(diǎn)傳入各個(gè)規(guī)則,并在遍歷到每個(gè)節(jié)點(diǎn)時(shí)提供回調(diào)绰上,這些便是oclint提供的回調(diào)函數(shù):
例如這個(gè) VisitObjCImplementationDecl 回調(diào)旨怠,是在遍歷的類的實(shí)現(xiàn)節(jié)點(diǎn)的時(shí)候就會(huì)回調(diào),VisitObjCInterfaceDecl在遍歷到類的聲明的時(shí)候會(huì)回調(diào)蜈块,那一個(gè)文件被clang生成的完整的節(jié)點(diǎn)樹是什么呢鉴腻,用這個(gè)命令可以打印出來:
clang -Xclang -ast-dump -fsyntax-only /Users/linzhesheng/Desktop/Goodman/Goodman/ViewController2.m
即使一個(gè)簡(jiǎn)單的文件,也會(huì)生成老長的一串:
這里就能顯示各個(gè)節(jié)點(diǎn)的名字百揭,至于各個(gè)節(jié)點(diǎn)代表什么爽哎,可以直接“節(jié)點(diǎn)名 clang”谷歌搜索出來。
回到我要實(shí)現(xiàn)的規(guī)則器一,我要實(shí)現(xiàn)的檢查具體就是“子類的某個(gè)方法是否和其某個(gè)父類的某個(gè)私有方法重名”课锌。要實(shí)現(xiàn)這個(gè)規(guī)則,我需要的數(shù)據(jù)是:子類的方法祈秕,子類的所有父類渺贤,所有父類的所有私有方法。通過查閱文檔并實(shí)際調(diào)試發(fā)現(xiàn)踢步,ObjCImplementationDecl代表一個(gè)類的實(shí)現(xiàn)癣亚,通過該節(jié)點(diǎn)可以獲取該類的所有數(shù)據(jù)(包括方法丑掺,父類)获印。而這個(gè) ObjCImplementationDecl節(jié)點(diǎn)會(huì)在 VisitObjCImplementationDecl 這個(gè)回調(diào)返回,那么應(yīng)該在這個(gè)回調(diào)里面去實(shí)現(xiàn)檢查的邏輯,基本思路就是遍歷這個(gè)類.m文件的每一個(gè)方法兼丰,然后再判斷方法在這個(gè)類的所有父類的.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) {
// 將信息寫進(jìn) report 里面黍翎,就是執(zhí)行 oclint-json-compilation-database 得到的分析報(bào)告
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();
// 如果是系統(tǒng)類或者庫,無法取到.m文件艳丛,已經(jīng)遍歷到頭匣掸,返回NULL
if (!superNode) {
return NULL;
}
// 嘗試從父類的.m文件取到該方法
ObjCMethodDecl *superMethodInImplementation = superNode->getMethod(method->getSelector(), method->isInstanceMethod());
// 如果能從父類的.m文件取到該方法,則需要判斷該方法在所有父類的.h文件里是否存在氮双,如果不存在碰酝,則該方法為私有方法,返回該方法
if (superMethodInImplementation && !this->methodIsExistInInterfaceAndAllSuperInterfaceK(method, superInterface)) {
return method;
}
// 遞歸往更上層的父類去執(zhí)行
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;
}
// 遞歸往更上層的父類去執(zhí)行
return this->methodIsExistInInterfaceAndAllSuperInterfaceK(method, interface->getSuperClass());
}
寫完之后戴差,直接運(yùn)行調(diào)試送爸,嗯木有問題,接下來就是把這個(gè)規(guī)則應(yīng)用到oclint-json-compilation-database命令中暖释,一開始我是再執(zhí)行make命令編譯一次袭厂,后面發(fā)現(xiàn)直接把規(guī)則對(duì)應(yīng)的動(dòng)態(tài)庫拖到這里就能生效了:
并且我還把系統(tǒng)規(guī)則對(duì)應(yīng)的動(dòng)態(tài)庫都移除掉了。
本來開開心心以后實(shí)現(xiàn)功能了球匕,結(jié)果使用 oclint-json-compilation-database 跑起來之后纹磺,發(fā)現(xiàn)這個(gè)規(guī)則并沒有生效。Xcode調(diào)試的時(shí)候是有效的亮曹,oclint-json-compilation-database執(zhí)行無效爽航,這兩個(gè)的區(qū)別就是Xcode調(diào)試時(shí)我是把所有類的實(shí)現(xiàn)寫到一個(gè).m文件里面,而 oclint-json-compilation-database執(zhí)行時(shí)類的實(shí)現(xiàn)我是分散在各自的.m文件乾忱。那是不是類的實(shí)現(xiàn)分散在各自的.m文件讥珍,檢測(cè)代碼就會(huì)因?yàn)槟撤N原因失效呢?于是我再次把所有類的實(shí)現(xiàn)都寫到一個(gè).m文件窄瘟,不過這次是執(zhí)行 oclint-json-compilation-database 去檢查衷佃,結(jié)果發(fā)現(xiàn)規(guī)則又生效了。后面通過添加 addViolation 把調(diào)試信息寫進(jìn)report發(fā)現(xiàn)蹄葱,在類的實(shí)現(xiàn)分散在各自的.m文件時(shí)氏义,ObjCImplementationDecl *superNode = superInterface->getImplementation() 取出來的 superNode 為 NULL,也就是無法獲取獲取到父類.m文件的信息图云。
于是找有沒有其他方法可以獲取父類的 ObjCImplementationDecl惯悠,發(fā)現(xiàn)文檔中還有這兩個(gè)方法:
,第二個(gè)方法的unsigned ID找不到獲取方式竣况, 使用第一個(gè)方法:
其他的參數(shù)看起來可以從 node 獲取到父類的信息克婶,只有最后一個(gè)參數(shù),找不到獲取的方式。嘗試填入node自己的startLoc情萤,獲取到的 superNode 還是 NULL鸭蛙。網(wǎng)上也搜索不到相關(guān)資料,思路卡在這里了筋岛。娶视。。
是不是 oclint 本身的實(shí)現(xiàn)方式就不支持獲取不同.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
打開這個(gè) compile_commands.json 文件柒傻,發(fā)現(xiàn)是一個(gè)如下數(shù)據(jù)結(jié)構(gòu)組成的數(shù)組:
每條數(shù)據(jù)都對(duì)應(yīng)一個(gè).m文件贪磺。command是這個(gè).m文件的編譯依賴,file 和 directory 組成.m文件的路徑诅愚。因此猜想oclint是依次根據(jù)這個(gè)數(shù)據(jù)結(jié)構(gòu)去拿到每一個(gè).m文件進(jìn)行分析寒锚,不同.m文件之間不存在聯(lián)系。由于對(duì)編譯原理相關(guān)的知識(shí)不是很熟违孝,這里我沒有深究下去刹前。
那么既然無法獲取到另一個(gè).m文件的信息,我能不能先把項(xiàng)目里所有的.m文件的內(nèi)容合到一個(gè).m文件雌桑,再用oclint分析喇喉。因?yàn)樵赬code調(diào)試的時(shí)候就是多個(gè)類的實(shí)現(xiàn)寫在同一個(gè).m文件里面,所以我首先想到的是這種實(shí)現(xiàn)方式校坑。
不過因?yàn)闀?huì)對(duì)項(xiàng)目產(chǎn)生影響拣技,需要把項(xiàng)目copy一份出來。在當(dāng)前項(xiàng)目根目錄執(zhí)行:
mkdir ../copy
cp -af ./ ../copy
cd ../copy
find ./Goodman -name "*.m" | xargs sed 'a\' > ./allFile.m
mv ./allFile.m ./Goodman/allFile.m
這只是創(chuàng)建了一個(gè)包含其他所有.m文件內(nèi)容的.m文件耍目,還要把這個(gè)文件添加到項(xiàng)目的引用中膏斤。具體來說,我們平時(shí)把文件拖入到項(xiàng)目中邪驮,是會(huì)出現(xiàn)這個(gè)東西的:
勾選了 Add to targets 之后莫辨,文件才會(huì)出現(xiàn)在左邊目錄以及這個(gè)編譯選項(xiàng)中:
而如果直接把文件拖到項(xiàng)目的文件夾中,這個(gè)文件跟你項(xiàng)目是沒有任何聯(lián)系的毅访,不會(huì)參與到編譯之中沮榜。那么如何用代碼實(shí)現(xiàn)這個(gè)拖動(dòng)文件到項(xiàng)目的過程呢,這兩篇文章有詳細(xì)的解釋:
用腳本來修改Xcode工程
使用代碼為Xcode工程添加文件
ruby腳本實(shí)現(xiàn)如下:
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文件完成了喻粹,編譯通過蟆融,接下來執(zhí)行 oclint 的腳本,嗯守呜,自定義的規(guī)則又可以生效了型酥。
然而事情還是沒有那么順利滴山憨,當(dāng)我把合成.m文件的腳本在我們實(shí)際項(xiàng)目中執(zhí)行時(shí),合成的.m文件出現(xiàn)了大量的編譯錯(cuò)誤冕末。之前的Demo項(xiàng)目沒出現(xiàn)錯(cuò)誤時(shí)因?yàn)槲募容^少萍歉,且實(shí)現(xiàn)方式比較簡(jiǎn)單侣颂。而我們的項(xiàng)目有大量的.m文件档桃,各種各樣的實(shí)現(xiàn),合成之后就出現(xiàn)很多錯(cuò)誤了憔晒。
那么看看是什么錯(cuò)誤藻肄,有沒有辦法逐個(gè)解決,我只需要想辦法規(guī)避編譯錯(cuò)誤拒担,不需要管代碼具體邏輯對(duì)不對(duì)嘹屯。
(1)Reimplementation of class 'XXX' 和 'XXX.h' file not found
類重復(fù)定義,出現(xiàn)的原因是我們項(xiàng)目的文件夾里有一些已經(jīng)無用的文件从撼,沒有添加到項(xiàng)目引用州弟,靜靜的躺在項(xiàng)目目錄之中,而我是把項(xiàng)目文件夾里所有的.m文件合成低零,這樣這些無用的文件就都參與到編譯中了婆翔。有些是和項(xiàng)目已有的實(shí)現(xiàn)重復(fù)了,有些是.h文件找不到(因?yàn)槲抑缓铣闪?m)掏婶】信總之,把這些無用的文件刪掉就行了雄妥,這也算是一個(gè)意外的收獲最蕾。
(2)Duplicate protocol definition of 'XXX' is ignored
如果一個(gè)文件里引入了同名的協(xié)議溜宽,這些同名的協(xié)議會(huì)全部失效油够,用到這些協(xié)議的時(shí)候也會(huì)報(bào)錯(cuò)。這個(gè)也是之前代碼的漏洞膜钓,解決方式是在同名協(xié)議里加上條件編譯枝秤。
#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'
重復(fù)定義了變量壹粟,出現(xiàn)這個(gè)問題的原因是項(xiàng)目中在.m文件里定義了在這個(gè)文件內(nèi)可訪問的靜態(tài)變量,合成之后宿百,有些靜態(tài)變量名重復(fù)了趁仙。解決方式和第二點(diǎn)一樣,也是為每個(gè)靜態(tài)變量添加條件編譯垦页,不過是使用腳本處理
brew install grep sed
# 將項(xiàng)目所有.m文件內(nèi)容寫到一個(gè).m文件
find ./EasiPass -name "*.m" | xargs sed 'a\' > ./temporaryAllFile.m
# 將 temporaryAllFile.m 同時(shí)包含 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
# 為靜態(tài)變量添加條件編譯
File.open("./constDefine.m", "r").each_line do |line|
# 項(xiàng)目中靜態(tài)變量名以k或K開頭,匹配出變量名(去除開頭的k或K)
$nameArray = line.match /(?<= k).*?(?= =)|(?<= K).*?(?= =)/
f = open("./temporaryAllFile.m","a")
if $nameArray then
# 取到變量名
$name = $nameArray[0]
# 宏定義不能以數(shù)字開頭痊焊,需要作處理
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)變量和宏定義重名
類似這樣:
這種想不到什么好辦法處理了盏袄。
(5)Property 'XXX' attempting to use instance variable '_XXX' declared in super class 'XXX'
類似這樣:
userInfo 這個(gè)屬性是寫在 TestB 的父類的.h文件中忿峻,如果要在 TestB 中使用 _userInfo,就要使用 @synthesize userInfo = _userInfo; 為 TestB 生成這個(gè)成員變量辕羽,這樣寫在正常寫法中沒有問題逛尚,合在同一個(gè).m文件就不知為何報(bào)錯(cuò)了。這種也不好解決刁愿。
所有.m文件合成到同一個(gè).m文件绰寞,我的項(xiàng)目就報(bào)如上5個(gè)錯(cuò)誤,第4铣口,第5點(diǎn)不好解決滤钱,而且也不能保證后面不會(huì)出現(xiàn)其他的錯(cuò)誤∧蕴猓看來這種解決方式是行不通了件缸。
于是重新回到原點(diǎn),思考其他的解決方式叔遂。在 oclint 回調(diào) VisitObjCImplementationDecl 函數(shù)時(shí)他炊,是可以拿到一個(gè)類的.m文件信息的,那么在拿到每個(gè)類的.m文件信息時(shí)已艰,用集合把它保存起來痊末,等到 oclint 檢查最后一個(gè)類,回調(diào) VisitObjCImplementationDecl 函數(shù)時(shí)旗芬,遍歷集合中每個(gè)類去執(zhí)行檢查的邏輯舌胶,根據(jù)父類的類名去集合中取到父類信息,這樣不就解決父類.m文件取不到的問題了嗎疮丛?
按照這種思路幔嫂,一開始我是直接把 VisitObjCImplementationDecl 返回的 ObjCImplementationDecl 保存起來,后面根據(jù)類名取出來誊薄,調(diào)用 ObjCImplementationDecl 的方法的時(shí)候履恩,卻報(bào)了內(nèi)存訪問錯(cuò)誤,可能 oclint 在走完整個(gè)類的檢查之后直接調(diào)用 delete 把 ObjCImplementationDecl 對(duì)象釋放了呢蔫∏行模考慮用智能指針也不一定能解決對(duì)象被釋放的問題,實(shí)現(xiàn)拷貝構(gòu)造函數(shù), ObjCImplementationDecl 有很多屬性指向了其他的對(duì)象片吊,貌似更加復(fù)雜绽昏,因此還是決定自定義類,從 ObjCImplementationDecl 取出必要的信息俏脊,用自定義類保存起來全谤。
在上文“子類的某個(gè)方法是否和其某個(gè)父類的某個(gè)私有方法重名”的檢查邏輯中,是檢查到本類的一個(gè)方法如果能在父類的.m文件中找到爷贫,就去從該父類開始到NSObject的.h文件中找是否有這個(gè)方法认然,如果沒有找到則報(bào)錯(cuò)补憾。實(shí)際上還應(yīng)該把尋找的范圍擴(kuò)大到從該父類開始到NSObject的所有分類的頭文件,.h文件遵循的所有協(xié)議卷员,分類遵循的協(xié)議盈匾,協(xié)議遵循的協(xié)議,因?yàn)樵谶@些范圍里聲明的方法毕骡,子類重寫沒有問題削饵。因此自定義類的數(shù)據(jù)結(jié)構(gòu)如下:
// 方法
class MyMethod {
public:
string name;
bool isInstance;
};
// 協(xié)議
class MyProtocolDecl {
public:
string name; // 協(xié)議名字
vector<MyMethod> instanceMethodArray; // 實(shí)例方法數(shù)組
vector<MyMethod> classMethodArray; // 類方法數(shù)組
vector<MyProtocolDecl *> protocolArray; // 該協(xié)議繼承的協(xié)議數(shù)組
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;
}
}
}
// 遞歸往父協(xié)議尋找
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; // 該分類繼承的協(xié)議數(shù)組
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;
}
}
}
// 遞歸往父協(xié)議尋找
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 相關(guān)的類中獲取必要信息挺峡,生成自己的類的對(duì)象
// 創(chuàng)建方法
MyMethod createMyMethod(clang::ObjCMethodDecl *methodDecl) {
MyMethod myMethod;
if (methodDecl) {
myMethod.name = methodDecl->getNameAsString();
myMethod.isInstance = methodDecl->isInstanceMethod();
}
return myMethod;
}
// 創(chuàng)建分類
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;
}
// 創(chuàng)建協(xié)議
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;
}
// 創(chuàng)建類的聲明
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;
}
// 創(chuàng)建類的實(shí)現(xiàn)
MyImplementationDecl* createMyImplementationDecl(ObjCImplementationDecl *node) {
// 創(chuàng)建 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;
}
然后每次回調(diào) VisitObjCImplementationDecl 函數(shù)時(shí)都將類的信息保存起來:
class OverrideSuperClassPrivateMethodRule : public AbstractASTVisitorRule<OverrideSuperClassPrivateMethodRule>
{
private:
map<string, MyImplementationDecl *> allClassImplementationDeclMap; // 所有類的實(shí)現(xiàn)
map<string, MyInterfaceDecl *> allClassInterfaceDeclMap; // 所有類的聲明
vector<string> checkClassNameArray; // 接受檢查的類
...
bool VisitObjCImplementationDecl(ObjCImplementationDecl *node)
{
this->checkOmyGa(node);
return true;
}
void checkOmyGa(ObjCImplementationDecl *node) {
// 創(chuàng)建類的實(shí)現(xiàn)
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 已經(jīng)有 nodeName 相關(guān)的信息葵孤,說明其所有父類的相關(guān)信息都有了担钮,可直接 return
string nodeName = interfaceNode->getNameAsString();
if (allClassInterfaceDeclMap[nodeName]) {
return;
}
MyInterfaceDecl *interfaceDecl = this->createMyInterfaceDecl(interfaceNode);
allClassInterfaceDeclMap[nodeName] = interfaceDecl;
this->cycleAddMyInterfaceDecl(interfaceNode->getSuperClass());
}
這里有兩點(diǎn)要注意:
(1)map在取一個(gè)不存在的key時(shí)橱赠,取完之后會(huì)把key添加到map里,導(dǎo)致map集合發(fā)生變化箫津,因此把 所有類的實(shí)現(xiàn) 和 接受檢查的類 使用兩個(gè)集合區(qū)分開來狭姨。
(2)單獨(dú)搞一個(gè) 所有類的聲明 的集合,而不是根據(jù) 類的實(shí)現(xiàn) 去取到 類的聲明苏遥。一個(gè)原因是因?yàn)榧偃缡褂谩邦惖膶?shí)現(xiàn) 取 類的聲明 取 父類的聲明 ”(clang就是這樣實(shí)現(xiàn)的)這種方式饼拍,在創(chuàng)建自定義的 類的實(shí)現(xiàn) 的時(shí)候就要根據(jù)這個(gè)鏈條一直創(chuàng)建下去,而很多類具有共同的父類田炭,這樣共同的這些父類都是單獨(dú)的對(duì)象师抄,沒有復(fù)用起來,而通過集合把所有的聲明存儲(chǔ)起來可以解決這個(gè)問題教硫。另一個(gè)原因是很多類只能取到聲明取不到實(shí)現(xiàn)叨吮,比如系統(tǒng)類,各種庫瞬矩。
還有一個(gè)要解決的問題茶鉴,就是上文提到的 “等到 oclint 檢查最后一個(gè)類,回調(diào) VisitObjCImplementationDecl 函數(shù)時(shí)景用,遍歷集合中每個(gè)類去執(zhí)行檢查的邏輯”涵叮,怎么知道 oclint 檢查到了最后一個(gè)類呢, oclint 可沒有提供這樣的回調(diào)伞插。
一開始的想法是將項(xiàng)目中所有的.h文件寫到一個(gè)文件割粮,在通過腳本計(jì)算這個(gè)文件包含了 "@interface : " 的行數(shù),后面發(fā)現(xiàn)不準(zhǔn)媚污,有的項(xiàng)目的.h文件并沒有參與編譯舀瓢。最后解決方案是跑一次oclint,每次執(zhí)行 VisitObjCImplementationDecl 方法計(jì)數(shù)+1杠步,寫到一個(gè)文件中氢伟,等跑完之后就能得到oclint總共檢查了多少個(gè)類榜轿。然后再跑一次oclint,去文件中取這個(gè)值就行了朵锣。這種方案檢查時(shí)間會(huì)大大增加谬盐,不過能取到準(zhǔn)確的類的總數(shù):
class OverrideSuperClassPrivateMethodRule : public AbstractASTVisitorRule<OverrideSuperClassPrivateMethodRule>
{
private:
int currentClassNumber = 0; // 當(dāng)前執(zhí)行到的類
int maxClassNumber = 0; // 接受檢查的類的最大數(shù)量
bool isInitMaxClassNumber = false; // 是否已經(jīng)初始化maxClassNumber
string classNumberPath = "/Users/jz-linzhesheng/Desktop/oclint-release/classNumber.txt";
...
bool VisitObjCImplementationDecl(ObjCImplementationDecl *node)
{
this->checkOmyGa(node);
return true;
}
void checkOmyGa(ObjCImplementationDecl *node) {
// 初始化接受檢查的類的最大數(shù)量
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,則開始計(jì)算類的總數(shù)
if (maxClassNumber == 0) {
this->write_string_to_file(classNumberPath, to_string(currentClassNumber));
return;
}
if (currentClassNumber == maxClassNumber) {
// 執(zhí)行檢查邏輯
}
}
int write_string_to_file(string & file_string, string str )
{
ofstream OsWrite(file_string);
OsWrite<<str;
OsWrite<<endl;
OsWrite.close();
return 0;
}
最后诚些,執(zhí)行檢查的邏輯:
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文件的所有方法(實(shí)例方法和類方法)
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);
}
}
}
// 檢查類的一個(gè)方法是否重寫了父類的私有方法
MyImplementationDecl* methodIsExistInSuperClassPrivateMethod(MyMethod method, MyImplementationDecl *implementationDecl) {
string methodName = method.name;
string nodeName = implementationDecl->name;
// 得到當(dāng)前類.h文件
// MyInterfaceDecl *interfaceDel = implementationDecl->interfaceDel;
// 父類類名
string superNodeName = implementationDecl->superName;
// 得到父類.m文件
MyImplementationDecl *superImplementationDecl = allClassImplementationDeclMap[superNodeName];
// 如果是系統(tǒng)類或者動(dòng)態(tài)庫飞傀,無法取到.m文件,已經(jīng)遍歷到頭诬烹,返回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;
}
// 判斷方法在類的協(xié)議中是否有聲明
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;
}
寫完之后跑到項(xiàng)目中砸烦,規(guī)則總算是生效了。
不過绞吁,還有一個(gè)未解決的問題:
這個(gè) compile_commands.json文件幢痘,如果包含的要分析的文件實(shí)在是太多(具體數(shù)量不明,我的是2000文件以內(nèi)就不會(huì)報(bào)錯(cuò))家破,執(zhí)行 oclint-json-compilation-database 時(shí)颜说,會(huì)報(bào) "OSError: [Errno 7] Argument list too long" 的錯(cuò)誤,參數(shù)太多了汰聋,不能分析门粪。這位老哥寫了一個(gè)python腳本去解決,大致就是把 compile_commands.json 文件拆分成幾個(gè)小的文件烹困,再依次輸入給 oclint-json-compilation-database玄妈,再把生成的 report 合成一個(gè) report。但是髓梅,對(duì)于我的規(guī)則這個(gè)解決方案是無效的拟蜻,因?yàn)椴鸱种螅揖筒荒塬@取不同oclint分析過程中的.m文件的信息了女淑。
這個(gè)問題后面再解決了瞭郑,對(duì)于我的項(xiàng)目來說,排除Pod目錄之后鸭你,就不會(huì)報(bào)這個(gè)錯(cuò)誤屈张。
總算解決了,中間走了很多彎路袱巨,但是最后能解決還是很有成就感的阁谆。