diff --git a/lib/definitions/evaluator.rb b/lib/definitions/evaluator.rb index 9be7fca..3556c18 100644 --- a/lib/definitions/evaluator.rb +++ b/lib/definitions/evaluator.rb @@ -40,26 +40,35 @@ def instance_evaluate(proxy:) eval(proxy.value, binding, proxy.file_path, proxy.start_line) # rubocop:disable Security/Eval end + def class_evaluate(proxy:, class_binding:) + # Not a security risk because the code comes from a trusted source; the file that included lowtype. + eval(proxy.value, class_binding, proxy.file_path, proxy.start_line) # rubocop:disable Security/Eval + end + class << self - def evaluate(method_proxies:) + def evaluate(method_proxies:, class_binding: nil) require_relative '../syntax/union_types' if LowType.config.union_type_expressions method_proxies.each_value do |method_proxy| - evaluate_param_proxy_expressions(method_proxy:) + evaluate_param_proxy_expressions(method_proxy:, class_binding:) evaluate_return_proxy_expression(return_proxy: method_proxy.return_proxy) if method_proxy.return_proxy end end - def evaluate_param_proxy_expressions(method_proxy:) + def evaluate_param_proxy_expressions(method_proxy:, class_binding: nil) begin # rubocop:disable Style/RedundantBegin method_proxy.tagged_params(:value).each do |param_proxy| - # TODO: Evaluate in the binding of the class that included LowType if not a type managed by LowType. - expression = new.instance_evaluate(proxy: param_proxy) + expression = begin + new.instance_evaluate(proxy: param_proxy) + rescue NameError + raise unless class_binding + new.class_evaluate(proxy: param_proxy, class_binding:) + end param_proxy.expression = cast_type_expression(expression:, method_proxy:) end - rescue NameError + rescue NameError => e mp = method_proxy - raise NameError, "Unknown type '#{mp.value}' for #{mp.scope} at #{mp.file_path}:#{mp.start_line}" + raise NameError, "Unknown type '#{e.name}' for #{mp.scope} at #{mp.file_path}:#{mp.start_line}" end end diff --git a/lib/low_type.rb b/lib/low_type.rb index 50fbd4c..304d430 100644 --- a/lib/low_type.rb +++ b/lib/low_type.rb @@ -50,7 +50,7 @@ def self.included(klass) class_proxy.class_binding = trace.binding - Low::Evaluator.evaluate(method_proxies: class_proxy.keyed_methods) + Low::Evaluator.evaluate(method_proxies: class_proxy.keyed_methods, class_binding: class_proxy.class_binding) klass.prepend Low::Redefiner.redefine(method_proxies: class_proxy.instance_methods, class_proxy:) klass.singleton_class.prepend Low::Redefiner.redefine(method_proxies: class_proxy.class_methods, class_proxy:) diff --git a/spec/features/custom_class_spec.rb b/spec/features/custom_class_spec.rb new file mode 100644 index 0000000..8559fa9 --- /dev/null +++ b/spec/features/custom_class_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative '../fixtures/custom_class' + +RSpec.describe Order do + subject { described_class.new } + + describe '#process' do + it 'accepts a user-defined class as a type without raising NameError' do + expect { subject.process(method: PaymentMethod.new) }.not_to raise_error + end + + it 'returns the passed value' do + payment = PaymentMethod.new + expect(subject.process(method: payment)).to eq(payment) + end + end +end diff --git a/spec/fixtures/custom_class.rb b/spec/fixtures/custom_class.rb new file mode 100644 index 0000000..520e8c0 --- /dev/null +++ b/spec/fixtures/custom_class.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative '../../lib/low_type' + +class PaymentMethod; end + +class Order + include LowType + + def process(method: PaymentMethod) + method + end +end