rubocop-rspec 2.4.0 → 2.8.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.
@@ -6,13 +6,13 @@ module RuboCop
6
6
  module Capybara
7
7
  # Checks that no expectations are set on Capybara's `current_path`.
8
8
  #
9
- # The `have_current_path` matcher (https://www.rubydoc.info/github/
10
- # teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-
11
- # instance_method) should be used on `page` to set expectations on
12
- # Capybara's current path, since it uses Capybara's waiting
13
- # functionality (https://github.com/teamcapybara/capybara/blob/master/
14
- # README.md#asynchronous-javascript-ajax-and-friends) which ensures that
15
- # preceding actions (like `click_link`) have completed.
9
+ # The
10
+ # https://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-instance_method[`have_current_path` matcher]
11
+ # should be used on `page` to set expectations on Capybara's
12
+ # current path, since it uses
13
+ # https://github.com/teamcapybara/capybara/blob/master/README.md#asynchronous-javascript-ajax-and-friends[Capybara's waiting functionality]
14
+ # which ensures that preceding actions (like `click_link`) have
15
+ # completed.
16
16
  #
17
17
  # @example
18
18
  # # bad
@@ -42,6 +42,7 @@ module RuboCop
42
42
  # end
43
43
  class FeatureMethods < Base
44
44
  extend AutoCorrector
45
+ include InsideExampleGroup
45
46
 
46
47
  MSG = 'Use `%<replacement>s` instead of `%<method>s`.'
47
48
 
