class RuboCop::Cop::RSpec::ChangeByZero

Prefer negated matchers over ‘to change.by(0)`.

In the case of composite expectations, cop suggest using the negation matchers of ‘RSpec::Matchers#change`.

By default the cop does not support autocorrect of compound expectations, but if you set the negated matcher for ‘change`, e.g. `not_change` with the `NegatedMatcher` option, the cop will perform the autocorrection.

@example NegatedMatcher: ~ (default)

# bad
expect { run }.to change(Foo, :bar).by(0)
expect { run }.to change { Foo.bar }.by(0)

# bad - compound expectations (does not support autocorrection)
expect { run }
  .to change(Foo, :bar).by(0)
  .and change(Foo, :baz).by(0)
expect { run }
  .to change { Foo.bar }.by(0)
  .and change { Foo.baz }.by(0)

# good
expect { run }.not_to change(Foo, :bar)
expect { run }.not_to change { Foo.bar }

# good - compound expectations
define_negated_matcher :not_change, :change
expect { run }
  .to not_change(Foo, :bar)
  .and not_change(Foo, :baz)
expect { run }
  .to not_change { Foo.bar }
  .and not_change { Foo.baz }

@example NegatedMatcher: not_change

# bad (support autocorrection to good case)
expect { run }
  .to change(Foo, :bar).by(0)
  .and change(Foo, :baz).by(0)
expect { run }
  .to change { Foo.bar }.by(0)
  .and change { Foo.baz }.by(0)

# good
define_negated_matcher :not_change, :change
expect { run }
  .to not_change(Foo, :bar)
  .and not_change(Foo, :baz)
expect { run }
  .to not_change { Foo.bar }
  .and not_change { Foo.baz }

Constants

CHANGE_METHODS
MSG
MSG_COMPOUND
RESTRICT_ON_SEND

Public Instance Methods

on_send(node) click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 92
def on_send(node)
  expect_change_with_arguments(node.parent) do |change|
    register_offense(node.parent, change)
  end

  expect_change_with_block(node.parent.parent) do |change|
    register_offense(node.parent.parent, change)
  end
end

Private Instance Methods

autocorrect(corrector, node, change_node) click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 134
def autocorrect(corrector, node, change_node)
  corrector.replace(node.parent.loc.selector, 'not_to')
  corrector.replace(change_node.loc.selector, 'change')
  range = node.loc.dot.with(end_pos: node.source_range.end_pos)
  corrector.remove(range)
end
autocorrect_compound(corrector, node) click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 141
def autocorrect_compound(corrector, node)
  return unless negated_matcher

  change_nodes(node) do |change_node|
    corrector.replace(change_node.loc.selector, negated_matcher)
    insert_operator(corrector, node, change_node)
    remove_by_zero(corrector, node, change_node)
  end
end
compound_expectations?(node) click to toggle source

rubocop:enable Metrics/MethodLength

# File lib/rubocop/cop/rspec/change_by_zero.rb, line 120
def compound_expectations?(node)
  node.parent.send_type? &&
    %i[and or & |].include?(node.parent.method_name)
end
insert_operator(corrector, node, change_node) click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 151
def insert_operator(corrector, node, change_node)
  operator = node.right_siblings.first
  return unless %i[& |].include?(operator)

  corrector.insert_after(
    replace_node(node, change_node), " #{operator}"
  )
end
message(change_node) click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 125
def message(change_node)
  format(MSG, method: change_node.method_name)
end
message_compound(change_node) click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 129
def message_compound(change_node)
  format(MSG_COMPOUND, preferred: preferred_method,
                       method: change_node.method_name)
end
negated_matcher() click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 175
def negated_matcher
  cop_config['NegatedMatcher']
end
preferred_method() click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 179
def preferred_method
  negated_matcher ? "`#{negated_matcher}`" : 'negated matchers'
end
register_offense(node, change_node) click to toggle source

rubocop:disable Metrics/MethodLength

# File lib/rubocop/cop/rspec/change_by_zero.rb, line 105
def register_offense(node, change_node)
  if compound_expectations?(node)
    add_offense(node,
                message: message_compound(change_node)) do |corrector|
      autocorrect_compound(corrector, node)
    end
  else
    add_offense(node,
                message: message(change_node)) do |corrector|
      autocorrect(corrector, node, change_node)
    end
  end
end
remove_by_zero(corrector, node, change_node) click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 164
def remove_by_zero(corrector, node, change_node)
  range = node.loc.dot.with(end_pos: node.source_range.end_pos)
  if change_node.loc.line == range.line
    corrector.remove(range)
  else
    corrector.remove(
      range_by_whole_lines(range, include_final_newline: true)
    )
  end
end
replace_node(node, change_node) click to toggle source
# File lib/rubocop/cop/rspec/change_by_zero.rb, line 160
def replace_node(node, change_node)
  expect_change_with_arguments(node) ? change_node : change_node.parent
end