RuboCop 开发指南:如何编写自定义代码检查规则
前言
RuboCop 是一个强大的 Ruby 代码静态分析工具,它可以帮助开发者保持代码风格一致并发现潜在问题。本文将深入讲解如何为 RuboCop 开发自定义检查规则(Cop),从基础概念到高级技巧全面覆盖。
准备工作
在开始开发新 Cop 前,需要先搭建开发环境:
- 获取 RuboCop 源代码
- 运行
bundle install
安装依赖 - 确保在 RuboCop 项目目录下操作
创建新 Cop
RuboCop 提供了便捷的生成器来创建 Cop 模板:
$ bundle exec rake 'new_cop[Department/Name]'
这个命令会生成:
- Cop 实现文件:
lib/rubocop/cop/department/name.rb
- 测试文件:
spec/rubocop/cop/department/name_spec.rb
- 自动更新配置文件:
config/default.yml
生成后需要完成以下步骤:
- 在配置文件中完善 Cop 描述
- 实现 Cop 的具体逻辑
- 提交代码变更
- 生成更新日志条目
理解抽象语法树(AST)
RuboCop 使用 Parser 库将 Ruby 代码转换为抽象语法树(AST)。理解 AST 是开发 Cop 的基础。
安装 Parser 工具
$ gem install parser
查看代码的 AST 表示
使用 ruby-parse
工具可以查看代码的 AST:
$ ruby-parse -e '1'
(int 1)
$ ruby-parse -e 'name = "John"'
(lvasgn :name
(str "John"))
每个括号内的表达式代表一个 AST 节点,第一个元素是节点类型,其余是子节点信息。
在 RuboCop 中调试 AST
RuboCop 提供了 REPL 环境来调试 AST:
$ bin/console
在控制台中可以这样分析代码:
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?"
实现 Cop 逻辑
使用节点模式(Node Pattern)
节点模式是 RuboCop 提供的 DSL,可以简化 AST 节点的匹配逻辑。
基本模式匹配
NodePattern = RuboCop::AST::NodePattern
NodePattern.new('send').match(node) # => true
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]
实现 Cop 示例
假设我们要实现一个将 !array.empty?
简化为 array.any?
的 Cop:
- 生成 Cop 模板:
$ rake 'new_cop[Style/SimplifyNotEmptyWithAny]'
- 定义节点匹配器:
def_node_matcher :not_empty_call?, <<~PATTERN
(send (send $(...) :empty?) :!)
PATTERN
- 实现
on_send
回调:
def on_send(node)
return unless not_empty_call?(node)
add_offense(node)
end
- 完整 Cop 实现:
module RuboCop
module Cop
module Style
class SimplifyNotEmptyWithAny < Base
MSG = 'Use `.any?` and remove the negation part.'.freeze
RESTRICT_ON_SEND = [:!].freeze
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
编写测试
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
自动修正功能
RuboCop 支持自动修正检测到的问题:
- 首先扩展
AutoCorrector
模块 - 在
add_offense
块中使用 corrector 对象
测试自动修正
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
实现自动修正
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
Corrector 支持的操作:
insert_after
- 在节点后插入insert_before
- 在节点前插入wrap
- 包装节点replace
- 替换节点
防止修正冲突
当多个修正操作重叠时,可以使用 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 可能只在特定 Ruby 版本或 gem 版本下适用。
限制 Ruby 版本
class RuboCop::Cop::Performance::SelectMap < Base
extend TargetRubyVersion
minimum_target_ruby_version 2.7
# ...
end
限制 gem 版本
class MyCop < Base
requires_gem "my-gem", ">= 1.2.3", "< 4.5.6"
# ...
end
在测试中可以指定 gem 版本:
describe RuboCop::Cop::Style::MyCop, :config do
context 'when `my-gem` is at version `1.X`' do
let(:gem_versions) { { 'my-gem' => '1.0.0' } }
# ...
end
end
配置选项
Cop 可以支持配置选项,通过 cop_config
访问:
Style/SimplifyNotEmptyWithAny:
Enabled: true
ReplaceAnyWith: "size > 0"
在 Cop 中使用配置:
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 都需要完善的文档,包含描述和示例:
module Department
# Description of your cop. Include description of ALL config options.
#
# @example EnforcedStyle: bar
# # Description about this particular option
#
# # bad
# bad_example1
# bad_example2
#
# # good
# good_example1
# good_example2
#
class YourCop
# ...
文档需要注意:
- 配置键按字母顺序排列
- 默认值标记为
(default)
- 类定义前空一行
- 使用有效的 Ruby 语法示例
在实际项目中测试
建议在大型项目(如 Rails)中测试新 Cop,以确保它在各种语法场景下都能正常工作。
结语
开发 RuboCop 自定义检查规则需要理解 Ruby 的抽象语法树和 RuboCop 的工作机制。通过本文的指导,你应该能够创建功能完善的自定义 Cop,包括节点匹配、自动修正、版本限制和配置选项等功能。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考