工程代碼質(zhì)量,一個永恒的話題。好的質(zhì)量的好處不言而喻,團(tuán)隊成員間除了保持統(tǒng)一的風(fēng)格和較高的自我約束力之外,還需要一些工具來統(tǒng)計分析代碼質(zhì)量問題。
本文就是針對 OC 項目,提出的一個思路和實踐步驟的記錄,最后形成了一個可以直接用的腳本。如果覺得文章篇幅過長,則直接可以下載腳本
OCLint is a static code analysis tool for improving quality and reducing defects by inspecting C, C++ and Objective-C code and looking for potential problems ...
從官方的解釋來看,它通過檢查 C、C++、Objective-C 代碼來尋找潛在問題,來提高代碼質(zhì)量并減少缺陷的靜態(tài)代碼分析工具
一、OCLint 的下載和安裝
有3種方式安裝,分別為 Homebrew、源代碼編譯安裝、下載安裝包安裝。
區(qū)別:
如果需要自定義 Lint 規(guī)則,則需要下載源碼編譯安裝
如果僅僅是使用自帶的規(guī)則來 Lint,那么以上3種安裝方式都可以
1. Homebrew 安裝
在安裝前,確保安裝了 homebrew。步驟簡單快捷
brew tap oclint/formulae?
brew install oclint
2. 安裝包安裝
進(jìn)入 OCLint 在 Github 中的地址,選擇 Release。選擇最新版本的安裝包(目前最新版本為:oclint-0.13.1-x86_64-darwin-17.4.0.tar.gz)
解壓下載文件。將文件存放到一個合適的位置。(比如我選擇將這些需要的源代碼存放到 Document 目錄下)
在終端編輯當(dāng)前環(huán)境的配置文件,我使用的是 zsh,所以編輯 .zshrc 文件。(如果使用系統(tǒng)的終端則編輯 .bash_profile 文件)
OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH
將配置文件 source 一下。
source .zshrc // 如果你使用系統(tǒng)的終端則執(zhí)行 soucer .bash_profile
驗證是否安裝成功。在終端輸入oclint --version
3. 源碼編譯安裝
homebrew 安裝 CMake 和 Ninja 這2個編譯工具
brew install cmake ninja
進(jìn)入 Github 搜索 OCLint,clone 源碼
gc https://github.com/oclint/oclint
或者:git clone https://github.com/oclint/oclint.git
進(jìn)入 oclint-scripts 目錄,執(zhí)行 ./make 命令。這一步的時間非常長。會下載 oclint-json-compilation-database、oclint-xcodebuild、llvm 源碼以及 clang 源碼。并進(jìn)行相關(guān)的編譯得到 oclint。且必須使用翻墻環(huán)境不然會報 timeout。如果你的電腦支持翻墻環(huán)境,但是在終端下不支持翻墻,可以查看我的這篇文章
./make
編譯結(jié)束,進(jìn)入同級 build 文件夾,該文件夾下的內(nèi)容即為 oclint??梢钥吹絙uild/oclint-release。方式2下載的安裝包的內(nèi)容就是該文件夾下的內(nèi)容。
cd 到根目錄,編輯環(huán)境文件,比如我 zsh 對應(yīng)的 .zshrc 文件。編輯下面的內(nèi)容
? OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release
? export PATH=$OCLint_PATH/bin:$PATH
source 下 .zshrc 文件
source .zshrc // source .bash_profile
進(jìn)入oclint/build/oclint-release目錄執(zhí)行腳本
cp ~/Documents/oclint/build/oclint-release/bin/oclint* /usr/local/bin/
ln -s ~/Documents/oclint/build/oclint-release/lib/oclint /usr/local/lib
ln -s ~/Documents/oclint/build/oclint-release/lib/clang /usr/local/lib
這里使用 ln -s,把 lib 中的 clang 和 oclint 鏈接到 /usr/local/bin 目錄下。這樣做的目的是為了后面如果編寫了自己創(chuàng)建的 lint 規(guī)則,不必要每次更新自定義的 rule 庫,必須手動復(fù)制到 /usr/local/bin 目錄下。
驗證下 OCLint 是否安裝成功。輸入 oclint --version

