Class: Scryer::PerformanceRules::NPlusOneQueryRule
- Defined in:
- lib/scryer/performance_rules/n_plus_one_query_rule.rb
Overview
Flags likely N+1 queries: inside a .each/.map block whose receiver
looks like an Active Record collection (a local variable previously
assigned from Model.where(...)/Model.all/Model.find(...), or a bare
instance variable — the common @orders.each do |order| ... end shape),
a bare no-arg .method_name call directly on the block variable (likely
an association access, e.g. order.line_items) is flagged unless an
.includes(:that_name) appeared earlier in the same base query chain.
This is a heuristic, not type inference: it can't tell an association
read (order.line_items, which issues a query per row) from a plain
attribute/column read (order.status) or a harmless Ruby method call —
both parse identically as a bare no-arg call on the block variable. A
small block-list of universally-common non-association methods
(to_s, present?, class, ...) cuts down the noisiest false positives, but
real false positives on attribute reads are still expected and normal
for this class of tool (see README).
Constant Summary collapse
- QUERY_METHODS =
%w[where all find find_by find_by! order limit].freeze
- LOOP_METHODS =
%w[each map collect each_with_index].freeze
- NON_ASSOCIATION_METHODS =
Bare methods that are overwhelmingly plain Ruby/attribute reads rather than association traversal — flagging these would be mostly noise.
%w[ to_s to_i to_a to_h inspect class dup clone freeze frozen? hash present? blank? nil? empty? any? id id_value == equal? try tap then is_a? kind_of? instance_of? respond_to? send public_send object_id itself ].freeze
Instance Attribute Summary
Attributes inherited from Rule
Instance Method Summary collapse
Methods inherited from Rule
Constructor Details
This class inherits a constructor from Scryer::Rule
Instance Method Details
#scan ⇒ Object
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/scryer/performance_rules/n_plus_one_query_rule.rb', line 37 def scan findings = [] Ast.each_node(sexp) do |node| next unless Ast.tagged?(node, :def, :defs) body = node.last bindings = query_bindings(body) Ast.each_node(body) do |block_call| next unless Ast.tagged?(block_call, :method_add_block) call_node = block_call[1] receiver, loop_method = Ast.call_name(call_node) || [nil, nil] next unless LOOP_METHODS.include?(loop_method) next unless receiver base_name = var_or_ivar_name(receiver) next unless base_name includes = if bindings.key?(base_name) bindings[base_name] elsif base_name.start_with?("@") [] # ivar set elsewhere — treat as an AR collection with no known eager-loading end next unless includes # local var we never saw assigned from a query — don't guess block_node = block_call[2] param_name = block_param_name(block_node) next unless param_name findings.concat(association_calls(block_node[2], param_name, includes)) end end findings end |