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
@@ -47,27 +47,30 @@ module RuboCop
47
47
  # end
48
48
  #
49
49
  class InstanceVariable < Cop
50
+ include RuboCop::RSpec::TopLevelGroup
51
+
50
52
  MSG = 'Avoid instance variables – use let, ' \
51
53
  'a method call, or a local variable (if possible).'
52
54
 
53
- EXAMPLE_GROUP_METHODS = ExampleGroups::ALL + SharedGroups::ALL
54
-
55
- def_node_matcher :spec_group?, EXAMPLE_GROUP_METHODS.block_pattern
56
-
57
55
  def_node_matcher :dynamic_class?, <<-PATTERN
58
56
  (block (send (const nil? :Class) :new ...) ...)
59
57
  PATTERN
60
58
 
59
+ def_node_matcher :custom_matcher?, <<-PATTERN
60
+ (block {
61
+ (send nil? :matcher sym)
62
+ (send (const (const nil? :RSpec) :Matchers) :define sym)
63
+ } ...)
64
+ PATTERN
65
+
61
66
  def_node_search :ivar_usage, '$(ivar $_)'
62
67
 
63
68
  def_node_search :ivar_assigned?, '(ivasgn % ...)'
64
69
 
65
- def on_block(node)
66
- return unless spec_group?(node)
67
-
70
+ def on_top_level_group(node)
68
71
  ivar_usage(node) do |ivar, name|
69
- return if inside_dynamic_class?(ivar)
70
- return if assignment_only? && !ivar_assigned?(node, name)
72
+ next if valid_usage?(ivar)
73
+ next if assignment_only? && !ivar_assigned?(node, name)
71
74
 
72
75
  add_offense(ivar)
73
76
  end
@@ -75,8 +78,10 @@ module RuboCop
75
78
 
76
79
  private
77
80
 
78
- def inside_dynamic_class?(node)
79
- node.each_ancestor(:block).any? { |block| dynamic_class?(block) }
81
+ def valid_usage?(node)
82
+ node.each_ancestor(:block).any? do |block|
83
+ dynamic_class?(block) || custom_matcher?(block)
84
+ end
80
85
  end
81
86
 
82
87
  def assignment_only?
@@ -32,8 +32,6 @@ module RuboCop
32
32
  # it { expect_something_else }
33
33
  #
34
34
  class LeadingSubject < Cop
35
- include RangeHelp
36
-
37
35
  MSG = 'Declare `subject` above any other `%<offending>s` declarations.'
38
36
 
39
37
  def on_block(node)
@@ -58,10 +56,9 @@ module RuboCop
58
56
  def autocorrect(node)
59
57
  lambda do |corrector|
60
58
  first_node = find_first_offending_node(node)
61
- first_node_position = first_node.loc.expression
62
- indent = "\n" + ' ' * first_node.loc.column
63
- corrector.insert_before(first_node_position, node.source + indent)
64
- corrector.remove(node_range(node))
59
+ RuboCop::RSpec::Corrector::MoveNode.new(
60
+ node, corrector, processed_source
61
+ ).move_before(first_node)
65
62
  end
66
63
  end
67
64
 
@@ -75,10 +72,6 @@ module RuboCop
75
72
  node.parent.children.find { |sibling| offending?(sibling) }
76
73
  end
77
74
 
78
- def node_range(node)
79
- range_by_whole_lines(node.source_range, include_final_newline: true)
80
- end
81
-
82
75
  def in_spec_block?(node)
83
76
  node.each_ancestor(:block).any? do |ancestor|
84
77
  example?(ancestor)
@@ -119,11 +119,8 @@ module RuboCop
119
119
  private
120
120
 
121
121
  def inside_describe_block?(node)
122
- node.each_ancestor(:block).any?(&method(:in_example_or_shared_group?))
122
+ node.each_ancestor(:block).any?(&method(:spec_group?))
123
123
  end
124
-
125
- def_node_matcher :in_example_or_shared_group?,
126
- (ExampleGroups::ALL + SharedGroups::ALL).block_pattern
127
124
  end
128
125
  end
129
126
  end
@@ -31,9 +31,6 @@ module RuboCop
31
31
  # expect(some).to be
32
32
  # end
33
33
  class LetBeforeExamples < Cop
34
- include RangeHelp
35
- include RuboCop::RSpec::FinalEndLocation
36
-
37
34
  MSG = 'Move `let` before the examples in the group.'
38
35
 
39
36
  def_node_matcher :example_or_group?, <<-PATTERN
