iOS-OC、Swift代码规范检查

1,780 阅读5分钟

前言

在多人开发过程中,虽然定义了代码开发规范,但是要保证大家完全遵循定义的规范还是比较难的,这样对后期项目的维护来讲是比较困难的。随着项目的扩大,依靠人工CodeReview来保证项目的质量,越来越不现实,这时就有必要借助于一种自动化的代码审查工具:程序静态分析

程序静态分析是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。

词法分析,语法分析等工作是由编译器进行的,所以对iOS项目为了完成静态分析,我们需要借助于编译器。对于OC语言的静态分析可以完全通过Clang,对于Swift的静态分析除了Clange还需要借助于SourceKit

OC语言对应的静态分析工具有InferOClint,而Swift语言对应的静态分析工具是SwiftLint。下面会对各种工具的安装和使用做一个介绍。

OC的静态分析工具

Infer工具检查

什么是 Infer?

Infer 是 Facebook 开发的一个静态分析工具。Infer 可以分析 Objective-C、Java 或者 C 代码,报告潜在的问题。任何人都可以使用 Infer 检测应用,这可以将那些严重的 bug 扼杀在发布之前,同时防止应用崩溃和性能低下。

截屏2022-01-04 下午8.23.38.png

检测的问题点

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

安装及使用

  • 通过 brew 安装
brew install infer

.bash_profile添加环境变量,添加完要在终端执行source ~/.bash_profile就会生效

export PATH=\"\$PATH:`pwd`/infer/infer/bin\"
  • 手动下载安装(我使用这种方式安装的)

安装包下载地址:github.com/facebook/in…

下载后解压安装包,安装包我一般放在 Home 目录下,然后配置环境变量

infer_PATH=/Users/zhoucandy/infer
export PATH=$infer_PATH/bin:$PATH

终端执行 source ~/.bash_profile 使环境变量生效

通过 infer -version 查看是否安装成功

分析文件

  • 分析单个文件
infer run -- clang -c xxx.m
  • 分析非pod工程
infer run -- xcodebuild -target XXX -configuration Debug -sdk iphonesimulator

  • 分析 pod工程
infer run -- xcodebuild -workspace xxx.xcworkspace -scheme "xxx"

注意:

如果出现了这个错误 External Error: *** capture failed to execute: exited with code 65

截屏2022-01-07 上午10.13.02.png

解决方案:

先执行

//没用pod
xcodebuild -project OCCheck.xcodeproj -scheme OCCheck -sdk iphoneos15.0 | tee xcodebuild.log | xcpretty -r json-compilation-database -o compile_commands.json

//用了pod
xcodebuild -workspace OCCheck.xcworkspace -scheme OCCheck -sdk iphoneos15.0  | tee xcodebuild.log | xcpretty -r json-compilation-database -o compile_commands.json

再执行脚本

infer run --skip-analysis-in-path Pods --no-xcpretty --keep-going --clang-compilation-db-files-escaped compile_commands.json

这里使用了--no-xcpretty --keep-going,如不需要可以去掉

WARNING: '--clang-compilation-db-files-escaped' is deprecated. Use '--compilation-database-escaped' instead. //即使用--compilation-database-escaped是新接口

或者直接解析database。infer也支持直接解析compile_commands.json文件,其原理与OCLint一致,都是基于Clang。

infer --compilation-database compile_commands.json
  • 添加静态分析工具忽略文件 在工程目录下新建 .inferconfig 文件
touch .inferconfig

修改.inferconfig,添加过滤选项

{
    "skip-analysis-in-path":["Pods"]
}
  • infer 区分增量分析/非增加分析
    • 增量分析表示每次只对新增或者修改的代码做静态分析,忽视掉之前已经分析过的代码
    • 非增量分析表示每次都会分析项目中的所有代码,不论之前是否已经分析过

infer默认是增量编译,只会分析变动的代码,如果我们想整体编译的话,需要clean一下项目:

xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator clean

然后再执行编译命令。

Infer 的工作机制

截屏2022-01-04 下午8.51.20.png

Infer 检查的结果,在 infer-out 目录下,是 JSON 格式的,名字叫做 report.json。

官网直接下包地址:github.com/facebook/in…

官方参考链接:fbinfer.com

github.com/facebook/in…

OCLint工具检查

什么是OCLint

github.com/oclint/ocli…

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

  • 可能的bug - 空的 if / else / try / catch / finally 语句
  • 未使用的代码 - 未使用的局部变量和参数
  • 复杂的代码 - 高圈复杂度, NPath复杂, 高NCSS
  • 冗余代码 - 多余的if语句和无用的括号
  • 坏味道的代码 - 过长的方法和过长的参数列表
  • 不好的使用 - 倒逻辑和入参重新赋值

安装和使用

brew 安装

brew tap oclint/formulae   
brew install oclint

安装包安装

安装包下载地址github.com/oclint/ocli…

解压下载文件。将文件存放到一个合适的位置。我是一般都放在 Home 目录,在终端编辑当前环境的配置文件,编辑 .bash_profile 。(如果使用系统的终端则编辑 .zshrc 文件)

OCLint_PATH=/Users/zhoucandy/oclint/
export PATH=$OCLint_PATH/bin:$PATH

然后执行 source ~/.bash_profile。哪种方式安装都需要配置环境变量

验证是否安装成功。在终端输入 oclint --version

截屏2022-01-06 下午4.50.44.png

查找安装的位置通过 which oclint

安装 xcpretty

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

gem install xcpretty

查看项目基本信息

xcodebuild -list

截屏2022-01-06 下午4.53.41.png

编译项目

#我这里是对于非pod项目的脚本,其他的脚本在下面都有
xcodebuild -scheme ObjcTest -project  ObjcTest.xcodeproj clean && xcodebuild -scheme ObjcTest -project ObjcTest.xcodeproj -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json

#对应pod项目的编译
xcodebuild -scheme ObjcTest -workspace ObjcTest.xcworkspace clean && xcodebuild -scheme ObjcTest -workspace ObjcTest.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json

截屏2022-01-06 下午4.59.16.png

编译成功后,会在项目的文件夹下出现 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
  • 如果项目工程太大,整个 lint 会比较耗时,所幸 oclint 支持针对某个代码文件夹进行 lint
oclint-json-compilation-database -i 需要静态分析的文件夹或文件 -- -report-type html -o oclintReport.html  其他的参数

OCLint 规则

OCLint是有一套默认的规则的. 你可以对这些默认的扫描规则做出修改, 如忽略一些规则或者改变某些规则的阈值, 这是我自己自定义的一些扫描规则:

# 扫描规则
# http://docs.oclint.org/en/stable/rules/index.html
# -- 共约
# 忽略 if折叠
#-disable-rule CollapsibleIfStatements \
# 忽略 直接使用变量
#-disable-rule ObjCAssignIvarOutsideAccessors \
# switch case 最少数量
#-rc=MINIMUM_CASES_IN_SWITCH=3 \
# --命名
# 变量名字最长字节
#-rc=LONG_VARIABLE_NAME=20 \
# 变量名字最短字节
#-disable-rule ShortVariableName \
# --size
# 圈复杂度
#-re=CYCLOMATIC_COMPLEXITY=10 \
# 每个类最行数
#-rc=LONG_CLASS=700 \
# 每行字节数量
#-rc=LONG_LINE=200 \
# 每个方法行数
#-rc=LONG_METHOD=80 \
# 忽略注释后括号后的有效代码行数
#-rc=NCSS_METHOD=40 \
# 嵌套深度
#-rc=NESTED_BLOCK_DEPTH=5 \
# 字段数量
#-rc=TOO_MANY_FIELDS=20 \
# 方法数量
#-rc=TOO_MANY_METHODS=30 \
# 方法参数
#-rc=TOO_MANY_PARAMETERS=6

根据规则写了下面的脚本

脚本检查

# oclint
workspaceExt=".xcworkspace"
tempPath=""
project_path=$(pwd)
project_name=$(ls | grep xcodeproj | awk -F.xcodeproj '{print $1}')

