Skip to content

Commit

Permalink
Add RSpec/StringAsInstanceDoubleConstant
Browse files Browse the repository at this point in the history
Addresses #1136

Adds a cop which can autocorrect from String declarations for instance_double to Class declarations. Symbol declarations are not affected.
  • Loading branch information
corsonknowles committed Sep 27, 2024
1 parent 16cf19c commit befbe23
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 16 deletions.
25 changes: 13 additions & 12 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,34 +101,34 @@ RSpec:
- expect_no_offenses
- expect_offense

RSpec/DescribeClass:
Exclude:
- spec/project/**/*.rb

RSpec/ExampleLength:
CountAsOne:
- heredoc
Max: 11

RSpec/DescribeClass:
Exclude:
- spec/project/**/*.rb

RSpec/MultipleExpectations:
Max: 2

RSpec/SpecFilePathFormat:
Exclude:
- spec/rubocop/cop/rspec/mixin/**/*.rb

# `expect_offense` does not use Kernel#format or String#%
Style/FormatStringToken:
Exclude:
- spec/rubocop/**/*.rb

Style/RequireOrder:
Enabled: true

RSpec/SpecFilePathFormat:
Exclude:
- spec/rubocop/cop/rspec/mixin/**/*.rb

Style/NumberedParameters:
Enabled: true
EnforcedStyle: disallow

Style/RequireOrder:
Enabled: true

# Enable RuboCop's pending cops up to v1.63

Gemspec/DeprecatedAttributeAssignment: {Enabled: true}
Expand Down Expand Up @@ -253,4 +253,5 @@ Style/YAMLFileRead: {Enabled: true}

# Enable our own pending cops.
#
# No pending cops yet.
RSpec/StringAsInstanceDoubleConstant:
Enabled: true
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Master (Unreleased)

- Add `RSpec/StringAsInstanceDoubleConstant` to check for and correct strings used as instance_doubles. ([@corsonknowles])
- Fix false-positive for `RSpec/UnspecifiedException` when a method is literally named `raise_exception`. ([@aarestad])
- Fix false-positive for `RSpec/UnspecifiedException` when `not_to raise_error` is used within a block. ([@aarestad], [@G-Rath])

## 3.0.5 (2024-09-07)

- Fix false-negative and error for `RSpec/MetadataStyle` when non-literal args are used in metadata in `EnforceStyle: hash`. ([@cbliard])
Expand Down Expand Up @@ -899,6 +903,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.

<!-- Contributors (alphabetically) -->

[@aarestad]: https://github.com/aarestad
[@abrom]: https://github.com/abrom
[@ahukkanen]: https://github.com/ahukkanen
[@akiomik]: https://github.com/akiomik
Expand All @@ -920,6 +925,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
[@cfabianski]: https://github.com/cfabianski
[@clupprich]: https://github.com/clupprich
[@composerinteralia]: https://github.com/composerinteralia
[@corsonknowles]: https://github.com/corsonknowles
[@corydiamand]: https://github.com/corydiamand
[@darhazer]: https://github.com/Darhazer
[@daveworth]: https://github.com/daveworth
Expand Down
7 changes: 7 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,13 @@ RSpec/SpecFilePathSuffix:
- "**/spec/**/*"
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SpecFilePathSuffix

RSpec/StringAsInstanceDoubleConstant:
Description: Do not use a string as `instance_double` constant.
Enabled: pending
Safe: false
VersionAdded: "<<next>>"
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/StringAsInstanceDoubleConstant