@@ -52,11 +49,9 @@ module RuboCop
52
49
  def autocorrect(node)
53
50
  lambda do |corrector|
54
51
  first_example = find_first_example(node.parent)
55
- first_example_pos = first_example.loc.expression
56
- indent = "\n" + ' ' * first_example.loc.column
57
-
58
- corrector.insert_before(first_example_pos, source(node) + indent)
59
- corrector.remove(node_range_with_surrounding_space(node))
52
+ RuboCop::RSpec::Corrector::MoveNode.new(
53
+ node, corrector, processed_source
54
+ ).move_before(first_example)
60
55
  end
61
56
  end
62
57
 
@@ -80,19 +75,6 @@ module RuboCop
80
75
  def find_first_example(node)
81
76
  node.children.find { |sibling| example_or_group?(sibling) }
82
77
  end
83
-
84
- def node_range_with_surrounding_space(node)
85
- range = node_range(node)
86
- range_by_whole_lines(range, include_final_newline: true)
87
- end
88
-
89
- def source(node)
90
- node_range(node).source
91
- end
92
-
93
- def node_range(node)
94
- node.loc.expression.with(end_pos: final_end_location(node).end_pos)
95
- end
96
78
  end
97
79
  end
98
80
  end
@@ -28,14 +28,20 @@ module RuboCop
28
28
  class LetSetup < Cop
29
29
  MSG = 'Do not use `let!` to setup objects not referenced in tests.'
30
30
 
31
- def_node_search :let_bang, <<-PATTERN
31
+ def_node_matcher :example_or_shared_group_or_including?,
32
+ (
33
+ ExampleGroups::ALL + SharedGroups::ALL +
34
+ Includes::ALL
35
+ ).block_pattern
36
+
37
+ def_node_matcher :let_bang, <<-PATTERN
32
38
  (block $(send nil? :let! (sym $_)) args ...)
33
39
  PATTERN
34
40
 
35
41
  def_node_search :method_called?, '(send nil? %)'
36
42
 
37
43
  def on_block(node)
38
- return unless example_group?(node)
44
+ return unless example_or_shared_group_or_including?(node)
39
45
 
40
46
  unused_let_bang(node) do |let|
41
47
  add_offense(let)
@@ -45,10 +51,16 @@ module RuboCop
45
51
  private
46
52
 
47
53
  def unused_let_bang(node)
48
- let_bang(node) do |method_send, method_name|
54
+ child_let_bang(node) do |method_send, method_name|
49
55
  yield(method_send) unless method_called?(node, method_name)
50
56
  end
51
57
  end
58
+
59
+ def child_let_bang(node, &block)
60
+ RuboCop::RSpec::ExampleGroup.new(node).lets.each do |let|
61
+ let_bang(let, &block)
62
+ end
63
+ end
52
64
  end
53
65
  end
54
66
  end
@@ -6,11 +6,11 @@ module RuboCop
6
6
  # Checks for explicitly referenced test subjects.
7
7
  #
8
8
  # RSpec lets you declare an "implicit subject" using `subject { ... }`
9
- # which allows for tests like `it { should be_valid }`. If you need to
10
- # reference your test subject you should explicitly name it using
11
- # `subject(:your_subject_name) { ... }`. Your test subjects should be
12
- # the most important object in your tests so they deserve a descriptive
13
- # name.
9
+ # which allows for tests like `it { is_expected.to be_valid }`.
10
+ # If you need to reference your test subject you should explicitly
11
+ # name it using `subject(:your_subject_name) { ... }`. Your test subjects
12
+ # should be the most important object in your tests so they deserve
13
+ # a descriptive name.
14
14
  #
15
15
  # This cop can be configured in your configuration using the
16
16
  # `IgnoreSharedExamples` which will not report offenses for implicit
@@ -39,7 +39,7 @@ module RuboCop
39
39
  # RSpec.describe Foo do
40
40
  # subject(:user) { described_class.new }
41
41
  #
42
- # it { should be_valid }
42
+ # it { is_expected.to be_valid }
43
43
  # end
44
44
  class NamedSubject < Cop
45
45
  MSG = 'Name your test subject if you need '\
@@ -97,13 +97,11 @@ module RuboCop
97
97
  "Configuration key `#{DEPRECATED_MAX_KEY}` for #{cop_name} is " \
98
98
  'deprecated in favor of `Max`. Please use that instead.'
99
99
 
100
- def_node_search :find_contexts, ExampleGroups::ALL.block_pattern
101
-
102
100
  def on_top_level_describe(node, _args)
