lib/rubocop/ast/node_pattern/method_definer.rb
# frozen_string_literal: true module RuboCop module AST class NodePattern # Functionality to turn `match_code` into methods/lambda module MethodDefiner def def_node_matcher(base, method_name, **defaults) def_helper(base, method_name, **defaults) do |name| params = emit_params('param0 = self') <<~RUBY def #{name}(#{params}) #{VAR} = param0 #{compile_init} #{emit_method_code} end RUBY end end def def_node_search(base, method_name, **defaults) def_helper(base, method_name, **defaults) do |name| emit_node_search(name) end end def compile_as_lambda <<~RUBY ->(#{emit_params('param0')}, block: nil) do #{VAR} = param0 #{compile_init} #{emit_lambda_code} end RUBY end def as_lambda eval(compile_as_lambda) # rubocop:disable Security/Eval end private # This method minimizes the closure for our method def wrapping_block(method_name, **defaults) proc do |*args, **values| send method_name, *args, **defaults, **values end end def def_helper(base, method_name, **defaults) location = caller_locations(3, 1).first unless defaults.empty? call = :"without_defaults_#{method_name}" base.send :define_method, method_name, &wrapping_block(call, **defaults) method_name = call end src = yield method_name base.class_eval(src, location.path, location.lineno) method_name end def emit_node_search(method_name) if method_name.to_s.end_with?('?') on_match = 'return true' else args = emit_params(":#{method_name}", 'param0', forwarding: true) prelude = "return enum_for(#{args}) unless block_given?\n" on_match = emit_yield_capture(VAR) end emit_node_search_body(method_name, prelude: prelude, on_match: on_match) end def emit_node_search_body(method_name, prelude:, on_match:) <<~RUBY def #{method_name}(#{emit_params('param0')}) #{compile_init} #{prelude} param0.each_node do |#{VAR}| if #{match_code} #{on_match} end end nil end RUBY end def emit_yield_capture(when_no_capture = '', yield_with: 'yield') yield_val = if captures.zero? when_no_capture elsif captures == 1 'captures[0]' # Circumvent https://github.com/jruby/jruby/issues/5710 else '*captures' end "#{yield_with}(#{yield_val})" end def emit_retval if captures.zero? 'true' elsif captures == 1 'captures[0]' else 'captures' end end def emit_param_list (1..positional_parameters).map { |n| "param#{n}" }.join(',') end def emit_keyword_list(forwarding: false) pattern = "%<keyword>s: #{'%<keyword>s' if forwarding}" named_parameters.map { |k| format(pattern, keyword: k) }.join(',') end def emit_params(*first, forwarding: false) params = emit_param_list keywords = emit_keyword_list(forwarding: forwarding) [*first, params, keywords].reject(&:empty?).join(',') end def emit_method_code <<~RUBY return unless #{match_code} block_given? ? #{emit_yield_capture} : (return #{emit_retval}) RUBY end def emit_lambda_code <<~RUBY return unless #{match_code} block ? #{emit_yield_capture(yield_with: 'block.call')} : (return #{emit_retval}) RUBY end def compile_init "captures = Array.new(#{captures})" if captures.positive? end end end end end