rubocop-rspec 1.6.0 → 1.7.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +23 -0
  4. data/Rakefile +19 -1
  5. data/config/default.yml +78 -15
  6. data/lib/rubocop-rspec.rb +19 -1
  7. data/lib/rubocop/cop/rspec/any_instance.rb +5 -1
  8. data/lib/rubocop/cop/rspec/be_eql.rb +58 -0
  9. data/lib/rubocop/cop/rspec/describe_class.rb +2 -3
  10. data/lib/rubocop/cop/rspec/describe_method.rb +4 -3
  11. data/lib/rubocop/cop/rspec/described_class.rb +4 -35
  12. data/lib/rubocop/cop/rspec/empty_example_group.rb +100 -0
  13. data/lib/rubocop/cop/rspec/example_length.rb +5 -2
  14. data/lib/rubocop/cop/rspec/example_wording.rb +5 -2
  15. data/lib/rubocop/cop/rspec/expect_actual.rb +79 -0
  16. data/lib/rubocop/cop/rspec/file_path.rb +3 -1
  17. data/lib/rubocop/cop/rspec/focus.rb +12 -28
  18. data/lib/rubocop/cop/rspec/hook_argument.rb +122 -0
  19. data/lib/rubocop/cop/rspec/instance_variable.rb +53 -12
  20. data/lib/rubocop/cop/rspec/leading_subject.rb +58 -0
  21. data/lib/rubocop/cop/rspec/let_setup.rb +58 -0
  22. data/lib/rubocop/cop/rspec/message_chain.rb +33 -0
  23. data/lib/rubocop/cop/rspec/message_expectation.rb +58 -0
  24. data/lib/rubocop/cop/rspec/multiple_describes.rb +10 -7
  25. data/lib/rubocop/cop/rspec/multiple_expectations.rb +89 -0
  26. data/lib/rubocop/cop/rspec/named_subject.rb +10 -1
  27. data/lib/rubocop/cop/rspec/nested_groups.rb +125 -0
  28. data/lib/rubocop/cop/rspec/not_to_not.rb +3 -3
  29. data/lib/rubocop/cop/rspec/subject_stub.rb +136 -0
  30. data/lib/rubocop/cop/rspec/verified_doubles.rb +4 -1
  31. data/lib/rubocop/rspec.rb +10 -0
  32. data/lib/rubocop/rspec/config_formatter.rb +33 -0
  33. data/lib/rubocop/rspec/description_extractor.rb +35 -0
  34. data/lib/rubocop/rspec/inject.rb +2 -6
  35. data/lib/rubocop/rspec/language.rb +73 -0
  36. data/lib/rubocop/rspec/language/node_pattern.rb +16 -0
  37. data/lib/rubocop/rspec/spec_only.rb +61 -0
  38. data/lib/rubocop/rspec/top_level_describe.rb +6 -0
  39. data/lib/rubocop/rspec/version.rb +1 -1
  40. data/rubocop-rspec.gemspec +1 -0
  41. data/spec/project/changelog_spec.rb +81 -0
  42. data/spec/project/default_config_spec.rb +52 -0
  43. data/spec/project/project_requires_spec.rb +8 -0
  44. data/spec/rubocop/cop/rspec/be_eql_spec.rb +59 -0
  45. data/spec/rubocop/cop/rspec/described_class_spec.rb +2 -2
  46. data/spec/rubocop/cop/rspec/empty_example_group_spec.rb +79 -0
  47. data/spec/rubocop/cop/rspec/example_length_spec.rb +50 -30
  48. data/spec/rubocop/cop/rspec/example_wording_spec.rb +21 -3
  49. data/spec/rubocop/cop/rspec/expect_actual_spec.rb +136 -0
  50. data/spec/rubocop/cop/rspec/file_path_spec.rb +48 -71
  51. data/spec/rubocop/cop/rspec/focus_spec.rb +1 -1
  52. data/spec/rubocop/cop/rspec/hook_argument_spec.rb +189 -0
  53. data/spec/rubocop/cop/rspec/instance_variable_spec.rb +37 -0
  54. data/spec/rubocop/cop/rspec/leading_subject_spec.rb +54 -0
  55. data/spec/rubocop/cop/rspec/let_setup_spec.rb +66 -0
  56. data/spec/rubocop/cop/rspec/message_chain_spec.rb +21 -0
  57. data/spec/rubocop/cop/rspec/message_expectation_spec.rb +63 -0
  58. data/spec/rubocop/cop/rspec/multiple_expectations_spec.rb +84 -0
  59. data/spec/rubocop/cop/rspec/nested_groups_spec.rb +55 -0
  60. data/spec/rubocop/cop/rspec/not_to_not_spec.rb +12 -2
  61. data/spec/rubocop/cop/rspec/subject_stub_spec.rb +183 -0
  62. data/spec/rubocop/rspec/config_formatter_spec.rb +48 -0
  63. data/spec/rubocop/rspec/description_extractor_spec.rb +35 -0
  64. data/spec/rubocop/rspec/language/selector_set_spec.rb +29 -0
  65. data/spec/rubocop/rspec/spec_only_spec.rb +97 -0
  66. data/spec/shared/rspec_only_cop_behavior.rb +68 -0
  67. data/spec/spec_helper.rb +13 -1
  68. metadata +72 -5
  69. data/spec/project_spec.rb +0 -115