103
- find_nested_contexts(node.parent) do |context, nesting|
101
+ find_nested_example_groups(node.parent) do |example_group, nesting|
104
102
  self.max = nesting
105
103
  add_offense(
106
- context.send_node,
104
+ example_group.send_node,
107
105
  message: message(nesting)
108
106
  )
109
107
  end
@@ -111,13 +109,14 @@ module RuboCop
111
109
 
112
110
  private
113
111
 
114
- def find_nested_contexts(node, nesting: 1, &block)
115
- find_contexts(node) do |nested_context|
116
- yield(nested_context, nesting) if nesting > max_nesting
112
+ def find_nested_example_groups(node, nesting: 1, &block)
113
+ example_group = example_group?(node)
114
+ yield node, nesting if example_group && nesting > max_nesting
115
+
116
+ next_nesting = example_group ? nesting + 1 : nesting
117
117
 
118
- nested_context.each_child_node do |child|
119
- find_nested_contexts(child, nesting: nesting + 1, &block)
120
- end
118
+ node.each_child_node(:block, :begin) do |child|
119
+ find_nested_example_groups(child, nesting: next_nesting, &block)
121
120
  end
122
121
  end
123
122
 
@@ -277,12 +277,12 @@ module RuboCop
277
277
  # expect(foo).to be_something
278
278
  #
279
279
  # # also good - It checks "true" strictly.
280
- # expect(foo).to be(true)
280
+ # expect(foo.something?).to be(true)
281
281
  #
282
282
  # @example Strict: false, EnforcedStyle: inflected
283
283
  # # bad
284
284
  # expect(foo.something?).to be_truthy
285
- # expect(foo).to be(true)
285
+ # expect(foo.something?).to be(true)
286
286
  #
287
287
  # # good
288
288
  # expect(foo).to be_something
@@ -70,6 +70,7 @@ module RuboCop
70
70
  'to describe HTTP status code.'
71
71
 
72
72
  attr_reader :node
73
+
73
74
  def initialize(node)
74
75
  @node = node
75
76
  end
@@ -110,6 +111,7 @@ module RuboCop
110
111
  ALLOWED_STATUSES = %i[error success missing redirect].freeze
111
112
 
112
113
  attr_reader :node
114
+
113
115
  def initialize(node)
114
116
  @node = node
115
117
  end
@@ -29,6 +29,17 @@ module RuboCop
29
29
  # end
30
30
  # end
31
31
  #
32
+ # # good
33
+ # RSpec.describe User do
34
+ # it 'is valid' do
35
+ # # ...
36
+ # end
37
+ #
38
+ # it 'is valid', :flag do
39
+ # # ...
40
+ # end
41
+ # end
42
+ #
32
43
  class RepeatedDescription < Cop
33
44
  MSG = "Don't repeat descriptions within an example group."
34
45
 
@@ -47,14 +58,18 @@ module RuboCop
47
58
  grouped_examples =
48
59
  RuboCop::RSpec::ExampleGroup.new(node)
49
60
  .examples
50
- .group_by(&:doc_string)
61
+ .group_by { |example| example_signature(example) }
51
62
 
52
63
  grouped_examples
53
- .select { |description, group| description && group.size > 1 }
64
+ .select { |signatures, group| signatures.any? && group.size > 1 }
54
65
  .values
55
66
  .flatten
56
67
  .map(&:definition)
57
68
  end
69
+
70
+ def example_signature(example)
71
+ [example.metadata, example.doc_string]
72
+ end
58
73
  end
59
74
  end
