开发
本节文档将教你如何开发新的 Cops。我们将从生成一个 Cop 模板开始,然后我们将讨论其实现的各个方面(与 AST 交互、自动修正、配置)以及测试。
创建新的 cop
克隆仓库并运行 bundle install (如果尚未完成)。以下 rake 任务只能在 rubocop 项目目录本身内运行。
|
使用捆绑的 rake 任务 new_cop
生成 cop 模板
$ bundle exec rake 'new_cop[Department/Name]'
[create] lib/rubocop/cop/department/name.rb
[create] spec/rubocop/cop/department/name_spec.rb
[modify] lib/rubocop.rb - `require_relative 'rubocop/cop/department/name'` was injected.
[modify] A configuration for the cop is added into config/default.yml.
Do 4 steps:
1. Modify the description of Department/Name in config/default.yml
2. Implement your new cop in the generated file!
3. Commit your new cop with a message such as
e.g. "Add new `Department/Name` cop"
4. Run `bundle exec rake changelog:new` to generate a changelog entry
for your new cop.
基础知识
RuboCop 使用 parser 库来创建代码的抽象语法树 (AST) 表示。
您可以安装 parser
gem 并使用 ruby-parse
命令行工具来检查 AST 在输出中的样子。
$ gem install parser
然后尝试使用 ruby-parse
解析一个简单的整数表示。
$ ruby-parse -e '1'
(int 1)
每个用括号括起来的表达式代表 AST 中的一个节点。第一个元素是节点类型,尾部包含所有表示代码所需信息的所有子节点。
以下是一个示例 - 将字符串值 "John" 赋值给局部变量 name
$ ruby-parse -e 'name = "John"'
(lvasgn :name
(str "John"))
检查 AST 表示
假设我们要将语句从 !array.empty?
简化为 array.any?
首先,检查错误代码在抽象语法树表示中的返回值。
$ ruby-parse -e '!array.empty?'
(send
(send
(send nil :array) :empty?) :!)
现在,是时候使用 RuboCop 的 REPL 调试我们的表达式了
$ bin/console
首先,我们需要声明要匹配的代码,并使用 ProcessedSource,它是一个简单的包装,用于让解析器解释代码并构建 AST
code = '!something.empty?'
source = RuboCop::ProcessedSource.new(code, RUBY_VERSION.to_f)
node = source.ast
# => s(:send, s(:send, s(:send, nil, :something), :empty?), :!)
该节点有一些属性,在旅程中可能会有用
node.type # => :send
node.children # => [s(:send, s(:send, nil, :something), :empty?), :!]
node.source # => "!something.empty?"
实现
编写节点模式规则
您可以编写不使用 NodePattern 的 cop(许多旧的 cop 不使用它),但它通常会简化很多代码,因为手动节点匹配和解构可能非常冗长。
|
现在您已经熟悉了 AST,您可以了解一下 节点模式 并使用模式来匹配您想要匹配的特定节点。
您可以在 此处 了解有关节点模式的更多信息。
节点模式匹配的内容与 AST 表示的当前输出非常相似,那么让我们从一个非常通用的内容开始
NodePattern.new('send').match(node) # => true
它匹配是因为根节点是 send
类型。现在让我们使用括号来定义子节点的详细信息,从而深入匹配它。如果您不关心内部节点是什么,可以使用 ...
跳过它,只考虑 "一个节点"。
NodePattern.new('(send ...)').match(node) # => true
NodePattern.new('(send (send ...) :!)').match(node) # => true
NodePattern.new('(send (send (send ...) :empty?) :!)').match(node) # => true
有时很难理解您使用模式构建的复杂表达式,那么,如果您对围绕深层的节点模式括号感到困惑,请尝试使用 $
来捕获内部表达式并准确检查表达式的每个部分
NodePattern.new('(send (send (send $...) :empty?) :!)').match(node) # => [nil, :something]
在内部节点中,严格接收发送不是必需的,因为它也可能是一个字面量数组,例如
![].empty?
上面的代码具有以下表示形式
=> s(:send, s(:send, s(:array), :empty?), :!)
可以使用 ...
跳过内部节点,以确保它只是另一个内部节点
NodePattern.new('(send (send (...) :empty?) :!)').match(node) # => true
换句话说,它表示:“匹配调用 !<expression>.empty?
的代码”。
太好了!现在,让我们实现我们的 cop 来简化此类语句
$ rake 'new_cop[Style/SimplifyNotEmptyWithAny]'
在生成 cop 脚手架后,将节点匹配器更改为与之前获得的表达式匹配
def_node_matcher :not_empty_call?, <<~PATTERN
(send (send $(...) :empty?) :!)
PATTERN
请注意,我们在 !<expression>.empty?
中添加了一个 $
符号来捕获“表达式”,它将在稍后变得有用。
熟悉 parser
和 rubocop-ast
提供的 AST 节点钩子。
由于它以 send
类型开头,因此需要实现 on_send
方法,正如 cop 脚手架已经建议的那样
def on_send(node)
return unless not_empty_call?(node)
add_offense(node)
end
on_send
回调是最常用的,可以通过使用常量 RESTRICT_ON_SEND
限制可接受的方法名称来优化它。
最终的 cop 代码将类似于以下内容
module RuboCop
module Cop
module Style
# `array.any?` is a simplified way to say `!array.empty?`
#
# @example
# # bad
# !array.empty?
#
# # good
# array.any?
#
class SimplifyNotEmptyWithAny < Base
MSG = 'Use `.any?` and remove the negation part.'.freeze
RESTRICT_ON_SEND = [:!].freeze # optimization: don't call `on_send` unless
# the method name is in this list
def_node_matcher :not_empty_call?, <<~PATTERN
(send (send $(...) :empty?) :!)
PATTERN
def on_send(node)
return unless not_empty_call?(node)
add_offense(node)
end
end
end
end
end
请注意,on_send
将在给定 node
上调用,然后才会调用其子节点的回调 on_<some type>
。还有一个回调 after_send
,它在处理完子节点后调用。所有类型都有类似的 after_<some type>
回调,除了那些从不包含子节点的类型。
更新规范以涵盖预期语法
describe RuboCop::Cop::Style::SimplifyNotEmptyWithAny, :config do
it 'registers an offense when using `!a.empty?`' do
expect_offense(<<~RUBY)
!array.empty?
^^^^^^^^^^^^^ Use `.any?` and remove the negation part.
RUBY
end
it 'does not register an offense when using `.any?` or `.empty?`' do
expect_no_offenses(<<~RUBY)
array.any?
array.empty?
RUBY
end
end
如果您的代码包含不同长度的变量,可以使用 %{foo}
、^{foo}
和 _{foo}
来格式化模板;您还可以使用 […]
来缩写违规消息
%w[raise fail].each do |keyword|
expect_offense(<<~RUBY, keyword: keyword)
%{keyword}(RuntimeError, msg)
^{keyword}^^^^^^^^^^^^^^^^^^^ Redundant `RuntimeError` argument [...]
RUBY
%w[has_one has_many].each do |type|
expect_offense(<<~RUBY, type: type)
class Book
%{type} :chapter, foreign_key: 'book_id'
_{type} ^^^^^^^^^^^^^^^^^^^^^^ Specifying the default [...]
end
RUBY
end
自动更正
自动更正可以帮助人类自动修复已检测到的违规行为。需要 extend AutoCorrector
。方法 add_offense
会产生一个更正器对象,它是 parser 的 TreeRewriter 的一个薄包装器,您可以向其提供有关如何处理违规节点的说明。
让我们从一个简单的规范开始,以涵盖它
it 'corrects `!a.empty?`' do
expect_offense(<<~RUBY)
!array.empty?
^^^^^^^^^^^^^ Use `.any?` and remove the negation part.
RUBY
expect_correction(<<~RUBY)
array.any?
RUBY
end
然后在 cop 侧添加自动更正块
extend AutoCorrector
def on_send(node)
expression = not_empty_call?(node)
return unless expression
add_offense(node) do |corrector|
corrector.replace(node, "#{expression.source}.any?")
end
end
更正器允许您 insert_after
、insert_before
、wrap
或 replace
特定节点或代码中的任何特定范围。
范围可以在 node.location
上确定,它会为表达式或节点持有的其他内部信息提供特定的范围。
防止覆盖
更正器会检测并防止更正重叠的节点,以防止一个更正覆盖另一个更正。通过多次传递来支持嵌套更正,并跳过对嵌套节点的更正。这可以使用 IgnoredNode
模块来实现
extend AutoCorrector
+include IgnoredNode
def on_send(node)
return unless some_condition?(node)
add_offense(node) do |corrector|
+ next if part_of_ignored_node?(node)
+
corrector.replace(node, "...")
end
+
+ ignore_node(node)
end
这是因为文件校正的实现是通过重复调查和校正直到文件不再需要校正,这意味着所有嵌套节点最终都会被处理。
请注意,Cop
规范中的 expect_correction
仅断言一次传递后的结果。
按 Ruby 或 Gem 版本限制
一些 cop 应用仅在特定上下文中适用的更改,例如,如果用户具有最低 Ruby 版本。有一些助手可以让你自动约束你的 cop,使其仅在适用时运行。
要求最低 Ruby 版本
如果你的 cop 使用新的 Ruby 语法或标准库 API,它应该只在用户具有目标 Ruby 版本时自动校正,你使用 TargetRubyVersion#minimum_target_ruby_version
设置它。
例如,Performance/SelectMap
cop 需要 Ruby 2.7,它引入了 Enumerable#filter_map
module RuboCop::Cop::Performance::SelectMap < Base
extend TargetRubyVersion
minimum_target_ruby_version 2.7
# ...
end
此 cop 不会在 Ruby 2.6 或更早版本上自动校正,它甚至不会报告违规(因为没有更好的替代方案可以推荐)。
要求一个 gem
如果你的 cop 依赖于 gem 的存在,你可以使用 RuboCop::Cop::Base.requires_gem
声明它。
例如,要声明 MyCop
应该只在捆绑包使用版本在 1.2.3
和 4.5.6
之间的 my-gem
时应用
class MyCop < Base
requires_gem "my-gem", ">= 1.2.3", "< 4.5.6"
# ...
end
你可以使用 与你的 Gemfile
相同的语法 指定任何 gem 要求。
特殊情况:Rails
从历史上看,rubocop-rails
中的许多 cop 实际上并不特定于 Rails 本身,而是它的一些组件(例如 ActiveSupport
)。这些依赖关系是使用 TargetRailsVersion.minimum_target_rails_version
声明的。
例如,Rails/Pluck
cop 需要 ActiveSupport 6.0,它引入了 Enumerable#pluck
module RuboCop::Cop::Rails::Pluck < Base
extend TargetRailsVersion
minimum_target_rails_version 6.0
#...
end
运行测试
RuboCop 支持两个解析器引擎:Parser gem 和 Prism。默认情况下,测试使用 Parser 执行
$ bundle exec rake spec
要使用对 Prism 的实验性支持运行所有测试,请使用 bundle exec prism_spec
,要执行单个文件的测试,请指定环境变量 PARSER_ENGINE=parser_prism
。
例如,PARSER_ENGINE=parser_prism spec/rubocop/cop/style/hash_syntax_spec.rb
bundle exec rake
运行 Parser gem 和 Prism 解析器的测试。
但 ascii_spec
rake 任务默认不运行。因为 ascii_spec
任务已经有一段时间没有失败了。但是,ascii_spec
任务将在 CI 中继续检查。
在 CI 中,合并所需的所有测试都会执行。请调查是否有任何失败。
配置
每个 cop 可以保存一个配置,你可以引用实例中的 cop_config
,它将返回一个包含在 .rubocop.yml
文件中声明的选项的哈希表。
例如,假设我们想让配置可配置,以便替换可以使用除 .any?
之外的其他方法。
Style/SimplifyNotEmptyWithAny:
Enabled: true
ReplaceAnyWith: "size > 0"
然后在自动更正方法中,你只需要使用 cop_config
即可。
def on_send(node)
expression = not_empty_call?(node)
return unless expression
add_offense(node) do |corrector|
replacement = cop_config['ReplaceAnyWith'] || 'any?'
corrector.replace(node, "#{expression.source}.#{replacement}")
end
end
文档
每个新的 cop 都需要解释和示例,以便社区能够轻松地理解其目的。此文档由 yard
生成,并直接添加到 cop.rb
文件中。对于每个 SupportedStyle
和你在 cop 中包含的唯一配置,都需要有示例。示例必须具有有效的 Ruby 语法。不要使用反引号。
module Department
# Description of your cop. Include description of ALL config options. Particularly
# ones that take booleans and arrays, because we generally do not show examples for
# configs with these value types.
#
# @example EnforcedStyle: bar
# # Description about this particular option
#
# # bad
# bad_example1
# bad_example2
#
# # good
# good_example1
# good_example2
#
# @example EnforcedStyle: foo (default)
# # Description about this particular option
#
# # bad
# bad_example1
# bad_example2
#
# # good
# good_example1
# good_example2
#
# @example AnyUniqueConfigKeyThatIsAString: qux (default)
# # Description about this particular option
#
# # bad
# bad_example1
# bad_example2
#
# # good
# good_example1
# good_example2
#
# @example AnyUniqueConfigKeyThatIsAString: thud
# # Description about this particular option
#
# # bad
# bad_example1
# bad_example2
#
# # good
# good_example1
# good_example2
#
class YourCop
# ...
注意所有文档部分的放置和间距。例如,配置键按字母顺序排列,指定了 (default)
,并且在 class YourCop
之前有一行空行。虽然代码库中的并非所有示例都遵循这种确切的格式,但我们努力使它保持一致。非常欢迎改进 RuboCop 文档的 PR。
在真实代码库中测试你的 cop
通常,最好检查你的 cop 是否在重要的代码库(例如 Rails 或你正在处理的某个大型项目)上正常工作,以确保它在各种不同的语法中都能正常工作。
有几种方法可以做到这一点。两种常见的方法
-
在你的本地
rubocop
仓库中,运行exe/rubocop ~/your/other/codebase
。 -
在另一个代码库的
Gemfile
中,设置指向你的本地仓库的路径,如下所示:gem 'rubocop', path: '/full/path/to/rubocop'
。然后在你的代码库中运行rubocop
。
使用方法 #2,你也可以使用 RuboCop 扩展仓库的本地版本,例如 rubocop-rspec
。
为了快速执行并且避免与其他正在运行的代码检查器混淆,您可以在命令行中使用 `--only` 参数来根据您的代码检查器名称进行过滤。
$ rubocop --only Style/SimplifyNotEmptyWithAny