前言

我司今年开始尝试一些代码质量相关建设,比如组织 codereview、修复代码扫描漏洞.这是一个很好的现象,当我们为了快速迭代,往往为了让需求上线,导致代码并使很规范,时间长了就留下了一堆技术债.

前日的一天,iOS 老哥找我说让看看能不能弄弄 iOS 代码扫描,扫描出一些代码漏洞,尝试去修复漏洞和 bug.

于是乎,下面就是我记录一下折腾了几天完成的 iOS 代码扫描初探的过程.

工具选择

从去年开始,就一直研究 iOS 代码扫描这款.无奈乎,iOS 在代码扫描这个领域能选的工具其实不算太多.

这次主要介绍如下几个工具:

  1. oclint
  2. infer
  3. sonar-swift

oclint + sonarqube 方案

所需安装工具一览

下面是在 mac 机器上安装的工具

oclint

OCLint 是基于 Clang Tooling 开发的静态分析工具,主要用来发现编译器检查不到的那些潜在的关键技术问题.

命令安装

brew tap oclint/formulae
brew install oclint

下载安装包安装

https://github.com/oclint/oclint/releases

配置环境变量

OCLint_PATH=/Users/xinxi/Documents/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH

source .bash_profile

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

xcpretty

用于对 xcodebuild 的输出进行格式化

gem install xcpretty

用法

紧跟在 xcodebuild 相关语句后面,比如:

xcodebuild [flags] | xcpretty

可以结合 tee 进行日志收集

xcodebuild [flags] | tee xcodebuild.log | xcpretty

sonarqube 安装

官网地址,sonarqube 分社区版本和商业化版本,能扫描多种语言并且开源

https://www.sonarqube.org/downloads/

docker 安装

docker pull sonarqube:8.6-community

二进制文件安装

在 bin/macosx-universal-64 目录下的输入:

sh sonar.sh start

控制台输出"Started SonarQube"说明启动成功.

在浏览器访问,能打开页面说明启动成功.

http://127.0.0.1:9000/

需要说明的是 SonarQube 如果想持久化保存数据,是需要依赖 mysql 数据库的.

SonarQube 默认提供 H2 存储,只能暂时存储一些小项目结果,仅为了演示使用.

在 conf/sonar.properties 下配置数据库地址即可.

可选 MySQL、Oracle、PostgreSQL

sonar-objective-c 插件

sonarqube 默认没有扫描 oc 的检查,sonarqube 官方的 sonar-objective-c 插件是收费的.

需要在找一个免费的插件,在 github 找到两个项目

插件一

https://github.com/Backelite/sonar-objective-c

这个插件在三年前没有修改了,在使用中发现有些扫描规则并没有.

插件二

这个项目稍微更新的时间短一些,有些规则适当的更新了

https://github.com/raatiniemi/sonar-objective-c

下载插件放到/extensions/plugins 目录下

sonar-scanner

sonar-scanner 用来扫描本地代码,并且上传到 SonarQube 平台中.

下载地址: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/

按照不同的操作系统选择不同安装包即可.

配置环境变量:

vim /etc/profile

export SONAR_HOME=/usr/local/sonarqube-6.7.5
export SONAR_SCANNER_HOME=/usr/local/sonar-scanner
PATH=$PATH:$SONAR_HOME/bin:$SONAR_RUNNER_HOME/bin

source /etc/profile

sonar-scanner 分为两种使用方式:

配置文件方式:

在项目根目录下新建 sonar-project.properties 文件,内容如下:

//项目的key

sonar.projectKey=projectKey

//项目的名字

sonar.projectName=projectName

//项目的版本

sonar.projectVersion=1.0.0

//需要分析的源码的目录,多个目录用英文逗号隔开
sonar.sources=D:/workspace/Demo/src

进入项目根目录下,然后输入 “sonar-scanner” 命令,执行代码分析

命令行方式:

在命令中设置了参数

sonar-scanner -Dsonar.host.url=http://sonarqube -Dsonar.projectKey=app -Dsonar.sources=.

项目实验

上面的软件安装完成后,基本上具备的代码扫描的条件.找一个开源项目实验下

使用网络库 AFNetworking 项目:https://github.com/AFNetworking/AFNetworking

脚本

xcodebuild -workspace AFNetworking.xcworkspace -scheme AFNetworking\ iOS -sdk iphonesimulator11.2 -configuration Debug clean

xcodebuild -workspace AFNetworking.xcworkspace -scheme AFNetworking\ iOS -sdk iphonesimulator11.2 -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json

