rubocop-rspec 2.13.2 → 2.14.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +402 -383
  3. data/CODE_OF_CONDUCT.md +4 -4
  4. data/MIT-LICENSE.md +1 -2
  5. data/config/default.yml +65 -4
  6. data/lib/rubocop/cop/rspec/capybara/negation_matcher.rb +106 -0
  7. data/lib/rubocop/cop/rspec/capybara/specific_actions.rb +85 -0
  8. data/lib/rubocop/cop/rspec/capybara/specific_matcher.rb +5 -82
  9. data/lib/rubocop/cop/rspec/change_by_zero.rb +1 -1
  10. data/lib/rubocop/cop/rspec/context_wording.rb +4 -2
  11. data/lib/rubocop/cop/rspec/example_wording.rb +32 -0
  12. data/lib/rubocop/cop/rspec/factory_bot/consistent_parentheses_style.rb +99 -0
  13. data/lib/rubocop/cop/rspec/factory_bot/create_list.rb +2 -2
  14. data/lib/rubocop/cop/rspec/factory_bot/syntax_methods.rb +1 -19
  15. data/lib/rubocop/cop/rspec/implicit_subject.rb +86 -19
  16. data/lib/rubocop/cop/rspec/let_before_examples.rb +15 -1
  17. data/lib/rubocop/cop/rspec/mixin/capybara_help.rb +80 -0
  18. data/lib/rubocop/cop/rspec/mixin/css_selector.rb +48 -1
  19. data/lib/rubocop/cop/rspec/mixin/skip_or_pending.rb +23 -0
  20. data/lib/rubocop/cop/rspec/no_expectation_example.rb +42 -9
  21. data/lib/rubocop/cop/rspec/pending.rb +2 -11
  22. data/lib/rubocop/cop/rspec/rails/inferred_spec_type.rb +135 -0
  23. data/lib/rubocop/cop/rspec/repeated_include_example.rb +1 -1
  24. data/lib/rubocop/cop/rspec/sort_metadata.rb +102 -0
  25. data/lib/rubocop/cop/rspec/subject_declaration.rb +1 -1
  26. data/lib/rubocop/cop/rspec_cops.rb +5 -0
  27. data/lib/rubocop/rspec/factory_bot/language.rb +20 -0
  28. data/lib/rubocop/rspec/version.rb +1 -1
  29. data/lib/rubocop-rspec.rb +2 -0
  30. metadata +9 -2
data/CODE_OF_CONDUCT.md CHANGED
@@ -9,9 +9,9 @@ to the RuboCop community. It applies to all "collaborative spaces", which are
9
9
  defined as community communications channels (such as mailing lists, submitted
10
10
  patches, commit comments, etc.).
11
11
 
12
- * Participants will be tolerant of opposing views.
13
- * Participants must ensure that their language and actions are free of personal
12
+ - Participants will be tolerant of opposing views.
13
+ - Participants must ensure that their language and actions are free of personal
14
14
  attacks and disparaging personal remarks.
15
- * When interpreting the words and actions of others, participants should always
15
+ - When interpreting the words and actions of others, participants should always
16
16
  assume good intentions.
17
- * Behaviour which can be reasonably considered harassment will not be tolerated.
17
+ - Behaviour which can be reasonably considered harassment will not be tolerated.
data/MIT-LICENSE.md CHANGED
@@ -1,5 +1,4 @@
1
- The MIT License (MIT)
2
- =====================
1
+ # The MIT License (MIT)
3
2
 
4
3
  Copyright (c) 2014 Ian MacLeod <ian@nevir.net>
5
4
 
data/config/default.yml CHANGED
@@ -194,7 +194,7 @@ RSpec/ChangeByZero:
194
194
  Description: Prefer negated matchers over `to change.by(0)`.
195
195
  Enabled: pending
196
196
  VersionAdded: '2.11'
