rubocop-rspec 2.13.2 → 2.14.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +402 -383
  3. data/CODE_OF_CONDUCT.md +4 -4
  4. data/MIT-LICENSE.md +1 -2
  5. data/config/default.yml +65 -4
  6. data/lib/rubocop/cop/rspec/capybara/negation_matcher.rb +106 -0
  7. data/lib/rubocop/cop/rspec/capybara/specific_actions.rb +85 -0
  8. data/lib/rubocop/cop/rspec/capybara/specific_matcher.rb +5 -82
  9. data/lib/rubocop/cop/rspec/change_by_zero.rb +1 -1
  10. data/lib/rubocop/cop/rspec/context_wording.rb +4 -2
  11. data/lib/rubocop/cop/rspec/example_wording.rb +32 -0
  12. data/lib/rubocop/cop/rspec/factory_bot/consistent_parentheses_style.rb +99 -0
  13. data/lib/rubocop/cop/rspec/factory_bot/create_list.rb +2 -2
  14. data/lib/rubocop/cop/rspec/factory_bot/syntax_methods.rb +1 -19
  15. data/lib/rubocop/cop/rspec/implicit_subject.rb +86 -19
  16. data/lib/rubocop/cop/rspec/let_before_examples.rb +15 -1
  17. data/lib/rubocop/cop/rspec/mixin/capybara_help.rb +80 -0
  18. data/lib/rubocop/cop/rspec/mixin/css_selector.rb +48 -1
  19. data/lib/rubocop/cop/rspec/mixin/skip_or_pending.rb +23 -0
  20. data/lib/rubocop/cop/rspec/no_expectation_example.rb +42 -9
  21. data/lib/rubocop/cop/rspec/pending.rb +2 -11
  22. data/lib/rubocop/cop/rspec/rails/inferred_spec_type.rb +135 -0
  23. data/lib/rubocop/cop/rspec/repeated_include_example.rb +1 -1
  24. data/lib/rubocop/cop/rspec/sort_metadata.rb +102 -0
  25. data/lib/rubocop/cop/rspec/subject_declaration.rb +1 -1
  26. data/lib/rubocop/cop/rspec_cops.rb +5 -0
  27. data/lib/rubocop/rspec/factory_bot/language.rb +20 -0
  28. data/lib/rubocop/rspec/version.rb +1 -1
  29. data/lib/rubocop-rspec.rb +2 -0
  30. metadata +9 -2
@@ -54,25 +54,7 @@ module RuboCop
54
54
 
55
55
  MSG = 'Use `%<method>s` from `FactoryBot::Syntax::Methods`.'
56
56
 
57
- RESTRICT_ON_SEND = %i[
58
- attributes_for
59
- attributes_for_list
60
- attributes_for_pair
61
- build
62
- build_list
63
- build_pair
64
- build_stubbed
65
- build_stubbed_list
66
- build_stubbed_pair
67
- create
68
- create_list
69
- create_pair
70
- generate
71
- generate_list
72
- null
73
- null_list
74
- null_pair
75
- ].to_set.freeze
57
+ RESTRICT_ON_SEND = RuboCop::RSpec::FactoryBot::Language::METHODS
76
58
 
77
59
  def on_send(node)
78
60
  return unless factory_bot?(node.receiver)
@@ -42,12 +42,45 @@ module RuboCop
42
42
  # # good
43
43
  # it { expect(subject).to be_truthy }
44
44
  #
45
+ # @example `EnforcedStyle: require_implicit`
46
+ # # bad
47
+ # it { expect(subject).to be_truthy }
48
+ #
49
+ # # good
50
+ # it { is_expected.to be_truthy }
51
+ #
52
+ # # bad
53
+ # it do
54
+ # expect(subject).to be_truthy
55
+ # end
56
+ #
57
+ # # good
58
+ # it do
59
+ # is_expected.to be_truthy
60
+ # end
61
+ #
62
+ # # good
63
+ # it { expect(named_subject).to be_truthy }
64
+ #
45
65
  class ImplicitSubject < Base
