rubocop-rspec 1.37.1 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/CODE_OF_CONDUCT.md +17 -0
  4. data/README.md +2 -62
  5. data/config/default.yml +155 -15
  6. data/lib/rubocop-rspec.rb +3 -1
  7. data/lib/rubocop/cop/rspec/capybara/visibility_matcher.rb +69 -0
  8. data/lib/rubocop/cop/rspec/context_wording.rb +5 -1
  9. data/lib/rubocop/cop/rspec/cop.rb +9 -29
  10. data/lib/rubocop/cop/rspec/describe_class.rb +20 -5
  11. data/lib/rubocop/cop/rspec/describe_method.rb +0 -1
  12. data/lib/rubocop/cop/rspec/empty_hook.rb +50 -0
  13. data/lib/rubocop/cop/rspec/expect_actual.rb +27 -2
  14. data/lib/rubocop/cop/rspec/factory_bot/attribute_defined_statically.rb +5 -2
  15. data/lib/rubocop/cop/rspec/file_path.rb +32 -4
  16. data/lib/rubocop/cop/rspec/hooks_before_examples.rb +3 -21
  17. data/lib/rubocop/cop/rspec/instance_variable.rb +16 -11
  18. data/lib/rubocop/cop/rspec/leading_subject.rb +3 -10
  19. data/lib/rubocop/cop/rspec/leaky_constant_declaration.rb +1 -4
  20. data/lib/rubocop/cop/rspec/let_before_examples.rb +3 -21
  21. data/lib/rubocop/cop/rspec/let_setup.rb +15 -3
  22. data/lib/rubocop/cop/rspec/named_subject.rb +6 -6
  23. data/lib/rubocop/cop/rspec/nested_groups.rb +9 -10
  24. data/lib/rubocop/cop/rspec/predicate_matcher.rb +2 -2
  25. data/lib/rubocop/cop/rspec/rails/http_status.rb +2 -0
  26. data/lib/rubocop/cop/rspec/repeated_description.rb +17 -2
  27. data/lib/rubocop/cop/rspec/repeated_example_group_body.rb +97 -0
  28. data/lib/rubocop/cop/rspec/repeated_example_group_description.rb +96 -0
  29. data/lib/rubocop/cop/rspec/return_from_stub.rb +3 -2
  30. data/lib/rubocop/cop/rspec/scattered_let.rb +13 -0
  31. data/lib/rubocop/cop/rspec/scattered_setup.rb +27 -9
  32. data/lib/rubocop/cop/rspec/shared_examples.rb +1 -0
  33. data/lib/rubocop/cop/rspec/subject_stub.rb +23 -51
  34. data/lib/rubocop/cop/rspec/variable_definition.rb +56 -0
  35. data/lib/rubocop/cop/rspec/variable_name.rb +47 -0
  36. data/lib/rubocop/cop/rspec_cops.rb +7 -1
  37. data/lib/rubocop/rspec/corrector/move_node.rb +52 -0
  38. data/lib/rubocop/rspec/description_extractor.rb +2 -6
  39. data/lib/rubocop/rspec/example.rb +1 -1
  40. data/lib/rubocop/rspec/example_group.rb +21 -49
  41. data/lib/rubocop/rspec/factory_bot.rb +7 -1
  42. data/lib/rubocop/rspec/hook.rb +44 -11
  43. data/lib/rubocop/rspec/language.rb +20 -0
  44. data/lib/rubocop/rspec/language/node_pattern.rb +5 -1
  45. data/lib/rubocop/rspec/top_level_group.rb +44 -0
  46. data/lib/rubocop/rspec/variable.rb +16 -0
  47. data/lib/rubocop/rspec/version.rb +1 -1
  48. metadata +20 -11
  49. data/lib/rubocop/rspec/util.rb +0 -19
@@ -11,18 +11,20 @@ require_relative 'rubocop/rspec/inject'
11
11
  require_relative 'rubocop/rspec/node'
12
12
  require_relative 'rubocop/rspec/top_level_describe'
13
13
  require_relative 'rubocop/rspec/wording'
