使用 OCLint 自定義 MVVM 規則
最近在搞 iOS MVVM 框架,雖說是 N 年前就老生常談的知識了,但設計模式畢竟是隻一種規範,無法約束專案中所有程式設計師都去遵循。我做了個 OCLint 的自定義規則,對 ViewModel 執行靜態檢查。
然而過程中踩了不少坑,OCLint 的官方 Repo 早已失效,可以使用我修改後基於 LLVM 7 的版本: https://github.com/yulingtianxia/oclint/tree/llvm-7.0
伸手黨也可以使用我寫的 指令碼 直接安裝,已包含 MVVM 規則。
編譯 OCLint
編譯 OCLint 時,會先下載 LLVM 等專案。由於 LLVM 原始碼廢棄了在 SVN 上的版本管理,將其遷移到了 Git 上,所以目前各種版本的 OCLint 都無法編譯了。而且最新版本的 OCLint 還是基於 LLVM 5 的!我從作者的 Repo 那發現有 LLVM 7 的 branch,依然無法編譯,只好自己動手改了。
雖然 Git 上的提交與歷史 SVN 提交記錄有對映,但是經過實踐發現並不精準可信。編譯時需要用到 llvm-project 下的 llvm,cfe 和 compile-rt,而且三個 repo 的 release 版本一定要一致。然而 LLVM 在 Git 上同一個 release 的程式碼卻無法編譯通過。更離譜的是即便 LLVM 在 GitHub 上把整個 llvm-project 作為一個 repo,依然無法將其 release 版本編譯通過。
算了,Git 不靠譜,還是改下 OCLint 的程式碼,從官網直接下以前打包好的吧。
llvm = 'http://releases.llvm.org/7.0.0/llvm' clang = 'http://releases.llvm.org/7.0.0/cfe' clang_rt = 'http://releases.llvm.org/7.0.0/compiler-rt'
我把最終可以正常編譯的 0.18.10 版本發了個非官方的 release 包,macOS 親測 ok: https://github.com/yulingtianxia/oclint/releases/tag/0.18.10
嫌麻煩不想編譯的,可以直接跑我提供的指令碼來安裝已經編譯好的 0.18.10 版本。以前安裝過 OCLint 舊版本的可以先備份下,因為會被覆蓋安裝。
wget --no-check-certificate -O install-oclint https://github.com/yulingtianxia/oclint/releases/download/0.18.10/install-0.18.10 chmod +x install-oclint ./install-oclint
自定義規則
網上有很多介紹如何編寫自定義規則的文章,這裡假設已經成功編譯好 OCLint,總體流程如下。
建立規則
使用 oclint-scripts 資料夾下的 scaffoldRule 指令碼建立一個新規則,並指定模板。注意規則名不需要帶 “Rule”:
oclint-scripts/scaffoldRule MVVM -t ASTVisitor
生成除錯工程
建立一個資料夾用於生成除錯 Rule 的工程。我已經建立好了: https://github.com/yulingtianxia/oclint/tree/llvm-7.0/oclint-xcodeproject
執行 xcode-debug.sh 指令碼即可使用 oclint-rules 資料夾的內容建立一個 Xcode 工程。因為這裡是想除錯剛剛建立的 MVVM 規則,所以選擇 oclint-rules。理論上可以修改指令碼引數使用其他資料夾建立 Xcode 工程。
#! /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
每個規則都有對應的 Scheme,選擇我們自定義的 MVVMRule,新增啟動引數。 -R
傳入自定義的規則名,這裡使用除錯工程生成的 Debug 目錄。接著傳入一個隨便寫的測試用檔案 TestViewModel.m
,此檔案所依賴的 Framework 等環境引數也需要傳入。別忘了需要把我貼的絕對路徑修改成你電腦上的路徑。
-R /Users/yangxiaoyu/Code/oclint/oclint-xcodeproject/rules.dl/Debug /Users/yangxiaoyu/Code/oclint/oclint-rules-test/OCLintTest/OCLintTest/TestViewModel.m-- -x objective-c -isystem /Users/yangxiaoyu/Code/oclint/build/oclint-release/lib/clang/7.0.0/include -iframework /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks -isystem /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include
為了能夠除錯執行,還需要在 Scheme 的 Info 下選擇 Executable 為編譯好的 oclint 的可執行檔案。oclint-0.18.10 檔案由於字尾名問題不允許被選擇為 Executable,刪掉字尾名的數字就可以了。這樣就可以無需重新編譯 OCLint 直接執行除錯了!
實現規則
在 MVVM 設計模式下,我想讓 ViewModel 的屬性都是隻讀的。因為我只想通過與 Model 的資料繫結來更新 ViewModel 的值,或是在其內部更新狀態。現在我需要實現一個規則來找出那些非只讀屬性。
先找幾個自帶的規則例子看看,結合 Clang AST 文件 學習下各種資料結構的定義。如果不知道自己的測試程式碼如何下手,可以用 clang
命令將測試程式碼轉化為 Clang AST :
clang -Xclang -ast-dump -fsyntax-only TestViewModel.m
思路是遍歷每個字尾名為 ViewModel
類的 Interface 中的所有 Property,判斷每個 Property 的 Attribute,如果包含 readwrite
就觸發 warning。提高優先順序可以產生 error。
/* 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; }
整合到 Xcode
先放一張整合後的效果:
在 CI 執行靜態檢查可以減少一部分人工 Code Review 的成本,缺點是發現問題滯後,解決問題有一定成本。而如果在本地 Xcode 執行靜態檢查,則可把問題扼殺在搖籃之中,缺點是佔用開發機資源。
如何在 Xcode 中整合 OCLint 靜態檢查,官方有很詳細的文件,圖文並茂: https://oclint-docs.readthedocs.io/en/stable/guide/xcode.html
美中不足的是 Xcode Run Script 欠一點火候,可以參考下下面我提供的指令碼:
if which oclint 2>/dev/null; then echo 'oclint exist' else wget --no-check-certificate -O install-oclint https://github.com/yulingtianxia/oclint/releases/download/0.18.10/install-0.18.10 chmod +x install-oclint ./install-oclint fi if which xcpretty 2>/dev/null; then echo 'xcpretty exist' else sudo gem install xcpretty fi source ~/.bash_profile cd ${SRCROOT} xcodebuild clean xcodebuild | xcpretty -r json-compilation-database --output compile_commands.json oclint-json-compilation-database -- -report-type xcode
後記
我只是簡單的寫了一個 ViewModel 的規則來跑通和驗證整個流程,其實 MVVM 設計模式裡還有更多的規則需要實現,比如 ViewModel 中不能引入 UIKit
等。歡迎有興趣的同學提 PR!