46
66
  extend AutoCorrector
47
67
  include ConfigurableEnforcedStyle
48
68
 
49
- MSG = "Don't use implicit subject."
50
- RESTRICT_ON_SEND = %i[is_expected should should_not].freeze
69
+ MSG_REQUIRE_EXPLICIT = "Don't use implicit subject."
70
+
71
+ MSG_REQUIRE_IMPLICIT = "Don't use explicit subject."
72
+
73
+ RESTRICT_ON_SEND = %i[
74
+ expect
75
+ is_expected
76
+ should
77
+ should_not
78
+ ].freeze
79
+
80
+ # @!method explicit_unnamed_subject?(node)
81
+ def_node_matcher :explicit_unnamed_subject?, <<-PATTERN
82
+ (send nil? :expect (send nil? :subject))
83
+ PATTERN
51
84
 
52
85
  # @!method implicit_subject?(node)
53
86
  def_node_matcher :implicit_subject?, <<-PATTERN
@@ -55,8 +88,7 @@ module RuboCop
55
88
  PATTERN
56
89
 
57
90
  def on_send(node)
58
- return unless implicit_subject?(node)
59
- return if valid_usage?(node)
91
+ return unless invalid?(node)
60
92
 
61
93
  add_offense(node) do |corrector|
62
94
  autocorrect(corrector, node)
@@ -66,32 +98,67 @@ module RuboCop
66
98
  private
67
99
 
68
100
  def autocorrect(corrector, node)
69
- replacement = 'expect(subject)'
70
101
  case node.method_name
102
+ when :expect
103
+ corrector.replace(node, 'is_expected')
104
+ when :is_expected
105
+ corrector.replace(node.location.selector, 'expect(subject)')
71
106
  when :should
72
- replacement += '.to'
107
+ corrector.replace(node.location.selector, 'expect(subject).to')
73
108
  when :should_not
74
- replacement += '.not_to'
109
+ corrector.replace(node.location.selector, 'expect(subject).not_to')
75
110
  end
76
-
77
- corrector.replace(node.loc.selector, replacement)
78
111
  end
79
112
 
80
- def valid_usage?(node)
81
- example = node.ancestors.find { |parent| example?(parent) }
82
- return false if example.nil?
83
-
84
- example.method?(:its) || allowed_by_style?(example)
113
+ def message(_node)
114
+ case style
115
+ when :require_implicit
116
+ MSG_REQUIRE_IMPLICIT
117
+ else
118
+ MSG_REQUIRE_EXPLICIT
119
+ end
85
120
  end
86
121
 
87
- def allowed_by_style?(example)
122
+ def invalid?(node)
88
123
  case style
124
+ when :require_implicit
125
+ explicit_unnamed_subject?(node)
126
+ when :disallow
127
+ implicit_subject_in_non_its?(node)
89
128
  when :single_line_only
90
- example.single_line?
129
+ implicit_subject_in_non_its_and_non_single_line?(node)
91
130
  when :single_statement_only
92
- !example.body.begin_type?
93
- else
94
- false
131
+ implicit_subject_in_non_its_and_non_single_statement?(node)
132
+ end
133
+ end
134
+
135
+ def implicit_subject_in_non_its?(node)
136
+ implicit_subject?(node) && !its?(node)
137
+ end
138
+
139
+ def implicit_subject_in_non_its_and_non_single_line?(node)
140
+ implicit_subject_in_non_its?(node) && !single_line?(node)
141
+ end
142
+
143
+ def implicit_subject_in_non_its_and_non_single_statement?(node)
144
+ implicit_subject_in_non_its?(node) && !single_statement?(node)
145
+ end
146
+
147
+ def its?(node)
148
+ example_of(node)&.method?(:its)
149
+ end
150
+
151
+ def single_line?(node)
152
+ example_of(node)&.single_line?
153
+ end
154
+
155
+ def single_statement?(node)
156
+ !example_of(node)&.body&.begin_type?
157
+ end
158
+
159
+ def example_of(node)
160
+ node.each_ancestor.find do |ancestor|
161
+ example?(ancestor)
95
162
  end
