Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new RSpec/FactoryBot/SyntaxMethods cop. #1215

Merged
merged 1 commit into from Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .yardopts
@@ -0,0 +1,3 @@
--markup markdown
--hide-void-return
--tag safety:"Cop Safety Information"
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>>"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't use that notion in the past, but it's quite a useful trick.

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
pirj marked this conversation as resolved.
Show resolved Hide resolved

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
pirj marked this conversation as resolved.
Show resolved Hide resolved

# @!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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL ❤️

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FactoryBot::Syntax::Methods.instance_methods.sort 😉

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)
pirj marked this conversation as resolved.
Show resolved Hide resolved
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
pirj marked this conversation as resolved.
Show resolved Hide resolved
# is within an example group or not.
module InsideExampleGroup
private

def inside_example_group?(node)
leoarnold marked this conversation as resolved.
Show resolved Hide resolved
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