123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Style::PartitionInsteadOfDoubleSelect

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, ::RuboCop::Cop::AutoCorrector, ::RuboCop::Cop::Base, ::RuboCop::ExcludeLimit, NodePattern::Macros, RuboCop::AST::Sexp
Instance Chain:
Inherits: RuboCop::Cop::Base
Defined in: lib/rubocop/cop/style/partition_instead_of_double_select.rb

Overview

Checks for consecutive calls to select/filter/find_all and reject on the same receiver with the same block body, where partition could be used instead. Also detects two select or two reject calls where one block negates the other with !. Using partition reduces two collection traversals to one.

Examples:

# bad
positives = array.select { |x| x > 0 }
negatives = array.reject { |x| x > 0 }

# bad
positives = array.filter { |x| x > 0 }
negatives = array.reject { |x| x > 0 }

# bad
negatives = array.reject { |x| x > 0 }
positives = array.select { |x| x > 0 }

# bad
positives = array.select(&:positive?)
negatives = array.reject(&:positive?)

# bad
positives = array.select(&:positive?)
negatives = array.reject { |x| x.positive? }

# bad
positives = array.select { |x| x.positive? }
non_positives = array.select { |x| !x.positive? }

# good
positives, negatives = array.partition { |x| x > 0 }

# good
positives, non_positives = array.partition { |x| x.positive? }

# good
positives, negatives = array.partition(&:positive?)

Cop Safety Information:

  • This cop is unsafe because:

    • Hash#select and Hash#reject return hashes, but Hash#partition returns nested arrays.

    • When the receiver has side effects, calling it once (with partition) versus twice (with select + reject) may produce different results.

    • Custom classes may override select/reject without providing a compatible partition method.

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

::RuboCop::Cop::RangeHelp - Included

BYTE_ORDER_MARK, NOT_GIVEN

Class Attribute Summary

::RuboCop::Cop::AutoCorrector - Extended

::RuboCop::Cop::Base - Inherited

.gem_requirements, .lint?,
.support_autocorrect?

Returns if class supports autocorrect.

.support_multiple_source?

Override if your cop should be called repeatedly for multiple investigations Between calls to on_new_investigation and on_investigation_end, the result of processed_source will remain constant.

Class Method Summary

::RuboCop::Cop::Base - Inherited

.autocorrect_incompatible_with

List of cops that should not try to autocorrect at the same time as this cop.

.badge

Naming.

.callbacks_needed, .cop_name, .department,
.documentation_url

Returns a url to view this cops documentation online.

.exclude_from_registry

Call for abstract Cop classes.

.inherited,
.joining_forces

Override and return the Force class(es) you need to join.

.match?

Returns true if the cop name or the cop namespace matches any of the given names.

.new,
.requires_gem

Register a version requirement for the given gem name.

.restrict_on_send

::RuboCop::ExcludeLimit - Extended

exclude_limit

Sets up a configuration option to have an exclude limit tracked.

transform

Instance Attribute Summary

Instance Method Summary

::RuboCop::Cop::RangeHelp - Included

#add_range,
#arguments_range

A range containing the first to the last argument of a method call or method definition.

#column_offset_between,
#contents_range

