如何经过静态分析提升iOS代码质量

cover
随着项目的扩大,依靠人工codereview来保证项目的质量,愈来愈不现实,这时就有必要借助于一种自动化的代码审查工具: 程序静态分析

程序静态分析(Program Static Analysis)是指在不运行代码的方式下,经过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否知足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。(来自百度百科)html

词法分析,语法分析等工做是由编译器进行的,因此对iOS项目为了完成静态分析,咱们须要借助于编译器。对于OC语言的静态分析能够彻底经过Clang,对于Swift的静态分析除了Clange还须要借助于SourceKitjava

Swift语言对应的静态分析工具是SwiftLint,OC语言对应的静态分析工具备Infer和OCLitn。如下会是对各个静态分析工具的安装和使用作一个介绍。git

SwiftLint

对于Swift项目的静态分析可使用 SwiftLint。SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具。它的实现是 Hook 了 Clang 和 SourceKit 从而可以使用 AST 来表示源代码文件的更多精确结果。Clange咱们了解了,那SourceKit是干什么用的?

SourceKit包含在Swift项目的主仓库,它是一套工具集,支持Swift的大多数源代码操做特性:源代码解析、语法突出显示、排版、自动完成、跨语言头生成等工做。github

安装

安装有两种方式,任选其一: 方式一:经过Homebrewshell

$ brew install swiftlint
复制代码

这种是全局安装,各个应用均可以使用。 方式二:经过CocoaPodsjson

pod 'SwiftLint', :configurations => ['Debug']
复制代码

这种方式至关于把SwiftLint做为一个三方库集成进了项目,由于它只是调试工具,因此咱们应该将其指定为仅Debug环境下生效。swift

集成进Xcode

咱们须要在项目中的Build Phases,添加一个Run Script Phase。若是是经过homebrew安装的,你的脚本应该是这样的。xcode

if which swiftlint >/dev/null; then
  swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi
复制代码

若是是经过cocoapods安装的,你得脚本应该是这样的:安全

"${PODS_ROOT}/SwiftLint/swiftlint"
复制代码

运行SwiftLint

键入CMD + B编译项目,在编译完后会运行咱们刚才加入的脚本,以后咱们就能看到项目中大片的警告信息。有时候build信息并不能填入项目代码中,咱们能够在编译的log日志里查看。ruby

定制

SwiftLint规则太多了,若是咱们不想执行某一规则,或者想要滤掉对Pods库的分析,咱们能够对SwfitLint进行配置。

在项目根目录新建一个.swiftlint.yml文件,而后填入以下内容:

disabled_rules: # rule identifiers to exclude from running
 - colon
 - trailing_whitespace
 - vertical_whitespace
 - function_body_length
opt_in_rules: # some rules are only opt-in
 - empty_count
  # Find all the available rules by running:
  # swiftlint rules
included: # paths to include during linting. `--path` is ignored if present.
 - Source
