rubocop-petal 1.1.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50feab9f7db5e7eb2bc4e1cf84a7f2273bd954f116be6901582e07d1e2c1150c
4
- data.tar.gz: c4ce73b925d3811bf92a1779314a592701157b22daa4f0f7885e7d074dd1f1d5
3
+ metadata.gz: 15da074670760958ac74e715caa688618632bcbb5379e5fc47dd1a1055c8e956
4
+ data.tar.gz: 0d70524ec6b85733e931c09c864151454cde3fc4287c87f6a1722d1d3c6ea81d
5
5
  SHA512:
6
- metadata.gz: 5385ce76c50bcd195adcd658300e0a10ccb7e5c01e518ea15842530de709066e9016b411bf1c605e30760830c6b1ad210583834efafb1fea99b46bb72eef1876
7
- data.tar.gz: 42b2ed6467edce4b0cdccf153d7258b2e69bf52d2cfcc0809f720d01d0865016cd5c0d0dfabff53af45d30d2c33bcd85aad6f7867418c10883cfff28474f94ea
6
+ metadata.gz: 2271b3c9bdc14b033c248ec2b7492a2248e017c98a48daded7cee9621e4e5a42c5c30f8036b5c16fcd182408af56d6b213ee9c6a8f719c65506f913aac741b1e
7
+ data.tar.gz: 8780348f8074c78603b2fc8988297c12f4531080bd07f54af0cdaa4d9b63d021f5328b8e192b0c8cb34ee53d4933e3d04769240f6907e550a8e7c4e6e042f965
data/config/base.yml CHANGED
@@ -65,8 +65,6 @@ RSpec/LetSetup:
65
65
  Enabled: false
66
66
  RSpec/MessageSpies:
67
67
  EnforcedStyle: receive
68
- RSpec/MultipleExpectations:
69
- Enabled: false
70
68
  RSpec/MultipleMemoizedHelpers:
71
69
  Enabled: false
72
70
  RSpec/NamedSubject:
data/config/default.yml CHANGED
@@ -1,3 +1,9 @@
1
+ Chewy/FieldType:
2
+ Description: 'Assure that a type is defined for a Chewy field.'
3
+ Enabled: true
4
+ Include:
5
+ - app/chewy/**/*
6
+
1
7
  Grape/PreferNamespace:
2
8
  Description: 'Prevent usage of namespace aliases.'
3
9
  Enabled: true
@@ -29,6 +35,26 @@ Migration/SchemaStatementsMethods:
29
35
  Include:
30
36
  - db/migrate/**
31
37
 
38
+ Migration/StandaloneAddReference:
39
+ Description: 'Prevent using `add_reference/belongs_to` outside of a change_table.'
40
+ Enabled: true
41
+ Include:
42
+ - db/migrate/**
43
+
44
+ RSpec/AggregateExamples:
45
+ Description: Checks if example group contains two or more aggregatable examples.
46
+ Enabled: true
47
+ StyleGuide: https://rspec.rubystyle.guide/#expectation-per-example
48
+ AddAggregateFailuresMetadata: true
49
+ MatchersWithSideEffects:
50
+ - allow_value
51
+ - allow_values
52
+ - validate_presence_of
53
+ - validate_absence_of
54
+ - validate_length_of
55
+ - validate_inclusion_of
56
+ - validates_exclusion_of
57
+
32
58
  RSpec/AuthenticatedAs:
33
59
  Description: 'Suggest to use authenticated_as instead of legacy api_key.'
34
60
  Enabled: true
@@ -68,6 +94,11 @@ Grape/UnnecessaryNamespace:
68
94
  Include:
69
95
  - app/api/**/*
70
96
 
97
+ Rails/DestroyAllBang:
98
+ Description: 'Prevent using `destroy_all` in favor of `each(&:destroy!)` to go along Rails/SaveBang cop.'
99
+ StyleGuide: https://rails.rubystyle.guide#save-bang
100
+ Enabled: true
101
+
71
102
  Rails/EnumPrefix:
72
103
  Description: 'Set prefix options when using enums.'
73
104
  Enabled: true
@@ -75,6 +106,13 @@ Rails/EnumPrefix:
75
106
  Include:
76
107
  - app/models/**/*
77
108
 
