module RuboCop::RSpec::ExpectOffense
Mixin for `expect_offense` and `expect_no_offenses`
This mixin makes it easier to specify strict offense expectations in a declarative and visual fashion. Just type out the code that should generate a 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.
@example Usage
expect_offense(<<~RUBY) a do b end.c ^^^^^ Avoid chaining a method call on a do...end block. RUBY
@example 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.' )
Autocorrection can be tested using `expect_correction` after `expect_offense`.
@example `expect_offense` and `expect_correction`
expect_offense(<<~RUBY) x % 2 == 0 ^^^^^^^^^^ Replace with `Integer#even?`. RUBY expect_correction(<<~RUBY) x.even? RUBY
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`.
@example `expect_offense` and `expect_no_corrections`
expect_offense(<<~RUBY) a do b end.c ^^^^^ Avoid chaining a method call on a do...end block. RUBY expect_no_corrections
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:
@example `^{}` empty line offense
expect_offense(<<~RUBY) ^{} Missing frozen string literal comment. puts 1 RUBY
Public Instance Methods
rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
# File lib/rubocop/rspec/expect_offense.rb, line 130 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 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? break corrected_source if corrected_source == @processed_source.buffer.source 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 'Use `expect_no_corrections` if the code will not change' if new_source == source expect(new_source).to eq(correction) end
rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
# File lib/rubocop/rspec/expect_offense.rb, line 166 def expect_no_corrections raise '`expect_no_corrections` must follow `expect_offense`' unless @processed_source return if @last_corrector.empty? # In order to print a nice diff, e.g. what source got corrected to, # we need to run the actual corrections new_source = @last_corrector.rewrite expect(new_source).to eq(@processed_source.buffer.source) end
# File lib/rubocop/rspec/expect_offense.rb, line 179 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
# File lib/rubocop/rspec/expect_offense.rb, line 114 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 @offenses end
# 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
# File lib/rubocop/rspec/expect_offense.rb, line 187 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
# File lib/rubocop/rspec/expect_offense.rb, line 197 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
# File lib/rubocop/rspec/expect_offense.rb, line 205 def set_formatter_options RuboCop::Formatter::DisabledConfigFormatter.config_to_allow_offenses = {} RuboCop::Formatter::DisabledConfigFormatter.detected_styles = {} cop.instance_variable_get(:@options)[:autocorrect] = true end