rubocop-rspec 1.9.1 → 1.10.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/Gemfile +2 -1
  4. data/config/default.yml +13 -1
  5. data/lib/rubocop-rspec.rb +5 -0
  6. data/lib/rubocop/cop/rspec/any_instance.rb +1 -1
  7. data/lib/rubocop/cop/rspec/cop.rb +1 -2
  8. data/lib/rubocop/cop/rspec/described_class.rb +1 -1
  9. data/lib/rubocop/cop/rspec/expect_output.rb +52 -0
  10. data/lib/rubocop/cop/rspec/implicit_expect.rb +3 -2
  11. data/lib/rubocop/cop/rspec/message_chain.rb +1 -1
  12. data/lib/rubocop/cop/rspec/message_spies.rb +2 -2
  13. data/lib/rubocop/cop/rspec/multiple_expectations.rb +2 -4
  14. data/lib/rubocop/cop/rspec/named_subject.rb +1 -1
  15. data/lib/rubocop/cop/rspec/nested_groups.rb +16 -1
  16. data/lib/rubocop/cop/rspec/repeated_example.rb +41 -0
  17. data/lib/rubocop/cop/rspec/scattered_setup.rb +49 -0
  18. data/lib/rubocop/cop/rspec/single_argument_message_chain.rb +17 -5
  19. data/lib/rubocop/rspec.rb +1 -1
  20. data/lib/rubocop/rspec/concept.rb +33 -0
  21. data/lib/rubocop/rspec/example.rb +1 -25
  22. data/lib/rubocop/rspec/example_group.rb +28 -12
  23. data/lib/rubocop/rspec/hook.rb +49 -0
  24. data/lib/rubocop/rspec/language/node_pattern.rb +1 -0
  25. data/lib/rubocop/rspec/version.rb +1 -1
  26. data/spec/rubocop/cop/rspec/cop_spec.rb +4 -1
  27. data/spec/rubocop/cop/rspec/expect_output_spec.rb +62 -0
  28. data/spec/rubocop/cop/rspec/message_spies_spec.rb +74 -2
  29. data/spec/rubocop/cop/rspec/multiple_expectations_spec.rb +2 -2
  30. data/spec/rubocop/cop/rspec/nested_groups_spec.rb +14 -2
  31. data/spec/rubocop/cop/rspec/repeated_example_spec.rb +65 -0
  32. data/spec/rubocop/cop/rspec/scattered_setup_spec.rb +96 -0
  33. data/spec/rubocop/cop/rspec/single_argument_message_chain_spec.rb +27 -3
  34. data/spec/rubocop/rspec/description_extractor_spec.rb +1 -1
  35. data/spec/rubocop/rspec/example_group_spec.rb +1 -1
  36. data/spec/rubocop/rspec/example_spec.rb +2 -2
  37. data/spec/rubocop/rspec/hook_spec.rb +53 -0
  38. metadata +15 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7161e38d3d46174e4acad35bf1ab6b7fdd8d62d4
4
- data.tar.gz: d9b7e70692513359abc3501795939c31075bd3dc
3
+ metadata.gz: afa52f47ad386fe20a2a338ff9da7c6a032c385b
4
+ data.tar.gz: 07143368c4d5e201d12bfba8379e5c2c79bcba99
5
5
  SHA512:
6
- metadata.gz: 7d57d6b57e5333d78635bcc4282ece219f7c2ebc0b7ed394e65da9a4167861752ec69bc4e588038737c21af69b1cfe86959d82fef02cce16e9dccd2ef9718fd0
7
- data.tar.gz: d961bdccd80bab88bb0c04f00c2e27003e7203aa212a0ec12f8403455e522d8b85b7171e0fd27e495dd524569df2246f3590dd660db7974997f052160f51c91b
6
+ metadata.gz: 89dfa9c530533694d03d670112ae870f9ba626875bcff32bb9ab4c708f393e99e1e4eb5562adb5ad2e8231c5366594fabbd431a8ec758ec856491fa747f84b66
7
+ data.tar.gz: 9517b60ae000d18800f11bb8e579d5889883eff511975ce4ff77a56e72cc0450e9f998b93694c3c90d9e47c9546aebb0fe8296fdbcfa4048e16cfddfc7fda5b1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Master (unreleased)
4
4
 