96
163
  end
97
164
  end
@@ -43,6 +43,14 @@ module RuboCop
43
43
  }
44
44
  PATTERN
45
45
 
46
+ # @!method include_examples?(node)
47
+ def_node_matcher :include_examples?, <<~PATTERN
48
+ {
49
+ #{block_pattern(':include_examples')}
50
+ #{send_pattern(':include_examples')}
51
+ }
52
+ PATTERN
53
+
46
54
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
47
55
  return unless example_group_with_body?(node)
48
56
 
@@ -51,6 +59,10 @@ module RuboCop
51
59
 
52
60
  private
53
61
 
62
+ def example_group_with_include_examples?(body)
63
+ body.children.any? { |sibling| include_examples?(sibling) }
64
+ end
65
+
54
66
  def multiline_block?(block)
55
67
  block.begin_type?
56
68
  end
@@ -59,11 +71,13 @@ module RuboCop
59
71
  first_example = find_first_example(node)
60
72
  return unless first_example
61
73
 
74
+ correct = !example_group_with_include_examples?(node)
75
+
62
76
  first_example.right_siblings.each do |sibling|
63
77
  next unless let?(sibling)
64
78
 
65
79
  add_offense(sibling) do |corrector|
66
- autocorrect(corrector, sibling, first_example)
80
+ autocorrect(corrector, sibling, first_example) if correct
67
81
  end
68
82
  end
69
83
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Help methods for capybara.
7
+ module CapybaraHelp
8
+ module_function
9
+
10
+ # @param node [RuboCop::AST::SendNode]
11
+ # @param locator [String]
12
+ # @param element [String]
13
+ # @return [Boolean]
14
+ def specific_option?(node, locator, element)
15
+ attrs = CssSelector.attributes(locator).keys
16
+ return false unless replaceable_element?(node, element, attrs)
17
+
18
+ attrs.all? do |attr|
19
+ CssSelector.specific_options?(element, attr)
20
+ end
21
+ end
22
+
23
+ # @param locator [String]
24
+ # @return [Boolean]
25
+ def specific_pseudo_classes?(locator)
26
+ CssSelector.pseudo_classes(locator).all? do |pseudo_class|
27
+ replaceable_pseudo_class?(pseudo_class, locator)
28
+ end
29
+ end
30
+
31
+ # @param pseudo_class [String]
32
+ # @param locator [String]
33
+ # @return [Boolean]
34
+ def replaceable_pseudo_class?(pseudo_class, locator)
35
+ return false unless CssSelector.specific_pesudo_classes?(pseudo_class)
36
+
37
+ case pseudo_class
38
+ when 'not()' then replaceable_pseudo_class_not?(locator)
39
+ else true
40
+ end
41
+ end
42
+
43
+ # @param locator [String]
44
+ # @return [Boolean]
45
+ def replaceable_pseudo_class_not?(locator)
46
+ locator.scan(/not\(.*?\)/).all? do |negation|
47
+ CssSelector.attributes(negation).values.all? do |v|
48
+ v.is_a?(TrueClass) || v.is_a?(FalseClass)
49
+ end
50
+ end
51
+ end
52
+
53
+ # @param node [RuboCop::AST::SendNode]
54
+ # @param element [String]
55
+ # @param attrs [Array<String>]
56
+ # @return [Boolean]
57
+ def replaceable_element?(node, element, attrs)
58
+ case element
59
+ when 'link' then replaceable_to_link?(node, attrs)
60
+ else true
61
+ end
62
+ end
63
+
64
+ # @param node [RuboCop::AST::SendNode]
65
+ # @param attrs [Array<String>]
66
+ # @return [Boolean]
67
+ def replaceable_to_link?(node, attrs)
68
+ include_option?(node, :href) || attrs.include?('href')
69
+ end
70
+
71
+ # @param node [RuboCop::AST::SendNode]
72
+ # @param option [Symbol]
73
+ # @return [Boolean]
74
+ def include_option?(node, option)
75
+ node.each_descendant(:sym).find { |opt| opt.value == option }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -10,9 +10,56 @@ module RuboCop
10
10
  id class style visible obscured exact exact_text normalize_ws match
