class RuboCop::Cop::Rails::RedundantPresenceValidationOnBelongsTo
belongs_to :author, foreign_key: :user_id
# good
belongs_to :user
# good
validates :user_id, presence: true
belongs_to :author, foreign_key: :user_id
# bad
validates :user_id, presence: true
belongs_to :user
# bad
validates :user, presence: true
belongs_to :user
# bad
@example
from “can’t be blank” to “must exist”.
This cop’s autocorrection is unsafe because it changes the default error message
@safety
automatically, and explicit presence validation is redundant.
explicitly set to ‘false`. The presence validator is added
unless `config.active_record.belongs_to_required_by_default` is
Since Rails 5.0 the default for `belongs_to` is `optional: false`
def add_offense_and_correct(node, all_keys, keys, options, presence)
def add_offense_and_correct(node, all_keys, keys, options, presence) add_offense(presence, message: message_for(keys)) do |corrector| if options.children.one? # `presence: true` is the only option if keys == all_keys remove_validation(corrector, node) else remove_keys_from_validation(corrector, node, keys) end elsif keys == all_keys remove_presence_option(corrector, presence) else extract_validation_for_keys(corrector, node, keys, options) end end end
def belongs_to_for(model_class_node, key)
def belongs_to_for(model_class_node, key) if key.to_s.end_with?('_id') normalized_key = key.to_s.delete_suffix('_id').to_sym belongs_to?(model_class_node, key: normalized_key, fk: key) else any_belongs_to?(model_class_node, association: key) end end
def extract_validation_for_keys(corrector, node, keys, options)
def extract_validation_for_keys(corrector, node, keys, options) indentation = ' ' * node.source_range.column options_without_presence = options.children.reject { |pair| pair.key.value == :presence } source = [ indentation, 'validates ', keys.map(&:inspect).join(', '), ', ', options_without_presence.map(&:source).join(', '), "\n" ].join remove_keys_from_validation(corrector, node, keys) corrector.insert_after(validation_range(node), source) end
def message_for(keys)
def message_for(keys) display_keys = keys.map { |key| "`#{key}`" }.join('/') format(MSG, association: display_keys) end
def non_optional_belongs_to(node, keys)
def non_optional_belongs_to(node, keys) keys.select do |key| belongs_to = belongs_to_for(node, key) belongs_to && !optional?(belongs_to) end end
def on_send(node)
def on_send(node) presence_validation?(node) do |all_keys, options, presence| # If presence is the only validation option and other non-validation options # are present, removing it will cause rails to error. used_option_keys = options.keys.select(&:sym_type?).map(&:value) remaining_validations = used_option_keys - NON_VALIDATION_OPTIONS - [:presence] return if remaining_validations.none? && options.keys.length > 1 keys = non_optional_belongs_to(node.parent, all_keys) return if keys.none? add_offense_and_correct(node, all_keys, keys, options, presence) end end
def remove_keys_from_validation(corrector, node, keys)
def remove_keys_from_validation(corrector, node, keys) keys.each do |key| key_node = node.arguments.find { |arg| arg.value == key } key_range = range_with_surrounding_space( range_with_surrounding_comma(key_node.source_range, :right), side: :right ) corrector.remove(key_range) end end
def remove_presence_option(corrector, presence)
def remove_presence_option(corrector, presence) range = range_with_surrounding_comma( range_with_surrounding_space(presence.source_range, side: :left), :left ) corrector.remove(range) end
def remove_validation(corrector, node)
def remove_validation(corrector, node) corrector.remove(validation_range(node)) end
def validation_range(node)
def validation_range(node) range_by_whole_lines(node.source_range, include_final_newline: true) end