Skip to content

Commit

Permalink
Extract Factory Bot cops
Browse files Browse the repository at this point in the history
  • Loading branch information
ydah committed May 6, 2023
1 parent 75c16dd commit 12616e1
Show file tree
Hide file tree
Showing 22 changed files with 228 additions and 2,066 deletions.
2 changes: 1 addition & 1 deletion .simplecov
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

SimpleCov.start do
enable_coverage :branch
minimum_coverage line: 99.60, branch: 95.32
minimum_coverage line: 99.60, branch: 94.84
add_filter '/spec/'
add_filter '/vendor/bundle/'
end
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Master (Unreleased)

- Extract factory_bot cops to a separate repository, [`rubocop-factory_bot`](https://github.com/rubocop/rubocop-factory_bot). The `rubocop-factory_bot` repository is a dependency of `rubocop-rspec` and the factory_bot cops are aliased (`RSpec/FactoryBot/Foo` == `FactoryBot/Foo`) until v3.0 is released, so the change will be invisible to users until then. ([@ydah])

## 2.21.0 (2023-05-05)

- Fix a false positive in `RSpec/IndexedLet` with suffixes after index-like numbers. ([@pirj])
Expand Down
6 changes: 6 additions & 0 deletions config/obsoletion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ renamed:
RSpec/Capybara/SpecificFinders: Capybara/SpecificFinders
RSpec/Capybara/SpecificMatcher: Capybara/SpecificMatcher
RSpec/Capybara/VisibilityMatcher: Capybara/VisibilityMatcher
RSpec/FactoryBot/AttributeDefinedStatically: FactoryBot/AttributeDefinedStatically
RSpec/FactoryBot/ConsistentParenthesesStyle: FactoryBot/ConsistentParenthesesStyle
RSpec/FactoryBot/CreateList: FactoryBot/CreateList
RSpec/FactoryBot/FactoryClassName: FactoryBot/FactoryClassName
RSpec/FactoryBot/FactoryNameStyle: FactoryBot/FactoryNameStyle
RSpec/FactoryBot/SyntaxMethods: FactoryBot/SyntaxMethods
10 changes: 5 additions & 5 deletions docs/modules/ROOT/pages/cops_rspec_factorybot.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,9 @@ create_list :user, 3
Use string value when setting the class attribute explicitly.

This cop would promote faster tests by lazy-loading of
application files. Also, this could help you suppress potential bugs
in combination with external libraries by avoiding a preload of
application files from the factory files.
application files. Also, this could help you suppress potential
bugs in combination with external libraries by avoiding a preload
of application files from the factory files.

=== Examples

Expand Down Expand Up @@ -312,8 +312,8 @@ 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`:
`spec/support/factory_bot.rb` and be sure to require that file
in `rails_helper.rb`:

[source,ruby]
----
Expand Down
4 changes: 1 addition & 3 deletions lib/rubocop-rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

require 'rubocop'
require 'rubocop-capybara'
require 'rubocop-factory_bot'

require_relative 'rubocop/rspec'
require_relative 'rubocop/rspec/inject'
Expand All @@ -16,8 +17,6 @@
# Dependent on `RuboCop::RSpec::Language::NodePattern`.
require_relative 'rubocop/rspec/language'

require_relative 'rubocop/rspec/factory_bot/language'

require_relative 'rubocop/cop/rspec/mixin/final_end_location'
require_relative 'rubocop/cop/rspec/mixin/inside_example_group'
require_relative 'rubocop/cop/rspec/mixin/location_help'
Expand All @@ -37,7 +36,6 @@
require_relative 'rubocop/rspec/corrector/move_node'
require_relative 'rubocop/rspec/example'
require_relative 'rubocop/rspec/example_group'
require_relative 'rubocop/rspec/factory_bot'
require_relative 'rubocop/rspec/hook'

RuboCop::RSpec::Inject.defaults!
Expand Down
143 changes: 25 additions & 118 deletions lib/rubocop/cop/rspec/factory_bot/attribute_defined_statically.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,124 +4,31 @@ module RuboCop
module Cop
module RSpec
module FactoryBot
# Always declare attribute values as blocks.
#
# @example
# # bad
# kind [:active, :rejected].sample
#
# # good
# kind { [:active, :rejected].sample }
#
# # bad
# closed_at 1.day.from_now
#
# # good
# closed_at { 1.day.from_now }
#
# # bad
# count 1
#
# # good
# count { 1 }
#
class AttributeDefinedStatically < ::RuboCop::Cop::Base
extend AutoCorrector

MSG = 'Use a block to declare attribute values.'

# @!method value_matcher(node)
def_node_matcher :value_matcher, <<-PATTERN
(send _ !#reserved_method? $...)
PATTERN

# @!method factory_attributes(node)
def_node_matcher :factory_attributes, <<-PATTERN
(block (send _ #attribute_defining_method? ...) _ { (begin $...) $(send ...) } )
PATTERN

def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
attributes = factory_attributes(node) || []
attributes = [attributes] unless attributes.is_a?(Array) # rubocop:disable Style/ArrayCoercion, Lint/RedundantCopDisableDirective

attributes.each do |attribute|
next unless offensive_receiver?(attribute.receiver, node)
next if proc?(attribute) || association?(attribute.first_argument)

add_offense(attribute) do |corrector|
autocorrect(corrector, attribute)
end
end
end

private

def autocorrect(corrector, node)
if node.parenthesized?
autocorrect_replacing_parens(corrector, node)
else
autocorrect_without_parens(corrector, node)
end
end

def offensive_receiver?(receiver, node)
receiver.nil? ||
receiver.self_type? ||
receiver_matches_first_block_argument?(receiver, node)
end

def receiver_matches_first_block_argument?(receiver, node)
first_block_argument = node.arguments.first

!first_block_argument.nil? &&
receiver.lvar_type? &&
receiver.node_parts == first_block_argument.node_parts
end

def proc?(attribute)
value_matcher(attribute).to_a.all?(&:block_pass_type?)
end

# @!method association?(node)
def_node_matcher :association?, '(hash <(pair (sym :factory) _) ...>)'

def autocorrect_replacing_parens(corrector, node)
left_braces, right_braces = braces(node)

corrector.replace(node.location.begin, " #{left_braces}")
corrector.replace(node.location.end, right_braces)
end

def autocorrect_without_parens(corrector, node)
left_braces, right_braces = braces(node)

argument = node.first_argument
expression = argument.source_range
corrector.insert_before(expression, left_braces)
corrector.insert_after(expression, right_braces)
end

def braces(node)
if value_hash_without_braces?(node.first_argument)
['{ { ', ' } }']
else
['{ ', ' }']
end
end

def value_hash_without_braces?(node)
node.hash_type? && !node.braces?
end

def reserved_method?(method_name)
RuboCop::RSpec::FactoryBot.reserved_methods.include?(method_name)
end

def attribute_defining_method?(method_name)
RuboCop::RSpec::FactoryBot.attribute_defining_methods
.include?(method_name)
end
end
# @!parse
# # Always declare attribute values as blocks.
# #
# # @example
# # # bad
# # kind [:active, :rejected].sample
# #
# # # good
# # kind { [:active, :rejected].sample }
# #
# # # bad
# # closed_at 1.day.from_now
# #
# # # good
# # closed_at { 1.day.from_now }
# #
# # # bad
# # count 1
# #
# # # good
# # count { 1 }
# #
# class AttributeDefinedStatically < ::RuboCop::Cop::Base; end
AttributeDefinedStatically =
::RuboCop::Cop::FactoryBot::AttributeDefinedStatically
end
end
end
Expand Down
147 changes: 40 additions & 107 deletions lib/rubocop/cop/rspec/factory_bot/consistent_parentheses_style.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,113 +4,46 @@ module RuboCop
module Cop
module RSpec
module FactoryBot
# Use a consistent style for parentheses in factory bot calls.
#
# @example
#
# # bad
# create :user
# build(:user)
# create(:login)
# create :login
#
# @example `EnforcedStyle: require_parentheses` (default)
#
# # good
# create(:user)
# create(:user)
# create(:login)
# build(:login)
#
# @example `EnforcedStyle: omit_parentheses`
#
# # good
# create :user
# build :user
# create :login
# create :login
#
# # also good
# # when method name and first argument are not on same line
# create(
# :user
# )
# build(
# :user,
# name: 'foo'
# )
#
class ConsistentParenthesesStyle < ::RuboCop::Cop::Base
extend AutoCorrector
include ConfigurableEnforcedStyle
include RuboCop::RSpec::FactoryBot::Language
include RuboCop::Cop::Util

def self.autocorrect_incompatible_with
[Style::MethodCallWithArgsParentheses]
end

MSG_REQUIRE_PARENS = 'Prefer method call with parentheses'
MSG_OMIT_PARENS = 'Prefer method call without parentheses'

FACTORY_CALLS = RuboCop::RSpec::FactoryBot::Language::METHODS

RESTRICT_ON_SEND = FACTORY_CALLS

# @!method factory_call(node)
def_node_matcher :factory_call, <<-PATTERN
(send
{#factory_bot? nil?} %FACTORY_CALLS
{sym str send lvar} _*
)
PATTERN

def on_send(node)
return if ambiguous_without_parentheses?(node)

factory_call(node) do
return if node.method?(:generate) && node.arguments.count > 1

if node.parenthesized?
process_with_parentheses(node)
else
process_without_parentheses(node)
end
end
end

private

def process_with_parentheses(node)
return unless style == :omit_parentheses
return unless same_line?(node, node.first_argument)

add_offense(node.loc.selector,
message: MSG_OMIT_PARENS) do |corrector|
remove_parentheses(corrector, node)
end
end

def process_without_parentheses(node)
return unless style == :require_parentheses

add_offense(node.loc.selector,
message: MSG_REQUIRE_PARENS) do |corrector|
add_parentheses(node, corrector)
end
end

AMBIGUOUS_TYPES = %i[send pair array and or if].freeze

def ambiguous_without_parentheses?(node)
node.parent && AMBIGUOUS_TYPES.include?(node.parent.type)
end

def remove_parentheses(corrector, node)
corrector.replace(node.location.begin, ' ')
corrector.remove(node.location.end)
end
end
# @!parse
# # Use a consistent style for parentheses in factory bot calls.
# #
# # @example
# #
# # # bad
# # create :user
# # build(:user)
# # create(:login)
# # create :login
# #
# # @example `EnforcedStyle: require_parentheses` (default)
# #
# # # good
# # create(:user)
# # create(:user)
# # create(:login)
# # build(:login)
# #
# # @example `EnforcedStyle: omit_parentheses`
# #
# # # good
# # create :user
# # build :user
# # create :login
# # create :login
# #
# # # also good
# # # when method name and first argument are not on same line
# # create(
# # :user
# # )
# # build(
# # :user,
# # name: 'foo'
# # )
# #
# class ConsistentParenthesesStyle < ::RuboCop::Cop::Base; end
ConsistentParenthesesStyle =
::RuboCop::Cop::FactoryBot::ConsistentParenthesesStyle
end
end
end
Expand Down

0 comments on commit 12616e1

Please sign in to comment.