注意:如果你采用源碼編譯的時候直接 clone 官方的源碼會有問題,編譯不過,所以提供了一個可以編譯過的版本。分支切換到 llvm-7.0。
4. xcodebuild 的安裝
xcode 下載安裝好就已經(jīng)成功安裝了
5. xcpretty 的安裝
先決條件,你的機(jī)器已經(jīng)安裝好了 Ruby gem.
gem install xcpretty
二、 自定義 Rule
OClint 提供了 70+ 項的檢查規(guī)則,你可以直接去使用。但是某些時候你需要制作自己的檢測規(guī)則,接下來就說說如何自定義 lint 規(guī)則。
1、進(jìn)入 ~/Document/oclint 目錄,執(zhí)行下面的腳本
oclint-scripts/scaffoldRule CustomLintRules -t ASTVisitor
其中,CustomLintRules就是定義的檢查規(guī)則的名字,ASTVisitor就是你繼承的 lint 規(guī)則
可以繼承的規(guī)則有:ASTVisitor、SourceCodeReader、ASTMatcher。
2、執(zhí)行上面的腳本,會生成下面的文件
Documents/oclint/oclint-rules/rules/custom/CustomLintRulesRule.cpp
Documents/oclint/oclint-rules/test/custom/CustomLintRulesRuleTest.cpp
3、要方便的開發(fā)自定義的 lint 規(guī)則,則需要生成一個 xcodeproj 項目。切換到項目根目錄,也就是 Documents/oclint,執(zhí)行下面的命令
mkdir Lint-XcodeProject
cd Lint-XcodeProject
touch generate-lint-rules.sh
chmod +x generate-lint-rules.sh
給上面的 generate-lint-rules.sh 里面添加下面的腳本
#! /bin/sh -ecmake -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
4、執(zhí)行 generate-lint-rules.sh 腳本(./generate-lint-rules.sh)。如果出現(xiàn)下面的 Log 則說明生成 xcodeproj 項目成功


5、打開步驟4生成的項目,看到有很多文件夾,代表 oclint 自帶的 lint 規(guī)則,我們自定義的 lint 規(guī)則在最下面。