197
- VersionChanged: '2.13'
197
+ VersionChanged: '2.14'
198
198
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ChangeByZero
199
199
  NegatedMatcher: ~
200
200
 
@@ -383,8 +383,10 @@ RSpec/ExampleWording:
383
383
  have: has
384
384
  HAVE: HAS
385
385
  IgnoredWords: []
386
+ DisallowedExamples:
387
+ - works
386
388
  VersionAdded: '1.0'
387
- VersionChanged: '1.2'
389
+ VersionChanged: '2.13'
388
390
  StyleGuide: https://rspec.rubystyle.guide/#should-in-example-docstrings
389
391
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExampleWording
390
392
 
@@ -492,8 +494,9 @@ RSpec/ImplicitSubject:
492
494
  - single_line_only
493
495
  - single_statement_only
494
496
  - disallow
497
+ - require_implicit
495
498
  VersionAdded: '1.29'
496
- VersionChanged: '1.30'
499
+ VersionChanged: '2.13'
497
500
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ImplicitSubject
498
501
 
499
502
  RSpec/InstanceSpy:
@@ -640,7 +643,11 @@ RSpec/NoExpectationExample:
640
643
  Enabled: pending
641
644
  Safe: false
642
645
  VersionAdded: '2.13'
646
+ VersionChanged: '2.14'
643
647
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NoExpectationExample
648
+ AllowedPatterns:
649
+ - "^expect_"
650
+ - "^assert_"
644
651
 
645
652
  RSpec/NotToNot:
646
653
  Description: Checks for consistent method usage for negating expectations.
@@ -763,6 +770,12 @@ RSpec/SingleArgumentMessageChain:
763
770
  VersionChanged: '1.10'
764
771
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SingleArgumentMessageChain
765
772
 
773
+ RSpec/SortMetadata:
774
+ Description: Sort RSpec metadata alphabetically.
775
+ Enabled: pending
776
+ VersionAdded: '2.14'
777
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SortMetadata
778
+
766
779
  RSpec/StubbedMock:
767
780
  Description: Checks that message expectations do not have a configured response.
768
781
  Enabled: true
@@ -807,7 +820,6 @@ RSpec/VariableName:
807
820
  - snake_case
808
821
  - camelCase
809
822
  AllowedPatterns: []
810
- IgnoredPatterns: []
811
823
  VersionAdded: '1.40'
812
824
  VersionChanged: '2.13'
813
825
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/VariableName
@@ -866,6 +878,22 @@ RSpec/Capybara/FeatureMethods:
866
878
  VersionChanged: '2.0'
867
879
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/FeatureMethods
868
880
 
881
+ RSpec/Capybara/NegationMatcher:
882
+ Description: Enforces use of `have_no_*` or `not_to` for negated expectations.
883
+ Enabled: pending
884
+ VersionAdded: '2.14'
885
+ EnforcedStyle: not_to
886
+ SupportedStyles:
887
+ - have_no
888
+ - not_to
889
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/NegationMatcher
890
+
891
+ RSpec/Capybara/SpecificActions:
892
+ Description: Checks for there is a more specific actions offered by Capybara.
893
+ Enabled: pending
894
+ VersionAdded: '2.14'
895
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/SpecificActions
896
+
869
897
  RSpec/Capybara/SpecificFinders:
870
898
  Description: Checks if there is a more specific finder offered by Capybara.
871
899
  Enabled: pending
@@ -901,6 +929,16 @@ RSpec/FactoryBot/AttributeDefinedStatically:
901
929
  VersionChanged: '2.0'
902
930
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/AttributeDefinedStatically
903
931
 
932
+ RSpec/FactoryBot/ConsistentParenthesesStyle:
933
+ Description: Use a consistent style for parentheses in factory bot calls.
934
+ Enabled: pending
935
+ EnforcedStyle: require_parentheses
936
+ SupportedStyles:
937
+ - require_parentheses
938
+ - omit_parentheses
939
+ VersionAdded: '2.14'
940
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/ConsistentParenthesesStyle
941
+
904
942
  RSpec/FactoryBot/CreateList:
