iOS 代码 lint 自动化试水

3,539

一、关于 lint

对于已经搭建CI开发环境的团队来说,代码规范管理必然会成为团队协作的基石。但人工的CodeReview中加入格式及规范检查会存在如下问题

  1. 技术含量低,大量重复劳动
  2. 容易出现误判
  3. 规范变更同步困难,难以维护

针对以上问题,代码lint概念诞生。

历史背景

相比其他技术,代码lint其实由来已久。维基百科解释,lint 作为一种工具程序,主要负责静态源码分析,负责代码规范、代码缺陷等检测,最早出现在 Unix 系统中,用于C语言代码检测。

使用场景

而在实际lint 场景中,凡是作为第三方检测校验的辅助工具都可算lint范畴。比如:

主要用途
commitizen lint 检测 git commit 提交信息规范
eslint 静态检测 js 代码规范及缺陷
imageSizeLint 检查项目中的异常尺寸图片检测

常用的代码 lint 库

语言 lint 库
Objective-C OCLint
Swift SwiftLint
java(android) AndroidStudio Lint
kotlin klint
javascript eslint
go golint

lint 可参与的生命周期

image

IDE 支持

Jetbrain 出品的系列 IDE(WebStorm、AndroidStudio、AppCode)、VSCode 都拥有丰富的 lint 插件,大多数是基于常用lint库开发的。上述的两个 IDE 除了Objective-C,其他语言基本都支持。 Xcode 已不再支持插件植入,但可以通过 Xcode 辅助API,在编译时进行 lint 错误日志输出,代表作为 OClint、SwiftLint。

其他周期

除了 IDE 支持,其他的生命周期均可使用 lint 库自带的命令行工具实现。

二、定制自己的 lint 工作流

看完上面的介绍,大家可能会想。我只要在 CI 阶段集成 lint 不就一劳永逸了?这里其实会类比一个场景--- SVN 和 Git 的特性差异,其中最主要的一个就是集中式分布式。 说回 lint 流,当一些 lint 检查,堆积在一个 PR 提交时再做最终拦截时,会有以下问题:

  1. 反馈链太长,要到最后 PR 跑 CI 才能监听到。
  2. CI单点反馈影响因素较多,包括网速、CI机器性能、代码集成等等。
  3. 修复体验差,无法针对已修复的问题做实时验证。 综上,我们需要根据分布式集中式的问题进行相互补充。

关于 集中式 lint 模式也有非常完善的实践,但侧重点和本文讨论主题有所差异,这里不做深入讨论。

多 repo 场景
根据上图,我们尝试解决如下问题:

  1. 在代码的不同阶段,均有 lint 进行监控。
  2. 每个阶段均有 lint 配置中心进行规则分发。
  3. 多个 repo 项目,同时由 lint 配置中心进行统一管理,特指后端微服务、前端多 repo 这种场景。
  4. 最终的信息收集会通过 CI 检测后,通过反馈服务进行邮件同步,反馈每次CI集成的质量,lint质量作为集成质量的一个子集。

如何实现 iOS 的 lint 流管理

根据上文的环节,我们需要逐步进行实现。

IDE编辑器实践

对于 iOS,你有如下选择

IDE Type OCLint SwiftLint IDE default lint
Xcode
AppCode
VSCode nonsupport

如上,除了 VSCode 不支持 OC 之外,其他编辑器大大小小都集成了lint环境。

其中,只有 AppCode 支持代码编辑实时反馈。

IDE default lint 基本不支持自定义规则制定,大家在使用时可以有所取舍。

Xcode lint 集成

Xcode 支持的lint检查,基本都是要基于编译的,所以 lint 结果查看位置比较特别,并不是在代码书写时实时反馈的,如下图所示。

image
image

OCLint 集成

OCLint 官网提供了《Use OCLint in Xcode》进行 Xcode 集成。并且为项目级别 lint,需要跟随编译进行lint检测。

SwitLint 集成

官网提供的《Use Swift in Xcode》,配置更简单,同样为项目级 lint,跟随编译进行。这里强烈推荐使用 Pod 集成,主要可以降低大家集成的成本(避开Homebrew,路径配置这些成本),其他好处下文 git lint 关节会提到。 但可以通过命令行方式进行单个文件 Lint,使用方式如下。

