rubocop-rspec 1.8.0 → 1.9.0

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