# 更新第三方库
if [ -f Podfile ]; then
echo "==========pod update========="
#pod update
fi
# find .xcworkspace
for workspacePath in `find ${project_path} -name "$project_name$workspaceExt" -print`
do
tempPath=${workspacePath}
break
done

echo "===========oclint=========="

if [ "$tempPath" == "" ];then
# 没有使用pod的项目
# oclint clean
xcodebuild -scheme ${project_name} -project ${project_name}.xcodeproj clean
echo "===========oclint=project=clean=done========="
# build
xcodebuild -scheme ${project_name} -project ${project_name}.xcodeproj -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json
echo "===========oclint=project=build=done========="
else
# 对应用了pod
# oclint clean
xcodebuild -scheme ${project_name} -workspace ${project_name}.xcworkspace clean
echo "===========oclint=workspace=clean=done========="
# build
xcodebuild -scheme ${project_name} -workspace ${project_name}.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json
echo "===========oclint=workspace=build=done========="
fi
oclint-json-compilation-database -v 
-e Pods
oclint_args -- -report-type html -o oclintReport.html 
-disable-rule ObjCAssignIvarOutsideAccessors 
-rc=MINIMUM_CASES_IN_SWITCH=3 
-rc=LONG_VARIABLE_NAME=20
-disable-rule ShortVariableName 
-rc=CYCLOMATIC_COMPLEXITY=10
-rc=LONG_CLASS=700
-rc=LONG_LINE=200
-rc=LONG_METHOD=80
-rc=NCSS_METHOD=40
-rc=NESTED_BLOCK_DEPTH=5
-rc=TOO_MANY_FIELDS=20
-rc=TOO_MANY_METHODS=30
-rc=TOO_MANY_PARAMETERS=6

# 删除 compile_commands.json 可能会很大
jsonPath=$project_path/"compile_commands.json"
#echo ${jsonPath}
rm $jsonPath
open oclintReport.html
exit

运行脚本的结果:

这里我只是写了一个测试项目,确实检查出来的问题确实存在。

截屏2022-01-06 下午6.24.49.png

如果执行出现这个错误: oclint: error: one compiler command contains multiple jobs

解决方案:

  • 修改这个 Xcode 的配置

截屏2022-01-06 下午5.22.02.png

  • Podfile 文件中加入以下代码
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

Xcode 配置检查脚本

也就是配置新的 target,然后添加检查脚本。

截屏2022-01-07 下午3.33.49.png

截屏2022-01-07 下午3.34.25.png

截屏2022-01-07 下午3.35.58.png

添加编译和分析脚本

export LC_CTYPE=en_US.UTF-8
cd ${SRCROOT}
xcodebuild -scheme ObjcTest -workspace ObjcTest.xcworkspace clean && xcodebuild -scheme ObjcTest -workspace ObjcTest.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json && oclint-json-compilation-database -e Pods -- -report-type xcode

截屏2022-01-07 下午3.44.56.png

Jenkins集成OCLint

  • 创建任务
  • 配置 Source Code Management
  • 添加脚本,直接把上面写好的脚本粘贴过去,注意项目路径
  • 增加 PMD-Plugin 插件,然后在任务中增加构建后的操作,配置下就可以构建了

截屏2022-01-07 下午2.51.51 3.png

Swift的静态分析工具

什么是SwiftLint

SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具。它的实现是 HookClangSourceKit 从而能够使用 AST 来表示源代码文件的更多精确结果。

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

安装和使用

通过homebrew安装

#这样安装时全局的,其他项目都可以使用
brew install swiftlint

通过Cocoapods安装

#这样安装是局部的,只在当前项目debug环境引入
pod 'SwiftLint', :configurations => ['Debug']

这种方式相当于把SwiftLint作为一个三方库集成进了项目,因为它只是调试工具,所以我们应该将其指定为仅Debug环境下生效。

在Xcode中Build Phases,添加一个Run Script Phase

  • 如果是通过 homebrew 安装的,你的脚本应该是这样的
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"

常用的命令

更新命令
brew upgrade swiftlint

