123456789_123456789_123456789_123456789_123456789_

Module: RuboCop::RSpec::ExpectOffense

Relationships & Source Files
Namespace Children
Classes:
Defined in: lib/rubocop/rspec/expect_offense.rb

Overview

This mixin makes it easier to specify strict offense expectations in a declarative and visual fashion. Just type out the code that should generate an offense, annotate code by writing '^'s underneath each character that should be highlighted, and follow the carets with a string (separated by a space) that is the message of the offense. You can include multiple offenses in one code snippet.

Autocorrection can be tested using #expect_correction after #expect_offense.

If you do not want to specify an offense then use the companion method #expect_no_offenses. This method is a much simpler assertion since it just inspects the source and checks that there were no offenses. The #expect_offense method has to do more work by parsing out lines that contain carets.

If the code produces an offense that could not be autocorrected, you can use #expect_no_corrections after #expect_offense.

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

If you need to specify an offense on a blank line, use the empty ^{} marker:

Examples:

Usage

expect_offense(<<~RUBY)
  a do
    b
  end.c
  ^^^^^ Avoid chaining a method call on a do...end block.
RUBY

Equivalent assertion without #expect_offense

inspect_source(<<~RUBY)
  a do
    b
  end.c
RUBY

expect(cop.offenses.size).to be(1)

offense = cop.offenses.first
expect(offense.line).to be(3)
expect(offense.column_range).to be(0...5)
expect(offense.message).to eql(
  'Avoid chaining a method call on a do...end block.'
)

expect_offense(<<~RUBY)
  x % 2 == 0
  ^^^^^^^^^^ Replace with {Integer#even?}.
RUBY

expect_correction(<<~RUBY)
  x.even?
RUBY

expect_offense(<<~RUBY)
  a do
    b
  end.c
  ^^^^^ Avoid chaining a method call on a do...end block.
RUBY

expect_no_corrections

^{} empty line offense

expect_offense(<<~RUBY)

  ^{} Missing frozen string literal comment.
  puts 1
RUBY

Instance Method Summary

Instance Method Details

#expect_correction(correction, loop: true, source: nil)

Metrics/MethodLength, Metrics/CyclomaticComplexity

[ GitHub ]

  
# File 'lib/rubocop/rspec/expect_offense.rb', line 138

def expect_correction(correction, loop: true, source: nil)
  if source
    expected_annotations = parse_annotations(source, raise_error: false)
    @processed_source = parse_processed_source(expected_annotations.plain_source)
    _investigate(cop, @processed_source)
  end

  raise '`expect_correction` must follow `expect_offense`' unless @processed_source

  source = @processed_source.raw_source

  raise 'Use `expect_no_corrections` if the code will not change' if correction == source

  iteration = 0
  new_source = loop do
    iteration += 1

    corrected_source = @last_corrector.rewrite

    break corrected_source unless loop
    break corrected_source if @last_corrector.empty?

    if iteration > RuboCop::Runner::MAX_ITERATIONS
      raise RuboCop::Runner::InfiniteCorrectionLoop.new(@processed_source.path, [@offenses])
    end

    # Prepare for next loop
    @processed_source = parse_source(corrected_source, @processed_source.path)
    _investigate(cop, @processed_source)
  end

  raise 'Expected correction but no corrections were made' if new_source == source

  expect(new_source).to eq(correction)
  expect(@processed_source).to be_valid_syntax, 'Expected correction to be valid syntax'
end

#expect_no_corrections

Metrics/MethodLength, Metrics/CyclomaticComplexity

[ GitHub ]

  
# File 'lib/rubocop/rspec/expect_offense.rb', line 176

def expect_no_corrections
  raise '`expect_no_corrections` must follow `expect_offense`' unless @processed_source

  return if @last_corrector.empty?

  # This is just here for a pretty diff if the source actually got changed
  new_source = @last_corrector.rewrite
  expect(new_source).to eq(@processed_source.buffer.source)

  # There is an infinite loop if a corrector is present that did not make
  # any changes. It will cause the same offense/correction on the next loop.
  raise RuboCop::Runner::InfiniteCorrectionLoop.new(@processed_source.path, [@offenses])
end

#expect_no_offenses(source, file = nil)

[ GitHub ]

  
# File 'lib/rubocop/rspec/expect_offense.rb', line 190

def expect_no_offenses(source, file = nil)
  offenses = inspect_source(source, file)

  expected_annotations = AnnotatedSource.parse(source)
  actual_annotations = expected_annotations.with_offense_annotations(offenses)
  expect(actual_annotations.to_s).to eq(source)
end

#expect_offense(source, file = nil, severity: nil, chomp: false, **replacements)

[ GitHub ]

  
# File 'lib/rubocop/rspec/expect_offense.rb', line 115

def expect_offense(source, file = nil, severity: nil, chomp: false, **replacements)
  expected_annotations = parse_annotations(source, **replacements)
  source = expected_annotations.plain_source
  source = source.chomp if chomp

  @processed_source = parse_processed_source(source, file)
  @offenses = _investigate(cop, @processed_source)
  actual_annotations = expected_annotations.with_offense_annotations(@offenses)

  expect(actual_annotations).to eq(expected_annotations), ''
  expect(@offenses.map(&:severity).uniq).to eq([severity]) if severity

  # Validate that all offenses have a range that formatters can display
  expect do
    @offenses.each { |offense| offense.location.source_line }
  end.not_to raise_error, 'One of the offenses has a misconstructed range, for ' \
                          'example if the offense is on line 1 and the source is empty'

  @offenses
end

#format_offense(source, **replacements)

[ GitHub ]

  
# File 'lib/rubocop/rspec/expect_offense.rb', line 104

def format_offense(source, **replacements)
  replacements.each do |keyword, value|
    value = value.to_s
    source = source.gsub("%{#{keyword}}", value)
                   .gsub("^{#{keyword}}", '^' * value.size)
                   .gsub("_{#{keyword}}", ' ' * value.size)
  end
  source
end

#parse_annotations(source, raise_error: true, **replacements)

[ GitHub ]

  
# File 'lib/rubocop/rspec/expect_offense.rb', line 198

def parse_annotations(source, raise_error: true, **replacements)
  set_formatter_options

  source = format_offense(source, **replacements)
  annotations = AnnotatedSource.parse(source)
  return annotations unless raise_error && annotations.plain_source == source

  raise 'Use `expect_no_offenses` to assert that no offenses are found'
end

#parse_processed_source(source, file = nil)

[ GitHub ]

  
# File 'lib/rubocop/rspec/expect_offense.rb', line 208

def parse_processed_source(source, file = nil)
  processed_source = parse_source(source, file)
  return processed_source if processed_source.valid_syntax?

  raise 'Error parsing example code: ' \
        "#{processed_source.diagnostics.map(&:render).join("\n")}"
end

#set_formatter_options

[ GitHub ]

  
# File 'lib/rubocop/rspec/expect_offense.rb', line 216

def set_formatter_options
  RuboCop::Formatter::DisabledConfigFormatter.config_to_allow_offenses = {}
  RuboCop::Formatter::DisabledConfigFormatter.detected_styles = {}
  cop.instance_variable_get(:@options)[:autocorrect] = true
end