14
- require_relative 'rubocop/rspec/util'
15
14
  require_relative 'rubocop/rspec/language'
16
15
  require_relative 'rubocop/rspec/language/node_pattern'
16
+ require_relative 'rubocop/rspec/top_level_group'
17
17
  require_relative 'rubocop/rspec/concept'
18
18
  require_relative 'rubocop/rspec/example_group'
19
19
  require_relative 'rubocop/rspec/example'
20
20
  require_relative 'rubocop/rspec/hook'
21
+ require_relative 'rubocop/rspec/variable'
21
22
  require_relative 'rubocop/cop/rspec/cop'
22
23
  require_relative 'rubocop/rspec/align_let_brace'
23
24
  require_relative 'rubocop/rspec/factory_bot'
24
25
  require_relative 'rubocop/rspec/final_end_location'
25
26
  require_relative 'rubocop/rspec/blank_line_separation'
27
+ require_relative 'rubocop/rspec/corrector/move_node'
26
28
 
27
29
  RuboCop::RSpec::Inject.defaults!
28
30
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ module Capybara
7
+ # Checks for boolean visibility in capybara finders.
8
+ #
9
+ # Capybara lets you find elements that match a certain visibility using
10
+ # the `:visible` option. `:visible` accepts both boolean and symbols as
11
+ # values, however using booleans can have unwanted effects. `visible:
12
+ # false` does not find just invisible elements, but both visible and
13
+ # invisible elements. For expressiveness and clarity, use one of the
14
+ # symbol values, `:all`, `:hidden` or `:visible`.
15
+ # (https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all)
16
+ #
17
+ # @example
18
+ #
19
+ # # bad
20
+ # expect(page).to have_selector('.foo', visible: false)
21
+ # expect(page).to have_css('.foo', visible: true)
22
+ # expect(page).to have_link('my link', visible: false)
23
+ #
24
+ # # good
25
+ # expect(page).to have_selector('.foo', visible: :visible)
26
+ # expect(page).to have_css('.foo', visible: :all)
27
+ # expect(page).to have_link('my link', visible: :hidden)
28
+ #
29
+ class VisibilityMatcher < Cop
30
+ MSG_FALSE = 'Use `:all` or `:hidden` instead of `false`.'
31
+ MSG_TRUE = 'Use `:visible` instead of `true`.'
32
+ CAPYBARA_MATCHER_METHODS = %i[
33
+ have_selector
34
+ have_css
35
+ have_xpath
36
+ have_link
37
+ have_button
38
+ have_field
39
+ have_select
40
+ have_table
41
+ have_checked_field
42
+ have_unchecked_field
43
+ have_text
44
+ have_content
45
+ ].freeze
46
+
47
+ def_node_matcher :visible_true?, <<~PATTERN
48
+ (send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) true) ...>))
49
+ PATTERN
50
+
51
+ def_node_matcher :visible_false?, <<~PATTERN
52
+ (send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) false) ...>))
53
+ PATTERN
54
+
55
+ def on_send(node)
56
+ visible_false?(node) { |arg| add_offense(arg, message: MSG_FALSE) }
57
+ visible_true?(node) { |arg| add_offense(arg, message: MSG_TRUE) }
58
+ end
59
+
60
+ private
61
+
62
+ def capybara_matcher?(method_name)
63
+ CAPYBARA_MATCHER_METHODS.include? method_name
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -5,7 +5,11 @@ module RuboCop
5
5
  module RSpec
6
6
  # Checks that `context` docstring starts with an allowed prefix.
7
7
  #
8
- # @see https://github.com/reachlocal/rspec-style-guide#context-descriptions
8
+ # The default list of prefixes is minimal. Users are encouraged to tailor
9
+ # the configuration to meet project needs. Other acceptable prefixes may
10
+ # include `if`, `unless`, `for`, `before`, `after`, or `during`.
11
+ #
12
+ # @see https://rspec.rubystyle.guide/#context-descriptions
9
13
  # @see http://www.betterspecs.org/#contexts
10
14
  #
11
15
  # @example `Prefixes` configuration