11
11
  wait filter_set focused
12
12
  ].freeze
13
+ SPECIFIC_OPTIONS = {
14
+ 'button' => (
15
+ COMMON_OPTIONS + %w[disabled name value title type]
16
+ ).freeze,
17
+ 'link' => (
18
+ COMMON_OPTIONS + %w[href alt title download]
19
+ ).freeze,
20
+ 'table' => (
21
+ COMMON_OPTIONS + %w[
22
+ caption with_cols cols with_rows rows
23
+ ]
24
+ ).freeze,
25
+ 'select' => (
26
+ COMMON_OPTIONS + %w[
27
+ disabled name placeholder options enabled_options
28
+ disabled_options selected with_selected multiple with_options
29
+ ]
30
+ ).freeze,
31
+ 'field' => (
32
+ COMMON_OPTIONS + %w[
33
+ checked unchecked disabled valid name placeholder
34
+ validation_message readonly with type multiple
35
+ ]
36
+ ).freeze
37
+ }.freeze
38
+ SPECIFIC_PSEUDO_CLASSES = %w[
39
+ not() disabled enabled checked unchecked
40
+ ].freeze
13
41
 
14
42
  module_function
15
43
 
44
+ # @param element [String]
45
+ # @param attribute [String]
46
+ # @return [Boolean]
47
+ # @example
48
+ # specific_pesudo_classes?('button', 'name') # => true
49
+ # specific_pesudo_classes?('link', 'invalid') # => false
50
+ def specific_options?(element, attribute)
51
+ SPECIFIC_OPTIONS.fetch(element, []).include?(attribute)
52
+ end
53
+
54
+ # @param pseudo_class [String]
55
+ # @return [Boolean]
56
+ # @example
57
+ # specific_pesudo_classes?('disabled') # => true
58
+ # specific_pesudo_classes?('first-of-type') # => false
59
+ def specific_pesudo_classes?(pseudo_class)
60
+ SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
61
+ end
62
+
16
63
  # @param selector [String]
17
64
  # @return [Boolean]
18
65
  # @example
@@ -75,7 +122,7 @@ module RuboCop
75
122
  # multiple_selectors?('a.cls b#id') # => true
76
123
  # multiple_selectors?('a.cls') # => false
77
124
  def multiple_selectors?(selector)
78
- selector.match?(/[ >,+]/)
125
+ selector.match?(/[ >,+~]/)
79
126
  end
80
127
 
