Skip to content

Commit 4981738

Browse files
lovro-bikicbbatsov
authored andcommitted
Fix to recognize safe navigation when config is enabled
1 parent 6f066b8 commit 4981738

File tree

3 files changed

+161
-67
lines changed

3 files changed

+161
-67
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#13517](https://github.com/rubocop/rubocop/pull/13517): Fixes `Style/HashExcept` to recognize safe navigation when `ActiveSupportExtensionsEnabled` config is enabled. ([@lovro-bikic][])

lib/rubocop/cop/style/hash_except.rb

Lines changed: 54 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ module Style
1010
# (`Hash#except` was added in Ruby 3.0.)
1111
#
1212
# For safe detection, it is limited to commonly used string and symbol comparisons
13-
# when used `==`.
14-
# And do not check `Hash#delete_if` and `Hash#keep_if` to change receiver object.
13+
# when using `==` or `!=`.
14+
#
15+
# This cop doesn't check for `Hash#delete_if` and `Hash#keep_if` because they
16+
# modify the receiver.
1517
#
1618
# @safety
1719
# This cop is unsafe because it cannot be guaranteed that the receiver
@@ -51,44 +53,31 @@ class HashExcept < Base
5153
MSG = 'Use `%<prefer>s` instead.'
5254
RESTRICT_ON_SEND = %i[reject select filter].freeze
5355

54-
# @!method bad_method_with_poro?(node)
55-
def_node_matcher :bad_method_with_poro?, <<~PATTERN
56-
(block
57-
(call _ _)
58-
(args
59-
$(arg _)
60-
(arg _))
61-
{
62-
$(send
63-
_ {:== :!= :eql? :include?} _)
64-
(send
65-
$(send
66-
_ {:== :!= :eql? :include?} _) :!)
67-
})
68-
PATTERN
56+
SUBSET_METHODS = %i[== != eql? include?].freeze
57+
ACTIVE_SUPPORT_SUBSET_METHODS = (SUBSET_METHODS + %i[in? exclude?]).freeze
6958

70-
# @!method bad_method_with_active_support?(node)
71-
def_node_matcher :bad_method_with_active_support?, <<~PATTERN
59+
# @!method block_with_first_arg_check?(node)
60+
def_node_matcher :block_with_first_arg_check?, <<~PATTERN
7261
(block
73-
(send _ _)
62+
(call _ _)
7463
(args
75-
$(arg _)
64+
$(arg _key)
7665
(arg _))
7766
{
7867
$(send
79-
_ {:== :!= :eql? :in? :include? :exclude?} _)
68+
{(lvar _key) $_ _ | _ $_ (lvar _key)})
8069
(send
8170
$(send
82-
_ {:== :!= :eql? :in? :include? :exclude?} _) :!)
71+
{(lvar _key) $_ _ | _ $_ (lvar _key)}) :!)
8372
})
8473
PATTERN
8574

8675
def on_send(node)
8776
block = node.parent
88-
return unless bad_method?(block) && semantically_except_method?(node, block)
77+
return unless extracts_hash_subset?(block) && semantically_except_method?(node, block)
8978

9079
except_key = except_key(block)
91-
return if except_key.nil? || !safe_to_register_offense?(block, except_key)
80+
return unless safe_to_register_offense?(block, except_key)
9281

9382
range = offense_range(node)
9483
preferred_method = "except(#{except_key_source(except_key)})"
@@ -101,68 +90,67 @@ def on_send(node)
10190

10291
private
10392

104-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
105-
def bad_method?(block)
106-
if active_support_extensions_enabled?
107-
bad_method_with_active_support?(block) do |key_arg, send_node|
108-
if send_node.method?(:in?) && send_node.receiver&.source != key_arg.source
109-
return false
110-
end
111-
return true if !send_node.method?(:include?) && !send_node.method?(:exclude?)
112-
113-
send_node.first_argument&.source == key_arg.source
114-
end
115-
else
116-
bad_method_with_poro?(block) do |key_arg, send_node|
117-
!send_node.method?(:include?) || send_node.first_argument&.source == key_arg.source
93+
def extracts_hash_subset?(block)
94+
block_with_first_arg_check?(block) do |key_arg, send_node, method|
95+
return false unless supported_subset_method?(method)
96+
97+
case method
98+
when :include?, :exclude?
99+
send_node.first_argument.source == key_arg.source
100+
when :in?
101+
send_node.receiver.source == key_arg.source
102+
else
103+
true
118104
end
119105
end
120106
end
121-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
122107

123-
def semantically_except_method?(send, block)
124-
body = block.body
108+
def supported_subset_method?(method)
109+
if active_support_extensions_enabled?
110+
ACTIVE_SUPPORT_SUBSET_METHODS.include?(method)
111+
else
112+
SUBSET_METHODS.include?(method)
113+
end
114+
end
125115

126-
negated = body.method?('!')
127-
body = body.receiver if negated
116+
def semantically_except_method?(node, block)
117+
body, negated = extract_body_if_negated(block.body)
128118

