# frozen_string_literal: truemoduleRuboCopmoduleCopmoduleSorbet# Ensures that callback conditionals are bound to the right type# so that they are type checked properly.## @safety# Auto-correction is unsafe because other libraries define similar style callbacks as Rails, but don't always need# binding to the attached class. Auto-correcting those usages can lead to false positives and auto-correction# introduces new typing errors.## @example## # bad# class Post < ApplicationRecord# before_create :do_it, if: -> { should_do_it? }## def should_do_it?# true# end# end## # good# class Post < ApplicationRecord# before_create :do_it, if: -> {# T.bind(self, Post)# should_do_it?# }## def should_do_it?# true# end# endclassCallbackConditionalsBinding<RuboCop::Cop::BaseextendAutoCorrectorincludeAlignmentMSG="Callback conditionals should be bound to the right type. Use T.bind(self, %{type})"RESTRICT_ON_SEND=[:validate,:validates,:validates_with,:before_validation,:around_validation,:before_create,:before_save,:before_destroy,:before_update,:after_create,:after_save,:after_destroy,:after_update,:after_touch,:after_initialize,:after_find,:around_create,:around_save,:around_destroy,:around_update,:before_commit,:after_commit,:after_create_commit,:after_destroy_commit,:after_rollback,:after_save_commit,:after_update_commit,:before_action,:prepend_before_action,:append_before_action,:around_action,:prepend_around_action,:append_around_action,:after_action,:prepend_after_action,:append_after_action,].freeze# @!method argumentless_unbound_callable_callback_conditional?(node)def_node_matcher:argumentless_unbound_callable_callback_conditional?,<<~PATTERN
(pair (sym {:if :unless}) # callback conditional
$(block
(send nil? {:lambda :proc}) # callable
(args) # argumentless
!`(send(const {cbase nil?} :T) :bind self $_ ) # unbound
)
)
PATTERNdefon_send(node)type=immediately_enclosing_module_name(node)returnunlesstypenode.arguments.eachdo|arg|nextunlessarg.hash_type?# Skip non-keyword argumentsarg.each_child_nodedo|pair_node|argumentless_unbound_callable_callback_conditional?(pair_node)do|block|add_offense(pair_node,message: format(MSG,type: type))do|corrector|block_opening_indentation=block.source_range.source_line[/\A */]block_body_indentation=block_opening_indentation+SPACE*configured_indentation_widthifblock.single_line?# then convert to multi-line block first# 1. Replace whitespace (if any) between the opening delimiter and the block body,# with newline and the correct indentation for the block body.preceeding_whitespace_range=block.loc.begin.end.join(block.body.source_range.begin)corrector.replace(preceeding_whitespace_range,"\n#{block_body_indentation}")# 2. Replace whitespace (if any) between the block body and the closing delimiter,# with newline and the same indentation as the block opening.trailing_whitespace_range=block.body.source_range.end.join(block.loc.end.begin)corrector.replace(trailing_whitespace_range,"\n#{block_opening_indentation}")end# Prepend the binding to the block bodycorrector.insert_before(block.body,"T.bind(self, #{type})\n#{block_body_indentation}")endendendendendprivate# Find the immediately enclosing class or module name.# Returns `nil`` if the immediate parent (skipping begin if present) is not a class or module.defimmediately_enclosing_module_name(node)(node.parent&.begin_type??node.parent.parent:node.parent)&.defined_module_nameendendendendend