Merge pull request #5106 from MikeMcQuaid/vendor-rubocop-rspec

Vendor rubocop-rspec
This commit is contained in:
Mike McQuaid 2018-10-20 12:43:00 +01:00 committed by GitHub
commit 57968a29a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 6993 additions and 5 deletions

13
.gitignore vendored
View File

@ -23,7 +23,9 @@
**/.bundle/cache
**/vendor/bundle
**/vendor/ruby
**/vendor/bundle-standalone/ruby/*/bin
**/vendor/bundle-standalone/ruby/*/cache
**/vendor/bundle-standalone/ruby/*/extensions
**/vendor/bundle-standalone/ruby/*/gems/*/*
**/vendor/bundle-standalone/ruby/*/specifications
@ -39,6 +41,17 @@
**/vendor/bundle-standalone/ruby/*/gems/thread_safe-*/lib
**/vendor/bundle-standalone/ruby/*/gems/tzinfo-*/lib
# Ignore rubocop dependencies we don't wish to vendor
**/vendor/bundle-standalone/ruby/*/gems/ast-*/
**/vendor/bundle-standalone/ruby/*/gems/jaro_winkler-*/
**/vendor/bundle-standalone/ruby/*/gems/parallel-*/
**/vendor/bundle-standalone/ruby/*/gems/parser-*/
**/vendor/bundle-standalone/ruby/*/gems/powerpack-*/
**/vendor/bundle-standalone/ruby/*/gems/rainbow-*/
**/vendor/bundle-standalone/ruby/*/gems/rubocop-*/
**/vendor/bundle-standalone/ruby/*/gems/ruby-progressbar-*/
**/vendor/bundle-standalone/ruby/*/gems/unicode-display_width-*/
# Ignore `bin` contents (again).
/bin

View File

@ -5,9 +5,7 @@ AllCops:
- '**/vendor/**/*'
DisplayCopNames: false
require:
- ./Homebrew/rubocops.rb
- rubocop-rspec
require: ./Homebrew/rubocops.rb
# enable all formulae audits
FormulaAudit:

View File

@ -1,5 +1,6 @@
require_relative "load_path"
require "rubocop-rspec"
require "rubocops/formula_desc_cop"
require "rubocops/components_order_cop"
require "rubocops/components_redundancy_cop"

View File

@ -8,7 +8,6 @@ gem "rspec-its", require: false
gem "rspec-retry", require: false
gem "rspec-wait", require: false
gem "rubocop", HOMEBREW_RUBOCOP_VERSION
gem "rubocop-rspec", require: false
group :development do
gem "ronn", require: false

View File

@ -5,3 +5,8 @@ gem "concurrent-ruby"
gem "backports"
gem "plist"
gem "ruby-macho"
gem "rubocop-rspec"
# not actually vendored but used to ensure the version matches here.
require_relative "../constants"
gem "rubocop", HOMEBREW_RUBOCOP_VERSION

View File

@ -6,16 +6,35 @@ GEM
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
ast (2.4.0)
backports (3.11.4)
concurrent-ruby (1.0.5)
i18n (1.1.0)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.1)
minitest (5.11.3)
parallel (1.12.1)
parser (2.5.1.2)
ast (~> 2.4.0)
plist (3.4.0)
powerpack (0.1.2)
rainbow (3.0.0)
rubocop (0.59.1)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-rspec (1.30.0)
rubocop (>= 0.58.0)
ruby-macho (2.1.0)
ruby-progressbar (1.10.0)
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unicode-display_width (1.4.0)
PLATFORMS
ruby
@ -25,7 +44,9 @@ DEPENDENCIES
backports
concurrent-ruby
plist
rubocop (= 0.59.1)
rubocop-rspec
ruby-macho
BUNDLED WITH
1.16.4
1.16.6

View File

@ -9,7 +9,18 @@ $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/minitest-5.11.3/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/thread_safe-0.3.6/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/tzinfo-1.2.5/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/activesupport-5.2.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/ast-2.4.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/backports-3.11.4/lib"
$:.unshift "#{path}/"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/universal-darwin-18/2.3.0/jaro_winkler-1.5.1"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/jaro_winkler-1.5.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parallel-1.12.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parser-2.5.1.2/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/plist-3.4.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/powerpack-0.1.2/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rainbow-3.0.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/ruby-progressbar-1.10.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/unicode-display_width-1.4.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-0.59.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-rspec-1.30.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/ruby-macho-2.1.0/lib"

View File

@ -0,0 +1,451 @@
---
AllCops:
RSpec:
Patterns:
- _spec.rb
- "(?:^|/)spec/"
RSpec/FactoryBot:
Patterns:
- spec/factories.rb
- spec/factories/**/*.rb
- features/support/factories/**/*.rb
RSpec/AlignLeftLetBrace:
Description: Checks that left braces for adjacent single line lets are aligned.
Enabled: false
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/AlignLeftLetBrace
RSpec/AlignRightLetBrace:
Description: Checks that right braces for adjacent single line lets are aligned.
Enabled: false
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/AlignRightLetBrace
RSpec/AnyInstance:
Description: Check that instances are not being stubbed globally.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/AnyInstance
RSpec/AroundBlock:
Description: Checks that around blocks actually run the test.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/AroundBlock
RSpec/Be:
Description: Check for expectations where `be` is used without argument.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Be
RSpec/BeEql:
Description: Check for expectations where `be(...)` can replace `eql(...)`.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/BeEql
RSpec/BeforeAfterAll:
Description: Check that before/after(:all) isn't being used.
Enabled: true
Exclude:
- spec/spec_helper.rb
- spec/rails_helper.rb
- spec/support/**/*.rb
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/BeforeAfterAll
RSpec/ContextWording:
Description: "`context` block descriptions should start with 'when', or 'with'."
Enabled: true
Prefixes:
- when
- with
- without
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ContextWording
RSpec/DescribeClass:
Description: Check that the first argument to the top level describe is a constant.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/DescribeClass
RSpec/DescribeMethod:
Description: Checks that the second argument to `describe` specifies a method.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/DescribeMethod
RSpec/DescribeSymbol:
Description: Avoid describing symbols.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/DescribeSymbol
RSpec/DescribedClass:
Description: Checks that tests use `described_class`.
SkipBlocks: false
Enabled: true
EnforcedStyle: described_class
SupportedStyles:
- described_class
- explicit
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/DescribedClass
RSpec/EmptyExampleGroup:
Description: Checks if an example group does not include any tests.
Enabled: true
CustomIncludeMethods: []
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyExampleGroup
RSpec/EmptyLineAfterExampleGroup:
Description: Checks if there is an empty line after example group blocks.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyLineAfterExampleGroup
RSpec/EmptyLineAfterFinalLet:
Description: Checks if there is an empty line after the last let block.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyLineAfterFinalLet
RSpec/EmptyLineAfterHook:
Description: Checks if there is an empty line after hook blocks.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyLineAfterHook
RSpec/EmptyLineAfterSubject:
Description: Checks if there is an empty line after subject block.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyLineAfterSubject
RSpec/ExampleLength:
Description: Checks for long examples.
Enabled: true
Max: 5
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExampleLength
RSpec/ExampleWithoutDescription:
Description: Checks for examples without a description.
Enabled: true
EnforcedStyle: always_allow
SupportedStyles:
- always_allow
- single_line_only
- disallow
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExampleWithoutDescription
RSpec/ExampleWording:
Description: Checks for common mistakes in example descriptions.
Enabled: true
CustomTransform:
be: is
BE: IS
have: has
HAVE: HAS
IgnoredWords: []
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExampleWording
RSpec/ExpectActual:
Description: Checks for `expect(...)` calls containing literal values.
Enabled: true
Exclude:
- spec/routing/**/*
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExpectActual
RSpec/ExpectChange:
Description: Checks for consistent style of change matcher.
Enabled: true
EnforcedStyle: method_call
SupportedStyles:
- method_call
- block
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExpectChange
RSpec/ExpectInHook:
Enabled: true
Description: Do not use `expect` in hooks such as `before`.
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExpectInHook
RSpec/ExpectOutput:
Description: Checks for opportunities to use `expect { ... }.to output`.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExpectOutput
RSpec/FilePath:
Description: Checks that spec file paths are consistent with the test subject.
Enabled: true
CustomTransform:
RuboCop: rubocop
RSpec: rspec
IgnoreMethods: false
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FilePath
RSpec/Focus:
Description: Checks if examples are focused.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Focus
RSpec/HookArgument:
Description: Checks the arguments passed to `before`, `around`, and `after`.
Enabled: true
EnforcedStyle: implicit
SupportedStyles:
- implicit
- each
- example
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/HookArgument
RSpec/HooksBeforeExamples:
Enabled: true
Description: Checks for before/around/after hooks that come after an example.
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/HooksBeforeExamples
RSpec/ImplicitExpect:
Description: Check that a consistent implicit expectation style is used.
Enabled: true
EnforcedStyle: is_expected
SupportedStyles:
- is_expected
- should
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ImplicitExpect
RSpec/ImplicitSubject:
Enabled: true
Description: Checks for usage of implicit subject (`is_expected` / `should`).
EnforcedStyle: single_line_only
SupportedStyles:
- single_line_only
- single_statement_only
- disallow
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ImplicitSubject
RSpec/InstanceSpy:
Description: Checks for `instance_double` used with `have_received`.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/InstanceSpy
RSpec/InstanceVariable:
Description: Checks for instance variable usage in specs.
AssignmentOnly: false
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/InstanceVariable
RSpec/InvalidPredicateMatcher:
Description: Checks invalid usage for predicate matcher.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/InvalidPredicateMatcher
RSpec/ItBehavesLike:
Description: Checks that only one `it_behaves_like` style is used.
Enabled: true
EnforcedStyle: it_behaves_like
SupportedStyles:
- it_behaves_like
- it_should_behave_like
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ItBehavesLike
RSpec/IteratedExpectation:
Description: Check that `all` matcher is used instead of iterating over an array.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/IteratedExpectation
RSpec/LeadingSubject:
Description: Enforce that subject is the first definition in the test.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LeadingSubject
RSpec/LetBeforeExamples:
Description: Checks for `let` definitions that come after an example.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LetBeforeExamples
RSpec/LetSetup:
Description: Checks unreferenced `let!` calls being used for test setup.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LetSetup
RSpec/MessageChain:
Description: Check that chains of messages are not being stubbed.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MessageChain
RSpec/MessageExpectation:
Description: Checks for consistent message expectation style.
Enabled: false
EnforcedStyle: allow
SupportedStyles:
- allow
- expect
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MessageExpectation
RSpec/MessageSpies:
Description: Checks that message expectations are set using spies.
Enabled: true
EnforcedStyle: have_received
SupportedStyles:
- have_received
- receive
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MessageSpies
RSpec/MissingExampleGroupArgument:
Description: Checks that the first argument to an example group is not empty.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MissingExampleGroupArgument
RSpec/MultipleDescribes:
Description: Checks for multiple top level describes.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleDescribes
RSpec/MultipleExpectations:
Description: Checks if examples contain too many `expect` calls.
Enabled: true
Max: 1
AggregateFailuresByDefault: false
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleExpectations
RSpec/MultipleSubjects:
Description: Checks if an example group defines `subject` multiple times.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleSubjects
RSpec/NamedSubject:
Description: Checks for explicitly referenced test subjects.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NamedSubject
RSpec/NestedGroups:
Description: Checks for nested example groups.
Enabled: true
Max: 3
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NestedGroups
RSpec/NotToNot:
Description: Checks for consistent method usage for negating expectations.
EnforcedStyle: not_to
SupportedStyles:
- not_to
- to_not
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NotToNot
RSpec/OverwritingSetup:
Enabled: true
Description: Checks if there is a let/subject that overwrites an existing one.
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/OverwritingSetup
RSpec/Pending:
Enabled: false
Description: Checks for any pending or skipped examples.
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Pending
RSpec/PredicateMatcher:
Description: Prefer using predicate matcher over using predicate method directly.
Enabled: true
Strict: true
EnforcedStyle: inflected
SupportedStyles:
- inflected
- explicit
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/PredicateMatcher
RSpec/ReceiveCounts:
Enabled: true
Description: Check for `once` and `twice` receive counts matchers usage.
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ReceiveCounts
RSpec/ReceiveNever:
Enabled: true
Description: Prefer `not_to receive(...)` over `receive(...).never`.
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ReceiveNever
RSpec/RepeatedDescription:
Enabled: true
Description: Check for repeated description strings in example groups.
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedDescription
RSpec/RepeatedExample:
Enabled: true
Description: Check for repeated examples within example groups.
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExample
RSpec/ReturnFromStub:
Enabled: true
Description: Checks for consistent style of stub's return setting.
EnforcedStyle: and_return
SupportedStyles:
- and_return
- block
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ReturnFromStub
RSpec/ScatteredLet:
Description: Checks for let scattered across the example group.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ScatteredLet
RSpec/ScatteredSetup:
Description: Checks for setup scattered across multiple hooks in an example group.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ScatteredSetup
RSpec/SharedContext:
Description: Checks for proper shared_context and shared_examples usage.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SharedContext
RSpec/SharedExamples:
Description: Enforces use of string to titleize shared examples.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SharedExamples
RSpec/SingleArgumentMessageChain:
Description: Checks that chains of messages contain more than one element.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SingleArgumentMessageChain
RSpec/SubjectStub:
Description: Checks for stubbed test subjects.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SubjectStub
RSpec/UnspecifiedException:
Description: Checks for a specified error in checking raised errors.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/UnspecifiedException
RSpec/VerifiedDoubles:
Description: Prefer using verifying doubles over normal doubles.
Enabled: true
IgnoreNameless: true
IgnoreSymbolicNames: false
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/VerifiedDoubles
RSpec/VoidExpect:
Description: This cop checks void `expect()`.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/VoidExpect
Capybara/CurrentPathExpectation:
Description: Checks that no expectations are set on Capybara's `current_path`.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/CurrentPathExpectation
Capybara/FeatureMethods:
Description: Checks for consistent method usage in feature specs.
Enabled: true
EnabledMethods: []
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/FeatureMethods
FactoryBot/AttributeDefinedStatically:
Description: Always declare attribute values as blocks.
Enabled: true
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/AttributeDefinedStatically
FactoryBot/CreateList:
Description: Checks for create_list usage.
Enabled: true
EnforcedStyle: create_list
SupportedStyles:
- create_list
- n_times
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/CreateList
Rails/HttpStatus:
Description: Enforces use of symbolic or numeric value to describe HTTP status.
Enabled: true
EnforcedStyle: symbolic
SupportedStyles:
- numeric
- symbolic
StyleGuide: http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/HttpStatus

View File

@ -0,0 +1,43 @@
require 'pathname'
require 'yaml'
require 'rubocop'
require_relative 'rubocop/rspec'
require_relative 'rubocop/rspec/version'
require_relative 'rubocop/rspec/inject'
require_relative 'rubocop/rspec/node'
require_relative 'rubocop/rspec/top_level_describe'
require_relative 'rubocop/rspec/wording'
require_relative 'rubocop/rspec/util'
require_relative 'rubocop/rspec/language'
require_relative 'rubocop/rspec/language/node_pattern'
require_relative 'rubocop/rspec/concept'
require_relative 'rubocop/rspec/example_group'
require_relative 'rubocop/rspec/example'
require_relative 'rubocop/rspec/hook'
require_relative 'rubocop/cop/rspec/cop'
require_relative 'rubocop/rspec/align_let_brace'
require_relative 'rubocop/rspec/final_end_location'
require_relative 'rubocop/rspec/blank_line_separation'
RuboCop::RSpec::Inject.defaults!
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
module RuboCop
module Cop
module Layout
class ExtraSpacing # rubocop:disable Style/Documentation
def self.autocorrect_incompatible_with
[RSpec::AlignLeftLetBrace, RSpec::AlignRightLetBrace]
end
end
end
end
end
RuboCop::AST::Node.include(RuboCop::RSpec::Node)

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks that left braces for adjacent single line lets are aligned.
#
# @example
#
# # bad
# let(:foobar) { blahblah }
# let(:baz) { bar }
# let(:a) { b }
#
# # good
# let(:foobar) { blahblah }
# let(:baz) { bar }
# let(:a) { b }
#
class AlignLeftLetBrace < Cop
MSG = 'Align left let brace'.freeze
def self.autocorrect_incompatible_with
[Layout::ExtraSpacing]
end
def investigate(_processed_source)
return if processed_source.blank?
token_aligner.offending_tokens.each do |let|
add_offense(let, location: :begin)
end
end
def autocorrect(let)
lambda do |corrector|
corrector.insert_before(
let.loc.begin,
token_aligner.indent_for(let)
)
end
end
private
def token_aligner
@token_aligner ||=
RuboCop::RSpec::AlignLetBrace.new(processed_source.ast, :begin)
end
end
end
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks that right braces for adjacent single line lets are aligned.
#
# @example
#
# # bad
# let(:foobar) { blahblah }
# let(:baz) { bar }
# let(:a) { b }
#
# # good
# let(:foobar) { blahblah }
# let(:baz) { bar }
# let(:a) { b }
#
class AlignRightLetBrace < Cop
MSG = 'Align right let brace'.freeze
def self.autocorrect_incompatible_with
[Layout::ExtraSpacing]
end
def investigate(_processed_source)
return if processed_source.blank?
token_aligner.offending_tokens.each do |let|
add_offense(let, location: :end)
end
end
def autocorrect(let)
lambda do |corrector|
corrector.insert_before(
let.loc.end,
token_aligner.indent_for(let)
)
end
end
private
def token_aligner
@token_aligner ||=
RuboCop::RSpec::AlignLetBrace.new(processed_source.ast, :end)
end
end
end
end
end

View File

@ -0,0 +1,42 @@
module RuboCop
module Cop
module RSpec
# Check that instances are not being stubbed globally.
#
# Prefer instance doubles over stubbing any instance of a class
#
# @example
# # bad
# describe MyClass do
# before { allow_any_instance_of(MyClass).to receive(:foo) }
# end
#
# # good
# describe MyClass do
# let(:my_instance) { instance_double(MyClass) }
#
# before do
# allow(MyClass).to receive(:new).and_return(my_instance)
# allow(my_instance).to receive(:foo)
# end
# end
class AnyInstance < Cop
MSG = 'Avoid stubbing using `%<method>s`.'.freeze
def_node_matcher :disallowed_stub, <<-PATTERN
(send _ ${:any_instance :allow_any_instance_of :expect_any_instance_of} ...)
PATTERN
def on_send(node)
disallowed_stub(node) do |method|
add_offense(
node,
location: :expression,
message: format(MSG, method: method)
)
end
end
end
end
end
end

