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
@@ -40,20 +40,21 @@ module RuboCop
40
40
  MSG_BLOCK = 'Use block for static values.'
41
41
 
42
42
  def_node_search :contains_stub?, '(send nil? :receive (...))'
43
+ def_node_matcher :stub_with_block?, '(block #contains_stub? ...)'
43
44
  def_node_search :and_return_value, <<-PATTERN
44
45
  $(send _ :and_return $(...))
45
46
  PATTERN
46
47
 
47
48
  def on_send(node)
48
- return unless contains_stub?(node)
49
49
  return unless style == :block
50
+ return unless contains_stub?(node)
50
51
 
51
52
  check_and_return_call(node)
52
53
  end
53
54
 
54
55
  def on_block(node)
55
- return unless contains_stub?(node)
56
56
  return unless style == :and_return
57
+ return unless stub_with_block?(node)
57
58
 
58
59
  check_block_body(node)
59
60
  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
@@ -47,6 +47,7 @@ module RuboCop
47
47
  'to titleize shared examples.'
48
48
 
49
49
  attr_reader :node
50
+
50
51
  def initialize(node)
51
52
  @node = node
52
53
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  module RuboCop
4
6
  module Cop
5
7
  module RSpec
@@ -20,6 +22,8 @@ module RuboCop
20
22
  # end
21
23
  #
22
24
  class SubjectStub < Cop
25
+ include RuboCop::RSpec::TopLevelGroup
26
+
23
27
  MSG = 'Do not stub methods of the object under test.'
24
28
 
25
29
  # @!method subject(node)
@@ -73,72 +77,40 @@ module RuboCop
73
77
  } ...)
74
78
  PATTERN
75
79
 
76
- def on_block(node)
77
- return unless example_group?(node)
80
+ def on_top_level_group(node)
81
+ @explicit_subjects = find_all_explicit_subjects(node)
78
82
 
79
- find_subject_stub(node) do |stub|
83
+ find_subject_expectations(node) do |stub|
80
84
  add_offense(stub)
81
85
  end
82
86
  end
83
87
 
84
88
  private
85
89
 
86
- # Find subjects within tree and then find (send) nodes for that subject
87
- #
88
- # @param node [RuboCop::Node] example group
89
- #
90
- # @yield [RuboCop::Node] message expectations for subject
91
- def find_subject_stub(node, &block)
92
- find_subject(node) do |subject_name, context|
93
- find_subject_expectation(context, subject_name, &block)
94
- end
95
- end
96
-
97
- # Find a subject message expectation
98
- #
99
- # @param node [RuboCop::Node]
100
- # @param subject_name [Symbol] name of subject
101
- #
102
- # @yield [RuboCop::Node] message expectation
103
- def find_subject_expectation(node, subject_name, &block)
104
- # Do not search node if it is an example group with its own subject.
105
- return if example_group?(node) && redefines_subject?(node)
106
-
107
- # Yield the current node if it is a message expectation.
108
- yield(node) if message_expectation?(node, subject_name)
90
+ def find_all_explicit_subjects(node)
91
+ node.each_descendant(:block).with_object({}) do |child, h|
92
+ name = subject(child)
93
+ next unless name
109
94
 
110
- # Recurse through node's children looking for a message expectation.
111
- node.each_child_node do |child|
112
- find_subject_expectation(child, subject_name, &block)
113
- end
114
- end
95
+ outer_example_group = child.each_ancestor.find do |a|
96
+ example_group?(a)
97
+ end
115
98
 
116
- # Check if node's children contain a subject definition
117
- #
118
- # @param node [RuboCop::Node]
119
- #
120
- # @return [Boolean]
121
- def redefines_subject?(node)
122
- node.each_child_node.any? do |child|
123
- subject(child) || redefines_subject?(child)
99
+ h[outer_example_group] ||= []
100
+ h[outer_example_group] << name
124
101
  end
125
102
  end
126
103
 
127
- # Find a subject definition
128
- #
129
- # @param node [RuboCop::Node]
130
- # @param parent [RuboCop::Node,nil]
131
- #
132
- # @yieldparam subject_name [Symbol] name of subject being defined
133
- # @yieldparam parent [RuboCop::Node] parent of subject definition
134
- def find_subject(node, parent: nil, &block)
135
- # An implicit subject is defined by RSpec when no subject is declared
136
- subject_name = subject(node) || :subject
104
+ def find_subject_expectations(node, subject_names = [], &block)
105
+ subject_names = @explicit_subjects[node] if @explicit_subjects[node]
137
106
 