A range containing only the contents of a literal with delimiters (e.g.

#directions,
#effective_column

Returns the column attribute of the range, except if the range is on the first line and there’s a byte order mark at the beginning of that line, in which case 1 is subtracted from the column value.

#final_pos, #move_pos, #move_pos_str, #range_between, #range_by_whole_lines, #range_with_comments, #range_with_comments_and_lines, #range_with_surrounding_comma, #range_with_surrounding_space, #source_range

::RuboCop::Cop::Base - Inherited

#add_global_offense

Adds an offense that has no particular location.

#add_offense

Adds an offense on the specified range (or node with an expression) Unless that offense is disabled for this range, a corrector will be yielded to provide the cop the opportunity to autocorrect the offense.

#begin_investigation

Called before any investigation.

#callbacks_needed,
#cop_config

Configuration Helpers.

#cop_name, #excluded_file?,
#external_dependency_checksum

This method should be overridden when a cop’s behavior depends on state that lives outside of these locations:

#inspect,
#message

Gets called if no message is specified when calling add_offense or add_global_offense Cops are discouraged to override this; instead pass your message directly.

#name

Alias for Base#cop_name.

#offenses,
#on_investigation_end

Called after all on_…​

#on_new_investigation

Called before all on_…​

#on_other_file

Called instead of all on_…​

#parse

There should be very limited reasons for a Cop to do it’s own parsing.

#parser_engine,
#ready

Called between investigations.

#relevant_file?,
#target_gem_version

Returns a gems locked versions (i.e.

#target_rails_version, #target_ruby_version, #annotate, #apply_correction, #attempt_correction,
#callback_argument

Reserved for Cop::Cop.

#complete_investigation

Called to complete an investigation.

#correct, #current_corrector,
#current_offense_locations

Reserved for Commissioner:

#current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?, #file_name_matches_any?, #find_message, #find_severity, #range_for_original, #range_from_node_or_range,
#reset_investigation

Actually private methods.

#use_corrector

::RuboCop::Cop::AutocorrectLogic - Included

::RuboCop::Cop::IgnoredNode - Included

Constructor Details

This class inherits a constructor from RuboCop::Cop::Base

Instance Method Details

#autocorrect(corrector, node, sibling, container, sibling_container) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 209

def autocorrect(corrector, node, sibling, container, sibling_container)
  if complementary_pair?(node, sibling)
    select_var, reject_var =
      complementary_variable_order(sibling, container, sibling_container)
    partition_node = select_node_for(sibling, container)
  else
    select_var, reject_var, partition_node =
      negation_partition_args(node, sibling, container, sibling_container)
  end

  partition_call = build_partition_call(partition_node)
  replacement = "#{select_var}, #{reject_var} = #{partition_call}"

  corrector.replace(sibling_container, replacement)
  range = range_by_whole_lines(container.source_range, include_final_newline: true)
  corrector.remove(range)
end

#block_matches_block_pass?(block_node, send_node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 175

def block_matches_block_pass?(block_node, send_node)
  method_name = symbol_proc_method?(block_node)
  return false unless method_name

  sym_node = send_node.last_argument.children.first
  sym_node.sym_type? && sym_node.children.first == method_name
end

#both_lvasgn?(container, sibling_container) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 205

def both_lvasgn?(container, sibling_container)
  container.lvasgn_type? && sibling_container.lvasgn_type?
end

#build_partition_call(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 257

def build_partition_call(node)
  source = node.source
  send_node = node.any_block_type? ? node.send_node : node
  selector = send_node.loc.selector
  offset = node.source_range.begin_pos
  method_start = selector.begin_pos - offset
  method_end = selector.end_pos - offset

  "#{source[0...method_start]}partition#{source[method_end..]}"
end

#complementary_pair?(node1, node2) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 145

def complementary_pair?(node1, node2)
  m1 = node1.method_name
  m2 = node2.method_name
  (SELECT_METHODS.include?(m1) && m2 == :reject) ||
    (m1 == :reject && SELECT_METHODS.include?(m2))
end

#complementary_variable_order(sibling, container, sibling_container) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 227

def complementary_variable_order(sibling, container, sibling_container)
  if SELECT_METHODS.include?(sibling.method_name)
    [sibling_container.children.first, container.children.first]
  else
    [container.children.first, sibling_container.children.first]
  end
end

#equivalent_predicate?(node1, node2) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 152

def equivalent_predicate?(node1, node2)
  if node1.any_block_type? && node2.any_block_type?
    same_block_contents?(node1, node2)
  elsif node1.any_block_type?
    block_matches_block_pass?(node1, node2)
  elsif node2.any_block_type?
    block_matches_block_pass?(node2, node1)
  else
    node1.last_argument == node2.last_argument
  end
end

#extract_block(container) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 128

def extract_block(container)
  if container.any_block_type?
    container
  elsif container.assignment?
    rhs = container.children.last
    rhs if rhs&.any_block_type?
  end
end

#extract_block_pass_send(container) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 137

def extract_block_pass_send(container)
  node = container.assignment? ? container.children.last : container
  return unless node&.type?(:call)
  return unless node.last_argument&.block_pass_type?

  node
end

#extract_candidate(container) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 124

def extract_candidate(container)
  extract_block(container) || extract_block_pass_send(container)
end

#find_and_register_offense(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 88

def find_and_register_offense(node)
  container = node_container(node)
  return unless container

  sibling_container = container.left_sibling
  sibling = find_matching_candidate(node, sibling_container)
  return unless sibling

  register_offense(node, sibling, container, sibling_container)
end

#find_matching_candidate(node, sibling_container) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 108

def find_matching_candidate(node, sibling_container)
  return unless sibling_container

  sibling = extract_candidate(sibling_container)
  return unless sibling
  return unless node.receiver == sibling.receiver
  return unless matching_pair?(node, sibling)

  sibling
end

#matching_pair?(node, sibling) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 119

def matching_pair?(node, sibling)
  (complementary_pair?(node, sibling) && equivalent_predicate?(node, sibling)) ||
    (node.method?(sibling.method_name) && negated_predicate?(node, sibling))
end

#negated_body?(body1, body2) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 191

def negated_body?(body1, body2)
  body1&.send_type? && body1.method?(:!) && body1.receiver == body2
end

#negated_predicate?(node1, node2) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 183

def negated_predicate?(node1, node2)
  return false unless node1.any_block_type? && node2.any_block_type?
  return false unless node1.type == node2.type
  return false if node1.block_type? && node1.arguments != node2.arguments

  negated_body?(node1.body, node2.body) || negated_body?(node2.body, node1.body)
end

#negation_partition_args(node, sibling, container, sibling_container) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 235

def negation_partition_args(node, sibling, container, sibling_container)
  node_is_negated = negated_body?(node.body, sibling.body)
  is_select = SELECT_METHODS.include?(node.method_name)
  # For select: non-negated is truthy (first). For reject: negated is truthy (first).
  node_is_truthy = is_select != node_is_negated
  partition_node = node_is_negated ? sibling : node

  if node_is_truthy
    [container.children.first, sibling_container.children.first, partition_node]
  else
    [sibling_container.children.first, container.children.first, partition_node]
  end
end

#node_container(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 99

def node_container(node)
  parent = node.parent
  if parent&.begin_type?
    node
  elsif parent&.assignment? && parent.parent&.begin_type?
    parent
  end
end

#on_block(node) Also known as: #on_numblock, #on_itblock

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 71

def on_block(node)
  return unless CANDIDATE_METHODS.include?(node.method_name)

  find_and_register_offense(node)
end

#on_csend(node)

Alias for #on_send.

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 84

alias on_csend on_send

#on_itblock(node)

Alias for #on_block.

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 77

alias on_itblock on_block

#on_numblock(node)

Alias for #on_block.

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 76

alias on_numblock on_block

#on_send(node) Also known as: #on_csend

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 79

def on_send(node)
  return unless node.last_argument&.block_pass_type?

  find_and_register_offense(node)
end

#register_offense(node, sibling, container, sibling_container) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 195

def register_offense(node, sibling, container, sibling_container)
  message = format(MSG, first: sibling.method_name, second: node.method_name)

  add_offense(container, message: message) do |corrector|
    next unless both_lvasgn?(container, sibling_container)

    autocorrect(corrector, node, sibling, container, sibling_container)
  end
end

#same_block_contents?(block1, block2) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 164

def same_block_contents?(block1, block2)
  return false unless block1.type == block2.type

  if block1.block_type?
    block1.arguments == block2.arguments &&
      block1.body == block2.body
  else
    block1.body == block2.body
  end
end

#select_node_for(sibling, container) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 249

def select_node_for(sibling, container)
  if SELECT_METHODS.include?(sibling.method_name)
    sibling
  else
    container.children.last
  end
end

#symbol_proc_method?(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/partition_instead_of_double_select.rb', line 67

def_node_matcher :symbol_proc_method?, <<~PATTERN
  (block _ (args (arg _name)) (send (lvar _name) $_method_name))
PATTERN