View File

@ -0,0 +1,71 @@
module RuboCop
module Cop
module RSpec
# Checks that around blocks actually run the test.
#
# @example
# # bad
# around do
# some_method
# end
#
# around do |test|
# some_method
# end
#
# # good
# around do |test|
# some_method
# test.call
# end
#
# around do |test|
# some_method
# test.run
# end
class AroundBlock < Cop
MSG_NO_ARG = 'Test object should be passed to around block.'.freeze
MSG_UNUSED_ARG = 'You should call `%<arg>s.call` '\
'or `%<arg>s.run`.'.freeze
def_node_matcher :hook, <<-PATTERN
(block {(send nil? :around) (send nil? :around sym)} (args $...) ...)
PATTERN
def_node_search :find_arg_usage, <<-PATTERN
{(send $... {:call :run}) (send _ _ $...) (yield $...) (block-pass $...)}
PATTERN
def on_block(node)
hook(node) do |(example_proxy)|
if example_proxy.nil?
add_no_arg_offense(node)
else
check_for_unused_proxy(node, example_proxy)
end
end
end
private
def add_no_arg_offense(node)
add_offense(node, location: :expression, message: MSG_NO_ARG)
end
def check_for_unused_proxy(block, proxy)
name, = *proxy
find_arg_usage(block) do |usage|
return if usage.include?(s(:lvar, name))
end
add_offense(
proxy,
location: :expression,
message: format(MSG_UNUSED_ARG, arg: name)
)
end
end
end
end
end

View File

@ -0,0 +1,35 @@
module RuboCop
module Cop
module RSpec
# Check for expectations where `be` is used without argument.
#
# The `be` matcher is too generic, as it pass on everything that is not
# nil or false. If that is the exact intend, use `be_truthy`. In all other
# cases it's better to specify what exactly is the expected value.
#
# @example
#
# # bad
# expect(foo).to be
#
# # good
# expect(foo).to be_truthy
# expect(foo).to be 1.0
# expect(foo).to be(true)
#
class Be < Cop
MSG = 'Don\'t use `be` without an argument.'.freeze
def_node_matcher :be_without_args, <<-PATTERN
(send _ {:to :not_to :to_not} $(send nil? :be))
PATTERN
def on_send(node)
be_without_args(node) do |matcher|
add_offense(matcher, location: :selector)
end
end
end
end
end
end

View File

@ -0,0 +1,55 @@
module RuboCop
module Cop
module RSpec
# Check for expectations where `be(...)` can replace `eql(...)`.
#
# The `be` matcher compares by identity while the `eql` matcher
# compares using `eql?`. Integers, floats, booleans, symbols, and nil
# can be compared by identity and therefore the `be` matcher is
# preferable as it is a more strict test.
#
# @example
#
# # bad
# expect(foo).to eql(1)
# expect(foo).to eql(1.0)
# expect(foo).to eql(true)
# expect(foo).to eql(false)
# expect(foo).to eql(:bar)
# expect(foo).to eql(nil)
#
# # good
# expect(foo).to be(1)
# expect(foo).to be(1.0)
# expect(foo).to be(true)
# expect(foo).to be(false)
# expect(foo).to be(:bar)
# expect(foo).to be(nil)
#
# This cop only looks for instances of `expect(...).to eql(...)`. We
# do not check `to_not` or `not_to` since `!eql?` is more strict
# than `!equal?`. We also do not try to flag `eq` because if
# `a == b`, and `b` is comparable by identity, `a` is still not
# necessarily the same type as `b` since the `#==` operator can
# coerce objects for comparison.
#
class BeEql < Cop
MSG = 'Prefer `be` over `eql`.'.freeze
def_node_matcher :eql_type_with_identity, <<-PATTERN
(send _ :to $(send nil? :eql {true false int float sym nil_type?}))
PATTERN
def on_send(node)
eql_type_with_identity(node) do |eql|
add_offense(eql, location: :selector)
end
end
def autocorrect(node)
->(corrector) { corrector.replace(node.loc.selector, 'be') }
end
end
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Check that before/after(:all) isn't being used.
#
# @example
# # bad
# #
# # Faster but risk of state leaking between examples
# #
# describe MyClass do
# before(:all) { Widget.create }
# after(:all) { Widget.delete_all }
# end
#
# # good
# #
# # Slower but examples are properly isolated
# #
# describe MyClass do
# before(:each) { Widget.create }
# after(:each) { Widget.delete_all }
# end
class BeforeAfterAll < Cop
MSG = 'Beware of using `%<hook>s` as it may cause state to leak '\
'between tests. If you are using `rspec-rails`, and '\
'`use_transactional_fixtures` is enabled, then records created '\
'in `%<hook>s` are not automatically rolled back.'.freeze
def_node_matcher :before_or_after_all, <<-PATTERN
$(send _ {:before :after} (sym {:all :context}))
PATTERN
def on_send(node)
before_or_after_all(node) do |hook|
add_offense(
node,
location: :expression,
message: format(MSG, hook: hook.source)
)
end
end
end
end
end
end

View File

@ -0,0 +1,42 @@
module RuboCop
module Cop
module RSpec
module Capybara
# Checks that no expectations are set on Capybara's `current_path`.
#
# The `have_current_path` matcher (http://www.rubydoc.info/github/
# teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-
# instance_method) should be used on `page` to set expectations on
# Capybara's current path, since it uses Capybara's waiting
# functionality (https://github.com/teamcapybara/capybara/blob/master/
# README.md#asynchronous-javascript-ajax-and-friends) which ensures that
# preceding actions (like `click_link`) have completed.
#
# @example
# # bad
# expect(current_path).to eq('/callback')
# expect(page.current_path).to match(/widgets/)
#
# # good
# expect(page).to have_current_path("/callback")
# expect(page).to have_current_path(/widgets/)
#
class CurrentPathExpectation < Cop
MSG = 'Do not set an RSpec expectation on `current_path` in ' \
'Capybara feature specs - instead, use the ' \
'`have_current_path` matcher on `page`'.freeze
def_node_matcher :expectation_set_on_current_path, <<-PATTERN
(send nil? :expect (send {(send nil? :page) nil?} :current_path))
PATTERN
def on_send(node)
expectation_set_on_current_path(node) do
add_offense(node, location: :selector)
end
end
end
end
end
end
end

View File

