Development

This section of the documentation will teach you how to develop new cops. We’ll start with generating a cop template and then we’ll address the various aspects of its implementation (interacting with the AST, auto-correct, configuration) and testing.

Create a new cop

Use the bundled rake task new_cop to generate a cop template:

$ bundle exec rake 'new_cop[Department/Name]'
Files created:
  - lib/rubocop/cop/department/name.rb
  - spec/rubocop/cop/department/name_spec.rb
File modified:
  - `require_relative 'rubocop/cop/department/name'` added into lib/rubocop.rb
  - A configuration for the cop is added into config/default.yml

Do 3 steps:
  1. Add an entry to the "New features" section in CHANGELOG.md,
     e.g. "Add new `Department/Name` cop. ([@your_id][])"
  2. Modify the description of Department/Name in config/default.yml
  3. Implement your new cop in the generated file!

Basics

RuboCop uses the parser library to create the Abstract Syntax Tree (AST) representation of the code.

You can install parser gem and use ruby-parse command line utility to check what the AST looks like in the output.

$ gem install parser

And then try to parse a simple integer representation with ruby-parse:

$ ruby-parse -e '1'
(int 1)

Each expression surrounded by parentheses represents a node in the AST. The first element is the node type and the tail contains the children with all information needed to represent the code.

Here’s another example - a local variable name being assigned the string value "John":

$ ruby-parse -e 'name = "John"'
(lvasgn :name
  (str "John"))

Inspecting the AST representation

Let’s imagine we want to simplify statements from !array.empty? to array.any?:

First, check what the bad code returns in the Abstract Syntax Tree representation.

$ ruby-parse -e '!array.empty?'
(send
  (send
    (send nil :array) :empty?) :!)

Now, it’s time to debug our expression using the REPL from RuboCop:

$ bin/console

First we need to declare the code that we want to match, and use the ProcessedSource that is a simple wrap to make the parser interpret the code and build the 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?), :!)

The node has a few attributes that can be useful in the journey:

node.type # => :send
node.children # => [s(:send, s(:send, nil, :something), :empty?), :!]
node.source # => "!something.empty?"

Implementation

Writing Node Pattern Rules

You can write cops without using NodePattern (and many older cops don’t use it), but it generally simplifies a lot the code, as manual node matching and destructuring can be quite verbose.

Now that you’re familiar with AST, you can learn a bit about the node pattern and use patterns to match with specific nodes that you want to match.

You can learn more about Node Pattern here.

Node pattern matches something very similar to the current output from AST representation, then let’s start with something very generic:

NodePattern.new('send').match(node) # => true

It matches because the root is a send type. Now lets match it deeply using parentheses to define details for sub-nodes. If you don’t care about what an internal node is, you can use ... to skip it and just consider " a node".

NodePattern.new('(send ...)').match(node) # => true
NodePattern.new('(send (send ...) :!)').match(node) # => true
NodePattern.new('(send (send (send ...) :empty?) :!)').match(node) # => true

Sometimes it’s hard to comprehend complex expressions you’re building with the pattern, then, if you got lost with the node pattern parens surrounding deeply, try to use the $ to capture the internal expression and check exactly each piece of the expression:

NodePattern.new('(send (send (send $...) :empty?) :!)').match(node) # => [nil, :something]

It’s not needed to strictly receive a send in the internal node because maybe it can also be a literal array like:

![].empty?

The code above has the following representation:

=> s(:send, s(:send, s(:array), :empty?), :!)

It’s possible to skip the internal node with ... to make sure that it’s just another internal node:

NodePattern.new('(send (send (...) :empty?) :!)').match(node) # => true

In other words, it says: "Match code calling !<expression>.empty?".

Great! Now, lets implement our cop to simplify such statements:

$ rake 'new_cop[Style/SimplifyNotEmptyWithAny]'

After the cop scaffold is generated, change the node matcher to match with the expression achieved previously:

def_node_matcher :not_empty_call?, <<~PATTERN
  (send (send $(...) :empty?) :!)
PATTERN

Note that we added a $ sign to capture the "expression" in !<expression>.empty?, it will become useful later.

Get yourself familiar with the AST node hooks that parser and rubocop-ast provide.

As it starts with a send type, it’s needed to implement the on_send method, as the cop scaffold already suggested:

def on_send(node)
  return unless not_empty_call?(node)

  add_offense(node)
end

And the final cop code will look like something like this:

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

        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

Update the spec to cover the expected syntax:

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

If your code has variables of different lengths, you can use %{foo}, ^{foo}, and _{foo} to format your template; you can also abbreviate offense messages with […​]:

%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

Auto-correct

The auto-correct can help humans automatically fix offenses that have been detected. It’s necessary to extend AutoCorrector. The method add_offense yields a corrector object that is a thin wrapper on parser’s TreeRewriter to which you can give instructions about what to do with the offensive node.

Let’s start with a simple spec to cover it:

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

And then add the autocorrecting block on the cop side:

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

The corrector allows you to insert_after, insert_before, wrap or replace a specific node or in any specific range of the code.

Range can be determined on node.location where it brings specific ranges for expression or other internal information that the node holds.

Configuration

Each cop can hold a configuration and you can refer to cop_config in the instance and it will bring a hash with options declared in the .rubocop.yml file.

For example, lets imagine we want to make configurable to make the replacement works with other method than .any?:

Style/SimplifyNotEmptyWithAny:
  Enabled: true
  ReplaceAnyWith: "size > 0"

And then on the autocorrect method, you just need to use the cop_config it:

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

Documentation

Every new cop requires explanation and examples to make it easy for the community to understand its purpose. This documentation is generated by yard and is added directly into the cop.rb file. For every SupportedStyle and unique configuration you have included in the cop, there needs to be examples. Examples must have valid Ruby syntax. Do not use upticks.

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
    # ...

Take note of the placement and spacing of all the documentation pieces. Such as config keys being in alphabetical order, the (default) being specified, and one empty line before class YourCop. While not all examples in the codebase follow this exact format, we strive to make this consistent. PRs improving RuboCop documentation are very welcome.

Run rake generate_cops_documentation to apply your yard documentation into the manual. CI will fail if the manual and yard comments do not match exactly. rake default will also generate the new documentation.

Testing your cop in a real codebase

Generally, is a good practice to check if your cop is working properly over a significant codebase (e.g. Rails or some big project you’re working on) to guarantee it’s working in a range of different syntaxes.

There are several ways to do this. Two common approaches:

  1. From within your local rubocop repo, run exe/rubocop ~/your/other/codebase.

  2. From within the other codebase’s Gemfile, set a path to your local repo like this: gem 'rubocop', path: '/full/path/to/rubocop'. Then run rubocop within your codebase.

With approach #2, you can use local versions of RuboCop extension repos such as rubocop-rspec as well.

To make it fast and do not get confused with other cops in action, you can use --only parameter in the command line to filter by your cop name:

$ rubocop --only Style/SimplifyNotEmptyWithAny

In the end, do not forget to run rake generate_cops_documentation to update the docs.