# frozen_string_literal: truemoduleRuboCopmoduleCopmoduleStyle# This cop is used to identify instances of sorting and then# taking only the first or last element. The same behavior can# be accomplished without a relatively expensive sort by using# `Enumerable#min` instead of sorting and taking the first# element and `Enumerable#max` instead of sorting and taking the# last element. Similarly, `Enumerable#min_by` and# `Enumerable#max_by` can replace `Enumerable#sort_by` calls# after which only the first or last element is used.## @example# # bad# [2, 1, 3].sort.first# [2, 1, 3].sort[0]# [2, 1, 3].sort.at(0)# [2, 1, 3].sort.slice(0)## # good# [2, 1, 3].min## # bad# [2, 1, 3].sort.last# [2, 1, 3].sort[-1]# [2, 1, 3].sort.at(-1)# [2, 1, 3].sort.slice(-1)## # good# [2, 1, 3].max## # bad# arr.sort_by(&:foo).first# arr.sort_by(&:foo)[0]# arr.sort_by(&:foo).at(0)# arr.sort_by(&:foo).slice(0)## # good# arr.min_by(&:foo)## # bad# arr.sort_by(&:foo).last# arr.sort_by(&:foo)[-1]# arr.sort_by(&:foo).at(-1)# arr.sort_by(&:foo).slice(-1)## # good# arr.max_by(&:foo)#classRedundantSort<CopincludeRangeHelpMSG='Use `%<suggestion>s` instead of '\'`%<sorter>s...%<accessor_source>s`.'def_node_matcher:redundant_sort?,<<~MATCHER
{
(send $(send _ $:sort ...) ${:last :first})
(send $(send _ $:sort ...) ${:[] :at :slice} {(int 0) (int -1)})
(send $(send _ $:sort_by _) ${:last :first})
(send $(send _ $:sort_by _) ${:[] :at :slice} {(int 0) (int -1)})
(send (block $(send _ ${:sort_by :sort}) ...) ${:last :first})
(send
(block $(send _ ${:sort_by :sort}) ...)
${:[] :at :slice} {(int 0) (int -1)}
)
}
MATCHERdefon_send(node)redundant_sort?(node)do|sort_node,sorter,accessor|range=range_between(sort_node.loc.selector.begin_pos,node.loc.expression.end_pos)add_offense(node,location: range,message: message(node,sorter,accessor))endenddefautocorrect(node)sort_node,sorter,accessor=redundant_sort?(node)lambdado|corrector|# Remove accessor, e.g. `first` or `[-1]`.corrector.remove(range_between(accessor_start(node),node.loc.expression.end_pos))# Replace "sort" or "sort_by" with the appropriate min/max method.corrector.replace(sort_node.loc.selector,suggestion(sorter,accessor,arg_value(node)))endendprivatedefmessage(node,sorter,accessor)accessor_source=range_between(node.loc.selector.begin_pos,node.loc.expression.end_pos).sourceformat(MSG,suggestion: suggestion(sorter,accessor,arg_value(node)),sorter: sorter,accessor_source: accessor_source)enddefsuggestion(sorter,accessor,arg)base(accessor,arg)+suffix(sorter)enddefbase(accessor,arg)ifaccessor==:first||(arg&.zero?)'min'elsifaccessor==:last||arg==-1'max'endenddefsuffix(sorter)ifsorter==:sort''elsifsorter==:sort_by'_by'endenddefarg_node(node)node.arguments.firstenddefarg_value(node)arg_node(node).nil??nil:arg_node(node).node_parts.firstend# This gets the start of the accessor whether it has a dot# (e.g. `.first`) or doesn't (e.g. `[0]`)defaccessor_start(node)ifnode.loc.dotnode.loc.dot.begin_poselsenode.loc.selector.begin_posendendendendendend