129-
case send.method_name
130-
when :reject
131-
body.method?('==') || body.method?('eql?') || included?(negated, body)
132-
when :select, :filter
133-
body.method?('!=') || not_included?(negated, body)
119+
if node.method?('reject')
120+
body.method?('==') || body.method?('eql?') || included?(body, negated)
134121
else
135-
false
122+
body.method?('!=') || not_included?(body, negated)
136123
end
137124
end
138125

139-
def included?(negated, body)
126+
def included?(body, negated)
140127
if negated
141128
body.method?('exclude?')
142129
else
143130
body.method?('include?') || body.method?('in?')
144131
end
145132
end
146133

147-
def not_included?(negated, body)
148-
included?(!negated, body)
134+
def not_included?(body, negated)
135+
included?(body, !negated)
149136
end
150137

151138
def safe_to_register_offense?(block, except_key)
152-
extracted = extract_body_if_negated(block.body)
153-
if extracted.method?('in?') || extracted.method?('include?') ||
154-
extracted.method?('exclude?')
155-
return true
156-
end
157-
return true if block.body.method?('eql?')
139+
body = block.body
158140

159-
except_key.sym_type? || except_key.str_type?
141+
if body.method?('==') || body.method?('!=')
142+
except_key.sym_type? || except_key.str_type?
143+
else
144+
true
145+
end
160146
end
161147

162148
def extract_body_if_negated(body)
163-
return body unless body.method?('!')
164-
165-
body.receiver
149+
if body.method?('!')
150+
[body.receiver, true]
151+
else
152+
[body, false]
153+
end
166154
end
167155

168156
def except_key_source(key)
@@ -187,12 +175,11 @@ def decorate_source(value)
187175
end
188176

189177
def except_key(node)
190-
key_argument = node.argument_list.first.source
191-
body = extract_body_if_negated(node.body)
178+
key_arg = node.argument_list.first.source
179+
body, = extract_body_if_negated(node.body)
192180
lhs, _method_name, rhs = *body
193-
return if [lhs, rhs].map(&:source).none?(key_argument)
194181

195-
[lhs, rhs].find { |operand| operand.source != key_argument }
182+
lhs.source == key_arg ? rhs : lhs
196183
end
197184

198185
def offense_range(node)

spec/rubocop/cop/style/hash_except_spec.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@
9696
{foo: 1, bar: 2, baz: 3}.reject { |k, v| k.in?(%i[foo bar]) }
9797
RUBY
9898
end
99+
100+
it 'does not register offenses when using safe navigation `reject` and calling `key.in?` method with symbol array' do
101+
expect_no_offenses(<<~RUBY)
102+
{foo: 1, bar: 2, baz: 3}&.reject { |k, v| k.in?(%i[foo bar]) }
103+
RUBY
104+
end
105+
106+
it 'does not register offenses when using `reject` and calling `in?` method with key' do
107+
expect_no_offenses(<<~RUBY)
108+
{foo: 1, bar: 2, baz: 3}.reject { |k, v| %i[foo bar].in?(k) }
109+
RUBY
110+
end
99111
end
100112

101113
context 'using `include?`' do
@@ -188,6 +200,12 @@
188200
RUBY
189201
end
190202

203+
it 'does not register offenses when using safe navigation `reject` and calling `!exclude?` method with symbol array' do
204+
expect_no_offenses(<<~RUBY)
205+
{foo: 1, bar: 2, baz: 3}&.reject { |k, v| !%i[foo bar].exclude?(k) }
206+
RUBY
207+
end
208+
191209
it 'does not register an offense when using `reject` and calling `exclude?` method on a key' do
192210
expect_no_offenses(<<~RUBY)
193211
{foo: 1, bar: 2, baz: 3}.reject { |k, v| k.exclude?('oo') }
@@ -207,6 +225,24 @@
207225
RUBY
208226
end
209227

228+
it 'does not register an offense when using `select` and other than comparison by string and symbol using `!=`' do
229+
expect_no_offenses(<<~RUBY)
230+
hash.select { |k, v| k != 0.0 }
231+
RUBY
232+
end
233+
234+
it 'does not register an offense when using `select` and other than comparison by string and symbol using `==` with bang' do
235+
expect_no_offenses(<<~RUBY)
236+
hash.select { |k, v| !(k == 0.0) }
237+
RUBY
238+
end
239+
240+
it 'does not register an offense when using `reject` and other than comparison by string and symbol using `!=` with bang' do
241+
expect_no_offenses(<<~RUBY)
242+
hash.reject { |k, v| !(k != 0.0) }
243+
RUBY
244+
end
245+
210246
it 'does not register an offense when using `delete_if` and comparing with `lvar == :sym`' do
211247
expect_no_offenses(<<~RUBY)
212248
{foo: 1, bar: 2, baz: 3}.delete_if { |k, v| k == :bar }
@@ -225,6 +261,18 @@
225261
RUBY
226262
end
227263