oclint 生成的报告中如下形式的规则会导致 Objective-c 分析插件出错(ERROR: The rule 'OCLint:compiler warning' does not exist, 刚才上面提到的 sonar-objective-c 插件并没有处理 warning 这些规则. 所以需要通过脚本删除这个结果.

脚本

#!/usr/bin/python

import xml.etree.ElementTree as ET
import os

os.system('mv oclint.xml oclint.xml.origin')
tree = ET.ElementTree(file='oclint.xml.origin')
root = tree.getroot()
del_items = []
for child in root:
    for one in child:
        if one.attrib['ruleset'] == 'clang':
            print child.attrib['name']
            del_items.append(child)
            break

for del_item in del_items:
    root.remove(del_item)

tree.write('oclint.xml')

在和 oclint.xml 一个目录下,执行该脚本

将如下内容保存为 sonar-project.properties 文件,放到 AFNetworking 目录下

sonar.projectKey=AFNetworking
sonar.host.url=http://localhost:9000
sonar.login=admin
sonar.password=admin

sonar.language=objc
sonar.objectivec.workspace=AFNetworking.xcworkspace
sonar.objectivec.appScheme=AFNetworking iOS
sonar.sources=AFNetworking

sonar.objectivec.oclint.report=oclint.xml

扫描结果图一:

扫描结果图二:

问题记录

在使用 demo 中非常顺滑,没什么问题.但是接入了实际项目,出现了如下问题.

编译项目失败

解决方案:

命令行编译的问题,必须携带参数" COMPILER_INDEX_STORE_ENABLE=NO"

oclint: error: violations exceed threshold

解决方案:

maxPriority=15000

${oclint_in} ${oclint_ex} -- -o=$BUILD_WORK_DIR/oclint/lint.xml -report-type=pmd -stats -max-priority-1=$maxPriority -max-priority-2=$maxPriority -max-priority-3=$maxPriority -rc LONG_LINE=500 -rc LONG_VARIABLE_NAME=10

如果扫描的生成的 compile_commands.json 文件过大,oclint-json-compilation-database 会提示出错 “OSError: [Errno 7] Argument list too long”

这个问题在网上看了很多帖子都是如下解决方案,但是实际中使用根本没有解决问题.

https://github.com/oclint/oclint/issues/233有网友给出解决方案
https://github.com/wuwen1030/oclint_argument_list_too_long_solution/tree/master

解决方案:

oclint-json-compilation-database 可以过滤不想扫描的文件和需要扫描的文件夹

-e 忽略扫描和-i 是指定扫描路径

oclint-json-compilation-database -e pods -i build

虽然使用上面的命令扫描,不报错误,但是在平台中扫描的 bug 数是 0,这个问题目前一直未解决.

mysql 存储问题

infer + sonar-swift

基于上面失败方案一度想放弃,但是无意中在社区中,看到了好未来开源的 iOS 代码扫描的帖子"我们开源了一款 SonarQube iOS 代码扫描插件",https://testerhome.com/topics/26967, 又激起了我想重新尝试的勇气.

github 地址:

https://github.com/tal-tech/sonar-swift

简单看了一下需要工具,需要 infer、xcpretty、sonar、sonar-swift 插件.

扫描规则:

https://github.com/tal-tech/sonar-swift/blob/master/docs/rule.md

这次尝试并没有急于着手干,看到帖子下面有个微信群并加了群

询问了开发者一些细节,确认是可以扫描 oc 项目的.

其中和开发者聊到工具怎么选择,推荐我在极客时间学习https://time.geekbang.org/column/article/87477,当然这个是付费的专栏.

sonar-swift

插件地址:

https://github.com/tal-tech/sonar-swift/releases

当时我下载的是 v1.0.2 版本,把插件放到/extensions/plugins 目录下,重启 sonar

脚本

官方提供的脚本

xcodebuild clean build -workspace app.xcworkspace -scheme scheme -destination 'generic/platform=iOS' COMPILER_INDEX_STORE_ENABLE=NO | tee xcodebuild.log > /dev/null
xcpretty -r json-compilation-database -o compile_commands.json < xcodebuild.log > /dev/null
# --skip-analysis-in-path 是忽略扫描目录
infer run --skip-analysis-in-path Pods --compilation-database compile_commands.json

# 可选,如果有 swift 语言使用
swiftlint lint > swiftlint.txt

lizard --xml > lizard-report.xml
sonar-scanner -Dsonar.host.url=http://sonarqube -Dsonar.projectKey=app -Dsonar.sources=. -Dsonar.swift.swiftlint.report=swiftlint.txt -Dsonar.swift.lizard.report=lizard-report.xml -Dsonar.swift.infer.report=infer-out/report.json

infer

infer 是 facebook 开源的一款代码扫描软件,可以分析 Objective-C, Java 或者 C 代码,报告潜在的问题

在 releases 页面中下载二进制文件

https://github.com/facebook/infer/releases

设置环境变量

tar xf infer-osx-vXX.tar.xz
# this assumes you use bash, adapt to your needs in case you use
# another shell
echo "export PATH=$PATH:`pwd`/infer-osx/infer/infer/bin" \
     >> ~/.bashrc && source ~/.bashrc

通过 infer --version 查看 infer 版本信息,说明安装成功.

扫描 iOS 命令:

infer -- xcodebuild -workspace "test.xcworkspace" -scheme "scheme"

扫描出的结果会在工程目录下的 infer-out 文件中,其中具体的代码会以 csv,txt,json 的格式分别存在对应的文件中。可以供我们分析.

扫描过程

infer 扫描阶段

扫描的 bug 数量

扫描规则

结果上传成功

sonar 平台展示数据

扫描结果图三:

扫描结果图四:

从下载代码到上传扫描结果,大概 1 小时 30 分支,和项目规模成正比.

问题记录

问题 1

解决方案: lizard 这个报告不要了,暂时去掉

问题 2

java 包中没有这个规则

解决方案:

1、用 -Dsonar.exclusions=文件路径这个排除

2、在 report.json 中删除这个规则

问题 3

没有这个规则

解决方案:使用新版本的 jar 包

问题 4

有个异常,去掉-Dsonar.swift.swiftlint.report=swiftlint.txt

问题 5

因为每次扫描都是增量扫描,如果使用多个分支同一个项目扫描,结果会被覆盖,sonar 本身也不支持多个分支扫描.

每次扫描的时候想知道是扫描的哪个版本的数据,通过参数-Dsonar.projectVersion 参数可以上传版本号.

shell 中获取版本号

version_number=`sed -n '/MARKETING_VERSION/{s/MARKETING_VERSION = //;s/;//;s/^[[:space:]]*//;p;q;}' ./PROJECTNAME.xcodeproj/project.pbxproj`

shell 中获取版本号构建号

build_number=`sed -n '/CURRENT_PROJECT_VERSION/{s/CURRENT_PROJECT_VERSION = //;s/;//;s/^[[:space:]]*//;p;q;}' ./PROJECTNAME.xcodeproj/project.pbxproj`

在活动页面展示了版本号

结语

经过折腾了几天,也算顺利的完成了基础环境搭建,能正常扫描出结果来了.

给我最大的启发是:

做事需要有专业的"社区",如果我没有去 testerhome 中有浏览的习惯,也很难找到不错的工具.

做事需要有专业的"圈子",专业的人做专业事,方可事半功倍.

最后放一张截图, 技术共享、开源万岁.

参考

如何使用脚本读取 Xcode 11 中的当前应用程序版本

https://stackoom.com/question/3q09t/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC%E8%AF%BB%E5%8F%96Xcode-%E4%B8%AD%E7%9A%84%E5%BD%93%E5%89%8D%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E7%89%88%E6%9C%AC

iOS+Jenkins 持续构建 - 代码扫描

https://www.jianshu.com/p/c0d49bcefeb0

使用 Jenkins+OCLint+SonarCube 对 iOS 项目进行代码分析

https://juejin.cn/post/6844903575680729102

iOS 静态代码扫描平台 Sonarqube 实战 Objective-C、Swift

https://testerhome.com/topics/13158

ios fastline sonarqube

https://medium.com/@aamir.ali/sonarqube-integration-with-fastlane-in-ios-3cd33e5abdc8

oclint_argument_list_too_long_solution 解决方案

https://github.com/wuwen1030/oclint_argument_list_too_long_solution

OCLint 静态代码检测实践

https://juejin.cn/post/6844904017424809998

OCLint 基本使用

https://www.jianshu.com/p/b2513f16d246

iOS 静态代码扫描平台 Sonarqube 实战 Objective-C、Swift

https://testerhome.com/topics/13158

Sonarqube & ObjectiveC 环境搭建

https://www.jianshu.com/p/5a01e56176bf


↙↙↙阅读原文可查看相关链接,并与作者交流