編輯文章參考自iOS 持續(xù)集成系列 - 自動(dòng)化 Code Review
為了保證代碼質(zhì)量,Code Review 是非常重要的一環(huán)。細(xì)到*的位置是否正確,大到代碼的結(jié)構(gòu)是否符合了軟件開(kāi)發(fā)的一些基本原則,都在這項(xiàng)工作的范圍內(nèi)。
受限于現(xiàn)實(shí)情況,大多數(shù)團(tuán)隊(duì)沒(méi)有足夠的時(shí)間進(jìn)行 Code Review,那么只能把一部分 CR 工作交給計(jì)算機(jī)去完成了。我們只需要定下合理的流程,用代碼告訴計(jì)算機(jī)需要做什么,剩下的就交給我們可靠的伙伴吧。
應(yīng)用了自動(dòng)化 Code Review 后,如果你的代碼寫(xiě)得不好,Xcode 會(huì)表示不開(kāi)心。
如果你忽略 Xcode 的心情,那么質(zhì)量管理平臺(tái)會(huì)默默地記錄這一切。
這套東西既幫助開(kāi)發(fā)們寫(xiě)出更高質(zhì)量的的代碼,也給經(jīng)理們對(duì)工程質(zhì)量的評(píng)估提供了一個(gè)切面的支持,同時(shí)只需要花費(fèi)較少的人力維護(hù),聽(tīng)起來(lái)是不是躍躍欲試了呢 ??
流程
整體的工作流程非常簡(jiǎn)單,如圖:
關(guān)鍵點(diǎn)在于本地 Review和遠(yuǎn)端 Review這兩步。前者是提供給開(kāi)發(fā)者一個(gè)即時(shí)的代碼質(zhì)量反饋,以便開(kāi)發(fā)者修改,從而避免在接下來(lái)的遠(yuǎn)端 Review 中得到一個(gè)較低的得分。后者則是為了生成相關(guān)報(bào)表,為項(xiàng)目管理人員跟蹤項(xiàng)目質(zhì)量提供依據(jù)。在很多大公司里,這也是開(kāi)發(fā)者們績(jī)效的參考之一。
剩下的就是一些膠水步驟了,如何讓過(guò)程更自動(dòng)化,就是膠水步驟要做的事。例如利用 WebHook 自動(dòng)觸發(fā)遠(yuǎn)端 Review,利用 Git 的鉤子進(jìn)行增量校驗(yàn)而不是全量校驗(yàn)等。這些我們放在后面聊,先來(lái)看看本地校驗(yàn)的流程。
本地 Review
在本地 Review 環(huán)節(jié),開(kāi)發(fā)者只需要像往常一樣按下 CMD + B,然后只要靜靜地等待進(jìn)度條讀完,滿屏的??就會(huì)精確地指示出某一行的代碼違反了哪條規(guī)則。此時(shí)開(kāi)發(fā)者就可以根據(jù)代碼規(guī)范進(jìn)行對(duì)應(yīng)修改。
從按下按鍵到產(chǎn)生警告主要發(fā)生了這么幾件事情:
- 生成 compile_commands.json 文件
- OCLint 讀取相關(guān)的 Rules,逐個(gè)掃描 compile_commands.json 中的 .m 文件
- OCLint 將生成的報(bào)告展示在 Xcode 上
實(shí)現(xiàn)本地 Review 的核心就是 OCLint 和 compile_commands.json文件
OCLint
工欲善其事,必先利其器
OCLint 是一個(gè)開(kāi)源的,基于 Clang 用 C++ 編寫(xiě)而成的,可以用于 C、C++ 和 Objective-C 的靜態(tài)代碼分析器。它可以在掃描的過(guò)程中動(dòng)態(tài)加載規(guī)則文件(Rules),因此可以實(shí)現(xiàn)非常靈活的,高度可自定義的代碼分析方案。它幾乎可以和大多數(shù)系統(tǒng)無(wú)縫集成,例如 Cmake、Bear、xcodebuild、xctool、Xcode、xcpretty、Jenkins CI、Travis CI 等。你可以在這里找到如何將其和 Xcode 配合使用。
最新版本的 OCLint 已經(jīng)自帶了 71 條 Rules,基本上都是先人寶貴的經(jīng)驗(yàn),比如這條禁用 goto 語(yǔ)句的 Rule,就是來(lái)源于 Edsger W. Dijkstra 1968 年的一篇手稿。
這 71 條 Rules 已經(jīng)可以幫助我們避免一部分因書(shū)寫(xiě)習(xí)慣和語(yǔ)言誤區(qū)而導(dǎo)致的問(wèn)題,但是對(duì)于有完整編碼規(guī)范的公司來(lái)說(shuō)顯然是不夠的。我們必須要自己開(kāi)發(fā) Rules。
幸運(yùn)的是,OCLint 已經(jīng)為我們準(zhǔn)備好了一切。
OCLint 提供了 Clang 和 AST (Abstract Syntax Tree) 的一層封裝,使我們不必對(duì)抽象語(yǔ)法樹(shù)進(jìn)行解析,只需要專注規(guī)則相關(guān)的邏輯開(kāi)發(fā)即可。從其提供的接口中我們可以很明顯地看出這一點(diǎn)。
// 遇到一元操作符
bool VisitUnaryOperator(UnaryOperator *node)
// 遇到二元操作符
bool VisitBinaryOperator(BinaryOperator *node)
// 遇到 Objective-C 的函數(shù)聲明
bool VisitObjCMethodDecl(ObjCMethodDecl *node)
在開(kāi)發(fā)好相關(guān)的規(guī)則后,打包成 dylib,就可以在分析的時(shí)候加載我們自己的 Rule 了。
compile_commands.json
compile_commands.json 是 Clang 定義的一個(gè)規(guī)范,里面存放了一組工作目錄、目標(biāo)文件、需要被執(zhí)行的命令,幫助相關(guān)工具可以獨(dú)立于編譯系統(tǒng)來(lái)將源代碼文件轉(zhuǎn)換為 AST 并做對(duì)應(yīng)的事。
看文件內(nèi)容會(huì)更直觀一些:
[
{
"directory": "/path/to/project/",
"command": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x ...",
"file": "/path/to/project/XXXViewController.m"
},
...
]
OCLint 可以根據(jù) compile_commands.json 中的內(nèi)容,批量檢查源代碼文件。
xcpretty
還有一個(gè)點(diǎn)需要關(guān)注的是,如何生成 compile_commands.json 文件?
最便捷的方式是使用 oclint-xcodebuild 來(lái)生成。首先,利用xcodebuild 生成 xcodebuild.log 文件。
xcodebuild | tee xcodebuild.log
然后利用 oclint-xcodebuild 生成 compile_commands.json
oclint-xcodebuild
截至 Xcode 8.1,這種做法可以正確生成 json 文件。由于 OCLint 團(tuán)隊(duì)已經(jīng)聲稱不再維護(hù) oclint-xcodebuild , 因此可能在未來(lái)的某個(gè) Xcode 版本中這個(gè)方法將不再適用。
另一個(gè)推薦的方法是利用 xcpretty 。
xcpretty 可以一句話生成 json 文件。
xcodebuild | xcpretty -r json-compilation-database --output /path/to/compile_commands.json
使用本地 Review
了解了這些工具后就很容易明白本地自動(dòng)化 Code Review 是如何工作的,使用方式也非常容易理解了:
1.首先在電腦本地安裝好 OCLint 并拿到公司自定義的 Rules 文件
2.在 Xcode 上配置好工程
3.build 工程,等待結(jié)果顯示在 Xcode 上。
附一個(gè)我們團(tuán)隊(duì)的配置腳本供參考:
source ~/.bash_profile
cd ${SRCROOT}
xcodebuild clean
xcodebuild | tee xcodebuild.log
oclint-xcodebuild
oclint-json-compilation-database \
-e Vendor \
-e Pods \
-- \
-max-priority-1 100000 \
-max-priority-2 100000 \
-max-priority-3 100000 \
-report-type xcode \
-R /path/to/rules
遠(yuǎn)端 Review
遠(yuǎn)端 Review 和 本地 Review 大體相似,區(qū)別在與引用構(gòu)建的腳本的對(duì)象從 Xcode 變成了 Jenkins CI ,報(bào)告的展示者從 Xcode 變成了 SonarQube 。其流程是這樣的:
工程師通過(guò) git push 提交代碼
→ Web Hook 觸發(fā) Jenkins 構(gòu)建
→ OCLint 掃描代碼生成PMD格式報(bào)告
→ Sonar-runner 讀取報(bào)告并展現(xiàn)到 SonarQube。
CI 環(huán)境
為了實(shí)現(xiàn)遠(yuǎn)端 Review ,服務(wù)端必須首先有一套 CI 環(huán)境。鑒于 iOS 的特殊性,服務(wù)器必須是 macOS 系統(tǒng)。CI 我們直接選擇開(kāi)源的 Jenkins,質(zhì)量管理平臺(tái)則選用開(kāi)源的 SonarQube。Jenkins 大名鼎鼎大家都非常熟悉了,SonarQube 則相對(duì)少的人了解。
SonarQube 是一個(gè)質(zhì)量管理平臺(tái),在 SonarQube 上,你可以看到一個(gè)項(xiàng)目的代碼行數(shù)、文件數(shù)量、代碼重復(fù)率、違反的代碼規(guī)范、技術(shù)債時(shí)間等等指標(biāo)。SonarQube 對(duì) Java 的支持極度友好,提供了 SonarScanner 可以直接對(duì) Java 源代碼進(jìn)行掃描。Objective-C 就沒(méi)有這么幸運(yùn)了。雖然 SonarQube 也提供了 Objective-C 的報(bào)告展示的支持,但靜態(tài)分析還是得依靠 OCLint 。
Sonnar-Runner
我們?cè)?Jenkins 上運(yùn)行 OCLint 生成了報(bào)告。需要一個(gè)中間人將報(bào)告解析成 SonarQube 可以理解的格式并傳輸?shù)?SonarQube 平臺(tái)。這個(gè)中間人就是 Sonnar-Runner。Sonnar-Runner 在我們的系統(tǒng)中也僅僅扮演這個(gè)搬運(yùn)工的角色。你可以從這里了解到如何在 Jenkins 上安裝和使用 Sonnar-Runner。
Sonnar-Runner 只能解析 PMD 格式的報(bào)告,因此我們?cè)谑褂?OCLint 分析代碼后,需要將報(bào)告格式輸出為 PMD 格式。
oclint -report-type pmd -o ./report.xml
Rules in Sonar
SonarQube 有一套規(guī)則,將代碼問(wèn)題按照嚴(yán)重程度分為 5 個(gè)等級(jí),不同等級(jí)的問(wèn)題會(huì)以不同權(quán)重影響到項(xiàng)目質(zhì)量評(píng)分。這套規(guī)則和 OCLint 生成的報(bào)告中的 Rule name 必須要一一對(duì)應(yīng),SonarQube 才能正確將報(bào)告中的問(wèn)題歸類并評(píng)分。
如果你使用 OCLint 原生的 Rules 來(lái)檢查代碼,只需要在 SonarQube 上安裝 SonarQube Plugin for Objective C 插件,相關(guān)的報(bào)告就會(huì)被正確識(shí)別了。
如果是使用了自行開(kāi)發(fā)的 Rules ,只需要 Clone 上述插件,并在profile-oclint.xml 和 rules.txt 中添加相關(guān)的 rule name ,然后打包并將這個(gè)插件安裝到 SonarQube 上即可。
舉個(gè)例子:
當(dāng)我們用自行開(kāi)發(fā)的 Rule 檢查完代碼后,生成了report.xml,內(nèi)容如下:
其中 binary operator space (HT_iOS_Coding_style 2.8) 是我們定義的錯(cuò)誤rule name。在 SonarQube 上,也必須對(duì)應(yīng)有這么一條 rule 的 name,才能正確識(shí)別這個(gè)錯(cuò)誤。
此時(shí)我們只需要在上述插件的 rules.txt 中添加一段
在上述插件的 profile-oclint.xml 中添加另外一段代碼
然后將這個(gè)插件打包并安裝到 SonarQube 上,SonarQube 就可以正確識(shí)別我們的問(wèn)題并分類了。
使用遠(yuǎn)端 Review
在使用前,一定要確保你的 macOS 服務(wù)器已經(jīng)安裝好了最新版的 Xocde、OCLint、Jenkins、sonnar-runner,安裝好 Jenkins 的相關(guān)插件,并將自定義的 Rule 放置在服務(wù)器上(如果有的話)。
檢查并生成報(bào)告
在 Jenkins 上新建工程并配置好Git、構(gòu)建觸發(fā)器等其他內(nèi)容。在構(gòu)建步驟中添加一步 Execute Shell ,填入下述腳本
cd YourProjectDir
xcodebuild clean
xcodebuild -workspace MyProject.xcworkspace -scheme HTMarket -sdk iphonesimulator | tee xcodebuild.log | xcpretty
oclint-xcodebuild
oclint-json-compilation-database -e Pods \
-v \
-- \
-max-priority-1 100000 \
-max-priority-2 100000 \
-max-priority-3 100000 \
-report-type pmd \
-R /path/to/diy-rules \
-o /path/to/report.xml
腳本大致和本地 Review 一致,有三個(gè)地方需要注意一下。
1.xcodebuild 命令添加了 -sdk iphonesimulator參數(shù),以避免 build 需要 Code Sign 的問(wèn)題。
2.-report-type pmd 輸出格式必須為 pmd 格式
3.-o /path/to/report.xml 注意輸出報(bào)告的路徑,下一步sonnar-runner 讀取時(shí)會(huì)用到。
讀取到 SonarQube
在上一步的下方再添加一步 Invoke Standalone SonarQube Analysis,選擇好你的 sonnar-runner。并在 Analysis Properties 中添加如下配置:(如果沒(méi)有這一項(xiàng),你可能需要安裝 SonarQube 相關(guān)的插件。)
sonar.projectKey=YOUR_PROJECT_NAME
sonar.projectName=YOUR_PROJECT_NAME
sonar.projectVersion=1.0
sonar.language=objc
sonar.projectDescription=YOUR_PROJECT_DESCRIPTION
# Path to source directories
sonar.sources=/path/to/source/directories
# Xcode project configuration (.xcodeproj or .xcworkspace)
# -> If you have a project: configure only sonar.objectivec.project
# -> If you have a workspace: configure sonar.objectivec.workspace and sonar.objectivec.project
# and use the later to specify which project(s) to include in the analysis (comma separated list)
sonar.objectivec.project=YOUR_PROJECT_NAME.xcodeproj
sonar.objectivec.workspace= YOUR_PROJECT_NAME.xcworkspace
# Scheme to build your application
sonar.objectivec.appScheme=YOUR_PROJECT_NAME
sonar.sourceEncoding=UTF-8
# OCLint report generated by run-sonar.sh is stored in sonar-reports/oclint.xml
# Change it only if you generate the file on your own
sonar.objectivec.oclint.report=YOUR_REPORT_FILE_PATH
注意看注釋并修改為你項(xiàng)目的值:
YOUR_PROJECT_NAME
YOUR_PROJECT_DESCRIPTION
YOUR_REPORT_FILE_PATH
一切順利的話,在 Jenkins 上立即構(gòu)建,你就可以在你的 Sonar 平臺(tái)上看到代碼質(zhì)量報(bào)告了。
配合好構(gòu)建觸發(fā)器 和 Git 平臺(tái)的 WebHook 功能,就可以在開(kāi)發(fā)提交代碼或者合并分支等關(guān)鍵點(diǎn)自動(dòng)觸發(fā)構(gòu)建了。
Troubleshooting
為什么生成的 compile_commands.json 為空
檢查 log 是否為空,如果 log 為空則代表 build 失敗。排除失敗原因后即可正常生成。
Jenkins 構(gòu)建遇到了如下問(wèn)題
Code signing is required for product type 'Application' in SDK 'iOS 10.0'
遇到這樣的情況,是因?yàn)闃?gòu)建了 Release 版本,且項(xiàng)目在 Xcode8+ 上開(kāi)啟了 Automatic Code Sign。解決方法如下:
- 如果只需要檢查代碼規(guī)范,則在 xcodebuild 命令后添加 -sdk iphonesimulator 參數(shù)指明以 Debug 方式構(gòu)建即可。
- 如果希望構(gòu)建 Release 版本,那么關(guān)閉自動(dòng)簽名,在 CI 系統(tǒng)上手動(dòng)配置證書(shū)和Proversion Profile。或者保留自動(dòng)簽名,參考這個(gè)回答用 sed 命令在構(gòu)建前修改相關(guān)配置。
The End.