@@ -12,6 +12,14 @@ module Performance
12
12
# You can set the minimum number of elements to consider
13
13
# an offense with `MinSize`.
14
14
#
15
+ # NOTE: Since Ruby 3.4, certain simple arguments to `Array#include?` are
16
+ # optimized directly in Ruby. This avoids allocations without changing the
17
+ # code, as such no offense will be registered in those cases. Currently that
18
+ # includes: strings, `self`, local variables, instance variables, and method
19
+ # calls without arguments. Additionally, any number of methods can be chained:
20
+ # `[1, 2, 3].include?(@foo)` and `[1, 2, 3].include?(@foo.bar.baz)` both avoid
21
+ # the array allocation.
22
+ #
15
23
# @example
16
24
# # bad
17
25
# users.select do |user|
@@ -55,6 +63,8 @@ class CollectionLiteralInLoop < Base
55
63
56
64
ARRAY_METHODS = ( ENUMERABLE_METHOD_NAMES | NONMUTATING_ARRAY_METHODS ) . to_set . freeze
57
65
66
+ ARRAY_INCLUDE_OPTIMIZED_TYPES = %i[ str self lvar ivar send ] . freeze
67
+
58
68
NONMUTATING_HASH_METHODS = %i[ < <= == > >= [] any? assoc compact dig
59
69
each each_key each_pair each_value empty?
60
70
eql? fetch fetch_values filter flatten has_key?
@@ -80,21 +90,42 @@ class CollectionLiteralInLoop < Base
80
90
PATTERN
81
91
82
92
def on_send ( node )
83
- receiver , method , = *node . children
84
- return unless check_literal? ( receiver , method ) && parent_is_loop? ( receiver )
93
+ receiver , method , * arguments = *node . children
94
+ return unless check_literal? ( receiver , method , arguments ) && parent_is_loop? ( receiver )
85
95
86
96
message = format ( MSG , literal_class : literal_class ( receiver ) )
87
97
add_offense ( receiver , message : message )
88
98
end
89
99
90
100
private
91
101
92
- def check_literal? ( node , method )
102
+ def check_literal? ( node , method , arguments )
93
103
!node . nil? &&
94
104
nonmutable_method_of_array_or_hash? ( node , method ) &&
95
105
node . children . size >= min_size &&
96
- node . recursive_basic_literal?
106
+ node . recursive_basic_literal? &&
107
+ !optimized_array_include? ( node , method , arguments )
108
+ end
109
+
110
+ # Since Ruby 3.4, simple arguments to Array#include? are optimized.
111
+ # See https://github.com/ruby/ruby/pull/12123 for more details.
112
+ # rubocop:disable Metrics/CyclomaticComplexity
113
+ def optimized_array_include? ( node , method , arguments )
114
+ return false unless target_ruby_version >= 3.4 && node . array_type? && method == :include?
115
+ # Disallow include?(1, 2)
116
+ return false if arguments . count != 1
117
+
118
+ arg = arguments . first
119
+ # Allow `include?(foo.bar.baz.bat)`
120
+ while arg . send_type?
121
+ return false if arg . arguments . any? # Disallow include?(foo(bar))
122
+ break unless arg . receiver
123
+
124
+ arg = arg . receiver
125
+ end
126
+ ARRAY_INCLUDE_OPTIMIZED_TYPES . include? ( arg . type )
97
127
end
128
+ # rubocop:enable Metrics/CyclomaticComplexity
98
129
99
130
def nonmutable_method_of_array_or_hash? ( node , method )
100
131
( node . array_type? && ARRAY_METHODS . include? ( method ) ) ||
0 commit comments