5
+ ## 1.10.0 (2017-01-15)
6
+
7
+ * Fix false negative for `RSpec/MessageSpies` cop. ([@onk][])
8
+ * Fix internal dependencies on RuboCop to be compatible with 0.47 release. ([@backus][])
9
+ * Add autocorrect support for `SingleArgumentMessageChain` cop. ([@bquorning][])
10
+ * Rename `NestedGroups`' configuration key from `MaxNesting` to `Max` in order to be consistent with other cop configuration. ([@backus][])
11
+ * Add `RepeatedExample` cop for detecting repeated examples within example groups. ([@backus][])
12
+ * Add `ScatteredSetup` cop for enforcing that only one `before`, `around`, and `after` hook are used per example group scope. ([@backus][])
13
+ * Add `ExpectOutput` cop for recommending `expect { ... }.to output(...).to_stdout`. ([@backus][])
14
+
5
15
  ## 1.9.1 (2017-01-02)
6
16
 
7
17
  * Fix unintentional regression change in `NestedGroups` reported in #270. ([@backus][])
@@ -161,3 +171,4 @@
161
171
  [@baberthal]: https://github.com/baberthal
162
172
  [@jeffreyc]: https://github.com/jeffreyc
163
173
  [@clupprich]: https://github.com/clupprich
174
+ [@onk]: https://github.com/onk
data/Gemfile CHANGED
@@ -4,11 +4,12 @@ gemspec
4
4
 
5
5
  group :test do
6
6
  gem 'codeclimate-test-reporter', '~> 1.0.0'
7
+ gem 'rubocop', '~> 0.47'
7
8
  gem 'simplecov', '~> 0.12.0', require: false
8
9
  end
9
10
 
10
11
  local_gemfile = 'Gemfile.local'
11
12
 
12
13
  if File.exist?(local_gemfile)
13
- eval(File.read(local_gemfile)) # rubocop:disable Lint/Eval
14
+ eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
14
15
  end
data/config/default.yml CHANGED
@@ -49,6 +49,10 @@ RSpec/ExpectActual:
49
49
  Description: Checks for `expect(...)` calls containing literal values.
50
50
  Enabled: true
51
51
 
52
+ RSpec/ExpectOutput:
53
+ Description: Checks for opportunities to use `expect { ... }.to output`.
54
+ Enabled: true
55
+
52
56
  RSpec/FilePath:
53
57
  Description: Checks that spec file paths are consistent with the test subject.
54
58
  Enabled: true
@@ -127,7 +131,7 @@ RSpec/NamedSubject:
127
131
  RSpec/NestedGroups:
128
132
  Description: Checks for nested example groups.
129
133
  Enabled: true
130
- MaxNesting: 3
134
+ Max: 3
131
135
 
132
136
  RSpec/NotToNot:
133
137
  Description: Checks for consistent method usage for negating expectations.
@@ -141,10 +145,18 @@ RSpec/RepeatedDescription:
141
145
  Enabled: true
142
146
  Description: Check for repeated description strings in example groups.
143
147
 
148
+ RSpec/RepeatedExample:
149
+ Enabled: true
150
+ Description: Check for repeated examples within example groups.
151
+
144
152
  RSpec/SingleArgumentMessageChain:
145
153
  Description: Checks that chains of messages contain more than one element.
146
154
  Enabled: true
147
155
 
156
+ RSpec/ScatteredSetup:
157
+ Description: Checks for setup scattered across multiple hooks in an example group.
158
+ Enabled: true
159
+
148
160
  RSpec/SubjectStub:
149
161
  Description: Checks for stubbed test subjects.
150
162
  Enabled: true
data/lib/rubocop-rspec.rb CHANGED
@@ -11,8 +11,10 @@ require 'rubocop/rspec/wording'
11
11
  require 'rubocop/rspec/util'
12
12
  require 'rubocop/rspec/language'
13
13
  require 'rubocop/rspec/language/node_pattern'
14
+ require 'rubocop/rspec/concept'
14
15
  require 'rubocop/rspec/example_group'
15
16
  require 'rubocop/rspec/example'
17
+ require 'rubocop/rspec/hook'
16
18
  require 'rubocop/cop/rspec/cop'
17
19
 
18
20
  RuboCop::RSpec::Inject.defaults!
@@ -27,6 +29,7 @@ require 'rubocop/cop/rspec/empty_example_group'
27
29
  require 'rubocop/cop/rspec/example_length'
28
30
  require 'rubocop/cop/rspec/example_wording'
29
31
  require 'rubocop/cop/rspec/expect_actual'
32
+ require 'rubocop/cop/rspec/expect_output'
30
33
  require 'rubocop/cop/rspec/file_path'
31
34
  require 'rubocop/cop/rspec/focus'
32
35
  require 'rubocop/cop/rspec/hook_argument'
@@ -43,6 +46,8 @@ require 'rubocop/cop/rspec/named_subject'
43
46
  require 'rubocop/cop/rspec/nested_groups'
44
47
  require 'rubocop/cop/rspec/not_to_not'
45
48
  require 'rubocop/cop/rspec/repeated_description'
49
+ require 'rubocop/cop/rspec/repeated_example'
50
+ require 'rubocop/cop/rspec/scattered_setup'
46
51
  require 'rubocop/cop/rspec/single_argument_message_chain'
47
52
  require 'rubocop/cop/rspec/subject_stub'
48
53
  require 'rubocop/cop/rspec/verified_doubles'
@@ -33,7 +33,7 @@ module RuboCop
33
33
  _receiver, method_name, *_args = *node
34
34
  return unless ANY_INSTANCE_METHODS.include?(method_name)
35
35
 
36
- add_offense(node, :expression, MESSAGE % { method: method_name })
36
+ add_offense(node, :expression, format(MESSAGE, method: method_name))
37
37
  end
38
38
  end
39
39
  end
@@ -7,8 +7,7 @@ module RuboCop
7
7
  class WorkaroundCop
8
8
  # Overwrite the cop inherited method to be a noop. Our RSpec::Cop
9
9
  # class will invoke the inherited hook instead
10
- def self.inherited(*)
11
- end
10
+ def self.inherited(*); end
12
11
 
13
12
  # Special case `Module#<` so that the rspec support rubocop exports
14
13
  # is compatible with our subclass
@@ -54,7 +54,7 @@ module RuboCop
54
54
  def find_constant_usage(node, described_class, &block)
55
55
  yield(node) if node.eql?(described_class)
56
56
 
57
- return unless node.instance_of?(Node)
57
+ return unless node.is_a?(Parser::AST::Node)
58
58
  return if scope_change?(node) || node.const_type?
59
59
 
60
60
  node.children.each do |child|
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for opportunities to use `expect { ... }.to output`.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # $stdout = StringIO.new
11
+ # my_app.print_report
12
+ # $stdout = STDOUT
13
+ # expect($stdout.string).to eq('Hello World')
14
+ #
15
+ # # good
16
+ # expect { my_app.print_report }.to output('Hello World').to_stdout
17
+ class ExpectOutput < Cop
18
+ MSG = 'Use `expect { ... }.to output(...).to_%<name>s` ' \
19
+ 'instead of mutating $%<name>s'.freeze
20
+
21
+ def_node_matcher :hook?, Hooks::ALL.block_pattern
22
+
23
+ def on_gvasgn(node)
24
+ return unless inside_example_scope?(node)
25
+
26
+ variable_name, _rhs = *node
27
+ name = variable_name[1..-1]
28
+ return unless name.eql?('stdout') || name.eql?('stderr')
29
+
30
+ add_offense(node, :name, format(MSG, name: name))
31
+ end
32
+
33
+ private
34
+
35
+ # Detect if we are inside the scope of a single example
36
+ #
37
+ # We want to encourage using `expect { ... }.to output` so
38
+ # we only care about situations where you would replace with
39
+ # an expectation. Therefore, assignments to stderr or stdout
40
+ # within a `before(:all)` or otherwise outside of an example
41
+ # don't matter.
42
+ def inside_example_scope?(node)
43
+ return false if node.nil? || example_group?(node)
44
+ return true if example?(node)
45
+ return RuboCop::RSpec::Hook.new(node).example? if hook?(node)
46
+
47
+ inside_example_scope?(node.parent)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -87,10 +87,11 @@ module RuboCop
87
87
  end
88
88
 
89
89
  def offense_message(offending_source)
90
- MSG % {
90
+ format(
91
+ MSG,
91
92
  good: replacement_source(offending_source),
92
93
  bad: offending_source
93
- }
94
+ )
94
95
  end
95
96
 
96
97
  def replacement_source(offending_source)
@@ -18,7 +18,7 @@ module RuboCop
18
18
  _receiver, method_name, *_args = *node
19
19
  return unless Matchers::MESSAGE_CHAIN.include?(method_name)
20
20
 
21
- add_offense(node, :selector, MESSAGE % { method: method_name })
21
+ add_offense(node, :selector, format(MESSAGE, method: method_name))
22
22
  end
23
23
  end
24
24
  end
@@ -36,7 +36,7 @@ module RuboCop
36
36
  SUPPORTED_STYLES = %w(have_received receive).freeze
37
37
 
38
38
  def_node_matcher :message_expectation, %(
39
- (send (send nil :expect (send nil $_)) :to ...)
39
+ (send (send nil :expect $_) {:to :to_not :not_to} ...)
40
40
  )
41
41
 
42
42
  def_node_search :receive_message, %(
@@ -70,7 +70,7 @@ module RuboCop
70
70
  when :receive
71
71
  MSG_RECEIVE
72
72
  when :have_received
73
- MSG_HAVE_RECEIVED % receiver
73
+ MSG_HAVE_RECEIVED % receiver.source
74
74
  end
75
75
  end
76
76
  end
@@ -48,9 +48,7 @@ module RuboCop
48
48
  class MultipleExpectations < Cop
49
49
  include ConfigurableMax
50
50
 
51
- MSG = 'Too many expectations.'.freeze
52
-
53
- def_node_matcher :example?, Examples::ALL.block_pattern
51
+ MSG = 'Example has too many expectations [%{total}/%{max}]'.freeze
54
52
 
55
53
  def_node_search :expect, '(send _ :expect ...)'
56
54
 
@@ -72,7 +70,7 @@ module RuboCop
72
70
  add_offense(
73
71
  method,
74
72
  :expression,
75
- MSG % { total: expectation_count, max: max_expectations }
73
+ format(MSG, total: expectation_count, max: max_expectations)
76
74
  )
77
75
  end
78
76
 
@@ -60,7 +60,7 @@ module RuboCop
60
60
  private
61
61
 
62
62
  def subject_usage(node, &block)
63
- return unless node.instance_of?(Node)
63
+ return unless node.is_a?(Parser::AST::Node)
64
64
 
65
65
  unnamed_subject(node, &block)
66
66
 
@@ -89,6 +89,12 @@ module RuboCop
89
89
 
90
90
  MSG = 'Maximum example group nesting exceeded'.freeze
91
91
 
92
+ DEPRECATED_MAX_KEY = 'MaxNesting'.freeze
93
+
94
+ DEPRECATION_WARNING =
95
+ "Configuration key `#{DEPRECATED_MAX_KEY}` for #{cop_name} is " \
96
+ 'deprecated in favor of `Max`. Please use that instead.'.freeze
97
+
92
98
  def_node_search :find_contexts, ExampleGroups::ALL.block_pattern
93
99
 
94
100
  def on_top_level_describe(node, _)
@@ -110,7 +116,16 @@ module RuboCop
110
116
  end
111
117
 
112
118
  def max_nesting
113
- Integer(cop_config.fetch('MaxNesting', 3))
119
+ @max_nesting ||= Integer(max_nesting_config)
120
+ end
121
+
122
+ def max_nesting_config
123
+ if cop_config.key?(DEPRECATED_MAX_KEY)
124
+ warn DEPRECATION_WARNING
125
+ cop_config.fetch(DEPRECATED_MAX_KEY)
126
+ else
127
+ cop_config.fetch('Max', 3)
128
+ end
114
129
  end
115
130
  end
116
131
  end
@@ -0,0 +1,41 @@
1
+ module RuboCop
2
+ module Cop
3
+ module RSpec
4
+ # Check for repeated examples within example groups.
5
+ #
6
+ # @example
7
+ #
8
+ # it 'is valid' do
9
+ # expect(user).to be_valid
10
+ # end
11
+ #
12
+ # it 'validates the user' do
13
+ # expect(user).to be_valid
14
+ # end
15
+ #
16
+ class RepeatedExample < Cop
17
+ MSG = "Don't repeat examples within an example group.".freeze
18
+
19
+ def on_block(node)
20
+ return unless example_group?(node)
21
+
22
+ repeated_examples(node).each do |repeated_example|
23
+ add_offense(repeated_example, :expression)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def repeated_examples(node)
30
+ RuboCop::RSpec::ExampleGroup.new(node)
31
+ .examples
32
+ .group_by { |example| [example.metadata, example.implementation] }
33
+ .values
34
+ .reject(&:one?)
35
+ .flatten
36
+ .map(&:to_node)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for setup scattered across multiple hooks in an example group.
7
+ #
8
+ # Unify `before`, `after`, and `around` hooks when possible.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # describe Foo do
13
+ # before { setup1 }
14
+ # before { setup2 }
15
+ # end
16
+ #
17
+ # # good
18
+ # describe Foo do
19
+ # before do
20
+ # setup1
21
+ # setup2
22
+ # end
23
+ # end
24
+ #
25
+ class ScatteredSetup < Cop
26
+ MSG = 'Do not define multiple hooks in the same example group.'.freeze
27
+
28
+ def on_block(node)
29
+ return unless example_group?(node)
30
+
31
+ analyzable_hooks(node).each do |repeated_hook|
32
+ add_offense(repeated_hook, :expression)
33
+ end
34
+ end
35
+
36
+ def analyzable_hooks(node)
37
+ RuboCop::RSpec::ExampleGroup.new(node)
38
+ .hooks
39
+ .select { |hook| hook.knowable_scope? && hook.valid_scope? }
40
+ .group_by { |hook| [hook.name, hook.scope] }
41
+ .values
42
+ .reject(&:one?)
43
+ .flatten
44
+ .map(&:to_node)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -27,6 +27,16 @@ module RuboCop
27
27
  add_offense(node, :selector, message(method_name))
28
28
  end
29
29
 
30
+ def autocorrect(node)
31
+ _receiver, method_name, *_args = *node
32
+ lambda do |corrector|
33
+ corrector.replace(
34
+ node.loc.selector,
35
+ method_name.equal?(:receive_message_chain) ? 'receive' : 'stub'
36
+ )
37
+ end
38
+ end
39
+
30
40
  private
31
41
 
32
42
  def multi_argument_string?(args)
@@ -36,11 +46,13 @@ module RuboCop
36
46
  end
37
47
 
38
48
  def message(method)
39
- if method == :receive_message_chain
40
- MESSAGE % { recommended_method: :receive, called_method: method }
41
- else
42
- MESSAGE % { recommended_method: :stub, called_method: method }
43
- end
49
+ recommendation = method == :receive_message_chain ? :receive : :stub
50
+
51
+ format(
52
+ MESSAGE,
53
+ recommended_method: recommendation,
54
+ called_method: method
55
+ )
44
56
  end
45
57
  end
46
58
  end
data/lib/rubocop/rspec.rb CHANGED
@@ -3,7 +3,7 @@ module RuboCop
3
3
  module RSpec
4
4
  PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
5
5
  CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
6
- CONFIG = YAML.load(CONFIG_DEFAULT.read).freeze
6
+ CONFIG = YAML.safe_load(CONFIG_DEFAULT.read).freeze
7
7
 
8
8
  private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
9
9
  end