rubocop-rspec 2.13.2 → 2.14.0

Sign up to get free protection for your applications and to get access to all the features.
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