Class: RuboCop::Cop::DevDoc::Rails::NoBlockPredicateOnRelation
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Rails::NoBlockPredicateOnRelation
- Defined in:
- lib/rubocop/cop/dev_doc/rails/no_block_predicate_on_relation.rb
Overview
Avoid block-form predicates on ActiveRecord relations.
Rationale
.count { }, .reject { }, .select { }, .any? { }, and
.find { } accept a block, which silently converts the relation to
an Array — every row is loaded into Ruby memory and filtered there,
defeating database indexes and pagination.
Push the predicate into SQL with .where(...) or a model scope so
the database does the filtering.
❌ Loads every pending record into memory before counting
pending_subscriptions.count { |s| !s.expired_for_context? }
✔️ Becomes a SQL COUNT via a scope
class Subscription < ApplicationRecord
scope :not_expired_for_context, ->(context) { ... }
end
pending_subscriptions.not_expired_for_context(context).count
Exception
Some predicates genuinely can't be expressed in SQL (decrypted
attributes, non-trivial Ruby logic). For those, add a
# rubocop:disable with a brief reason.
Excluded receivers
To keep false positives low, the cop skips receivers that clearly
aren't AR relations: array literals ([...]), hash literals
({...}), screaming-case constants (e.g. PRICING_PLANS), and
send-chains ending in a method known to return a non-Relation:
- Array-returning —
pluck,to_a,map,flatten,compact,uniq,sort,sort_by,reduce,inject,each_with_object,zip,take,drop,group_by,partition,tally,chunk_while,slice_when, etc. - Hash-returning —
slice,except,merge,transform_values,transform_keys,to_h,compact_blank,with_indifferent_access. - Enumerator-returning —
each_with_index,each_slice,each_cons,lazy,with_index,with_object. Calling these on a Relation forces eager loading; the next.select/etc. then operates on an in-memory Enumerator, so pushing into SQL is no longer possible.
Parenthesised receivers ((expr).select { ... }) are looked through
to expr so the rules above still apply.
Excluded block shapes
Even when the receiver is opaque (a local/instance variable, method parameter, etc.), the block itself sometimes proves the element can't be an AR record:
- 2+ block arguments —
|k, v|,|item, _index|, etc. AR relations only yield single records; multi-arg destructuring means the iterator is Hash#each, zip, or similar. - Symbol-key indexing on the block arg —
arg[:foo]proves the element is a Hash (AR's[]is rarely called with literal symbol keys). - Single-char string indexing on the block arg —
c[0] == '+'proves the element is a String.
Configuration
AdditionalNonRelationMethods (default []): per-project list of
method names that return non-Relation collections. Useful when a
codebase has its own helper methods returning plain Arrays / Hashes
(e.g. a presenter factory) — adding them here lets the cop skip
send-chains ending in those methods. Example:
DevDoc/Rails/NoBlockPredicateOnRelation:
AdditionalNonRelationMethods:
- all_items
- for_account
- gst_registration_ranges
NOTE: The cop cannot determine whether a local variable, instance
variable, or method parameter is an AR relation or a plain
collection. When the receiver is in fact a plain Array/Hash, add
# rubocop:disable DevDoc/Rails/NoBlockPredicateOnRelation with a
brief reason — the friction is intentional and ensures the choice
is reviewed.
Constant Summary collapse
- MSG =
'`%<method>s` with a block loads every row into Ruby. ' \ 'Push the predicate into SQL with `.where(...)` or a model scope.'.freeze
- RESTRICT_ON_SEND =
%i[count reject select find any?].freeze
- NON_RELATION_RETURNING_METHODS =
Methods whose return value is known to be a non-Relation collection (Array, Hash, or Enumerator). When a
.select/.reject/etc. with a block is chained onto a call to one of these, the block runs over the materialised collection — pushing into SQL isn't possible. %i[ pluck pluck_to_hash to_a to_ary values keys map flat_map collect collect_concat filter_map flatten compact uniq sort sort_by reverse reduce inject each_with_object split lines chars bytes zip take drop drop_while take_while group_by partition tally tally_by chunk_while slice_when slice except merge transform_values transform_keys to_h compact_blank with_indifferent_access index_by index_with each_with_index each_slice each_cons each_entry each_key each_value each_pair chunk slice_before slice_after lazy with_index with_object ].freeze
Instance Method Summary collapse
Instance Method Details
#on_send(node) ⇒ Object
131 132 133 134 135 136 137 138 |
# File 'lib/rubocop/cop/dev_doc/rails/no_block_predicate_on_relation.rb', line 131 def on_send(node) return unless node.block_literal? return if node.receiver.nil? return if excluded_receiver?(node.receiver) return if excluded_block?(node) add_offense(node.loc.selector, message: format(MSG, method: node.method_name)) end |