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` proves the element is a Hash (AR’s ‘[]` is rarely called with literal symbol keys).
-
**Single-char string indexing on the block arg** — ‘c == ’+‘` 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 |