905
943
  Description: Checks for create_list usage.
906
944
  Enabled: true
@@ -954,6 +992,29 @@ RSpec/Rails/HaveHttpStatus:
954
992
  VersionAdded: '2.12'
955
993
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/HaveHttpStatus
956
994
 
995
+ RSpec/Rails/InferredSpecType:
996
+ Description: Identifies redundant spec type.
997
+ Enabled: pending
998
+ Safe: false
999
+ VersionAdded: '2.14'
1000
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/InferredSpecType
1001
+ Inferences:
1002
+ channels: channel
1003
+ controllers: controller
1004
+ features: feature
1005
+ generator: generator
1006
+ helpers: helper
1007
+ jobs: job
1008
+ mailboxes: mailbox
1009
+ mailers: mailer
1010
+ models: model
1011
+ requests: request
1012
+ integration: request
1013
+ api: request
1014
+ routing: routing
1015
+ system: system
1016
+ views: view
1017
+
957
1018
  RSpec/Rails/HttpStatus:
958
1019
  Description: Enforces use of symbolic or numeric value to describe HTTP status.
959
1020
  Enabled: true
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ module Capybara
7
+ # Enforces use of `have_no_*` or `not_to` for negated expectations.
8
+ #
9
+ # @example EnforcedStyle: not_to (default)
10
+ # # bad
11
+ # expect(page).to have_no_selector
12
+ # expect(page).to have_no_css('a')
13
+ #
14
+ # # good
15
+ # expect(page).not_to have_selector
16
+ # expect(page).not_to have_css('a')
17
+ #
18
+ # @example EnforcedStyle: have_no
19
+ # # bad
20
+ # expect(page).not_to have_selector
21
+ # expect(page).not_to have_css('a')
22
+ #
23
+ # # good
24
+ # expect(page).to have_no_selector
25
+ # expect(page).to have_no_css('a')
26
+ #
27
+ class NegationMatcher < Base
28
+ extend AutoCorrector
29
+ include ConfigurableEnforcedStyle
30
+
31
+ MSG = 'Use `expect(...).%<runner>s %<matcher>s`.'
32
+ CAPYBARA_MATCHERS = %w[
33
+ selector css xpath text title current_path link button
34
+ field checked_field unchecked_field select table
35
+ sibling ancestor
36
+ ].freeze
37
+ POSITIVE_MATCHERS =
38
+ Set.new(CAPYBARA_MATCHERS) { |element| :"have_#{element}" }.freeze
39
+ NEGATIVE_MATCHERS =
40
+ Set.new(CAPYBARA_MATCHERS) { |element| :"have_no_#{element}" }
41
+ .freeze
42
+ RESTRICT_ON_SEND = (POSITIVE_MATCHERS + NEGATIVE_MATCHERS).freeze
43
+
44
+ # @!method not_to?(node)
45
+ def_node_matcher :not_to?, <<~PATTERN
46
+ (send ... :not_to
47
+ (send nil? %POSITIVE_MATCHERS ...))
48
+ PATTERN
49
+
50
+ # @!method have_no?(node)
51
+ def_node_matcher :have_no?, <<~PATTERN
52
+ (send ... :to
53
+ (send nil? %NEGATIVE_MATCHERS ...))
54
+ PATTERN
55
+
56
+ def on_send(node)
57
+ return unless offense?(node.parent)
58
+
59
+ matcher = node.method_name.to_s
60
+ add_offense(offense_range(node),
61
+ message: message(matcher)) do |corrector|
62
+ corrector.replace(node.parent.loc.selector, replaced_runner)
63
+ corrector.replace(node.loc.selector,
64
+ replaced_matcher(matcher))
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def offense?(node)
71
+ (style == :have_no && not_to?(node)) ||
72
+ (style == :not_to && have_no?(node))
73
+ end
74
+
75
+ def offense_range(node)
76
+ node.parent.loc.selector.with(end_pos: node.loc.selector.end_pos)
77
+ end
78
+
79
+ def message(matcher)
80
+ format(MSG,
81
+ runner: replaced_runner,
82
+ matcher: replaced_matcher(matcher))
83
+ end
84
+
85
+ def replaced_runner
86
+ case style
87
+ when :have_no
88
+ 'to'
89
+ when :not_to
90
+ 'not_to'
91
+ end
92
+ end
93
+
94
+ def replaced_matcher(matcher)
95
+ case style
96
+ when :have_no
97
+ matcher.sub('have_', 'have_no_')
98
+ when :not_to
99
+ matcher.sub('have_no_', 'have_')
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ module Capybara
7
+ # Checks for there is a more specific actions offered by Capybara.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # find('a').click
13
+ # find('button.cls').click
14
+ # find('a', exact_text: 'foo').click
15
+ # find('div button').click
16
+ #
17
+ # # good
18
+ # click_link
19
+ # click_button(class: 'cls')
20
+ # click_link(exact_text: 'foo')
21
+ # find('div').click_button
22
+ #
23
+ class SpecificActions < Base
24
+ MSG = "Prefer `%<good_action>s` over `find('%<selector>s').click`."
25
+ RESTRICT_ON_SEND = %i[click].freeze
26
+ SPECIFIC_ACTION = {
27
+ 'button' => 'button',
28
+ 'a' => 'link'
29
+ }.freeze
30
+
31
+ # @!method click_on_selector(node)
32
+ def_node_matcher :click_on_selector, <<-PATTERN
33
+ (send _ :find (str $_) ...)
34
+ PATTERN
35
+
36
+ def on_send(node)
37
+ click_on_selector(node.receiver) do |arg|
38
+ next unless supported_selector?(arg)
39
+ # Always check the last selector in the case of multiple selectors
40
+ # separated by whitespace.
41
+ # because the `.click` is executed on the element to
42
+ # which the last selector points.
43
+ next unless (selector = last_selector(arg))
44
+ next unless (action = specific_action(selector))
45
+ next unless CapybaraHelp.specific_option?(node.receiver, arg,
46
+ action)
47
+ next unless CapybaraHelp.specific_pseudo_classes?(arg)
48
+
49
+ range = offense_range(node, node.receiver)
50
+ add_offense(range, message: message(action, selector))
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def specific_action(selector)
57
+ SPECIFIC_ACTION[last_selector(selector)]
58
+ end
59
+
60
+ def supported_selector?(selector)
61
+ !selector.match?(/[>,+~]/)
62
+ end
63
+
64
+ def last_selector(arg)
65
+ arg.split.last[/^\w+/, 0]
66
+ end
67
+
68
+ def offense_range(node, receiver)
69
+ receiver.loc.selector.with(end_pos: node.loc.expression.end_pos)
70
+ end
71
+
72
+ def message(action, selector)
73
+ format(MSG,
74
+ good_action: good_action(action),
75
+ selector: selector)
76
+ end
77
+
78
+ def good_action(action)
79
+ "click_#{action}"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -26,7 +26,9 @@ module RuboCop
26
26
  # expect(page).to have_select