excluded: # paths to ignore during linting. Takes precedence over `included`.
 - Carthage
 - Pods
 - Source/ExcludedFolder
 - Source/ExcludedFile.swift
 - Source/*/ExcludedFile.swift # Exclude files with a wildcard
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
 - explicit_self

# configurable rules can be customized from this configuration file
# binary rules can set their severity level
force_cast: warning # implicitly
force_try:
 severity: warning # explicitly
# rules that have both warning and error levels, can set just the warning level
# implicitly
line_length: 110
# they can set both implicitly with an array
type_body_length:
 - 300 # warning
 - 400 # error
# or they can set both explicitly
file_length:
 warning: 500
 error: 1200
# naming rules can set warnings/errors for min_length and max_length
# additionally they can set excluded names
type_name:
 min_length: 4 # only warning
 max_length: # warning and error
 warning: 40
 error: 50
 excluded: iPhone # excluded via string
 allowed_symbols: ["_"] # these are allowed in type names
identifier_name:
 min_length: # only min_length
 error: 4 # only error
 excluded: # excluded via string array
 - id
 - URL
 - GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
复制代码

一条rules提示以下,其对应的rules名就是function_body_length

! Function Body Length Violation: Function body should span 40 lines or less excluding comments and whitespace: currently spans 43 lines (function_body_length)
复制代码

disabled_rules下填入咱们不想遵循的规则。

excluded设置咱们想跳过检查的目录,Carthage、Pod、SubModule这些通常能够过滤掉。

其余的一些像是文件长度(file_length),类型名长度(type_name),咱们能够经过设置具体的数值来调节。

另外SwiftLint也支持自定义规则,咱们能够根据本身的需求,定义本身的rule

生成报告

若是咱们想将这次分析生成一份报告,也是能够的(该命令是经过homebrew安装的swiftlint):

# reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
$ swiftlint lint --reporter html > swiftlint.html
复制代码

xcodebuild

xcodebuild是xcode内置的编译命令,咱们能够用它来编译打包咱们的iOS项目,接下来介绍的Infer和OCLint都是基于xcodebuild的编译产物进行分析的,因此有必要简单介绍一下它。

通常编译一个项目,咱们须要指定项目名,configuration,scheme,sdk等信息如下是几个简单的命令及说明。

# 不带pod的项目,target名为TargetName,在Debug下,指定模拟器sdk环境进行编译
xcodebuild -target TargetName -configuration Debug -sdk iphonesimulator
# 带pod的项目,workspace名为TargetName.xcworkspace,在Release下,scheme为TargetName,指定真机环境进行编译。不指定模拟器环境会验证证书
xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release
# 清楚项目的编译产物
xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release clean
复制代码

以后对xcodebuild命令的使用都须要将这些参数替换为本身项目的参数。

Infer

Infer是Facebook开发的针对C、OC、Java语言的静态分析工具,它同时支持对iOS和Android应用的分析。对于Facebook内部的应用像是 Messenger、Instagram 和其余一些应用均是有它进行静态分析的。它主要检测隐含的问题,主要包括如下几条:

  • 资源泄露,内存泄露
  • 变量和参数的非空检测
  • 循环引用
  • 过早的nil操做

暂不支持自定义规则。

安装及使用

$ brew install infer
复制代码

运行infer

$ cd projectDir
# 跳过对Pods的分析
$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator
复制代码

咱们会获得一个infer-out的文件夹,里面是各类代码分析的文件,有txt,json等文件格式,当这样不方便查看,咱们能够将其转成html格式:

$ infer explore --html
复制代码

点击trace,咱们会看到该问题代码的上下文。

由于Infer默认是增量编译,只会分析变更的代码,若是咱们想总体编译的话,须要clean一下项目:

$ xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator clean
复制代码

再次运行Infer去编译。

$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator
复制代码

Infer的大体原理

Infer的静态分析主要分两个阶段:

一、捕获阶段

Infer 捕获编译命令,将文件翻译成 Infer 内部的中间语言。

这种翻译和编译相似,Infer 从编译过程获取信息,并进行翻译。这就是咱们调用 Infer 时带上一个编译命令的缘由了,好比: infer -- clang -c file.c, infer -- javac File.java。结果就是文件照常编译,同时被 Infer 翻译成中间语言,留做第二阶段处理。特别注意的就是,若是没有文件被编译,那么也没有任何文件会被分析。

Infer 把中间文件存储在结果文件夹中,通常来讲,这个文件夹会在运行 infer 的目录下建立,命名是 infer-out/

二、分析阶段

在分析阶段,Infer 分析 infer-out/ 下的全部文件。分析时,会单独分析每一个方法和函数。

在分析一个函数的时候,若是发现错误,将会中止分析,但这不影响其余函数的继续分析。

因此你在检查问题的时候,修复输出的错误以后,须要继续运行 Infer 进行检查,知道确认全部问题都已经修复。

错误除了会显示在标准输出以外,还会输出到文件 infer-out/bug.txt 中,咱们过滤这些问题,仅显示最有可能存在的。

在结果文件夹中(infer-out),同时还有一个 csv 文件 report.csv,这里包含了全部 Infer 产生的信息,包括:错误,警告和信息。

OCLint

OCLint是基于Clange Tooling编写的库,它支持扩展,检测的范围比Infer要大。不光是隐藏bug,一些代码规范性的问题,例如命名和函数复杂度也均在检测范围以内。

安装OCLint

OCLint通常经过Homebrew安装

$ brew tap oclint/formulae   
$ brew install oclint
复制代码

经过Hombrew安装的版本为0.13。

$ oclint --version
LLVM (http://llvm.org/):
  LLVM version 5.0.0svn-r313528
  Optimized build.
  Default target: x86_64-apple-darwin19.0.0
  Host CPU: skylake

OCLint (http://oclint.org/):
  OCLint version 0.13.
  Built Sep 18 2017 (08:58:40).
复制代码

我分别用Xcode11在两个项目上运行过OCLint,一个实例项目能够正常运行,另外一个复杂的项目却运行失败,报以下错误:

1 error generated
1 error generated
...
oclint: error: cannot open report output file ..../onlintReport.html
复制代码

我并不清楚缘由,若是你想试试0.13可否使用的话,直接跳到安装xcpretty。若是你也遇到了这个问题,能够回来安装oclint0.15版本。

OCLint0.15

我在oclint issuse #547这里找到了这个问题和对应的解决方案。

咱们须要更新oclint至0.15版本。brew上的最新版本是0.13,github上的最新版本是0.15。我下载github上的release0.15版本,可是这个包并非编译过的,不清楚是否是官方本身搞错了,只能手动编译了。由于编译要下载llvm和clange,这两个包较大,因此我将编译事后的包直接传到了这里CodeChecker

若是不关心编译过程,能够下载编译好的包,跳到设置环境变量那一步。

编译OCLint

一、安装CMakeNinja这两个编译工具

$ brew install cmake ninja
复制代码

二、clone OCLint项目

$ git clone https://github.com/oclint/oclint
复制代码

三、进入oclint-scripts目录,执行make命令

$ ./make
复制代码

成功以后会出现build文件夹,里面有个oclint-release就是编译成功的oclint工具。

设置oclint工具的环境变量

设置环境变量的目的是为了咱们可以快捷访问。而后咱们须要配置PATH环境变量,注意OCLint_PATH的路径为你存放oclint-release的路径。将其添加到.zshrc,或者.bash_profile文件末尾:

OCLint_PATH=/Users/zhangferry/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH
复制代码

执行source .zshrc,刷新环境变量,而后验证oclint是否安装成功:

$ oclint --version
OCLint (http://oclint.org/):
OCLint version 0.15.
Built May 19 2020 (11:48:49).
复制代码

出现这个介绍就说明咱们已经完成了安装。

安装xcpretty

xcpretty是一个格式化xcodebuild输出内容的脚本工具,oclint的解析依赖于它的输出。它的安装方式为:

$ gem install xcpretty
复制代码

OCLint的使用

在使用OCLint以前还须要一些准备工做,须要将编译项COMPILER_INDEX_STORE_ENABLE设置为NO。

  • 将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 NO
  • 在 podfile 中 target 'target' do 前面添加下面的脚本,将各个pod的编译配置也改成此选项
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
复制代码

使用方式

一、进入项目根目录,运行以下脚本:

$ xcodebuild -workspace ProjectName.xcworkspace -scheme ProjectScheme -configuration Debug -sdk iphonesimulator | xcpretty -r json-compilation-database -o compile_commands.json
复制代码

会将xcodebuild编译过程当中的一些信息记录成一个文件compile_commands.json,若是咱们在项目根目录看到了该文件,且里面是有内容的,证实咱们完成了第一步。

二、咱们将这个json文件转成方便查看的html,过滤掉对Pods文件的分析,为了防止行数上限,咱们加上行数的限制:

$ 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
复制代码

最终会产生一个oclintReport.html文件。

OCLint支持自定义规则,由于其自己规则已经很丰富了,自定义规则的需求应该很小,也就没有尝试。

封装脚本

OCLint跟Infer同样都是经过运行几个脚本语言进行执行的,咱们能够将这几个命令封装成一个脚本文件,以OCLint为例,Infer也相似:

#!/bin/bash
# mark sure you had install the oclint and xcpretty
 # You need to replace these values with your own project configuration
workspace_name="WorkSpaceName.xcworkspace"
scheme_name="SchemeName"
 # remove history
rm compile_commands.json
rm oclint_result.xml
# clean project
# -sdk iphonesimulator means run simulator
xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator clean || (echo "command failed"; exit 1);
 # export compile_commands.json
xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator \
| xcpretty -r json-compilation-database -o compile_commands.json \
|| (echo "command failed"; exit 1);
 # export report html
# you can run `oclint -help` to see all USAGE
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \
-disable-rule ShortVariableName \
-rc LONG_LINE=1000 \
|| (echo "command failed"; exit 1);

open -a "/Applications/Safari.app" oclintReport.html
复制代码

oclint-json-compilation-database命令的几个参数说明:

-e 须要忽略分析的文件,这些文件的警告不会出如今报告中

-rc 须要覆盖的规则的阀值,这里能够自定义项目的阀值,默认阀值

-enable-rule 支持的规则,默认是oclint提供的都支持,能够组合-disable-rule来过滤掉一些规则 规则列表

-disable-rule 须要忽略的规则,根据项目需求设置

在Xcode中使用OCLint

由于OCLint提供了xcode格式的输出样式,因此咱们能够将它做为一个脚本放在Xcode中。

一、在项目的 TARGETS 下面,点击下方的 "+" ,选择 cross-platform 下面的 Aggregate。输入名字,这里命名为 OCLint

../_images/xcode_screenshot_1.png

二、选中该Target,进入Build Phases,添加Run Script,写入下面脚本:

# Type a script or drag a script file from your workspace to insert its path.
# 内置变量
cd ${SRCROOT}
xcodebuild clean 
xcodebuild | xcpretty -r json-compilation-database
oclint-json-compilation-database -e Pods -- -report-type xcode
复制代码

能够看出该脚本跟上面的脚本同样,只不过 将oclint-json-compilation-database命令的-report-typehtml改成了xcode。而OCLint做为一个target自己就运行在特定的环境下,因此xcodebuild能够省去配置参数。

三、经过CMD + B咱们编译一下项目,执行脚本任务,会获得可以定位到代码的warning信息:

../_images/xcode_screenshot_8.png

总结

如下是对这几种静态分析方案的对比,咱们能够根据需求选择适合本身的静态分析方案。

SwiftLint Infer OCLint
支持语言 Swift C、C++、OC、Java C、C++、OC
易用性 简单 较简单 较简单
可否集成进Xcode 能够 不能集成进xcode 能够
自带规则丰富度 较多,包含代码规范 相对较少,主要检测潜在问题 较多,包含代码规范
规则扩展性 能够 不能够 能够

参考

OCLint 实现 Code Review - 给你的代码提提质量

Using OCLint in Xcode

Infer 的工做机制

LLVM & Clang 入门

相关文章
相关标签/搜索