From cb3ca9cc08b16a4906a68305fff4d43571fc4cec Mon Sep 17 00:00:00 2001 From: Benjamin Quorning Date: Sun, 31 Mar 2024 00:14:22 +0100 Subject: [PATCH] Add new RSpec/EmptyOutput cop Instead of calling `output` with an empty string, you should use the inverse runner and call `output` without an argument. E.g. instead of expect { foo }.to output('').to_stdout you should call expect { foo }.not_to output.to_stdout --- .rubocop.yml | 2 + CHANGELOG.md | 1 + config/default.yml | 6 ++ docs/modules/ROOT/pages/cops.adoc | 1 + docs/modules/ROOT/pages/cops_rspec.adoc | 31 ++++++++++ lib/rubocop/cop/rspec/empty_output.rb | 47 +++++++++++++++ lib/rubocop/cop/rspec_cops.rb | 1 + spec/rubocop/cop/rspec/empty_output_spec.rb | 67 +++++++++++++++++++++ 8 files changed, 156 insertions(+) create mode 100644 lib/rubocop/cop/rspec/empty_output.rb create mode 100644 spec/rubocop/cop/rspec/empty_output_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index ba314d92a..4a19f5f8e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -164,6 +164,8 @@ RSpec/DuplicatedMetadata: Enabled: true RSpec/EmptyMetadata: Enabled: true +RSpec/EmptyOutput: + Enabled: true RSpec/Eq: Enabled: true RSpec/ExcessiveDocstringSpacing: diff --git a/CHANGELOG.md b/CHANGELOG.md index ef48eaa5c..6d969dd65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Fix an autocorrect error for `RSpec/ExpectActual`. ([@bquorning]) - Add new `RSpec/UndescriptiveLiteralsDescription` cop. ([@ydah]) +- Add new `RSpec/EmptyOutput` cop. ([@bquorning]) ## 2.28.0 (2024-03-30) diff --git a/config/default.yml b/config/default.yml index fb96a34d0..0544dcb08 100644 --- a/config/default.yml +++ b/config/default.yml @@ -366,6 +366,12 @@ RSpec/EmptyMetadata: VersionAdded: '2.24' Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyMetadata +RSpec/EmptyOutput: + Description: Check that the `output` matcher is not called with an empty string. + Enabled: pending + VersionAdded: "<>" + Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyOutput + RSpec/Eq: Description: Use `eq` instead of `be ==` to compare objects. Enabled: pending diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index e11215741..dd6fcfaa9 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -32,6 +32,7 @@ * xref:cops_rspec.adoc#rspecemptylineafterhook[RSpec/EmptyLineAfterHook] * xref:cops_rspec.adoc#rspecemptylineaftersubject[RSpec/EmptyLineAfterSubject] * xref:cops_rspec.adoc#rspecemptymetadata[RSpec/EmptyMetadata] +* xref:cops_rspec.adoc#rspecemptyoutput[RSpec/EmptyOutput] * xref:cops_rspec.adoc#rspeceq[RSpec/Eq] * xref:cops_rspec.adoc#rspecexamplelength[RSpec/ExampleLength] * xref:cops_rspec.adoc#rspecexamplewithoutdescription[RSpec/ExampleWithoutDescription] diff --git a/docs/modules/ROOT/pages/cops_rspec.adoc b/docs/modules/ROOT/pages/cops_rspec.adoc index 3f424196d..bd432e2b9 100644 --- a/docs/modules/ROOT/pages/cops_rspec.adoc +++ b/docs/modules/ROOT/pages/cops_rspec.adoc @@ -1521,6 +1521,37 @@ describe 'Something' * https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyMetadata +== RSpec/EmptyOutput + +|=== +| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed + +| Pending +| Yes +| Always +| <> +| - +|=== + +Check that the `output` matcher is not called with an empty string. + +=== Examples + +[source,ruby] +---- +# bad +expect { foo }.to output('').to_stdout +expect { bar }.not_to output('').to_stderr + +# good +expect { foo }.not_to output.to_stdout +expect { bar }.to output.to_stderr +---- + +=== References + +* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyOutput + == RSpec/Eq |=== diff --git a/lib/rubocop/cop/rspec/empty_output.rb b/lib/rubocop/cop/rspec/empty_output.rb new file mode 100644 index 000000000..f70d3de21 --- /dev/null +++ b/lib/rubocop/cop/rspec/empty_output.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Check that the `output` matcher is not called with an empty string. + # + # @example + # # bad + # expect { foo }.to output('').to_stdout + # expect { bar }.not_to output('').to_stderr + # + # # good + # expect { foo }.not_to output.to_stdout + # expect { bar }.to output.to_stderr + # + class EmptyOutput < Base + extend AutoCorrector + + MSG = 'Use `%s` instead of matching on an empty output.' + RESTRICT_ON_SEND = Runners.all + + # @!method matching_empty_output(node) + def_node_matcher :matching_empty_output, <<~PATTERN + (send + (block + (send nil? :expect) ... + ) + #Runners.all + (send $(send nil? :output (str empty?)) ...) + ) + PATTERN + + def on_send(send_node) + matching_empty_output(send_node) do |node| + runner = send_node.method?(:to) ? 'not_to' : 'to' + message = format(MSG, runner: runner) + add_offense(node, message: message) do |corrector| + corrector.replace(send_node.loc.selector, runner) + corrector.replace(node, 'output') + end + end + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec_cops.rb b/lib/rubocop/cop/rspec_cops.rb index 6c83b7dbe..7af420e4e 100644 --- a/lib/rubocop/cop/rspec_cops.rb +++ b/lib/rubocop/cop/rspec_cops.rb @@ -54,6 +54,7 @@ require_relative 'rspec/empty_line_after_hook' require_relative 'rspec/empty_line_after_subject' require_relative 'rspec/empty_metadata' +require_relative 'rspec/empty_output' require_relative 'rspec/eq' require_relative 'rspec/example_length' require_relative 'rspec/example_without_description' diff --git a/spec/rubocop/cop/rspec/empty_output_spec.rb b/spec/rubocop/cop/rspec/empty_output_spec.rb new file mode 100644 index 000000000..d9357a7a2 --- /dev/null +++ b/spec/rubocop/cop/rspec/empty_output_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::RSpec::EmptyOutput, :config do + it 'registers an offense when using `#output` with an empty string' do + expect_offense(<<~RUBY) + expect { foo }.to output('').to_stderr + ^^^^^^^^^^ Use `not_to` instead of matching on an empty output. + expect { foo }.to output('').to_stdout + ^^^^^^^^^^ Use `not_to` instead of matching on an empty output. + RUBY + + expect_correction(<<~RUBY) + expect { foo }.not_to output.to_stderr + expect { foo }.not_to output.to_stdout + RUBY + end + + it 'registers an offense when negatively matching `#output` with ' \ + 'an empty string' do + expect_offense(<<~RUBY) + expect { foo }.not_to output('').to_stderr + ^^^^^^^^^^ Use `to` instead of matching on an empty output. + expect { foo }.to_not output('').to_stdout + ^^^^^^^^^^ Use `to` instead of matching on an empty output. + RUBY + + expect_correction(<<~RUBY) + expect { foo }.to output.to_stderr + expect { foo }.to output.to_stdout + RUBY + end + + describe 'compound expectations' do + it 'does not register an offense when matching empty strings' do + expect_no_offenses(<<~RUBY) + expect { + :noop + }.to output('').to_stdout.and output('').to_stderr + RUBY + end + + it 'does not register an offense when matching non-empty strings' do + expect_no_offenses(<<~RUBY) + expect { + warn "foo" + puts "bar" + }.to output("bar\n").to_stdout.and output(/foo/).to_stderr + RUBY + end + end + + it 'does not register an offense when using `#output` with ' \ + 'a non-empty string' do + expect_no_offenses(<<~RUBY) + expect { foo }.to output('foo').to_stderr + expect { foo }.not_to output('foo').to_stderr + expect { foo }.to_not output('foo').to_stderr + RUBY + end + + it 'does not register an offense when using `not_to output`' do + expect_no_offenses(<<~RUBY) + expect { foo }.not_to output.to_stderr + expect { foo }.to_not output.to_stderr + RUBY + end +end