@@ -1,29 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RuboCop
4
- module Cop # rubocop:disable Style/Documentation
5
- WorkaroundCop = Cop.dup
6
-
7
- # Clone of the the normal RuboCop::Cop::Cop class so we can rewrite
8
- # the inherited method without breaking functionality
9
- class WorkaroundCop
10
- # Remove the Cop.inherited method to be a noop. Our RSpec::Cop
11
- # class will invoke the inherited hook instead
12
- class << self
13
- undef inherited
14
- def inherited(*) end
15
- end
16
-
17
- # Special case `Module#<` so that the rspec support rubocop exports
18
- # is compatible with our subclass
19
- def self.<(other)
20
- other.equal?(RuboCop::Cop::Cop) || super
21
- end
22
- end
23
- private_constant(:WorkaroundCop)
24
-
4
+ module Cop
25
5
  module RSpec
26
- # @abstract parent class to rspec cops
6
+ # @abstract parent class to RSpec cops
27
7
  #
28
8
  # The criteria for whether rubocop-rspec analyzes a certain ruby file
29
9
  # is configured via `AllCops/RSpec`. For example, if you want to
@@ -31,13 +11,13 @@ module RuboCop
31
11
  # then you could add this to your configuration:
32
12
  #
33
13
  # @example configuring analyzed paths
34
- #
35
- # AllCops:
36
- # RSpec:
37
- # Patterns:
38
- # - '_test.rb$'
39
- # - '(?:^|/)test/'
40
- class Cop < WorkaroundCop
14
+ # # .rubocop.yml
15
+ # # AllCops:
16
+ # # RSpec:
17
+ # # Patterns:
18
+ # # - '_test.rb$'
19
+ # # - '(?:^|/)test/'
20
+ class Cop < ::RuboCop::Cop::Cop
41
21
  include RuboCop::RSpec::Language
42
22
  include RuboCop::RSpec::Language::NodePattern
43
23
 
@@ -12,6 +12,11 @@ module RuboCop
12
12
  #
13
13
  # # good
14
14
  # describe TestedClass do
15
+ # subject { described_class }
16
+ # end
17
+ #
18
+ # describe 'TestedClass::VERSION' do
19
+ # subject { Object.const_get(self.class.description) }
15
20
  # end
16
21
  #
17
22
  # describe "A feature example", type: :feature do
@@ -38,18 +43,28 @@ module RuboCop
38
43
  def_node_matcher :rails_metadata?, <<-PATTERN
39
44
  (pair
40
45
  (sym :type)
41
- (sym {:request :feature :system :routing :view})
46
+ (sym {
47
+ :channel :controller :helper :job :mailer :model :request
48
+ :routing :view :feature :system :mailbox
49
+ }
50
+ )
42
51
  )
43
52
  PATTERN
44
53
 
45
- def_node_matcher :shared_group?, SharedGroups::ALL.block_pattern
46
-
47
- def on_top_level_describe(node, args)
54
+ def on_top_level_describe(node, (described_value, _))
48
55
  return if shared_group?(root_node)
49
56
  return if valid_describe?(node)
50
57
  return if describe_with_rails_metadata?(node)
58
+ return if string_constant_describe?(described_value)
59
+
60
+ add_offense(described_value)
61
+ end
62
+
63
+ private
51
64
 
52
- add_offense(args.first)
65
+ def string_constant_describe?(described_value)
66
+ described_value.str_type? &&
67
+ described_value.value =~ /^((::)?[A-Z]\w*)+$/
53
68
  end
54
69
  end
55
70
  end
@@ -18,7 +18,6 @@ module RuboCop
18
18
  # end
19
19
  class DescribeMethod < Cop
20
20
  include RuboCop::RSpec::TopLevelDescribe
21
- include RuboCop::RSpec::Util
22
21
 
23
22
  MSG = 'The second argument to describe should be the method '\