138
- yield(subject_name, parent) if parent
107
+ expectation_detected = (subject_names + [:subject]).any? do |name|
108
+ message_expectation?(node, name)
109
+ end
110
+ return yield(node) if expectation_detected
139
111
 
140
112
  node.each_child_node do |child|
141
- find_subject(child, parent: node, &block)
113
+ find_subject_expectations(child, subject_names, &block)
142
114
  end
143
115
  end
144
116
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks that memoized helpers names are symbols or strings.
7
+ #
8
+ # @example EnforcedStyle: symbols (default)
9
+ # # bad
10
+ # let('user_name') { 'Adam' }
11
+ # subject('user') { create_user }
12
+ #
13
+ # # good
14
+ # let(:user_name) { 'Adam' }
15
+ # subject(:user) { create_user }
16
+ #
17
+ # @example EnforcedStyle: strings
18
+ # # bad
19
+ # let(:user_name) { 'Adam' }
20
+ # subject(:user) { create_user }
21
+ #
22
+ # # good
23
+ # let('user_name') { 'Adam' }
24
+ # subject('user') { create_user }
25
+ class VariableDefinition < Cop
26
+ include ConfigurableEnforcedStyle
27
+ include RuboCop::RSpec::Variable
28
+
29
+ MSG = 'Use %<style>s for variable names.'
30
+
31
+ def on_send(node)
32
+ variable_definition?(node) do |variable|
33
+ if style_violation?(variable)
34
+ add_offense(variable, message: format(MSG, style: style))
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def style_violation?(variable)
42
+ style == :symbols && string?(variable) ||
43
+ style == :strings && symbol?(variable)
44
+ end
45
+
46
+ def string?(node)
47
+ node.str_type? || node.dstr_type?
48
+ end
49
+
50
+ def symbol?(node)
51
+ node.sym_type? || node.dsym_type?
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks that memoized helper names use the configured style.
7
+ #
8
+ # @example EnforcedStyle: snake_case (default)
9
+ # # bad
10
+ # let(:userName) { 'Adam' }
11
+ # subject(:userName) { 'Adam' }
12
+ #
13
+ # # good
14
+ # let(:user_name) { 'Adam' }
15
+ # subject(:user_name) { 'Adam' }
16
+ #
17
+ # @example EnforcedStyle: camelCase
18
+ # # bad
19
+ # let(:user_name) { 'Adam' }
20
+ # subject(:user_name) { 'Adam' }
21
+ #
22
+ # # good
23
+ # let(:userName) { 'Adam' }
24
+ # subject(:userName) { 'Adam' }
25
+ class VariableName < Cop
26
+ include ConfigurableNaming
27
+ include RuboCop::RSpec::Variable
28
+
29
+ MSG = 'Use %<style>s for variable names.'
30
+
31
+ def on_send(node)
32
+ variable_definition?(node) do |variable|
33
+ return if variable.dstr_type? || variable.dsym_type?
34
+
35
+ check_name(node, variable.value, variable.loc.expression)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def message(style)
42
+ format(MSG, style: style)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'rspec/capybara/current_path_expectation'
4
4
  require_relative 'rspec/capybara/feature_methods'
5
+ require_relative 'rspec/capybara/visibility_matcher'
5
6
 
6
7
  require_relative 'rspec/factory_bot/attribute_defined_statically'
7
8
  require_relative 'rspec/factory_bot/create_list'
@@ -9,7 +10,7 @@ require_relative 'rspec/factory_bot/factory_class_name'
9
10
 
10
11
  begin
11
12
  require_relative 'rspec/rails/http_status'
12
- rescue LoadError # rubocop:disable Lint/SuppressedException
13
+ rescue LoadError
13
14
  # Rails/HttpStatus cannot be loaded if rack/utils is unavailable.
14
15
  end
15
16
 
@@ -29,6 +30,7 @@ require_relative 'rspec/described_class'
29
30
  require_relative 'rspec/described_class_module_wrapping'
30
31
  require_relative 'rspec/dialect'
31
32
  require_relative 'rspec/empty_example_group'
33
+ require_relative 'rspec/empty_hook'
32
34
  require_relative 'rspec/empty_line_after_example'