109
+ Rails/EnumStartingValue:
110
+ Description: 'Prevent starting from zero with an enum.'
111
+ Enabled: true
112
+ StyleGuide: https://github.com/petalmd/rubocop-petal/issues/56
113
+ Include:
114
+ - app/models/**/*
115
+
78
116
  Rails/RiskyActiverecordInvocation:
79
117
  Description: 'Interpolation, use hash or parameterized syntax.'
80
118
  Enabled: true
@@ -93,3 +131,7 @@ RSpec/JsonResponse:
93
131
  Performance/Snif:
94
132
  Description: 'Prevent snif in favor of detect'
95
133
  Enabled: true
134
+
135
+ Sidekiq/NoNilReturn:
136
+ Description: 'Prevent early nil return in workers'
137
+ Enabled: false
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Chewy
6
+ # This cop force to specify a type for Chewy field
7
+ #
8
+ # # bad
9
+ # field :name
10
+ #
11
+ # # good
12
+ # field :name, type: 'text'
13
+ class FieldType < Base
14
+ MSG = 'Specify a `type` for Chewy field.'
15
+
16
+ RESTRICT_ON_SEND = %i[field].freeze
17
+
18
+ # @!method options_has_field?(node)
19
+ def_node_matcher :options_has_field?, <<~PATTERN
20
+ (send nil? :field (sym _)+ (hash <(pair (sym :type) {str sym}) ...>))
21
+ PATTERN
22
+
23
+ def on_send(node)
24
+ return if options_has_field?(node)
25
+
26
+ add_offense(node)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -21,11 +21,12 @@ module RuboCop
21
21
  # t.foreign_key :users, column: :user_id
22
22
  # end
23
23
  class ChangeTableReferences < Base
24
- MSG = 'Use a combination of `t.bigint`, `t.index` and `t.foreign_key` in a change_table.'
24
+ MSG = 'Use a combination of `t.bigint`, `t.index` and `t.foreign_key` in a change_table to add a reference.' \
25
+ 'Or `t.remove_foreign_key`, `t.remove` to remove a reference.'
25
26
 
26
27
  # @!method add_references_in_block?(node)
27
- def_node_search :add_references_in_block?, <<~PATTERN
28
- (send lvar /references|belongs_to/ ...)
28
+ def_node_matcher :add_references_in_block?, <<~PATTERN
29
+ (send lvar /references|belongs_to|remove_references|remove_belongs_to/ ...)
29
30
  PATTERN
30
31
 
31
32
  # @!method change_table?(node)
@@ -36,7 +37,7 @@ module RuboCop
36
37
  def on_block(node)
37
38
  return unless change_table?(node)
38
39
 
39
- references_node = node.children.detect { |n| add_references_in_block?(n) }
40
+ references_node = node.child_nodes[2].each_node.detect { |n| add_references_in_block?(n) }
40
41
  return unless references_node
41
42
 
