@@ -12,6 +12,14 @@ module Performance
1212 # You can set the minimum number of elements to consider
1313 # an offense with `MinSize`.
1414 #
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+ #
1523 # @example
1624 # # bad
1725 # users.select do |user|
@@ -55,6 +63,8 @@ class CollectionLiteralInLoop < Base
5563
5664 ARRAY_METHODS = ( ENUMERABLE_METHOD_NAMES | NONMUTATING_ARRAY_METHODS ) . to_set . freeze
5765
66+ ARRAY_INCLUDE_OPTIMIZED_TYPES = %i[ str self lvar ivar send ] . freeze
67+
5868 NONMUTATING_HASH_METHODS = %i[ < <= == > >= [] any? assoc compact dig
5969 each each_key each_pair each_value empty?
6070 eql? fetch fetch_values filter flatten has_key?
@@ -80,21 +90,42 @@ class CollectionLiteralInLoop < Base
8090 PATTERN
8191
8292 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 )
8595
8696 message = format ( MSG , literal_class : literal_class ( receiver ) )
8797 add_offense ( receiver , message : message )
8898 end
8999
90100 private
91101
92- def check_literal? ( node , method )
102+ def check_literal? ( node , method , arguments )
93103 !node . nil? &&
94104 nonmutable_method_of_array_or_hash? ( node , method ) &&
95105 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 )
97127 end
128+ # rubocop:enable Metrics/CyclomaticComplexity
98129
99130 def nonmutable_method_of_array_or_hash? ( node , method )
100131 ( node . array_type? && ARRAY_METHODS . include? ( method ) ) ||
0 commit comments