27
27
  # expect(page).to have_field('foo')
28
28
  #
29
- class SpecificMatcher < Base # rubocop:disable Metrics/ClassLength
29
+ class SpecificMatcher < Base
30
+ include CapybaraHelp
31
+
30
32
  MSG = 'Prefer `%<good_matcher>s` over `%<bad_matcher>s`.'
31
33
  RESTRICT_ON_SEND = %i[have_selector have_no_selector have_css
32
34
  have_no_css].freeze
@@ -37,51 +39,18 @@ module RuboCop
37
39
  'select' => 'select',
38
40
  'input' => 'field'
39
41
  }.freeze
40
- SPECIFIC_MATCHER_OPTIONS = {
41
- 'button' => (
42
- CssSelector::COMMON_OPTIONS + %w[disabled name value title type]
43
- ).freeze,
44
- 'link' => (
45
- CssSelector::COMMON_OPTIONS + %w[href alt title download]
46
- ).freeze,
47
- 'table' => (
48
- CssSelector::COMMON_OPTIONS + %w[
49
- caption with_cols cols with_rows rows
50
- ]
51
- ).freeze,
52
- 'select' => (
53
- CssSelector::COMMON_OPTIONS + %w[
54
- disabled name placeholder options enabled_options
55
- disabled_options selected with_selected multiple with_options
56
- ]
57
- ).freeze,
58
- 'field' => (
59
- CssSelector::COMMON_OPTIONS + %w[
60
- checked unchecked disabled valid name placeholder
61
- validation_message readonly with type multiple
62
- ]
63
- ).freeze
64
- }.freeze
65
- SPECIFIC_MATCHER_PSEUDO_CLASSES = %w[
66
- not() disabled enabled checked unchecked
67
- ].freeze
68
42
 
