rubocop-rspec 1.6.0 → 1.7.0

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