lib/rubocop/cop/sorbet/block_method_definition.rb
# frozen_string_literal: true module RuboCop module Cop module Sorbet # Disallow defining methods in blocks, to prevent running into issues # caused by https://github.com/sorbet/sorbet/issues/3609. # # As a workaround, use `define_method` instead. # # The one exception is for `Class.new` blocks, as long as the result is # assigned to a constant (i.e. as long as it is not an anonymous class). # Another exception is for ActiveSupport::Concern `class_methods` blocks. # # @example # # bad # yielding_method do # def bad(args) # # ... # end # end # # # bad # Class.new do # def bad(args) # # ... # end # end # # # good # yielding_method do # define_method(:good) do |args| # # ... # end # end # # # good # MyClass = Class.new do # def good(args) # # ... # end # end # # # good # module SomeConcern # extend ActiveSupport::Concern # # class_methods do # def good(args) # # ... # end # end # end # class BlockMethodDefinition < Base include RuboCop::Cop::Alignment extend AutoCorrector MSG = "Do not define methods in blocks (use `define_method` as a workaround)." # @!method activesupport_concern_class_methods_block?(node) def_node_matcher :activesupport_concern_class_methods_block?, <<~PATTERN (block (send nil? :class_methods) _ _ ) PATTERN # @!method module_extends_activesupport_concern?(node) def_node_matcher :module_extends_activesupport_concern?, <<~PATTERN (module _ (begin <(send nil? :extend (const (const {nil? cbase} :ActiveSupport) :Concern)) ...> ... ) ) PATTERN def on_block(node) if (parent = node.parent) return if parent.casgn_type? end # Check if this is a class_methods block inside an ActiveSupport::Concern return if in_activesupport_concern_class_methods_block?(node) node.each_descendant(:any_def) do |def_node| add_offense(def_node) do |corrector| autocorrect_method_in_block(corrector, def_node) end end end alias_method :on_numblock, :on_block private def in_activesupport_concern_class_methods_block?(node) return false unless activesupport_concern_class_methods_block?(node) immediate_module = node.each_ancestor(:module).first module_extends_activesupport_concern?(immediate_module) end def autocorrect_method_in_block(corrector, node) indent = offset(node) method_name = node.method_name args = transform_args_to_block_args(node) # Build the method signature replacement if node.def_type? signature_replacement = "define_method(:#{method_name}) do#{args}" elsif node.defs_type? receiver = node.receiver.source signature_replacement = "#{receiver}.define_singleton_method(:#{method_name}) do#{args}" end if node.body end_pos = node.body.source_range.begin_pos indentation = "\n#{indent} " else end_pos, indentation = handle_method_without_body(node, indent) end signature_range = node.source_range.with(end_pos: end_pos) corrector.replace(signature_range, signature_replacement + indentation) end def transform_args_to_block_args(node) args = node.arguments if args.empty? "" else args_string = args.map(&:source).join(", ") " |#{args_string}|" end end def handle_method_without_body(node, indent) if single_line_method?(node) handle_single_line_method(node, indent) else handle_multiline_method_without_body(node) end end def single_line_method?(node) !node.source.include?("\n") end def handle_single_line_method(node, indent) end_pos = node.source_range.end_pos indentation = "\n#{indent}end" [end_pos, indentation] end def handle_multiline_method_without_body(node) end_pos = find_method_signature_end_position(node) indentation = "" [end_pos, indentation] end def find_method_signature_end_position(node) if node.arguments.any? find_end_position_with_arguments(node) else find_end_position_without_arguments(node) end end def find_end_position_with_arguments(node) last_arg = node.last_argument end_pos = last_arg.source_range.end_pos adjust_for_closing_parenthesis(end_pos) end def find_end_position_without_arguments(node) node.loc.name.end_pos end def adjust_for_closing_parenthesis(end_pos) source_after_last_arg = processed_source.buffer.source[end_pos..-1] match = closing_parenthesis_follows(source_after_last_arg) if match end_pos + match.end(0) else end_pos end end def closing_parenthesis_follows(source) source.match(/\A\s*\)/) end end end end end