69
43
  # @!method first_argument(node)
70
44
  def_node_matcher :first_argument, <<-PATTERN
71
45
  (send nil? _ (str $_) ... )
72
46
  PATTERN
73
47
 
74
- # @!method option?(node)
75
- def_node_search :option?, <<-PATTERN
76
- (pair (sym %) _)
77
- PATTERN
78
-
79
48
  def on_send(node)
80
49
  first_argument(node) do |arg|
81
50
  next unless (matcher = specific_matcher(arg))
82
51
  next if CssSelector.multiple_selectors?(arg)
83
- next unless specific_matcher_option?(node, arg, matcher)
84
- next unless specific_matcher_pseudo_classes?(arg)
52
+ next unless specific_option?(node, arg, matcher)
53
+ next unless specific_pseudo_classes?(arg)
85
54
 
86
55
  add_offense(node, message: message(node, matcher))
87
56
  end
@@ -94,52 +63,6 @@ module RuboCop
94
63
  SPECIFIC_MATCHER[splitted_arg]
95
64
  end
96
65
 
97
- def specific_matcher_option?(node, arg, matcher)
98
- attrs = CssSelector.attributes(arg).keys
99
- return true if attrs.empty?
100
- return false unless replaceable_matcher?(node, matcher, attrs)
101
-
102
- attrs.all? do |attr|
103
- SPECIFIC_MATCHER_OPTIONS.fetch(matcher, []).include?(attr)
104
- end
105
- end
106
-
107
- def specific_matcher_pseudo_classes?(arg)
108
- CssSelector.pseudo_classes(arg).all? do |pseudo_class|
109
- replaceable_pseudo_class?(pseudo_class, arg)
110
- end
111
- end
112
-
113
- def replaceable_pseudo_class?(pseudo_class, arg)
114
- unless SPECIFIC_MATCHER_PSEUDO_CLASSES.include?(pseudo_class)
115
- return false
116
- end
117
-
118
- case pseudo_class
119
- when 'not()' then replaceable_pseudo_class_not?(arg)
120
- else true
121
- end
122
- end
123
-
124
- def replaceable_pseudo_class_not?(arg)
125
- arg.scan(/not\(.*?\)/).all? do |not_arg|
126
- CssSelector.attributes(not_arg).values.all? do |v|
127
- v.is_a?(TrueClass) || v.is_a?(FalseClass)
128
- end
129
- end
130
- end
131
-
132
- def replaceable_matcher?(node, matcher, attrs)
133
- case matcher
134
- when 'link' then replaceable_to_have_link?(node, attrs)
135
- else true
136
- end
137
- end
138
-
139
- def replaceable_to_have_link?(node, attrs)
140
- option?(node, :href) || attrs.include?('href')
141
- end
142
-
143
66
  def message(node, matcher)