RSpec/StubbedMock:
Description: Checks that message expectations do not have a configured response.
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
* xref:cops_rspec.adoc#rspecsortmetadata[RSpec/SortMetadata]
* xref:cops_rspec.adoc#rspecspecfilepathformat[RSpec/SpecFilePathFormat]
* xref:cops_rspec.adoc#rspecspecfilepathsuffix[RSpec/SpecFilePathSuffix]
* xref:cops_rspec.adoc#rspecstringasinstancedoubleconstant[RSpec/StringAsInstanceDoubleConstant]
* xref:cops_rspec.adoc#rspecstubbedmock[RSpec/StubbedMock]
* xref:cops_rspec.adoc#rspecsubjectdeclaration[RSpec/SubjectDeclaration]
* xref:cops_rspec.adoc#rspecsubjectstub[RSpec/SubjectStub]
Expand Down
35 changes: 35 additions & 0 deletions docs/modules/ROOT/pages/cops_rspec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5493,6 +5493,41 @@ spec/models/user.rb # shared_examples_for 'foo'
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SpecFilePathSuffix
== RSpec/StringAsInstanceDoubleConstant
|===
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
| Pending
| No
| Always (Unsafe)
| <<next>>
| -
|===
Do not use a string as `instance_double` constant.
=== Safety
This cop is unsafe because the correction requires loading the class.
Loading before stubbing causes RSpec to only allow instance methods
to be stubbed.
=== Examples
[source,ruby]
----
# bad
instance_double('User', name: 'John')
# good
instance_double(User, name: 'John')
----
=== References
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/StringAsInstanceDoubleConstant
== RSpec/StubbedMock
|===
Expand Down
4 changes: 2 additions & 2 deletions lib/rubocop/cop/rspec/change_by_zero.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ def on_send(node)
# rubocop:disable Metrics/MethodLength
def register_offense(node, change_node)
if compound_expectations?(node)
add_offense(node.source_range,
add_offense(node,
message: message_compound(change_node)) do |corrector|
autocorrect_compound(corrector, node)
end
else
add_offense(node.source_range,
add_offense(node,
message: message(change_node)) do |corrector|
autocorrect(corrector, node, change_node)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/rspec/expect_actual.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def on_send(node) # rubocop:disable Metrics/MethodLength
expect_literal(node) do |actual, send_node, matcher, expected|
next if SKIPPED_MATCHERS.include?(matcher)

add_offense(actual.source_range) do |corrector|
add_offense(actual) do |corrector|
next unless CORRECTABLE_MATCHERS.include?(matcher)
next if literal?(expected)

Expand Down
45 changes: 45 additions & 0 deletions lib/rubocop/cop/rspec/string_as_instance_double_constant.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module RuboCop
module Cop
module RSpec
# Do not use a string as `instance_double` constant.
#
# @safety
# This cop is unsafe because the correction requires loading the class.
# Loading before stubbing causes RSpec to only allow instance methods
# to be stubbed.
#
# @example
# # bad
# instance_double('User', name: 'John')
#
# # good
# instance_double(User, name: 'John')
#
class StringAsInstanceDoubleConstant < Base
extend AutoCorrector

MSG = 'Do not use a string as `instance_double` constant.'
RESTRICT_ON_SEND = %i[instance_double].freeze

# @!method stringified_instance_double_const?(node)
def_node_matcher :stringified_instance_double_const?, <<~PATTERN
(send nil? :instance_double $str ...)
PATTERN

def on_send(node)
stringified_instance_double_const?(node) do |args_node|
add_offense(args_node) do |corrector|
autocorrect(corrector, args_node)
end
end
end

def autocorrect(corrector, node)
corrector.replace(node, node.value)
end
end
end
end
end
5 changes: 4 additions & 1 deletion lib/rubocop/cop/rspec/unspecified_exception.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ def empty_exception_matcher?(node)
end

def find_expect_to(node)
node.each_ancestor(:send).find do |ancestor|
node.each_ancestor.find do |ancestor|
break if ancestor.block_type?
next unless ancestor.send_type?

expect_to?(ancestor)
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop/cop/rspec_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
require_relative 'rspec/sort_metadata'
require_relative 'rspec/spec_file_path_format'
require_relative 'rspec/spec_file_path_suffix'
require_relative 'rspec/string_as_instance_double_constant'
require_relative 'rspec/stubbed_mock'
require_relative 'rspec/subject_declaration'
require_relative 'rspec/subject_stub'
Expand Down
33 changes: 33 additions & 0 deletions spec/rubocop/cop/rspec/string_as_instance_double_constant_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::RSpec::StringAsInstanceDoubleConstant,
:config do
context 'when using a class for instance_double' do
it 'does not register an offense' do
expect_no_offenses(<<~RUBY)
instance_double(Shape, area: 12)
RUBY
end
end

context 'when passing a variable to initialize instance_double' do
it 'does not register an offense' do
expect_no_offenses(<<~RUBY)
instance_double(type_undetectable_in_static_analysis)
RUBY
end
end

context 'when using a string for instance_double' do
it 'replaces the string with the class' do
expect_offense <<~RUBY
instance_double('Shape', area: 12)
^^^^^^^ Do not use a string as `instance_double` constant.
RUBY

expect_correction <<~RUBY
instance_double(Shape, area: 12)
RUBY
end
end
end
56 changes: 56 additions & 0 deletions spec/rubocop/cop/rspec/unspecified_exception_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,30 @@
}.to raise_error(my_exception)
RUBY
end

it 'allows a subject function to be named raise_error' do
expect_no_offenses(<<~RUBY)
def raise_exception
raise StandardError
end
expect {
raise_error
}.to raise_error(StandardError)
RUBY
end

it 'allows a subject function to be named raise_exception' do
expect_no_offenses(<<~RUBY)
def raise_exception
raise StandardError
end
expect {
raise_exception
}.to raise_error(StandardError)
RUBY
end
end

context 'with raise_exception matcher' do
Expand Down Expand Up @@ -198,5 +222,37 @@
^^^^^^^^^^^^^^^ Specify the exception being captured
RUBY
end

it 'does not confuse blocks with chains' do
expect_no_offenses(<<~RUBY)
expect do
expect { foo }.not_to raise_error
end.to change(Foo, :count).by(3)
RUBY
end

it 'allows a subject function to be named raise_exception' do
expect_no_offenses(<<~RUBY)
def raise_error
raise StandardError
end
expect {
raise_exception
}.to raise_exception(StandardError)
RUBY
end

it 'allows a subject function to be named raise_error' do
expect_no_offenses(<<~RUBY)
def raise_error
raise StandardError
end
expect {
raise_error
}.to raise_exception(StandardError)
RUBY
end
end
end

0 comments on commit befbe23

Please sign in to comment.