# frozen_string_literal: true require "dry/core/deprecations" module Dry module Core module Memoizable MEMOIZED_HASH = {}.freeze module ClassInterface module Base def memoize(*names) prepend(Memoizer.new(self, names)) end end module BasicObject include Base def new(*) obj = super obj.instance_eval { @__memoized__ = MEMOIZED_HASH.dup } obj end end module Object include Base def new(*) obj = super obj.instance_variable_set(:@__memoized__, MEMOIZED_HASH.dup) obj end if respond_to?(:ruby2_keywords, true) ruby2_keywords(:new) end end end def self.included(klass) super if klass <= Object klass.extend(ClassInterface::Object) else klass.extend(ClassInterface::BasicObject) end end # @api private class Memoizer < ::Module # @api private def initialize(klass, names) super() names.each do |name| define_memoizable( method: klass.instance_method(name) ) end end private # @api private def define_memoizable(method:) parameters = method.parameters if parameters.empty? key = method.name.hash module_eval <<~RUBY, __FILE__, __LINE__ + 1 def #{method.name} if @__memoized__.key?(#{key}) @__memoized__[#{key}] else @__memoized__[#{key}] = super end end RUBY else mapping = parameters.to_h { |k, v = nil| [k, v] } params, binds = declaration(parameters, mapping) if parameters.last[0].eql?(:block) Deprecations.warn(<<~WARN) Memoization for block-accepting methods isn't safe. Every call creates a new block instance bloating cached results. In the future, blocks will still be allowed but won't participate in cache key calculation. WARN end module_eval <<~RUBY, __FILE__, __LINE__ + 1 def #{method.name}(#{params.join(", ")}) key = [:"#{method.name}", #{binds.join(", ")}].hash if @__memoized__.key?(key) @__memoized__[key] else @__memoized__[key] = super end end RUBY if respond_to?(:ruby2_keywords, true) && mapping.key?(:reyrest) ruby2_keywords(method.name) end end end # @api private def declaration(definition, lookup) params = [] binds = [] defined = {} definition.each do |type, name| mapped_type = map_bind_type(type, lookup, defined) do raise ::NotImplementedError, "type: #{type}, name: #{name}" end if mapped_type defined[mapped_type] = true bind = name || make_bind_name(binds.size) binds << bind params << param(bind, mapped_type) end end [params, binds] end # @api private def make_bind_name(idx) :"__lv_#{idx}__" end # @api private def map_bind_type(type, original_params, defined_types) # rubocop:disable Metrics/PerceivedComplexity case type when :req :reqular when :rest, :keyreq, :keyrest, :block type when :opt if original_params.key?(:rest) || defined_types[:rest] nil else :rest end when :key if original_params.key?(:keyrest) || defined_types[:keyrest] nil else :keyrest end else yield end end # @api private def param(name, type) case type when :reqular name when :rest "*#{name}" when :keyreq "#{name}:" when :keyrest "**#{name}" when :block "&#{name}" end end end end end end