RuboCop 开发指南:如何编写自定义代码检查规则

RuboCop 开发指南:如何编写自定义代码检查规则

rubocop A Ruby static code analyzer and formatter, based on the community Ruby style guide. rubocop 项目地址: https://gitcode.com/gh_mirrors/rub/rubocop

前言

RuboCop 是一个强大的 Ruby 代码静态分析工具,它可以帮助开发者保持代码风格一致并发现潜在问题。本文将深入讲解如何为 RuboCop 开发自定义检查规则(Cop),从基础概念到高级技巧全面覆盖。

准备工作

在开始开发新 Cop 前,需要先搭建开发环境:

  1. 获取 RuboCop 源代码
  2. 运行 bundle install 安装依赖
  3. 确保在 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

生成后需要完成以下步骤:

  1. 在配置文件中完善 Cop 描述
  2. 实现 Cop 的具体逻辑
  3. 提交代码变更
  4. 生成更新日志条目

理解抽象语法树(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:

  1. 生成 Cop 模板:
$ rake 'new_cop[Style/SimplifyNotEmptyWithAny]'
  1. 定义节点匹配器:
def_node_matcher :not_empty_call?, <<~PATTERN
  (send (send $(...) :empty?) :!)
PATTERN
  1. 实现 on_send 回调:
def on_send(node)
  return unless not_empty_call?(node)
  add_offense(node)
end
  1. 完整 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 支持自动修正检测到的问题:

  1. 首先扩展 AutoCorrector 模块
  2. 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,包括节点匹配、自动修正、版本限制和配置选项等功能。

rubocop A Ruby static code analyzer and formatter, based on the community Ruby style guide. rubocop 项目地址: https://gitcode.com/gh_mirrors/rub/rubocop

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林菁琚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值