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
- SPECIFY_MSG
Public Instance Methods
# File lib/rubocop/cop/rails/inverse_of.rb, line 176 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, message: message(options), location: :selector) end
# File lib/rubocop/cop/rails/inverse_of.rb, line 216 def options_contain_inverse_of?(options) options.any? { |opt| inverse_of_option?(opt) } end
# File lib/rubocop/cop/rails/inverse_of.rb, line 210 def options_ignoring_inverse_of?(options) options.any? do |opt| through_option?(opt) || polymorphic_option?(opt) end end
# File lib/rubocop/cop/rails/inverse_of.rb, line 199 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
# File lib/rubocop/cop/rails/inverse_of.rb, line 228 def same_context_in_with_options?(arg, recv) return true if arg.nil? && recv.nil? arg && recv && arg.children[0] == recv.children[0] end
# File lib/rubocop/cop/rails/inverse_of.rb, line 195 def scope?(arguments) arguments.any?(&:block_type?) end
# File lib/rubocop/cop/rails/inverse_of.rb, line 220 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
# File lib/rubocop/cop/rails/inverse_of.rb, line 236 def message(options) if options.any? { |opt| inverse_of_nil_option?(opt) } NIL_MSG else SPECIFY_MSG end end