$ swiftlint lint <source file0> <source file1> ...

default lint

主要指 Clang Static Analyzer,是 Xcode 编译时自己产生的 warning 及 error,但缺乏代码风格检查,不过多赘述。

AppCode lint 配置

SwiftLint

使用 JetBrain 插件进行集成,具体可以参考这里

IDE Default lint

image
image
除了编辑是的实时反馈,手动 lint 和 JetBrain 其他编辑器一样,通过自带的 Inspect Code 功能实现,并且可以指定 lint 的范围和需要检测的规则,如上图。

VSCode

VSCode 仅支持 SwiftLint 的应用市场插件,使用非常小众,不做赘述。

IDE lint 总结

综上,考虑到统一性和扩展性,优先推荐 AppCode 进行日常代码的开发和 lint 反馈,更加轻量,不依赖编译。

编译 lint

在上文 IDE Lint 其实已经涉及到编译 lint,不管是 OCLint 还是 SwiftLint 天然支持,这里也不再过多赘述。

Git 提交 lint

本次试水耗时最长的环节,描述一下在这个时机做 lint 的优势。

  1. 只 lint 最小范围,即本次提交的 staged 区的文件,文件最少,性能最优。
  2. 老项目存在的通病,由于老旧代码没有适配新的 lint 规则,导致在项目 lint 适配之前,基本无法通过 IDE lint编译 lint
  3. 除了和上面的周期同样具备分布式性能优势,相对反馈周期相比CI也更及时,粒度为每个 commit。

所以,对于公司自己的项目,由于老旧代码 lint 规则适配周期较长,我们首先在项目中集成了这个时机的 lint。

Git Lint Flow

image

由于 OCLint 配置相对繁杂,我们先拿 SwiftLint 练手。如上图,我们需要按照流程实现这2个关键点。

  1. lint 环境配置。
  2. commit hook 触发器。

lint 环境配置

在上文的 IDE Lint 环节其实已经介绍了,SwiftLint 支持单文件的 lint。

$ swiftlint lint <source file0> <source file1> ...

但如何将其与 git hook 完美结合?

不出意外你会遇到以下问题:

  1. git hook 的维护困难,需要自己编写完 hook 脚本后,在群里告诉大家把脚本复制到 .git 目录中。
  2. git hook 默认使用 shell 编写,需要一定的 shell 经验。
  3. 如何判断出哪些是待提交的文件,虽然 Git 提供了相关的命令,但需要自己和 git hook 做事件绑定。
  4. 判断出待提交文件后,如何在 lint 失败时跳出 commit 并提供有好的提示?

这个在 iOS 领域基本没有相关的实践,不如我们抬头看下其他技术栈的解决方案。

image
上图是前端关于 eslint 的一个解决方案。

说一下这几个环节

husky:主要负责对 git hook 的管理,实际上是一个 node 编写的触发器,并且对 git hook 做了依赖反转,方便你的 repo 或者其他配置中心统一管理,避免 .git 目录频繁的复制粘贴。附《husky 使用教程》

lint-staged:主要负责检测 Git 待提交的文件,并将 list 提交给 lint 工具。附《lint-staged 使用教程》

lint 工具eslintSwiftLint 等 lint 工具。

动手实践

  1. 在项目根目录配置 package.json
{
  "name": "ycMath",
  "version": "0.0.1",
  "private": true,
  "devDependencies": { # 在开发环境配置两个工具
    "husky": "^2.7.0",
    "lint-staged": "^8.2.1"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged" # 指定在 pre-commit hook 时触发 lint-staged
    }
  },
  "lint-staged": {
    "linters": {
      "*.swift":["Pods/SwiftLint/swiftlint lint"] # 使用pod安装 SwiftLint
    }
  }
}
  1. 根目录执行 npm install(这里默认大家都安装了 npm,因为实在太高频使用了😂)
$ npm install

note!: 这里不要忘记在 .gitignore 文件里忽略根目录的 node_modules 这个文件夹。

  1. 创建一个新的 swift 文件进行验证
$ vim AppDelegate.swift # 在文件里写一写超长注释,比如218个字符的注释。
  1. 得到反馈结果,并提交失败