81
128
  # @param value [String]
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Helps check offenses with variable definitions
7
+ module SkipOrPending
8
+ extend RuboCop::NodePattern::Macros
9
+
10
+ # @!method skipped_in_metadata?(node)
11
+ def_node_matcher :skipped_in_metadata?, <<-PATTERN
12
+ {
13
+ (send _ _ <#skip_or_pending? ...>)
14
+ (send _ _ ... (hash <(pair #skip_or_pending? { true str }) ...>))
15
+ }
16
+ PATTERN
17
+
18
+ # @!method skip_or_pending?(node)
19
+ def_node_matcher :skip_or_pending?, '{(sym :skip) (sym :pending)}'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -28,7 +28,37 @@ module RuboCop
28
28
  # expect(a?).to be(true)
29
29
  # end
30
30
  #
31
+ # This cop can be customized with an allowed expectation methods pattern
32
+ # with an `AllowedPatterns` option. ^expect_ and ^assert_ are allowed
33
+ # by default.
34
+ #
35
+ # @example `AllowedPatterns` configuration
36
+ #
37
+ # # .rubocop.yml
38
+ # # RSpec/NoExpectationExample:
39
+ # # AllowedPatterns:
40
+ # # - ^expect_
41
+ # # - ^assert_
42
+ #
43
+ # @example
44
+ # # bad
45
+ # it do
46
+ # not_expect_something
47
+ # end
48
+ #
49
+ # # good
50
+ # it do
51
+ # expect_something
52
+ # end
53
+ #
54
+ # it do
55
+ # assert_something
56
+ # end
57
+ #
31
58
  class NoExpectationExample < Base
59
+ include AllowedPattern
60
+ include SkipOrPending
61
+
32
62
  MSG = 'No expectation found in this example.'
33
63
 
34
64
  # @!method regular_or_focused_example?(node)
@@ -41,26 +71,29 @@ module RuboCop
41
71
  }
42
72
  PATTERN
43
73
 
44
- # @!method including_any_expectation?(node)
74
+ # @!method includes_expectation?(node)
45
75
  # @param [RuboCop::AST::Node] node
46
76
  # @return [Boolean]
47
- def_node_search(
48
- :including_any_expectation?,
49
- send_pattern('#Expectations.all')
50
- )
77
+ def_node_search :includes_expectation?, <<~PATTERN
78
+ {
79
+ #{send_pattern('#Expectations.all')}
80
+ (send nil? `#matches_allowed_pattern?)
81
+ }
82
+ PATTERN
51
83
 
52
- # @!method including_any_skip_example?(node)
84
+ # @!method includes_skip_example?(node)
53
85
  # @param [RuboCop::AST::Node] node
54
86
  # @return [Boolean]
55
- def_node_search :including_any_skip_example?, <<~PATTERN
87
+ def_node_search :includes_skip_example?, <<~PATTERN
56
88
  (send nil? {:pending :skip} ...)
57
89
  PATTERN
58
90
 
59
91
  # @param [RuboCop::AST::BlockNode] node
60
92
  def on_block(node)
61
93
  return unless regular_or_focused_example?(node)
62
- return if including_any_expectation?(node)
63
- return if including_any_skip_example?(node)
94
+ return if includes_expectation?(node)
95
+ return if includes_skip_example?(node)
96
+ return if skipped_in_metadata?(node.send_node)
64
97
 
65
98
  add_offense(node)
66
99
  end
@@ -33,6 +33,8 @@ module RuboCop
33
33
  # end
34
34
  #
35
35
  class Pending < Base
36
+ include SkipOrPending
37
+
36
38
  MSG = 'Pending spec found.'
37
39
 
38
40
  # @!method skippable?(node)
@@ -41,17 +43,6 @@ module RuboCop
41
43
  {#ExampleGroups.regular #Examples.regular}
42
44
  PATTERN
43
45
 
44
- # @!method skipped_in_metadata?(node)
45
- def_node_matcher :skipped_in_metadata?, <<-PATTERN
46
- {
47
- (send _ _ <#skip_or_pending? ...>)
48
- (send _ _ ... (hash <(pair #skip_or_pending? { true str }) ...>))
49
- }
50
- PATTERN
51
-
52
- # @!method skip_or_pending?(node)
53
- def_node_matcher :skip_or_pending?, '{(sym :skip) (sym :pending)}'
54
-
55
46
  # @!method pending_block?(node)
56
47
  def_node_matcher :pending_block?,
57
48
  send_pattern(<<~PATTERN)
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ module Rails
7
+ # Identifies redundant spec type.
8
+ #
9
+ # After setting up rspec-rails, you will have enabled
10
+ # `config.infer_spec_type_from_file_location!` by default in
11
+ # spec/rails_helper.rb. This cop works in conjunction with this config.
12
+ # If you disable this config, disable this cop as well.
13
+ #
14
+ # @safety
15
+ # This cop is marked as unsafe because
16
+ # `config.infer_spec_type_from_file_location!` may not be enabled.
17
+ #
18
+ # @example
19
+ # # bad
20
+ # # spec/models/user_spec.rb
21
+ # RSpec.describe User, type: :model do
22
+ # end
23
+ #
24
+ # # good
25
+ # # spec/models/user_spec.rb
26
+ # RSpec.describe User do
27
+ # end
28
+ #
29
+ # # good
30
+ # # spec/models/user_spec.rb
31
+ # RSpec.describe User, type: :common do
32
+ # end
33
+ #
34
+ # @example `Inferences` configuration
35
+ # # .rubocop.yml
36
+ # # RSpec/InferredSpecType:
37
+ # # Inferences:
38
+ # # services: service
39
+ #
40
+ # # bad
41
+ # # spec/services/user_spec.rb
42
+ # RSpec.describe User, type: :service do
43
+ # end
44
+ #
45
+ # # good
46
+ # # spec/services/user_spec.rb
47
+ # RSpec.describe User do
48
+ # end
49
+ #
50
+ # # good
51
+ # # spec/services/user_spec.rb
52
+ # RSpec.describe User, type: :common do
53
+ # end
54
+ class InferredSpecType < Base
55
+ extend AutoCorrector
56
+
57
+ MSG = 'Remove redundant spec type.'
58
+
59
+ # @param [RuboCop::AST::BlockNode] node
60
+ def on_block(node)
61
+ return unless example_group?(node)
62
+
63
+ pair_node = describe_with_type(node)
64
+ return unless pair_node
65
+ return unless inferred_type?(pair_node)
66
+
67
+ removable_node = detect_removable_node(pair_node)
68
+ add_offense(removable_node) do |corrector|
69
+ autocorrect(corrector, removable_node)
70
+ end
71
+ end
72
+ alias on_numblock on_block
73
+
74
+ private
75
+
76
+ # @!method describe_with_type(node)
77
+ # @param [RuboCop::AST::BlockNode] node
78
+ # @return [RuboCop::AST::PairNode, nil]
79
+ def_node_matcher :describe_with_type, <<~PATTERN
80
+ (block
81
+ (send #rspec? #ExampleGroups.all
82
+ ...
83
+ (hash <$(pair (sym :type) sym) ...>)
84
+ )
85
+ ...
86
+ )
87
+ PATTERN
88
+
89
+ # @param [RuboCop::AST::Corrector] corrector
90
+ # @param [RuboCop::AST::Node] node
91
+ def autocorrect(corrector, node)
92
+ corrector.remove(
93
+ node.location.expression.with(
94
+ begin_pos: node.left_sibling.location.expression.end_pos
95
+ )
96
+ )
97
+ end
98
+
99
+ # @param [RuboCop::AST::PairNode] node
100
+ # @return [RuboCop::AST::Node]
101
+ def detect_removable_node(node)
102
+ if node.parent.pairs.size == 1
103
+ node.parent
104
+ else
105
+ node
106
+ end
107
+ end
108
+
109
+ # @return [String]
110
+ def file_path
111
+ processed_source.file_path
112
+ end
113
+
114
+ # @param [RuboCop::AST::PairNode] node
115
+ # @return [Boolean]
116
+ def inferred_type?(node)
117
+ inferred_type_from_file_path.inspect == node.value.source
118
+ end
119
+
120
+ # @return [Symbol, nil]
121
+ def inferred_type_from_file_path
122
+ inferences.find do |prefix, type|
123
+ break type.to_sym if file_path.include?("spec/#{prefix}/")
124
+ end
125
+ end
126
+
127
+ # @return [Hash]
128
+ def inferences
129
+ cop_config['Inferences'] || {}
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -47,7 +47,7 @@ module RuboCop
47
47
  #
48
48
  class RepeatedIncludeExample < Base
49
49
  MSG = 'Repeated include of shared_examples %<name>s ' \
50
- 'on line(s) %<repeat>s'
50
+ 'on line(s) %<repeat>s'
51
51
 
52
52
  # @!method several_include_examples?(node)
53
53
  def_node_matcher :several_include_examples?, <<-PATTERN