@@ -60,13 +61,6 @@ module RuboCop
60
61
  {#{MAP.keys.map(&:inspect).join(' ')}}
61
62
  PATTERN
62
63
 
63
- # @!method spec?(node)
64
- def_node_matcher :spec?, <<-PATTERN
65
- (block
66
- (send #rspec? {:describe :feature} ...)
67
- ...)
68
- PATTERN
69
-
70
64
  # @!method feature_method(node)
71
65
  def_node_matcher :feature_method, <<-PATTERN
72
66
  (block
@@ -75,7 +69,7 @@ module RuboCop
75
69
  PATTERN
76
70
 
77
71
  def on_block(node)
78
- return unless inside_spec?(node)
72
+ return unless inside_example_group?(node)
79
73
 
80
74
  feature_method(node) do |send_node, match|
81
75
  next if enabled?(match)
@@ -93,21 +87,6 @@ module RuboCop
93
87
 
94
88
  private
95
89
 
96
- def inside_spec?(node)
97
- return spec?(node) if root_node?(node)
98
-
99
- root = node.ancestors.find { |parent| root_node?(parent) }
100
- spec?(root)
101
- end
102
-
103
- def root_node?(node)
104
- node.parent.nil? || root_with_siblings?(node.parent)
105
- end
106
-
107
- def root_with_siblings?(node)
108
- node.begin_type? && node.parent.nil?
109
- end
110
-
111
90
  def enabled?(method_name)
112
91
  enabled_methods.include?(method_name)
113
92
  end
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  module Capybara
7
- # Checks for boolean visibility in capybara finders.
7
+ # Checks for boolean visibility in Capybara finders.
8
8
  #
9
9
  # Capybara lets you find elements that match a certain visibility using
10
10
  # the `:visible` option. `:visible` accepts both boolean and symbols as
@@ -12,7 +12,8 @@ module RuboCop
12
12
  # false` does not find just invisible elements, but both visible and
13
13
  # invisible elements. For expressiveness and clarity, use one of the
14
14
  # symbol values, `:all`, `:hidden` or `:visible`.
15
- # (https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all)
15
+ # Read more in
16
+ # https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all[the documentation].
16
17
  #
17
18
  # @example
18
19
  #
@@ -36,7 +36,10 @@ module RuboCop
36
36
  def on_block(node)
37
37
  empty_hook?(node) do |hook|
38
38
  add_offense(hook) do |corrector|
39
- range = range_with_surrounding_space(range: node.loc.expression)
39
+ range = range_with_surrounding_space(
40
+ range: node.loc.expression,
41
+ side: :left
42
+ )
40
43
  corrector.remove(range)
41
44
  end
42
45
  end
@@ -17,24 +17,18 @@ module RuboCop
17
17
  class EmptyLineAfterSubject < Base
18
18
  extend AutoCorrector
19
19
  include EmptyLineSeparation
20
+ include InsideExampleGroup
20
21
 
21
22
  MSG = 'Add an empty line after `%<subject>s`.'
22
23
 
23
24
  def on_block(node)
24
- return unless subject?(node) && !in_spec_block?(node)
25
+ return unless subject?(node)
26
+ return unless inside_example_group?(node)
25
27
 
26
28
  missing_separating_line_offense(node) do |method|
27
29
  format(MSG, subject: method)
28
30
  end
29
31
  end
30
-
31
- private
32
-
33
- def in_spec_block?(node)
34
- node.each_ancestor(:block).any? do |ancestor|
35
- Examples.all(ancestor.method_name)
36
- end
37
- end
38
32
  end
39
33
  end
40
34
  end
@@ -60,7 +60,10 @@ module RuboCop
60
60
 
61
61
  def add_wording_offense(node, message)
62
62
  docstring = docstring(node)
63
+
63
64
  add_offense(docstring, message: message) do |corrector|
65
+ next if node.heredoc?
66
+
64
67
  corrector.replace(docstring, replacement_text(node))
65
68
  end
66
69
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for excessive whitespace in example descriptions.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # it ' has excessive spacing ' do
11
+ # end
12
+ #
13
+ # # good
14
+ # it 'has excessive spacing' do
15
+ # end
16
+ #
17
+ # @example
18
+ # # bad
19
+ # context ' when a condition is met ' do
20
+ # end
21
+ #
22
+ # # good
23
+ # context 'when a condition is met' do
24
+ # end
25
+ class ExcessiveDocstringSpacing < Base
26
+ extend AutoCorrector
27
+
28
+ MSG = 'Excessive whitespace.'
29
+
30
+ # @!method example_description(node)
31
+ def_node_matcher :example_description, <<-PATTERN
32
+ (send _ {#Examples.all #ExampleGroups.all} ${
33
+ $str
34
+ $(dstr ({str dstr `sym} ...) ...)
35
+ } ...)
36
+ PATTERN
37
+
38
+ def on_send(node)
39
+ example_description(node) do |description_node, message|
40
+ return if description_node.heredoc?
41
+
42
+ text = text(message)
43
+
44
+ return unless excessive_whitespace?(text)
45
+
46
+ add_whitespace_offense(description_node, text)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ # @param text [String]
53
+ def excessive_whitespace?(text)
54
+ return true if text.start_with?(' ') || text.end_with?(' ')
55
+
56
+ text.match?(/[^\n ] +[^ ]/)
57
+ end
58
+
59
+ # @param text [String]
60
+ def strip_excessive_whitespace(text)
61
+ text.strip.gsub(/ +/, ' ')
62
+ end
63
+
64
+ # @param node [RuboCop::AST::Node]
65
+ # @param text [String]
66
+ def add_whitespace_offense(node, text)
67
+ docstring = docstring(node)
68
+ corrected = strip_excessive_whitespace(text)
69
+
70
+ add_offense(docstring) do |corrector|
71
+ corrector.replace(docstring, corrected)
72
+ end
73
+ end
74
+
75
+ def docstring(node)
76
+ expr = node.loc.expression
77
+
78
+ Parser::Source::Range.new(
79
+ expr.source_buffer,
80
+ expr.begin_pos + 1,
81
+ expr.end_pos - 1
82
+ )
83
+ end
84
+
85
+ # Recursive processing is required to process nested dstr nodes
86
+ # that is the case for \-separated multiline strings with interpolation.
87
+ def text(node)
88
+ case node.type
89
+ when :dstr
90
+ node.node_parts.map { |child_node| text(child_node) }.join
91
+ when :str, :sym
92
+ node.value
93
+ when :begin
94
+ node.source
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -27,6 +27,7 @@ module RuboCop
27
27
  class CreateList < Base
28
28
  extend AutoCorrector
29
29
  include ConfigurableEnforcedStyle
30
+ include RuboCop::RSpec::FactoryBot::Language
30
31
 
31
32
  MSG_CREATE_LIST = 'Prefer create_list.'
32
33
  MSG_N_TIMES = 'Prefer %<number>s.times.'
@@ -43,12 +44,12 @@ module RuboCop
43
44
 
44
45
  # @!method factory_call(node)
45
46
  def_node_matcher :factory_call, <<-PATTERN
46
- (send ${(const nil? {:FactoryGirl :FactoryBot}) nil?} :create (sym $_) $...)
47
+ (send ${nil? #factory_bot?} :create (sym $_) $...)
47
48
  PATTERN
48
49
 
49
50
  # @!method factory_list_call(node)
50
51
  def_node_matcher :factory_list_call, <<-PATTERN
51
- (send {(const nil? {:FactoryGirl :FactoryBot}) nil?} :create_list (sym _) (int $_) ...)
52
+ (send {nil? #factory_bot?} :create_list (sym _) (int $_) ...)
52
53
  PATTERN
53
54
 
54
55
  def on_block(node)
@@ -160,7 +161,7 @@ module RuboCop
160
161
  def call_with_block_replacement(node)
161
162
  block = node.body
162
163
  arguments = build_arguments(block, node.receiver.source)
163
- replacement = format_receiver(block.send_node.receiver)
164
+ replacement = format_receiver(block.receiver)
164
165
  replacement += format_method_call(block, 'create_list', arguments)
165
166
  replacement += format_block(block)
166
167
  replacement
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ module FactoryBot
7
+ # Use shorthands from `FactoryBot::Syntax::Methods` in your specs.
8
+ #
9
+ # @safety
10
+ # The auto-correction is marked as unsafe because the cop
11
+ # cannot verify whether you already include
12
+ # `FactoryBot::Syntax::Methods` in your test suite.
13
+ #
14
+ # If you're using Rails, add the following configuration to
15
+ # `spec/support/factory_bot.rb` and be sure to require that file in
16
+ # `rails_helper.rb`:
17
+ #
18
+ # [source,ruby]
19
+ # ----
20
+ # RSpec.configure do |config|
21
+ # config.include FactoryBot::Syntax::Methods
22
+ # end
23
+ # ----
24
+ #
25
+ # If you're not using Rails:
26
+ #
27
+ # [source,ruby]
28
+ # ----
29
+ # RSpec.configure do |config|
30
+ # config.include FactoryBot::Syntax::Methods
31
+ #
32
+ # config.before(:suite) do
33
+ # FactoryBot.find_definitions
34
+ # end
35
+ # end
36
+ # ----
37
+ #
38
+ # @example
39
+ # # bad
40
+ # FactoryBot.create(:bar)
41
+ # FactoryBot.build(:bar)
42
+ # FactoryBot.attributes_for(:bar)
43
+ #
44
+ # # good
45
+ # create(:bar)
46
+ # build(:bar)
47
+ # attributes_for(:bar)
48
+ #
49
+ class SyntaxMethods < Base
50
+ extend AutoCorrector
51
+ include InsideExampleGroup
52
+ include RangeHelp
53
+ include RuboCop::RSpec::FactoryBot::Language
54
+
55
+ MSG = 'Use `%<method>s` from `FactoryBot::Syntax::Methods`.'
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
76
+
77
+ def on_send(node)
78
+ return unless factory_bot?(node.receiver)
79
+ return unless inside_example_group?(node)
80
+
81
+ message = format(MSG, method: node.method_name)
82
+
83
+ add_offense(crime_scene(node), message: message) do |corrector|
84
+ corrector.remove(offense(node))
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def crime_scene(node)
91
+ range_between(
92
+ node.loc.expression.begin_pos,
93
+ node.loc.selector.end_pos
94
+ )
95
+ end
96
+
97
+ def offense(node)
98
+ range_between(
99
+ node.loc.expression.begin_pos,
100
+ node.loc.selector.begin_pos
101
+ )
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -32,7 +32,7 @@ module RuboCop
32
32
  # # ...
33
33
  # end
34
34
  #
35
- # # good
35
+ # # bad
36
36
  # before do
37
37
  # # ...
38
38
  # end
@@ -48,6 +48,8 @@ module RuboCop
48
48
  end
49
49
 
50
50
  def only_expectations?(body, arg)
51
+ return false unless body.each_child_node.any?
52
+
51
53
  body.each_child_node.all? { |child| expectation?(child, arg) }
52
54
  end
53
55
  end
@@ -33,11 +33,13 @@ module RuboCop
33
33
  #
34
34
  class LeadingSubject < Base
35
35
  extend AutoCorrector
36
+ include InsideExampleGroup
36
37
 
37
38
  MSG = 'Declare `subject` above any other `%<offending>s` declarations.'
38
39
 
39
40
  def on_block(node)
40
- return unless subject?(node) && !in_spec_block?(node)
41
+ return unless subject?(node)
42
+ return unless inside_example_group?(node)
41
43
 
42
44
  check_previous_nodes(node)
43
45
  end
@@ -78,12 +80,6 @@ module RuboCop
78
80
  spec_group?(node) ||
79
81
  include?(node)
80
82
  end
81
-
82
- def in_spec_block?(node)
83
- node.each_ancestor(:block).any? do |ancestor|
84
- example?(ancestor)
85
- end
86
- end
87
83
  end
88
84
  end
89
85
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Helps you identify whether a given node
7
+ # is within an example group or not.
8
+ module InsideExampleGroup
9
+ private
10
+
11
+ def inside_example_group?(node)
12
+ return spec_group?(node) if example_group_root?(node)
13
+
14
+ root = node.ancestors.find { |parent| example_group_root?(parent) }
15
+
16
+ spec_group?(root)
17
+ end
18
+
19
+ def example_group_root?(node)
20
+ node.parent.nil? || example_group_root_with_siblings?(node.parent)
21
+ end
22
+
23
+ def example_group_root_with_siblings?(node)
24
+ node.begin_type? && node.parent.nil?
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -6,7 +6,7 @@ module RuboCop
6
6
  # Checks for nested example groups.
7
7
  #
8
8
  # This cop is configurable using the `Max` option
9
- # and supports `--auto-gen-config
9
+ # and supports `--auto-gen-config`.
10
10
  #
11
11
  # @example
12
12
  # # bad
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Ensure that subject is defined using subject helper.
7
+ #
8
+ # @example
9
+ #
10
+ # # bad
11
+ # let(:subject) { foo }
12
+ # let!(:subject) { foo }
13
+ # subject(:subject) { foo }
14
+ # subject!(:subject) { foo }
15
+ #
16
+ # # bad
17
+ # block = -> {}
18
+ # let(:subject, &block)
19
+ #
20
+ # # good
21
+ # subject(:test_subject) { foo }
22
+ #
23
+ class SubjectDeclaration < Base
24
+ MSG_LET = 'Use subject explicitly rather than using let'
25
+ MSG_REDUNDANT = 'Ambiguous declaration of subject'
26
+
27
+ # @!method offensive_subject_declaration?(node)
28
+ def_node_matcher :offensive_subject_declaration?, <<~PATTERN
29
+ (send nil? ${#Subjects.all #Helpers.all} {(sym :subject) (str "subject")} ...)
30
+ PATTERN
31
+
32
+ def on_send(node)
33
+ offense = offensive_subject_declaration?(node)
34
+ return unless offense
35
+
36
+ add_offense(node, message: message_for(offense))
37
+ end
38
+
39
+ private
40
+
41
+ def message_for(offense)
42
+ Helpers.all(offense) ? MSG_LET : MSG_REDUNDANT
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -7,6 +7,9 @@ module RuboCop
7
7
  module RSpec
8
8
  # Checks for stubbed test subjects.
9
9
  #
10
+ # Checks nested subject stubs for innermost subject definition
11
+ # when subject is also defined in parent example groups.
12
+ #
10
13
  # @see https://robots.thoughtbot.com/don-t-stub-the-system-under-test
11
14
  # @see https://samphippen.com/introducing-rspec-smells-and-where-to-find-them#smell-1-stubject
12
15
  # @see https://github.com/rubocop-hq/rspec-style-guide#dont-stub-subject
@@ -22,6 +25,20 @@ module RuboCop
22
25
  # end
23
26
  # end
24
27
  #
28
+ # # bad
29
+ # describe Article do
30
+ # subject(:foo) { Article.new }
31
+ #
32
+ # context 'nested subject' do
33
+ # subject(:article) { Article.new }
34
+ #
35
+ # it 'indicates that the author is unknown' do
36
+ # allow(article).to receive(:author).and_return(nil)
37
+ # expect(article.description).to include('by an unknown author')
38
+ # end
39
+ # end
40
+ # end
41
+ #
25
42
  # # good
26
43
  # describe Article do
27
44
  # subject(:article) { Article.new(author: nil) }
@@ -36,27 +53,35 @@ module RuboCop
36
53
 
37
54
  MSG = 'Do not stub methods of the object under test.'
38
55
 
39
- # @!method subject(node)
56
+ # @!method subject?(node)
40
57
  # Find a named or unnamed subject definition
41
58
  #
42
59
  # @example anonymous subject
43
- # subject(parse('subject { foo }').ast) do |name|
60
+ # subject?(parse('subject { foo }').ast) do |name|
44
61
  # name # => :subject
45
62
  # end
46
63
  #
47
64
  # @example named subject
48
- # subject(parse('subject(:thing) { foo }').ast) do |name|
65
+ # subject?(parse('subject(:thing) { foo }').ast) do |name|
49
66
  # name # => :thing
50
67
  # end
51
68
  #
52
69
  # @param node [RuboCop::AST::Node]
53
70
  #
54
71
  # @yield [Symbol] subject name
55
- def_node_matcher :subject, <<-PATTERN
56
- (block
57
- (send nil?
58
- {:subject (sym $_) | $:subject}
59
- ) args ...)
72
+ def_node_matcher :subject?, <<-PATTERN
73
+ (block
74
+ (send nil?
75
+ {:subject (sym $_) | $:subject}
76
+ ) args ...)
77
+ PATTERN
78
+
79
+ # @!method let?(node)
80
+ # Find a memoized helper
81
+ def_node_matcher :let?, <<-PATTERN
82
+ (block
83
+ (send nil? :let (sym $_)
84
+ ) args ...)
60
85
  PATTERN
61
86
 
62
87
  # @!method message_expectation?(node, method_name)
@@ -73,7 +98,7 @@ module RuboCop
73
98
  def_node_matcher :message_expectation?, <<-PATTERN
74
99
  (send
75
100
  {
76
- (send nil? { :expect :allow } (send nil? {% :subject}))
101
+ (send nil? { :expect :allow } (send nil? %))
77
102
  (send nil? :is_expected)
78
103
  }
79
104
  #Runners.all
@@ -89,7 +114,8 @@ module RuboCop
89
114
  PATTERN
90
115
 
91
116
  def on_top_level_group(node)
92
- @explicit_subjects = find_all_explicit_subjects(node)
117
+ @explicit_subjects = find_all_explicit(node, &method(:subject?))
118
+ @subject_overrides = find_all_explicit(node, &method(:let?))
93
119
 
94
120
  find_subject_expectations(node) do |stub|
95
121
  add_offense(stub)
@@ -98,12 +124,12 @@ module RuboCop
98
124
 
99
125
  private
100
126
 
101
- def find_all_explicit_subjects(node)
127
+ def find_all_explicit(node)
102
128
  node.each_descendant(:block).with_object({}) do |child, h|
103
- name = subject(child)
129
+ name = yield(child)
104
130
  next unless name
105
131
 
106
- outer_example_group = child.each_ancestor.find do |a|
132
+ outer_example_group = child.each_ancestor(:block).find do |a|
107
133
  example_group?(a)
108
134
  end
109
135
 
@@ -113,14 +139,14 @@ module RuboCop
113
139
  end
114
140
 
115
141
  def find_subject_expectations(node, subject_names = [], &block)
116
- subject_names = @explicit_subjects[node] if @explicit_subjects[node]
142
+ subject_names = [*subject_names, *@explicit_subjects[node]]
143
+ subject_names -= @subject_overrides[node] if @subject_overrides[node]
117
144
 
118
- expectation_detected = (subject_names + [:subject]).any? do |name|
119
- message_expectation?(node, name)
120
- end
145
+ names = Set[*subject_names, :subject]
146
+ expectation_detected = message_expectation?(node, names)
121
147
  return yield(node) if expectation_detected
122
148
 
123
- node.each_child_node do |child|
149
+ node.each_child_node(:send, :def, :block, :begin) do |child|
124
150
  find_subject_expectations(child, subject_names, &block)
125
151
  end
126
152
  end