$ git add .
$ git commit -m "test"
husky > pre-commit (node v8.9.1)
  ↓ Stashing changes... [skipped]
    → No partially staged files found...
  ❯ Running linters...
    ❯ Running tasks for *.swift
      ✖ Pods/SwiftLint/swiftlint lint

✖ Pods/SwiftLint/swiftlint lint found some errors. Please fix them and try committing again.
/Users/chaoyang/Dev/testLint/AppDelegate.swift
⛔️ Line 27: Line should be 120 characters or less: currently 218 characters
⚠️ Line 40: Line should be 120 characters or less: currently 128 characters
...

至此,你的 git lint 环节已经完成功能。

基于 DRY 继续优化

在把这些教程和原理同步大家后,发现很多人会忘记 npm install lint 工具,或者不知道如何安装。本着 DRY 原则,能不重复的工作尽量不要重复。 那么我需要解决以下问题:

  1. 如何探测大家的 lint 环境是否安装完毕?
  2. 如果有探测方案,应该选在什么时机?
  3. 如果探测到,如何提示开发者进行安装?

针对问题,又开始了新一轮的探索,终于在《基于 cocoapod 进行 iOS 开发》这片文章找到了灵感。

  1. 配置你的 Podfile,在里面添加如下 ruby 代码
## lint 插件校验
pre_install do |installer|
  # 获取插件路径
  sandbox_root = Pathname(installer.sandbox.root)
  ycroot = File.expand_path("..", sandbox_root)
  node_module_lint_stage = File.expand_path("node_modules/lint-staged/",ycroot)
  node_module_husky = File.expand_path("node_modules/husky",ycroot)
  
  has_lint_stage = File.exist?(node_module_lint_stage)
  has_husky = File.exist?(node_module_husky)
  
  # 检查是否安装 lint-staged 及 husky
  if has_husky && has_lint_stage
    Pod::UI.puts "lint staged 和 husky 存在,请继续。"
  else
    # 方案一
    raise "warning by @SuperYang\n\nYCLog error: lint staged 或 husky 不存在, 请使用以下命令安装: npm install "

    # 这里的异常提示可能不太友好,如果你觉得大家本地都是必装 npm 的,你可以使用下面的方案二
    # 方案二
    # Pod::UI.puts "lint staged 和 husky 不存在,正在尝试安装。"
    # `rm -rf node_modules && npm install&&echo lint staged 和 husky 安装完毕`
    # Pod::UI.puts "lint staged 和 husky 完成安装🍺🍺🍺~~~"
  end
end
  1. 在根目录删除 node_modules 目录。
$ rm -rf node_modules
  1. 执行 pod install或update 命令。
$ pod install
  1. 查看错误提示
# 方案一
[!] An error occurred while processing the pre-install hook of the Podfile.

warning by @SuperYang

YCLog error: lint staged 或 husky 不存在, 请使用以下命令安装: npm install
# 方案二
lint staged 和 husky 不存在,正在尝试安装。
lint staged 和 husky 完成安装🍺🍺🍺~~~

强烈推荐方案二,实测额外问题很少,只有几个人的新电脑忘装npm

CI Lint 配置 & 自动化 lint 报告

上文花了大幅度的篇幅介绍了几个场景的 lint 玩法,关于CI lint 和 反馈报告发送环节,本文不做详细介绍,之后会在项目实战中进行测试,完成后会通过博客分享给大家。

三、总结

  • 回顾一下我们的知识点

    1. 基于分布式思想的 lint 架构设计。
    2. 对于不同生命周期的lint实践
    3. 对于 git commit lint 环节的设计及实践。 以上环节,git lint 是使用后感觉最轻量的,也是最容易自动化完成的流程。对于老旧项目对lint规则还未及时适配的,优先推荐进行实践,从文件级别进行检测,也是业界比较常用的 lint 切口。
  • 以上方案的大多数设计,同样适用于其他技术栈。

  • 当一个问题出现时,首先要接受他的必然性,同时尽量避难闭门造车,同样的问题,其他人肯定也会遇到,更不要把自己束缚在自己的技术栈。拥抱更广的社区,拥抱其他技术栈😎。

  • 本文涉及的其他技术栈nodeJSRubyGit等。