rubocop-rspec 2.13.1 → 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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +405 -379
  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_finders.rb +9 -2
  9. data/lib/rubocop/cop/rspec/capybara/specific_matcher.rb +5 -82
  10. data/lib/rubocop/cop/rspec/capybara/visibility_matcher.rb +14 -14
  11. data/lib/rubocop/cop/rspec/change_by_zero.rb +1 -1
  12. data/lib/rubocop/cop/rspec/context_wording.rb +4 -2
  13. data/lib/rubocop/cop/rspec/example_wording.rb +32 -0
  14. data/lib/rubocop/cop/rspec/factory_bot/consistent_parentheses_style.rb +99 -0
  15. data/lib/rubocop/cop/rspec/factory_bot/create_list.rb +2 -2
  16. data/lib/rubocop/cop/rspec/factory_bot/syntax_methods.rb +1 -19
  17. data/lib/rubocop/cop/rspec/implicit_subject.rb +86 -19
  18. data/lib/rubocop/cop/rspec/let_before_examples.rb +15 -1
  19. data/lib/rubocop/cop/rspec/mixin/capybara_help.rb +80 -0
  20. data/lib/rubocop/cop/rspec/mixin/css_selector.rb +48 -1
  21. data/lib/rubocop/cop/rspec/mixin/skip_or_pending.rb +23 -0
  22. data/lib/rubocop/cop/rspec/no_expectation_example.rb +47 -6
  23. data/lib/rubocop/cop/rspec/pending.rb +2 -11
  24. data/lib/rubocop/cop/rspec/rails/inferred_spec_type.rb +135 -0
  25. data/lib/rubocop/cop/rspec/repeated_include_example.rb +1 -1
  26. data/lib/rubocop/cop/rspec/sort_metadata.rb +102 -0
  27. data/lib/rubocop/cop/rspec/subject_declaration.rb +1 -1
  28. data/lib/rubocop/cop/rspec_cops.rb +5 -0
  29. data/lib/rubocop/rspec/factory_bot/language.rb +20 -0
  30. data/lib/rubocop/rspec/version.rb +1 -1
  31. data/lib/rubocop-rspec.rb +2 -0
  32. metadata +9 -2
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ module FactoryBot
7
+ # Use a consistent style for parentheses in factory bot calls.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # create :user
13
+ # build(:user)
14
+ # create(:login)
15
+ # create :login
16
+ #
17
+ # @example `EnforcedStyle: require_parentheses` (default)
18
+ #
19
+ # # good
20
+ # create(:user)
21
+ # create(:user)
22
+ # create(:login)
23
+ # build(:login)
24
+ #
25
+ # @example `EnforcedStyle: omit_parentheses`
26
+ #
27
+ # # good
28
+ # create :user
29
+ # build :user
30
+ # create :login
31
+ # create :login
32
+ #
33
+ class ConsistentParenthesesStyle < Base
34
+ extend AutoCorrector
35
+ include ConfigurableEnforcedStyle
36
+ include RuboCop::RSpec::FactoryBot::Language
37
+ include RuboCop::Cop::Util
38
+
39
+ def self.autocorrect_incompatible_with
40
+ [Style::MethodCallWithArgsParentheses]
41
+ end
42
+
43
+ MSG_REQUIRE_PARENS = 'Prefer method call with parentheses'
44
+ MSG_OMIT_PARENS = 'Prefer method call without parentheses'
45
+
46
+ FACTORY_CALLS = RuboCop::RSpec::FactoryBot::Language::METHODS
47
+
48
+ # @!method factory_call(node)
49
+ def_node_matcher :factory_call, <<-PATTERN
50
+ (send
51
+ ${#factory_bot? nil?} %FACTORY_CALLS
52
+ $...)
53
+ PATTERN
54
+
55
+ def on_send(node)
56
+ return if nested_call?(node) # prevent from nested matching
57
+
58
+ factory_call(node) do
59
+ if node.parenthesized?
60
+ process_with_parentheses(node)
61
+ else
62
+ process_without_parentheses(node)
63
+ end
64
+ end
65
+ end
66
+
67
+ def process_with_parentheses(node)
68
+ return unless style == :omit_parentheses
69
+
70
+ add_offense(node.loc.selector,
71
+ message: MSG_OMIT_PARENS) do |corrector|
72
+ remove_parentheses(corrector, node)
73
+ end
74
+ end
75
+
76
+ def process_without_parentheses(node)
77
+ return unless style == :require_parentheses
78
+
79
+ add_offense(node.loc.selector,
80
+ message: MSG_REQUIRE_PARENS) do |corrector|
81
+ add_parentheses(node, corrector)
82
+ end
83
+ end
84
+
85
+ def nested_call?(node)
86
+ node.parent&.send_type?
87
+ end
88
+
89
+ private
90
+
91
+ def remove_parentheses(corrector, node)
92
+ corrector.replace(node.location.begin, ' ')
93
+ corrector.remove(node.location.end)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -238,8 +238,8 @@ module RuboCop
238
238
  indent = ' ' * node.body.loc.column
239
239
  indent_end = ' ' * node.parent.loc.column
240
240
  " do #{node.arguments.source}\n" \
241
- "#{indent}#{node.body.source}\n" \
242
- "#{indent_end}end"
241
+ "#{indent}#{node.body.source}\n" \
242
+ "#{indent_end}end"
243
243
  end
244
244
 
245
245
  def format_singleline_block(node)
@@ -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,18 +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
83
+
84
+ # @!method includes_skip_example?(node)
85
+ # @param [RuboCop::AST::Node] node
86
+ # @return [Boolean]
87
+ def_node_search :includes_skip_example?, <<~PATTERN
88
+ (send nil? {:pending :skip} ...)
89
+ PATTERN
51
90
 
52
91
  # @param [RuboCop::AST::BlockNode] node
53
92
  def on_block(node)
54
93
  return unless regular_or_focused_example?(node)
55
- return if including_any_expectation?(node)
94
+ return if includes_expectation?(node)
95
+ return if includes_skip_example?(node)
96
+ return if skipped_in_metadata?(node.send_node)
56
97
 
57
98
  add_offense(node)
58
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)