From dfb8949eea271d724f5132f59ba6619be2511a57 Mon Sep 17 00:00:00 2001 From: obsidiannnn Date: Fri, 1 May 2026 23:01:50 +0530 Subject: [PATCH 1/3] Add class_evaluate to resolve user-defined constants via class binding, fixes #31 --- lib/definitions/evaluator.rb | 14 ++++++++------ lib/low_type.rb | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/definitions/evaluator.rb b/lib/definitions/evaluator.rb index 9be7fca..b2014a9 100644 --- a/lib/definitions/evaluator.rb +++ b/lib/definitions/evaluator.rb @@ -36,25 +36,27 @@ class Evaluator using LowType::Syntax def instance_evaluate(proxy:) - # Not a security risk because the code comes from a trusted source; the file that included lowtype. eval(proxy.value, binding, proxy.file_path, proxy.start_line) # rubocop:disable Security/Eval end + def class_evaluate(proxy:, class_binding:) + 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 = class_binding ? new.class_evaluate(proxy: param_proxy, class_binding:) : new.instance_evaluate(proxy: param_proxy) param_proxy.expression = cast_type_expression(expression:, method_proxy:) end rescue NameError 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:) From bd1461d6e697238bda644080daa999958f3c4792 Mon Sep 17 00:00:00 2001 From: obsidiannnn Date: Mon, 4 May 2026 04:36:56 +0530 Subject: [PATCH 2/3] Restore security comment on eval calls, add spec for custom class type resolution --- lib/definitions/evaluator.rb | 2 ++ spec/features/custom_class_spec.rb | 18 ++++++++++++++++++ spec/fixtures/custom_class.rb | 13 +++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 spec/features/custom_class_spec.rb create mode 100644 spec/fixtures/custom_class.rb diff --git a/lib/definitions/evaluator.rb b/lib/definitions/evaluator.rb index b2014a9..25007ef 100644 --- a/lib/definitions/evaluator.rb +++ b/lib/definitions/evaluator.rb @@ -36,10 +36,12 @@ class Evaluator using LowType::Syntax def instance_evaluate(proxy:) + # Not a security risk because the code comes from a trusted source; the file that included lowtype. 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 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 From bab306d253ebe41d567b406d0f8d4138c2fd6497 Mon Sep 17 00:00:00 2001 From: obsidiannnn Date: Mon, 4 May 2026 04:45:04 +0530 Subject: [PATCH 3/3] Fix class_evaluate fallback: try instance_evaluate first, fall back to class_evaluate on NameError --- lib/definitions/evaluator.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/definitions/evaluator.rb b/lib/definitions/evaluator.rb index 25007ef..3556c18 100644 --- a/lib/definitions/evaluator.rb +++ b/lib/definitions/evaluator.rb @@ -58,12 +58,17 @@ def evaluate(method_proxies:, class_binding: nil) def evaluate_param_proxy_expressions(method_proxy:, class_binding: nil) begin # rubocop:disable Style/RedundantBegin method_proxy.tagged_params(:value).each do |param_proxy| - expression = class_binding ? new.class_evaluate(proxy: param_proxy, class_binding:) : 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