24
23
  "being tested. '#instance' or '.class'."
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for empty before and after hooks.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # before {}
11
+ # after do; end
12
+ # before(:all) do
13
+ # end
14
+ # after(:all) { }
15
+ #
16
+ # # good
17
+ # before { create_users }
18
+ # after do
19
+ # cleanup_users
20
+ # end
21
+ # before(:all) do
22
+ # create_feed
23
+ # end
24
+ # after(:all) { cleanup_feed }
25
+ class EmptyHook < Cop
26
+ include RuboCop::Cop::RangeHelp
27
+
28
+ MSG = 'Empty hook detected.'
29
+
30
+ def_node_matcher :empty_hook?, <<~PATTERN
31
+ (block $#{Hooks::ALL.send_pattern} _ nil?)
32
+ PATTERN
33
+
34
+ def on_block(node)
35
+ empty_hook?(node) do |hook|
36
+ add_offense(hook)
37
+ end
38
+ end
39
+
40
+ def autocorrect(node)
41
+ lambda do |corrector|
42
+ block = node.parent
43
+ range = range_with_surrounding_space(range: block.loc.expression)
44
+ corrector.remove(range)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -41,11 +41,31 @@ module RuboCop
41
41
  regexp
42
42
  ].freeze
43
43
 
44
- def_node_matcher :expect_literal, '(send _ :expect $#literal?)'
44
+ SUPPORTED_MATCHERS = %i[eq eql equal be].freeze
45
+
46
+ def_node_matcher :expect_literal, <<~PATTERN
47
+ (send
48
+ (send nil? :expect $#literal?)
49
+ #{Runners::ALL.node_pattern_union}
50
+ {
51
+ (send (send nil? $:be) :== $_)
52
+ (send nil? $_ $_ ...)
53
+ }
54
+ )
55
+ PATTERN
45
56
 
46
57
  def on_send(node)
47
58
  expect_literal(node) do |argument|
48
- add_offense(argument)
59
+ add_offense(node, location: argument.source_range)
60
+ end
61
+ end
62
+
63
+ def autocorrect(node)
64
+ actual, matcher, expected = expect_literal(node)
65
+ lambda do |corrector|
66
+ return unless SUPPORTED_MATCHERS.include?(matcher)
67
+
68
+ swap(corrector, actual, expected)
49
69
  end
50
70
  end
51
71
 
@@ -65,6 +85,11 @@ module RuboCop
65
85
  COMPLEX_LITERALS.include?(node.type) &&
66
86
  node.each_child_node.all?(&method(:literal?))
67
87
  end
88
+
89
+ def swap(corrector, actual, expected)
90
+ corrector.replace(actual.source_range, expected.source)
91
+ corrector.replace(expected.source_range, actual.source)
92
+ end
68
93
  end
69
94
  end
70
95
  end
@@ -31,12 +31,15 @@ module RuboCop
31
31
  (send _ !#reserved_method? $...)
32
32
  PATTERN
33
33
 
34
- def_node_search :factory_attributes, <<-PATTERN
34
+ def_node_matcher :factory_attributes, <<-PATTERN
35
35
  (block (send _ #attribute_defining_method? ...) _ { (begin $...) $(send ...) } )
36
36
  PATTERN
37
37
 
38
38
  def on_block(node)
39
- factory_attributes(node).to_a.flatten.each do |attribute|
39
+ attributes = factory_attributes(node) || []
40
+ attributes = [attributes] unless attributes.is_a?(Array)
41
+
42
+ attributes.each do |attribute|
40
43
  next unless offensive_receiver?(attribute.receiver, node)
41
44
  next if proc?(attribute) || association?(attribute.first_argument)
42
45
 
@@ -3,10 +3,11 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- # Checks that spec file paths are consistent with the test subject.
6
+ # Checks that spec file paths are consistent and well-formed.
7
7
  #
8
- # Checks the path of the spec file and enforces that it reflects the
9
- # described class/module and its optionally called out method.
8
+ # By default, this checks that spec file paths are consistent with the
9
+ # test subject and and enforces that it reflects the described
10
+ # class/module and its optionally called out method.
10
11
  #
11
12
  # With the configuration option `IgnoreMethods` the called out method will
12
13
  # be ignored when determining the enforced path.
@@ -15,6 +16,10 @@ module RuboCop
15
16
  # be specified that should not as usual be transformed from CamelCase to
16
17
  # snake_case (e.g. 'RuboCop' => 'rubocop' ).
17
18
  #
19
+ # With the configuration option `SpecSuffixOnly` test files will only
20
+ # be checked to ensure they end in '_spec.rb'. This option disables
21
+ # checking for consistency in the test subject or test methods.
22
+ #
18
23
  # @example
19
24
  # # bad
20
25
  # whatever_spec.rb # describe MyClass
@@ -41,6 +46,16 @@ module RuboCop
41
46
  # # good
42
47
  # my_class_spec.rb # describe MyClass, '#method'
43
48
  #
49
+ # @example when configuration is `SpecSuffixOnly: true`
50
+ # # good
51
+ # whatever_spec.rb # describe MyClass
52
+ #
53
+ # # good
54
+ # my_class_spec.rb # describe MyClass
55
+ #
56
+ # # good
57
+ # my_class_spec.rb # describe MyClass, '#method'
58
+ #
44
59
  class FilePath < Cop
45
60
  include RuboCop::RSpec::TopLevelDescribe
46
61
 
@@ -70,9 +85,15 @@ module RuboCop
70
85
  end
71
86
 
72
87
  def glob_for((described_class, method_name))
88
+ return glob_for_spec_suffix_only? if spec_suffix_only?
89
+
73
90
  "#{expected_path(described_class)}#{name_glob(method_name)}*_spec.rb"
74
91
  end
75
92
 
93
+ def glob_for_spec_suffix_only?
94
+ '*_spec.rb'
95
+ end
96
+
76
97
  def name_glob(name)
77
98
  return unless name&.str_type?
78
99
 
@@ -103,12 +124,19 @@ module RuboCop
103
124
  end
104
125
 
105
126
  def filename_ends_with?(glob)
106
- File.fnmatch?("*#{glob}", processed_source.buffer.name)
127
+ filename =
128
+ RuboCop::PathUtil.relative_path(processed_source.buffer.name)
129
+ .gsub('../', '')
130
+ File.fnmatch?("*#{glob}", filename)
107
131
  end
108
132
 
109
133
  def relevant_rubocop_rspec_file?(_file)
110
134
  true
111
135
  end
136
+
137
+ def spec_suffix_only?
138
+ cop_config['SpecSuffixOnly']
139
+ end
112
140
  end
113
141
  end
114
142
  end
@@ -24,9 +24,6 @@ module RuboCop
24
24
  # end
25
25
  #
26
26
  class HooksBeforeExamples < Cop
27
- include RangeHelp
28
- include RuboCop::RSpec::FinalEndLocation
29
-
30
27
  MSG = 'Move `%<hook>s` above the examples in the group.'
31
28
 
32
29
  def_node_matcher :example_or_group?, <<-PATTERN
@@ -45,11 +42,9 @@ module RuboCop
45
42
  def autocorrect(node)
46
43
  lambda do |corrector|
47
44
  first_example = find_first_example(node.parent)
48
- first_example_pos = first_example.loc.expression
49
- indent = "\n" + ' ' * first_example.loc.column
50
-
51
- corrector.insert_before(first_example_pos, source(node) + indent)
52
- corrector.remove(node_range_with_surrounding_space(node))
45
+ RuboCop::RSpec::Corrector::MoveNode.new(
46
+ node, corrector, processed_source
47
+ ).move_before(first_example)
53
48
  end
54
49
  end
55
50
 
@@ -77,19 +72,6 @@ module RuboCop
77
72
  def find_first_example(node)
78
73
  node.children.find { |sibling| example_or_group?(sibling) }
79
74
  end
80
-
81
- def node_range_with_surrounding_space(node)
82
- range = node_range(node)
83
- range_by_whole_lines(range, include_final_newline: true)
84
- end
85
-
86
- def source(node)
87
- node_range(node).source
88
- end
89
-
90
- def node_range(node)
91
- node.loc.expression.with(end_pos: final_end_location(node).end_pos)
92
- end
93
75
  end
94
76
  end
95
77
  end