rubocop-rspec 1.7.0 → 3.0.2

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 (193) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +955 -79
  3. data/CODE_OF_CONDUCT.md +17 -0
  4. data/MIT-LICENSE.md +1 -2
  5. data/README.md +35 -35
  6. data/config/default.yml +940 -52
  7. data/config/obsoletion.yml +30 -0
  8. data/lib/rubocop/cop/rspec/align_left_let_brace.rb +49 -0
  9. data/lib/rubocop/cop/rspec/align_right_let_brace.rb +49 -0
  10. data/lib/rubocop/cop/rspec/any_instance.rb +10 -13
  11. data/lib/rubocop/cop/rspec/around_block.rb +97 -0
  12. data/lib/rubocop/cop/rspec/base.rb +26 -0
  13. data/lib/rubocop/cop/rspec/be.rb +39 -0
  14. data/lib/rubocop/cop/rspec/be_empty.rb +45 -0
  15. data/lib/rubocop/cop/rspec/be_eq.rb +47 -0
  16. data/lib/rubocop/cop/rspec/be_eql.rb +18 -15
  17. data/lib/rubocop/cop/rspec/be_nil.rb +74 -0
  18. data/lib/rubocop/cop/rspec/before_after_all.rb +45 -0
  19. data/lib/rubocop/cop/rspec/change_by_zero.rb +184 -0
  20. data/lib/rubocop/cop/rspec/class_check.rb +101 -0
  21. data/lib/rubocop/cop/rspec/contain_exactly.rb +56 -0
  22. data/lib/rubocop/cop/rspec/context_method.rb +57 -0
  23. data/lib/rubocop/cop/rspec/context_wording.rb +117 -0
  24. data/lib/rubocop/cop/rspec/describe_class.rb +52 -21
  25. data/lib/rubocop/cop/rspec/describe_method.rb +26 -11
  26. data/lib/rubocop/cop/rspec/describe_symbol.rb +37 -0
  27. data/lib/rubocop/cop/rspec/described_class.rb +181 -34
  28. data/lib/rubocop/cop/rspec/described_class_module_wrapping.rb +38 -0
  29. data/lib/rubocop/cop/rspec/dialect.rb +84 -0
  30. data/lib/rubocop/cop/rspec/duplicated_metadata.rb +58 -0
  31. data/lib/rubocop/cop/rspec/empty_example_group.rb +134 -47
  32. data/lib/rubocop/cop/rspec/empty_hook.rb +49 -0
  33. data/lib/rubocop/cop/rspec/empty_line_after_example.rb +82 -0
  34. data/lib/rubocop/cop/rspec/empty_line_after_example_group.rb +42 -0
  35. data/lib/rubocop/cop/rspec/empty_line_after_final_let.rb +40 -0
  36. data/lib/rubocop/cop/rspec/empty_line_after_hook.rb +82 -0
  37. data/lib/rubocop/cop/rspec/empty_line_after_subject.rb +36 -0
  38. data/lib/rubocop/cop/rspec/empty_metadata.rb +46 -0
  39. data/lib/rubocop/cop/rspec/empty_output.rb +47 -0
  40. data/lib/rubocop/cop/rspec/eq.rb +47 -0
  41. data/lib/rubocop/cop/rspec/example_length.rb +38 -20
  42. data/lib/rubocop/cop/rspec/example_without_description.rb +98 -0
  43. data/lib/rubocop/cop/rspec/example_wording.rb +117 -27
  44. data/lib/rubocop/cop/rspec/excessive_docstring_spacing.rb +110 -0
  45. data/lib/rubocop/cop/rspec/expect_actual.rb +46 -20
  46. data/lib/rubocop/cop/rspec/expect_change.rb +86 -0
  47. data/lib/rubocop/cop/rspec/expect_in_hook.rb +50 -0
  48. data/lib/rubocop/cop/rspec/expect_in_let.rb +42 -0
  49. data/lib/rubocop/cop/rspec/expect_output.rb +50 -0
  50. data/lib/rubocop/cop/rspec/focus.rb +79 -25
  51. data/lib/rubocop/cop/rspec/hook_argument.rb +48 -36
  52. data/lib/rubocop/cop/rspec/hooks_before_examples.rb +81 -0
  53. data/lib/rubocop/cop/rspec/identical_equality_assertion.rb +37 -0
  54. data/lib/rubocop/cop/rspec/implicit_block_expectation.rb +68 -0
  55. data/lib/rubocop/cop/rspec/implicit_expect.rb +100 -0
  56. data/lib/rubocop/cop/rspec/implicit_subject.rb +167 -0
  57. data/lib/rubocop/cop/rspec/indexed_let.rb +112 -0
  58. data/lib/rubocop/cop/rspec/instance_spy.rb +74 -0
  59. data/lib/rubocop/cop/rspec/instance_variable.rb +28 -14
  60. data/lib/rubocop/cop/rspec/is_expected_specify.rb +45 -0
  61. data/lib/rubocop/cop/rspec/it_behaves_like.rb +49 -0
  62. data/lib/rubocop/cop/rspec/iterated_expectation.rb +74 -0
  63. data/lib/rubocop/cop/rspec/leading_subject.rb +57 -29
  64. data/lib/rubocop/cop/rspec/leaky_constant_declaration.rb +127 -0
  65. data/lib/rubocop/cop/rspec/let_before_examples.rb +101 -0
  66. data/lib/rubocop/cop/rspec/let_setup.rb +32 -16
  67. data/lib/rubocop/cop/rspec/match_array.rb +59 -0
  68. data/lib/rubocop/cop/rspec/message_chain.rb +10 -15
  69. data/lib/rubocop/cop/rspec/message_expectation.rb +12 -9
  70. data/lib/rubocop/cop/rspec/message_spies.rb +88 -0
  71. data/lib/rubocop/cop/rspec/metadata_style.rb +202 -0
  72. data/lib/rubocop/cop/rspec/missing_example_group_argument.rb +35 -0
  73. data/lib/rubocop/cop/rspec/missing_expectation_target_method.rb +54 -0
  74. data/lib/rubocop/cop/rspec/mixin/comments_help.rb +38 -0
  75. data/lib/rubocop/cop/rspec/mixin/empty_line_separation.rb +59 -0
  76. data/lib/rubocop/cop/rspec/mixin/file_help.rb +14 -0
  77. data/lib/rubocop/cop/rspec/mixin/final_end_location.rb +19 -0
  78. data/lib/rubocop/cop/rspec/mixin/inside_example_group.rb +29 -0
  79. data/lib/rubocop/cop/rspec/mixin/location_help.rb +37 -0
  80. data/lib/rubocop/cop/rspec/mixin/metadata.rb +63 -0
  81. data/lib/rubocop/cop/rspec/mixin/namespace.rb +23 -0
  82. data/lib/rubocop/cop/rspec/mixin/skip_or_pending.rb +39 -0
  83. data/lib/rubocop/cop/rspec/mixin/top_level_group.rb +54 -0
  84. data/lib/rubocop/cop/rspec/mixin/variable.rb +21 -0
  85. data/lib/rubocop/cop/rspec/multiple_describes.rb +14 -12
  86. data/lib/rubocop/cop/rspec/multiple_expectations.rb +86 -26
  87. data/lib/rubocop/cop/rspec/multiple_memoized_helpers.rb +146 -0
  88. data/lib/rubocop/cop/rspec/multiple_subjects.rb +97 -0
  89. data/lib/rubocop/cop/rspec/named_subject.rb +107 -27
  90. data/lib/rubocop/cop/rspec/nested_groups.rb +84 -47
  91. data/lib/rubocop/cop/rspec/no_expectation_example.rb +102 -0
  92. data/lib/rubocop/cop/rspec/not_to_not.rb +30 -27
  93. data/lib/rubocop/cop/rspec/overwriting_setup.rb +74 -0
  94. data/lib/rubocop/cop/rspec/pending.rb +80 -0
  95. data/lib/rubocop/cop/rspec/pending_without_reason.rb +159 -0
  96. data/lib/rubocop/cop/rspec/predicate_matcher.rb +341 -0
  97. data/lib/rubocop/cop/rspec/receive_counts.rb +89 -0
  98. data/lib/rubocop/cop/rspec/receive_messages.rb +161 -0
  99. data/lib/rubocop/cop/rspec/receive_never.rb +41 -0
  100. data/lib/rubocop/cop/rspec/redundant_around.rb +65 -0
  101. data/lib/rubocop/cop/rspec/redundant_predicate_matcher.rb +67 -0
  102. data/lib/rubocop/cop/rspec/remove_const.rb +39 -0
  103. data/lib/rubocop/cop/rspec/repeated_description.rb +98 -0
  104. data/lib/rubocop/cop/rspec/repeated_example.rb +53 -0
  105. data/lib/rubocop/cop/rspec/repeated_example_group_body.rb +100 -0
  106. data/lib/rubocop/cop/rspec/repeated_example_group_description.rb +96 -0
  107. data/lib/rubocop/cop/rspec/repeated_include_example.rb +105 -0
  108. data/lib/rubocop/cop/rspec/repeated_subject_call.rb +125 -0
  109. data/lib/rubocop/cop/rspec/return_from_stub.rb +169 -0
  110. data/lib/rubocop/cop/rspec/scattered_let.rb +59 -0
  111. data/lib/rubocop/cop/rspec/scattered_setup.rb +92 -0
  112. data/lib/rubocop/cop/rspec/shared_context.rb +107 -0
  113. data/lib/rubocop/cop/rspec/shared_examples.rb +125 -0
  114. data/lib/rubocop/cop/rspec/single_argument_message_chain.rb +93 -0
  115. data/lib/rubocop/cop/rspec/skip_block_inside_example.rb +46 -0
  116. data/lib/rubocop/cop/rspec/sort_metadata.rb +71 -0
  117. data/lib/rubocop/cop/rspec/spec_file_path_format.rb +133 -0
  118. data/lib/rubocop/cop/rspec/spec_file_path_suffix.rb +40 -0
  119. data/lib/rubocop/cop/rspec/stubbed_mock.rb +176 -0
  120. data/lib/rubocop/cop/rspec/subject_declaration.rb +46 -0
  121. data/lib/rubocop/cop/rspec/subject_stub.rb +93 -74
  122. data/lib/rubocop/cop/rspec/undescriptive_literals_description.rb +69 -0
  123. data/lib/rubocop/cop/rspec/unspecified_exception.rb +67 -0
  124. data/lib/rubocop/cop/rspec/variable_definition.rb +77 -0
  125. data/lib/rubocop/cop/rspec/variable_name.rb +68 -0
  126. data/lib/rubocop/cop/rspec/verified_double_reference.rb +111 -0
  127. data/lib/rubocop/cop/rspec/verified_doubles.rb +28 -14
  128. data/lib/rubocop/cop/rspec/void_expect.rb +60 -0
  129. data/lib/rubocop/cop/rspec/yield.rb +82 -0
  130. data/lib/rubocop/cop/rspec_cops.rb +112 -0
  131. data/lib/rubocop/rspec/align_let_brace.rb +63 -0
  132. data/lib/rubocop/rspec/concept.rb +33 -0
  133. data/lib/rubocop/rspec/config_formatter.rb +27 -4
  134. data/lib/rubocop/rspec/cop/generator.rb +25 -0
  135. data/lib/rubocop/rspec/corrector/move_node.rb +51 -0
  136. data/lib/rubocop/rspec/description_extractor.rb +60 -18
  137. data/lib/rubocop/rspec/example.rb +37 -0
  138. data/lib/rubocop/rspec/example_group.rb +67 -0
  139. data/lib/rubocop/rspec/hook.rb +79 -0
  140. data/lib/rubocop/rspec/inject.rb +3 -1
  141. data/lib/rubocop/rspec/language.rb +184 -41
  142. data/lib/rubocop/rspec/node.rb +19 -0
  143. data/lib/rubocop/rspec/shared_contexts/default_rspec_language_config_context.rb +29 -0
  144. data/lib/rubocop/rspec/version.rb +1 -1
  145. data/lib/rubocop/rspec/wording.rb +61 -19
  146. data/lib/rubocop/rspec.rb +6 -2
  147. data/lib/rubocop-rspec.rb +45 -34
  148. metadata +130 -195
  149. data/Gemfile +0 -13
  150. data/Rakefile +0 -48
  151. data/lib/rubocop/cop/rspec/file_path.rb +0 -83
  152. data/lib/rubocop/rspec/language/node_pattern.rb +0 -16
  153. data/lib/rubocop/rspec/spec_only.rb +0 -61
  154. data/lib/rubocop/rspec/top_level_describe.rb +0 -61
  155. data/lib/rubocop/rspec/util.rb +0 -19
  156. data/rubocop-rspec.gemspec +0 -42
  157. data/spec/expect_violation/expectation_spec.rb +0 -85
  158. data/spec/project/changelog_spec.rb +0 -81
  159. data/spec/project/default_config_spec.rb +0 -52
  160. data/spec/project/project_requires_spec.rb +0 -8
  161. data/spec/rubocop/cop/rspec/any_instance_spec.rb +0 -30
  162. data/spec/rubocop/cop/rspec/be_eql_spec.rb +0 -59
  163. data/spec/rubocop/cop/rspec/describe_class_spec.rb +0 -113
  164. data/spec/rubocop/cop/rspec/describe_method_spec.rb +0 -32
  165. data/spec/rubocop/cop/rspec/described_class_spec.rb +0 -219
  166. data/spec/rubocop/cop/rspec/empty_example_group_spec.rb +0 -79
  167. data/spec/rubocop/cop/rspec/example_length_spec.rb +0 -117
  168. data/spec/rubocop/cop/rspec/example_wording_spec.rb +0 -82
  169. data/spec/rubocop/cop/rspec/expect_actual_spec.rb +0 -136
  170. data/spec/rubocop/cop/rspec/file_path_spec.rb +0 -236
  171. data/spec/rubocop/cop/rspec/focus_spec.rb +0 -130
  172. data/spec/rubocop/cop/rspec/hook_argument_spec.rb +0 -189
  173. data/spec/rubocop/cop/rspec/instance_variable_spec.rb +0 -75
  174. data/spec/rubocop/cop/rspec/leading_subject_spec.rb +0 -54
  175. data/spec/rubocop/cop/rspec/let_setup_spec.rb +0 -66
  176. data/spec/rubocop/cop/rspec/message_chain_spec.rb +0 -21
  177. data/spec/rubocop/cop/rspec/message_expectation_spec.rb +0 -63
  178. data/spec/rubocop/cop/rspec/multiple_describes_spec.rb +0 -28
  179. data/spec/rubocop/cop/rspec/multiple_expectations_spec.rb +0 -84
  180. data/spec/rubocop/cop/rspec/named_subject_spec.rb +0 -62
  181. data/spec/rubocop/cop/rspec/nested_groups_spec.rb +0 -55
  182. data/spec/rubocop/cop/rspec/not_to_not_spec.rb +0 -57
  183. data/spec/rubocop/cop/rspec/subject_stub_spec.rb +0 -183
  184. data/spec/rubocop/cop/rspec/verified_doubles_spec.rb +0 -71
  185. data/spec/rubocop/rspec/config_formatter_spec.rb +0 -48
  186. data/spec/rubocop/rspec/description_extractor_spec.rb +0 -35
  187. data/spec/rubocop/rspec/language/selector_set_spec.rb +0 -29
  188. data/spec/rubocop/rspec/spec_only_spec.rb +0 -97
  189. data/spec/rubocop/rspec/util/one_spec.rb +0 -21
  190. data/spec/rubocop/rspec/wording_spec.rb +0 -44
  191. data/spec/shared/rspec_only_cop_behavior.rb +0 -68
  192. data/spec/spec_helper.rb +0 -41
  193. data/spec/support/expect_violation.rb +0 -166
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Sort RSpec metadata alphabetically.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # describe 'Something', :b, :a
11
+ # context 'Something', foo: 'bar', baz: true
12
+ # it 'works', :b, :a, foo: 'bar', baz: true
13
+ #
14
+ # # good
15
+ # describe 'Something', :a, :b
16
+ # context 'Something', baz: true, foo: 'bar'
17
+ # it 'works', :a, :b, baz: true, foo: 'bar'
18
+ #
19
+ class SortMetadata < Base
20
+ extend AutoCorrector
21
+ include Metadata
22
+ include RangeHelp
23
+
24
+ MSG = 'Sort metadata alphabetically.'
25
+
26
+ def on_metadata(symbols, hash)
27
+ pairs = hash&.pairs || []
28
+ return if sorted?(symbols, pairs)
29
+
30
+ crime_scene = crime_scene(symbols, pairs)
31
+ add_offense(crime_scene) do |corrector|
32
+ corrector.replace(crime_scene, replacement(symbols, pairs))
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def crime_scene(symbols, pairs)
39
+ metadata = symbols + pairs
40
+
41
+ range_between(
42
+ metadata.first.source_range.begin_pos,
43
+ metadata.last.source_range.end_pos
44
+ )
45
+ end
46
+
47
+ def replacement(symbols, pairs)
48
+ (sort_symbols(symbols) + sort_pairs(pairs)).map(&:source).join(', ')
49
+ end
50
+
51
+ def sorted?(symbols, pairs)
52
+ symbols == sort_symbols(symbols) && pairs == sort_pairs(pairs)
53
+ end
54
+
55
+ def sort_pairs(pairs)
56
+ pairs.sort_by { |pair| pair.key.source.downcase }
57
+ end
58
+
59
+ def sort_symbols(symbols)
60
+ symbols.sort_by do |symbol|
61
+ if symbol.str_type? || symbol.sym_type?
62
+ symbol.value.to_s.downcase
63
+ else
64
+ symbol.source.downcase
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks that spec file paths are consistent and well-formed.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # whatever_spec.rb # describe MyClass
11
+ # my_class_spec.rb # describe MyClass, '#method'
12
+ #
13
+ # # good
14
+ # my_class_spec.rb # describe MyClass
15
+ # my_class_method_spec.rb # describe MyClass, '#method'
16
+ # my_class/method_spec.rb # describe MyClass, '#method'
17
+ #
18
+ # @example `CustomTransform: {RuboCop=>rubocop, RSpec=>rspec}` (default)
19
+ # # good
20
+ # rubocop_spec.rb # describe RuboCop
21
+ # rspec_spec.rb # describe RSpec
22
+ #
23
+ # @example `IgnoreMethods: false` (default)
24
+ # # bad
25
+ # my_class_spec.rb # describe MyClass, '#method'
26
+ #
27
+ # @example `IgnoreMethods: true`
28
+ # # good
29
+ # my_class_spec.rb # describe MyClass, '#method'
30
+ #
31
+ # @example `IgnoreMetadata: {type=>routing}` (default)
32
+ # # good
33
+ # whatever_spec.rb # describe MyClass, type: :routing do; end
34
+ #
35
+ class SpecFilePathFormat < Base
36
+ include TopLevelGroup
37
+ include Namespace
38
+ include FileHelp
39
+
40
+ MSG = 'Spec path should end with `%<suffix>s`.'
41
+
42
+ # @!method example_group_arguments(node)
43
+ def_node_matcher :example_group_arguments, <<~PATTERN
44
+ (block $(send #rspec? #ExampleGroups.all $_ $...) ...)
45
+ PATTERN
46
+
47
+ # @!method metadata_key_value(node)
48
+ def_node_search :metadata_key_value, '(pair (sym $_key) (sym $_value))'
49
+
50
+ def on_top_level_example_group(node)
51
+ return unless top_level_groups.one?
52
+
53
+ example_group_arguments(node) do |send_node, class_name, arguments|
54
+ next if !class_name.const_type? || ignore_metadata?(arguments)
55
+
56
+ ensure_correct_file_path(send_node, class_name, arguments)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def ensure_correct_file_path(send_node, class_name, arguments)
63
+ pattern = correct_path_pattern(class_name, arguments)
64
+ return if filename_ends_with?(pattern)
65
+
66
+ # For the suffix shown in the offense message, modify the regular
67
+ # expression pattern to resemble a glob pattern for clearer error
68
+ # messages.
69
+ suffix = pattern.sub('.*', '*').sub('[^/]*', '*').sub('\.', '.')
70
+ add_offense(send_node, message: format(MSG, suffix: suffix))
71
+ end
72
+
73
+ def ignore_metadata?(arguments)
74
+ arguments.any? do |argument|
75
+ metadata_key_value(argument).any? do |key, value|
76
+ ignore_metadata.values_at(key.to_s).include?(value.to_s)
77
+ end
78
+ end
79
+ end
80
+
81
+ def correct_path_pattern(class_name, arguments)
82
+ path = [expected_path(class_name)]
83
+ path << '.*' unless ignore?(arguments.first)
84
+ path << [name_pattern(arguments.first), '[^/]*_spec\.rb']
85
+ path.join
86
+ end
87
+
88
+ def name_pattern(method_name)
89
+ return if ignore?(method_name)
90
+
91
+ method_name.str_content.gsub(/\s/, '_').gsub(/\W/, '')
92
+ end
93
+
94
+ def ignore?(method_name)
95
+ !method_name&.str_type? || ignore_methods?
96
+ end
97
+
98
+ def expected_path(constant)
99
+ constants = namespace(constant) + constant.const_name.split('::')
100
+
101
+ File.join(
102
+ constants.map do |name|
103
+ custom_transform.fetch(name) { camel_to_snake_case(name) }
104
+ end
105
+ )
106
+ end
107
+
108
+ def camel_to_snake_case(string)
109
+ string
110
+ .gsub(/([^A-Z])([A-Z]+)/, '\1_\2')
111
+ .gsub(/([A-Z])([A-Z][^A-Z\d]+)/, '\1_\2')
112
+ .downcase
113
+ end
114
+
115
+ def custom_transform
116
+ cop_config.fetch('CustomTransform', {})
117
+ end
118
+
119
+ def ignore_methods?
120
+ cop_config['IgnoreMethods']
121
+ end
122
+
123
+ def ignore_metadata
124
+ cop_config.fetch('IgnoreMetadata', {})
125
+ end
126
+
127
+ def filename_ends_with?(pattern)
128
+ expanded_file_path.match?("#{pattern}$")
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks that spec file paths suffix are consistent and well-formed.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # my_class/foo_specorb.rb # describe MyClass
11
+ # spec/models/user.rb # describe User
12
+ # spec/models/user_specxrb # describe User
13
+ #
14
+ # # good
15
+ # my_class_spec.rb # describe MyClass
16
+ #
17
+ # # good - shared examples are allowed
18
+ # spec/models/user.rb # shared_examples_for 'foo'
19
+ #
20
+ class SpecFilePathSuffix < Base
21
+ include TopLevelGroup
22
+ include FileHelp
23
+
24
+ MSG = 'Spec path should end with `_spec.rb`.'
25
+
26
+ def on_top_level_example_group(node)
27
+ example_group?(node) do
28
+ add_global_offense(MSG) unless correct_path?
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def correct_path?
35
+ expanded_file_path.end_with?('_spec.rb')
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks that message expectations do not have a configured response.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # expect(foo).to receive(:bar).with(42).and_return("hello world")
11
+ #
12
+ # # good (without spies)
13
+ # allow(foo).to receive(:bar).with(42).and_return("hello world")
14
+ # expect(foo).to receive(:bar).with(42)
15
+ #
16
+ class StubbedMock < Base
17
+ MSG = 'Prefer `%<replacement>s` over `%<method_name>s` when ' \
18
+ 'configuring a response.'
19
+
20
+ # @!method message_expectation?(node)
21
+ # Match message expectation matcher
22
+ #
23
+ # @example source that matches
24
+ # receive(:foo)
25
+ #
26
+ # @example source that matches
27
+ # receive_message_chain(:foo, :bar)
28
+ #
29
+ # @example source that matches
30
+ # receive(:foo).with('bar')
31
+ #
32
+ # @param node [RuboCop::AST::Node]
33
+ # @return [Array<RuboCop::AST::Node>] matching nodes
34
+ def_node_matcher :message_expectation?, <<~PATTERN
35
+ {
36
+ (send nil? { :receive :receive_message_chain } ...) # receive(:foo)
37
+ (send (send nil? :receive ...) :with ...) # receive(:foo).with('bar')
38
+ }
39
+ PATTERN
40
+
41
+ # @!method configured_response?(node)
42
+ def_node_matcher :configured_response?, <<~PATTERN
43
+ { :and_return :and_raise :and_throw :and_yield
44
+ :and_call_original :and_wrap_original }
45
+ PATTERN
46
+
47
+ # @!method expectation(node)
48
+ # Match expectation
49
+ #
50
+ # @example source that matches
51
+ # is_expected.to be_in_the_bar
52
+ #
53
+ # @example source that matches
54
+ # expect(cocktail).to contain_exactly(:fresh_orange_juice, :campari)
55
+ #
56
+ # @example source that matches
57
+ # expect_any_instance_of(Officer).to be_alert
58
+ #
59
+ # @param node [RuboCop::AST::Node]
60
+ # @yield [RuboCop::AST::Node] expectation, method name, matcher
61
+ def_node_matcher :expectation, <<~PATTERN
62
+ (send
63
+ $(send nil? $#Expectations.all ...)
64
+ :to $_)
65
+ PATTERN
66
+
67
+ # @!method matcher_with_configured_response(node)
68
+ # Match matcher with a configured response
69
+ #
70
+ # @example source that matches
71
+ # receive(:foo).and_return('bar')
72
+ #
73
+ # @example source that matches
74
+ # receive(:lower).and_raise(SomeError)
75
+ #
76
+ # @example source that matches
77
+ # receive(:redirect).and_call_original
78
+ #
79
+ # @param node [RuboCop::AST::Node]
80
+ # @yield [RuboCop::AST::Node] matcher
81
+ def_node_matcher :matcher_with_configured_response, <<~PATTERN
82
+ (send #message_expectation? #configured_response? _)
83
+ PATTERN
84
+
85
+ # @!method matcher_with_return_block(node)
86
+ # Match matcher with a return block
87
+ #
88
+ # @example source that matches
89
+ # receive(:foo) { 'bar' }
90
+ #
91
+ # @param node [RuboCop::AST::Node]
92
+ # @yield [RuboCop::AST::Node] matcher
93
+ def_node_matcher :matcher_with_return_block, <<~PATTERN
94
+ (block #message_expectation? (args) _) # receive(:foo) { 'bar' }
95
+ PATTERN
96
+
97
+ # @!method matcher_with_hash(node)
98
+ # Match matcher with a configured response defined as a hash
99
+ #
100
+ # @example source that matches
101
+ # receive_messages(foo: 'bar', baz: 'qux')
102
+ #
103
+ # @example source that matches
104
+ # receive_message_chain(:foo, bar: 'baz')
105
+ #
106
+ # @param node [RuboCop::AST::Node]
107
+ # @yield [RuboCop::AST::Node] matcher
108
+ def_node_matcher :matcher_with_hash, <<~PATTERN
109
+ {
110
+ (send nil? :receive_messages hash) # receive_messages(foo: 'bar', baz: 'qux')
111
+ (send nil? :receive_message_chain ... hash) # receive_message_chain(:foo, bar: 'baz')
112
+ }
113
+ PATTERN
114
+
115
+ # @!method matcher_with_blockpass(node)
116
+ # Match matcher with a configured response in block-pass
117
+ #
118
+ # @example source that matches
119
+ # receive(:foo, &canned)
120
+ #
121
+ # @example source that matches
122
+ # receive_message_chain(:foo, :bar, &canned)
123
+ #
124
+ # @example source that matches
125
+ # receive(:foo).with('bar', &canned)
126
+ #
127
+ # @param node [RuboCop::AST::Node]
128
+ # @yield [RuboCop::AST::Node] matcher
129
+ def_node_matcher :matcher_with_blockpass, <<~PATTERN
130
+ {
131
+ (send nil? { :receive :receive_message_chain } ... block_pass) # receive(:foo, &canned)
132
+ (send (send nil? :receive ...) :with ... block_pass) # receive(:foo).with('foo', &canned)
133
+ }
134
+ PATTERN
135
+
136
+ RESTRICT_ON_SEND = %i[to].freeze
137
+
138
+ def on_send(node)
139
+ expectation(node) do |expectation, method_name, matcher|
140
+ on_expectation(expectation, method_name, matcher)
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def on_expectation(expectation, method_name, matcher)
147
+ flag_expectation = lambda do
148
+ add_offense(expectation, message: msg(method_name))
149
+ end
150
+
151
+ matcher_with_configured_response(matcher, &flag_expectation)
152
+ matcher_with_return_block(matcher, &flag_expectation)
153
+ matcher_with_hash(matcher, &flag_expectation)
154
+ matcher_with_blockpass(matcher, &flag_expectation)
155
+ end
156
+
157
+ def msg(method_name)
158
+ format(MSG,
159
+ method_name: method_name,
160
+ replacement: replacement(method_name))
161
+ end
162
+
163
+ def replacement(method_name)
164
+ case method_name
165
+ when :expect
166
+ :allow
167
+ when :is_expected
168
+ 'allow(subject)'
169
+ when :expect_any_instance_of
170
+ :allow_any_instance_of
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Ensure that subject is defined using subject helper.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # let(:subject) { foo }
11
+ # let!(:subject) { foo }
12
+ # subject(:subject) { foo }
13
+ # subject!(:subject) { foo }
14
+ #
15
+ # # bad
16
+ # block = -> {}
17
+ # let(:subject, &block)
18
+ #
19
+ # # good
20
+ # subject(:test_subject) { foo }
21
+ #
22
+ class SubjectDeclaration < Base
23
+ MSG_LET = 'Use subject explicitly rather than using let'
24
+ MSG_REDUNDANT = 'Ambiguous declaration of subject'
25
+
26
+ # @!method offensive_subject_declaration?(node)
27
+ def_node_matcher :offensive_subject_declaration?, <<~PATTERN
28
+ (send nil? ${#Subjects.all #Helpers.all} ({sym str} #Subjects.all) ...)
29
+ PATTERN
30
+
31
+ def on_send(node)
32
+ offense = offensive_subject_declaration?(node)
33
+ return unless offense
34
+
35
+ add_offense(node, message: message_for(offense))
36
+ end
37
+
38
+ private
39
+
40
+ def message_for(offense)
41
+ Helpers.all(offense) ? MSG_LET : MSG_REDUNDANT
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,51 +1,86 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  module RuboCop
4
6
  module Cop
5
7
  module RSpec
6
8
  # Checks for stubbed test subjects.
7
9
  #
10
+ # Checks nested subject stubs for innermost subject definition
11
+ # when subject is also defined in parent example groups.
12
+ #
8
13
  # @see https://robots.thoughtbot.com/don-t-stub-the-system-under-test
14
+ # @see https://penelope.zone/2015/12/27/introducing-rspec-smells-and-where-to-find-them.html#smell-1-stubjec
9
15
  #
10
16
  # @example
11
17
  # # bad
12
- # describe Foo do
13
- # subject(:bar) { baz }
18
+ # describe Article do
19
+ # subject(:article) { Article.new }
20
+ #
21
+ # it 'indicates that the author is unknown' do
22
+ # allow(article).to receive(:author).and_return(nil)
23
+ # expect(article.description).to include('by an unknown author')
24
+ # end
25
+ # end
26
+ #
27
+ # # bad
28
+ # describe Article do
29
+ # subject(:foo) { Article.new }
30
+ #
31
+ # context 'nested subject' do
32
+ # subject(:article) { Article.new }
14
33
  #
15
- # before do
16
- # allow(bar).to receive(:qux?).and_return(true)
34
+ # it 'indicates that the author is unknown' do
35
+ # allow(article).to receive(:author).and_return(nil)
36
+ # expect(article.description).to include('by an unknown author')
37
+ # end
17
38
  # end
18
39
  # end
19
40
  #
20
- class SubjectStub < Cop
21
- include RuboCop::RSpec::SpecOnly,
22
- RuboCop::RSpec::TopLevelDescribe,
23
- RuboCop::RSpec::Language,
24
- RuboCop::RSpec::Language::NodePattern
41
+ # # good
42
+ # describe Article do
43
+ # subject(:article) { Article.new(author: nil) }
44
+ #
45
+ # it 'indicates that the author is unknown' do
46
+ # expect(article.description).to include('by an unknown author')
47
+ # end
48
+ # end
49
+ #
50
+ class SubjectStub < Base
51
+ include TopLevelGroup
25
52
 
26
- MSG = 'Do not stub your test subject.'.freeze
53
+ MSG = 'Do not stub methods of the object under test.'
27
54
 
28
- # @!method subject(node)
55
+ # @!method subject?(node)
29
56
  # Find a named or unnamed subject definition
30
57
  #
31
58
  # @example anonymous subject
32
- # subject(parse('subject { foo }').ast) do |name|
59
+ # subject?(parse('subject { foo }').ast) do |name|
33
60
  # name # => :subject
34
61
  # end
35
62
  #
36
63
  # @example named subject
37
- # subject(parse('subject(:thing) { foo }').ast) do |name|
64
+ # subject?(parse('subject(:thing) { foo }').ast) do |name|
38
65
  # name # => :thing
39
66
  # end
40
67
  #
41
- # @param node [RuboCop::Node]
68
+ # @param node [RuboCop::AST::Node]
42
69
  #
43
70
  # @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
- }
71
+ def_node_matcher :subject?, <<~PATTERN
72
+ (block
73
+ (send nil?
74
+ { #Subjects.all (sym $_) | $#Subjects.all }
75
+ ) args ...)
76
+ PATTERN
77
+
78
+ # @!method let?(node)
79
+ # Find a memoized helper
80
+ def_node_matcher :let?, <<~PATTERN
81
+ (block
82
+ (send nil? :let (sym $_)
83
+ ) args ...)
49
84
  PATTERN
50
85
 
51
86
  # @!method message_expectation?(node, method_name)
@@ -58,76 +93,60 @@ module RuboCop
58
93
  # expect(foo).to receive(:bar)
59
94
  # expect(foo).to receive(:bar).with(1)
60
95
  # 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
- }
96
+ #
97
+ def_node_matcher :message_expectation?, <<~PATTERN
98
+ (send
99
+ {
100
+ (send nil? { :expect :allow } (send nil? %))
101
+ (send nil? :is_expected)
102
+ }
103
+ #Runners.all
104
+ #message_expectation_matcher?
105
+ )
66
106
  PATTERN
67
107
 
68
- def_node_search :receive_message?, '(send nil :receive ...)'
108
+ # @!method message_expectation_matcher?(node)
109
+ def_node_search :message_expectation_matcher?, <<~PATTERN
110
+ (send nil? {
111
+ :receive :receive_messages :receive_message_chain :have_received
112
+ } ...)
113
+ PATTERN
69
114
 
70
- def on_block(node)
71
- return unless example_group?(node)
115
+ def on_top_level_group(node)
116
+ @explicit_subjects = find_all_explicit(node) { |n| subject?(n) }
117
+ @subject_overrides = find_all_explicit(node) { |n| let?(n) }
72
118
 
73
- find_subject_stub(node) { |stub| add_offense(stub, :expression) }
119
+ find_subject_expectations(node) do |stub|
120
+ add_offense(stub)
121
+ end
74
122
  end
75
123
 
76
124
  private
77
125
 
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
126
+ def find_all_explicit(node)
127
+ node.each_descendant(:block).with_object({}) do |child, h|
128
+ name = yield(child)
129
+ next unless name
88
130
 
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)
131
+ outer_example_group = child.each_ancestor(:block).find do |a|
132
+ example_group?(a)
133
+ end
98
134
 
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)
135
+ h[outer_example_group] ||= []
136
+ h[outer_example_group] << name
105
137
  end
106
138
  end
107
139
 
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
140
+ def find_subject_expectations(node, subject_names = [], &block)
141
+ subject_names = [*subject_names, *@explicit_subjects[node]]
142
+ subject_names -= @subject_overrides[node] if @subject_overrides[node]
118
143
 
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) }
144
+ names = Set[*subject_names, :subject]
145
+ expectation_detected = message_expectation?(node, names)
146
+ return yield(node) if expectation_detected
128
147
 
129
- node.each_child_node do |child|
130
- find_subject(child, parent: node, &block)
148
+ node.each_child_node(:send, :def, :block, :begin) do |child|
149
+ find_subject_expectations(child, subject_names, &block)
131
150
  end
132
151
  end
133
152
  end