lib/rubocop/cop/rails/uniq_before_pluck.rb
# frozen_string_literal: true module RuboCop module Cop module Rails # Prefer using `distinct` before `pluck` instead of `uniq` after `pluck`. # # The use of distinct before pluck is preferred because it executes by # the database. # # This cop has two different enforcement modes. When the EnforcedStyle # is `conservative` (the default), then only calls to `pluck` on a constant # (i.e. a model class) before `uniq` are added as offenses. # # When the EnforcedStyle is `aggressive` then all calls to `pluck` before # distinct are added as offenses. This may lead to false positives # as the cop cannot distinguish between calls to `pluck` on an # ActiveRecord::Relation vs a call to pluck on an # ActiveRecord::Associations::CollectionProxy. # # @safety # This cop is unsafe for autocorrection because the behavior may change # depending on the database collation. # # @example EnforcedStyle: conservative (default) # # bad - redundantly fetches duplicate values # Album.pluck(:band_name).uniq # # # good # Album.distinct.pluck(:band_name) # # @example EnforcedStyle: aggressive # # bad - redundantly fetches duplicate values # Album.pluck(:band_name).uniq # # # bad - redundantly fetches duplicate values # Album.where(year: 1985).pluck(:band_name).uniq # # # bad - redundantly fetches duplicate values # customer.favourites.pluck(:color).uniq # # # good # Album.distinct.pluck(:band_name) # Album.distinct.where(year: 1985).pluck(:band_name) # customer.favourites.distinct.pluck(:color) # class UniqBeforePluck < Base include ConfigurableEnforcedStyle include RangeHelp extend AutoCorrector MSG = 'Use `distinct` before `pluck`.' RESTRICT_ON_SEND = %i[uniq].freeze def_node_matcher :uniq_before_pluck, '[!^any_block $(send $(send _ :pluck ...) :uniq ...)]' def on_send(node) uniq_before_pluck(node) do |uniq_node, pluck_node| next if style == :conservative && !pluck_node.receiver&.const_type? add_offense(uniq_node.loc.selector) do |corrector| autocorrect(corrector, uniq_node, pluck_node) end end end private def autocorrect(corrector, uniq_node, pluck_node) corrector.remove(range_between(pluck_node.loc.end.end_pos, uniq_node.loc.selector.end_pos)) if (dot = pluck_node.loc.dot) corrector.insert_before(dot.begin, '.distinct') else corrector.insert_before(pluck_node, 'distinct.') end end end end end end