rubocop-rspec 2.13.2 → 2.14.0

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