42
43
  arguments = references_node.child_nodes[1]
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Prevent using `add_reference` and `remove_reference` outside of
7
+ # a `change_table` block. `add_reference` create multiples `ALTER TABLE`
8
+ # statements. Using `change_table` with `bulk: true` is more efficient.
9
+ #
10
+ # # bad
11
+ # add_reference :products, :user, foreign_key: true
12
+ #
13
+ # # good
14
+ # change_table :products, bulk: true do |t|
15
+ # t.bigint :user_id, null: false
16
+ # t.index :user_id
17
+ # t.foreign_key :users, column: :user_id
18
+ # end
19
+ class StandaloneAddReference < Base
20
+ MSG = 'Modifying references must be done in a change_table block.'
21
+
22
+ RESTRICT_ON_SEND = %i[add_reference add_belongs_to remove_reference remove_belongs_to].freeze
23
+
24
+ def on_send(node)
25
+ reference_method = node.source_range.with(end_pos: node.child_nodes.first.source_range.begin_pos - 1)
26
+
27
+ add_offense(reference_method)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Cop to enforce the use of `each(&:destroy!)` over `destroy_all`.
7
+ # https://docs.rubocop.org/rubocop-rails/cops_rails.html#railssavebang
8
+ # https://github.com/rails/rails/pull/37782
9
+ # https://discuss.rubyonrails.org/t/proposal-add-destroy-all-method-to-activerecord-relation/80959
10
+ #
11
+ # # bad
12
+ # User.destroy_all
13
+ #
14
+ # # good
15
+ # User.each(&:destroy!)
16
+ class DestroyAllBang < Base
17
+ extend AutoCorrector
18
+ MSG = 'Use `each(&:destroy!)` instead of `destroy_all`.'
19
+ RESTRICT_ON_SEND = %i[destroy_all].freeze
20
+
21
+ def on_send(node)
22
+ destroy_all_range = node.loc.selector
23
+ add_offense(destroy_all_range) do |corrector|
24
+ corrector.replace(node.loc.selector, 'each(&:destroy!)')
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Prevent the user to start from Zero with an enum.
7
+ # When using a wrong value like .where(state: :patato) let Rails + MySQL do a WHERE state = 0.
8
+ # It will match nothing since no record will have a 0 value.
9
+ #
10
+ # # bad
11
+ # enum my_enum: {apple: 0, bannana: 1}
12
+ #
13
+ # # good
14
+ # enum my_enum: {apple: 1, banana: 2}
15
+
16
+ class EnumStartingValue < Base
17
+ MSG = 'Prefer starting from `1` instead of `0` with `enum`.'
18
+
19
+ def_node_matcher :enum?, <<~PATTERN
20
+ (send nil? :enum (hash ...))
21
+ PATTERN
22
+
23
+ def_node_matcher :enum_attributes, <<~PATTERN
24
+ (send nil? :enum (:hash (:pair (...)$(...) )...))
25
+ PATTERN
26
+
27
+ def on_send(node)
28
+ return unless enum? node
29
+
30
+ add_offense(node) if start_with_zero?(enum_attributes(node))
31
+ end
32
+
33
+ def start_with_zero?(node)
34
+ return false unless node.type == :hash
35
+
36
+ node.children.any? do |child|
37
+ value = child.value
38
+ value.type == :int && value.value.zero?
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is shamelessly borrowed from RuboCop RSpec
4
+ # https://github.com/rubocop-hq/rubocop-rspec/blob/master/lib/rubocop/rspec/language.rb
5
+ module RuboCop
6
+ module Cop
7
+ module RSpec
8
+ # RSpec public API methods that are commonly used in cops
9
+ class AggregateExamples < ::RuboCop::Cop::Cop
10
+ module Language
11
+ RSPEC = '{(const {nil? cbase} :RSpec) nil?}'
12
+
13
+ # Set of method selectors
14
+ class SelectorSet
15
+ def initialize(selectors)
16
+ @selectors = selectors
17
+ end
18
+
19
+ def ==(other)
20
+ selectors.eql?(other.selectors)
21
+ end
22
+
23
+ def +(other)
24
+ self.class.new(selectors + other.selectors)
25
+ end
26
+
27
+ delegate :include?, to: :selectors
28
+
29
+ def block_pattern
30
+ "(block #{send_pattern} ...)"
31
+ end
32
+
33
+ def send_pattern
34
+ "(send #{RSPEC} #{node_pattern_union} ...)"
35
+ end
36
+
37
+ def node_pattern_union
38
+ "{#{node_pattern}}"
39
+ end
40
+
41
+ def node_pattern
42
+ selectors.map(&:inspect).join(' ')
43
+ end
44
+
45
+ protected
46
+
47
+ attr_reader :selectors
48
+ end
49
+
50
+ module ExampleGroups
51
+ GROUPS = SelectorSet.new(%i[describe context feature example_group])
52
+ SKIPPED = SelectorSet.new(%i[xdescribe xcontext xfeature])
53
+ FOCUSED = SelectorSet.new(%i[fdescribe fcontext ffeature])
54
+
55
+ ALL = GROUPS + SKIPPED + FOCUSED
56
+ end
57
+
58
+ module Examples
59
+ EXAMPLES = SelectorSet.new(%i[it specify example scenario its])
60
+ FOCUSED = SelectorSet.new(%i[fit fspecify fexample fscenario focus])
61
+ SKIPPED = SelectorSet.new(%i[xit xspecify xexample xscenario skip])
62
+ PENDING = SelectorSet.new(%i[pending])
63
+
64
+ ALL = EXAMPLES + FOCUSED + SKIPPED + PENDING
65
+ end
66
+
67
+ module Runners
68
+ ALL = SelectorSet.new(%i[to to_not not_to])
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ class AggregateExamples < ::RuboCop::Cop::Cop
7
+ # @internal Support methods for keeping newlines around examples.
8
+ module LineRangeHelpers
9
+ include RangeHelp
10
+
11
+ private
12
+
13
+ def range_for_replace(examples)
14
+ range = range_by_whole_lines(examples.first.source_range,
15
+ include_final_newline: true)
16
+ next_range = range_by_whole_lines(examples[1].source_range)
17
+ if adjacent?(range, next_range)
18
+ range.resize(range.length + 1)
19
+ else
20
+ range
21
+ end
22
+ end
23
+
24
+ def adjacent?(range, another_range)
25
+ range.end_pos + 1 == another_range.begin_pos
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'language'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module RSpec
8
+ class AggregateExamples < ::RuboCop::Cop::Cop
9
+ # When aggregated, the expectations will fail when not supposed to or
10
+ # have a risk of not failing when expected to. One example is
11
+ # `validate_presence_of :comment` as it leaves an empty comment after
12
+ # itself on the subject making it invalid and the subsequent expectation
13
+ # to fail.
14
+ # Examples with those matchers are not supposed to be aggregated.
15
+ #
16
+ # @example MatchersWithSideEffects
17
+ #
18
+ # # .rubocop.yml
19
+ # # RSpec/AggregateExamples:
20
+ # # MatchersWithSideEffects:
21
+ # # - allow_value
22
+ # # - allow_values
23
+ # # - validate_presence_of
24
+ #
25
+ # # bad, but isn't automatically correctable
26
+ # describe do
27
+ # it { is_expected.to validate_presence_of(:comment) }
28
+ # it { is_expected.to be_valid }
29
+ # end
30
+ #
31
+ # @internal
32
+ # Support for taking special care of the matchers that have side
33
+ # effects, i.e. leave the subject in a modified state.
34
+ module MatchersWithSideEffects
35
+ extend RuboCop::NodePattern::Macros
36
+ include Language
37
+
38
+ MSG_FOR_EXPECTATIONS_WITH_SIDE_EFFECTS =
39
+ 'Aggregate with the example at line %d. IMPORTANT! Pay attention ' \
40
+ 'to the expectation order, some of the matchers have side effects.'
41
+
42
+ private
43
+
44
+ def message_for(example, first_example)
45
+ return super unless example_with_side_effects?(example)
46
+
47
+ format(MSG_FOR_EXPECTATIONS_WITH_SIDE_EFFECTS, first_example.loc.line)
48
+ end
49
+
50
+ def matcher_with_side_effects_names
51
+ cop_config.fetch('MatchersWithSideEffects', [])
52
+ .map(&:to_sym)
53
+ end
54
+
55
+ def matcher_with_side_effects_name?(matcher_name)
56
+ matcher_with_side_effects_names.include?(matcher_name)
57
+ end
58
+
59
+ # In addition to base definition, matches examples with:
60
+ # - no matchers known to have side-effects
61
+ def_node_matcher :example_for_autocorrect?, <<-PATTERN
62
+ [ #super !#example_with_side_effects? ]
63
+ PATTERN
64
+
65
+ # Matches the example with matcher with side effects
66
+ def_node_matcher :example_with_side_effects?, <<-PATTERN
67
+ (block #{Examples::EXAMPLES.send_pattern} _ #expectation_with_side_effects?)
68
+ PATTERN
69
+
70
+ # Matches the expectation with matcher with side effects
71
+ def_node_matcher :expectation_with_side_effects?, <<-PATTERN
72
+ (send #expectation? #{Runners::ALL.node_pattern_union} #matcher_with_side_effects?)
73
+ PATTERN
74
+
75
+ # Matches the matcher with side effects
76
+ def_node_search :matcher_with_side_effects?, <<-PATTERN
77
+ (send nil? #matcher_with_side_effects_name? ...)
78
+ PATTERN
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ class AggregateExamples < ::RuboCop::Cop::Cop
7
+ # @internal
8
+ # Support methods for example metadata.
9
+ # Examples with similar metadata are grouped.
10
+ #
11
+ # Depending on the configuration, `aggregate_failures` metadata
12
+ # is added to aggregated examples.
13
+ module MetadataHelpers
14
+ private
15
+
16
+ def metadata_for_aggregated_example(metadata)
17
+ metadata_to_add = metadata.compact.map(&:source)
18
+ metadata_to_add.unshift(':aggregate_failures') if add_aggregate_failures_metadata?
19
+ if metadata_to_add.any?
20
+ "(#{metadata_to_add.join(', ')})"
21
+ else
22
+ ''
23
+ end
24
+ end
25
+
26
+ # Used to group examples for aggregation. `aggregate_failures`
27
+ # and `aggregate_failures: true` metadata are not taken in
28
+ # consideration, as it is dynamically set basing on cofiguration.
29
+ # If `aggregate_failures: false` is set on the example, it's
30
+ # preserved and is treated as regular metadata.
31
+ def metadata_without_aggregate_failures(example)
32
+ metadata = example_metadata(example) || []
33
+
34
+ symbols = metadata_symbols_without_aggregate_failures(metadata)
35
+ pairs = metadata_pairs_without_aggegate_failures(metadata)
36
+
37
+ [*symbols, pairs].flatten.compact
38
+ end
39
+
40
+ def example_metadata(example)
41
+ example.send_node.arguments
42
+ end
43
+
44
+ def metadata_symbols_without_aggregate_failures(metadata)
45
+ metadata
46
+ .select(&:sym_type?)
47
+ .reject { |item| item.value == :aggregate_failures }
48
+ end
49
+
50
+ def metadata_pairs_without_aggegate_failures(metadata)
51
+ map = metadata.find(&:hash_type?)
52
+ pairs = map&.pairs || []
53
+ pairs.reject do |pair|
54
+ pair.key.value == :aggregate_failures && pair.value.true_type?
55
+ end
56
+ end
57
+
58
+ def add_aggregate_failures_metadata?
59
+ cop_config.fetch('AddAggregateFailuresMetadata', false)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'language'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module RSpec
8
+ class AggregateExamples < ::RuboCop::Cop::Cop
9
+ # @internal
10
+ # Node matchers and searchers.
11
+ module NodeMatchers
12
+ extend RuboCop::NodePattern::Macros
13
+ include Language
14
+
15
+ private
16
+
17
+ def_node_matcher :example_group_with_several_examples, <<-PATTERN
18
+ (block
19
+ #{ExampleGroups::ALL.send_pattern}
20
+ _
21
+ (begin $...)
22
+ )
23
+ PATTERN
24
+
25
+ def example_method?(method_name)
26
+ %i[it specify example scenario].include?(method_name)
27
+ end
28
+
29
+ # Matches examples with:
30
+ # - expectation statements exclusively
31
+ # - no title (e.g. `it('jumps over the lazy dog')`)
32
+ # - no HEREDOC
33
+ def_node_matcher :example_for_autocorrect?, <<-PATTERN
34
+ [
35
+ #example_with_expectations_only?
36
+ !#example_has_title?
37
+ !#contains_heredoc?
38
+ ]
39
+ PATTERN
40
+
41
+ def_node_matcher :example_with_expectations_only?, <<-PATTERN
42
+ (block #{Examples::EXAMPLES.send_pattern} _
43
+ { #single_expectation? (begin #single_expectation?+) }
44
+ )
45
+ PATTERN
46
+
47
+ # Matches the example with a title (e.g. `it('is valid')`)
48
+ def_node_matcher :example_has_title?, <<-PATTERN
49
+ (block
50
+ (send nil? #example_method? str ...)
51
+ ...
52
+ )
53
+ PATTERN
54
+
55
+ # Searches for HEREDOC in examples. It can be tricky to aggregate,
56
+ # especially when interleaved with parenthesis or curly braces.
57
+ def contains_heredoc?(node)
58
+ node.each_descendant(:str, :xstr, :dstr).any?(&:heredoc?)
59
+ end
60
+
61
+ def_node_matcher :subject_with_no_args?, <<-PATTERN
62
+ (send _ _)
63
+ PATTERN
64
+
65
+ def_node_matcher :expectation?, <<-PATTERN
66
+ {
67
+ (send nil? {:is_expected :are_expected})
68
+ (send nil? :expect #subject_with_no_args?)
69
+ }
70
+ PATTERN
71
+
72
+ def_node_matcher :single_expectation?, <<-PATTERN
73
+ (send #expectation? #{Runners::ALL.node_pattern_union} _)
74
+ PATTERN
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'aggregate_examples/matchers_with_side_effects'
4
+ require_relative 'aggregate_examples/metadata_helpers'
5
+ require_relative 'aggregate_examples/line_range_helpers'
6
+ require_relative 'aggregate_examples/node_matchers'
7
+
8
+ # This is shamelessly borrowed from test-prof
9
+ # https://github.com/test-prof/test-prof/blob/02d8f355c158fb021e58ff1327d624a8299762b6/lib/test_prof/cops/rspec/aggregate_examples.rb
10
+
11
+ module RuboCop
12
+ module Cop
13
+ module RSpec
14
+ # Checks if example groups contain two or more aggregatable examples.
15
+ #
16
+ # @see https://github.com/rubocop-hq/rspec-style-guide#expectation-per-example
17
+ #
18
+ # This cop is primarily for reducing the cost of repeated expensive
19
+ # context initialization.
20
+ #
21
+ # @example
22
+ #
23
+ # # bad
24
+ # describe do
25
+ # it do
26
+ # expect(number).to be_positive
27
+ # expect(number).to be_odd
28
+ # end
29
+ #
30
+ # it { is_expected.to be_prime }
31
+ # end
32
+ #
33
+ # # good
34
+ # describe do
35
+ # it do
36
+ # expect(number).to be_positive
37
+ # expect(number).to be_odd
38
+ # is_expected.to be_prime
39
+ # end
40
+ # end
41
+ #
42
+ # # fair - subject has side effects
43
+ # describe do
44
+ # it do
45
+ # expect(multiply_by(2)).to be_multiple_of(2)
46
+ # end
47
+ #
48
+ # it do
49
+ # expect(multiply_by(3)).to be_multiple_of(3)
50
+ # end
51
+ # end
52
+ #
53
+ # Block expectation syntax is deliberately not supported due to:
54
+ #
55
+ # 1. `subject { -> { ... } }` syntax being hard to detect, e.g. the
56
+ # following looks like an example with non-block syntax, but it might
57
+ # be, depending on how the subject is defined:
58
+ #
59
+ # it { is_expected.to do_something }
60
+ #
61
+ # If the subject is defined in a `shared_context`, it's impossible to
62
+ # detect that at all.
63
+ #
64
+ # 2. Aggregation should use composition with an `.and`. Also, aggregation
65
+ # of the `not_to` expectations is barely possible when a matcher
66
+ # doesn't provide a negated variant.
67
+ #
68
+ # 3. Aggregation of block syntax with non-block syntax should be in a
69
+ # specific order.
70
+ #
71
+ # RSpec [comes with an `aggregate_failures` helper](https://relishapp.com/rspec/rspec-expectations/docs/aggregating-failures)
72
+ # not to fail the example on first unmet expectation that might come
73
+ # handy with aggregated examples.
74
+ # It can be [used in metadata form](https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures#use-%60:aggregate-failures%60-metadata),
75
+ # or [enabled globally](https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures#enable-failure-aggregation-globally-using-%60define-derived-metadata%60).
76
+ #
77
+ # @example Globally enable `aggregate_failures`
78
+ #
79
+ # # spec/spec_helper.rb
80
+ # config.define_derived_metadata do |metadata|
81
+ # unless metadata.key?(:aggregate_failures)
82
+ # metadata[:aggregate_failures] = true
83
+ # end
84
+ # end
85
+ #
86
+ # To match the style being used in the spec suite, AggregateExamples
87
+ # can be configured to add `:aggregate_failures` metadata to the
88
+ # example or not. The option not to add metadata can be also used
89
+ # when it's not desired to make expectations after previously failed
90
+ # ones, commonly known as fail-fast.
91
+ #
92
+ # The terms "aggregate examples" and "aggregate failures" not to be
93
+ # confused. The former stands for putting several expectations to
94
+ # a single example. The latter means to run all the expectations in
95
+ # the example instead of aborting on the first one.
96
+ #
97
+ # @example AddAggregateFailuresMetadata: true (default)
98
+ #
99
+ # # Metadata set using a symbol
100
+ # it(:aggregate_failures) do
101
+ # expect(number).to be_positive
102
+ # expect(number).to be_odd
103
+ # end
104
+ #
105
+ # @example AddAggregateFailuresMetadata: false
106
+ #
107
+ # it do
108
+ # expect(number).to be_positive
109
+ # expect(number).to be_odd
110
+ # end
111
+ #
112
+ class AggregateExamples < ::RuboCop::Cop::Cop
113
+ include LineRangeHelpers
114
+ include MetadataHelpers
115
+ include NodeMatchers
116
+
117
+ # Methods from the following modules override and extend methods of this
118
+ # class, extracting specific behavior.
119
+ prepend MatchersWithSideEffects
120
+
121
+ MSG = 'Aggregate with the example at line %d.'
122
+
123
+ def on_block(node)
124
+ example_group_with_several_examples(node) do |all_examples|
125
+ example_clusters(all_examples).each_value do |examples|
126
+ examples[1..].each do |example|
127
+ add_offense(example,
128
+ location: :expression,
129
+ message: message_for(example, examples[0]))
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def autocorrect(example_node)
136
+ clusters = example_clusters_for_autocorrect(example_node)
137
+ return if clusters.empty?
138
+
139
+ lambda do |corrector|
140
+ clusters.each do |metadata, examples|
141
+ range = range_for_replace(examples)
142
+ replacement = aggregated_example(examples, metadata)
143
+ corrector.replace(range, replacement)
144
+ examples[1..].map { |example| drop_example(corrector, example) }
145
+ end
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ # Clusters of examples in the same example group, on the same nesting
152
+ # level that can be aggregated.
153
+ def example_clusters(all_examples)
154
+ all_examples
155
+ .select { |example| example_with_expectations_only?(example) }
156
+ .group_by { |example| metadata_without_aggregate_failures(example) }
157
+ .select { |_, examples| examples.count > 1 }
158
+ end
159
+
160
+ # Clusters of examples that can be aggregated without losing any
161
+ # information (e.g. metadata or docstrings)
162
+ def example_clusters_for_autocorrect(example_node)
163
+ examples_in_group = example_node.parent.each_child_node(:block)
164
+ .select { |example| example_for_autocorrect?(example) }
165
+ example_clusters(examples_in_group)
166
+ end
167
+
168
+ def message_for(_example, first_example)
169
+ format(MSG, first_example.loc.line)
170
+ end
171
+
172
+ def drop_example(corrector, example)
173
+ aggregated_range = range_by_whole_lines(example.source_range,
174
+ include_final_newline: true)
175
+ corrector.remove(aggregated_range)
176
+ end
177
+
178
+ def aggregated_example(examples, metadata)
179
+ base_indent = ' ' * examples.first.source_range.column
180
+ metadata = metadata_for_aggregated_example(metadata)
181
+ [
182
+ "#{base_indent}it#{metadata} do",
183
+ *examples.map { |example| transform_body(example, base_indent) },
184
+ "#{base_indent}end\n"
185
+ ].join("\n")
186
+ end
187
+
188
+ # Extracts and transforms the body, keeping proper indentation.
189
+ def transform_body(node, base_indent)
190
+ "#{base_indent} #{new_body(node)}"
191
+ end
192
+
193
+ def new_body(node)
194
+ node.body.source
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop-rspec'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module RSpec
8
+ module MultipleExpectationsAutoCorrect
9
+ def self.prepended(base)
10
+ base.extend(AutoCorrector)
11
+ end
12
+
13
+ private
14
+
15
+ def flag_example(node, expectation_count:)
16
+ add_offense(
17
+ node.send_node,
18
+ message: format(
19
+ MultipleExpectations::MSG,
20
+ total: expectation_count,
21
+ max: max_expectations
22
+ )
23
+ ) do |corrector|
24
+ autocorrect(node, corrector)
25
+ end
26
+ end
27
+
28
+ def autocorrect(node, corrector)
29
+ if (args = node.children.first.arguments.first)
30
+ corrector.insert_after(args, ', :aggregate_failures')
31
+ else
32
+ corrector.insert_after(node.children.first, ' :aggregate_failures')
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ RuboCop::Cop::RSpec::MultipleExpectations
41
+ .prepend(RuboCop::Cop::RSpec::MultipleExpectationsAutoCorrect)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sidekiq
6
+ # Ensure workers avoid returning early
7
+ #
8
+ # # bad
9
+ # def perform
10
+ # return if condition
11
+ # ...
12
+ # end
13
+ #
14
+ # # good
15
+ # def perform
16
+ # # TDB, idea would be `raise SilentError if condition`
17
+ # end
18
+
19
+ class NoNilReturn < Base
20
+ MSG = 'Avoid using early nil return in workers.'
21
+
22
+ def on_return(node)
23
+ add_offense(node) if node.arguments.first&.nil_type? || node.arguments.empty?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Petal
5
- VERSION = '1.1.2'
5
+ VERSION = '1.3.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-petal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean-Francis Bastien
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-30 00:00:00.000000000 Z
11
+ date: 2024-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -16,57 +16,71 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.50'
19
+ version: '1.59'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.50'
26
+ version: '1.59'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop-factory_bot
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.24'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.24'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rubocop-performance
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - "~>"
32
46
  - !ruby/object:Gem::Version
33
- version: '1.17'
47
+ version: '1.20'
34
48
  type: :runtime
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '1.17'
54
+ version: '1.20'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rubocop-rails
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '2.19'
61
+ version: '2.23'
48
62
  type: :runtime
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: '2.19'
68
+ version: '2.23'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rubocop-rspec
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '2.17'
75
+ version: '2.25'
62
76
  type: :runtime
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '2.17'
69
- description:
82
+ version: '2.25'
83
+ description:
70
84
  email:
71
85
  - jfbastien@petalmd.com
72
86
  executables: []
@@ -80,6 +94,7 @@ files:
80
94
  - config/base.yml
81
95
  - config/default.yml
82
96
  - lib/rubocop-petal.rb
97
+ - lib/rubocop/cop/chewy/field_type.rb
83
98
  - lib/rubocop/cop/chewy/reset_on_type.rb
84
99
  - lib/rubocop/cop/grape/helpers_include_module.rb
85
100
  - lib/rubocop/cop/grape/prefer_namespace.rb
@@ -88,15 +103,26 @@ files:
88
103
  - lib/rubocop/cop/migration/change_table_references.rb
89
104
  - lib/rubocop/cop/migration/foreign_key_option.rb
90
105
  - lib/rubocop/cop/migration/schema_statements_methods.rb
106
+ - lib/rubocop/cop/migration/standalone_add_reference.rb
91
107
  - lib/rubocop/cop/performance/snif.rb
92
108
  - lib/rubocop/cop/petal_cops.rb
109
+ - lib/rubocop/cop/rails/destroy_all_bang.rb
93
110
  - lib/rubocop/cop/rails/enum_prefix.rb
111
+ - lib/rubocop/cop/rails/enum_starting_value.rb
94
112
  - lib/rubocop/cop/rails/risky_activerecord_invocation.rb
113
+ - lib/rubocop/cop/rspec/aggregate_examples.rb
114
+ - lib/rubocop/cop/rspec/aggregate_examples/language.rb
115
+ - lib/rubocop/cop/rspec/aggregate_examples/line_range_helpers.rb
116
+ - lib/rubocop/cop/rspec/aggregate_examples/matchers_with_side_effects.rb
117
+ - lib/rubocop/cop/rspec/aggregate_examples/metadata_helpers.rb
118
+ - lib/rubocop/cop/rspec/aggregate_examples/node_matchers.rb
95
119
  - lib/rubocop/cop/rspec/authenticated_as.rb
96
120
  - lib/rubocop/cop/rspec/create_list_max.rb
97
121
  - lib/rubocop/cop/rspec/json_response.rb
122
+ - lib/rubocop/cop/rspec/multiple_expectations_auto_correct.rb
98
123
  - lib/rubocop/cop/rspec/sidekiq_inline.rb
99
124
  - lib/rubocop/cop/rspec/stub_products.rb
125
+ - lib/rubocop/cop/sidekiq/no_nil_return.rb
100
126
  - lib/rubocop/petal.rb
101
127
  - lib/rubocop/petal/inject.rb
102
128
  - lib/rubocop/petal/version.rb
@@ -109,7 +135,7 @@ metadata:
109
135
  source_code_uri: https://github.com/petalmd/rubocop-petal
110
136
  bug_tracker_uri: https://github.com/petalmd/rubocop-petal/issues
111
137
  rubygems_mfa_required: 'true'
112
- post_install_message:
138
+ post_install_message:
113
139
  rdoc_options: []
114
140
  require_paths:
115
141
  - lib
@@ -117,15 +143,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
143
  requirements:
118
144
  - - ">="
119
145
  - !ruby/object:Gem::Version
120
- version: '2.6'
146
+ version: '2.7'
121
147
  required_rubygems_version: !ruby/object:Gem::Requirement
122
148
  requirements:
123
149
  - - ">="
124
150
  - !ruby/object:Gem::Version
125
151
  version: '0'
126
152
  requirements: []
127
- rubygems_version: 3.2.3
128
- signing_key:
153
+ rubygems_version: 3.4.18
154
+ signing_key:
129
155
  specification_version: 4
130
156
  summary: Petal custom cops
131
157
  test_files: []