rubocop-rspec 1.7.0 → 3.0.2

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