Skip to content

Commit

Permalink
Add new RSpec/FactoryBot/SyntaxMethods cop.
Browse files Browse the repository at this point in the history
`FactoryBot` provides a mixin called
[`FactoryBot::Syntax::Methods'](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#rspec)
which allows to replace

```ruby
FactoryBot.create(:bar)
FactoryBot.build(:bar)
FactoryBot.attributes_for(:bar)
```

with

```
create(:bar)
build(:bar)
attributes_for(:bar)
```

The auto-correction is unsafe as there is no way to tell just from syntax
whether a given example group includes the mixin or not.
  • Loading branch information
leoarnold committed Nov 24, 2021
1 parent 51b1fbf commit 16ce074
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 27 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

## Master (Unreleased)

* [#1215](https://github.com/rubocop/rubocop-rails/pull/1215): Add new `RSpec/FactoryBot/SyntaxMethods` cop. ([@leoarnold][])

## 2.6.0 (2021-11-08)

* Fix merging RSpec DSL configuration from third-party gems. ([@pirj][])
Expand Down Expand Up @@ -651,3 +653,4 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
[@dswij]: https://github.com/dswij
[@francois-ferrandis]: https://github.com/francois-ferrandis
[@r7kamura]: https://github.com/r7kamura
[@leoarnold]: https://github.com/leoarnold
7 changes: 7 additions & 0 deletions config/default.yml
Expand Up @@ -816,6 +816,13 @@ RSpec/FactoryBot/FactoryClassName:
VersionChanged: '2.0'
StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/FactoryClassName

RSpec/FactoryBot/SyntaxMethods:
Description: Use shorthands from `FactoryBot::Syntax::Methods` in your specs.
Enabled: pending
SafeAutoCorrect: false
VersionAdded: "<<next>>"
StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/SyntaxMethods

RSpec/Rails:
Enabled: true
Include: *1
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Expand Up @@ -96,6 +96,7 @@
* xref:cops_rspec_factorybot.adoc#rspecfactorybot/attributedefinedstatically[RSpec/FactoryBot/AttributeDefinedStatically]
* xref:cops_rspec_factorybot.adoc#rspecfactorybot/createlist[RSpec/FactoryBot/CreateList]
* xref:cops_rspec_factorybot.adoc#rspecfactorybot/factoryclassname[RSpec/FactoryBot/FactoryClassName]
* xref:cops_rspec_factorybot.adoc#rspecfactorybot/syntaxmethods[RSpec/FactoryBot/SyntaxMethods]

=== Department xref:cops_rspec_rails.adoc[RSpec/Rails]

Expand Down
63 changes: 63 additions & 0 deletions docs/modules/ROOT/pages/cops_rspec_factorybot.adoc
Expand Up @@ -157,3 +157,66 @@ end
=== References

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/FactoryClassName

== RSpec/FactoryBot/SyntaxMethods

|===
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed

| Pending
| Yes
| Yes (Unsafe)
| <<next>>
| -
|===

Use shorthands from `FactoryBot::Syntax::Methods` in your specs.

=== Safety

The auto-correction is marked as unsafe because the cop
cannot verify whether you already include
`FactoryBot::Syntax::Methods` in your test suite.

If you're using Rails, add the following configuration to
`spec/support/factory_bot.rb` and be sure to require that file in
`rails_helper.rb`:

[source,ruby]
----
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
----

If you're not using Rails:

[source,ruby]
----
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
config.before(:suite) do
FactoryBot.find_definitions
end
end
----

=== Examples

[source,ruby]
----
# bad
FactoryBot.create(:bar)
FactoryBot.build(:bar)
FactoryBot.attributes_for(:bar)
# good
create(:bar)
build(:bar)
attributes_for(:bar)
----

=== References

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/SyntaxMethods
7 changes: 5 additions & 2 deletions lib/rubocop-rspec.rb
Expand Up @@ -12,11 +12,14 @@
require_relative 'rubocop/rspec/language/node_pattern'
require_relative 'rubocop/rspec/language'

require_relative 'rubocop/rspec/factory_bot/language'

require_relative 'rubocop/cop/rspec/mixin/top_level_group'
require_relative 'rubocop/cop/rspec/mixin/variable'
require_relative 'rubocop/cop/rspec/mixin/final_end_location'
require_relative 'rubocop/cop/rspec/mixin/comments_help'
require_relative 'rubocop/cop/rspec/mixin/empty_line_separation'
require_relative 'rubocop/cop/rspec/mixin/inside_example_group'

require_relative 'rubocop/rspec/concept'
require_relative 'rubocop/rspec/example_group'
Expand All @@ -31,8 +34,8 @@

require_relative 'rubocop/cop/rspec_cops'

# We have to register our autocorrect incompatibilies in RuboCop's cops as well
# so we do not hit infinite loops
# We have to register our autocorrect incompatibilities in RuboCop's cops
# as well so we do not hit infinite loops

module RuboCop
module Cop
Expand Down
25 changes: 2 additions & 23 deletions lib/rubocop/cop/rspec/capybara/feature_methods.rb
Expand Up @@ -42,6 +42,7 @@ module Capybara
# end
class FeatureMethods < Base
extend AutoCorrector
include InsideExampleGroup

MSG = 'Use `%<replacement>s` instead of `%<method>s`.'

Expand All @@ -60,13 +61,6 @@ class FeatureMethods < Base
{#{MAP.keys.map(&:inspect).join(' ')}}
PATTERN

# @!method spec?(node)
def_node_matcher :spec?, <<-PATTERN
(block
(send #rspec? {:describe :feature} ...)
...)
PATTERN

# @!method feature_method(node)
def_node_matcher :feature_method, <<-PATTERN
(block
Expand All @@ -75,7 +69,7 @@ class FeatureMethods < Base
PATTERN

def on_block(node)
return unless inside_spec?(node)
return unless inside_example_group?(node)

feature_method(node) do |send_node, match|
next if enabled?(match)
Expand All @@ -93,21 +87,6 @@ def message(range)

private

def inside_spec?(node)
return spec?(node) if root_node?(node)

root = node.ancestors.find { |parent| root_node?(parent) }
spec?(root)
end

def root_node?(node)
node.parent.nil? || root_with_siblings?(node.parent)
end

def root_with_siblings?(node)
node.begin_type? && node.parent.nil?
end

def enabled?(method_name)
enabled_methods.include?(method_name)
end
Expand Down
5 changes: 3 additions & 2 deletions lib/rubocop/cop/rspec/factory_bot/create_list.rb
Expand Up @@ -27,6 +27,7 @@ module FactoryBot
class CreateList < Base
extend AutoCorrector
include ConfigurableEnforcedStyle
include RuboCop::RSpec::FactoryBot::Language

MSG_CREATE_LIST = 'Prefer create_list.'
MSG_N_TIMES = 'Prefer %<number>s.times.'
Expand All @@ -43,12 +44,12 @@ class CreateList < Base

# @!method factory_call(node)
def_node_matcher :factory_call, <<-PATTERN
(send ${(const nil? {:FactoryGirl :FactoryBot}) nil?} :create (sym $_) $...)
(send ${nil? #factory_bot?} :create (sym $_) $...)
PATTERN

# @!method factory_list_call(node)
def_node_matcher :factory_list_call, <<-PATTERN
(send {(const nil? {:FactoryGirl :FactoryBot}) nil?} :create_list (sym _) (int $_) ...)
(send {nil? #factory_bot?} :create_list (sym _) (int $_) ...)
PATTERN

def on_block(node)
Expand Down
107 changes: 107 additions & 0 deletions lib/rubocop/cop/rspec/factory_bot/syntax_methods.rb
@@ -0,0 +1,107 @@
# frozen_string_literal: true

module RuboCop
module Cop
module RSpec
module FactoryBot
# Use shorthands from `FactoryBot::Syntax::Methods` in your specs.
#
# @safety
# The auto-correction is marked as unsafe because the cop
# cannot verify whether you already include
# `FactoryBot::Syntax::Methods` in your test suite.
#
# If you're using Rails, add the following configuration to
# `spec/support/factory_bot.rb` and be sure to require that file in
# `rails_helper.rb`:
#
# [source,ruby]
# ----
# RSpec.configure do |config|
# config.include FactoryBot::Syntax::Methods
# end
# ----
#
# If you're not using Rails:
#
# [source,ruby]
# ----
# RSpec.configure do |config|
# config.include FactoryBot::Syntax::Methods
#
# config.before(:suite) do
# FactoryBot.find_definitions
# end
# end
# ----
#
# @example
# # bad
# FactoryBot.create(:bar)
# FactoryBot.build(:bar)
# FactoryBot.attributes_for(:bar)
#
# # good
# create(:bar)
# build(:bar)
# attributes_for(:bar)
#
class SyntaxMethods < Base
extend AutoCorrector
include InsideExampleGroup
include RangeHelp
include RuboCop::RSpec::FactoryBot::Language

MSG = 'Use `%<method>s` from `FactoryBot::Syntax::Methods`.'

RESTRICT_ON_SEND = %i[
attributes_for
attributes_for_list
attributes_for_pair
build
build_list
build_pair
build_stubbed
build_stubbed_list
build_stubbed_pair
create
create_list
create_pair
generate
generate_list
null
null_list
null_pair
].to_set.freeze

def on_send(node)
return unless factory_bot?(node.receiver)
return unless inside_example_group?(node)

message = format(MSG, method: node.method_name)

add_offense(crime_scene(node), message: message) do |corrector|
corrector.remove(offense(node))
end
end

private

def crime_scene(node)
range_between(
node.loc.expression.begin_pos,
node.loc.selector.end_pos
)
end

def offense(node)
range_between(
node.loc.expression.begin_pos,
node.loc.selector.begin_pos
)
end
end
end
end
end
end
29 changes: 29 additions & 0 deletions lib/rubocop/cop/rspec/mixin/inside_example_group.rb
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module RuboCop
module Cop
module RSpec
# Helps you identify whether a given node
# is within an example group or not.
module InsideExampleGroup
private

def inside_example_group?(node)
return example_group?(node) if example_group_root?(node)

root = node.ancestors.find { |parent| example_group_root?(parent) }

example_group?(root)
end

def example_group_root?(node)
node.parent.nil? || example_group_root_with_siblings?(node.parent)
end

def example_group_root_with_siblings?(node)
node.begin_type? && node.parent.nil?
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rspec_cops.rb
Expand Up @@ -7,6 +7,7 @@
require_relative 'rspec/factory_bot/attribute_defined_statically'
require_relative 'rspec/factory_bot/create_list'
require_relative 'rspec/factory_bot/factory_class_name'
require_relative 'rspec/factory_bot/syntax_methods'

require_relative 'rspec/rails/avoid_setup_hook'
begin
Expand Down
17 changes: 17 additions & 0 deletions lib/rubocop/rspec/factory_bot/language.rb
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module RuboCop
module RSpec
module FactoryBot
# Contains node matchers for common FactoryBot DSL.
module Language
extend RuboCop::NodePattern::Macros

# @!method factory_bot?(node)
def_node_matcher :factory_bot?, <<~PATTERN
(const {nil? cbase} {:FactoryGirl :FactoryBot})
PATTERN
end
end
end
end

0 comments on commit 16ce074

Please sign in to comment.