Vendor rubocop-rspec
We aren't going to vendor its dependencies because we already require a systemwide installation of `rubocop` to work in your editor. This avoids requiring users to manually do another `gem install rubocop-rspec` to have their editor integration behave as expected.
This commit is contained in:
parent
f04b0b3130
commit
800853a28d
13
.gitignore
vendored
13
.gitignore
vendored
@ -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
|
||||
|
||||
|
||||
@ -5,9 +5,7 @@ AllCops:
|
||||
- '**/vendor/**/*'
|
||||
DisplayCopNames: false
|
||||
|
||||
require:
|
||||
- ./Homebrew/rubocops.rb
|
||||
- rubocop-rspec
|
||||
require: ./Homebrew/rubocops.rb
|
||||
|
||||
# enable all formulae audits
|
||||
FormulaAudit:
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
5
Library/Homebrew/vendor/Gemfile
vendored
5
Library/Homebrew/vendor/Gemfile
vendored
@ -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
|
||||
|
||||
23
Library/Homebrew/vendor/Gemfile.lock
vendored
23
Library/Homebrew/vendor/Gemfile.lock
vendored
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
451
Library/Homebrew/vendor/bundle-standalone/ruby/2.3.0/gems/rubocop-rspec-1.30.0/config/default.yml
vendored
Normal file
451
Library/Homebrew/vendor/bundle-standalone/ruby/2.3.0/gems/rubocop-rspec-1.30.0/config/default.yml
vendored
Normal 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
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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'
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user