@@ -1,8 +1,7 @@
1
1
  module RuboCop
2
2
  module Cop
3
3
  module RSpec
4
- # Enforces the usage of the same method on all negative message
5
- # expectations.
4
+ # Checks for consistent method usage for negating expectations.
6
5
  #
7
6
  # @example
8
7
  # # bad
@@ -15,7 +14,8 @@ module RuboCop
15
14
  # expect(false).not_to be_true
16
15
  # end
17
16
  class NotToNot < Cop
18
- include RuboCop::Cop::ConfigurableEnforcedStyle
17
+ include RuboCop::RSpec::SpecOnly,
18
+ RuboCop::Cop::ConfigurableEnforcedStyle
19
19
 
20
20
  MSG = 'Prefer `%s` over `%s`'.freeze
21
21
 
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for stubbed test subjects.
7
+ #
8
+ # @see https://robots.thoughtbot.com/don-t-stub-the-system-under-test
9
+ #
10
+ # @example
11
+ # # bad
12
+ # describe Foo do
13
+ # subject(:bar) { baz }
14
+ #
15
+ # before do
16
+ # allow(bar).to receive(:qux?).and_return(true)
17
+ # end
18
+ # end
19
+ #
20
+ class SubjectStub < Cop
21
+ include RuboCop::RSpec::SpecOnly,
22
+ RuboCop::RSpec::TopLevelDescribe,
23
+ RuboCop::RSpec::Language,
24
+ RuboCop::RSpec::Language::NodePattern
25
+
26
+ MSG = 'Do not stub your test subject.'.freeze
27
+
28
+ # @!method subject(node)
29
+ # Find a named or unnamed subject definition
30
+ #
31
+ # @example anonymous subject
32
+ # subject(parse('subject { foo }').ast) do |name|
33
+ # name # => :subject
34
+ # end
35
+ #
36
+ # @example named subject
37
+ # subject(parse('subject(:thing) { foo }').ast) do |name|
38
+ # name # => :thing
39
+ # end
40
+ #
41
+ # @param node [RuboCop::Node]
42
+ #
43
+ # @yield [Symbol] subject name
44
+ def_node_matcher :subject, <<-PATTERN
45
+ {
46
+ (block (send nil :subject (sym $_)) args ...)
47
+ (block (send nil $:subject) args ...)
48
+ }
49
+ PATTERN
50
+
51
+ # @!method message_expectation?(node, method_name)
52
+ # Match `allow` and `expect(...).to receive`
53
+ #
54
+ # @example source that matches
55
+ # allow(foo).to receive(:bar)
56
+ # allow(foo).to receive(:bar).with(1)
57
+ # allow(foo).to receive(:bar).with(1).and_return(2)
58
+ # expect(foo).to receive(:bar)
59
+ # expect(foo).to receive(:bar).with(1)
60
+ # expect(foo).to receive(:bar).with(1).and_return(2)
61
+ def_node_matcher :message_expectation?, <<-PATTERN
62
+ {
63
+ (send nil :allow (send nil %))
64
+ (send (send nil :expect (send nil %)) :to #receive_message?)
65
+ }
66
+ PATTERN
67
+
68
+ def_node_search :receive_message?, '(send nil :receive ...)'
69
+
70
+ def on_block(node)
71
+ return unless example_group?(node)
72
+
73
+ find_subject_stub(node) { |stub| add_offense(stub, :expression) }
74
+ end
75
+
76
+ private
77
+
78
+ # Find subjects within tree and then find (send) nodes for that subject
79
+ #
80
+ # @param node [RuboCop::Node] example group
81
+ #
82
+ # @yield [RuboCop::Node] message expectations for subject
83
+ def find_subject_stub(node, &block)
84
+ find_subject(node) do |subject_name, context|
85
+ find_subject_expectation(context, subject_name, &block)
86
+ end
87
+ end
88
+
89
+ # Find a subject message expectation
90
+ #
91
+ # @param node [RuboCop::Node]
92
+ # @param subject_name [Symbol] name of subject
93
+ #
94
+ # @yield [RuboCop::Node] message expectation
95
+ def find_subject_expectation(node, subject_name, &block)
96
+ # Do not search node if it is an example group with its own subject.
97
+ return if example_group?(node) && redefines_subject?(node)
98
+
99
+ # Yield the current node if it is a message expectation.
100
+ yield(node) if message_expectation?(node, subject_name)
101
+
102
+ # Recurse through node's children looking for a message expectation.
103
+ node.each_child_node do |child|
104
+ find_subject_expectation(child, subject_name, &block)
105
+ end
106
+ end
107
+
108
+ # Check if node's children contain a subject definition
109
+ #
110
+ # @param node [RuboCop::Node]
111
+ #
112
+ # @return [Boolean]
113
+ def redefines_subject?(node)
114
+ node.each_child_node.any? do |child|
115
+ subject(child) || redefines_subject?(child)
116
+ end
117
+ end
118
+
119
+ # Find a subject definition
120
+ #
121
+ # @param node [RuboCop::Node]
122
+ # @param parent [RuboCop::Node,nil]
123
+ #
124
+ # @yieldparam subject_name [Symbol] name of subject being defined
125
+ # @yieldparam parent [RuboCop::Node] parent of subject definition
126
+ def find_subject(node, parent: nil, &block)
127
+ subject(node) { |name| yield(name, parent) }
128
+
129
+ node.each_child_node do |child|
130
+ find_subject(child, parent: node, &block)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -4,7 +4,8 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  # Prefer using verifying doubles over normal doubles.
7
- # see: https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles
7
+ #
8
+ # @see https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles
8
9
  #
9
10
  # @example
10
11
  # # bad
@@ -17,6 +18,8 @@ module RuboCop
17
18
  # widget = instance_double("Widget")
18
19
  # end
19
20
  class VerifiedDoubles < Cop
21
+ include RuboCop::RSpec::SpecOnly
22
+
20
23
  MSG = 'Prefer using verifying doubles over normal doubles.'.freeze
21
24
 
22
25
  def_node_matcher :unverified_double, <<-PATTERN
@@ -0,0 +1,10 @@
1
+ module RuboCop
2
+ # RuboCop RSpec project namespace
3
+ module RSpec
4
+ PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
5
+ CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
6
+ CONFIG = YAML.load(CONFIG_DEFAULT.read).freeze
7
+
8
+ private_constant(*constants(false))
9
+ end
10
+ end
@@ -0,0 +1,33 @@
1
+ require 'yaml'
2
+
3
+ module RuboCop
4
+ module RSpec
5
+ # Builds a YAML config file from two config hashes
6
+ class ConfigFormatter
7
+ NAMESPACE = 'RSpec'.freeze
8
+
9
+ def initialize(config, descriptions)
10
+ @config = config
11
+ @descriptions = descriptions
12
+ end
13
+
14
+ def dump
15
+ YAML.dump(unified_config).gsub(/^#{NAMESPACE}/, "\n#{NAMESPACE}")
16
+ end
17
+
18
+ private
19
+
20
+ def unified_config
21
+ cops.each_with_object(config.dup) do |cop, unified|
22
+ unified[cop] = config.fetch(cop).merge(descriptions.fetch(cop))
23
+ end
24
+ end
25
+
26
+ def cops
27
+ (descriptions.keys + config.keys).uniq.grep(/\A#{NAMESPACE}/)
28
+ end
29
+
30
+ attr_reader :config, :descriptions
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ module RuboCop
2
+ module RSpec
3
+ # Extracts cop descriptions from YARD docstrings
4
+ class DescriptionExtractor
5
+ COP_NAMESPACE = 'RuboCop::Cop::RSpec'.freeze
6
+ COP_FORMAT = 'RSpec/%s'.freeze
7
+
8
+ def initialize(yardocs)
9
+ @yardocs = yardocs
10
+ end
11
+
12
+ 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
18
+ end
19
+
20
+ private
21
+
22
+ def cop_documentation
23
+ yardocs
24
+ .select(&method(:cop?))
25
+ .map { |doc| [doc.name, doc.docstring] }
26
+ end
27
+
28
+ def cop?(doc)
29
+ doc.type.equal?(:class) && doc.to_s.start_with?(COP_NAMESPACE)
30
+ end
31
+
32
+ attr_reader :yardocs
33
+ end
34
+ end
35
+ end
@@ -3,15 +3,11 @@ module RuboCop
3
3
  # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
4
4
  # bit of our configuration.
5
5
  module Inject
6
- DEFAULT_FILE = File.expand_path(
7
- '../../../../config/default.yml', __FILE__
8
- )
9
-
10
6
  def self.defaults!
11
- path = File.absolute_path(DEFAULT_FILE)
7
+ path = CONFIG_DEFAULT.to_s
12
8
  hash = ConfigLoader.send(:load_yaml_configuration, path)
13
9
  config = Config.new(hash, path)
14
- puts "configuration from #{DEFAULT_FILE}" if ConfigLoader.debug?
10
+ puts "configuration from #{path}" if ConfigLoader.debug?
15
11
  config = ConfigLoader.merge_with_default(config, path)
16
12
  ConfigLoader.instance_variable_set(:@default_configuration, config)
17
13
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module RSpec
5
+ # RSpec public API methods that are commonly used in cops
6
+ module Language
7
+ # Set of method selectors
8
+ class SelectorSet
9
+ def initialize(selectors)
10
+ @selectors = selectors
11
+ end
12
+
13
+ def ==(other)
14
+ selectors.eql?(other.selectors)
15
+ end
16
+
17
+ def +(other)
18
+ self.class.new(selectors + other.selectors)
19
+ end
20
+
21
+ def include?(selector)
22
+ selectors.include?(selector)
23
+ end
24
+
25
+ def to_node_pattern
26
+ selectors.map(&:inspect).join(' ')
27
+ end
28
+
29
+ protected
30
+
31
+ attr_reader :selectors
32
+ end
33
+
34
+ module ExampleGroups
35
+ GROUPS = SelectorSet.new(%i(describe context feature example_group))
36
+ SKIPPED = SelectorSet.new(%i(xdescribe xcontext xfeature))
37
+ FOCUSED = SelectorSet.new(%i(fdescribe fcontext ffeature))
38
+
39
+ ALL = GROUPS + SKIPPED + FOCUSED
40
+ end
41
+
42
+ module SharedGroups
43
+ ALL = SelectorSet.new(
44
+ %i(shared_examples shared_context shared_examples_for)
45
+ )
46
+ end
47
+
48
+ module Examples
49
+ EXAMPLES = SelectorSet.new(%i(it specify example scenario its))
50
+ FOCUSED = SelectorSet.new(%i(fit fspecify fexample fscenario focus))
51
+ SKIPPED = SelectorSet.new(%i(xit xspecify xexample xscenario skip))
52
+ PENDING = SelectorSet.new(%i(pending))
53
+
54
+ ALL = EXAMPLES + FOCUSED + SKIPPED + PENDING
55
+ end
56
+
57
+ module Hooks
58
+ ALL = SelectorSet.new(%i(after around before))
59
+ end
60
+
61
+ module Helpers
62
+ ALL = SelectorSet.new(%i(let let!))
63
+ end
64
+
65
+ ALL =
66
+ ExampleGroups::ALL +
67
+ SharedGroups::ALL +
68
+ Examples::ALL +
69
+ Hooks::ALL +
70
+ Helpers::ALL
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module RSpec
5
+ module Language
6
+ # Common node matchers used for matching against the rspec DSL
7
+ module NodePattern
8
+ extend RuboCop::NodePattern::Macros
9
+
10
+ def_node_matcher :example_group?, <<-PATTERN
11
+ (block (send _ {#{ExampleGroups::ALL.to_node_pattern}} ...) ...)
12
+ PATTERN
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module RSpec
5
+ # Mixin for cops that skips non-spec files
6
+ #
7
+ # The criteria for whether rubocop-rspec analyzes a certain ruby file
8
+ # is configured via `AllCops/RSpec`. For example, if you want to
9
+ # customize your project to scan all files within a `test/` directory
10
+ # then you could add this to your configuration:
11
+ #
12
+ # @example configuring analyzed paths
13
+ #
14
+ # AllCops:
15
+ # RSpec:
16
+ # Patterns:
17
+ # - '_spec.rb$'
18
+ # - '(?:^|/)spec/'
19
+ #
20
+ # @note this functionality is implemented via this mixin instead of
21
+ # a subclass of `RuboCop::Cop::Cop` because the `Cop` class assumes
22
+ # that it will be the direct superclass of all cops. For example,
23
+ # if the ancestry of a cop looked like this:
24
+ #
25
+ # class RuboCop::RSpec::Cop < RuboCop::Cop::Cop
26
+ # end
27
+ #
28
+ # class RuboCop::RSpec::SpecCop < RuboCop::RSpec::Cop
29
+ # end
30
+ #
31
+ # then `SpecCop` will fail to be registered on the class instance
32
+ # variable of `Cop` which tracks all descendants via `.inherited`.
33
+ #
34
+ # While we could match this behavior and provide a rubocop-rspec Cop
35
+ # parent class, it would rely heavily on the implementation details
36
+ # of RuboCop itself which is largly private API. This would be
37
+ # irresponsible since any patch level release of rubocop could break
38
+ # integrations for users of rubocop-rspec
39
+ #
40
+ module SpecOnly
41
+ DEFAULT_CONFIGURATION = CONFIG.fetch('AllCops').fetch('RSpec')
42
+
43
+ def relevant_file?(file)
44
+ rspec_pattern =~ file && super
45
+ end
46
+
47
+ private
48
+
49
+ def rspec_pattern
50
+ Regexp.union(rspec_pattern_config.map(&Regexp.public_method(:new)))
51
+ end
52
+
53
+ def rspec_pattern_config
54
+ config
55
+ .for_all_cops
56
+ .fetch('RSpec', DEFAULT_CONFIGURATION)
57
+ .fetch('Patterns')
58
+ end
59
+ end
60
+ end
61
+ end
@@ -2,6 +2,12 @@ module RuboCop
2
2
  module RSpec
3
3
  # Helper methods for top level describe cops
4
4
  module TopLevelDescribe
5
+ extend NodePattern::Macros
6
+
7
+ def_node_matcher :described_constant, <<-PATTERN
8
+ (block $(send _ :describe $(const ...)) (args) $_)
9
+ PATTERN
10
+
5
11
  def on_send(node)
6
12
  return unless respond_to?(:on_top_level_describe)
7
13
  return unless top_level_describe?(node)