代碼質(zhì)量分析利器OCLint

前言

隨著團(tuán)隊(duì)人員的增多,開(kāi)發(fā)人員的編碼風(fēng)格不一致帶來(lái)的開(kāi)發(fā)效率的低下,以及編寫(xiě)代碼中容易犯錯(cuò)的一些問(wèn)題,盡管在人工code review 的能夠發(fā)現(xiàn)并且解決,但是效率將會(huì)大大降低,而且依靠人工來(lái)保證項(xiàng)目代碼質(zhì)量本身就不牢靠。我們必須在編碼階段,打包交付測(cè)試前就發(fā)現(xiàn)編寫(xiě)代碼的潛在問(wèn)題,并且解決這些問(wèn)題來(lái)提高工程代碼質(zhì)量。

本文就是筆者在實(shí)際項(xiàng)目中運(yùn)用OCLint對(duì)整個(gè)項(xiàng)目進(jìn)行的一次實(shí)踐,記錄在實(shí)踐過(guò)程中的思路和所遇到的坑點(diǎn),分享給大家。

筆者的所使用的開(kāi)發(fā)工具和和開(kāi)發(fā)環(huán)境如下:

Mac系統(tǒng)版本:macOS Mojave 10.14.5
ruby版本:2.6.3p62
gem版本:3.0.3
Xcode版本: 10.2.1
OCLint版本: 0.13
xcpretty版本: 0.3.0 # 這個(gè)是在項(xiàng)目文件夾下使用gem配置了的本地ruby環(huán)境安裝的,后面會(huì)說(shuō)明為啥不安裝在系統(tǒng)默認(rèn)的ruby環(huán)境下

筆者使用OCLint所做的codereview是結(jié)合Xcode在編碼開(kāi)發(fā)階段進(jìn)行的,ocline結(jié)合x(chóng)code的配置教程,這么做的優(yōu)點(diǎn)是:能夠在在開(kāi)發(fā)階段發(fā)現(xiàn)編寫(xiě)代碼的潛在問(wèn)題,把問(wèn)題提前暴露出來(lái)。缺點(diǎn)就是會(huì):延長(zhǎng)開(kāi)發(fā)編譯時(shí)長(zhǎng),降低開(kāi)發(fā)效率。

再講如何安裝OCLint和使用之前,我先講一下衡量代碼質(zhì)量的幾個(gè)指標(biāo):

代碼質(zhì)量衡量指標(biāo)

  1. Cyclomatic Complexity:循環(huán)復(fù)雜度(又叫圈復(fù)雜度),用來(lái)表示程序的復(fù)雜度,圈復(fù)雜度越高,代碼就越難復(fù)雜難維護(hù)。OCLint給的默認(rèn)閾值是10。
    復(fù)雜度計(jì)算:M = E ? N + 2P,其中 E 為圖中邊的個(gè)數(shù),N 為圖中節(jié)點(diǎn)的個(gè)數(shù),P 為連接組件的個(gè)數(shù)。
    簡(jiǎn)單程序的控制流圖。此程序由紅色的節(jié)點(diǎn)開(kāi)始運(yùn)行,然后進(jìn)入循環(huán)(紅色節(jié)點(diǎn)下由三個(gè)節(jié)點(diǎn)組成),離開(kāi)循環(huán)后有條件分支,最后運(yùn)行藍(lán)色節(jié)點(diǎn)后結(jié)束,此控制流圖中,E = 9, N = 8, P = 1,因此其循環(huán)復(fù)雜度為 9 - 8 + (2*1) = 3。
    具體可見(jiàn)維基百科介紹這篇文章的計(jì)算方法。
    下面我用例子介紹一個(gè)簡(jiǎn)單的計(jì)算方式:如下
    例子:下面這段代碼的 M = 2(if) + 1(for) + 1(for) + 2(if) + 2(if) + 2(if) + 1 = 11.