144
67
  format(MSG,
145
68
  good_matcher: good_matcher(node, matcher),
@@ -61,7 +61,7 @@ module RuboCop
61
61
  extend AutoCorrector
62
62
  MSG = 'Prefer `not_to change` over `to change.by(0)`.'
63
63
  MSG_COMPOUND = 'Prefer %<preferred>s with compound expectations ' \
64
- 'over `change.by(0)`.'
64
+ 'over `change.by(0)`.'
65
65
  RESTRICT_ON_SEND = %i[change].freeze
66
66
 
67
67
  # @!method expect_change_with_arguments(node)
@@ -43,7 +43,7 @@ module RuboCop
43
43
  # # .rubocop.yml
44
44
  # # RSpec/ContextWording:
45
45
  # # AllowedPatterns:
46
- # # - /とき$/
46
+ # # - とき$
47
47
  #
48
48
  # @example
49
49
  # # bad
@@ -92,7 +92,9 @@ module RuboCop
92
92
  end
93
93
 
94
94
  def expect_patterns
95
- inspected = allowed_patterns.map(&:inspect)
95
+ inspected = allowed_patterns.map do |pattern|
96
+ pattern.inspect.gsub(/\A"|"\z/, '/')
97
+ end
96
98
  return inspected.first if inspected.size == 1
97
99
 
98
100
  inspected << "or #{inspected.pop}"
@@ -6,12 +6,17 @@ module RuboCop
6
6
  # Checks for common mistakes in example descriptions.
7
7
  #
8
8
  # This cop will correct docstrings that begin with 'should' and 'it'.
9
+ # This cop will also look for insufficient examples and call them out.
9
10
  #
10
11
  # @see http://betterspecs.org/#should
11
12
  #
12
13
  # The autocorrect is experimental - use with care! It can be configured
13
14
  # with CustomTransform (e.g. have => has) and IgnoredWords (e.g. only).
14
15
  #
16
+ # Use the DisallowedExamples setting to prevent unclear or insufficient
17
+ # descriptions. Please note that this config will not be treated as
18
+ # case sensitive.
19
+ #
15
20
  # @example
16
21
  # # bad
17
22
  # it 'should find nothing' do
@@ -30,11 +35,21 @@ module RuboCop
30
35
  # it 'does things' do
31
36
  # end
32
37
  #
38
+ # @example `DisallowedExamples: ['works']` (default)
39
+ # # bad
40
+ # it 'works' do
41
+ # end
42
+ #
43
+ # # good
44
+ # it 'marks the task as done' do
45
+ # end
33
46
  class ExampleWording < Base
34
47
  extend AutoCorrector
35
48
 
36
49
  MSG_SHOULD = 'Do not use should when describing your tests.'
37
50
  MSG_IT = "Do not repeat 'it' when describing your tests."
51
+ MSG_INSUFFICIENT_DESCRIPTION = 'Your example description is ' \
52
+ 'insufficient.'
38
53
 
39
54
  SHOULD_PREFIX = /\Ashould(?:n't)?\b/i.freeze
40
55
  IT_PREFIX = /\Ait /i.freeze
@@ -53,12 +68,20 @@ module RuboCop
53
68
  add_wording_offense(description_node, MSG_SHOULD)
54
69
  elsif message.match?(IT_PREFIX)
55
70
  add_wording_offense(description_node, MSG_IT)
71
+ else
72
+ check_and_handle_insufficient_examples(description_node)
56
73
  end
57
74
  end
58
75
  end
59
76
 
60
77
  private
61
78
 
79
+ def check_and_handle_insufficient_examples(description)
80
+ if insufficient_examples.include?(preprocess(text(description)))
81
+ add_wording_offense(description, MSG_INSUFFICIENT_DESCRIPTION)
82
+ end
83
+ end
84
+
62
85
  def add_wording_offense(node, message)
63
86
  docstring = docstring(node)
64
87
 
@@ -113,6 +136,15 @@ module RuboCop
113
136
  def ignored_words
114
137
  cop_config.fetch('IgnoredWords', [])
115
138
  end
139
+
140
+ def insufficient_examples
141
+ examples = cop_config.fetch('DisallowedExamples', [])
142
+ examples.map! { |example| preprocess(example) }
143
+ end
144
+
145
+ def preprocess(message)
146
+ message.strip.squeeze(' ').downcase
147
+ end
116
148
  end
117
149
  end
118
150
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ module FactoryBot
7
+ # Use a consistent style for parentheses in factory bot calls.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # create :user
13
+ # build(:user)
14
+ # create(:login)
15
+ # create :login
16
+ #
17
+ # @example `EnforcedStyle: require_parentheses` (default)
18
+ #
19
+ # # good
20
+ # create(:user)
21
+ # create(:user)
22
+ # create(:login)
23
+ # build(:login)
24
+ #
25
+ # @example `EnforcedStyle: omit_parentheses`
26
+ #
27
+ # # good
28
+ # create :user
29
+ # build :user
30
+ # create :login
31
+ # create :login
32
+ #
33
+ class ConsistentParenthesesStyle < Base
34
+ extend AutoCorrector
35
+ include ConfigurableEnforcedStyle
36
+ include RuboCop::RSpec::FactoryBot::Language
37
+ include RuboCop::Cop::Util
38
+
39
+ def self.autocorrect_incompatible_with
40
+ [Style::MethodCallWithArgsParentheses]
41
+ end
42
+
43
+ MSG_REQUIRE_PARENS = 'Prefer method call with parentheses'
44
+ MSG_OMIT_PARENS = 'Prefer method call without parentheses'
45
+
46
+ FACTORY_CALLS = RuboCop::RSpec::FactoryBot::Language::METHODS
47
+
48
+ # @!method factory_call(node)
49
+ def_node_matcher :factory_call, <<-PATTERN
50
+ (send
51
+ ${#factory_bot? nil?} %FACTORY_CALLS
52
+ $...)
53
+ PATTERN
54
+
55
+ def on_send(node)
56
+ return if nested_call?(node) # prevent from nested matching
57
+
58
+ factory_call(node) do
59
+ if node.parenthesized?
60
+ process_with_parentheses(node)
61
+ else
62
+ process_without_parentheses(node)
63
+ end
64
+ end
65
+ end
66
+
67
+ def process_with_parentheses(node)
68
+ return unless style == :omit_parentheses
69
+
70
+ add_offense(node.loc.selector,
71
+ message: MSG_OMIT_PARENS) do |corrector|
72
+ remove_parentheses(corrector, node)
73
+ end
74
+ end
75
+
76
+ def process_without_parentheses(node)
77
+ return unless style == :require_parentheses
78
+
79
+ add_offense(node.loc.selector,
80
+ message: MSG_REQUIRE_PARENS) do |corrector|
81
+ add_parentheses(node, corrector)
82
+ end
83
+ end
84
+
85
+ def nested_call?(node)
86
+ node.parent&.send_type?
87
+ end
88
+
89
+ private
90
+
91
+ def remove_parentheses(corrector, node)
92
+ corrector.replace(node.location.begin, ' ')
93
+ corrector.remove(node.location.end)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -238,8 +238,8 @@ module RuboCop
238
238
  indent = ' ' * node.body.loc.column
239
239
  indent_end = ' ' * node.parent.loc.column
240
240
  " do #{node.arguments.source}\n" \
241
- "#{indent}#{node.body.source}\n" \
242
- "#{indent_end}end"
241
+ "#{indent}#{node.body.source}\n" \
242
+ "#{indent_end}end"
243
243
  end
244
244
 
245
245
  def format_singleline_block(node)