class RuboCop::Cop::Rails::InverseOf

This cop looks for has_(one|many) and belongs_to associations where Active Record can't automatically determine the inverse association because of a scope or the options used. Using the blog with order scope example below, traversing the a Blog's association in both directions with `blog.posts.first.blog` would cause the `blog` to be loaded from the database twice.

`:inverse_of` must be manually specified for Active Record to use the associated object in memory, or set to `false` to opt-out. Note that setting `nil` does not stop Active Record from trying to determine the inverse automatically, and is not considered a valid value for this.

@example

# good
class Blog < ApplicationRecord
  has_many :posts
end

class Post < ApplicationRecord
  belongs_to :blog
end

@example

# bad
class Blog < ApplicationRecord
  has_many :posts, -> { order(published_at: :desc) }
end

class Post < ApplicationRecord
  belongs_to :blog
end

# good
class Blog < ApplicationRecord
  has_many(:posts,
           -> { order(published_at: :desc) },
           inverse_of: :blog)
end

class Post < ApplicationRecord
  belongs_to :blog
end

# good
class Blog < ApplicationRecord
  with_options inverse_of: :blog do
    has_many :posts, -> { order(published_at: :desc) }
  end
end

class Post < ApplicationRecord
  belongs_to :blog
end

# good
# When you don't want to use the inverse association.
class Blog < ApplicationRecord
  has_many(:posts,
           -> { order(published_at: :desc) },
           inverse_of: false)
end

@example

# bad
class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end

class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

# good
class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

class Employee < ApplicationRecord
  has_many :pictures, as: :imageable, inverse_of: :imageable
end

class Product < ApplicationRecord
  has_many :pictures, as: :imageable, inverse_of: :imageable
end

@example

# bad
# However, RuboCop can not detect this pattern...
class Physician < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
end

class Appointment < ApplicationRecord
  belongs_to :physician
  belongs_to :patient
end

class Patient < ApplicationRecord
  has_many :appointments
  has_many :physicians, through: :appointments
end

# good
class Physician < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
end

class Appointment < ApplicationRecord
  belongs_to :physician, inverse_of: :appointments
  belongs_to :patient, inverse_of: :appointments
end

class Patient < ApplicationRecord
  has_many :appointments
  has_many :physicians, through: :appointments
end

@see guides.rubyonrails.org/association_basics.html#bi-directional-associations @see api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Setting+Inverses

Constants

NIL_MSG
RESTRICT_ON_SEND
SPECIFY_MSG

Public Instance Methods

on_send(node) click to toggle source
# File lib/rubocop/cop/rails/inverse_of.rb, line 172
def on_send(node)
  recv, arguments = association_recv_arguments(node)
  return unless arguments

  with_options = with_options_arguments(recv, node)

  options = arguments.concat(with_options).flat_map do |arg|
    options_from_argument(arg)
  end
  return if options_ignoring_inverse_of?(options)

  return unless scope?(arguments) ||
                options_requiring_inverse_of?(options)

  return if options_contain_inverse_of?(options)

  add_offense(node.loc.selector, message: message(options))
end
options_contain_inverse_of?(options) click to toggle source
# File lib/rubocop/cop/rails/inverse_of.rb, line 212
def options_contain_inverse_of?(options)
  options.any? { |opt| inverse_of_option?(opt) }
end
options_ignoring_inverse_of?(options) click to toggle source
# File lib/rubocop/cop/rails/inverse_of.rb, line 206
def options_ignoring_inverse_of?(options)
  options.any? do |opt|
    through_option?(opt) || polymorphic_option?(opt)
  end
end
options_requiring_inverse_of?(options) click to toggle source
# File lib/rubocop/cop/rails/inverse_of.rb, line 195
def options_requiring_inverse_of?(options)
  required = options.any? do |opt|
    conditions_option?(opt) ||
      foreign_key_option?(opt)
  end

  return required if target_rails_version >= 5.2

  required || options.any? { |opt| as_option?(opt) }
end
same_context_in_with_options?(arg, recv) click to toggle source
# File lib/rubocop/cop/rails/inverse_of.rb, line 224
def same_context_in_with_options?(arg, recv)
  return true if arg.nil? && recv.nil?

  arg && recv && arg.children[0] == recv.children[0]
end
scope?(arguments) click to toggle source
# File lib/rubocop/cop/rails/inverse_of.rb, line 191
def scope?(arguments)
  arguments.any?(&:block_type?)
end
with_options_arguments(recv, node) click to toggle source
# File lib/rubocop/cop/rails/inverse_of.rb, line 216
def with_options_arguments(recv, node)
  blocks = node.each_ancestor(:block).select do |block|
    block.send_node.command?(:with_options) &&
      same_context_in_with_options?(block.arguments.first, recv)
  end
  blocks.flat_map { |n| n.send_node.arguments }
end

Private Instance Methods

message(options) click to toggle source
# File lib/rubocop/cop/rails/inverse_of.rb, line 232
def message(options)
  if options.any? { |opt| inverse_of_nil_option?(opt) }
    NIL_MSG
  else
    SPECIFY_MSG
  end
end