rubocop-rspec 1.9.1 → 1.10.0

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