@ -0,0 +1,118 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# Checks for consistent method usage in feature specs.
#
# By default, the cop disables all Capybara-specific methods that have
# the same native RSpec method (e.g. are just aliases). Some teams
# however may prefer using some of the Capybara methods (like `feature`)
# to make it obvious that the test uses Capybara, while still disable
# the rest of the methods, like `given` (alias for `let`), `background`
# (alias for `before`), etc. You can configure which of the methods to
# be enabled by using the EnabledMethods configuration option.
#
# @example
# # bad
# feature 'User logs in' do
# given(:user) { User.new }
#
# background do
# visit new_session_path
# end
#
# scenario 'with OAuth' do
# # ...
# end
# end
#
# # good
# describe 'User logs in' do
# let(:user) { User.new }
#
# before do
# visit new_session_path
# end
#
# it 'with OAuth' do
# # ...
# end
# end
class FeatureMethods < Cop
MSG = 'Use `%<replacement>s` instead of `%<method>s`.'.freeze
# https://git.io/v7Kwr
MAP = {
background: :before,
scenario: :it,
xscenario: :xit,
given: :let,
given!: :let!,
feature: :describe
}.freeze
def_node_matcher :spec?, <<-PATTERN
(block
(send {(const nil? :RSpec) nil?} {:describe :feature} ...)
...)
PATTERN
def_node_matcher :feature_method, <<-PATTERN
(block
$(send {(const nil? :RSpec) nil?} ${#{MAP.keys.map(&:inspect).join(' ')}} ...)
...)
PATTERN
def on_block(node)
return unless inside_spec?(node)
feature_method(node) do |send_node, match|
next if enabled?(match)
add_offense(
send_node,
location: :selector,
message: format(MSG, method: match, replacement: MAP[match])
)
end
end
def autocorrect(node)
lambda do |corrector|
corrector.replace(node.loc.selector, MAP[node.method_name].to_s)
end
end
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
def enabled_methods
cop_config
.fetch('EnabledMethods', [])
.map(&:to_sym)
end
end
end
end
end
end

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# `context` block descriptions should start with 'when', or 'with'.
#
# @see https://github.com/reachlocal/rspec-style-guide#context-descriptions
# @see http://www.betterspecs.org/#contexts
#
# @example `Prefixes` configuration option, defaults: 'when', 'with', and
# 'without'
# Prefixes:
# - when
# - with
# - without
# - if
#
# @example
# # bad
# context 'the display name not present' do
# # ...
# end
#
# # good
# context 'when the display name is not present' do
# # ...
# end
class ContextWording < Cop
MSG = 'Start context description with %<prefixes>s.'.freeze
def_node_matcher :context_wording, <<-PATTERN
(block (send _ { :context :shared_context } $(str #bad_prefix?)) ...)
PATTERN
def on_block(node)
context_wording(node) do |context|
add_offense(context, message: message)
end
end
private
def bad_prefix?(description)
!prefixes.include?(description.split.first)
end
def prefixes
cop_config['Prefixes'] || []
end
def message
format(MSG, prefixes: joined_prefixes)
end
def joined_prefixes
quoted = prefixes.map { |prefix| "'#{prefix}'" }
return quoted.first if quoted.size == 1
quoted << "or #{quoted.pop}"
quoted.join(', ')
end
end
end
end
end

View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
module RuboCop
module Cop # rubocop:disable Style/Documentation
WorkaroundCop = Cop.dup
# Clone of the the normal RuboCop::Cop::Cop class so we can rewrite
# the inherited method without breaking functionality
class WorkaroundCop
# Remove the Cop.inherited method to be a noop. Our RSpec::Cop
# class will invoke the inherited hook instead
class << self
undef inherited
def inherited(*) end
end
# Special case `Module#<` so that the rspec support rubocop exports
# is compatible with our subclass
def self.<(other)
other.equal?(RuboCop::Cop::Cop) || super
end
end
private_constant(:WorkaroundCop)
module RSpec
# @abstract parent class to rspec cops
#
# The criteria for whether rubocop-rspec analyzes a certain ruby file
# is configured via `AllCops/RSpec`. For example, if you want to
# customize your project to scan all files within a `test/` directory
# then you could add this to your configuration:
#
# @example configuring analyzed paths
#
# AllCops:
# RSpec:
# Patterns:
# - '_test.rb$'
# - '(?:^|/)test/'
class Cop < WorkaroundCop
include RuboCop::RSpec::Language
include RuboCop::RSpec::Language::NodePattern
DEFAULT_CONFIGURATION =
RuboCop::RSpec::CONFIG.fetch('AllCops').fetch('RSpec')
DEFAULT_PATTERN_RE = Regexp.union(
DEFAULT_CONFIGURATION.fetch('Patterns')
.map(&Regexp.public_method(:new))
)
# Invoke the original inherited hook so our cops are recognized
def self.inherited(subclass)
RuboCop::Cop::Cop.inherited(subclass)
end
def relevant_file?(file)
relevant_rubocop_rspec_file?(file) && super
end
private
def relevant_rubocop_rspec_file?(file)
rspec_pattern =~ file
end
def rspec_pattern
if rspec_pattern_config?
Regexp.union(rspec_pattern_config.map(&Regexp.public_method(:new)))
else
DEFAULT_PATTERN_RE
end
end
def all_cops_config
config
.for_all_cops
end
def rspec_pattern_config?
return unless all_cops_config.key?('RSpec')
all_cops_config.fetch('RSpec').key?('Patterns')
end
def rspec_pattern_config
all_cops_config
.fetch('RSpec', DEFAULT_CONFIGURATION)
.fetch('Patterns')
end
end
end
end
end

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Check that the first argument to the top level describe is a constant.
#
# @example
# # bad
# describe 'Do something' do
# end
#
# # good
# describe TestedClass do
# end
#
# describe "A feature example", type: :feature do
# end
class DescribeClass < Cop
include RuboCop::RSpec::TopLevelDescribe
MSG = 'The first argument to describe should be '\
'the class or module being tested.'.freeze
def_node_matcher :valid_describe?, <<-PATTERN
{
(send {(const nil? :RSpec) nil?} :describe const ...)
(send {(const nil? :RSpec) nil?} :describe)
}
PATTERN
def_node_matcher :describe_with_metadata, <<-PATTERN
(send {(const nil? :RSpec) nil?} :describe
!const
...
(hash $...))
PATTERN
def_node_matcher :rails_metadata?, <<-PATTERN
(pair
(sym :type)
(sym {:request :feature :system :routing :view}))
PATTERN
def_node_matcher :shared_group?, SharedGroups::ALL.block_pattern
def on_top_level_describe(node, args)
return if shared_group?(root_node)
return if valid_describe?(node)
describe_with_metadata(node) do |pairs|
return if pairs.any?(&method(:rails_metadata?))
end
add_offense(args.first, location: :expression)
end
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks that the second argument to `describe` specifies a method.
#
# @example
# # bad
# describe MyClass, 'do something' do
# end
#
# # good
# describe MyClass, '#my_instance_method' do
# end
#
# describe MyClass, '.my_class_method' do
# end
class DescribeMethod < Cop
include RuboCop::RSpec::TopLevelDescribe
include RuboCop::RSpec::Util
MSG = 'The second argument to describe should be the method '\
"being tested. '#instance' or '.class'.".freeze
def on_top_level_describe(_node, (_, second_arg))
return unless second_arg && second_arg.str_type?
return if second_arg.str_content.start_with?('#', '.')
add_offense(second_arg, location: :expression)
end
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Avoid describing symbols.
#
# @example
# # bad
# describe :my_method do
# # ...
# end
#
# # good
# describe '#my_method' do
# # ...
# end
#
# @see https://github.com/rspec/rspec-core/issues/1610
class DescribeSymbol < Cop
MSG = 'Avoid describing symbols.'.freeze
def_node_matcher :describe_symbol?, <<-PATTERN
(send {(const nil? :RSpec) nil?} :describe $sym ...)
PATTERN
def on_send(node)
describe_symbol?(node) do |match|
add_offense(match, location: :expression)
end
end
end
end
end
end

View File

@ -0,0 +1,124 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks that tests use `described_class`.
#
# If the first argument of describe is a class, the class is exposed to
# each example via described_class.
#
# This cop can be configured using the `EnforcedStyle` option
#
# @example `EnforcedStyle: described_class`
# # bad
# describe MyClass do
# subject { MyClass.do_something }
# end
#
# # good
# describe MyClass do
# subject { described_class.do_something }
# end
#
# @example `EnforcedStyle: explicit`
# # bad
# describe MyClass do
# subject { described_class.do_something }
# end
#
# # good
# describe MyClass do
# subject { MyClass.do_something }
# end
#
class DescribedClass < Cop
include RuboCop::RSpec::TopLevelDescribe
include ConfigurableEnforcedStyle
DESCRIBED_CLASS = 'described_class'.freeze
MSG = 'Use `%<replacement>s` instead of `%<src>s`.'.freeze
def_node_matcher :common_instance_exec_closure?, <<-PATTERN
(block (send (const nil? {:Class :Module}) :new ...) ...)
PATTERN
def_node_matcher :rspec_block?,
RuboCop::RSpec::Language::ALL.block_pattern
def_node_matcher :scope_changing_syntax?, '{def class module}'
def on_block(node)
# In case the explicit style is used, we needs to remember what's
# being described. Thus, we use an ivar for @described_class.
describe, @described_class, body = described_constant(node)
return if body.nil?
return unless top_level_describe?(describe)
find_usage(body) do |match|
add_offense(
match,
location: :expression,
message: message(match.const_name)
)
end
end
def autocorrect(node)
replacement = if style == :described_class
DESCRIBED_CLASS
else
@described_class.const_name
end
lambda do |corrector|
corrector.replace(node.loc.expression, replacement)
end
end
private
def find_usage(node, &block)
yield(node) if offensive?(node)
return if scope_change?(node) || node.const_type?
node.each_child_node do |child|
find_usage(child, &block)
end
end
def message(offense)
if style == :described_class
format(MSG, replacement: DESCRIBED_CLASS, src: offense)
else
format(MSG, replacement: @described_class.const_name,
src: DESCRIBED_CLASS)
end
end
def scope_change?(node)
scope_changing_syntax?(node) ||
common_instance_exec_closure?(node) ||
skippable_block?(node)
end
def skippable_block?(node)
node.block_type? && !rspec_block?(node) && skip_blocks?
end
def skip_blocks?
cop_config['SkipBlocks'].equal?(true)
end
def offensive?(node)
if style == :described_class
node.eql?(@described_class)
else
node.send_type? && node.method_name == :described_class
end
end
end
end
end
end

View File

@ -0,0 +1,90 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if an example group does not include any tests.
#
# This cop is configurable using the `CustomIncludeMethods` option
#
# @example usage
#
# # bad
# describe Bacon do
# let(:bacon) { Bacon.new(chunkiness) }
# let(:chunkiness) { false }
#
# context 'extra chunky' do # flagged by rubocop
# let(:chunkiness) { true }
# end
#
# it 'is chunky' do
# expect(bacon.chunky?).to be_truthy
# end
# end
#
# # good
# describe Bacon do
# let(:bacon) { Bacon.new(chunkiness) }
# let(:chunkiness) { false }
#
# it 'is chunky' do
# expect(bacon.chunky?).to be_truthy
# end
# end
#
# @example configuration
#
# # .rubocop.yml
# # RSpec/EmptyExampleGroup:
# # CustomIncludeMethods:
# # - include_tests
#
# # spec_helper.rb
# RSpec.configure do |config|
# config.alias_it_behaves_like_to(:include_tests)
# end
#
# # bacon_spec.rb
# describe Bacon do
# let(:bacon) { Bacon.new(chunkiness) }
# let(:chunkiness) { false }
#
# context 'extra chunky' do # not flagged by rubocop
# let(:chunkiness) { true }
#
# include_tests 'shared tests'
# end
# end
#
class EmptyExampleGroup < Cop
MSG = 'Empty example group detected.'.freeze
def_node_search :contains_example?, <<-PATTERN
{
#{(Examples::ALL + Includes::ALL).send_pattern}
(send _ #custom_include? ...)
}
PATTERN
def on_block(node)
return unless example_group?(node) && !contains_example?(node)
add_offense(node.send_node, location: :expression)
end
private
def custom_include?(method_name)
custom_include_methods.include?(method_name)
end
def custom_include_methods
cop_config
.fetch('CustomIncludeMethods', [])
.map(&:to_sym)
end
end
end
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if there is an empty line after example group blocks.
#
# @example
# # bad
# RSpec.describe Foo do
# describe '#bar' do
# end
# describe '#baz' do
# end
# end
#
# # good
# RSpec.describe Foo do
# describe '#bar' do
# end
#
# describe '#baz' do
# end
# end
#
class EmptyLineAfterExampleGroup < Cop
include RuboCop::RSpec::BlankLineSeparation
MSG = 'Add an empty line after `%<example_group>s`.'.freeze
def on_block(node)
return unless example_group?(node)
return if last_child?(node)
missing_separating_line(node) do |location|
add_offense(
node,
location: location,
message: format(MSG, example_group: node.method_name)
)
end
end
end
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if there is an empty line after the last let block.
#
# @example
# # bad
# let(:foo) { bar }
# let(:something) { other }
# it { does_something }
#
# # good
# let(:foo) { bar }
# let(:something) { other }
#
# it { does_something }
class EmptyLineAfterFinalLet < Cop
include RuboCop::RSpec::BlankLineSeparation
MSG = 'Add an empty line after the last `let` block.'.freeze
def on_block(node)
return unless example_group_with_body?(node)
latest_let = node.body.child_nodes.select { |child| let?(child) }.last
return if latest_let.nil?
return if last_child?(latest_let)
missing_separating_line(latest_let) do |location|
add_offense(latest_let, location: location)
end
end
end
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if there is an empty line after hook blocks.
#
# @example
# # bad
# before { do_something }
# it { does_something }
#
# # bad
# after { do_something }
# it { does_something }
#
# # bad
# around { |test| test.run }
# it { does_something }
#
# # good
# before { do_something }
#
# it { does_something }
#
# # good
# after { do_something }
#
# it { does_something }
#
# # good
# around { |test| test.run }
#
# it { does_something }
#
class EmptyLineAfterHook < Cop
include RuboCop::RSpec::BlankLineSeparation
MSG = 'Add an empty line after `%<hook>s`.'.freeze
def on_block(node)
return unless hook?(node)
return if last_child?(node)
missing_separating_line(node) do |location|
add_offense(
node,
location: location,
message: format(MSG, hook: node.method_name)
)
end
end
end
end
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if there is an empty line after subject block.
#
# @example
# # bad
# subject(:obj) { described_class }
# let(:foo) { bar }
#
# # good
# subject(:obj) { described_class }
#
# let(:foo) { bar }
class EmptyLineAfterSubject < Cop
include RuboCop::RSpec::BlankLineSeparation
MSG = 'Add empty line after `subject`.'.freeze
def on_block(node)
return unless subject?(node) && !in_spec_block?(node)
return if last_child?(node)
missing_separating_line(node) do |location|
add_offense(node, location: location, message: MSG)
end
end
private
def in_spec_block?(node)
node.each_ancestor(:block).any? do |ancestor|
Examples::ALL.include?(ancestor.method_name)
end
end
end
end
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for long examples.
#
# A long example is usually more difficult to understand. Consider
# extracting out some behaviour, e.g. with a `let` block, or a helper
# method.
#
# @example
# # bad
# it do
# service = described_class.new
# more_setup
# more_setup
# result = service.call
# expect(result).to be(true)
# end
#
# # good
# it do
# service = described_class.new
# result = service.call
# expect(result).to be(true)
# end
class ExampleLength < Cop
include CodeLength
MSG = 'Example has too many lines [%<total>d/%<max>d].'.freeze
def on_block(node)
return unless example?(node)
length = code_length(node)
return unless length > max_length
add_offense(node, location: :expression, message: message(length))
end
private
def code_length(node)
node.source.lines[1..-2].count { |line| !irrelevant_line(line) }
end
def message(length)
format(MSG, total: length, max: max_length)
end
end
end
end
end

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for examples without a description.
#
# RSpec allows for auto-generated example descriptions when there is no
# description provided or the description is an empty one.
#
# This cop removes empty descriptions.
# It also defines whether auto-generated description is allowed, based
# on the configured style.
#
# This cop can be configured using the `EnforcedStyle` option
#
# @example `EnforcedStyle: always_allow`
# # bad
# it('') { is_expected.to be_good }
# it '' do
# result = service.call
# expect(result).to be(true)
# end
#
# # good
# it { is_expected.to be_good }
# it do
# result = service.call
# expect(result).to be(true)
# end
#
# @example `EnforcedStyle: single_line_only`
# # bad
# it('') { is_expected.to be_good }
# it do
# result = service.call
# expect(result).to be(true)
# end
#
# # good
# it { is_expected.to be_good }
#
# @example `EnforcedStyle: disallow`
# # bad
# it { is_expected.to be_good }
# it do
# result = service.call
# expect(result).to be(true)
# end
class ExampleWithoutDescription < Cop
include ConfigurableEnforcedStyle
MSG_DEFAULT_ARGUMENT = 'Omit the argument when you want to ' \
'have auto-generated description.'.freeze
MSG_ADD_DESCRIPTION = 'Add a description.'.freeze
def_node_matcher :example_description, '(send nil? _ $(str $_))'
def on_block(node)
return unless example?(node)
check_example_without_description(node.send_node)
example_description(node.send_node) do |message_node, message|
return unless message.to_s.empty?
add_offense(message_node, message: MSG_DEFAULT_ARGUMENT)
end
end
private
def check_example_without_description(node)
return if node.arguments?
return unless disallow_empty_description?(node)
add_offense(node, message: MSG_ADD_DESCRIPTION)
end
def disallow_empty_description?(node)
style == :disallow ||
(style == :single_line_only && node.parent.multiline?)
end
end
end
end
end

View File

@ -0,0 +1,97 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for common mistakes in example descriptions.
#
# This cop will correct docstrings that begin with 'should' and 'it'.
#
# @see http://betterspecs.org/#should
#
# The autocorrect is experimental - use with care! It can be configured
# with CustomTransform (e.g. have => has) and IgnoredWords (e.g. only).
#
# @example
# # bad
# it 'should find nothing' do
# end
#
# # good
# it 'finds nothing' do
# end
#
# @example
# # bad
# it 'it does things' do
# end
#
# # good
# it 'does things' do
# end
class ExampleWording < Cop
MSG_SHOULD = 'Do not use should when describing your tests.'.freeze
MSG_IT = "Do not repeat 'it' when describing your tests.".freeze
SHOULD_PREFIX = /\Ashould(?:n't)?\b/i
IT_PREFIX = /\Ait /i
def_node_matcher(
:it_description,
'(block (send _ :it $(str $_) ...) ...)'
)
def on_block(node)
it_description(node) do |description_node, message|
if message =~ SHOULD_PREFIX
add_wording_offense(description_node, MSG_SHOULD)
elsif message =~ IT_PREFIX
add_wording_offense(description_node, MSG_IT)
end
end
end
def autocorrect(range)
->(corrector) { corrector.replace(range, replacement_text(range)) }
end
private
def add_wording_offense(node, message)
expr = node.loc.expression
docstring =
Parser::Source::Range.new(
expr.source_buffer,
expr.begin_pos + 1,
expr.end_pos - 1
)
add_offense(docstring, location: docstring, message: message)
end
def replacement_text(range)
text = range.source
if text =~ SHOULD_PREFIX
RuboCop::RSpec::Wording.new(
text,
ignore: ignored_words,
replace: custom_transform
).rewrite
else
text.sub(IT_PREFIX, '')
end
end
def custom_transform
cop_config.fetch('CustomTransform', {})
end
def ignored_words
cop_config.fetch('IgnoredWords', [])
end
end
end
end
end

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for `expect(...)` calls containing literal values.
#
# @example
# # bad
# expect(5).to eq(price)
# expect(/foo/).to eq(pattern)
# expect("John").to eq(name)
#
# # good
# expect(price).to eq(5)
# expect(pattern).to eq(/foo/)
# expect(name).to eq("John")
#
class ExpectActual < Cop
MSG = 'Provide the actual you are testing to `expect(...)`.'.freeze
SIMPLE_LITERALS = %i[
true
false
nil
int
float
str
sym
complex
rational
regopt
].freeze
COMPLEX_LITERALS = %i[
array
hash
pair
irange
erange
regexp
].freeze
def_node_matcher :expect_literal, '(send _ :expect $#literal?)'
def on_send(node)
expect_literal(node) do |argument|
add_offense(argument, location: :expression)
end
end
private
# This is not implement using a NodePattern because it seems
# to not be able to match against an explicit (nil) sexp
def literal?(node)
node && (simple_literal?(node) || complex_literal?(node))
end
def simple_literal?(node)
SIMPLE_LITERALS.include?(node.type)
end
def complex_literal?(node)
COMPLEX_LITERALS.include?(node.type) &&
node.each_child_node.all?(&method(:literal?))
end
end
end
end
end

View File

@ -0,0 +1,102 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for consistent style of change matcher.
#
# Enforces either passing object and attribute as arguments to the matcher
# or passing a block that reads the attribute value.
#
# This cop can be configured using the `EnforcedStyle` option.
#
# @example `EnforcedStyle: block`
# # bad
# expect(run).to change(Foo, :bar)
#
# # good
# expect(run).to change { Foo.bar }
#
# @example `EnforcedStyle: method_call`
# # bad
# expect(run).to change { Foo.bar }
# expect(run).to change { foo.baz }
#
# # good
# expect(run).to change(Foo, :bar)
# expect(run).to change(foo, :baz)
# # also good when there are arguments or chained method calls
# expect(run).to change { Foo.bar(:count) }
# expect(run).to change { user.reload.name }
#
class ExpectChange < Cop
include ConfigurableEnforcedStyle
MSG_BLOCK = 'Prefer `change(%<obj>s, :%<attr>s)`.'.freeze
MSG_CALL = 'Prefer `change { %<obj>s.%<attr>s }`.'.freeze
def_node_matcher :expect_change_with_arguments, <<-PATTERN
(send nil? :change ({const send} nil? $_) (sym $_))
PATTERN
def_node_matcher :expect_change_with_block, <<-PATTERN
(block
(send nil? :change)
(args)
(send ({const send} nil? $_) $_)
)
PATTERN
def on_send(node)
return unless style == :block
expect_change_with_arguments(node) do |receiver, message|
add_offense(
node,
message: format(MSG_CALL, obj: receiver, attr: message)
)
end
end
def on_block(node)
return unless style == :method_call
expect_change_with_block(node) do |receiver, message|
add_offense(
node,
message: format(MSG_BLOCK, obj: receiver, attr: message)
)
end
end
def autocorrect(node)
if style == :block
autocorrect_method_call_to_block(node)
else
autocorrect_block_to_method_call(node)
end
end
private
def autocorrect_method_call_to_block(node)
lambda do |corrector|
expect_change_with_arguments(node) do |receiver, message|
replacement = "change { #{receiver}.#{message} }"
corrector.replace(node.loc.expression, replacement)
end
end
end
def autocorrect_block_to_method_call(node)
lambda do |corrector|
expect_change_with_block(node) do |receiver, message|
replacement = "change(#{receiver}, :#{message})"
corrector.replace(node.loc.expression, replacement)
end
end
end
end
end
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Do not use `expect` in hooks such as `before`.
#
# @example
# # bad
# before do
# expect(something).to eq 'foo'
# end
#
# # bad
# after do
# expect_any_instance_of(Something).to receive(:foo)
# end
#
# # good
# it do
# expect(something).to eq 'foo'
# end
class ExpectInHook < Cop
MSG = 'Do not use `%<expect>s` in `%<hook>s` hook'.freeze
def_node_search :expectation, Expectations::ALL.send_pattern
def on_block(node)
return unless hook?(node)
return if node.body.nil?
expectation(node.body) do |expect|
add_offense(expect, location: :selector,
message: message(expect, node))
end
end
private
def message(expect, hook)
format(MSG, expect: expect.method_name, hook: hook.method_name)
end
end
end
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for opportunities to use `expect { ... }.to output`.
#
# @example
# # bad
# $stdout = StringIO.new
# my_app.print_report
# $stdout = STDOUT
# expect($stdout.string).to eq('Hello World')
#
# # good
# expect { my_app.print_report }.to output('Hello World').to_stdout
class ExpectOutput < Cop
MSG = 'Use `expect { ... }.to output(...).to_%<name>s` '\
'instead of mutating $%<name>s.'.freeze
def on_gvasgn(node)
return unless inside_example_scope?(node)
variable_name, _rhs = *node
name = variable_name[1..-1]
return unless name.eql?('stdout') || name.eql?('stderr')
add_offense(node, location: :name, message: format(MSG, name: name))
end
private
# Detect if we are inside the scope of a single example
#
# We want to encourage using `expect { ... }.to output` so
# we only care about situations where you would replace with
# an expectation. Therefore, assignments to stderr or stdout
# within a `before(:all)` or otherwise outside of an example
# don't matter.
def inside_example_scope?(node)
return false if node.nil? || example_group?(node)
return true if example?(node)
return RuboCop::RSpec::Hook.new(node).example? if hook?(node)
inside_example_scope?(node.parent)
end
end
end
end
end

View File

@ -0,0 +1,147 @@
# frozen_string_literal: true
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 < Cop
MSG = 'Use a block to declare attribute values.'.freeze
ATTRIBUTE_DEFINING_METHODS = %i[factory trait transient ignore].freeze
UNPROXIED_METHODS = %i[
__send__
__id__
nil?
send
object_id
extend
instance_eval
initialize
block_given?
raise
caller
method
].freeze
DEFINITION_PROXY_METHODS = %i[
add_attribute
after
association
before
callback
ignore
initialize_with
sequence
skip_create
to_create
].freeze
RESERVED_METHODS =
DEFINITION_PROXY_METHODS +
UNPROXIED_METHODS +
ATTRIBUTE_DEFINING_METHODS
def_node_matcher :value_matcher, <<-PATTERN
(send {self nil?} !#reserved_method? $...)
PATTERN
def_node_search :factory_attributes, <<-PATTERN
(block (send nil? #attribute_defining_method? ...) _ { (begin $...) $(send ...) } )
PATTERN
def on_block(node)
factory_attributes(node).to_a.flatten.each do |attribute|
next if proc?(attribute) || association?(attribute)
add_offense(attribute, location: :expression)
end
end
def autocorrect(node)
if node.parenthesized?
autocorrect_replacing_parens(node)
else
autocorrect_without_parens(node)
end
end
private
def proc?(attribute)
value_matcher(attribute).to_a.all?(&:block_pass_type?)
end
def association?(attribute)
argument = attribute.first_argument
argument.hash_type? && factory_key?(argument)
end
def factory_key?(hash_node)
hash_node.keys.any? { |key| key.sym_type? && key.value == :factory }
end
def autocorrect_replacing_parens(node)
left_braces, right_braces = braces(node)
lambda do |corrector|
corrector.replace(node.location.begin, ' ' + left_braces)
corrector.replace(node.location.end, right_braces)
end
end
def autocorrect_without_parens(node)
left_braces, right_braces = braces(node)
lambda do |corrector|
argument = node.first_argument
expression = argument.location.expression
corrector.insert_before(expression, left_braces)
corrector.insert_after(expression, right_braces)
end
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)
RESERVED_METHODS.include?(method_name)
end
def attribute_defining_method?(method_name)
ATTRIBUTE_DEFINING_METHODS.include?(method_name)
end
end
end
end
end
end

View File

@ -0,0 +1,149 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module FactoryBot
# Checks for create_list usage.
#
# This cop can be configured using the `EnforcedStyle` option
#
# @example `EnforcedStyle: create_list`
# # bad
# 3.times { create :user }
#
# # good
# create_list :user, 3
#
# # good
# 3.times { |n| create :user, created_at: n.months.ago }
#
# @example `EnforcedStyle: n_times`
# # bad
# create_list :user, 3
#
# # good
# 3.times { create :user }
class CreateList < Cop
include ConfigurableEnforcedStyle
MSG_CREATE_LIST = 'Prefer create_list.'.freeze
MSG_N_TIMES = 'Prefer %<number>s.times.'.freeze
def_node_matcher :n_times_block?, <<-PATTERN
(block
(send (int _) :times)
...
)
PATTERN
def_node_matcher :factory_call, <<-PATTERN
(send ${(const nil? {:FactoryGirl :FactoryBot}) nil?} :create (sym $_) $...)
PATTERN
def_node_matcher :factory_list_call, <<-PATTERN
(send ${(const nil? {:FactoryGirl :FactoryBot}) nil?} :create_list (sym $_) (int $_) $...)
PATTERN
def on_block(node)
return unless style == :create_list
return unless n_times_block?(node)
return unless contains_only_factory?(node.body)
add_offense(node.send_node,
location: :expression, message: MSG_CREATE_LIST)
end
def on_send(node)
return unless style == :n_times
factory_list_call(node) do |_receiver, _factory, count, _|
add_offense(
node,
location: :selector,
message: format(MSG_N_TIMES, number: count)
)
end
end
def autocorrect(node)
if style == :create_list
autocorrect_n_times_to_create_list(node)
else
autocorrect_create_list_to_n_times(node)
end
end
private
def contains_only_factory?(node)
if node.block_type?
factory_call(node.send_node)
else
factory_call(node)
end
end
def autocorrect_n_times_to_create_list(node)
block = node.parent
count = block.receiver.source
replacement = factory_call_replacement(block.body, count)
lambda do |corrector|
corrector.replace(block.loc.expression, replacement)
end
end
def autocorrect_create_list_to_n_times(node)
replacement = generate_n_times_block(node)
lambda do |corrector|
corrector.replace(node.loc.expression, replacement)
end
end
def generate_n_times_block(node)
receiver, factory, count, options = *factory_list_call(node)
arguments = ":#{factory}"
options = build_options_string(options)
arguments += ", #{options}" unless options.empty?
replacement = format_receiver(receiver)
replacement += format_method_call(node, 'create', arguments)
"#{count}.times { #{replacement} }"
end
def factory_call_replacement(body, count)
receiver, factory, options = *factory_call(body)
arguments = ":#{factory}, #{count}"
options = build_options_string(options)
arguments += ", #{options}" unless options.empty?
replacement = format_receiver(receiver)
replacement += format_method_call(body, 'create_list', arguments)
replacement
end
def build_options_string(options)
options.map(&:source).join(', ')
end
def format_method_call(node, method, arguments)
if node.parenthesized?
"#{method}(#{arguments})"
else
"#{method} #{arguments}"
end
end
def format_receiver(receiver)
return '' unless receiver
"#{receiver.source}."
end
end
end
end
end
end

View File

@ -0,0 +1,116 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks that spec file paths are consistent with the test subject.
#
# Checks the path of the spec file and enforces that it reflects the
# described class/module and its optionally called out method.
#
# With the configuration option `IgnoreMethods` the called out method will
# be ignored when determining the enforced path.
#
# With the configuration option `CustomTransform` modules or classes can
# be specified that should not as usual be transformed from CamelCase to
# snake_case (e.g. 'RuboCop' => 'rubocop' ).
#
# @example
# # bad
# whatever_spec.rb # describe MyClass
#
# # bad
# my_class_spec.rb # describe MyClass, '#method'
#
# # good
# my_class_spec.rb # describe MyClass
#
# # good
# my_class_method_spec.rb # describe MyClass, '#method'
#
# # good
# my_class/method_spec.rb # describe MyClass, '#method'
#
# @example when configuration is `IgnoreMethods: true`
# # bad
# whatever_spec.rb # describe MyClass
#
# # good
# my_class_spec.rb # describe MyClass
#
# # good
# my_class_spec.rb # describe MyClass, '#method'
#
class FilePath < Cop
include RuboCop::RSpec::TopLevelDescribe
MSG = 'Spec path should end with `%<suffix>s`.'.freeze
def_node_search :const_described?, '(send _ :describe (const ...) ...)'
def_node_search :routing_metadata?, '(pair (sym :type) (sym :routing))'
def on_top_level_describe(node, args)
return unless const_described?(node) && single_top_level_describe?
return if routing_spec?(args)
glob = glob_for(args)
return if filename_ends_with?(glob)
add_offense(
node,
location: :expression,
message: format(MSG, suffix: glob)
)
end
private
def routing_spec?(args)
args.any?(&method(:routing_metadata?))
end
def glob_for((described_class, method_name))
"#{expected_path(described_class)}#{name_glob(method_name)}*_spec.rb"
end
def name_glob(name)
return unless name && name.str_type?
"*#{name.str_content.gsub(/\W/, '')}" unless ignore_methods?
end
def expected_path(constant)
File.join(
constant.const_name.split('::').map do |name|
custom_transform.fetch(name) { camel_to_snake_case(name) }
end
)
end
def camel_to_snake_case(string)
string
.gsub(/([^A-Z])([A-Z]+)/, '\1_\2')
.gsub(/([A-Z])([A-Z][^A-Z\d]+)/, '\1_\2')
.downcase
end
def custom_transform
cop_config.fetch('CustomTransform', {})
end
def ignore_methods?
cop_config['IgnoreMethods']
end
def filename_ends_with?(glob)
File.fnmatch?("*#{glob}", processed_source.buffer.name)
end
def relevant_rubocop_rspec_file?(_file)
true
end
end
end
end
end

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if examples are focused.
#
# @example
# # bad
# describe MyClass, focus: true do
# end
#
# describe MyClass, :focus do
# end
#
# fdescribe MyClass do
# end
#
# # good
# describe MyClass do
# end
class Focus < Cop
MSG = 'Focused spec found.'.freeze
focusable =
ExampleGroups::GROUPS +
ExampleGroups::SKIPPED +
Examples::EXAMPLES +
Examples::SKIPPED
focused = ExampleGroups::FOCUSED + Examples::FOCUSED
FOCUSABLE_SELECTORS = focusable.node_pattern_union
FOCUS_SYMBOL = s(:sym, :focus)
FOCUS_TRUE = s(:pair, FOCUS_SYMBOL, s(:true))
def_node_matcher :metadata, <<-PATTERN
{(send nil? #{FOCUSABLE_SELECTORS} ... (hash $...))
(send nil? #{FOCUSABLE_SELECTORS} $...)}
PATTERN
def_node_matcher :focused_block?, focused.send_pattern
def on_send(node)
focus_metadata(node) do |focus|
add_offense(focus, location: :expression)
end
end
private
def focus_metadata(node, &block)
yield(node) if focused_block?(node)
metadata(node) do |matches|
matches.grep(FOCUS_SYMBOL, &block)
matches.grep(FOCUS_TRUE, &block)
end
end
end
end
end
end

View File

@ -0,0 +1,136 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks the arguments passed to `before`, `around`, and `after`.
#
# This cop checks for consistent style when specifying RSpec
# hooks which run for each example. There are three supported
# styles: "implicit", "each", and "example." All styles have
# the same behavior.
#
# @example when configuration is `EnforcedStyle: implicit`
# # bad
# before(:each) do
# # ...
# end
#
# # bad
# before(:example) do
# # ...
# end
#
# # good
# before do
# # ...
# end
#
# @example when configuration is `EnforcedStyle: each`
# # bad
# before(:example) do
# # ...
# end
#
# # good
# before do
# # ...
# end
#
# # good
# before(:each) do
# # ...
# end
#
# @example when configuration is `EnforcedStyle: example`
# # bad
# before(:each) do
# # ...
# end
#
# # bad
# before do
# # ...
# end
#
# # good
# before(:example) do
# # ...
# end
class HookArgument < Cop
include ConfigurableEnforcedStyle
include RangeHelp
IMPLICIT_MSG = 'Omit the default `%<scope>p` ' \
'argument for RSpec hooks.'.freeze
EXPLICIT_MSG = 'Use `%<scope>p` for RSpec hooks.'.freeze
HOOKS = Hooks::ALL.node_pattern_union.freeze
def_node_matcher :scoped_hook, <<-PATTERN
(block $(send _ #{HOOKS} (sym ${:each :example})) ...)
PATTERN
def_node_matcher :unscoped_hook, "(block $(send _ #{HOOKS}) ...)"
def on_block(node)
hook(node) do |method_send, scope_name|
return correct_style_detected if scope_name.equal?(style)
return check_implicit(method_send) unless scope_name
style_detected(scope_name)
add_offense(
method_send,
location: :expression,
message: explicit_message(scope_name)
)
end
end
def autocorrect(node)
scope = implicit_style? ? '' : "(#{style.inspect})"
lambda do |corrector|
corrector.replace(argument_range(node), scope)
end
end
private
def check_implicit(method_send)
style_detected(:implicit)
return if implicit_style?
add_offense(
method_send,
location: :selector,
message: format(EXPLICIT_MSG, scope: style)
)
end
def explicit_message(scope)
if implicit_style?
format(IMPLICIT_MSG, scope: scope)
else
format(EXPLICIT_MSG, scope: style)
end
end
def implicit_style?
style.equal?(:implicit)
end
def hook(node, &block)
scoped_hook(node, &block) || unscoped_hook(node, &block)
end
def argument_range(send_node)
range_between(
send_node.loc.selector.end_pos,
send_node.loc.expression.end_pos
)
end
end
end
end
end

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for before/around/after hooks that come after an example.
#
# @example
# # Bad
#
# it 'checks what foo does' do
# expect(foo).to be
# end
#
# before { prepare }
# after { clean_up }
#
# # Good
# before { prepare }
# after { clean_up }
#
# it 'checks what foo does' do
# expect(foo).to be
# end
#
class HooksBeforeExamples < Cop
include RangeHelp
include RuboCop::RSpec::FinalEndLocation
MSG = 'Move `%<hook>s` above the examples in the group.'.freeze
def_node_matcher :example_or_group?, <<-PATTERN
{
#{(Examples::ALL + ExampleGroups::ALL).block_pattern}
#{Includes::EXAMPLES.send_pattern}
}
PATTERN
def on_block(node)
return unless example_group_with_body?(node)
check_hooks(node.body) if multiline_block?(node.body)
end
def autocorrect(node)
lambda do |corrector|
first_example = find_first_example(node.parent)
first_example_pos = first_example.loc.expression
indent = "\n" + ' ' * first_example.loc.column
corrector.insert_before(first_example_pos, source(node) + indent)
corrector.remove(node_range_with_surrounding_space(node))
end
end
private
def multiline_block?(block)
block.begin_type?
end
def check_hooks(node)
first_example = find_first_example(node)
return unless first_example
node.each_child_node do |child|
next if child.sibling_index < first_example.sibling_index
next unless hook?(child)
add_offense(
child,
message: format(MSG, hook: child.method_name)
)
end
end
def find_first_example(node)
node.children.find { |sibling| example_or_group?(sibling) }
end
def node_range_with_surrounding_space(node)
range = node_range(node)
range_by_whole_lines(range, include_final_newline: true)
end
def source(node)
node_range(node).source
end
def node_range(node)
range_between(
node.loc.expression.begin_pos,
final_end_location(node).end_pos
)
end
end
end
end
end

View File

@ -0,0 +1,107 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Check that a consistent implicit expectation style is used.
#
# This cop can be configured using the `EnforcedStyle` option
# and supports the `--auto-gen-config` flag.
#
# @example `EnforcedStyle: is_expected`
#
# # bad
# it { should be_truthy }
#
# # good
# it { is_expected.to be_truthy }
#
# @example `EnforcedStyle: should`
#
# # bad
# it { is_expected.to be_truthy }
#
# # good
# it { should be_truthy }
#
class ImplicitExpect < Cop
include ConfigurableEnforcedStyle
MSG = 'Prefer `%<good>s` over `%<bad>s`.'.freeze
def_node_matcher :implicit_expect, <<-PATTERN
{
(send nil? ${:should :should_not} ...)
(send (send nil? $:is_expected) {:to :to_not :not_to} ...)
}
PATTERN
alternatives = {
'is_expected.to' => 'should',
'is_expected.not_to' => 'should_not',
'is_expected.to_not' => 'should_not'
}
ENFORCED_REPLACEMENTS = alternatives.merge(alternatives.invert).freeze
def on_send(node) # rubocop:disable Metrics/MethodLength
return unless (source_range = offending_expect(node))
expectation_source = source_range.source
if expectation_source.start_with?(style.to_s)
correct_style_detected
else
opposite_style_detected
add_offense(
node,
location: source_range,
message: offense_message(expectation_source)
)
end
end
def autocorrect(node)
lambda do |corrector|
offense = offending_expect(node)
replacement = replacement_source(offense.source)
corrector.replace(offense, replacement)
end
end
private
def offending_expect(node)
case implicit_expect(node)
when :is_expected
is_expected_range(node.loc)
when :should, :should_not
node.loc.selector
end
end
def is_expected_range(source_map) # rubocop:disable PredicateName
Parser::Source::Range.new(
source_map.expression.source_buffer,
source_map.expression.begin_pos,
source_map.selector.end_pos
)
end
def offense_message(offending_source)
format(
MSG,
good: replacement_source(offending_source),
bad: offending_source
)
end
def replacement_source(offending_source)
ENFORCED_REPLACEMENTS.fetch(offending_source)
end
end
end
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for usage of implicit subject (`is_expected` / `should`).
#
# This cop can be configured using the `EnforcedStyle` option
#
# @example `EnforcedStyle: single_line_only`
# # bad
# it do
# is_expected.to be_truthy
# end
#
# # good
# it { is_expected.to be_truthy }
# it do
# expect(subject).to be_truthy
# end
#
# @example `EnforcedStyle: disallow`
# # bad
# it { is_expected.to be_truthy }
#
# # good
# it { expect(subject).to be_truthy }
#
class ImplicitSubject < Cop
include ConfigurableEnforcedStyle
MSG = "Don't use implicit subject.".freeze
def_node_matcher :implicit_subject?, <<-PATTERN
(send nil? {:should :should_not :is_expected} ...)
PATTERN
def on_send(node)
return unless implicit_subject?(node)
return if valid_usage?(node)
add_offense(node)
end
def autocorrect(node)
replacement = 'expect(subject)'
if node.method_name == :should
replacement += '.to'
elsif node.method_name == :should_not
replacement += '.not_to'
end
->(corrector) { corrector.replace(node.loc.selector, replacement) }
end
private
def valid_usage?(node)
example = node.ancestors.find { |parent| example?(parent) }
return false if example.nil?
example.method_name == :its || allowed_by_style?(example)
end
def allowed_by_style?(example)
if style == :single_line_only
example.single_line?
elsif style == :single_statement_only
!example.body.begin_type?
else
false
end
end
end
end
end
end

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for `instance_double` used with `have_received`.
#
# @example
# # bad
# it do
# foo = instance_double(Foo).as_null_object
# expect(foo).to have_received(:bar)
# end
#
# # good
# it do
# foo = instance_spy(Foo)
# expect(foo).to have_received(:bar)
# end
#
class InstanceSpy < Cop
MSG = 'Use `instance_spy` when you check your double '\
'with `have_received`.'.freeze
def_node_search :null_double, <<-PATTERN
(lvasgn $_
(send
$(send nil? :instance_double
...) :as_null_object))
PATTERN
def_node_search :have_received_usage, <<-PATTERN
(send
(send nil? :expect
(lvar $_)) :to
(send nil? :have_received
...)
...)
PATTERN
def on_block(node)
return unless example?(node)
null_double(node) do |var, receiver|
have_received_usage(node) do |expected|
add_offense(receiver, location: :expression) if expected == var
end
end
end
def autocorrect(node)
lambda do |corrector|
replacement = 'instance_spy'
corrector.replace(node.loc.selector, replacement)
double_source_map = node.parent.loc
as_null_object_range = double_source_map
.dot
.join(double_source_map.selector)
corrector.remove(as_null_object_range)
end
end
end
end
end
end

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for instance variable usage in specs.
#
# This cop can be configured with the option `AssignmentOnly` which
# will configure the cop to only register offenses on instance
# variable usage if the instance variable is also assigned within
# the spec
#
# @example
# # bad
# describe MyClass do
# before { @foo = [] }
# it { expect(@foo).to be_empty }
# end
#
# # good
# describe MyClass do
# let(:foo) { [] }
# it { expect(foo).to be_empty }
# end
#
# @example with AssignmentOnly configuration
#
# # rubocop.yml
# # RSpec/InstanceVariable:
# # AssignmentOnly: false
#
# # bad
# describe MyClass do
# before { @foo = [] }
# it { expect(@foo).to be_empty }
# end
#
# # allowed
# describe MyClass do
# it { expect(@foo).to be_empty }
# end
#
# # good
# describe MyClass do
# let(:foo) { [] }
# it { expect(foo).to be_empty }
# end
#
class InstanceVariable < Cop
MSG = 'Replace instance variable with local variable or `let`.'.freeze
EXAMPLE_GROUP_METHODS = ExampleGroups::ALL + SharedGroups::ALL
def_node_matcher :spec_group?, EXAMPLE_GROUP_METHODS.block_pattern
def_node_matcher :dynamic_class?, <<-PATTERN
(block (send (const nil? :Class) :new ...) ...)
PATTERN
def_node_search :ivar_usage, '$(ivar $_)'
def_node_search :ivar_assigned?, '(ivasgn % ...)'
def on_block(node)
return unless spec_group?(node)
ivar_usage(node) do |ivar, name|
return if inside_dynamic_class?(ivar)
return if assignment_only? && !ivar_assigned?(node, name)
add_offense(ivar, location: :expression)
end
end
private
def inside_dynamic_class?(node)
node.each_ancestor(:block).any? { |block| dynamic_class?(block) }
end
def assignment_only?
cop_config['AssignmentOnly']
end
end
end
end
end

View File

@ -0,0 +1,42 @@
module RuboCop
module Cop
module RSpec
# Checks invalid usage for predicate matcher.
#
# Predicate matcher does not need a question.
# This cop checks an unnecessary question in predicate matcher.
#
# @example
#
# # bad
# expect(foo).to be_something?
#
# # good
# expect(foo).to be_something
class InvalidPredicateMatcher < Cop
MSG = 'Omit `?` from `%<matcher>s`.'.freeze
def_node_matcher :invalid_predicate_matcher?, <<-PATTERN
(send (send nil? :expect ...) {:to :not_to :to_not} $(send nil? #predicate?))
PATTERN
def on_send(node)
invalid_predicate_matcher?(node) do |predicate|
add_offense(predicate, location: :expression)
end
end
private
def predicate?(name)
name = name.to_s
name.start_with?('be_', 'have_') && name.end_with?('?')
end
def message(predicate)
format(MSG, matcher: predicate.method_name)
end
end
end
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks that only one `it_behaves_like` style is used.
#
# @example when configuration is `EnforcedStyle: it_behaves_like`
# # bad
# it_should_behave_like 'a foo'
#
# # good
# it_behaves_like 'a foo'
#
# @example when configuration is `EnforcedStyle: it_should_behave_like`
# # bad
# it_behaves_like 'a foo'
#
# # good
# it_should_behave_like 'a foo'
class ItBehavesLike < Cop
include ConfigurableEnforcedStyle
MSG = 'Prefer `%<replacement>s` over `%<original>s` when including '\
'examples in a nested context.'.freeze
def_node_matcher :example_inclusion_offense, '(send _ % ...)'
def on_send(node)
example_inclusion_offense(node, alternative_style) do
add_offense(node, location: :expression)
end
end
def autocorrect(node)
->(corrector) { corrector.replace(node.loc.selector, style.to_s) }
end
private
def message(_node)
format(MSG, replacement: style, original: alternative_style)
end
end
end
end
end

View File

@ -0,0 +1,52 @@
module RuboCop
module Cop
module RSpec
# Check that `all` matcher is used instead of iterating over an array.
#
# @example
# # bad
# it 'validates users' do
# [user1, user2, user3].each { |user| expect(user).to be_valid }
# end
#
# # good
# it 'validates users' do
# expect([user1, user2, user3]).to all(be_valid)
# end
class IteratedExpectation < Cop
MSG = 'Prefer using the `all` matcher instead ' \
'of iterating over an array.'.freeze
def_node_matcher :each?, <<-PATTERN
(block
(send ... :each)
(args (arg $_))
$(...)
)
PATTERN
def_node_matcher :expectation?, <<-PATTERN
(send (send nil? :expect (lvar %)) :to ...)
PATTERN
def on_block(node)
each?(node) do |arg, body|
if single_expectation?(body, arg) || only_expectations?(body, arg)
add_offense(node.send_node, location: :expression)
end
end
end
private
def single_expectation?(body, arg)
expectation?(body, arg)
end
def only_expectations?(body, arg)
body.each_child_node.all? { |child| expectation?(child, arg) }
end
end
end
end
end

View File

@ -0,0 +1,92 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Enforce that subject is the first definition in the test.
#
# @example
# # bad
# let(:params) { blah }
# subject { described_class.new(params) }
#
# before { do_something }
# subject { described_class.new(params) }
#
# it { expect_something }
# subject { described_class.new(params) }
# it { expect_something_else }
#
#
# # good
# subject { described_class.new(params) }
# let(:params) { blah }
#
# # good
# subject { described_class.new(params) }
# before { do_something }
#
# # good
# subject { described_class.new(params) }
# it { expect_something }
# it { expect_something_else }
#
class LeadingSubject < Cop
include RangeHelp
MSG = 'Declare `subject` above any other `%<offending>s` ' \
'declarations.'.freeze
def on_block(node)
return unless subject?(node) && !in_spec_block?(node)
check_previous_nodes(node)
end
def check_previous_nodes(node)
node.parent.each_child_node do |sibling|
if offending?(sibling)
add_offense(
node,
location: :expression,
message: format(MSG, offending: sibling.method_name)
)
end
break if offending?(sibling) || sibling.equal?(node)
end
end
def autocorrect(node)
lambda do |corrector|
first_node = find_first_offending_node(node)
first_node_position = first_node.loc.expression
indent = "\n" + ' ' * first_node.loc.column
corrector.insert_before(first_node_position, node.source + indent)
corrector.remove(node_range(node))
end
end
private
def offending?(node)
let?(node) || hook?(node) || example?(node)
end
def find_first_offending_node(node)
node.parent.children.find { |sibling| offending?(sibling) }
end
def node_range(node)
range_by_whole_lines(node.source_range, include_final_newline: true)
end
def in_spec_block?(node)
node.each_ancestor(:block).any? do |ancestor|
example?(ancestor)
end
end
end
end
end
end

View File

@ -0,0 +1,102 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for `let` definitions that come after an example.
#
# @example
# # Bad
# let(:foo) { bar }
#
# it 'checks what foo does' do
# expect(foo).to be
# end
#
# let(:some) { other }
#
# it 'checks what some does' do
# expect(some).to be
# end
#
# # Good
# let(:foo) { bar }
# let(:some) { other }
#
# it 'checks what foo does' do
# expect(foo).to be
# end
#
# it 'checks what some does' do
# expect(some).to be
# end
class LetBeforeExamples < Cop
include RangeHelp
include RuboCop::RSpec::FinalEndLocation
MSG = 'Move `let` before the examples in the group.'.freeze
def_node_matcher :example_or_group?, <<-PATTERN
{
#{(Examples::ALL + ExampleGroups::ALL).block_pattern}
#{Includes::EXAMPLES.send_pattern}
}
PATTERN
def on_block(node)
return unless example_group_with_body?(node)
check_let_declarations(node.body) if multiline_block?(node.body)
end
def autocorrect(node)
lambda do |corrector|
first_example = find_first_example(node.parent)
first_example_pos = first_example.loc.expression
indent = "\n" + ' ' * first_example.loc.column
corrector.insert_before(first_example_pos, source(node) + indent)
corrector.remove(node_range_with_surrounding_space(node))
end
end
private
def multiline_block?(block)
block.begin_type?
end
def check_let_declarations(node)
first_example = find_first_example(node)
return unless first_example
node.each_child_node do |child|
next if child.sibling_index < first_example.sibling_index
add_offense(child, location: :expression) if let?(child)
end
end
def find_first_example(node)
node.children.find { |sibling| example_or_group?(sibling) }
end
def node_range_with_surrounding_space(node)
range = node_range(node)
range_by_whole_lines(range, include_final_newline: true)
end
def source(node)
node_range(node).source
end
def node_range(node)
range_between(
node.loc.expression.begin_pos,
final_end_location(node).end_pos
)
end
end
end
end
end

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks unreferenced `let!` calls being used for test setup.
#
# @example
# # Bad
# let!(:my_widget) { create(:widget) }
#
# it 'counts widgets' do
# expect(Widget.count).to eq(1)
# end
#
# # Good
# it 'counts widgets' do
# create(:widget)
# expect(Widget.count).to eq(1)
# end
#
# # Good
# before { create(:widget) }
#
# it 'counts widgets' do
# expect(Widget.count).to eq(1)
# end
class LetSetup < Cop
include RuboCop::RSpec::TopLevelDescribe
MSG = 'Do not use `let!` for test setup.'.freeze
def_node_search :let_bang, <<-PATTERN
(block $(send nil? :let! (sym $_)) args ...)
PATTERN
def_node_search :method_called?, '(send nil? %)'
def on_block(node)
return unless example_group?(node)
unused_let_bang(node) do |let|
add_offense(let, location: :expression)
end
end
private
def unused_let_bang(node)
let_bang(node) do |method_send, method_name|
yield(method_send) unless method_called?(node, method_name)
end
end
end
end
end
end

View File

@ -0,0 +1,31 @@
module RuboCop
module Cop
module RSpec
# Check that chains of messages are not being stubbed.
#
# @example
# # bad
# allow(foo).to receive_message_chain(:bar, :baz).and_return(42)
#
# # better
# thing = Thing.new(baz: 42)
# allow(foo).to receive(bar: thing)
#
class MessageChain < Cop
MSG = 'Avoid stubbing using `%<method>s`.'.freeze
def_node_matcher :message_chain, <<-PATTERN
(send _ {:receive_message_chain :stub_chain} ...)
PATTERN
def on_send(node)
message_chain(node) { add_offense(node, location: :selector) }
end
def message(node)
format(MSG, method: node.method_name)
end
end
end
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for consistent message expectation style.
#
# This cop can be configured in your configuration using the
# `EnforcedStyle` option and supports `--auto-gen-config`.
#
# @example `EnforcedStyle: allow`
#
# # bad
# expect(foo).to receive(:bar)
#
# # good
# allow(foo).to receive(:bar)
#
# @example `EnforcedStyle: expect`
#
# # bad
# allow(foo).to receive(:bar)
#
# # good
# expect(foo).to receive(:bar)
#
class MessageExpectation < Cop
include ConfigurableEnforcedStyle
MSG = 'Prefer `%<style>s` for setting message expectations.'.freeze
SUPPORTED_STYLES = %w[allow expect].freeze
def_node_matcher :message_expectation, <<-PATTERN
(send $(send nil? {:expect :allow} ...) :to #receive_message?)
PATTERN
def_node_search :receive_message?, '(send nil? :receive ...)'
def on_send(node)
message_expectation(node) do |match|
return correct_style_detected if preferred_style?(match)
message = format(MSG, style: style)
add_offense(match, location: :selector, message: message) do
opposite_style_detected
end
end
end
private
def preferred_style?(expectation)
expectation.method_name.equal?(style)
end
end
end
end
end

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks that message expectations are set using spies.
#
# This cop can be configured in your configuration using the
# `EnforcedStyle` option and supports `--auto-gen-config`.
#
# @example `EnforcedStyle: have_received`
#
# # bad
# expect(foo).to receive(:bar)
#
# # good
# expect(foo).to have_received(:bar)
#
# @example `EnforcedStyle: receive`
#
# # bad
# expect(foo).to have_received(:bar)
#
# # good
# expect(foo).to receive(:bar)
#
class MessageSpies < Cop
include ConfigurableEnforcedStyle
MSG_RECEIVE = 'Prefer `receive` for setting message '\
'expectations.'.freeze
MSG_HAVE_RECEIVED = 'Prefer `have_received` for setting message '\
'expectations. Setup `%<source>s` as a spy using '\
'`allow` or `instance_spy`.'.freeze
SUPPORTED_STYLES = %w[have_received receive].freeze
def_node_matcher :message_expectation, %(
(send (send nil? :expect $_) {:to :to_not :not_to} ...)
)
def_node_search :receive_message, %(
$(send nil? {:receive :have_received} ...)
)
def on_send(node)
receive_message_matcher(node) do |receiver, message_matcher|
return correct_style_detected if preferred_style?(message_matcher)
add_offense(
message_matcher,
location: :selector,
message: error_message(receiver)
) { opposite_style_detected }
end
end
private
def receive_message_matcher(node)
return unless (receiver = message_expectation(node))
receive_message(node) { |match| yield(receiver, match) }
end
def preferred_style?(expectation)
expectation.method_name.equal?(style)
end
def error_message(receiver)
case style
when :receive
MSG_RECEIVE
when :have_received
format(MSG_HAVE_RECEIVED, source: receiver.source)
end
end
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks that the first argument to an example group is not empty.
#
# @example
# # bad
# describe do
# end
#
# RSpec.describe do
# end
#
# # good
# describe TestedClass do
# end
#
# describe "A feature example" do
# end
class MissingExampleGroupArgument < Cop
MSG = 'The first argument to `%<method>s` should not be empty.'.freeze
def on_block(node)
return unless example_group?(node)
return if node.send_node.arguments?
add_offense(node, location: :expression,
message: format(MSG, method: node.method_name))
end
end
end
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for multiple top level describes.
#
# Multiple descriptions for the same class or module should either
# be nested or separated into different test files.
#
# @example
# # bad
# describe MyClass, '.do_something' do
# end
# describe MyClass, '.do_something_else' do
# end
#
# # good
# describe MyClass do
# describe '.do_something' do
# end
# describe '.do_something_else' do
# end
# end
class MultipleDescribes < Cop
include RuboCop::RSpec::TopLevelDescribe
MSG = 'Do not use multiple top level describes - '\
'try to nest them.'.freeze
def on_top_level_describe(node, _args)
return if single_top_level_describe?
return unless top_level_nodes.first.equal?(node)
add_offense(node, location: :expression)
end
end
end
end
end

View File

@ -0,0 +1,120 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if examples contain too many `expect` calls.
#
# @see http://betterspecs.org/#single Single expectation test
#
# This cop is configurable using the `Max` option
# and works with `--auto-gen-config`.
#
# @example
#
# # bad
# describe UserCreator do
# it 'builds a user' do
# expect(user.name).to eq("John")
# expect(user.age).to eq(22)
# end
# end
#
# # good
# describe UserCreator do
# it 'sets the users name' do
# expect(user.name).to eq("John")
# end
#
# it 'sets the users age' do
# expect(user.age).to eq(22)
# end
# end
#
# @example configuration
#
# # .rubocop.yml
# # RSpec/MultipleExpectations:
# # Max: 2
#
# # not flagged by rubocop
# describe UserCreator do
# it 'builds a user' do
# expect(user.name).to eq("John")
# expect(user.age).to eq(22)
# end
# end
#
class MultipleExpectations < Cop
include ConfigurableMax
MSG = 'Example has too many expectations [%<total>d/%<max>d].'.freeze
def_node_search :with_aggregated_failures?, '(sym :aggregate_failures)'
def_node_search :disabled_aggregated_failures?, <<-PATTERN
(pair (sym :aggregate_failures) (false))
PATTERN
def_node_matcher :expect?, Expectations::ALL.send_pattern
def_node_matcher :aggregate_failures?, <<-PATTERN
(block (send _ :aggregate_failures ...) ...)
PATTERN
def on_block(node)
return unless example?(node)
return if example_with_aggregated_failures?(node)
expectations_count = to_enum(:find_expectation, node).count
return if expectations_count <= max_expectations
self.max = expectations_count
flag_example(node, expectation_count: expectations_count)
end
private
def example_with_aggregated_failures?(node)
example = node.send_node
(aggregated_failures_by_default? ||
with_aggregated_failures?(example)) &&
!disabled_aggregated_failures?(example)
end
def find_expectation(node, &block)
yield if expect?(node) || aggregate_failures?(node)
# do not search inside of aggregate_failures block
return if aggregate_failures?(node)
node.each_child_node do |child|
find_expectation(child, &block)
end
end
def flag_example(node, expectation_count:)
add_offense(
node.send_node,
location: :expression,
message: format(
MSG,
total: expectation_count,
max: max_expectations
)
)
end
def max_expectations
Integer(cop_config.fetch('Max', 1))
end
def aggregated_failures_by_default?
cop_config.fetch('AggregateFailuresByDefault', false)
end
end
end
end
end

View File

@ -0,0 +1,79 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if an example group defines `subject` multiple times.
#
# @example
#
# # bad
# describe Foo do
# subject(:user) { User.new }
# subject(:post) { Post.new }
# end
#
# # good
# describe Foo do
# let(:user) { User.new }
# subject(:post) { Post.new }
# end
#
# The autocorrect behavior for this cop depends on the type of
# duplication:
#
# - If multiple named subjects are defined then this probably indicates
# that the overwritten subjects (all subjects except the last
# definition) are effectively being used to define helpers. In this
# case they are replaced with `let`.
#
# - If multiple unnamed subjects are defined though then this can *only*
# be dead code and we remove the overwritten subject definitions.
#
# - If subjects are defined with `subject!` then we don't autocorrect.
# This is enough of an edge case that people can just move this to
# a `before` hook on their own
class MultipleSubjects < Cop
MSG = 'Do not set more than one subject per example group'.freeze
def on_block(node)
return unless example_group?(node)
subjects = RuboCop::RSpec::ExampleGroup.new(node).subjects
subjects[0...-1].each do |subject|
add_offense(subject, location: :expression)
end
end
def autocorrect(node)
return unless node.method_name.equal?(:subject) # Ignore `subject!`
if named_subject?(node)
rename_autocorrect(node)
else
remove_autocorrect(node)
end
end
private
def named_subject?(node)
node.send_node.arguments?
end
def rename_autocorrect(node)
lambda do |corrector|
corrector.replace(node.send_node.loc.selector, 'let')
end
end
def remove_autocorrect(node)
lambda do |corrector|
corrector.remove(node.loc.expression)
end
end
end
end
end
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for explicitly referenced test subjects.
#
# RSpec lets you declare an "implicit subject" using `subject { ... }`
# which allows for tests like `it { should be_valid }`. If you need to
# reference your test subject you should explicitly name it using
# `subject(:your_subject_name) { ... }`. Your test subjects should be
# the most important object in your tests so they deserve a descriptive
# name.
#
# @example
# # bad
# RSpec.describe User do
# subject { described_class.new }
#
# it 'is valid' do
# expect(subject.valid?).to be(true)
# end
# end
#
# # good
# RSpec.describe Foo do
# subject(:user) { described_class.new }
#
# it 'is valid' do
# expect(user.valid?).to be(true)
# end
# end
#
# # also good
# RSpec.describe Foo do
# subject(:user) { described_class.new }
#
# it { should be_valid }
# end
class NamedSubject < Cop
MSG = 'Name your test subject if you need '\
'to reference it explicitly.'.freeze
def_node_matcher :rspec_block?, <<-PATTERN
{
#{Examples::ALL.block_pattern}
#{Hooks::ALL.block_pattern}
}
PATTERN
def_node_search :subject_usage, '$(send nil? :subject)'
def on_block(node)
return unless rspec_block?(node)
subject_usage(node) do |subject_node|
add_offense(subject_node, location: :selector)
end
end
end
end
end
end

View File

@ -0,0 +1,145 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for nested example groups.
#
# This cop is configurable using the `Max` option
# and supports `--auto-gen-config
#
# @example
# # bad
# context 'when using some feature' do
# let(:some) { :various }
# let(:feature) { :setup }
#
# context 'when user is signed in' do # flagged by rubocop
# let(:user) do
# UserCreate.call(user_attributes)
# end
#
# let(:user_attributes) do
# {
# name: 'John',
# age: 22,
# role: role
# }
# end
#
# context 'when user is an admin' do # flagged by rubocop
# let(:role) { 'admin' }
#
# it 'blah blah'
# it 'yada yada'
# end
# end
# end
#
# # better
# context 'using some feature as an admin' do
# let(:some) { :various }
# let(:feature) { :setup }
#
# let(:user) do
# UserCreate.call(
# name: 'John',
# age: 22,
# role: 'admin'
# )
# end
#
# it 'blah blah'
# it 'yada yada'
# end
#
# @example configuration
#
# # .rubocop.yml
# # RSpec/NestedGroups:
# # Max: 2
#
# context 'when using some feature' do
# let(:some) { :various }
# let(:feature) { :setup }
#
# context 'when user is signed in' do
# let(:user) do
# UserCreate.call(user_attributes)
# end
#
# let(:user_attributes) do
# {
# name: 'John',
# age: 22,
# role: role
# }
# end
#
# context 'when user is an admin' do # flagged by rubocop
# let(:role) { 'admin' }
#
# it 'blah blah'
# it 'yada yada'
# end
# end
# end
#
class NestedGroups < Cop
include ConfigurableMax
include RuboCop::RSpec::TopLevelDescribe
MSG = 'Maximum example group nesting exceeded ' \
'[%<total>d/%<max>d].'.freeze
DEPRECATED_MAX_KEY = 'MaxNesting'.freeze
DEPRECATION_WARNING =
"Configuration key `#{DEPRECATED_MAX_KEY}` for #{cop_name} is " \
'deprecated in favor of `Max`. Please use that instead.'.freeze
def_node_search :find_contexts, ExampleGroups::ALL.block_pattern
def on_top_level_describe(node, _args)
find_nested_contexts(node.parent) do |context, nesting|
self.max = nesting
add_offense(
context.send_node,
location: :expression,
message: message(nesting)
)
end
end
private
def find_nested_contexts(node, nesting: 1, &block)
find_contexts(node) do |nested_context|
yield(nested_context, nesting) if nesting > max_nesting
nested_context.each_child_node do |child|
find_nested_contexts(child, nesting: nesting + 1, &block)
end
end
end
def message(nesting)
format(MSG, total: nesting, max: max_nesting)
end
def max_nesting
@max_nesting ||= Integer(max_nesting_config)
end
def max_nesting_config
if cop_config.key?(DEPRECATED_MAX_KEY)
warn DEPRECATION_WARNING
cop_config.fetch(DEPRECATED_MAX_KEY)
else
cop_config.fetch('Max', 3)
end
end
end
end
end
end

View File

@ -0,0 +1,41 @@
module RuboCop
module Cop
module RSpec
# Checks for consistent method usage for negating expectations.
#
# @example
# # bad
# it '...' do
# expect(false).to_not be_true
# end
#
# # good
# it '...' do
# expect(false).not_to be_true
# end
class NotToNot < Cop
include ConfigurableEnforcedStyle
MSG = 'Prefer `%<replacement>s` over `%<original>s`.'.freeze
def_node_matcher :not_to_not_offense, '(send _ % ...)'
def on_send(node)
not_to_not_offense(node, alternative_style) do
add_offense(node, location: :selector)
end
end
def autocorrect(node)
->(corrector) { corrector.replace(node.loc.selector, style.to_s) }
end
private
def message(_node)
format(MSG, replacement: style, original: alternative_style)
end
end
end
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if there is a let/subject that overwrites an existing one.
#
# @example
# # bad
# let(:foo) { bar }
# let(:foo) { baz }
#
# subject(:foo) { bar }
# let(:foo) { baz }
#
# let(:foo) { bar }
# let!(:foo) { baz }
#
# # good
# subject(:test) { something }
# let(:foo) { bar }
# let(:baz) { baz }
# let!(:other) { other }
class OverwritingSetup < Cop
MSG = '`%<name>s` is already defined.'.freeze
def_node_matcher :setup?, (Helpers::ALL + Subject::ALL).block_pattern
def_node_matcher :first_argument_name, '(send _ _ ({str sym} $_))'
def on_block(node)
return unless example_group_with_body?(node)
find_duplicates(node.body) do |duplicate, name|
add_offense(
duplicate,
location: :expression,
message: format(MSG, name: name)
)
end
end
private
def find_duplicates(node)
setup_expressions = Set.new
node.each_child_node(:block) do |child|
next unless common_setup?(child)
name = if child.send_node.arguments?
first_argument_name(child.send_node).to_sym
else
:subject
end
yield child, name unless setup_expressions.add?(name)
end
end
def common_setup?(node)
return false unless setup?(node)
# Search only for setup with basic_literal arguments (e.g. :sym, :str)
# or no arguments at all.
node.send_node.arguments.all?(&:basic_literal?)
end
end
end
end
end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for any pending or skipped examples.
#
# @example
# # bad
# describe MyClass do
# it "should be true"
# end
#
# describe MyClass do
# it "should be true" do
# pending
# end
# end
#
# describe MyClass do
# xit "should be true" do
# end
# end
#
# # good
# describe MyClass do
# end
class Pending < Cop
MSG = 'Pending spec found.'.freeze
PENDING_EXAMPLES = Examples::PENDING + Examples::SKIPPED \
+ ExampleGroups::SKIPPED
SKIPPABLE_EXAMPLES = ExampleGroups::GROUPS + Examples::EXAMPLES
SKIPPABLE_SELECTORS = SKIPPABLE_EXAMPLES.node_pattern_union
SKIP_SYMBOL = s(:sym, :skip)
PENDING_SYMBOL = s(:sym, :pending)
def_node_matcher :metadata, <<-PATTERN
{(send nil? #{SKIPPABLE_SELECTORS} ... (hash $...))
(send nil? #{SKIPPABLE_SELECTORS} $...)}
PATTERN
def_node_matcher :pending_block?, PENDING_EXAMPLES.send_pattern
def on_send(node)
return unless pending_block?(node) || skipped_from_metadata?(node)
add_offense(node, location: :expression)
end
private
def skipped_from_metadata?(node)
(metadata(node) || []).any? { |n| skip_node?(n) }
end
def skip_node?(node)
if node.respond_to?(:key)
skip_symbol?(node.key) && node.value.truthy_literal?
else
skip_symbol?(node)
end
end
def skip_symbol?(symbol_node)
[SKIP_SYMBOL, PENDING_SYMBOL].include?(symbol_node)
end
end
end
end
end

View File

@ -0,0 +1,350 @@
module RuboCop
module Cop
module RSpec
# A helper for `inflected` style
module InflectedHelper
extend NodePattern::Macros
MSG_INFLECTED = 'Prefer using `%<matcher_name>s` matcher over ' \
'`%<predicate_name>s`.'.freeze
private
def check_inflected(node)
predicate_in_actual?(node) do |predicate|
add_offense(
node,
location: :expression,
message: message_inflected(predicate)
)
end
end
def_node_matcher :predicate_in_actual?, <<-PATTERN
(send
(send nil? :expect {
(block $(send !nil? #predicate? ...) ...)
$(send !nil? #predicate? ...)})
${:to :not_to :to_not}
$#boolean_matcher?)
PATTERN
def_node_matcher :be_bool?, <<-PATTERN
(send nil? {:be :eq :eql :equal} {true false})
PATTERN
def_node_matcher :be_boolthy?, <<-PATTERN
(send nil? {:be_truthy :be_falsey :be_falsy :a_truthy_value :a_falsey_value :a_falsy_value})
PATTERN
def boolean_matcher?(node)
if cop_config['Strict']
be_boolthy?(node)
else
be_bool?(node) || be_boolthy?(node)
end
end
def predicate?(sym)
sym.to_s.end_with?('?')
end
def message_inflected(predicate)
format(MSG_INFLECTED,
predicate_name: predicate.method_name,
matcher_name: to_predicate_matcher(predicate.method_name))
end
# rubocop:disable Metrics/MethodLength
def to_predicate_matcher(name)
case name = name.to_s
when 'is_a?'
'be_a'
when 'instance_of?'
'be_an_instance_of'
when 'include?', 'respond_to?'
name[0..-2]
when /^has_/
name.sub('has_', 'have_')[0..-2]
else
"be_#{name[0..-2]}"
end
end
# rubocop:enable Metrics/MethodLength
def autocorrect_inflected(node)
predicate_in_actual?(node) do |predicate, to, matcher|
lambda do |corrector|
remove_predicate(corrector, predicate)
corrector.replace(node.loc.selector,
true?(to, matcher) ? 'to' : 'not_to')
rewrite_matcher(corrector, predicate, matcher)
end
end
end
def remove_predicate(corrector, predicate)
range = range_between(
predicate.loc.dot.begin_pos,
predicate.loc.expression.end_pos
)
corrector.remove(range)
block_range = block_loc(predicate)
corrector.remove(block_range) if block_range
end
def rewrite_matcher(corrector, predicate, matcher)
args = args_loc(predicate).source
block_loc = block_loc(predicate)
block = block_loc ? block_loc.source : ''
corrector.replace(
matcher.loc.expression,
to_predicate_matcher(predicate.method_name) + args + block
)
end
def true?(to_symbol, matcher)
result = case matcher.method_name
when :be, :eq
matcher.first_argument.true_type?
when :be_truthy, :a_truthy_value
true
when :be_falsey, :be_falsy, :a_falsey_value, :a_falsy_value
false
end
to_symbol == :to ? result : !result
end
end
# A helper for `explicit` style
# rubocop:disable Metrics/ModuleLength
module ExplicitHelper
extend NodePattern::Macros
MSG_EXPLICIT = 'Prefer using `%<predicate_name>s` over ' \
'`%<matcher_name>s` matcher.'.freeze
BUILT_IN_MATCHERS = %w[
be_truthy be_falsey be_falsy
have_attributes have_received
be_between be_within
].freeze
private
def check_explicit(node) # rubocop:disable Metrics/MethodLength
predicate_matcher_block?(node) do |_actual, matcher|
add_offense(
node,
location: :expression,
message: message_explicit(matcher)
)
ignore_node(node.children.first)
return
end
return if part_of_ignored_node?(node)
predicate_matcher?(node) do |_actual, matcher|
add_offense(
node,
location: :expression,
message: message_explicit(matcher)
)
end
end
def_node_matcher :predicate_matcher?, <<-PATTERN
(send
(send nil? :expect $!nil?)
{:to :not_to :to_not}
{$(send nil? #predicate_matcher_name? ...)
(block $(send nil? #predicate_matcher_name? ...) ...)})
PATTERN
def_node_matcher :predicate_matcher_block?, <<-PATTERN
(block
(send
(send nil? :expect $!nil?)
{:to :not_to :to_not}
$(send nil? #predicate_matcher_name?))
...)
PATTERN
def predicate_matcher_name?(name)
name = name.to_s
name.start_with?('be_', 'have_') &&
!BUILT_IN_MATCHERS.include?(name) &&
!name.end_with?('?')
end
def message_explicit(matcher)
format(MSG_EXPLICIT,
predicate_name: to_predicate_method(matcher.method_name),
matcher_name: matcher.method_name)
end
def autocorrect_explicit(node)
autocorrect_explicit_send(node) ||
autocorrect_explicit_block(node)
end
def autocorrect_explicit_send(node)
predicate_matcher?(node) do |actual, matcher|
corrector_explicit(node, actual, matcher, matcher)
end
end
def autocorrect_explicit_block(node)
predicate_matcher_block?(node) do |actual, matcher|
to_node = node.send_node
corrector_explicit(to_node, actual, matcher, to_node)
end
end
def corrector_explicit(to_node, actual, matcher, block_child)
lambda do |corrector|
replacement_matcher = replacement_matcher(to_node)
corrector.replace(matcher.loc.expression, replacement_matcher)
move_predicate(corrector, actual, matcher, block_child)
corrector.replace(to_node.loc.selector, 'to')
end
end
def move_predicate(corrector, actual, matcher, block_child)
predicate = to_predicate_method(matcher.method_name)
args = args_loc(matcher).source
block_loc = block_loc(block_child)
block = block_loc ? block_loc.source : ''
corrector.remove(block_loc) if block_loc
corrector.insert_after(actual.loc.expression,
".#{predicate}" + args + block)
end
# rubocop:disable Metrics/MethodLength
def to_predicate_method(matcher)
case matcher = matcher.to_s
when 'be_a', 'be_an', 'be_a_kind_of', 'a_kind_of', 'be_kind_of'
'is_a?'
when 'be_an_instance_of', 'be_instance_of', 'an_instance_of'
'instance_of?'
when 'include', 'respond_to'
matcher + '?'
when /^have_(.+)/
"has_#{Regexp.last_match(1)}?"
else
matcher[/^be_(.+)/, 1] + '?'
end
end
# rubocop:enable Metrics/MethodLength
def replacement_matcher(node)
case [cop_config['Strict'], node.method_name == :to]
when [true, true]
'be(true)'
when [true, false]
'be(false)'
when [false, true]
'be_truthy'
when [false, false]
'be_falsey'
end
end
end
# rubocop:enable Metrics/ModuleLength
# Prefer using predicate matcher over using predicate method directly.
#
# RSpec defines magic matchers for predicate methods.
# This cop recommends to use the predicate matcher instead of using
# predicate method directly.
#
# @example Strict: true, EnforcedStyle: inflected (default)
# # bad
# expect(foo.something?).to be_truthy
#
# # good
# expect(foo).to be_something
#
# # also good - It checks "true" strictly.
# expect(foo).to be(true)
#
# @example Strict: false, EnforcedStyle: inflected
# # bad
# expect(foo.something?).to be_truthy
# expect(foo).to be(true)
#
# # good
# expect(foo).to be_something
#
# @example Strict: true, EnforcedStyle: explicit
# # bad
# expect(foo).to be_something
#
# # good - the above code is rewritten to it by this cop
# expect(foo.something?).to be(true)
#
# @example Strict: false, EnforcedStyle: explicit
# # bad
# expect(foo).to be_something
#
# # good - the above code is rewritten to it by this cop
# expect(foo.something?).to be_truthy
class PredicateMatcher < Cop
include ConfigurableEnforcedStyle
include InflectedHelper
include ExplicitHelper
include RangeHelp
def on_send(node)
case style
when :inflected
check_inflected(node)
when :explicit
check_explicit(node)
end
end
def on_block(node)
check_explicit(node) if style == :explicit
end
def autocorrect(node)
case style
when :inflected
autocorrect_inflected(node)
when :explicit
autocorrect_explicit(node)
end
end
private
# returns args location with whitespace
# @example
# foo 1, 2
# ^^^^^
def args_loc(send_node)
range_between(send_node.loc.selector.end_pos,
send_node.loc.expression.end_pos)
end
# returns block location with whitespace
# @example
# foo { bar }
# ^^^^^^^^
def block_loc(send_node)
parent = send_node.parent
return unless parent.block_type?
range_between(
send_node.loc.expression.end_pos,
parent.loc.expression.end_pos
)
end
end
end
end
end

View File

@ -0,0 +1,147 @@
# frozen_string_literal: true
require 'rack/utils'
module RuboCop
module Cop
module RSpec
module Rails
# Enforces use of symbolic or numeric value to describe HTTP status.
#
# @example `EnforcedStyle: symbolic` (default)
# # bad
# it { is_expected.to have_http_status 200 }
# it { is_expected.to have_http_status 404 }
#
# # good
# it { is_expected.to have_http_status :ok }
# it { is_expected.to have_http_status :not_found }
# it { is_expected.to have_http_status :success }
# it { is_expected.to have_http_status :error }
#
# @example `EnforcedStyle: numeric`
# # bad
# it { is_expected.to have_http_status :ok }
# it { is_expected.to have_http_status :not_found }
#
# # good
# it { is_expected.to have_http_status 200 }
# it { is_expected.to have_http_status 404 }
# it { is_expected.to have_http_status :success }
# it { is_expected.to have_http_status :error }
#
class HttpStatus < Cop
include ConfigurableEnforcedStyle
def_node_matcher :http_status, <<-PATTERN
(send nil? :have_http_status ${int sym})
PATTERN
def on_send(node)
http_status(node) do |ast_node|
checker = checker_class.new(ast_node)
return unless checker.offensive?
add_offense(checker.node, message: checker.message)
end
end
def autocorrect(node)
lambda do |corrector|
checker = checker_class.new(node)
corrector.replace(node.loc.expression, checker.preferred_style)
end
end
private
def checker_class
case style
when :symbolic
SymbolicStyleChecker
when :numeric
NumericStyleChecker
end
end
# :nodoc:
class SymbolicStyleChecker
MSG = 'Prefer `%<prefer>s` over `%<current>s` ' \
'to describe HTTP status code.'.freeze
attr_reader :node
def initialize(node)
@node = node
end
def offensive?
!node.sym_type? && !custom_http_status_code?
end
def message
format(MSG, prefer: preferred_style, current: number.to_s)
end
def preferred_style
symbol.inspect
end
private
def symbol
::Rack::Utils::SYMBOL_TO_STATUS_CODE.key(number)
end
def number
node.source.to_i
end
def custom_http_status_code?
node.int_type? &&
!::Rack::Utils::SYMBOL_TO_STATUS_CODE.value?(node.source.to_i)
end
end
# :nodoc:
class NumericStyleChecker
MSG = 'Prefer `%<prefer>s` over `%<current>s` ' \
'to describe HTTP status code.'.freeze
WHITELIST_STATUS = %i[error success missing redirect].freeze
attr_reader :node
def initialize(node)
@node = node
end
def offensive?
!node.int_type? && !whitelisted_symbol?
end
def message
format(MSG, prefer: preferred_style, current: symbol.inspect)
end
def preferred_style
number.to_s
end
private
def number
::Rack::Utils::SYMBOL_TO_STATUS_CODE[symbol]
end
def symbol
node.value
end
def whitelisted_symbol?
node.sym_type? && WHITELIST_STATUS.include?(node.value)
end
end
end
end
end
end
end

View File

@ -0,0 +1,86 @@
module RuboCop
module Cop
module RSpec
# Check for `once` and `twice` receive counts matchers usage.
#
# @example
#
# # bad
# expect(foo).to receive(:bar).exactly(1).times
# expect(foo).to receive(:bar).exactly(2).times
# expect(foo).to receive(:bar).at_least(1).times
# expect(foo).to receive(:bar).at_least(2).times
# expect(foo).to receive(:bar).at_most(1).times
# expect(foo).to receive(:bar).at_most(2).times
#
# # good
# expect(foo).to receive(:bar).once
# expect(foo).to receive(:bar).twice
# expect(foo).to receive(:bar).at_least(:once)
# expect(foo).to receive(:bar).at_least(:twice)
# expect(foo).to receive(:bar).at_most(:once)
# expect(foo).to receive(:bar).at_most(:twice).times
#
class ReceiveCounts < Cop
include RangeHelp
MSG = 'Use `%<alternative>s` instead of `%<original>s`.'.freeze
def_node_matcher :receive_counts, <<-PATTERN
(send $(send _ {:exactly :at_least :at_most} (int {1 2})) :times)
PATTERN
def on_send(node)
receive_counts(node) do |offending_node|
offending_range = range(node, offending_node)
add_offense(
offending_node,
message: message_for(offending_node, offending_range.source),
location: offending_range
)
end
end
def autocorrect(node)
lambda do |corrector|
replacement = matcher_for(
node.method_name,
node.first_argument.source.to_i
)
corrector.replace(
range(node.parent, node),
replacement
)
end
end
private
def message_for(node, source)
alternative = matcher_for(
node.method_name,
node.first_argument.source.to_i
)
format(MSG, alternative: alternative, original: source)
end
def matcher_for(method, count)
matcher = count == 1 ? 'once' : 'twice'
if method == :exactly
".#{matcher}"
else
".#{method}(:#{matcher})"
end
end
def range(node, offending_node)
range_between(
offending_node.loc.dot.begin_pos,
node.loc.expression.end_pos
)
end
end
end
end
end

View File

@ -0,0 +1,43 @@
module RuboCop
module Cop
module RSpec
# Prefer `not_to receive(...)` over `receive(...).never`.
#
# @example
#
# # bad
# expect(foo).to receive(:bar).never
#
# # good
# expect(foo).not_to receive(:bar)
#
class ReceiveNever < Cop
include RangeHelp
MSG = 'Use `not_to receive` instead of `never`.'.freeze
def_node_search :method_on_stub?, '(send nil? :receive ...)'
def on_send(node)
return unless node.method_name == :never && method_on_stub?(node)
add_offense(
node,
location: :selector
)
end
def autocorrect(node)
lambda do |corrector|
corrector.replace(node.parent.loc.selector, 'not_to')
range = range_between(
node.loc.dot.begin_pos,
node.loc.selector.end_pos
)
corrector.remove(range)
end
end
end
end
end
end

View File

@ -0,0 +1,59 @@
module RuboCop
module Cop
module RSpec
# Check for repeated description strings in example groups.
#
# @example
#
# # bad
# RSpec.describe User do
# it 'is valid' do
# # ...
# end
#
# it 'is valid' do
# # ...
# end
# end
#
# # good
# RSpec.describe User do
# it 'is valid when first and last name are present' do
# # ...
# end
#
# it 'is valid when last name only is present' do
# # ...
# end
# end
#
class RepeatedDescription < Cop
MSG = "Don't repeat descriptions within an example group.".freeze
def on_block(node)
return unless example_group?(node)
repeated_descriptions(node).each do |repeated_description|
add_offense(repeated_description, location: :expression)
end
end
private
# Select examples in the current scope with repeated description strings
def repeated_descriptions(node)
grouped_examples =
RuboCop::RSpec::ExampleGroup.new(node)
.examples
.group_by(&:doc_string)
grouped_examples
.select { |description, group| description && group.size > 1 }
.values
.flatten
.map(&:definition)
end
end
end
end
end

View File

@ -0,0 +1,51 @@
module RuboCop
module Cop
module RSpec
# Check for repeated examples within example groups.
#
# @example
#
# it 'is valid' do
# expect(user).to be_valid
# end
#
# it 'validates the user' do
# expect(user).to be_valid
# end
#
class RepeatedExample < Cop
MSG = "Don't repeat examples within an example group.".freeze
def on_block(node)
return unless example_group?(node)
repeated_examples(node).each do |repeated_example|
add_offense(repeated_example, location: :expression)
end
end
private
def repeated_examples(node)
RuboCop::RSpec::ExampleGroup.new(node)
.examples
.group_by { |example| example_signature(example) }
.values
.reject(&:one?)
.flatten
.map(&:to_node)
end
def example_signature(example)
key_parts = [example.metadata, example.implementation]
if example.definition.method_name == :its
key_parts << example.definition.arguments
end
key_parts
end
end
end
end
end

View File

@ -0,0 +1,173 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for consistent style of stub's return setting.
#
# Enforces either `and_return` or block-style return in the cases
# where the returned value is constant. Ignores dynamic returned values
# are the result would be different
#
# This cop can be configured using the `EnforcedStyle` option
#
# @example `EnforcedStyle: block`
# # bad
# allow(Foo).to receive(:bar).and_return("baz")
# expect(Foo).to receive(:bar).and_return("baz")
#
# # good
# allow(Foo).to receive(:bar) { "baz" }
# expect(Foo).to receive(:bar) { "baz" }
# # also good as the returned value is dynamic
# allow(Foo).to receive(:bar).and_return(bar.baz)
#
# @example `EnforcedStyle: and_return`
# # bad
# allow(Foo).to receive(:bar) { "baz" }
# expect(Foo).to receive(:bar) { "baz" }
#
# # good
# allow(Foo).to receive(:bar).and_return("baz")
# expect(Foo).to receive(:bar).and_return("baz")
# # also good as the returned value is dynamic
# allow(Foo).to receive(:bar) { bar.baz }
#
class ReturnFromStub < Cop
include ConfigurableEnforcedStyle
MSG_AND_RETURN = 'Use `and_return` for static values.'.freeze
MSG_BLOCK = 'Use block for static values.'.freeze
def_node_search :contains_stub?, '(send nil? :receive (...))'
def_node_search :and_return_value, <<-PATTERN
$(send _ :and_return $(...))
PATTERN
def on_send(node)
return unless contains_stub?(node)
return unless style == :block
check_and_return_call(node)
end
def on_block(node)
return unless contains_stub?(node)
return unless style == :and_return
check_block_body(node)
end
def autocorrect(node)
if style == :block
AndReturnCallCorrector.new(node)
else
BlockBodyCorrector.new(node)
end
end
private
def check_and_return_call(node)
and_return_value(node) do |and_return, args|
unless dynamic?(args)
add_offense(
and_return,
location: :selector,
message: MSG_BLOCK
)
end
end
end
def check_block_body(block)
body = block.body
unless dynamic?(body) # rubocop:disable Style/GuardClause
add_offense(
block,
location: :begin,
message: MSG_AND_RETURN
)
end
end
def dynamic?(node)
node && !node.recursive_literal_or_const?
end
# :nodoc:
class AndReturnCallCorrector
def initialize(node)
@node = node
@receiver = node.receiver
@arg = node.first_argument
end
def call(corrector)
# Heredoc autocorrection is not yet implemented.
return if heredoc?
corrector.replace(range, " { #{replacement} }")
end
private
attr_reader :node, :receiver, :arg
def heredoc?
arg.loc.is_a?(Parser::Source::Map::Heredoc)
end
def range
Parser::Source::Range.new(
node.source_range.source_buffer,
receiver.source_range.end_pos,
node.source_range.end_pos
)
end
def replacement
if hash_without_braces?
"{ #{arg.source} }"
else
arg.source
end
end
def hash_without_braces?
arg.hash_type? && !arg.braces?
end
end
# :nodoc:
class BlockBodyCorrector
def initialize(block)
@block = block
@node = block.parent
@body = block.body || NULL_BLOCK_BODY
end
def call(corrector)
# Heredoc autocorrection is not yet implemented.
return if heredoc?
corrector.replace(
block.loc.expression,
"#{block.send_node.source}.and_return(#{body.source})"
)
end
private
attr_reader :node, :block, :body
def heredoc?
body.loc.is_a?(Parser::Source::Map::Heredoc)
end
NULL_BLOCK_BODY = Struct.new(:loc, :source).new(nil, 'nil')
end
end
end
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for let scattered across the example group.
#
# Group lets together
#
# @example
# # bad
# describe Foo do
# let(:foo) { 1 }
# subject { Foo }
# let(:bar) { 2 }
# before { prepare }
# let!(:baz) { 3 }
# end
#
# # good
# describe Foo do
# subject { Foo }
# before { prepare }
# let(:foo) { 1 }
# let(:bar) { 2 }
# let!(:baz) { 3 }
# end
#
class ScatteredLet < Cop
MSG = 'Group all let/let! blocks in the example group together.'.freeze
def on_block(node)
return unless example_group_with_body?(node)
check_let_declarations(node.body)
end
private
def check_let_declarations(body)
lets = body.each_child_node.select { |node| let?(node) }
first_let = lets.first
lets.each_with_index do |node, idx|
next if node.sibling_index == first_let.sibling_index + idx
add_offense(node, location: :expression)
end
end
end
end
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for setup scattered across multiple hooks in an example group.
#
# Unify `before`, `after`, and `around` hooks when possible.
#
# @example
# # bad
# describe Foo do
# before { setup1 }
# before { setup2 }
# end
#
# # good
# describe Foo do
# before do
# setup1
# setup2
# end
# end
#
class ScatteredSetup < Cop
MSG = 'Do not define multiple hooks in the same example group.'.freeze
def on_block(node)
return unless example_group?(node)
analyzable_hooks(node).each do |repeated_hook|
add_offense(repeated_hook, location: :expression)
end
end
def analyzable_hooks(node)
RuboCop::RSpec::ExampleGroup.new(node)
.hooks
.select { |hook| hook.knowable_scope? && hook.valid_scope? }
.group_by { |hook| [hook.name, hook.scope] }
.values
.reject(&:one?)
.flatten
.map(&:to_node)
end
end
end
end
end

View File

@ -0,0 +1,111 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for proper shared_context and shared_examples usage.
#
# If there are no examples defined, use shared_context.
# If there is no setup defined, use shared_examples.
#
# @example
# # bad
# RSpec.shared_context 'only examples here' do
# it 'does x' do
# end
#
# it 'does y' do
# end
# end
#
# # good
# RSpec.shared_examples 'only examples here' do
# it 'does x' do
# end
#
# it 'does y' do
# end
# end
#
# @example
# # bad
# RSpec.shared_examples 'only setup here' do
# subject(:foo) { :bar }
#
# let(:baz) { :bazz }
#
# before do
# something
# end
# end
#
# # good
# RSpec.shared_context 'only setup here' do
# subject(:foo) { :bar }
#
# let(:baz) { :bazz }
#
# before do
# something
# end
# end
#
class SharedContext < Cop
MSG_EXAMPLES = "Use `shared_examples` when you don't "\
'define context.'.freeze
MSG_CONTEXT = "Use `shared_context` when you don't "\
'define examples.'.freeze
examples = (Examples::ALL + Includes::EXAMPLES)
def_node_search :examples?, examples.send_pattern
context = (Hooks::ALL + Helpers::ALL + Includes::CONTEXT + Subject::ALL)
def_node_search :context?, context.send_pattern
def_node_matcher :shared_context, SharedGroups::CONTEXT.block_pattern
def_node_matcher :shared_example, SharedGroups::EXAMPLES.block_pattern
def on_block(node)
context_with_only_examples(node) do
add_shared_item_offense(node.send_node, MSG_EXAMPLES)
end
examples_with_only_context(node) do
add_shared_item_offense(node.send_node, MSG_CONTEXT)
end
end
def autocorrect(node)
lambda do |corrector|
context_with_only_examples(node.parent) do
corrector.replace(node.loc.selector, 'shared_examples')
end
examples_with_only_context(node.parent) do
corrector.replace(node.loc.selector, 'shared_context')
end
end
end
private
def context_with_only_examples(node)
shared_context(node) { yield if examples?(node) && !context?(node) }
end
def examples_with_only_context(node)
shared_example(node) { yield if context?(node) && !examples?(node) }
end
def add_shared_item_offense(node, message)
add_offense(
node,
location: :expression,
message: message
)
end
end
end
end
end

View File

@ -0,0 +1,76 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Enforces use of string to titleize shared examples.
#
# @example
# # bad
# it_behaves_like :foo_bar_baz
# it_should_behave_like :foo_bar_baz
# shared_examples :foo_bar_baz
# shared_examples_for :foo_bar_baz
# include_examples :foo_bar_baz
#
# # good
# it_behaves_like 'foo bar baz'
# it_should_behave_like 'foo bar baz'
# shared_examples 'foo bar baz'
# shared_examples_for 'foo bar baz'
# include_examples 'foo bar baz'
#
class SharedExamples < Cop
def_node_matcher :shared_examples, <<-PATTERN
(send
{(const nil? :RSpec) nil?}
{#{(SharedGroups::ALL + Includes::ALL).node_pattern}} $sym ...)
PATTERN
def on_send(node)
shared_examples(node) do |ast_node|
checker = Checker.new(ast_node)
add_offense(checker.node, message: checker.message)
end
end
def autocorrect(node)
lambda do |corrector|
checker = Checker.new(node)
corrector.replace(node.loc.expression, checker.preferred_style)
end
end
# :nodoc:
class Checker
MSG = 'Prefer %<prefer>s over `%<current>s` ' \
'to titleize shared examples.'.freeze
attr_reader :node
def initialize(node)
@node = node
end
def message
format(MSG, prefer: preferred_style, current: symbol.inspect)
end
def preferred_style
string = symbol.to_s.tr('_', ' ')
wrap_with_single_quotes(string)
end
private
def symbol
node.value
end
def wrap_with_single_quotes(string)
"'#{string}'"
end
end
end
end
end
end

View File

@ -0,0 +1,73 @@
module RuboCop
module Cop
module RSpec
# Checks that chains of messages contain more than one element.
#
# @example
# # bad
# allow(foo).to receive_message_chain(:bar).and_return(42)
#
# # good
# allow(foo).to receive(:bar).and_return(42)
#
# # also good
# allow(foo).to receive(:bar, :baz)
# allow(foo).to receive("bar.baz")
#
class SingleArgumentMessageChain < Cop
MSG = 'Use `%<recommended>s` instead of calling '\
'`%<called>s` with a single argument.'.freeze
def_node_matcher :message_chain, <<-PATTERN
(send _ {:receive_message_chain :stub_chain} $_)
PATTERN
def_node_matcher :single_key_hash?, '(hash pair)'
def on_send(node)
message_chain(node) do |arg|
return if arg.to_s.include?('.')
return if arg.hash_type? && !single_key_hash?(arg)
add_offense(node, location: :selector)
end
end
def autocorrect(node)
lambda do |corrector|
corrector.replace(node.loc.selector, replacement(node.method_name))
message_chain(node) do |arg|
autocorrect_hash_arg(corrector, arg) if single_key_hash?(arg)
end
end
end
private
def autocorrect_hash_arg(corrector, arg)
key, value = *arg.children.first
corrector.replace(arg.loc.expression, key_to_arg(key))
corrector.insert_after(arg.parent.loc.end,
".and_return(#{value.source})")
end
def key_to_arg(node)
key, = *node
node.sym_type? ? ":#{key}" : node.source
end
def replacement(method)
method.equal?(:receive_message_chain) ? 'receive' : 'stub'
end
def message(node)
method = node.method_name
format(MSG, recommended: replacement(method), called: method)
end
end
end
end
end

View File

@ -0,0 +1,147 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for stubbed test subjects.
#
# @see https://robots.thoughtbot.com/don-t-stub-the-system-under-test
#
# @example
# # bad
# describe Foo do
# subject(:bar) { baz }
#
# before do
# allow(bar).to receive(:qux?).and_return(true)
# end
# end
#
class SubjectStub < Cop
include RuboCop::RSpec::TopLevelDescribe
MSG = 'Do not stub your test subject.'.freeze
# @!method subject(node)
# Find a named or unnamed subject definition
#
# @example anonymous subject
# subject(parse('subject { foo }').ast) do |name|
# name # => :subject
# end
#
# @example named subject
# subject(parse('subject(:thing) { foo }').ast) do |name|
# name # => :thing
# end
#
# @param node [RuboCop::Node]
#
# @yield [Symbol] subject name
def_node_matcher :subject, <<-PATTERN
{
(block (send nil? :subject (sym $_)) args ...)
(block (send nil? $:subject) args ...)
}
PATTERN
# @!method message_expectation?(node, method_name)
# Match `allow` and `expect(...).to receive`
#
# @example source that matches
# allow(foo).to receive(:bar)
# allow(foo).to receive(:bar).with(1)
# allow(foo).to receive(:bar).with(1).and_return(2)
# expect(foo).to receive(:bar)
# expect(foo).to receive(:bar).with(1)
# expect(foo).to receive(:bar).with(1).and_return(2)
#
# @example source that not matches
# expect(foo).to all(receive(:bar))
#
def_node_matcher :message_expectation?, <<-PATTERN
{
(send nil? :allow (send nil? %))
(send (send nil? :expect (send nil? %)) :to #expectation?)
}
PATTERN
def_node_matcher :all_matcher?, '(send nil? :all ...)'
def_node_search :receive_message?, '(send nil? :receive ...)'
def expectation?(node)
return if all_matcher?(node)
receive_message?(node)
end
def on_block(node)
return unless example_group?(node)
find_subject_stub(node) do |stub|
add_offense(stub, location: :expression)
end
end
private
# Find subjects within tree and then find (send) nodes for that subject
#
# @param node [RuboCop::Node] example group
#
# @yield [RuboCop::Node] message expectations for subject
def find_subject_stub(node, &block)
find_subject(node) do |subject_name, context|
find_subject_expectation(context, subject_name, &block)
end
end
# Find a subject message expectation
#
# @param node [RuboCop::Node]
# @param subject_name [Symbol] name of subject
#
# @yield [RuboCop::Node] message expectation
def find_subject_expectation(node, subject_name, &block)
# Do not search node if it is an example group with its own subject.
return if example_group?(node) && redefines_subject?(node)
# Yield the current node if it is a message expectation.
yield(node) if message_expectation?(node, subject_name)
# Recurse through node's children looking for a message expectation.
node.each_child_node do |child|
find_subject_expectation(child, subject_name, &block)
end
end
# Check if node's children contain a subject definition
#
# @param node [RuboCop::Node]
#
# @return [Boolean]
def redefines_subject?(node)
node.each_child_node.any? do |child|
subject(child) || redefines_subject?(child)
end
end
# Find a subject definition
#
# @param node [RuboCop::Node]
# @param parent [RuboCop::Node,nil]
#
# @yieldparam subject_name [Symbol] name of subject being defined
# @yieldparam parent [RuboCop::Node] parent of subject definition
def find_subject(node, parent: nil, &block)
subject(node) { |name| yield(name, parent) }
node.each_child_node do |child|
find_subject(child, parent: node, &block)
end
end
end
end
end
end

View File

@ -0,0 +1,64 @@
module RuboCop
module Cop
module RSpec
# Checks for a specified error in checking raised errors.
#
# Enforces one of an Exception type, a string, or a regular
# expression to match against the exception message as a parameter
# to `raise_error`
#
# @example
#
# # bad
# expect {
# raise StandardError.new('error')
# }.to raise_error
#
# # good
# expect {
# raise StandardError.new('error')
# }.to raise_error(StandardError)
#
# expect {
# raise StandardError.new('error')
# }.to raise_error('error')
#
# expect {
# raise StandardError.new('error')
# }.to raise_error(/err/)
#
# expect { do_something }.not_to raise_error
class UnspecifiedException < Cop
MSG = 'Specify the exception being captured'.freeze
def_node_matcher :empty_raise_error_or_exception, <<-PATTERN.freeze
(send
(block
(send nil? :expect) ...)
:to
(send nil? {:raise_error :raise_exception})
)
PATTERN
def on_send(node)
return unless empty_exception_matcher?(node)
add_offense(
node.children.last,
location: :expression
)
end
def empty_exception_matcher?(node)
empty_raise_error_or_exception(node) && !block_with_args?(node.parent)
end
def block_with_args?(node)
return unless node && node.block_type?
node.arguments?
end
end
end
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Prefer using verifying doubles over normal doubles.
#
# @see https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles
#
# @example
# # bad
# let(:foo) do
# double(method_name: 'returned value')
# end
#
# # bad
# let(:foo) do
# double("ClassName", method_name: 'returned value')
# end
#
# # good
# let(:foo) do
# instance_double("ClassName", method_name: 'returned value')
# end
class VerifiedDoubles < Cop
MSG = 'Prefer using verifying doubles over normal doubles.'.freeze
def_node_matcher :unverified_double, <<-PATTERN
{(send nil? {:double :spy} $...)}
PATTERN
def on_send(node)
unverified_double(node) do |name, *_args|
return if name.nil? && cop_config['IgnoreNameless']
return if symbol?(name) && cop_config['IgnoreSymbolicNames']
add_offense(node, location: :expression)
end
end
private
def symbol?(name)
name && name.sym_type?
end
end
end
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# This cop checks void `expect()`.
#
# @example
# # bad
# expect(something)
#
# # good
# expect(something).to be(1)
class VoidExpect < Cop
MSG = 'Do not use `expect()` without `.to` or `.not_to`. ' \
'Chain the methods or remove it.'.freeze
def_node_matcher :expect?, <<-PATTERN
(send nil? :expect ...)
PATTERN
def_node_matcher :expect_block?, <<-PATTERN
(block #expect? (args) _body)
PATTERN
def on_send(node)
return unless expect?(node)
check_expect(node)
end
def on_block(node)
return unless expect_block?(node)
check_expect(node)
end
private
def check_expect(node)
return unless void?(node)
add_offense(node, location: :expression)
end
def void?(expect)
parent = expect.parent
return true unless parent
return true if parent.begin_type?
return true if parent.block_type? && parent.body == expect
end
end
end
end
end

View File

@ -0,0 +1,77 @@
require_relative 'rspec/capybara/current_path_expectation'
require_relative 'rspec/capybara/feature_methods'
require_relative 'rspec/factory_bot/attribute_defined_statically'
require_relative 'rspec/factory_bot/create_list'
begin
require_relative 'rspec/rails/http_status'
rescue LoadError # rubocop:disable Lint/HandleExceptions
# Rails/HttpStatus cannot be loaded if rack/utils is unavailable.
end
require_relative 'rspec/align_left_let_brace'
require_relative 'rspec/align_right_let_brace'
require_relative 'rspec/any_instance'
require_relative 'rspec/around_block'
require_relative 'rspec/be'
require_relative 'rspec/be_eql'
require_relative 'rspec/before_after_all'
require_relative 'rspec/context_wording'
require_relative 'rspec/describe_class'
require_relative 'rspec/describe_method'
require_relative 'rspec/describe_symbol'
require_relative 'rspec/described_class'
require_relative 'rspec/empty_example_group'
require_relative 'rspec/empty_line_after_example_group'
require_relative 'rspec/empty_line_after_final_let'
require_relative 'rspec/empty_line_after_hook'
require_relative 'rspec/empty_line_after_subject'
require_relative 'rspec/example_length'
require_relative 'rspec/example_without_description'
require_relative 'rspec/example_wording'
require_relative 'rspec/expect_actual'
require_relative 'rspec/expect_change'
require_relative 'rspec/expect_in_hook'
require_relative 'rspec/expect_output'
require_relative 'rspec/file_path'
require_relative 'rspec/focus'
require_relative 'rspec/hook_argument'
require_relative 'rspec/hooks_before_examples'
require_relative 'rspec/implicit_expect'
require_relative 'rspec/implicit_subject'
require_relative 'rspec/instance_spy'
require_relative 'rspec/instance_variable'
require_relative 'rspec/invalid_predicate_matcher'
require_relative 'rspec/it_behaves_like'
require_relative 'rspec/iterated_expectation'
require_relative 'rspec/leading_subject'
require_relative 'rspec/let_before_examples'
require_relative 'rspec/let_setup'
require_relative 'rspec/message_chain'
require_relative 'rspec/message_expectation'
require_relative 'rspec/message_spies'
require_relative 'rspec/missing_example_group_argument'
require_relative 'rspec/multiple_describes'
require_relative 'rspec/multiple_expectations'
require_relative 'rspec/multiple_subjects'
require_relative 'rspec/named_subject'
require_relative 'rspec/nested_groups'
require_relative 'rspec/not_to_not'
require_relative 'rspec/overwriting_setup'
require_relative 'rspec/pending'
require_relative 'rspec/predicate_matcher'
require_relative 'rspec/receive_counts'
require_relative 'rspec/receive_never'
require_relative 'rspec/repeated_description'
require_relative 'rspec/repeated_example'
require_relative 'rspec/return_from_stub'
require_relative 'rspec/scattered_let'
require_relative 'rspec/scattered_setup'
require_relative 'rspec/shared_context'
require_relative 'rspec/shared_examples'
require_relative 'rspec/single_argument_message_chain'
require_relative 'rspec/subject_stub'
require_relative 'rspec/unspecified_exception'
require_relative 'rspec/verified_doubles'
require_relative 'rspec/void_expect'

View File

@ -0,0 +1,10 @@
module RuboCop
# RuboCop RSpec project namespace
module RSpec
PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
CONFIG = YAML.safe_load(CONFIG_DEFAULT.read).freeze
private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
end
end

View File

@ -0,0 +1,62 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# Shared behavior for aligning braces for single line lets
class AlignLetBrace
include RuboCop::RSpec::Language::NodePattern
def initialize(root, token)
@root = root
@token = token
end
def offending_tokens
single_line_lets.reject do |let|
target_column_for(let) == let_token(let).column
end
end
def indent_for(node)
' ' * (target_column_for(node) - let_token(node).column)
end
private
def let_token(node)
node.loc.public_send(token)
end
def target_column_for(let)
let_group_for(let).map { |member| let_token(member).column }.max
end
def let_group_for(let)
adjacent_let_chunks.detect do |chunk|
chunk.any? do |member|
member == let && member.loc.line == let.loc.line
end
end
end
def adjacent_let_chunks
last_line = nil
single_line_lets.chunk do |node|
line = node.loc.line
last_line = (line if last_line.nil? || last_line + 1 == line)
last_line.nil?
end.map(&:last)
end
def single_line_lets
@single_line_lets ||=
root.each_node(:block).select do |node|
let?(node) && node.single_line?
end
end
attr_reader :root, :token
end
end
end

View File

@ -0,0 +1,43 @@
module RuboCop
module RSpec
# Helps determine the offending location if there is not a blank line
# following the node. Allows comments to follow directly after.
module BlankLineSeparation
include FinalEndLocation
include RuboCop::Cop::RangeHelp
def missing_separating_line(node)
line = final_end_location(node).line
line += 1 while comment_line?(processed_source[line])
return if processed_source[line].blank?
yield offending_loc(line)
end
def offending_loc(last_line)
offending_line = processed_source[last_line - 1]
content_length = offending_line.lstrip.length
start = offending_line.length - content_length
source_range(processed_source.buffer, last_line, start, content_length)
end
def last_child?(node)
return true unless node.parent && node.parent.begin_type?
node.equal?(node.parent.children.last)
end
def autocorrect(node)
lambda do |corrector|
missing_separating_line(node) do |location|
corrector.insert_after(location.end, "\n")
end
end
end
end
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# Wrapper for RSpec DSL methods
class Concept
include Language
include Language::NodePattern
extend NodePattern::Macros
def initialize(node)
@node = node
end
def eql?(other)
node == other.node
end
alias == eql?
def hash
[self.class, node].hash
end
def to_node
node
end
protected
attr_reader :node
end
end
end

View File

@ -0,0 +1,36 @@
require 'yaml'
module RuboCop
module RSpec
# Builds a YAML config file from two config hashes
class ConfigFormatter
NAMESPACES = /^(RSpec|Capybara|FactoryBot|Rails)/
STYLE_GUIDE_BASE_URL = 'http://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/'.freeze
def initialize(config, descriptions)
@config = config
@descriptions = descriptions
end
def dump
YAML.dump(unified_config).gsub(NAMESPACES, "\n\\1")
end
private
def unified_config
cops.each_with_object(config.dup) do |cop, unified|
unified[cop] = config.fetch(cop)
.merge(descriptions.fetch(cop))
.merge('StyleGuide' => STYLE_GUIDE_BASE_URL + cop.sub('RSpec/', ''))
end
end
def cops
(descriptions.keys | config.keys).grep(NAMESPACES)
end
attr_reader :config, :descriptions
end
end
end

View File

@ -0,0 +1,84 @@
module RuboCop
module RSpec
# Extracts cop descriptions from YARD docstrings
class DescriptionExtractor
def initialize(yardocs)
@code_objects = yardocs.map(&CodeObject.public_method(:new))
end
def to_h
code_objects
.select(&:rspec_cop?)
.map(&:configuration)
.reduce(:merge)
end
private
attr_reader :code_objects
# Decorator of a YARD code object for working with documented rspec cops
class CodeObject
COP_CLASS_NAMES = %w[RuboCop::Cop RuboCop::Cop::RSpec::Cop].freeze
RSPEC_NAMESPACE = 'RuboCop::Cop::RSpec'.freeze
def initialize(yardoc)
@yardoc = yardoc
end
# Test if the YARD code object documents a concrete rspec cop class
#
# @return [Boolean]
def rspec_cop?
class_documentation? &&
rspec_cop_namespace? &&
cop_subclass? &&
!abstract?
end
# Configuration for the documented cop that would live in default.yml
#
# @return [Hash]
def configuration
{ cop_name => { 'Description' => description } }
end
private
def cop_name
Object.const_get(documented_constant).cop_name
end
def description
yardoc.docstring.split("\n\n").first.to_s
end
def class_documentation?
yardoc.type.equal?(:class)
end
def rspec_cop_namespace?
documented_constant.start_with?(RSPEC_NAMESPACE)
end
def documented_constant
yardoc.to_s
end
def cop_subclass?
# YARD superclass resolution is a bit flaky: All classes loaded before
# RuboCop::Cop::WorkaroundCop are shown as having RuboCop::Cop as
# superclass, while all the following classes are listed as having
# RuboCop::Cop::RSpec::Cop as their superclass.
COP_CLASS_NAMES.include?(yardoc.superclass.path)
end
def abstract?
yardoc.tags.any? { |tag| tag.tag_name.eql?('abstract') }
end
attr_reader :yardoc
end
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# Wrapper for RSpec examples
class Example < Concept
def_node_matcher :extract_doc_string, '(send _ _ $str ...)'
def_node_matcher :extract_metadata, '(send _ _ _ $...)'
def_node_matcher :extract_implementation, '(block send args $_)'
def doc_string
extract_doc_string(definition)
end
def metadata
extract_metadata(definition)
end
def implementation
extract_implementation(node)
end
def definition
if node.send_type?
node
else
node.send_node
end
end
end
end
end

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# Wrapper for RSpec example groups
class ExampleGroup < Concept
# @!method scope_change?(node)
#
# Detect if the node is an example group or shared example
#
# Selectors which indicate that we should stop searching
#
def_node_matcher :scope_change?, (
ExampleGroups::ALL + SharedGroups::ALL + Includes::ALL
).block_pattern
def subjects
subjects_in_scope(node)
end
def examples
examples_in_scope(node).map(&Example.public_method(:new))
end
def hooks
hooks_in_scope(node).map(&Hook.public_method(:new))
end
private
def subjects_in_scope(node)
node.each_child_node.flat_map do |child|
find_subjects(child)
end
end
def find_subjects(node)
return [] if scope_change?(node)
if subject?(node)
[node]
else
subjects_in_scope(node)
end
end
def hooks_in_scope(node)
node.each_child_node.flat_map do |child|
find_hooks(child)
end
end
def find_hooks(node)
return [] if scope_change?(node) || example?(node)
if hook?(node)
[node]
else
hooks_in_scope(node)
end
end
def examples_in_scope(node, &blk)
node.each_child_node.flat_map do |child|
find_examples(child, &blk)
end
end
# Recursively search for examples within the current scope
#
# Searches node for examples and halts when a scope change is detected
#
# @param node [RuboCop::Node] node to recursively search for examples
#
# @return [Array<RuboCop::Node>] discovered example nodes
def find_examples(node)
return [] if scope_change?(node)
if example?(node)
[node]
else
examples_in_scope(node)
end
end
end
end
end

View File

@ -0,0 +1,15 @@
module RuboCop
module RSpec
# Helps find the true end location of nodes which might contain heredocs.
module FinalEndLocation
def final_end_location(start_node)
heredoc_endings =
start_node.each_node(:str, :dstr, :xstr)
.select(&:heredoc?)
.map { |node| node.loc.heredoc_end }
[start_node.loc.end, *heredoc_endings].max_by(&:line)
end
end
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# Wrapper for RSpec hook
class Hook < Concept
STANDARDIZED_SCOPES = %i[each context suite].freeze
private_constant(:STANDARDIZED_SCOPES)
def name
node.method_name
end
def knowable_scope?
return true unless scope_argument
scope_argument.sym_type?
end
def valid_scope?
STANDARDIZED_SCOPES.include?(scope)
end
def example?
scope.equal?(:each)
end
def scope
case scope_name
when nil, :each, :example then :each
when :context, :all then :context
when :suite then :suite
else
scope_name
end
end
private
def scope_name
scope_argument.to_a.first
end
def scope_argument
node.send_node.first_argument
end
end
end
end

View File

@ -0,0 +1,16 @@
module RuboCop
module RSpec
# Because RuboCop doesn't yet support plugins, we have to monkey patch in a
# bit of our configuration.
module Inject
def self.defaults!
path = CONFIG_DEFAULT.to_s
hash = ConfigLoader.send(:load_yaml_configuration, path)
config = Config.new(hash, path)
puts "configuration from #{path}" if ConfigLoader.debug?
config = ConfigLoader.merge_with_default(config, path)
ConfigLoader.instance_variable_set(:@default_configuration, config)
end
end
end
end

View File

@ -0,0 +1,119 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# RSpec public API methods that are commonly used in cops
module Language
# Set of method selectors
class SelectorSet
def initialize(selectors)
@selectors = selectors
end
def ==(other)
selectors.eql?(other.selectors)
end
def +(other)
self.class.new(selectors + other.selectors)
end
def include?(selector)
selectors.include?(selector)
end
def block_pattern
"(block #{send_pattern} ...)"
end
def send_pattern
"(send {(const nil? :RSpec) nil?} #{node_pattern_union} ...)"
end
def node_pattern_union
"{#{node_pattern}}"
end
def node_pattern
selectors.map(&:inspect).join(' ')
end
protected
attr_reader :selectors
end
module ExampleGroups
GROUPS = SelectorSet.new(%i[describe context feature example_group])
SKIPPED = SelectorSet.new(%i[xdescribe xcontext xfeature])
FOCUSED = SelectorSet.new(%i[fdescribe fcontext ffeature])
ALL = GROUPS + SKIPPED + FOCUSED
end
module SharedGroups
EXAMPLES = SelectorSet.new(%i[shared_examples shared_examples_for])
CONTEXT = SelectorSet.new(%i[shared_context])
ALL = EXAMPLES + CONTEXT
end
module Includes
EXAMPLES = SelectorSet.new(
%i[
it_behaves_like
it_should_behave_like
include_examples
]
)
CONTEXT = SelectorSet.new(%i[include_context])
ALL = EXAMPLES + CONTEXT
end
module Examples
EXAMPLES = SelectorSet.new(%i[it specify example scenario its])
FOCUSED = SelectorSet.new(%i[fit fspecify fexample fscenario focus])
SKIPPED = SelectorSet.new(%i[xit xspecify xexample xscenario skip])
PENDING = SelectorSet.new(%i[pending])
ALL = EXAMPLES + FOCUSED + SKIPPED + PENDING
end
module Hooks
ALL = SelectorSet.new(
%i[
prepend_before
before
append_before
around
prepend_after
after
append_after
]
)
end
module Helpers
ALL = SelectorSet.new(%i[let let!])
end
module Subject
ALL = SelectorSet.new(%i[subject subject!])
end
module Expectations
ALL = SelectorSet.new(%i[expect is_expected expect_any_instance_of])
end
ALL =
ExampleGroups::ALL +
SharedGroups::ALL +
Examples::ALL +
Hooks::ALL +
Helpers::ALL +
Subject::ALL +
Expectations::ALL
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
module RuboCop
module RSpec
module Language
# Common node matchers used for matching against the rspec DSL
module NodePattern
extend RuboCop::NodePattern::Macros
def_node_matcher :example_group?, ExampleGroups::ALL.block_pattern
def_node_matcher :example_group_with_body?, <<-PATTERN
(block #{ExampleGroups::ALL.send_pattern} args [!nil?])
PATTERN
def_node_matcher :example?, Examples::ALL.block_pattern
def_node_matcher :hook?, Hooks::ALL.block_pattern
def_node_matcher :let?, Helpers::ALL.block_pattern
def_node_matcher :subject?, Subject::ALL.block_pattern
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# RuboCop-RSpec specific extensions of RuboCop::AST::Node
module Node
# In various cops we want to regard const as literal althought it's not
# strictly literal.
def recursive_literal_or_const?
case type
when :begin, :pair, *AST::Node::COMPOSITE_LITERALS
children.all?(&:recursive_literal_or_const?)
else
literal? || const_type?
end
end
end
end
end

View File

@ -0,0 +1,54 @@
module RuboCop
module RSpec
# Helper methods for top level describe cops
module TopLevelDescribe
extend NodePattern::Macros
def_node_matcher :described_constant, <<-PATTERN
(block $(send _ :describe $(const ...)) (args) $_)
PATTERN
def on_send(node)
return unless respond_to?(:on_top_level_describe)
return unless top_level_describe?(node)
on_top_level_describe(node, node.arguments)
end
private
def top_level_describe?(node)
return false unless node.method_name == :describe
top_level_nodes.include?(node)
end
def top_level_nodes
nodes = describe_statement_children(root_node)
# If we have no top level describe statements, we need to check any
# blocks on the top level (e.g. after a require).
if nodes.empty?
nodes = root_node.each_child_node(:block).flat_map do |child|
describe_statement_children(child)
end
end
nodes
end
def root_node
processed_source.ast
end
def single_top_level_describe?
top_level_nodes.one?
end
def describe_statement_children(node)
node.each_child_node(:send).select do |element|
element.method_name == :describe
end
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# Utility methods
module Util
# Error raised by `Util.one` if size is less than zero or greater than one
SizeError = Class.new(IndexError)
# Return only element in array if it contains exactly one member
def one(array)
return array.first if array.one?
raise SizeError,
"expected size to be exactly 1 but size was #{array.size}"
end
end
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# Version information for the RSpec RuboCop plugin.
module Version
STRING = '1.30.0'.freeze
end
end
end

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
module RuboCop
module RSpec
# RSpec example wording rewriter
class Wording
SHOULDNT_PREFIX = /\Ashould(?:n't| not)\b/i
SHOULDNT_BE_PREFIX = /#{SHOULDNT_PREFIX} be\b/i
ES_SUFFIX_PATTERN = /(?:o|s|x|ch|sh|z)\z/i
IES_SUFFIX_PATTERN = /[^aeou]y\z/i
def initialize(text, ignore:, replace:)
@text = text
@ignores = ignore
@replacements = replace
end
def rewrite
case text
when SHOULDNT_BE_PREFIX
replace_prefix(SHOULDNT_BE_PREFIX, 'is not')
when SHOULDNT_PREFIX
replace_prefix(SHOULDNT_PREFIX, 'does not')
else
remove_should_and_pluralize
end
end
private
attr_reader :text, :ignores, :replacements
def replace_prefix(pattern, replacement)
text.sub(pattern) do |shouldnt|
uppercase?(shouldnt) ? replacement.upcase : replacement
end
end
def uppercase?(word)
word.upcase.eql?(word)
end
def remove_should_and_pluralize
_should, *words = text.split
words.each_with_index do |word, index|
next if ignored_word?(word)
words[index] = substitute(word)
break
end
words.join(' ')
end
def ignored_word?(word)
ignores.any? { |ignore| ignore.casecmp(word).zero? }
end
def substitute(word)
# NOTE: Custom replacements are case sensitive.
return replacements.fetch(word) if replacements.key?(word)
case word
when ES_SUFFIX_PATTERN then append_suffix(word, 'es')
when IES_SUFFIX_PATTERN then append_suffix(word[0..-2], 'ies')
else append_suffix(word, 's')
end
end
def append_suffix(word, suffix)
suffix = suffix.upcase if uppercase?(word)
"#{word}#{suffix}"
end
private_constant(*constants(false))
end
end
end