我们都有过这样的经历,我们在一个项目上工作,随着时间的推移,我们以不同的方式写出了类似的代码(要么是两个不同的开发者写的,要么是同一个开发者写的)。我们有两个代码块,它们遵循相同的逻辑,但看起来却不一样;而我们不得不做出额外的努力来理解它们,只因为代码是以不同的方式写的。
定义一个代码风格可以防止这种情况,但我们需要一种方法来强制执行它。在这篇文章中,我们将展示我们使用StandardRB(和RuboCop)的设置,通过保持一致的风格来帮助开发者提高代码的质量。
标准RB
StandardRB是一个建立在RuboCop之上的宝石,它定义了一系列的规则。它在引擎盖下使用RuboCop,但它也提供了自己的standardrb
可执行文件和配置选项。
为什么
我们不想浪费时间为RuboCop定义我们自己的特定规则集,所以我们决定使用StandardRB作为一个起点。它所定义的大多数规则对我们来说都是很合适的。
限制
最终,我们发现了一些限制。
- 我们不得不改变一个规则(*),而遵守StandardRB的整个想法是你不会改变配置。
- StandardRB不正式支持RuboCop扩展(如
rubocop-rails
,rubocop-rspec
或rubocop-minitest
)。
(*) 我们使用双启动技术来测试当前和下一个Rails版本,所以我们需要禁用
Bundler/DuplicatedGem
规则。
RuboCop
RuboCop是一个用于Ruby代码的linter/formatter。我们不想遵循默认的规则,因为它需要我们做很多改变(比如字符串的默认单/双引号配置),而且我们想使用扩展来添加新的规则。
可扩展性
RuboCop可以使用其他宝石来扩展,这些宝石将定义新的警察。你可以在这里找到一个扩展列表,你也许可以找到更多没有列在这里的扩展,甚至可以建立你自己的。
设置
所以,我们的设置使用RuboCop,但它使用StandardRB配置作为基础,我们只在此基础上添加一些定制的东西(只有当我们需要时)。
我们首先添加standard
gem和我们想要的扩展。StandardRB依赖于RuboCop,所以我们不需要添加这个明确的依赖关系,但如果你想定义一个特定的版本,你可以添加它。
# Gemfile
group :development, :test do
gem "standard", require: false
gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
end
然后,我们将使用项目根部的一个.rubocop.yml
文件来配置RuboCop。
扩展
我们必须告诉RuboCop我们要使用哪些扩展,因此,在.rubocop.yml
文件的开头,我们要添加。
require:
- standard
- rubocop-rails
- rubocop-rspec
Inherit Config
由于我们想使用StandardRB配置作为基础配置,我们也必须告诉RuboCop。我们可以通过inheriting
gem的配置来做到这一点。
require:
- standard
- rubocop-rails
- rubocop-rspec
inherit_gem:
standard: config/base.yml
排除文件
有时你想排除一些文件或目录,这样RuboCop就可以忽略它们了。例如,如果你的项目在vendor
目录中安装了宝石,你不希望RuboCop处理它们,它只是为你不会修复的东西花费时间。你可能也不希望RuboCop检查node_modules
目录中是否有什么东西需要分析,因为在那里可能没有Ruby代码的情况下,这样做只会花费时间。
所以我们要增加更多的配置。
require:
- standard
- rubocop-rails
- rubocop-rspec
inherit_gem:
standard: config/base.yml
AllCops:
NewCops: enable
Exclude:
- node_modules/**/*
- public/**/*
- vendor/**/*
配置
现在我们有了StandardRB的基本配置,我们可以对其进行小的调整。
require:
- standard
- rubocop-rails
- rubocop-rspec
inherit_gem:
standard: config/base.yml
AllCops:
NewCops: enable
Exclude:
- node_modules/**/*
- public/**/*
- vendor/**/*
Rails:
Enabled: true # enable rubocop-rails cops
RSpec:
Enabled: true # enable rubocop-rspec cops
RSpec/DescribeClass:
Enabled: false # ignore missing comments on classes
Bundler/DuplicatedGem:
Enabled: false # ignore duplicated gem errors because we will have duplicated gems when dual booting
在一个干净的项目中
如果你把它添加到一个新项目中,你就可以使用这个配置了。你可以bundle exec rubocop -A
,以便它为你的代码应用任何自动样式修正。只要添加所有的修改,然后发货就可以了
在一个现有的项目中
如果你要把它添加到一个已经写了很多代码的项目中,你会发现bundle exec rubocop -A
不能够自动修复所有的东西。如果不能修复的违规行为数量不多,你可能想立即修复它们,但有时这是不可能的(也许有数百个违规行为,或者你需要与你的团队讨论如何重构一个长的方法,等等)。
为此,我们可以使用一个todo
文件,该文件将包括所有我们最终应该修复的违规行为,但将配置RuboCop暂时忽略它们,直到我们有带宽来修复它们。
如果我们运行bundle exec rubocop --auto-gen-config
,RuboCop将创建一个.rubocop_todo.yml
文件,其中包括所有我们必须修复的TODO,并将更新.rubocop.yml
文件以继承.rubocop_todo.yml
文件中的配置。
TODO或不TODO
虽然todo
文件确实很有帮助,但它对完整的文件禁用了特定的违法行为,而不仅仅是在生成时发现的特定行。这是有道理的,因为代码会发生变化,而todo
文件无法跟踪这些变化,但这也增加了在一个已经禁用了特定代码的文件上引入新的罪行的空间。
相反,我们使用两个不同的配置文件。
- 一个
.rubocop.yml
,它不会继承TODO,我们可以在代码编辑器中使用这个文件,这样我们在开发过程中就可以在每个文件中看到违法行为。 - 一个将使用TODO的
.rubocop_with_todo.yml
,我们可以在预提交钩子或CI上使用它,以便在这些情况下保持TODO的配置(我们不想为他们没有碰过的文件对开发人员大喊大叫!)。
首先,我们将.rubocop.yml
文件复制为.rubocop_with_todo.yml
。然后,我们从.rubocop.yml
文件中删除inherit_from: .rubocop_todo.yml
行,这样它就不包括TODO。
因此,默认情况下,RuboCop将使用.rubocop.yml
配置,但我们可以在运行rubocop
命令时用一个标志告诉它使用.rubocop_with_todo.yml
。bundle exec rubocop -c .rubocop_with_todo.yml
最终,当你清除TODO文件时,你可以将此还原为只有一个
.rubocop.yml
文件,而没有todo
,更少的复杂性,更少的东西需要记忆,也没有隐藏的犯罪行为
Pre-commit Hook
对于我们的内部项目,我们使用Overcommit来管理不同的git hook。我们在提交前钩子中使用RuboCop,在这些代码被推送到 repo之前检测代码风格的违规行为。
对于这个钩子,我们想使用TODO配置,所以在这种情况下,Overcommit的配置将是
# .overcommit.yml
PreCommit:
RuboCop:
enabled: true
---
PreCommit:
RuboCop:
enabled: true
command: ["bundle", "exec", "rubocop", "-c", ".rubocop_with_todo.yml"]
CI
由于Overcommit预提交钩子可以被跳过或禁用,我们也想在CI上运行RuboCop。同样,在这种环境下,我们想使用TODO,所以我们将有一个CI工作,运行bundle exec rubocop -c ./.rubocop_with_todo.yml
。具体的设置将取决于你的CI服务。
开发
我们鼓励所有的开发者在他们选择的代码编辑器中添加一个RuboCop插件,这样编辑器就会为每个打开的文件显示提示。
你可以在这里找到不同编辑器的所有插件的列表。
在这种情况下,我们不希望使用TODO,我们希望开发人员看到每个文件的所有违规行为。这样就可以更容易地知道我们没有引入新的违法行为,而且当我们想去修复它们时,它可以帮助我们。
不同的插件将默认使用.rubocop.yml
文件,所以我们在这里没有什么需要改变的。
Formatter
另外,代码编辑器可以被配置为使用RuboCop作为格式化器,它将在保存时为任何文件修复代码样式。
我们也可以在每次要修复样式时运行bundle exec rubocop -A
,但代码编辑器可以为我们做这个。
结论
通过定义和执行代码规则,我们节省了讨论代码风格所浪费的时间,特别是在代码审查期间。我们可以时不时地讨论一些具体的事情,并在需要时调整配置,但几乎所有的时候我们都决定遵循已执行的规则,把注意力放在代码的作用上,而不是放在它的外观上。
另外,一致的风格有助于不同项目中的开发人员入职,他们知道应该期待什么,所以没有额外的精力去学习新的规则或理解用多种不同风格编写的代码。
最后,在这个设置中,我们使用了两个文件,一个有TODO,一个没有TODO,我们通过不为开发人员隐藏它们来防止引入新的违规行为,但我们也在CI和预提交钩子上隐藏了已知的违规行为,以避免在一个大的代码库中一次修复所有的违规行为。