class RuboCop::Cop::Style::RedundantSort

Identifies 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.

@safety

This cop is unsafe, because `sort...last` and `max` may not return the
same element in all cases.

In an enumerable where there are multiple elements where ``a <=> b == 0``,
or where the transformation done by the `sort_by` block has the
same result, `sort.last` and `max` (or `sort_by.last` and `max_by`)
will return different elements. `sort.last` will return the last
element but `max` will return the first element.

For example:

[source,ruby]
----
  class MyString < String; end
  strings = [MyString.new('test'), 'test']
  strings.sort.last.class   #=> String
  strings.max.class         #=> MyString
----

[source,ruby]
----
  words = %w(dog horse mouse)
  words.sort_by { |word| word.length }.last   #=> 'mouse'
  words.max_by { |word| word.length }         #=> 'horse'
----

@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)

Constants

MSG
RESTRICT_ON_SEND

Public Instance Methods

on_csend(node)
Alias for: on_send
on_send(node) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 104
def on_send(node)
  ancestor, sort_node, sorter, accessor =
    find_redundant_sort(node.parent, node.parent&.parent)
  return unless ancestor

  register_offense(ancestor, sort_node, sorter, accessor)
end
Also aliased as: on_csend

Private Instance Methods

accessor_start(node) click to toggle source

This gets the start of the accessor whether it has a dot (e.g. ‘.first`) or doesn’t (e.g. ‘[0]`)

# File lib/rubocop/cop/style/redundant_sort.rb, line 193
def accessor_start(node)
  if node.loc.dot
    node.loc.dot.begin_pos
  else
    node.loc.selector.begin_pos
  end
end
arg_node(node) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 183
def arg_node(node)
  node.first_argument
end
arg_value(node) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 187
def arg_value(node)
  arg_node(node)&.node_parts&.first
end
autocorrect(corrector, node, sort_node, sorter, accessor) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 148
def autocorrect(corrector, node, sort_node, sorter, accessor)
  # Remove accessor, e.g. `first` or `[-1]`.
  corrector.remove(range_between(accessor_start(node), node.source_range.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)))
  # Replace to avoid syntax errors when followed by a logical operator.
  replace_with_logical_operator(corrector, node) if with_logical_operator?(node)
end
base(accessor, arg) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 166
def base(accessor, arg)
  if accessor == :first || arg&.zero?
    'min'
  elsif accessor == :last || arg == -1
    'max'
  end
end
find_redundant_sort(*nodes) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 115
def find_redundant_sort(*nodes)
  nodes.each do |node|
    if (sort_node, sorter, accessor = redundant_sort?(node))
      return [node, sort_node, sorter, accessor]
    end
  end

  nil
end
message(node, sorter, accessor) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 136
def message(node, sorter, accessor)
  accessor_source = range_between(
    node.loc.selector.begin_pos,
    node.source_range.end_pos
  ).source

  format(MSG,
         suggestion: suggestion(sorter, accessor, arg_value(node)),
         sorter: sorter,
         accessor_source: accessor_source)
end
offense_range(sort_node, node) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 132
def offense_range(sort_node, node)
  range_between(sort_node.loc.selector.begin_pos, node.source_range.end_pos)
end
register_offense(node, sort_node, sorter, accessor) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 125
def register_offense(node, sort_node, sorter, accessor)
  message = message(node, sorter, accessor)
  add_offense(offense_range(sort_node, node), message: message) do |corrector|
    autocorrect(corrector, node, sort_node, sorter, accessor)
  end
end
replace_with_logical_operator(corrector, node) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 157
def replace_with_logical_operator(corrector, node)
  corrector.insert_after(node.child_nodes.first, " #{node.parent.loc.operator.source}")
  corrector.remove(node.parent.loc.operator)
end
suffix(sorter) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 174
def suffix(sorter)
  case sorter
  when :sort
    ''
  when :sort_by
    '_by'
  end
end
suggestion(sorter, accessor, arg) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 162
def suggestion(sorter, accessor, arg)
  base(accessor, arg) + suffix(sorter)
end
with_logical_operator?(node) click to toggle source
# File lib/rubocop/cop/style/redundant_sort.rb, line 201
def with_logical_operator?(node)
  return false unless (parent = node.parent)

  parent.or_type? || parent.and_type?
end