//卸载命令
brew uninstall swiftlint

//查看当前版本号
$ swiftlint version

//查看所有命令
$ swiftlint help

//忽略空格导致的警告和错误
$ swiftlint autocorrect

//输出所有的警告和错误
$ swiftlint lint

//查看所有可获得的规则以及对应的 ID
$ swiftlint rules

添加配置文件

可以通过在你需要执行 SwiftLint 的目录下添加一个 .swiftlint.yml 文件的方式来配置 SwiftLint。可以被配置的参数有:

包含的规则:

  • disabled_rules: 关闭某些默认开启的规则。
  • opt_in_rules: 一些规则是可选的。
  • only_rules: 不可以和 disabled_rules 或者 opt_in_rules 并列。类似一个白名单,只有在这个列表中的规则才是开启的。

下面是配置文件的说明:

disabled_rules: # 执行时排除掉的规则
  - colon
  - comma
  - control_statement
opt_in_rules: # 一些规则仅仅是可选的
  - empty_count
  - missing_docs
  # 可以通过执行如下指令来查找所有可用的规则:
  # swiftlint rules
included: # 执行 linting 时包含的路径。如果出现这个 `--path` 会被忽略。
  - Source
excluded: # 执行 linting 时忽略的路径。 优先级比 `included` 更高。
  - Carthage
  - Pods
  - Source/ExcludedFolder
  - Source/ExcludedFile.swift

# 可配置的规则可以通过这个配置文件来自定义
# 二进制规则可以设置他们的严格程度
force_cast: warning # 隐式
force_try:
  severity: warning # 显式
# 同时有警告和错误等级的规则,可以只设置它的警告等级
# 隐式
line_length: 110
# 可以通过一个数组同时进行隐式设置
type_body_length:
  - 300 # warning
  - 400 # error
# 或者也可以同时进行显式设置
file_length:
  warning: 500
  error: 1200
# 命名规则可以设置最小长度和最大程度的警告/错误
# 此外它们也可以设置排除在外的名字
type_name:
  min_length: 4 # 只是警告
  max_length: # 警告和错误
    warning: 40
    error: 50
  excluded: iPhone # 排除某个名字
identifier_name:
  min_length: # 只有最小长度
    error: 4 # 只有错误
  excluded: # 排除某些名字
    - id
    - URL
    - GlobalAPIKey
reporter: "xcode" # 报告类型 (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging)

SwiftLint 的全部规则可以在:Source/SwiftLintFramework/Rules 目录内找到

这里有规则的简单说明: cloud.tencent.com/developer/a…

定义自定义规则

你可以用如下语法在你的配置文件里定义基于正则表达式的自定义规则:

custom_rules:
  pirates_beat_ninjas: # 规则标识符
    name: "Pirates Beat Ninjas" # 规则名称,可选
    regex: "([nN]inja)" # 匹配的模式
    match_kinds: # 需要匹配的语法类型,可选
      - comment
      - identifier
    message: "Pirates are better than ninjas." # 提示信息,可选
    severity: error # 提示的级别,可选
  no_hiding_in_strings:
    regex: "([nN]inja)"
    match_kinds: string

swiftlint.yml配置文件的嵌套 在我们使用.swift.yml配置文件的时候,如果在系统扫描的过程中发现了一个新的配置文件,那么子目录下的规则就会改为新的配置规则。

生成swiftlint.html文件

  • 直接 CMD + B 编译项目就会生成文件
  • 终端进入项目目录,然后执行
# reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
swiftlint lint --reporter html > swiftlint.html

牛刀小试

截屏2022-01-08 下午3.42.09.png

截屏2022-01-08 下午3.42.20.png

我这里定义的规则是属性的名称在3-40个字符,确实检测出了问题所在,接下来我也会开始在项目里面实践。

总结

总的来说,这些分析工具对于代码规范管理是利远大于弊的,只要在前期需要一些时间建立一个规范体系。在后期由于代码整体的规范性,能利于项目的维护,以及可以节省代码Code Review的时间。

参考文章

blog.csdn.net/shengpeng33…

juejin.cn/post/684490…