60
75
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Check for repeated describe and context block body.
7
+ #
8
+ # @example
9
+ #
10
+ # # bad
11
+ # describe 'cool feature x' do
12
+ # it { cool_predicate }
13
+ # end
14
+ #
15
+ # describe 'cool feature y' do
16
+ # it { cool_predicate }
17
+ # end
18
+ #
19
+ # # good
20
+ # describe 'cool feature' do
21
+ # it { cool_predicate }
22
+ # end
23
+ #
24
+ # describe 'another cool feature' do
25
+ # it { another_predicate }
26
+ # end
27
+ #
28
+ # # good
29
+ # context 'when case x', :tag do
30
+ # it { cool_predicate }
31
+ # end
32
+ #
33
+ # context 'when case y' do
34
+ # it { cool_predicate }
35
+ # end
36
+ #
37
+ # # good
38
+ # context Array do
39
+ # it { is_expected.to respond_to :each }
40
+ # end
41
+ #
42
+ # context Hash do
43
+ # it { is_expected.to respond_to :each }
44
+ # end
45
+ #
46
+ class RepeatedExampleGroupBody < Cop
47
+ MSG = 'Repeated %<group>s block body on line(s) %<loc>s'
48
+
49
+ def_node_matcher :several_example_groups?, <<-PATTERN
50
+ (begin <#example_group_with_body? #example_group_with_body? ...>)
51
+ PATTERN
52
+
53
+ def_node_matcher :metadata, '(block (send _ _ _ $...) ...)'
54
+ def_node_matcher :body, '(block _ args $...)'
55
+ def_node_matcher :const_arg, '(block (send _ _ $const ...) ...)'
56
+
57
+ def_node_matcher :skip_or_pending?, <<-PATTERN
58
+ (block <(send nil? {:skip :pending}) ...>)
59
+ PATTERN
60
+
61
+ def on_begin(node)
62
+ return unless several_example_groups?(node)
63
+
64
+ repeated_group_bodies(node).each do |group, repeats|
65
+ add_offense(group, message: message(group, repeats))
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def repeated_group_bodies(node)
72
+ node
73
+ .children
74
+ .select { |child| example_group_with_body?(child) }
75
+ .reject { |child| skip_or_pending?(child) }
76
+ .group_by { |group| signature_keys(group) }
77
+ .values
78
+ .reject(&:one?)
79
+ .flat_map { |groups| add_repeated_lines(groups) }
80
+ end
81
+
82
+ def add_repeated_lines(groups)
83
+ repeated_lines = groups.map(&:first_line)
84
+ groups.map { |group| [group, repeated_lines - [group.first_line]] }
85
+ end
86
+
87
+ def signature_keys(group)
88
+ [metadata(group), body(group), const_arg(group)]
89
+ end
90
+
91
+ def message(group, repeats)
92
+ format(MSG, group: group.method_name, loc: repeats)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Check for repeated example group descriptions.
7
+ #
8
+ # @example
9
+ #
10
+ # # bad
11
+ # describe 'cool feature' do
12
+ # # example group
13
+ # end
14
+ #
15
+ # describe 'cool feature' do
16
+ # # example group
17
+ # end
18
+ #
19
+ # # bad
20
+ # context 'when case x' do
21
+ # # example group
22
+ # end
23
+ #
24
+ # describe 'when case x' do
25
+ # # example group
26
+ # end
27
+ #
28
+ # # good
29
+ # describe 'cool feature' do
30
+ # # example group
31
+ # end
32
+ #
33
+ # describe 'another cool feature' do
34
+ # # example group
35
+ # end
36
+ #
37
+ # # good
38
+ # context 'when case x' do
39
+ # # example group
40
+ # end
41
+ #
42
+ # context 'when another case' do
43
+ # # example group
44
+ # end
45
+ #
46
+ class RepeatedExampleGroupDescription < Cop
47
+ MSG = 'Repeated %<group>s block description on line(s) %<loc>s'
48
+
49
+ def_node_matcher :several_example_groups?, <<-PATTERN
50
+ (begin <#example_group? #example_group? ...>)
51
+ PATTERN
52
+
53
+ def_node_matcher :doc_string_and_metadata, <<-PATTERN
54
+ (block (send _ _ $_ $...) ...)
55
+ PATTERN
56
+
57
+ def_node_matcher :skip_or_pending?, <<-PATTERN
58
+ (block <(send nil? {:skip :pending}) ...>)
59
+ PATTERN
60
+
61
+ def_node_matcher :empty_description?, '(block (send _ _) ...)'
62
+
63
+ def on_begin(node)
64
+ return unless several_example_groups?(node)
65
+
66
+ repeated_group_descriptions(node).each do |group, repeats|
67
+ add_offense(group, message: message(group, repeats))
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def repeated_group_descriptions(node)
74
+ node
75
+ .children
76
+ .select { |child| example_group?(child) }
77
+ .reject { |child| skip_or_pending?(child) }
78
+ .reject { |child| empty_description?(child) }
79
+ .group_by { |group| doc_string_and_metadata(group) }
80
+ .values
81
+ .reject(&:one?)
82
+ .flat_map { |groups| add_repeated_lines(groups) }
83
+ end
84
+
85
+ def add_repeated_lines(groups)
86
+ repeated_lines = groups.map(&:first_line)
87
+ groups.map { |group| [group, repeated_lines - [group.first_line]] }
88
+ end
89
+
90
+ def message(group, repeats)
91
+ format(MSG, group: group.method_name, loc: repeats)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end