- (UIBezierPath *)pathForRect:(CGRect)rect {
    UIBezierPath *path = [[UIBezierPath alloc] init];
    if (self.width < 1.0) {
        self.width = self.segments.firstObject?self.segments.firstObject.width:1;
    }
    
    NSArray *segments = [_segments copy];
    xxxxxSegment *lastSegment = nil;
    xxxxxPoint *lastPoint = nil;
    for (xxxxxSegment *segment in segments) {
        xxxxxPoint *firstPoint = nil;
        for (xxxxxPoint *point in segment.points) {
            if (!firstPoint) { 
                firstPoint = point;
                if (!lastSegment) { 
                    [path moveToPoint:CGPoint(point)];
                    [path addLineToPoint:CGPoint(point)];
                } else { 
                    xxxxxPoint *lastPoint = lastSegment.points.lastObject;
                    if (![lastPoint isEqual:firstPoint]) {
                        [path addLineToPoint:CGPoint(lastPoint)];
                        [path moveToPoint:CGPoint(firstPoint)];
                    } else {
                        [path addQuadCurveToPoint:MID_CGPoint(lastPoint, firstPoint) controlPoint:CGPoint(lastPoint)];
                    }
                }
            } else { 
                NSAssert(lastPoint, @"last point should not be nil");
                [path addQuadCurveToPoint:MID_CGPoint(lastPoint, point) controlPoint:CGPoint(lastPoint)];
            }
            lastPoint = point;
        }
        lastSegment = segment;
    }
    
    if (lastPoint) {
        [path addLineToPoint:CGPoint(lastPoint)];
    }
    
    return path;
}
  1. NPath Complexity:NPath復(fù)雜度,用來(lái)表示一個(gè)方法所有可能執(zhí)行路徑的總和。OCLint給的默認(rèn)閾值是200。NPath復(fù)雜度越高,代碼越難以被理解。
    例子:下面的例子NPath復(fù)雜度為4。
void example()          
{
    if (xx)              // 1
    {
    }
    else                // 2
    {
    }

    if (xx)              // 1
    {
    }
    else                // 2
    {
    }
}
  1. Non Commenting Source Statements:除去空語(yǔ)句,注釋代碼之后的源代碼行數(shù)。OCLint給方法的默認(rèn)閾值是30,給一個(gè)類(lèi)文件的默認(rèn)閾值是1000。當(dāng)NCSS過(guò)高,代碼的維護(hù)成本就會(huì)提高,此時(shí)就得考慮方法和類(lèi)的瘦身,進(jìn)行拆分和重構(gòu)。
    例子:
void example()          // 1
{
    if (1)              // 2
    {
    }
    else                // 3
    {
    }
}
  1. Statement Depth:語(yǔ)句嵌套深度。OCLint給方法的默認(rèn)閾值是5。
    例子:
if (1)
{                // 1
    {           // 2
        {       // 3
        }
    }
}

為什么選擇則OCLint

iOS靜態(tài)代碼分析工具對(duì)比:

  1. Xcode自帶的Analyzer,使用方式超級(jí)簡(jiǎn)單Product > Analyze或者快捷鍵shift + command + B。
    能夠進(jìn)行以下的問(wèn)題檢測(cè),支持的語(yǔ)言包括C, C++ 和 Objective-C,不可進(jìn)行自定義;
    ?邏輯缺陷,例如訪問(wèn)未初始化的變量和解除引用空指針;
    ?內(nèi)存管理缺陷,例如內(nèi)存泄漏;
    ?未使用的變量;
    ?由于不遵循項(xiàng)目使用的框架和庫(kù)所需的策略而導(dǎo)致的API使用缺陷。
  2. Facebook開(kāi)源的Infer,使用方式用command line,可以持續(xù)集成
    能夠檢查的bug類(lèi)型比Analyzer豐富,具體見(jiàn)官方文檔。
  3. OCLint, 優(yōu)點(diǎn)是可檢查的規(guī)則最多,并且具有高可定制性,缺點(diǎn)是集成到Xcode中進(jìn)行檢測(cè)效率低。官方定義的支持的檢查規(guī)則有71條,詳見(jiàn)官方規(guī)則。
    以上三個(gè)工具的底層原理都類(lèi)似,都需要用到Clang進(jìn)行碼編譯后的產(chǎn)物,然后進(jìn)行分析。
    綜上比較,因OCLint的可定制化最高,并且可以和Xcode無(wú)縫結(jié)合,所以我們團(tuán)隊(duì)選擇使用OCLint可以非常方便和統(tǒng)一的進(jìn)行項(xiàng)目工程代碼質(zhì)量檢測(cè)并且修改。

OCLint安裝

