# frozen_string_literal: true require "set" # rubocop:disable Lint/RedundantRequireStatement require "ruby-next/config" require "ruby-next/utils" module RubyNext module Core # Patch contains the extension implementation # and meta information (e.g., Ruby version). class Patch attr_reader :refineables, :name, :mod, :method_name, :version, :body, :singleton, :core_ext, :supported, :native, :location # Create a new patch for module/class (mod) # with the specified uniq name # # `core_ext` defines the strategy for core extensions: # - :patch — extend class directly # - :prepend — extend class by prepending a module (e.g., when needs `super`) def initialize(mod = nil, method:, version:, name: nil, supported: nil, native: nil, location: nil, refineable: mod, core_ext: :patch, singleton: nil) @mod = mod @method_name = method @version = version if method_name && mod @supported = supported.nil? ? mod.method_defined?(method_name) : supported # define whether running Ruby has a native implementation for this method # for that, we check the source_location (which is nil for C defined methods) @native = native.nil? ? (supported? && native_location?(mod.instance_method(method_name).source_location)) : native end @singleton = singleton @refineables = Array(refineable) @body = yield @core_ext = core_ext @location = location || build_location(caller_locations(1, 5)) @name = name || build_module_name end def prepend? core_ext == :prepend end def core_ext? !mod.nil? end alias supported? supported alias native? native alias singleton? singleton def to_module Module.new.tap do |ext| ext.module_eval(body, *location) RubyNext::Core.const_set(name, ext) end end private def build_module_name mod_name = singleton? ? singleton.name : mod.name camelized_method_name = method_name.to_s.split("_").map(&:capitalize).join "#{mod_name}#{camelized_method_name}".gsub(/\W/, "") end def build_location(trace_locations) # The caller_locations behaviour depends on implementaion, # e.g. in JRuby https://github.com/jruby/jruby/issues/6055 while trace_locations.first.base_label != "patch" trace_locations.shift end trace_location = trace_locations[1] [trace_location.absolute_path, trace_location.lineno + 2] end def native_location?(location) location.nil? || location.first.match?(/(