關(guān)于如何自定義 lint 規(guī)則的具體還沒有深入研究,這里給個例子
<details>
<summary>點擊查看示例代碼</summary>
#include"oclint/AbstractASTVisitorRule.h"
#include"oclint/RuleSet.h"
using namespace std;
using namespace clang;
using namespace oclint;
#include <iostream>
class MVVMRule : public AbstractASTVisitorRule<MVVMRule>
{
public:
? ? virtual conststring name() const override {
? ? ? ? return"Property in 'ViewModel' Class interface should be readonly.";
? ? }
? ? virtual intpriority() const override {
? ? ? ? return3;
? ? }
? ? virtual conststring category() const override {
? ? ? ? return"mvvm";
? ? }
? ? virtual unsigned intsupportedLanguages() const override {
? ? ? ? return LANG_OBJC;
? ? }
#ifdef DOCGEN
? ? virtual conststd::string since() const override {
? ? ? ? return"0.18.10";
? ? }
?virtual conststd::string description() const override {
? ? ? ? return"Property in 'ViewModel' Class interface should be readonly.";
? ? }
? ? virtual conststd::string example() const override {
? ? ? ? returnR"rst( .. code-block:: cpp @interface FooViewModel : NSObject // This is a "ViewModel" Class. @property (nonatomic, strong) NSObject *bar; // should be readonly. @end )rst";
? ? }
? ? virtual conststd::string fileName() const override {
? ? ? ? return"MVVMRule.cpp";
? ? }
#endif
? ? virtual voidsetUp() override {}
? ? virtual voidtearDown() override {}
? ? /* Visit ObjCImplementationDecl */
? ? boolVisitObjCImplementationDecl(ObjCImplementationDecl *node) {
? ? ? ? ObjCInterfaceDecl *interface = node->getClassInterface();
? ? ? ? bool isViewModel = interface->getName().endswith("ViewModel");
? ? ? ? if (!isViewModel) {
? ? ? ? ? ? returnfalse;
? ? ? ? }
? ? ? ? for (auto property = interface->instprop_begin(),
? ? ? ? ? ? propertyEnd = interface->instprop_end(); property != propertyEnd; property++)
? ? ? ? {
? ? ? ? ? ? clang::ObjCPropertyDecl *propertyDecl = (clang::ObjCPropertyDecl *)*property;
? ? ? ? ? ? if (propertyDecl->getName().startswith("UI")) {
? ? ? ? ? ? ? ? addViolation(propertyDecl, this);
? ? ? ? ? ? }
? ? ? ? ? ? auto attrs = propertyDecl->getPropertyAttributes();
? ? ? ? ? ? bool isReadwrite = (attrs & ObjCPropertyDecl::PropertyAttributeKind::OBJC_PR_readwrite) >0;
? ? ? ? ? ? if (isReadwrite && isViewModel) {
? ? ? ? ? ? ? ? addViolation(propertyDecl, this);
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? returntrue;
? ? }
};
staticRuleSetrules(newMVVMRule());
</details>
6、修改自定義規(guī)則后就需要編譯。
成功后在 Products 目錄下會看到對應(yīng)名稱的 CustomLintRulesRule.dylib 文件,就需要復(fù)制到 /Documents/oclint/oclint-release/lib/oclint/rules。講道理,生成新的 lint rule 文件,需要把新的 dylib 文件復(fù)制到 /usr/local/lib。因為我們在源代碼安裝的第4部,設(shè)置了 ln -s 鏈接,所以不需要每次復(fù)制到相應(yīng)文件夾。
但是還是比較麻煩,每次都需要編譯新的 lint rule 之后需要將相應(yīng)的 dylib 文件復(fù)制到源代碼目錄下的 oclint-release/lib/oclint/rules 目錄下,本著「可以偷懶絕不動手」的原則,在自定義的 rule 的 target 中,在 Build Phases 選項下 CMake PostBuild Rules 中的腳本下將下面的代碼復(fù)制進(jìn)去
cp /Users/liubinpeng/Documents/oclint/Lint-XcodeProject/rules.dl/Debug/libCustomLintRulesRule.dylib /Users/liubinpeng/Documents/oclint/build/oclint-release/lib/oclint/rules/libCustomLintRulesRule.dylib
7、規(guī)則限定的3個類說明:
RuleBase
?|
?|-AbstractASTRuleBase
?|? ? ? |_ AbstractASTVisitorRule
?|? ? ? ? ? ? |_AbstractASTMatcherRule
?|
?|-AbstractSourceCodeReaderRule
AbstractSourceCodeReaderRule:eachLine 方法,讀取每行的代碼,如果想編寫的規(guī)則是需要針對每行的代碼內(nèi)容,則可以繼承自該類
AbstractASTVisitorRule:可以訪問 AST 上特定類型的所有節(jié)點,可以檢查特定類型的所有節(jié)點是遞歸實現(xiàn)的。在?apply?方法內(nèi)可以看到代碼實現(xiàn)。開發(fā)者只需要重載 bool visit* 方法來訪問特定類型的節(jié)點。其值表明是否繼續(xù)遞歸檢查
AbstractASTMatcherRule:實現(xiàn) setUpMatcher 方法,在方法中添加 matcher,當(dāng)檢查發(fā)現(xiàn)匹配結(jié)果時會調(diào)用 callback 方法。然后通過 callback 方法來繼續(xù)對匹配到的結(jié)果進(jìn)行處理
8、知其所以然
oclint 依賴與源代碼的語法抽象樹(AST)。開源 clang 是 oclint 獲的語法抽象樹的依賴工具。你如果想對 AST 有個了解,可以查看這個視頻
如果想查看某個文件的 AST 結(jié)構(gòu),你可以進(jìn)入該文件的命令行,然后執(zhí)行下面的腳本
clang -Xclang -ast-dump -fsyntax-only main.m
三、 Homebrew 方式安裝的 oclint 如何使用自定義規(guī)則
查看 OCLint 安裝路徑
which oclint
// 輸出:/usr/local/bin/oclint
ls -al? /usr/local/bin/oclint
// 輸出:本機(jī)安裝路徑
把上面生成的新的 lint rule 下的 dylib 文件復(fù)制到步驟1得到的額本機(jī)安裝路徑下
四、 使用 oclint
在命令行中使用
如果項目使用了 Cocopod,則需要指定 -workspace xxx.workspace
每次編譯之前需要 clean
實操:
進(jìn)入項目cd /Workspace/Native/iOS/lianhua
查看項目基本信息xcodebuild -list
//輸出
information about project "BridgeLabiPhone":
? Targets:
? ? ? BridgeLabiPhone
? ? ? lint
? Build Configurations:
? ? ? Debug
? ? ? Release
? If no build configuration is specified and -scheme is not passed then "Release" is used.
? Schemes:
? ? ? BridgeLabiPhone
? ? ? lint
編譯xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json編譯成功后,會在項目的文件夾下出現(xiàn) compile_commands.json 文件
生成 html 報表oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html看到有報錯,但是報錯信息太多了,不好定位,利用下面的腳本則可以將報錯信息寫入 log 文件,方便查看oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html 2>&1 | tee 1.log報錯信息是:oclint: error: one compiler command contains multiple jobs:查找資料,解決方案如下
將 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 設(shè)置為?NO
在 podfile 中 target 'xx' do 前面添加下面的腳本
post_install do |installer|
? installer.pods_project.targets.each do |target|
? ? ? target.build_configurations.each do |config|
? ? ? ? ? config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"
? ? ? end
? end
end
然后繼續(xù)嘗試編譯,發(fā)現(xiàn)還是報錯,但是報錯信息改變了,如下

看到報錯信息是默認(rèn)的警告數(shù)量超過限制,則 lint 失敗。事實上 lint 后可以跟參數(shù),所以我們修改腳本如下
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999
生成了 lint 的結(jié)果,查看 html 文件可以具體定位哪個代碼文件,哪一行哪一列有什么問題,方便修改。

如果項目工程太大,整個 lint 會比較耗時,所幸 oclint 支持針對某個代碼文件夾進(jìn)行 lint
oclint-json-compilation-database -i 需要靜態(tài)分析的文件夾或文件 -- -report-type html -o oclintReport.html 其他的參數(shù)
參數(shù)說明
| 名稱 | 描述 | 默認(rèn)閾值 |
| ----------------------- | ---------------------------- | ---- |
| CYCLOMATIC_COMPLEXITY | 方法的循環(huán)復(fù)雜性(圈負(fù)責(zé)度) | 10 |
| LONG_CLASS | C類或Objective-C接口,類別,協(xié)議和實現(xiàn)的行數(shù) | 1000 |
| LONG_LINE | 一行代碼的字符數(shù) | 100 |
| LONG_METHOD | 方法或函數(shù)的行數(shù) | 50 |
| LONG_VARIABLE_NAME | 變量名稱的字符數(shù) | 20 |
| MAXIMUM_IF_LENGTH |if語句的行數(shù) | 15 |
| MINIMUM_CASES_IN_SWITCH | switch語句中的case數(shù) | 3 |
| NPATH_COMPLEXITY | 方法的NPath復(fù)雜性 | 200 |
| NCSS_METHOD | 一個沒有注釋的方法語句數(shù) | 30 |
| NESTED_BLOCK_DEPTH | 塊或復(fù)合語句的深度 | 5 |
| SHORT_VARIABLE_NAME | 變量名稱的字符數(shù) | 3 |
| TOO_MANY_FIELDS | 類的字段數(shù) | 20 |
| TOO_MANY_METHODS | 類的方法數(shù) | 30 |
| TOO_MANY_PARAMETERS | 方法的參數(shù)數(shù) | 10 |
在 Xcode 中使用
在項目的 TARGETS 下面,點擊下方的 "+" ,選擇 cross-platform 下面的 Aggregate。輸入名字,這里命名為 Lint

選擇對應(yīng)的 TARGET -> lint。在 Build Phases 下 Run Script 下寫下面的腳本代碼
export LC_CTYPE=en_US.UTF-8
cd ${SRCROOT}
xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json && oclint-json-compilation-database -e Pods -- -report-type xcode
說明,雖然有時候沒有編譯通過,但是看到如下圖的關(guān)于代碼相關(guān)的 warning 則達(dá)到目的了。

lint 結(jié)果如下,根據(jù)相應(yīng)的提示信息對代碼進(jìn)行調(diào)整。當(dāng)然這只是一種參考,不一定要采納 lint 給的提示。

五、腳本化
每次都在終端命令行去寫 lint 的腳本,效率很低,所以想做成 shell 腳本。需要的同學(xué)直接直接拷貝進(jìn)去,直接在工程的根目錄下使用,我這邊是一個 Cocopod 工程。拿走拿走別客氣
腳本文件多次保存,Confluence提示:Unable to communicate with server. Saving is not possible at the moment. 故放附件文件中,詳情請見下面腳本:
腳本化
每次都在終端命令行去寫 lint 的腳本,效率很低,所以想做成 shell 腳本。需要的同學(xué)直接直接拷貝進(jìn)去,直接在工程的根目錄下使用,我這邊是一個 Cocopod 工程。拿走拿走別客氣
#!/bin/bash
COLOR_ERR="\033[1;31m"? ? #出錯提示
COLOR_SUCC="\033[0;32m"? #成功提示
COLOR_QS="\033[1;37m"? #問題顏色
COLOR_AW="\033[0;37m"? #答案提示
COLOR_END="\033[1;34m"? ? #顏色結(jié)束符
# 尋找項目的 ProjectName
function searchProjectName () {
# maxdepth 查找文件夾的深度
? find . -maxdepth 1 -name "*.xcodeproj"
}
function oclintForProject () {
? ? # 預(yù)先檢測所需的安裝包是否存在
? ? if which xcodebuild 2>/dev/null; then
? ? ? ? echo 'xcodebuild exist'
? ? else
? ? ? ? echo '??? 連 xcodebuild 都沒有安裝,玩雞毛?。????'
? ? fi
? ? if which oclint 2>/dev/null; then
? ? ? ? echo 'oclint exist'
? ? else
? ? ? ? echo '?? 完蛋了你,玩 oclint 卻不安裝嗎,你要鬧哪樣 ??'
? ? ? ? echo '?? 乖乖按照博文:https://github.com/FantasticLBP/knowledge-kit/blob/master/第一部分%20iOS/1.63.md 安裝所需環(huán)境 ??'
? ? fi
? ? if which xcpretty 2>/dev/null; then
? ? ? ? echo 'xcpretty exist'
? ? else
? ? ? ? gem install xcpretty
? ? fi
? ? # 指定編碼
? ? export LANG="zh_CN.UTF-8"
? ? export LC_COLLATE="zh_CN.UTF-8"
? ? export LC_CTYPE="zh_CN.UTF-8"
? ? export LC_MESSAGES="zh_CN.UTF-8"
? ? export LC_MONETARY="zh_CN.UTF-8"
? ? export LC_NUMERIC="zh_CN.UTF-8"
? ? export LC_TIME="zh_CN.UTF-8"
? ? export xcpretty=/usr/local/bin/xcpretty # xcpretty 的安裝位置可以在終端用 which xcpretty找到
? ? searchFunctionName=`searchProjectName`
? ? path=${searchFunctionName}
? ? # 字符串替換函數(shù)。//表示全局替換 /表示匹配到的第一個結(jié)果替換。
? ? path=${path//.\//}? # ./BridgeLabiPhone.xcodeproj -> BridgeLabiPhone.xcodeproj
? ? path=${path//.xcodeproj/} # BridgeLabiPhone.xcodeproj -> BridgeLabiPhone
? ? myworkspace=$path".xcworkspace" # workspace名字
? ? myscheme=$path? # scheme名字
? ? # 清除上次編譯數(shù)據(jù)
? ? if [ -d ./derivedData ]; then
? ? ? ? echo -e $COLOR_SUCC'-----清除上次編譯數(shù)據(jù)derivedData-----'$COLOR_SUCC
? ? ? ? rm -rf ./derivedData
? ? fi
? ? # xcodebuild clean
? ? xcodebuild -scheme $myscheme -workspace $myworkspace clean
? ? # # 生成編譯數(shù)據(jù)
? ? xcodebuild -scheme $myscheme -workspace $myworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json
? ? if [ -f ./compile_commands.json ]; then
? ? ? ? echo -e $COLOR_SUCC'編譯數(shù)據(jù)生成完畢??????'$COLOR_SUCC
? ? else
? ? ? ? echo -e $COLOR_ERR'編譯數(shù)據(jù)生成失敗??????'$COLOR_ERR
? ? ? ? return -1
? ? fi
? ? # 生成報表
? ? oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \
? ? -rc LONG_LINE=200 \
? ? -disable-rule ShortVariableName \
? ? -disable-rule ObjCAssignIvarOutsideAccessors \
? ? -disable-rule AssignIvarOutsideAccessors \
? ? -max-priority-1=100000 \
? ? -max-priority-2=100000 \
? ? -max-priority-3=100000
? ? if [ -f ./oclintReport.html ]; then
? ? ? ? rm compile_commands.json
? ? ? ? echo -e $COLOR_SUCC'??分析完畢??'$COLOR_SUCC
? ? else
? ? ? ? echo -e $COLOR_ERR'??分析失敗??'$COLOR_ERR
? ? ? ? return -1
? ? fi
? ? echo -e $COLOR_AW'將為大爺自動打開 lint 的分析結(jié)果'$COLOR_AW
? ? # 用 safari 瀏覽器打開 oclint 的結(jié)果
? ? open -a "/Applications/Safari.app" oclintReport.html
}
oclintForProject
參考:
OCLint:https://oclint-docs.readthedocs.io/en/stable/
iOS 工程自動化 - OCLint:http://www.cocoachina.com/articles/20181
iOS持續(xù)集成(CI)——OCLint靜態(tài)代碼分析:https://cloud.tencent.com/developer/article/1438626
OCLint 自定義規(guī)則:http://www.itdecent.cn/p/383d4166bec5
OCLint靜態(tài)代碼檢測實踐:https://juejin.im/post/6844904017424809998
iOS 持續(xù)集成系列 - 自動化 Code Review:http://www.itdecent.cn/p/c8b3b515ccf3
OCLint靜態(tài)代碼分析:https://chenjiangchuan.gitbooks.io/ios-documentation/content/oclintjing-tai-dai-ma-fen-xi.html