OCLint 實現 Code Review - 給你的程式碼提提質量

杭城小劉發表於2019-05-26

工程程式碼質量,一個永恆的話題。好的質量的好處不言而喻,團隊成員間除了保持統一的風格和較高的自我約束力之外,還需要一些工具來統計分析程式碼質量問題。

本文就是針對 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

    OCLint-驗證安裝成功

    注意:如果你採用原始碼編譯的時候直接 clone 官方的原始碼會有問題,編譯不過,所以提供了一個可以編譯過的版本。分支切換到 llvm-7.0。

4. xcodebuild 的安裝

xcode 下載安裝好就已經成功安裝了

5. xcpretty 的安裝

先決條件,你的機器已經安裝好了 Ruby gem.

gem install xcpretty
複製程式碼

二、 自定義 Rule

OClint 提供了 70+ 項的檢查規則,你可以直接去使用。但是某些時候你需要製作自己的檢測規則,接下來就說說如何自定義 lint 規則。

  1. 進入 ~/Document/oclint 目錄,執行下面的指令碼

    oclint-scripts/scaffoldRule CustomLintRules -t ASTVisitor
    複製程式碼

    其中,CustomLintRules 就是定義的檢查規則的名字, ASTVisitor 就是你繼承的 lint 規則

    可以繼承的規則有:ASTVisitor、SourceCodeReader、ASTMatcher。

  2. 執行上面的指令碼,會生成下面的檔案

    • Documents/oclint/oclint-rules/rules/custom/CustomLintRulesRule.cpp
    • Documents/oclint/oclint-rules/test/custom/CustomLintRulesRuleTest.cpp
  3. 要方便的開發自定義的 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
    複製程式碼
  4. 執行 generate-lint-rules.sh 指令碼(./generate-lint-rules.sh)。如果出現下面的 Log 則說明生成 xcodeproj 專案成功

生成編寫lint規則的xcodeproj工程1
生成編寫lint規則的xcodeproj工程2

  1. 開啟步驟4生成的專案,看到有很多資料夾,代表 oclint 自帶的 lint 規則,我們自定義的 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());
複製程式碼
  1. 修改自定義規則後就需要編譯。成功後在 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
複製程式碼
  1. 規則限定的3個類說明:
RuleBase
 |
 |-AbstractASTRuleBase
 |      |_ AbstractASTVisitorRule
 |             |_AbstractASTMatcherRule
 |
 |-AbstractSourceCodeReaderRule
複製程式碼
  • AbstractSourceCodeReaderRule:eachLine 方法,讀取每行的程式碼,如果想編寫的規則是需要針對每行的程式碼內容,則可以繼承自該類
  • AbstractASTVisitorRule:可以訪問 AST 上特定型別的所有節點,可以檢查特定型別的所有節點是遞迴實現的。在 apply 方法內可以看到程式碼實現。開發者只需要過載 bool visit* 方法來訪問特定型別的節點。其值表明是否繼續遞迴檢查
  • AbstractASTMatcherRule:實現 setUpMatcher 方法,在方法中新增 matcher,當檢查發現匹配結果時會呼叫 callback 方法。然後通過 callback 方法來繼續對匹配到的結果進行處理
  1. 知其所以然 oclint 依賴與原始碼的語法抽象樹(AST)。開源 clang 是 oclint 獲的語法抽象樹的依賴工具。你如果想對 AST 有個瞭解,可以檢視這個視訊

如果想檢視某個檔案的 AST 結構,你可以進入該檔案的命令列,然後執行下面的指令碼

clang -Xclang -ast-dump -fsyntax-only main.m 
複製程式碼

三、 Homebrew 方式安裝的 oclint 如何使用自定義規則

  1. 檢視 OCLint 安裝路徑
which oclint 
// 輸出:/usr/local/bin/oclint
ls -al  /usr/local/bin/oclint 
// 輸出:本機安裝路徑
複製程式碼
  1. 把上面生成的新的 lint rule 下的 dylib 檔案複製到步驟1得到的額本機安裝路徑下

四、 使用 oclint

在命令列中使用

  1. 如果專案使用了 Cocopod,則需要指定 -workspace xxx.workspace
  2. 每次編譯之前需要 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
    複製程式碼

    然後繼續嘗試編譯,發現還是報錯,但是報錯資訊改變了,如下

    generate-lintresult-html-error

    看到報錯資訊是預設的警告數量超過限制,則 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-result-html-report

  • 如果專案工程太大,整個 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

    Xcode中建立lint的target

  • 選擇對應的 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 則達到目的了。

    Xcode中Lint結果

  • lint 結果如下,根據相應的提示資訊對程式碼進行調整。當然這只是一種參考,不一定要採納 lint 給的提示。

    Xcode中顯示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
複製程式碼

同型別的文章:

相關文章