工程代碼質量,一個永恆的話題。好的質量的好處不言而喻,團隊成員間除了保持統一的風格和較高的自我約束力之外,還需要一些工具來統計分析代碼質量問題。
本文就是針對 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
同類型的文章: