rubocop-rspec 2.13.1 → 2.14.0

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