33
35
  require_relative 'rspec/empty_line_after_example_group'
34
36
  require_relative 'rspec/empty_line_after_final_let'
@@ -74,6 +76,8 @@ require_relative 'rspec/receive_counts'
74
76
  require_relative 'rspec/receive_never'
75
77
  require_relative 'rspec/repeated_description'
76
78
  require_relative 'rspec/repeated_example'
79
+ require_relative 'rspec/repeated_example_group_body'
80
+ require_relative 'rspec/repeated_example_group_description'
77
81
  require_relative 'rspec/return_from_stub'
78
82
  require_relative 'rspec/scattered_let'
79
83
  require_relative 'rspec/scattered_setup'
@@ -82,6 +86,8 @@ require_relative 'rspec/shared_examples'
82
86
  require_relative 'rspec/single_argument_message_chain'
83
87
  require_relative 'rspec/subject_stub'
84
88
  require_relative 'rspec/unspecified_exception'
89
+ require_relative 'rspec/variable_definition'
90
+ require_relative 'rspec/variable_name'
85
91
  require_relative 'rspec/verified_doubles'
86
92
  require_relative 'rspec/void_expect'
87
93
  require_relative 'rspec/yield'
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module RSpec
5
+ module Corrector
6
+ # Helper methods to move a node
7
+ class MoveNode
8
+ include RuboCop::Cop::RangeHelp
9
+ include RuboCop::RSpec::FinalEndLocation
10
+
11
+ attr_reader :original, :corrector, :processed_source
12
+
13
+ def initialize(node, corrector, processed_source)
14
+ @original = node
15
+ @corrector = corrector
16
+ @processed_source = processed_source # used by RangeHelp
17
+ end
18
+
19
+ def move_before(other) # rubocop:disable Metrics/AbcSize
20
+ position = other.loc.expression
21
+ indent = "\n" + ' ' * other.loc.column
22
+
23
+ corrector.insert_before(position, source(original) + indent)
24
+ corrector.remove(node_range_with_surrounding_space(original))
25
+ end
26
+
27
+ def move_after(other)
28
+ position = final_end_location(other)
29
+ indent = "\n" + ' ' * other.loc.column
30
+
31
+ corrector.insert_after(position, indent + source(original))
32
+ corrector.remove(node_range_with_surrounding_space(original))
33
+ end
34
+
35
+ private
36
+
37
+ def source(node)
38
+ node_range(node).source
39
+ end
40
+
41
+ def node_range(node)
42
+ node.loc.expression.with(end_pos: final_end_location(node).end_pos)
43
+ end
44
+
45
+ def node_range_with_surrounding_space(node)
46
+ range = node_range(node)
47
+ range_by_whole_lines(range, include_final_newline: true)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -21,7 +21,7 @@ module RuboCop
21
21
 
22
22
  # Decorator of a YARD code object for working with documented rspec cops
23
23
  class CodeObject
24
- COP_CLASS_NAMES = %w[RuboCop::Cop RuboCop::Cop::RSpec::Cop].freeze
24
+ COP_CLASS_NAME = 'RuboCop::Cop::RSpec::Cop'
25
25
  RSPEC_NAMESPACE = 'RuboCop::Cop::RSpec'
26
26
 
27
27
  def initialize(yardoc)
@@ -68,11 +68,7 @@ module RuboCop
68
68
  end
69
69
 
70
70
  def cop_subclass?
71
- # YARD superclass resolution is a bit flaky: All classes loaded before
72
- # RuboCop::Cop::WorkaroundCop are shown as having RuboCop::Cop as
73
- # superclass, while all the following classes are listed as having
74
- # RuboCop::Cop::RSpec::Cop as their superclass.
75
- COP_CLASS_NAMES.include?(yardoc.superclass.path)
71
+ yardoc.superclass.path == COP_CLASS_NAME
76
72
  end
77
73
 
78
74
  def abstract?
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module RSpec
5
5
  # Wrapper for RSpec examples
6
6
  class Example < Concept
7
- def_node_matcher :extract_doc_string, '(send _ _ $str ...)'
7
+ def_node_matcher :extract_doc_string, '(send _ _ $_ ...)'
8
8
  def_node_matcher :extract_metadata, '(send _ _ _ $...)'
9
9
  def_node_matcher :extract_implementation, '(block send args $_)'
10
10