264+
it 'does not register an offense when using more than two block arguments' do
265+
expect_no_offenses(<<~RUBY)
266+
{foo: 1, bar: 2, baz: 3}.reject { |k, v, o| k == :bar }
267+
RUBY
268+
end
269+
270+
it 'does not register an offense when calling `include?` method without a param' do
271+
expect_no_offenses(<<~RUBY)
272+
{foo: 1, bar: 2, baz: 3}.reject { |k, v| %i[foo bar].include? }
273+
RUBY
274+
end
275+
228276
context 'when `AllCops/ActiveSupportExtensionsEnabled: true`' do
229277
let(:config) do
230278
RuboCop::Config.new('AllCops' => {
@@ -322,6 +370,17 @@
322370
RUBY
323371
end
324372

373+
it 'registers and corrects an offense when using safe navigation `reject` and calling `key.in?` method with symbol array' do
374+
expect_offense(<<~RUBY)
375+
{foo: 1, bar: 2, baz: 3}&.reject { |k, v| k.in?(%i[foo bar]) }
376+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `except(:foo, :bar)` instead.
377+
RUBY
378+
379+
expect_correction(<<~RUBY)
380+
{foo: 1, bar: 2, baz: 3}&.except(:foo, :bar)
381+
RUBY
382+
end
383+
325384
it 'registers and corrects an offense when using `select` and calling `!key.in?` method with symbol array' do
326385
expect_offense(<<~RUBY)
327386
{foo: 1, bar: 2, baz: 3}.select { |k, v| !k.in?(%i[foo bar]) }
@@ -396,6 +455,12 @@
396455
RUBY
397456
end
398457

458+
it 'does not register an offense when using `reject` and calling `in?` method with key' do
459+
expect_no_offenses(<<~RUBY)
460+
{foo: 1, bar: 2, baz: 3}.reject { |k, v| %i[foo bar].in?(k) }
461+
RUBY
462+
end
463+
399464
it 'does not register an offense when using `reject` and calling `in?` method with symbol array and second block value' do
400465
expect_no_offenses(<<~RUBY)
401466
{foo: 1, bar: 2, baz: 3}.reject { |k, v| v.in?([1, 2]) }
@@ -503,6 +568,17 @@
503568
RUBY
504569
end
505570

571+
it 'registers and corrects an offense when using safe navigation `reject` and calling `!exclude?` method with symbol array' do
572+
expect_offense(<<~RUBY)
573+
{foo: 1, bar: 2, baz: 3}&.reject { |k, v| !%i[foo bar].exclude?(k) }
574+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `except(:foo, :bar)` instead.
575+
RUBY
576+
577+
expect_correction(<<~RUBY)
578+
{foo: 1, bar: 2, baz: 3}&.except(:foo, :bar)
579+
RUBY
580+
end
581+
506582
it 'registers and corrects an offense when using `select` and calling `exclude?` method with symbol array' do
507583
expect_offense(<<~RUBY)
508584
{foo: 1, bar: 2, baz: 3}.select { |k, v| %i[foo bar].exclude?(k) }
@@ -602,6 +678,24 @@
602678
RUBY
603679
end
604680

681+
it 'does not register an offense when using `select` and other than comparison by string and symbol using `!=`' do
682+
expect_no_offenses(<<~RUBY)
683+
hash.select { |k, v| k != 0.0 }
684+
RUBY
685+
end
686+
687+
it 'does not register an offense when using `select` and other than comparison by string and symbol using `==` with bang' do
688+
expect_no_offenses(<<~RUBY)
689+
hash.select { |k, v| !(k == 0.0) }
690+
RUBY
691+
end
692+
693+
it 'does not register an offense when using `reject` and other than comparison by string and symbol using `!=` with bang' do
694+
expect_no_offenses(<<~RUBY)
695+
hash.reject { |k, v| !(k != 0.0) }
696+
RUBY
697+
end
698+
605699
it 'does not register an offense when using `delete_if` and comparing with `lvar == :sym`' do
606700
expect_no_offenses(<<~RUBY)
607701
{foo: 1, bar: 2, baz: 3}.delete_if { |k, v| k == :bar }
@@ -619,6 +713,18 @@
619713
{foo: 1, bar: 2, baz: 3}.reject { |k, v| v.eql? :bar }
620714
RUBY
621715
end
716+
717+
it 'does not register an offense when using more than two block arguments' do
718+
expect_no_offenses(<<~RUBY)
719+
{foo: 1, bar: 2, baz: 3}.reject { |k, v, z| k == :bar }
720+
RUBY
721+
end
722+
723+
it 'does not register an offense when calling `include?` method without a param' do
724+
expect_no_offenses(<<~RUBY)
725+
{foo: 1, bar: 2, baz: 3}.reject { |k, v| %i[foo bar].include? }
726+
RUBY
727+
end
622728
end
623729

624730
it 'does not register an offense when using `reject` and comparing with `lvar != :key`' do

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy