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

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

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

同類型的文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章