官方文檔上提供了三種安裝方式了,分別是:Homebrew、下載安裝包安裝、源代碼編譯安裝;
如果需要自定義檢測(cè)規(guī)則,則必須使用第三種安裝方式:源代碼編譯安裝。不過(guò)筆者尚未嘗試過(guò),到后期根據(jù)團(tuán)隊(duì)的項(xiàng)目實(shí)際需要,如果需自定義則會(huì)嘗試使用。
筆者使用的是最簡(jiǎn)單的Homebrew安裝方式:

brew tap oclint/formulae
brew install oclint

有以下信息則表示安裝成功

$ oclint
oclint: Not enough positional command line arguments specified!
Must specify at least 1 positional arguments: See: oclint -help

xcpretty安裝

由于筆者使用方式是直接在xcode的Build Pahses添加的Run Script腳本,在使用gem install xcpretty的安裝方式在系統(tǒng)的ruby環(huán)境安裝會(huì)在編譯的時(shí)候會(huì)報(bào)錯(cuò),xcpretty command not found,原因就是xcode和teminal的環(huán)境不一樣,盡管在terminal上能很好工作但是在xcode就會(huì)報(bào)找不到xcprrety的錯(cuò)誤。當(dāng)時(shí)找到的一種解決方式直接在sh腳本中將xcpretty寫(xiě)為絕對(duì)路徑(找到安裝的絕對(duì)路徑就是which xcpretty),但是這種方式的弊端就是組內(nèi)成員沒(méi)法協(xié)同開(kāi)發(fā),畢竟比無(wú)法保證其他伙伴安裝的xcpretty的絕對(duì)路徑和你的保持一致,座椅最終放棄這種方式。最后在詢問(wèn)團(tuán)隊(duì)其他人員的協(xié)助下找到了另一個(gè)解決方案:使用bundler在工程目錄維護(hù)一個(gè)管理ruby gem,詳細(xì)介紹見(jiàn)官方文檔。
首先安裝bundler,在終端執(zhí)行如下命令:

gem install bundler

在xcode工程根目錄寫(xiě)一個(gè)Gemfile,內(nèi)容如下

source 'https://rubygems.org'
gem 'xcpretty', '0.3.0'

然后執(zhí)行

bundle install

執(zhí)行完成后,會(huì)有一個(gè)ruby文件夾的生成,這個(gè)就是本地的ruby環(huán)境,xcpretty就安裝完成。


bundle install

.oclint 規(guī)則配置文件編寫(xiě)

官方可配置的71條規(guī)則
在項(xiàng)目根目錄編寫(xiě)一個(gè).oclint文件,筆者的項(xiàng)目使用的規(guī)則內(nèi)容如下:

rule-configurations:
  - key: CYCLOMATIC_COMPLEXITY # Cyclomatic complexity of a method 10
    value: 30
  - key: LONG_LINE
    value: 110
  - key: NCSS_METHOD # Number of non-commenting source statements of a method 30
    value: 50
  - key: LONG_VARIABLE_NAME
    value: 40
  - key: NESTED_BLOCK_DEPTH
    value: 6
  - key: MINIMUM_CASES_IN_SWITCH
    value: 2
  - key: SHORT_VARIABLE_NAME
    value: 1
  - key: TOO_MANY_METHODS
    value: 50
  - key: LONG_METHOD
    value: 100
disable-rules:
  - RedundantLocalVariable 
  - SHORT_VARIABLE_NAME 
  - LongVariableName
  - UnnecessaryElseStatement 
  - RedundantNilCheck 
  - RedundantIfStatement
  - InvertedLogic
  - AssignIvarOutsideAccessors
  - UseObjectSubscripting
  - BitwiseOperatorInConditional
  - PreferEarlyExit
  - UnusedMethodParameter
max-priority-1: 1000
max-priority-2: 1000
max-priority-3: 1000
enable-clang-static-analyzer: false

sh腳本編寫(xiě)

內(nèi)容如下

# Type a script or drag a script file from your workspace to insert its path.

export LC_CTYPE=en_US.UTF-8
set -euo pipefail # 腳本只要發(fā)生錯(cuò)誤,就終止執(zhí)行
# 刪除DerivedData的build文件
#echo $(dirname ${BUILD_DIR})
rm -rf $(dirname ${BUILD_DIR})

# 1. 環(huán)境配置,判斷是否安裝oclint,沒(méi)有則安裝
if which oclint 2>/dev/null; then
echo 'oclint already installed'
else # install oclint
brew tap oclint/formulae
brew install oclint
fi

