工程程式碼質量,一個永恆的話題。好的質量的好處不言而喻,團隊成員間除了保持統一的風格和較高的自我約束力之外,還需要一些工具來統計分析程式碼質量問題。
本文就是針對 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 程式碼來尋找潛在問題,來提高程式碼質量並減少缺陷的靜態程式碼分析工具
OCLint 的下載和安裝
有3種方式安裝,分別為 Homebrew、原始碼編譯安裝、下載安裝包安裝。 區別:
- 如果需要自定義 Lint 規則,則需要下載原始碼編譯安裝
- 如果僅僅是使用自帶的規則來 Lint,那麼以上3種安裝方式都可以
1. Homebrew 安裝
在安裝前,確保安裝了 homebrew。步驟簡單快捷
brew tap oclint/formulae
brew install oclint
複製程式碼
2. 安裝包安裝
- 進入 OCLint 在 Github 中的地址,選擇 Release。選擇最新版本的安裝包(目前最新版本為:oclint-0.13.1-x86_64-darwin-17.4.0.tar.gz)
- 解壓下載檔案。將檔案存放到一個合適的位置。(比如我選擇將這些需要的原始碼存放到 Document 目錄下)
- 在終端編輯當前環境的配置檔案,我使用的是 zsh,所以編輯 .zshrc 檔案。(如果使用系統的終端則編輯 .bash_profile 檔案)
OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release export PATH=$OCLint_PATH/bin:$PATH 複製程式碼
- 將配置檔案 source 一下。
source .zshrc // 如果你使用系統的終端則執行 soucer .bash_profile 複製程式碼
- 驗證是否安裝成功。在終端輸入
oclint --version
3. 原始碼編譯安裝
-
homebrew 安裝 CMake 和 Ninja 這2個編譯工具
brew install cmake ninja 複製程式碼
-
進入 Github 搜尋 OCLint,clone 原始碼
gc https://github.com/oclint/oclint 複製程式碼
-
進入 oclint-scripts 目錄,執行 ./make 命令。這一步的時間非常長。會下載 oclint-json-compilation-database、oclint-xcodebuild、llvm 原始碼以及 clang 原始碼。並進行相關的編譯得到 oclint。且必須使用翻牆環境不然會報 timeout。如果你的電腦支援翻牆環境,但是在終端下不支援翻牆,可以檢視我的這篇文章
./make 複製程式碼
-
編譯結束,進入同級 build 資料夾,該資料夾下的內容即為 oclint。可以看到
build/oclint-release
。方式2下載的安裝包的內容就是該資料夾下的內容。 -
cd 到根目錄,編輯環境檔案,比如我 zsh 對應的 .zshrc 檔案。編輯下面的內容
OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release export PATH=$OCLint_PATH/bin:$PATH 複製程式碼
-
source 下 .zhsrc 檔案
source .zshrc // source .bash_profile 複製程式碼
-
進入
oclint/build/oclint-release
目錄執行指令碼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 目錄下。這樣做的目的是為了後面如果編寫了自己建立的 lint 規則,不必要每次更新自定義的 rule 庫,必須手動複製到 /usr/local/bin 目錄下。
-
驗證下 OCLint 是否安裝成功。輸入 oclint --version
注意:如果你採用原始碼編譯的時候直接 clone 官方的原始碼會有問題,編譯不過,所以提供了一個可以編譯過的版本。分支切換到 llvm-7.0。
4. xcodebuild 的安裝
xcode 下載安裝好就已經成功安裝了
5. xcpretty 的安裝
先決條件,你的機器已經安裝好了 Ruby gem.
gem install xcpretty
複製程式碼
二、 自定義 Rule
OClint 提供了 70+ 項的檢查規則,你可以直接去使用。但是某些時候你需要製作自己的檢測規則,接下來就說說如何自定義 lint 規則。
-
進入 ~/Document/oclint 目錄,執行下面的指令碼
oclint-scripts/scaffoldRule CustomLintRules -t ASTVisitor 複製程式碼
其中,CustomLintRules 就是定義的檢查規則的名字, ASTVisitor 就是你繼承的 lint 規則
可以繼承的規則有:ASTVisitor、SourceCodeReader、ASTMatcher。
-
執行上面的指令碼,會生成下面的檔案
- Documents/oclint/oclint-rules/rules/custom/CustomLintRulesRule.cpp
- Documents/oclint/oclint-rules/test/custom/CustomLintRulesRuleTest.cpp
-
要方便的開發自定義的 lint 規則,則需要生成一個 xcodeproj 專案。切換到專案根目錄,也就是 Documents/oclint,執行下面的命令
mkdir Lint-XcodeProject cd Lint-XcodeProject touch generate-lint-rules.sh chmod +x generate-lint-rules.sh 複製程式碼
給上面的 generate-lint-rules.sh 裡面新增下面的指令碼
#! /bin/sh -e 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 複製程式碼
-
執行 generate-lint-rules.sh 指令碼(./generate-lint-rules.sh)。如果出現下面的 Log 則說明生成 xcodeproj 專案成功
- 開啟步驟4生成的專案,看到有很多資料夾,代表 oclint 自帶的 lint 規則,我們自定義的 lint 規則在最下面。
關於如何自定義 lint 規則的具體還沒有深入研究,這裡給個例子
點選檢視示例程式碼
#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 const string name() const override
{
return "Property in 'ViewModel' Class interface should be readonly.";
}
virtual int priority() const override
{
return 3;
}
virtual const string category() const override
{
return "mvvm";
}
virtual unsigned int supportedLanguages() const override
{
return LANG_OBJC;
}
#ifdef DOCGEN
virtual const std::string since() const override
{
return "0.18.10";
}
virtual const std::string description() const override
{
return "Property in 'ViewModel' Class interface should be readonly.";
}
virtual const std::string example() const override
{
return R"rst(
.. code-block:: cpp
@interface FooViewModel : NSObject // This is a "ViewModel" Class.
@property (nonatomic, strong) NSObject *bar; // should be readonly.
@end
)rst";
}
virtual const std::string fileName() const override
{
return "MVVMRule.cpp";
}
#endif
virtual void setUp() override {}
virtual void tearDown() override {}
/* Visit ObjCImplementationDecl */
bool VisitObjCImplementationDecl(ObjCImplementationDecl *node)
{
ObjCInterfaceDecl *interface = node->getClassInterface();
bool isViewModel = interface->getName().endswith("ViewModel");
if (!isViewModel) {
return false;
}
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);
}
}
return true;
}
};
static RuleSet rules(new MVVMRule());
複製程式碼
- 修改自定義規則後就需要編譯。成功後在 Products 目錄下會看到對應名稱的 CustomLintRulesRule.dylib 檔案,就需要複製到 /Documents/oclint/oclint-release/lib/oclint/rules。講道理,生成新的 lint rule 檔案,需要把新的 dylib 檔案複製到 /usr/local/lib。因為我們在原始碼安裝的第4部,設定了 ln -s 連結,所以不需要每次複製到相應資料夾。
但是還是比較麻煩,每次都需要編譯新的 lint rule 之後需要將相應的 dylib 檔案複製到原始碼目錄下的 oclint-release/lib/oclint/rules 目錄下,本著「可以偷懶絕不動手」的原則,在自定義的 rule 的 target 中,在 Build Phases 選項下 CMake PostBuild Rules 中的指令碼下將下面的程式碼複製進去
cp /Users/liubinpeng/Documents/oclint/Lint-XcodeProject/rules.dl/Debug/libCustomLintRulesRule.dylib /Users/liubinpeng/Documents/oclint/build/oclint-release/lib/oclint/rules/libCustomLintRulesRule.dylib
複製程式碼
- 規則限定的3個類說明:
RuleBase
|
|-AbstractASTRuleBase
| |_ AbstractASTVisitorRule
| |_AbstractASTMatcherRule
|
|-AbstractSourceCodeReaderRule
複製程式碼
- AbstractSourceCodeReaderRule:eachLine 方法,讀取每行的程式碼,如果想編寫的規則是需要針對每行的程式碼內容,則可以繼承自該類
- AbstractASTVisitorRule:可以訪問 AST 上特定型別的所有節點,可以檢查特定型別的所有節點是遞迴實現的。在 apply 方法內可以看到程式碼實現。開發者只需要過載 bool visit* 方法來訪問特定型別的節點。其值表明是否繼續遞迴檢查
- AbstractASTMatcherRule:實現 setUpMatcher 方法,在方法中新增 matcher,當檢查發現匹配結果時會呼叫 callback 方法。然後通過 callback 方法來繼續對匹配到的結果進行處理
- 知其所以然 oclint 依賴與原始碼的語法抽象樹(AST)。開源 clang 是 oclint 獲的語法抽象樹的依賴工具。你如果想對 AST 有個瞭解,可以檢視這個視訊
如果想檢視某個檔案的 AST 結構,你可以進入該檔案的命令列,然後執行下面的指令碼
clang -Xclang -ast-dump -fsyntax-only main.m
複製程式碼
三、 Homebrew 方式安裝的 oclint 如何使用自定義規則
- 檢視 OCLint 安裝路徑
which oclint
// 輸出:/usr/local/bin/oclint
ls -al /usr/local/bin/oclint
// 輸出:本機安裝路徑
複製程式碼
- 把上面生成的新的 lint rule 下的 dylib 檔案複製到步驟1得到的額本機安裝路徑下
四、 使用 oclint
在命令列中使用
- 如果專案使用了 Cocopod,則需要指定 -workspace xxx.workspace
- 每次編譯之前需要 clean
實操:
-
進入專案
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 複製程式碼
編譯成功後,會在專案的資料夾下出現 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 設定為 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 複製程式碼
然後繼續嘗試編譯,發現還是報錯,但是報錯資訊改變了,如下
看到報錯資訊是預設的警告數量超過限制,則 lint 失敗。事實上 lint 後可以跟引數,所以我們修改指令碼如下
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 的結果,檢視 html 檔案可以具體定位哪個程式碼檔案,哪一行哪一列有什麼問題,方便修改。
-
如果專案工程太大,整個 lint 會比較耗時,所幸 oclint 支援針對某個程式碼資料夾進行 lint
oclint-json-compilation-database -i 需要靜態分析的資料夾或檔案 -- -report-type html -o oclintReport.html 其他的引數 複製程式碼
-
引數說明
名稱 描述 預設閾值 CYCLOMATIC_COMPLEXITY 方法的迴圈複雜性(圈負責度) 10 LONG_CLASS C類或Objective-C介面,類別,協議和實現的行數 1000 LONG_LINE 一行程式碼的字元數 100 LONG_METHOD 方法或函式的行數 50 LONG_VARIABLE_NAME 變數名稱的字元數 20 MAXIMUM_IF_LENGTH if
語句的行數15 MINIMUM_CASES_IN_SWITCH switch語句中的case數 3 NPATH_COMPLEXITY 方法的NPath複雜性 200 NCSS_METHOD 一個沒有註釋的方法語句數 30 NESTED_BLOCK_DEPTH 塊或複合語句的深度 5 SHORT_VARIABLE_NAME 變數名稱的字元數 3 TOO_MANY_FIELDS 類的欄位數 20 TOO_MANY_METHODS 類的方法數 30 TOO_MANY_PARAMETERS 方法的引數數 10
在 Xcode 中使用
-
在專案的 TARGETS 下面,點選下方的 "+" ,選擇 cross-platform 下面的 Aggregate。輸入名字,這裡命名為 Lint
-
選擇對應的 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 複製程式碼
-
說明,雖然有時候沒有編譯通過,但是看到如下圖的關於程式碼相關的 warning 則達到目的了。
-
lint 結果如下,根據相應的提示資訊對程式碼進行調整。當然這只是一種參考,不一定要採納 lint 給的提示。
指令碼化
每次都在終端命令列去寫 lint 的指令碼,效率很低,所以想做成 shell 指令碼。需要的同學直接直接拷貝進去,直接在工程的根目錄下使用,我這邊是一個 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" #顏色結束符
# 尋找專案的 ProjectName
function searchProjectName () {
# maxdepth 查詢資料夾的深度
find . -maxdepth 1 -name "*.xcodeproj"
}
function oclintForProject () {
# 預先檢測所需的安裝包是否存在
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 安裝所需環境 ?'
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}
# 字串替換函式。//表示全域性替換 /表示匹配到的第一個結果替換。
path=${path//.\//} # ./BridgeLabiPhone.xcodeproj -> BridgeLabiPhone.xcodeproj
path=${path//.xcodeproj/} # BridgeLabiPhone.xcodeproj -> BridgeLabiPhone
myworkspace=$path".xcworkspace" # workspace名字
myscheme=$path # scheme名字
# 清除上次編譯資料
if [ -d ./derivedData ]; then
echo -e $COLOR_SUCC'-----清除上次編譯資料derivedData-----'$COLOR_SUCC
rm -rf ./derivedData
fi
# xcodebuild clean
xcodebuild -scheme $myscheme -workspace $myworkspace clean
# # 生成編譯資料
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'編譯資料生成完畢???'$COLOR_SUCC
else
echo -e $COLOR_ERR'編譯資料生成失敗???'$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 的分析結果'$COLOR_AW
# 用 safari 瀏覽器開啟 oclint 的結果
open -a "/Applications/Safari.app" oclintReport.html
}
oclintForProject
複製程式碼
同型別的文章: