rubocop-rspec 1.8.0 → 1.9.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/Gemfile +2 -1
  4. data/Rakefile +3 -1
  5. data/config/default.yml +16 -0
  6. data/lib/rubocop-rspec.rb +6 -1
  7. data/lib/rubocop/cop/rspec/any_instance.rb +0 -2
  8. data/lib/rubocop/cop/rspec/be_eql.rb +1 -2
  9. data/lib/rubocop/cop/rspec/cop.rb +66 -0
  10. data/lib/rubocop/cop/rspec/describe_class.rb +6 -1
  11. data/lib/rubocop/cop/rspec/describe_method.rb +1 -2
  12. data/lib/rubocop/cop/rspec/described_class.rb +3 -6
  13. data/lib/rubocop/cop/rspec/empty_example_group.rb +1 -11
  14. data/lib/rubocop/cop/rspec/example_length.rb +1 -1
  15. data/lib/rubocop/cop/rspec/example_wording.rb +0 -2
  16. data/lib/rubocop/cop/rspec/expect_actual.rb +0 -2
  17. data/lib/rubocop/cop/rspec/file_path.rb +1 -1
  18. data/lib/rubocop/cop/rspec/focus.rb +4 -9
  19. data/lib/rubocop/cop/rspec/hook_argument.rb +3 -5
  20. data/lib/rubocop/cop/rspec/implicit_expect.rb +1 -1
  21. data/lib/rubocop/cop/rspec/instance_variable.rb +1 -5
  22. data/lib/rubocop/cop/rspec/leading_subject.rb +0 -2
  23. data/lib/rubocop/cop/rspec/let_setup.rb +1 -4
  24. data/lib/rubocop/cop/rspec/message_chain.rb +1 -8
  25. data/lib/rubocop/cop/rspec/message_expectation.rb +1 -1
  26. data/lib/rubocop/cop/rspec/message_spies.rb +79 -0
  27. data/lib/rubocop/cop/rspec/multiple_describes.rb +1 -2
  28. data/lib/rubocop/cop/rspec/multiple_expectations.rb +2 -6
  29. data/lib/rubocop/cop/rspec/named_subject.rb +0 -2
  30. data/lib/rubocop/cop/rspec/nested_groups.rb +2 -6
  31. data/lib/rubocop/cop/rspec/not_to_not.rb +1 -2
  32. data/lib/rubocop/cop/rspec/repeated_description.rb +59 -0
  33. data/lib/rubocop/cop/rspec/single_argument_message_chain.rb +48 -0
  34. data/lib/rubocop/cop/rspec/subject_stub.rb +1 -4
  35. data/lib/rubocop/cop/rspec/verified_doubles.rb +0 -2
  36. data/lib/rubocop/rspec.rb +1 -1
  37. data/lib/rubocop/rspec/description_extractor.rb +55 -18
  38. data/lib/rubocop/rspec/example.rb +56 -0
  39. data/lib/rubocop/rspec/example_group.rb +57 -0
  40. data/lib/rubocop/rspec/language.rb +28 -1
  41. data/lib/rubocop/rspec/language/node_pattern.rb +1 -3
  42. data/lib/rubocop/rspec/top_level_describe.rb +6 -10
  43. data/lib/rubocop/rspec/version.rb +1 -1
  44. data/spec/project/default_config_spec.rb +8 -5
  45. data/spec/project/project_requires_spec.rb +1 -1
  46. data/spec/rubocop/{rspec/spec_only_spec.rb → cop/rspec/cop_spec.rb} +2 -4
  47. data/spec/rubocop/cop/rspec/describe_class_spec.rb +36 -0
  48. data/spec/rubocop/cop/rspec/described_class_spec.rb +2 -2
  49. data/spec/rubocop/cop/rspec/example_length_spec.rb +48 -60
  50. data/spec/rubocop/cop/rspec/implicit_expect_spec.rb +1 -1
  51. data/spec/rubocop/cop/rspec/message_spies_spec.rb +93 -0
  52. data/spec/rubocop/cop/rspec/repeated_description_spec.rb +76 -0
  53. data/spec/rubocop/cop/rspec/single_argument_message_chain_spec.rb +75 -0
  54. data/spec/rubocop/rspec/description_extractor_spec.rb +46 -24
  55. data/spec/rubocop/rspec/example_group_spec.rb +44 -0
  56. data/spec/rubocop/rspec/example_spec.rb +62 -0
  57. data/spec/rubocop/rspec/language/selector_set_spec.rb +22 -2
  58. data/spec/spec_helper.rb +2 -9
  59. data/spec/support/expect_violation.rb +4 -2
  60. metadata +21 -8
  61. data/lib/rubocop/rspec/spec_only.rb +0 -61
  62. data/spec/shared/rspec_only_cop_behavior.rb +0 -68
@@ -25,7 +25,7 @@ module RuboCop
25
25
  # expect(foo).to receive(:bar)
26
26
  #
27
27
  class MessageExpectation < Cop
28
- include RuboCop::RSpec::SpecOnly, ConfigurableEnforcedStyle
28
+ include ConfigurableEnforcedStyle
29
29
 
30
30
  MSG = 'Prefer `%s` for setting message expectations.'.freeze
31
31
 
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks that message expectations are set using spies.
7
+ #
8
+ # This cop can be configured in your configuration using the
9
+ # `EnforcedStyle` option and supports `--auto-gen-config`.
10
+ #
11
+ # @example `EnforcedStyle: have_received`
12
+ #
13
+ # # bad
14
+ # expect(foo).to receive(:bar)
15
+ #
16
+ # # good
17
+ # expect(foo).to have_received(:bar)
18
+ #
19
+ # @example `EnforcedStyle: receive`
20
+ #
21
+ # # bad
22
+ # expect(foo).to have_received(:bar)
23
+ #
24
+ # # good
25
+ # expect(foo).to receive(:bar)
26
+ #
27
+ class MessageSpies < Cop
28
+ include ConfigurableEnforcedStyle
29
+
30
+ MSG_RECEIVE = 'Prefer `receive` for setting message ' \
31
+ 'expectations.'.freeze
32
+ MSG_HAVE_RECEIVED = 'Prefer `have_received` for setting message ' \
33
+ 'expectations. Setup `%s` as a spy using `allow` or `instance_spy`.'
34
+ .freeze
35
+
36
+ SUPPORTED_STYLES = %w(have_received receive).freeze
37
+
38
+ def_node_matcher :message_expectation, %(
39
+ (send (send nil :expect (send nil $_)) :to ...)
40
+ )
41
+
42
+ def_node_search :receive_message, %(
43
+ $(send nil {:receive :have_received} ...)
44
+ )
45
+
46
+ def on_send(node)
47
+ receive_message_matcher(node) do |receiver, message_matcher|
48
+ return correct_style_detected if preferred_style?(message_matcher)
49
+
50
+ add_offense(message_matcher, :selector, error_message(receiver)) do
51
+ opposite_style_detected
52
+ end
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def receive_message_matcher(node)
59
+ return unless (receiver = message_expectation(node))
60
+
61
+ receive_message(node) { |match| yield(receiver, match) }
62
+ end
63
+
64
+ def preferred_style?(expectation)
65
+ expectation.method_name.equal?(style)
66
+ end
67
+
68
+ def error_message(receiver)
69
+ case style
70
+ when :receive
71
+ MSG_RECEIVE
72
+ when :have_received
73
+ MSG_HAVE_RECEIVED % receiver
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -23,8 +23,7 @@ module RuboCop
23
23
  # end
24
24
  # end
25
25
  class MultipleDescribes < Cop
26
- include RuboCop::RSpec::SpecOnly,
27
- RuboCop::RSpec::TopLevelDescribe
26
+ include RuboCop::RSpec::TopLevelDescribe
28
27
 
29
28
  MSG = 'Do not use multiple top level describes - ' \
30
29
  'try to nest them.'.freeze
@@ -46,15 +46,11 @@ module RuboCop
46
46
  # end
47
47
  #
48
48
  class MultipleExpectations < Cop
49
- include RuboCop::RSpec::SpecOnly,
50
- RuboCop::RSpec::Language,
51
- ConfigurableMax
49
+ include ConfigurableMax
52
50
 
53
51
  MSG = 'Too many expectations.'.freeze
54
52
 
55
- def_node_matcher :example?, <<-PATTERN
56
- (block (send _ {#{Examples::ALL.to_node_pattern}} ...) ...)
57
- PATTERN
53
+ def_node_matcher :example?, Examples::ALL.block_pattern
58
54
 
59
55
  def_node_search :expect, '(send _ :expect ...)'
60
56
 
@@ -38,8 +38,6 @@ module RuboCop
38
38
  # it { should be_valid }
39
39
  # end
40
40
  class NamedSubject < Cop
41
- include RuboCop::RSpec::SpecOnly
42
-
43
41
  MSG = 'Name your test subject if '\
44
42
  'you need to reference it explicitly.'.freeze
45
43
 
@@ -85,15 +85,11 @@ module RuboCop
85
85
  # end
86
86
  #
87
87
  class NestedGroups < Cop
88
- include RuboCop::RSpec::SpecOnly,
89
- RuboCop::RSpec::TopLevelDescribe,
90
- RuboCop::RSpec::Language
88
+ include RuboCop::RSpec::TopLevelDescribe
91
89
 
92
90
  MSG = 'Maximum example group nesting exceeded'.freeze
93
91
 
94
- def_node_search :find_contexts, <<-PATTERN
95
- (block (send nil {#{ExampleGroups::ALL.to_node_pattern}} ...) (args) ...)
96
- PATTERN
92
+ def_node_search :find_contexts, ExampleGroups::ALL.block_pattern
97
93
 
98
94
  def on_block(node)
99
95
  describe, = described_constant(node)
@@ -14,8 +14,7 @@ module RuboCop
14
14
  # expect(false).not_to be_true
15
15
  # end
16
16
  class NotToNot < Cop
17
- include RuboCop::RSpec::SpecOnly,
18
- RuboCop::Cop::ConfigurableEnforcedStyle
17
+ include RuboCop::Cop::ConfigurableEnforcedStyle
19
18
 
20
19
  MSG = 'Prefer `%s` over `%s`'.freeze
21
20
 
@@ -0,0 +1,59 @@
1
+ module RuboCop
2
+ module Cop
3
+ module RSpec
4
+ # Check for repeated description strings in example groups.
5
+ #
6
+ # @example
7
+ #
8
+ # # bad
9
+ # RSpec.describe User do
10
+ # it 'is valid' do
11
+ # # ...
12
+ # end
13
+ #
14
+ # it 'is valid' do
15
+ # # ...
16
+ # end
17
+ # end
18
+ #
19
+ # # good
20
+ # RSpec.describe User do
21
+ # it 'is valid when first and last name are present' do
22
+ # # ...
23
+ # end
24
+ #
25
+ # it 'is valid when last name only is present' do
26
+ # # ...
27
+ # end
28
+ # end
29
+ #
30
+ class RepeatedDescription < Cop
31
+ MSG = "Don't repeat descriptions within an example group.".freeze
32
+
33
+ def on_block(node)
34
+ return unless example_group?(node)
35
+
36
+ repeated_descriptions(node).each do |repeated_description|
37
+ add_offense(repeated_description, :expression)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Select examples in the current scope with repeated description strings
44
+ def repeated_descriptions(node)
45
+ grouped_examples =
46
+ RuboCop::RSpec::ExampleGroup.new(node)
47
+ .examples
48
+ .group_by(&:doc_string)
49
+
50
+ grouped_examples
51
+ .select { |description, group| description && group.size > 1 }
52
+ .values
53
+ .flatten
54
+ .map(&:definition)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,48 @@
1
+ module RuboCop
2
+ module Cop
3
+ module RSpec
4
+ # Checks that chains of messages contain more than one element.
5
+ #
6
+ # @example
7
+ # # bad
8
+ # allow(foo).to receive_message_chain(:bar).and_return(42)
9
+ #
10
+ # # good
11
+ # allow(foo).to receive(:bar).and_return(42)
12
+ #
13
+ # # also good
14
+ # allow(foo).to receive(:bar, :baz)
15
+ # allow(foo).to receive("bar.baz")
16
+ #
17
+ class SingleArgumentMessageChain < Cop
18
+ MESSAGE = 'Use `%<recommended_method>s` instead of calling ' \
19
+ '`%<called_method>s` with a single argument'.freeze
20
+
21
+ def on_send(node)
22
+ _receiver, method_name, *args = *node
23
+ return unless Matchers::MESSAGE_CHAIN.include?(method_name)
24
+ return if args.size > 1
25
+ return if multi_argument_string?(args)
26
+
27
+ add_offense(node, :selector, message(method_name))
28
+ end
29
+
30
+ private
31
+
32
+ def multi_argument_string?(args)
33
+ args.size == 1 &&
34
+ args.first.type == :str &&
35
+ args.first.children.first.include?('.')
36
+ end
37
+
38
+ 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
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -18,10 +18,7 @@ module RuboCop
18
18
  # end
19
19
  #
20
20
  class SubjectStub < Cop
21
- include RuboCop::RSpec::SpecOnly,
22
- RuboCop::RSpec::TopLevelDescribe,
23
- RuboCop::RSpec::Language,
24
- RuboCop::RSpec::Language::NodePattern
21
+ include RuboCop::RSpec::TopLevelDescribe
25
22
 
26
23
  MSG = 'Do not stub your test subject.'.freeze
27
24
 
@@ -18,8 +18,6 @@ module RuboCop
18
18
  # widget = instance_double("Widget")
19
19
  # end
20
20
  class VerifiedDoubles < Cop
21
- include RuboCop::RSpec::SpecOnly
22
-
23
21
  MSG = 'Prefer using verifying doubles over normal doubles.'.freeze
24
22
 
25
23
  def_node_matcher :unverified_double, <<-PATTERN
data/lib/rubocop/rspec.rb CHANGED
@@ -5,6 +5,6 @@ module RuboCop
5
5
  CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
6
6
  CONFIG = YAML.load(CONFIG_DEFAULT.read).freeze
7
7
 
8
- private_constant(*constants(false))
8
+ private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
9
9
  end
10
10
  end
@@ -2,34 +2,71 @@ module RuboCop
2
2
  module RSpec
3
3
  # Extracts cop descriptions from YARD docstrings
4
4
  class DescriptionExtractor
5
- COP_NAMESPACE = 'RuboCop::Cop::RSpec'.freeze
6
- COP_FORMAT = 'RSpec/%s'.freeze
7
-
8
5
  def initialize(yardocs)
9
- @yardocs = yardocs
6
+ @code_objects = yardocs.map(&CodeObject.public_method(:new))
10
7
  end
11
8
 
12
9
  def to_h
13
- cop_documentation.each_with_object({}) do |(name, docstring), config|
14
- config[format(COP_FORMAT, name)] = {
15
- 'Description' => docstring.split("\n\n").first.to_s
16
- }
17
- end
10
+ code_objects
11
+ .select(&:rspec_cop?)
12
+ .map(&:configuration)
13
+ .reduce(:merge)
18
14
  end
19
15
 
20
16
  private
21
17
 
22
- def cop_documentation
23
- yardocs
24
- .select(&method(:cop?))
25
- .map { |doc| [doc.name, doc.docstring] }
26
- end
18
+ attr_reader :code_objects
27
19
 
28
- def cop?(doc)
29
- doc.type.equal?(:class) && doc.to_s.start_with?(COP_NAMESPACE)
30
- end
20
+ # Decorator of a YARD code object for working with documented rspec cops
21
+ class CodeObject
22
+ COP_NAMESPACE = 'RuboCop::Cop::RSpec'.freeze
23
+
24
+ def initialize(yardoc)
25
+ @yardoc = yardoc
26
+ end
27
+
28
+ # Test if the YARD code object documents a concrete rspec cop class
29
+ #
30
+ # @return [Boolean]
31
+ def rspec_cop?
32
+ class_documentation? && rspec_cop_namespace? && !abstract?
33
+ end
34
+
35
+ # Configuration for the documented cop that would live in default.yml
36
+ #
37
+ # @return [Hash]
38
+ def configuration
39
+ { cop_name => { 'Description' => description } }
40
+ end
41
+
42
+ private
43
+
44
+ def cop_name
45
+ Object.const_get(documented_constant).cop_name
46
+ end
47
+
48
+ def description
49
+ yardoc.docstring.split("\n\n").first.to_s
50
+ end
51
+
52
+ def class_documentation?
53
+ yardoc.type.equal?(:class)
54
+ end
55
+
56
+ def rspec_cop_namespace?
57
+ documented_constant.start_with?(COP_NAMESPACE)
58
+ end
59
+
60
+ def documented_constant
61
+ yardoc.to_s
62
+ end
31
63
 
32
- attr_reader :yardocs
64
+ def abstract?
65
+ yardoc.tags.any? { |tag| tag.tag_name.eql?('abstract') }
66
+ end
67
+
68
+ attr_reader :yardoc
69
+ end
33
70
  end
34
71
  end
35
72
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module RSpec
5
+ # Wrapper for RSpec examples
6
+ class Example
7
+ extend RuboCop::NodePattern::Macros
8
+
9
+ def_node_matcher :extract_doc_string, '(send _ _ $str ...)'
10
+ def_node_matcher :extract_metadata, '(send _ _ _ $...)'
11
+ def_node_matcher :extract_implementation, '(block send args $_)'
12
+
13
+ def initialize(node)
14
+ @node = node
15
+ end
16
+
17
+ def doc_string
18
+ extract_doc_string(definition)
19
+ end
20
+
21
+ def metadata
22
+ extract_metadata(definition)
23
+ end
24
+
25
+ def implementation
26
+ extract_implementation(node)
27
+ end
28
+
29
+ def eql?(other)
30
+ node.eql?(other.node)
31
+ end
32
+
33
+ alias == eql?
34
+
35
+ def hash
36
+ [self.class, node].hash
37
+ end
38
+
39
+ def to_node
40
+ node
41
+ end
42
+
43
+ def definition
44
+ if node.send_type?
45
+ node
46
+ else
47
+ node.children.first
48
+ end
49
+ end
50
+
51
+ protected
52
+
53
+ attr_reader :node
54
+ end
55
+ end
56
+ end