lib/rubocop/cop/performance/compare_with_block.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# Identifies places where `sort { |a, b| a.foo <=> b.foo }`
# can be replaced by `sort_by(&:foo)`.
# This cop also checks `sort!`, `min`, `max` and `minmax` methods.
#
# @example
# # bad
# array.sort { |a, b| a.foo <=> b.foo }
# array.sort! { |a, b| a.foo <=> b.foo }
# array.max { |a, b| a.foo <=> b.foo }
# array.min { |a, b| a.foo <=> b.foo }
# array.minmax { |a, b| a.foo <=> b.foo }
# array.sort { |a, b| a[:foo] <=> b[:foo] }
#
# # good
# array.sort_by(&:foo)
# array.sort_by!(&:foo)
# array.sort_by { |v| v.foo }
# array.sort_by do |var|
# var.foo
# end
# array.max_by(&:foo)
# array.min_by(&:foo)
# array.minmax_by(&:foo)
# array.sort_by { |a| a[:foo] }
class CompareWithBlock < Base
include RangeHelp
extend AutoCorrector
MSG = 'Use `%<replacement_method>s%<instead>s` instead of ' \
'`%<compare_method>s { |%<var_a>s, %<var_b>s| %<str_a>s ' \
'<=> %<str_b>s }`.'
REPLACEMENT = { sort: :sort_by, sort!: :sort_by!, min: :min_by, max: :max_by, minmax: :minmax_by }.freeze
private_constant :REPLACEMENT
def_node_matcher :compare?, <<~PATTERN
(block
$(send _ {:sort :sort! :min :max :minmax})
(args (arg $_a) (arg $_b))
$send)
PATTERN
def_node_matcher :replaceable_body?, <<~PATTERN
(send
(send (lvar %1) $_method $...)
:<=>
(send (lvar %2) _method $...))
PATTERN
def on_block(node)
compare?(node) do |send, var_a, var_b, body|
replaceable_body?(body, var_a, var_b) do |method, args_a, args_b|
return unless slow_compare?(method, args_a, args_b)
range = compare_range(send, node)
add_offense(range, message: message(send, method, var_a, var_b, args_a)) do |corrector|
replacement = if method == :[]
"#{REPLACEMENT[send.method_name]} { |a| a[#{args_a.first.source}] }"
else
"#{REPLACEMENT[send.method_name]}(&:#{method})"
end
corrector.replace(range, replacement)
end
end
end
end
private
def slow_compare?(method, args_a, args_b)
return false unless args_a == args_b
if method == :[]
return false unless args_a.size == 1
key = args_a.first
return false unless %i[sym str int].include?(key.type)
else
return false unless args_a.empty?
end
true
end
# rubocop:disable Metrics/MethodLength
def message(send, method, var_a, var_b, args)
compare_method = send.method_name
replacement_method = REPLACEMENT[compare_method]
if method == :[]
key = args.first
instead = " { |a| a[#{key.source}] }"
str_a = "#{var_a}[#{key.source}]"
str_b = "#{var_b}[#{key.source}]"
else
instead = "(&:#{method})"
str_a = "#{var_a}.#{method}"
str_b = "#{var_b}.#{method}"
end
format(MSG, compare_method: compare_method,
replacement_method: replacement_method,
instead: instead,
var_a: var_a,
var_b: var_b,
str_a: str_a,
str_b: str_b)
end
# rubocop:enable Metrics/MethodLength
def compare_range(send, node)
range_between(send.loc.selector.begin_pos, node.loc.end.end_pos)
end
end
end
end
end