rubocop-rspec 1.37.0 → 1.40.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +2 -62
  4. data/config/default.yml +155 -15
  5. data/lib/rubocop-rspec.rb +2 -1
  6. data/lib/rubocop/cop/rspec/capybara/visibility_matcher.rb +69 -0
  7. data/lib/rubocop/cop/rspec/context_wording.rb +5 -1
  8. data/lib/rubocop/cop/rspec/cop.rb +9 -29
  9. data/lib/rubocop/cop/rspec/describe_class.rb +15 -2
  10. data/lib/rubocop/cop/rspec/describe_method.rb +0 -1
  11. data/lib/rubocop/cop/rspec/empty_hook.rb +50 -0
  12. data/lib/rubocop/cop/rspec/expect_actual.rb +27 -2
  13. data/lib/rubocop/cop/rspec/factory_bot/factory_class_name.rb +16 -1
  14. data/lib/rubocop/cop/rspec/file_path.rb +32 -4
  15. data/lib/rubocop/cop/rspec/hooks_before_examples.rb +3 -21
  16. data/lib/rubocop/cop/rspec/instance_variable.rb +13 -4
  17. data/lib/rubocop/cop/rspec/leading_subject.rb +3 -10
  18. data/lib/rubocop/cop/rspec/let_before_examples.rb +3 -21
  19. data/lib/rubocop/cop/rspec/message_chain.rb +1 -1
  20. data/lib/rubocop/cop/rspec/named_subject.rb +6 -6
  21. data/lib/rubocop/cop/rspec/rails/http_status.rb +2 -0
  22. data/lib/rubocop/cop/rspec/repeated_description.rb +17 -2
  23. data/lib/rubocop/cop/rspec/repeated_example_group_body.rb +97 -0
  24. data/lib/rubocop/cop/rspec/repeated_example_group_description.rb +96 -0
  25. data/lib/rubocop/cop/rspec/scattered_let.rb +13 -0
  26. data/lib/rubocop/cop/rspec/scattered_setup.rb +27 -9
  27. data/lib/rubocop/cop/rspec/shared_examples.rb +1 -0
  28. data/lib/rubocop/cop/rspec/subject_stub.rb +25 -47
  29. data/lib/rubocop/cop/rspec/variable_definition.rb +56 -0
  30. data/lib/rubocop/cop/rspec/variable_name.rb +47 -0
  31. data/lib/rubocop/cop/rspec_cops.rb +7 -1
  32. data/lib/rubocop/rspec/corrector/move_node.rb +52 -0
  33. data/lib/rubocop/rspec/description_extractor.rb +2 -6
  34. data/lib/rubocop/rspec/example.rb +1 -1
  35. data/lib/rubocop/rspec/hook.rb +44 -11
  36. data/lib/rubocop/rspec/language.rb +20 -0
  37. data/lib/rubocop/rspec/language/node_pattern.rb +1 -1
  38. data/lib/rubocop/rspec/variable.rb +16 -0
  39. data/lib/rubocop/rspec/version.rb +1 -1
  40. metadata +16 -9
  41. data/lib/rubocop/rspec/util.rb +0 -19
@@ -58,6 +58,13 @@ module RuboCop
58
58
  (block (send (const nil? :Class) :new ...) ...)
59
59
  PATTERN
60
60
 
61
+ def_node_matcher :custom_matcher?, <<-PATTERN
62
+ (block {
63
+ (send nil? :matcher sym)
64
+ (send (const (const nil? :RSpec) :Matchers) :define sym)
65
+ } ...)
66
+ PATTERN
67
+
61
68
  def_node_search :ivar_usage, '$(ivar $_)'
62
69
 
63
70
  def_node_search :ivar_assigned?, '(ivasgn % ...)'
@@ -66,8 +73,8 @@ module RuboCop
66
73
  return unless spec_group?(node)
67
74
 
68
75
  ivar_usage(node) do |ivar, name|
69
- return if inside_dynamic_class?(ivar)
70
- return if assignment_only? && !ivar_assigned?(node, name)
76
+ next if valid_usage?(ivar)
77
+ next if assignment_only? && !ivar_assigned?(node, name)
71
78
 
72
79
  add_offense(ivar)
73
80
  end
@@ -75,8 +82,10 @@ module RuboCop
75
82
 
76
83
  private
77
84
 
78
- def inside_dynamic_class?(node)
79
- node.each_ancestor(:block).any? { |block| dynamic_class?(block) }
85
+ def valid_usage?(node)
86
+ node.each_ancestor(:block).any? do |block|
87
+ dynamic_class?(block) || custom_matcher?(block)
88
+ end
80
89
  end
81
90
 
82
91
  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)
@@ -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
@@ -11,7 +11,7 @@ module RuboCop
11
11
  #
12
12
  # # better
13
13
  # thing = Thing.new(baz: 42)
14
- # allow(foo).to receive(bar: thing)
14
+ # allow(foo).to receive(:bar).and_return(thing)
15
15
  #
16
16
  class MessageChain < Cop
17
17
  MSG = 'Avoid stubbing using `%<method>s`.'
@@ -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 '\
@@ -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
@@ -35,6 +35,15 @@ module RuboCop
35
35
  check_let_declarations(node.body)
36
36
  end
37
37
 
38
+ def autocorrect(node)
39
+ lambda do |corrector|
40
+ first_let = find_first_let(node.parent)
41
+ RuboCop::RSpec::Corrector::MoveNode.new(
42
+ node, corrector, processed_source
43
+ ).move_after(first_let)
44
+ end
45
+ end
46
+
38
47
  private
39
48
 
40
49
  def check_let_declarations(body)
@@ -47,6 +56,10 @@ module RuboCop
47
56
  add_offense(node)
48
57
  end
49
58
  end
59
+
60
+ def find_first_let(node)
61
+ node.children.find { |child| let?(child) }
62
+ end
50
63
  end
51
64
  end
52
65
  end
@@ -23,25 +23,43 @@ module RuboCop
23
23
  # end
24
24
  #
25
25
  class ScatteredSetup < Cop
26
- MSG = 'Do not define multiple hooks in the same example group.'
26
+ MSG = 'Do not define multiple `%<hook_name>s` hooks in the same '\
27
+ 'example group (also defined on %<lines>s).'
27
28
 
28
29
  def on_block(node)
29
30
  return unless example_group?(node)
30
31
 
31
- analyzable_hooks(node).each do |repeated_hook|
32
- add_offense(repeated_hook)
32
+ repeated_hooks(node).each do |occurrences|
33
+ lines = occurrences.map(&:first_line)
34
+
35
+ occurrences.each do |occurrence|
36
+ lines_except_current = lines - [occurrence.first_line]
37
+ message = format(MSG, hook_name: occurrences.first.method_name,
38
+ lines: lines_msg(lines_except_current))
39
+ add_offense(occurrence, message: message)
40
+ end
33
41
  end
34
42
  end
35
43
 
36
- def analyzable_hooks(node)
37
- RuboCop::RSpec::ExampleGroup.new(node)
44
+ def repeated_hooks(node)
45
+ hooks = RuboCop::RSpec::ExampleGroup.new(node)
38
46
  .hooks
39
- .select { |hook| hook.knowable_scope? && hook.valid_scope? }
40
- .group_by { |hook| [hook.name, hook.scope] }
47
+ .select(&:knowable_scope?)
48
+ .group_by { |hook| [hook.name, hook.scope, hook.metadata] }
41
49
  .values
42
50
  .reject(&:one?)
43
- .flatten
44
- .map(&:to_node)
51
+
52
+ hooks.map do |hook|
53
+ hook.map(&:to_node)
54
+ end
55
+ end
56
+
57
+ def lines_msg(numbers)
58
+ if numbers.size == 1
59
+ "line #{numbers.first}"
60
+ else
61
+ "lines #{numbers.join(', ')}"
62
+ end
45
63
  end
46
64
  end
47
65
  end