# 2.0 使用xcodebuild構(gòu)建項(xiàng)目,并且使用xcprretty將便于產(chǎn)物轉(zhuǎn)換為json
projectDir=${PROJECT_DIR}
prettyPath="${projectDir}/ruby/2.6.0/gems/xcpretty-0.3.0/bin/xcpretty" # 替換為你安裝的本地路徑
#echo ${prettyPath}
projectName="xxxxxxx" # 替換為你的project name
xcodebuild -scheme ${projectName} -workspace ${projectName}.xcworkspace clean && xcodebuild clean && xcodebuild -scheme ${projectName} -workspace ${projectName}.xcworkspace -configuration Debug -sdk iphonesimulator COMPILER_INDEX_STORE_ENABLE=NO | ${prettyPath} -r json-compilation-database  -o compile_commands.json

# 3.0 判斷json是否
if [ -f ./compile_commands.json ]; then echo "compile_commands.json 文件存在";
else echo "-----compile_commands.json文件不存在-----"; fi

# 4.0 oclint分析json
oclint-json-compilation-database -e Pods -- -report-type xcode
oclint.png

然后command + B之后,去喝泡一杯咖啡,回來(lái)你就能夠看到工程代碼的warning然后修改代碼。

Clang-format對(duì)團(tuán)隊(duì)代碼進(jìn)行風(fēng)格統(tǒng)一

筆者使用的.clangformat的配置如下,具體見(jiàn)gist

---
# Language:        ObjC
BasedOnStyle:  Google
AccessModifierOffset: 0
ConstructorInitializerIndentWidth: 4
SortIncludes: false

# 連續(xù)賦值時(shí),對(duì)齊所有等號(hào)
# AlignConsecutiveAssignments: true 
AlignAfterOpenBracket: true
AlignEscapedNewlinesLeft: true
AlignOperands: false
AlignTrailingComments: true

AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
AllowShortIfStatementsOnASingleLine: false 
# AllowShortFunctionsOnASingleLine: All
AllowShortLoopsOnASingleLine: false

AlwaysBreakAfterDefinitionReturnType: false
AlwaysBreakTemplateDeclarations: false
AlwaysBreakBeforeMultilineStrings: false

BreakBeforeBinaryOperators: None
BreakBeforeTernaryOperators: false
BreakConstructorInitializersBeforeComma: false

BinPackArguments: true
BinPackParameters: true
ColumnLimit: 110
ConstructorInitializerAllOnOneLineOrOnePerLine: true
DerivePointerAlignment: false
ExperimentalAutoDetectBinPacking: false
IndentCaseLabels: true
IndentWrappedFunctionNames: false
IndentFunctionDeclarationAfterType: false
MaxEmptyLinesToKeep: 1 # 連續(xù)的空行保留幾行
KeepEmptyLinesAtTheStartOfBlocks: false
NamespaceIndentation: Inner
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PenaltyBreakBeforeFirstCallParameter: 10000
PenaltyBreakComment: 300
PenaltyBreakString: 1000
PenaltyBreakFirstLessLess: 120
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Right
SpacesBeforeTrailingComments: 1
Cpp11BracedListStyle: true
Standard:        Auto
IndentWidth:     4
TabWidth:        4
UseTab:          Never
BreakBeforeBraces: Custom
BraceWrapping: 
    AfterClass: true
    AfterControlStatement: false
    AfterEnum: false
    AfterFunction: false
    AfterNamespace: true
    AfterObjCDeclaration: false # ObjC定義后面是否換行
    AfterStruct: false
    AfterUnion: false
    BeforeCatch: false
    BeforeElse: false
    IndentBraces: false

SpacesInParentheses: false
SpacesInSquareBrackets: false
SpacesInAngles:  false
SpaceInEmptyParentheses: false
SpacesInCStyleCastParentheses: false
SpaceAfterCStyleCast: false
SpacesInContainerLiterals: true
SpaceBeforeAssignmentOperators: true

ContinuationIndentWidth: 4
CommentPragmas:  '^ IWYU pragma:'
ForEachMacros:   [ foreach, Q_FOREACH, BOOST_FOREACH ]
SpaceBeforeParens: ControlStatements
DisableFormat:   false
...

使用命令clang-format -i [files]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容