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
@@ -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
@@ -75,70 +77,46 @@ module RuboCop
75
77
 
76
78
  def on_block(node)
77
79
  return unless example_group?(node)
80
+ return if (processed_example_groups & node.ancestors).any?
81
+
82
+ processed_example_groups << node
83
+ @explicit_subjects = find_all_explicit_subjects(node)
78
84
 
79
- find_subject_stub(node) do |stub|
85
+ find_subject_expectations(node) do |stub|
80
86
  add_offense(stub)
81
87
  end
82
88
  end
83
89
 
84
90
  private
85
91
 
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
92
+ def processed_example_groups
93
+ @processed_example_groups ||= Set.new
95
94
  end
96
95
 
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)
96
+ def find_all_explicit_subjects(node)
97
+ node.each_descendant(:block).with_object({}) do |child, h|
98
+ name = subject(child)
99
+ next unless name
109
100
 
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
101
+ outer_example_group = child.each_ancestor.find do |a|
102
+ example_group?(a)
103
+ end
115
104
 
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)
105
+ h[outer_example_group] ||= []
106
+ h[outer_example_group] << name
124
107
  end
125
108
  end
126
109
 
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
110
+ def find_subject_expectations(node, subject_names = [], &block)
111
+ subject_names = @explicit_subjects[node] if @explicit_subjects[node]
137
112
 
138
- yield(subject_name, parent) if parent
113
+ expectation_detected = (subject_names + [:subject]).any? do |name|
114
+ message_expectation?(node, name)
115
+ end
116
+ return yield(node) if expectation_detected
139
117
 
140
118
  node.each_child_node do |child|
141
- find_subject(child, parent: node, &block)
119
+ find_subject_expectations(child, subject_names, &block)
142
120
  end
143
121
  end
144
122
  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/HandleExceptions
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
 
@@ -4,21 +4,24 @@ module RuboCop
4
4
  module RSpec
5
5
  # Wrapper for RSpec hook
6
6
  class Hook < Concept
7
- STANDARDIZED_SCOPES = %i[each context suite].freeze
8
- private_constant(:STANDARDIZED_SCOPES)
7
+ def_node_matcher :extract_metadata, <<~PATTERN
8
+ (block
9
+ {
10
+ (send _ _ #valid_scope? $...)
11
+ (send _ _ $...)
12
+ }
13
+ ...
14
+ )
15
+ PATTERN
9
16
 
10
17
  def name
11
18
  node.method_name
12
19
  end
13
20
 
14
21
  def knowable_scope?
15
- return true unless scope_argument
16
-
17
- scope_argument.sym_type?
18
- end
19
-
20
- def valid_scope?
21
- STANDARDIZED_SCOPES.include?(scope)
22
+ scope_argument.nil? ||
23
+ scope_argument.sym_type? ||
24
+ scope_argument.hash_type?
22
25
  end
23
26
 
24
27
  def example?
@@ -26,17 +29,47 @@ module RuboCop
26
29
  end
27
30
 
28
31
  def scope
32
+ return :each if scope_argument&.hash_type?
33
+
29
34
  case scope_name
30
35
  when nil, :each, :example then :each
31
36
  when :context, :all then :context
32
37
  when :suite then :suite
33
- else
34
- scope_name
35
38
  end
36
39
  end
37
40
 
41
+ def metadata
42
+ (extract_metadata(node) || [])
43
+ .map { |meta| transform_metadata(meta) }
44
+ .flatten
45
+ .inject(&:merge)
46
+ end
47
+
38
48
  private
39
49
 
50
+ def valid_scope?(node)
51
+ node&.sym_type? && Hooks::Scopes::ALL.include?(node.value)
52
+ end
53
+
54
+ def transform_metadata(meta)
55
+ if meta.sym_type?
56
+ { meta => true }
57
+ else
58
+ # This check is to be able to compare those two hooks:
59
+ #
60
+ # before(:example, :special) { ... }
61
+ # before(:example, special: true) { ... }
62
+ #
63
+ # In the second case it's a node with a pair that has a value
64
+ # of a `true_type?`.
65
+ meta.pairs.map { |pair| { pair.key => transform_true(pair.value) } }
66
+ end
67
+ end
68
+
69
+ def transform_true(node)
70
+ node.true_type? ? true : node
71
+ end
72
+
40
73
  def scope_name
41
74
  scope_argument.to_a.first
42
75
  end
@@ -28,6 +28,14 @@ module RuboCop
28
28
  "(block #{send_pattern} ...)"
29
29
  end
30
30
 
31
+ def block_pass_pattern
32
+ "(send #{RSPEC} #{node_pattern_union} _ block_pass)"
33
+ end
34
+
35
+ def block_or_block_pass_pattern
36
+ "{#{block_pattern} #{block_pass_pattern}}"
37
+ end
38
+
31
39
  def send_pattern
32
40
  "(send #{RSPEC} #{node_pattern_union} ...)"
33
41
  end
@@ -94,6 +102,18 @@ module RuboCop
94
102
  append_after
95
103
  ]
96
104
  )
105
+
106
+ module Scopes
107
+ ALL = SelectorSet.new(
108
+ %i[
109
+ each
110
+ example
111
+ context
112
+ all
113
+ suite
114
+ ]
115
+ )
116
+ end
97
117
  end
98
118
 
99
119
  module Helpers