rubocop-vicenzo 0.9.0 → 0.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5b813b05ed3c69c557d8b01bb90a4f142cbbb4ff06240d2df571f23ca5b720b
4
- data.tar.gz: c121942b02a939b96de5a264fd53db21ebba6dd85e85c9e95424538e59c5240c
3
+ metadata.gz: 4f7a8f58f53c6f2e6b50f1645a55cb401cd33b1f2654c793eaecd52cbf879153
4
+ data.tar.gz: f2eaba7a6bbb615190abf109a64681372988e3f8cea255c6055f6ae40698c7e2
5
5
  SHA512:
6
- metadata.gz: 3d6832e1b1d8a011f1ded3b21451e79f1722234bd7424ccf418a59550a79b20edd4b8a3632345df748982b356b32d868d530fe96267ce5b81689f32d8e9559ae
7
- data.tar.gz: b2513e3ab2f35243a88029d8f08ff2bd1bd29c08552c305e919cb76b10717e113cccf837276bc77b390bb87b5f34b276b08415adc1fd64c05566028107f65471
6
+ metadata.gz: '0258853f6e8d32fb88d7d4c53fb5520e2c828bd31d5968be91008ab34c13a729188c802e91539ef95eef937c56dc584cb59c7dbb9ddcc8c50e3cd6e991eb49a1'
7
+ data.tar.gz: c8a4a8d41af0c5d5f294acfd9361f1226f002590b051508b97a6fbcb9f3f36dbe695a6ae6efbccb7c122395bc9dd76e83f7cffc9a026f3d822954e20236483df
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.9.0"
2
+ ".": "0.10.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.9.0...v0.10.0) (2026-10-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * add Vicenzo/RSpec/LetAliasingSubject cop ([#44](https://github.com/bvicenzo/rubocop-vicenzo/issues/44)) ([a8cebd3](https://github.com/bvicenzo/rubocop-vicenzo/commit/a8cebd3c0237e8805a58ff9efa7ca34f6beb34b0)), closes [#42](https://github.com/bvicenzo/rubocop-vicenzo/issues/42)
9
+ * add Vicenzo/Style/PreferManyOverSizeComparison cop ([#38](https://github.com/bvicenzo/rubocop-vicenzo/issues/38)) ([910ea02](https://github.com/bvicenzo/rubocop-vicenzo/commit/910ea026f7e4c46f9de27db580e6330cf845bfe7))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * report subjects that derive from an allowed method in SubjectIsMethodResult ([#41](https://github.com/bvicenzo/rubocop-vicenzo/issues/41)) ([a5efd33](https://github.com/bvicenzo/rubocop-vicenzo/commit/a5efd3330b32dd24b4291121a64e378091297a57)), closes [#40](https://github.com/bvicenzo/rubocop-vicenzo/issues/40)
15
+ * see subjects declared in shared groups in LetIsMethodResult and LetAliasingSubject ([#45](https://github.com/bvicenzo/rubocop-vicenzo/issues/45)) ([a98039e](https://github.com/bvicenzo/rubocop-vicenzo/commit/a98039e2cbea568267d8b7832b4717b763678d14)), closes [#43](https://github.com/bvicenzo/rubocop-vicenzo/issues/43)
16
+
3
17
  ## [0.9.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.8.0...v0.9.0) (2026-09-23)
4
18
 
5
19
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ Add it to the `development`/`test` group of your `Gemfile`:
11
11
  <!-- x-release-please-start-version -->
12
12
  ```ruby
13
13
  group :development, :test do
14
- gem 'rubocop-vicenzo', '~> 0.9.0', require: false
14
+ gem 'rubocop-vicenzo', '~> 0.10.0', require: false
15
15
  end
16
16
  ```
17
17
  <!-- x-release-please-end-version -->
data/config/default.yml CHANGED
@@ -127,6 +127,17 @@ Vicenzo/RSpec/LeakyDefinition:
127
127
  - '**/spec/support/**/*'
128
128
  - '**/spec/factories/**/*'
129
129
 
130
+ Vicenzo/RSpec/LetAliasingSubject:
131
+ Description: 'A let must not hold the subject itself under another name.'
132
+ Enabled: true
133
+ Severity: convention
134
+ Include:
135
+ - '**/spec/**/*_spec.rb'
136
+ Exclude:
137
+ - '**/spec/support/**/*'
138
+ - '**/spec/factories/**/*'
139
+ VersionAdded: '0.10.0'
140
+
130
141
  Vicenzo/RSpec/LetIsMethodResult:
131
142
  Description: 'A let under an example group describing a method must not hold the result of calling that method on the subject.'
132
143
  Enabled: true
@@ -244,3 +255,10 @@ Vicenzo/Style/MultilineMethodCallParentheses:
244
255
  Enabled: true
245
256
  AllowedMethods: []
246
257
  VersionAdded: '0.3.0'
258
+
259
+ Vicenzo/Style/PreferManyOverSizeComparison:
260
+ Description: 'Use many? instead of comparing size or length with one.'
261
+ Enabled: true
262
+ Severity: convention
263
+ SafeAutoCorrect: false
264
+ VersionAdded: '0.10.0'
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'returned_chain'
4
+ require_relative 'mixin/premise_tracking'
5
+ require_relative 'mixin/subject_reference'
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module Vicenzo
10
+ module RSpec
11
+ # A `let` whose value is the subject itself is the subject under another name. Refer to the subject by
12
+ # its own name instead.
13
+ #
14
+ # The subject is who the sentences are about. A second name for it splits that across two
15
+ # declarations: the examples reading `hunter` no longer say they are about the subject, and every cop
16
+ # that recognises the subject by its name stops recognising it there. In "the cat caught the ball",
17
+ # calling the cat `hunter` in half of the sentences does not add a character - it hides one.
18
+ #
19
+ # A subject that was declared without a name and is then given one by a `let` is the same thing the
20
+ # other way round: the name belongs to the subject's declaration, `subject(:hunter) { ... }`. Inside a
21
+ # shared group that declares no named subject, the declaration lives where the group is included, so
22
+ # the subject is referred to as `subject`.
23
+ #
24
+ # This composes with `Vicenzo/RSpec/LetIsMethodResult`: once the alias is gone, a `let` that called
25
+ # the described method through it calls it on the subject, and that cop reports it.
26
+ #
27
+ # Only a value that is the subject and nothing more counts. A `let` deriving something from the
28
+ # subject, such as `cat.name`, is a premise, not an alias.
29
+ #
30
+ # @example
31
+ # # bad - `hunter` is the cat
32
+ # describe '#catch' do
33
+ # subject(:cat) { Cat.new(name: 'Bixano') }
34
+ #
35
+ # let(:hunter) { cat }
36
+ #
37
+ # it { expect(hunter.catch(object: Ball.new)).to eq(:success) }
38
+ # end
39
+ #
40
+ # # good - one name for the subject
41
+ # describe '#catch' do
42
+ # subject(:cat) { Cat.new(name: 'Bixano') }
43
+ #
44
+ # it { expect(cat.catch(object: Ball.new)).to eq(:success) }
45
+ # end
46
+ #
47
+ # @example an anonymous subject named by a let
48
+ # # bad - the name belongs to the subject's declaration
49
+ # describe '#catch' do
50
+ # subject { Cat.new(name: 'Bixano') }
51
+ #
52
+ # let(:hunter) { subject }
53
+ # end
54
+ #
55
+ # # good
56
+ # describe '#catch' do
57
+ # subject(:hunter) { Cat.new(name: 'Bixano') }
58
+ # end
59
+ #
60
+ # @example a shared group naming the subject of whoever includes it
61
+ # # bad - the subject is declared where the group is included, under a name it cannot know
62
+ # shared_examples 'a hunter' do
63
+ # let(:hunter) { subject }
64
+ #
65
+ # it { expect(hunter.catch(object: Ball.new)).to eq(:success) }
66
+ # end
67
+ #
68
+ # # good - `subject` holds wherever the group is included
69
+ # shared_examples 'a hunter' do
70
+ # it { expect(subject.catch(object: Ball.new)).to eq(:success) }
71
+ # end
72
+ class LetAliasingSubject < RuboCop::Cop::RSpec::Base
73
+ include PremiseTracking
74
+ include SubjectReference
75
+
76
+ MSG = 'Let `:%<name>s` is the subject under another name. Refer to the subject as `%<subject>s`.'
77
+ UNNAMED_MSG = 'Let `:%<name>s` is the subject under another name. Name the subject `%<name>s` instead.'
78
+
79
+ def on_block(node)
80
+ return unless let?(node)
81
+
82
+ reference = sole_call(node.body)
83
+ return unless reference
84
+
85
+ names = subject_names(node)
86
+ return unless subject_reference?(reference, names)
87
+
88
+ add_offense(node.send_node, message: message(node, reference.method_name, names))
89
+ end
90
+
91
+ alias on_numblock on_block
92
+ alias on_itblock on_block
93
+
94
+ private
95
+
96
+ def sole_call(body)
97
+ calls = ReturnedChain.new(body).to_a
98
+
99
+ calls.first if calls.one?
100
+ end
101
+
102
+ # `subject` points at the nearest named subject when there is one; without it, the alias is the name
103
+ # the subject was missing. Inside a shared group the subject is declared where the group is included,
104
+ # under a name this group cannot know, so `subject` is the one name that holds everywhere.
105
+ def message(node, reference_name, names)
106
+ name = premise_name(node)
107
+ subject = reference_name == :subject ? names.find { |candidate| candidate != :subject } : reference_name
108
+ return format(MSG, name:, subject:) if subject
109
+ return format(MSG, name:, subject: :subject) if inside_shared_group?(node)
110
+
111
+ format(UNNAMED_MSG, name:)
112
+ end
113
+
114
+ def inside_shared_group?(node)
115
+ node.each_ancestor(:any_block).any? { |ancestor| shared_group?(ancestor) }
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'mixin/described_method'
4
4
  require_relative 'mixin/premise_tracking'
5
+ require_relative 'mixin/subject_reference'
5
6
 
6
7
  module RuboCop
7
8
  module Cop
@@ -21,8 +22,10 @@ module RuboCop
21
22
  #
22
23
  # Only calls on the subject count, which is what separates the result from the setup: a `let`
23
24
  # calling the described method on some other object is building a premise, not the outcome the
24
- # example is about. `AllowedMethods` is shared with the sibling cop and ships empty, for the
25
- # layers whose convention is a single entry point.
25
+ # example is about. A `let` that only renames the subject is `Vicenzo/RSpec/LetAliasingSubject`'s
26
+ # to report; once it is gone, the call goes through the subject's own name and is reported here.
27
+ # `AllowedMethods` is shared with the sibling cop and ships empty, for the layers whose convention
28
+ # is a single entry point.
26
29
  #
27
30
  # @example
28
31
  # # bad - the expectation asserts on a name; the cat never catches anything in the example
@@ -51,6 +54,7 @@ module RuboCop
51
54
  class LetIsMethodResult < RuboCop::Cop::RSpec::Base
52
55
  include DescribedMethod
53
56
  include PremiseTracking
57
+ include SubjectReference
54
58
 
55
59
  MSG = 'Let `:%<name>s` holds what `%<method>s` returned. Undo it and call `%<method>s` on the ' \
56
60
  'subject inside the expectation.'
@@ -72,39 +76,16 @@ module RuboCop
72
76
 
73
77
  private
74
78
 
79
+ # The receiver is read from the chain itself, the call right after the invocation, so that a subject
80
+ # passed along through a local variable or parentheses is still the subject.
75
81
  def invocation_on_subject(node, method_name)
76
82
  names = subject_names(node)
77
83
 
78
- call_in_chain(node.body) do |call|
79
- call.method?(method_name) && subject_reference?(call.receiver, names)
84
+ call, = ReturnedChain.new(node.body).each_cons(2).find do |invocation, receiver|
85
+ invocation.method?(method_name) && subject_reference?(receiver, names)
80
86
  end
81
- end
82
-
83
- # `subject` always refers to the subject, declared or implicit; a named one answers to its name
84
- # as well.
85
- def subject_names(node)
86
- names = Set[:subject]
87
-
88
- node.each_ancestor(:any_block).each do |ancestor|
89
- next unless example_group?(ancestor)
90
-
91
- each_child_block(ancestor) { |child| names << subject_name(child) if subject?(child) }
92
- end
93
-
94
- names
95
- end
96
-
97
- # An anonymous `subject!` is still read as `subject`.
98
- def subject_name(node)
99
- name = premise_name(node).to_sym
100
-
101
- name == :subject! ? :subject : name
102
- end
103
-
104
- def subject_reference?(receiver, names)
105
- return false unless receiver&.send_type? && receiver.receiver.nil? && receiver.arguments.empty?
106
87
 
107
- names.include?(receiver.method_name)
88
+ call
108
89
  end
109
90
  end
110
91
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../returned_chain'
4
+
3
5
  module RuboCop
4
6
  module Cop
5
7
  module Vicenzo
@@ -42,27 +44,11 @@ module RuboCop
42
44
  cop_config.fetch('AllowedMethods', [])
43
45
  end
44
46
 
45
- # Walks the expression a definition returns down its receiver chain, yielding each call, so that
46
- # `Cat.new.catch(ball).to_s` is seen as built on `catch` while `Cat.new(catches: other.catch)`
47
- # is not - there, nothing of `catch` reaches the value.
48
- def call_in_chain(body)
49
- node = returned_expression(body)
50
-
51
- while node
52
- node = node.send_node if node.any_block_type?
53
- break unless node.type?(:call)
54
- return node if yield(node)
55
-
56
- node = node.receiver
57
- end
58
-
59
- nil
60
- end
61
-
62
- def returned_expression(body)
63
- return if body.nil?
64
-
65
- body.begin_type? ? body.children.last : body
47
+ # The first call the value of a definition is built on that satisfies the block, seen from the outside in:
48
+ # `Cat.new.catch(ball).to_s` is built on `catch` while `Cat.new(catches: other.catch)` is not - there,
49
+ # nothing of `catch` reaches the value. See `ReturnedChain`.
50
+ def call_in_chain(body, &)
51
+ ReturnedChain.new(body).find(&)
66
52
  end
67
53
  end
68
54
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vicenzo
6
+ module RSpec
7
+ # Shared helpers for the cops that ask whether a definition reads the subject: which names answer to it
8
+ # where the definition sits, and whether a call is a bare reference to one of them.
9
+ #
10
+ # Meant for cops inheriting from `RuboCop::Cop::RSpec::Base` and including `PremiseTracking`:
11
+ # `spec_group?` and `subject?` come from the former, `each_child_block` and `premise_name` from the
12
+ # latter.
13
+ module SubjectReference
14
+ private
15
+
16
+ # `subject` always refers to the subject, declared or implicit; a named one answers to its name
17
+ # as well. A definition is read in every group below the one declaring it, so the subjects
18
+ # declared down there - the "abstract let" shape - answer too. Names come nearest group first, after
19
+ # `subject`.
20
+ #
21
+ # Shared groups count as groups, around the definition and below it: RSpec only lets a shared group
22
+ # be included under the group defining it, and a subject declared in one is the subject wherever it
23
+ # is included.
24
+ def subject_names(node)
25
+ names = Set[:subject]
26
+ groups = node.each_ancestor(:any_block).select { |ancestor| spec_group?(ancestor) }
27
+
28
+ collect_subject_names(groups.first, names, nested: true) if groups.any?
29
+ groups.drop(1).each { |group| collect_subject_names(group, names) }
30
+
31
+ names
32
+ end
33
+
34
+ def collect_subject_names(group, names, nested: false)
35
+ each_child_block(group) do |child|
36
+ if subject?(child)
37
+ names << subject_name(child)
38
+ elsif nested && spec_group?(child)
39
+ collect_subject_names(child, names, nested:)
40
+ end
41
+ end
42
+ end
43
+
44
+ # An anonymous `subject!` is still read as `subject`, so it never passes for a name the subject was
45
+ # given.
46
+ def subject_name(node)
47
+ name = premise_name(node).to_sym
48
+
49
+ name == :subject! ? :subject : name
50
+ end
51
+
52
+ def subject_reference?(receiver, names)
53
+ return false unless receiver.send_type? && receiver.receiver.nil? && receiver.arguments.empty?
54
+
55
+ names.include?(receiver.method_name)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vicenzo
6
+ module RSpec
7
+ # The calls the value of a block body is built on, outermost first: `Cat.new.catch(ball).to_s` yields `to_s`,
8
+ # `catch` and `new`, while `Cat.new(catches: other.catch)` yields only `new` - nothing of `catch` reaches the
9
+ # value.
10
+ #
11
+ # The walk starts at the statement the body returns and follows its receivers, seeing through what only
12
+ # passes a value along: parentheses, an assignment, and a local variable, which answers to its latest
13
+ # assignment among the statements before the one reading it.
14
+ class ReturnedChain
15
+ include Enumerable
16
+
17
+ attr_reader :expression
18
+
19
+ def initialize(body)
20
+ @statements = statements_of(body)
21
+ @expression = @statements.last
22
+ end
23
+
24
+ def each
25
+ node = expression
26
+ reach = @statements.size - 1
27
+
28
+ while node
29
+ node, reach = value_of(node, reach)
30
+ node = node.send_node if node&.any_block_type?
31
+ break unless node&.type?(:call)
32
+
33
+ yield node
34
+ node = node.receiver
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def statements_of(body)
41
+ return [] if body.nil?
42
+
43
+ body.begin_type? ? body.children : [body]
44
+ end
45
+
46
+ # `reach` is the index of the statement the node is read in: a variable only answers to an assignment made
47
+ # before it, which keeps `caught = caught.to_s` from answering to itself.
48
+ def value_of(node, reach)
49
+ loop do
50
+ case node&.type
51
+ when :begin then node = node.children.last
52
+ when :lvasgn then node = node.expression
53
+ when :lvar
54
+ reach = assignment_before(node.name, reach)
55
+ node = reach && @statements[reach]
56
+ else return [node, reach]
57
+ end
58
+ end
59
+ end
60
+
61
+ def assignment_before(name, reach)
62
+ (reach - 1).downto(0).find do |index|
63
+ statement = @statements[index]
64
+ statement.lvasgn_type? && statement.name == name
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -24,7 +24,11 @@ module RuboCop
24
24
  # (a service object always invoked through `call`, for instance). `AllowedMethods` exists
25
25
  # for those and ships empty: the convention belongs to the project that adopts it, not to
26
26
  # this gem. Declaring it there - or excluding a whole directory through the standard
27
- # `Exclude` - keeps the exemption visible and reviewable in `.rubocop.yml`.
27
+ # `Exclude` - keeps the exemption visible and reviewable in `.rubocop.yml`. The exemption
28
+ # covers the result itself, not what is derived from it: a subject that chains anything
29
+ # after the allowed call - a projection such as `map`, or unwrapping a monad through
30
+ # `value` - is still reported, since it drops the data the examples end up calling the
31
+ # method again to reach.
28
32
  #
29
33
  # An inline disable directive is not that: it is an exemption nobody reviewed. Dropping
30
34
  # the `#` from the description is not either - the cop reads bare descriptions too, and the
@@ -59,26 +63,54 @@ module RuboCop
59
63
  # describe '.call' do
60
64
  # subject(:result) { described_class.(object: Ball.new) }
61
65
  # end
66
+ #
67
+ # # bad - the subject is derived from the result, not the result itself
68
+ # describe '.call' do
69
+ # subject(:ids) { described_class.().map(&:id) }
70
+ # end
71
+ #
72
+ # # bad - unwrapping the monad keeps only a part of the result
73
+ # describe '.call' do
74
+ # subject(:value) { described_class.(object: Ball.new).value }
75
+ # end
62
76
  class SubjectIsMethodResult < RuboCop::Cop::RSpec::Base
63
77
  include DescribedMethod
64
78
 
65
79
  MSG = 'Subject holds what `%<method>s` returned, not the object under test. ' \
66
80
  'Make the subject the object that receives `%<method>s`, and call it inside the example.'
81
+ DERIVED_MSG = 'Subject derives from what `%<method>s` returned. ' \
82
+ 'Keep the subject as the result of `%<method>s`, and derive from it inside the example.'
67
83
 
68
84
  def on_block(node)
69
85
  return unless subject?(node)
70
86
 
71
87
  method_name = described_method_name(node)
72
- return if method_name.nil? || allowed_methods.include?(method_name)
88
+ return if method_name.nil?
73
89
 
74
- invocation = call_in_chain(node.body) { |call| call.method?(method_name) }
75
- return unless invocation
90
+ chain = ReturnedChain.new(node.body)
91
+ invocations = chain.select { |call| call.method?(method_name) }
92
+ return if invocations.empty?
76
93
 
77
- add_offense(invocation, message: format(MSG, method: method_name))
94
+ if allowed_methods.include?(method_name)
95
+ check_derivation(chain, invocations.last, method_name)
96
+ else
97
+ add_offense(invocations.first, message: format(MSG, method: method_name))
98
+ end
78
99
  end
79
100
 
80
101
  alias on_numblock on_block
81
102
  alias on_itblock on_block
103
+
104
+ private
105
+
106
+ # An allowed method may be the subject only as the returned expression itself: its innermost
107
+ # invocation has to be the outermost call of the chain. A block handed to it still belongs to the
108
+ # call; anything chained after it - calling the result again included - does not.
109
+ def check_derivation(chain, innermost_invocation, method_name)
110
+ return if chain.first.equal?(innermost_invocation)
111
+
112
+ add_offense(chain.expression, message: format(DERIVED_MSG, method: method_name))
113
+ end
82
114
  end
83
115
  end
84
116
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vicenzo
6
+ module Style
7
+ # Enforces `many?` over comparing `size` or `length` with one.
8
+ #
9
+ # Active Support's `Enumerable#many?` exists precisely to name the
10
+ # "more than one element" question that `size > 1` only spells out.
11
+ # RuboCop's own `Style/CollectionQuerying` already covers `count > 1`, but
12
+ # leaves `size` and `length` alone, which is the form most often written.
13
+ # This cop closes that gap.
14
+ #
15
+ # Only an explicit receiver calling `size` or `length` without arguments is
16
+ # flagged, compared with one through `>` or with two through `>=`, on
17
+ # either side of the operator. Like `Style/CollectionQuerying`, the cop
18
+ # only runs when `AllCops: ActiveSupportExtensionsEnabled` is on, since
19
+ # `many?` comes from Active Support. `rubocop-rails` turns it on.
20
+ #
21
+ # @safety
22
+ # The cop is unsafe because the receiver might not include `Enumerable`.
23
+ # `String`, for instance, responds to `size` and `length` but not to
24
+ # `many?`. The same holds for any object with its own `count`, which is
25
+ # why `Style/CollectionQuerying` is unsafe as well.
26
+ #
27
+ # @example
28
+ # # bad
29
+ # items.size > 1
30
+ # items.length >= 2
31
+ # 1 < items.size
32
+ #
33
+ # # good
34
+ # items.many?
35
+ class PreferManyOverSizeComparison < Base
36
+ extend AutoCorrector
37
+
38
+ MSG = 'Use `many?` instead of `%<comparison>s`.'
39
+
40
+ RESTRICT_ON_SEND = %i[> >= < <=].freeze
41
+
42
+ # @!method size_comparison(node)
43
+ def_node_matcher :size_comparison, <<~PATTERN
44
+ {
45
+ (call $(call !nil? {:size :length}) {:> (int 1) | :>= (int 2)})
46
+ (call {(int 1) :< | (int 2) :<=} $(call !nil? {:size :length}))
47
+ }
48
+ PATTERN
49
+
50
+ def on_send(node)
51
+ return unless active_support_extensions_enabled?
52
+ return unless (size_node = size_comparison(node))
53
+
54
+ message = format(MSG, comparison: comparison_source(node, size_node))
55
+
56
+ add_offense(node, message: message) do |corrector|
57
+ corrector.replace(node, "#{size_node.receiver.source}#{size_node.loc.dot.source}many?")
58
+ end
59
+ end
60
+ alias on_csend on_send
61
+
62
+ private
63
+
64
+ def comparison_source(node, size_node)
65
+ size_source = size_node.method_name
66
+
67
+ if node.receiver.equal?(size_node)
68
+ "#{size_source} #{node.method_name} #{node.first_argument.source}"
69
+ else
70
+ "#{node.receiver.source} #{node.method_name} #{size_source}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -13,6 +13,7 @@ require_relative 'vicenzo/rspec/nested_context_improper_start'
13
13
  require_relative 'vicenzo/rspec/nested_let_redefinition'
14
14
  require_relative 'vicenzo/rspec/nested_subject_redefinition'
15
15
  require_relative 'vicenzo/rspec/leaky_definition'
16
+ require_relative 'vicenzo/rspec/let_aliasing_subject'
16
17
  require_relative 'vicenzo/rspec/subject_defined_as_let'
17
18
  require_relative 'vicenzo/rspec/subject_is_method_result'
18
19
  require_relative 'vicenzo/rspec/top_level_context'
@@ -27,3 +28,4 @@ require_relative 'vicenzo/naming/anonymous_unused_name'
27
28
  require_relative 'vicenzo/naming/short_name'
28
29
  require_relative 'vicenzo/style/json_parse_symbolize_names'
29
30
  require_relative 'vicenzo/style/multiline_method_call_parentheses'
31
+ require_relative 'vicenzo/style/prefer_many_over_size_comparison'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Vicenzo
5
- VERSION = '0.9.0'
5
+ VERSION = '0.10.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-vicenzo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Vicenzo
@@ -86,15 +86,18 @@ files:
86
86
  - lib/rubocop/cop/vicenzo/rspec/inconsistent_sibling_structure.rb
87
87
  - lib/rubocop/cop/vicenzo/rspec/iteration_inside_example.rb
88
88
  - lib/rubocop/cop/vicenzo/rspec/leaky_definition.rb
89
+ - lib/rubocop/cop/vicenzo/rspec/let_aliasing_subject.rb
89
90
  - lib/rubocop/cop/vicenzo/rspec/let_is_method_result.rb
90
91
  - lib/rubocop/cop/vicenzo/rspec/mixin/described_method.rb
91
92
  - lib/rubocop/cop/vicenzo/rspec/mixin/expectation_target.rb
92
93
  - lib/rubocop/cop/vicenzo/rspec/mixin/premise_tracking.rb
94
+ - lib/rubocop/cop/vicenzo/rspec/mixin/subject_reference.rb
93
95
  - lib/rubocop/cop/vicenzo/rspec/mixin/top_level_definition.rb
94
96
  - lib/rubocop/cop/vicenzo/rspec/mutated_premises.rb
95
97
  - lib/rubocop/cop/vicenzo/rspec/nested_context_improper_start.rb
96
98
  - lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb
97
99
  - lib/rubocop/cop/vicenzo/rspec/nested_subject_redefinition.rb
100
+ - lib/rubocop/cop/vicenzo/rspec/returned_chain.rb
98
101
  - lib/rubocop/cop/vicenzo/rspec/subject_defined_as_let.rb
99
102
  - lib/rubocop/cop/vicenzo/rspec/subject_is_method_result.rb
100
103
  - lib/rubocop/cop/vicenzo/rspec/top_level_context.rb
@@ -105,6 +108,7 @@ files:
105
108
  - lib/rubocop/cop/vicenzo/rspec/unnested_context_improper_start.rb
106
109
  - lib/rubocop/cop/vicenzo/style/json_parse_symbolize_names.rb
107
110
  - lib/rubocop/cop/vicenzo/style/multiline_method_call_parentheses.rb
111
+ - lib/rubocop/cop/vicenzo/style/prefer_many_over_size_comparison.rb
108
112
  - lib/rubocop/cop/vicenzo_cops.rb
109
113
  - lib/rubocop/vicenzo.rb
110
114
  - lib/rubocop/vicenzo/plugin.rb