Skip to content

Commit 3d5619c

Browse files
committed
Introduce Namespace#eval
This commit adds an `eval` method to `Namespace` that takes a string and evaluates the string as Ruby code within the context of that namespace. For example: ```ruby n = Namespace.new n.eval("class TestClass; def hello; 'from namespace'; end; end") instance = n::TestClass.new instance.hello # => "from namespace" ``` [Feature #21365]
1 parent 242343f commit 3d5619c

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

namespace.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,23 @@ rb_namespace_require_relative(VALUE namespace, VALUE fname)
859859
return rb_ensure(rb_require_relative_entrypoint, fname, namespace_both_pop, (VALUE)&arg);
860860
}
861861

862+
static VALUE
863+
rb_namespace_eval_string(VALUE str)
864+
{
865+
return rb_eval_string(RSTRING_PTR(str));
866+
}
867+
868+
static VALUE
869+
rb_namespace_eval(VALUE namespace, VALUE str)
870+
{
871+
rb_thread_t *th = GET_THREAD();
872+
873+
StringValue(str);
874+
875+
namespace_push(th, namespace);
876+
return rb_ensure(rb_namespace_eval_string, str, namespace_pop, (VALUE)th);
877+
}
878+
862879
static int namespace_experimental_warned = 0;
863880

864881
void
@@ -1061,6 +1078,7 @@ Init_Namespace(void)
10611078
rb_define_method(rb_cNamespace, "load", rb_namespace_load, -1);
10621079
rb_define_method(rb_cNamespace, "require", rb_namespace_require, 1);
10631080
rb_define_method(rb_cNamespace, "require_relative", rb_namespace_require_relative, 1);
1081+
rb_define_method(rb_cNamespace, "eval", rb_namespace_eval, 1);
10641082

10651083
rb_define_method(rb_cNamespace, "inspect", rb_namespace_inspect, 0);
10661084

test/ruby/test_namespace.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,4 +533,86 @@ def test_load_path_and_loaded_features
533533
assert !$LOADED_FEATURES.include?(File.join(namespace_dir, 'blank1.rb'))
534534
assert !$LOADED_FEATURES.include?(File.join(namespace_dir, 'blank2.rb'))
535535
end
536+
537+
def test_eval_basic
538+
pend unless Namespace.enabled?
539+
540+
# Test basic evaluation
541+
result = @n.eval("1 + 1")
542+
assert_equal 2, result
543+
544+
# Test string evaluation
545+
result = @n.eval("'hello ' + 'world'")
546+
assert_equal "hello world", result
547+
end
548+
549+
def test_eval_with_constants
550+
pend unless Namespace.enabled?
551+
552+
# Define a constant in the namespace via eval
553+
@n.eval("TEST_CONST = 42")
554+
assert_equal 42, @n::TEST_CONST
555+
556+
# Constant should not be visible in main namespace
557+
assert_raise(NameError) { TEST_CONST }
558+
end
559+
560+
def test_eval_with_classes
561+
pend unless Namespace.enabled?
562+
563+
# Define a class in the namespace via eval
564+
@n.eval("class TestClass; def hello; 'from namespace'; end; end")
565+
566+
# Class should be accessible in the namespace
567+
instance = @n::TestClass.new
568+
assert_equal "from namespace", instance.hello
569+
570+
# Class should not be visible in main namespace
571+
assert_raise(NameError) { TestClass }
572+
end
573+
574+
def test_eval_isolation
575+
pend unless Namespace.enabled?
576+
577+
# Create another namespace
578+
n2 = Namespace.new
579+
580+
# Define different constants in each namespace
581+
@n.eval("ISOLATION_TEST = 'first'")
582+
n2.eval("ISOLATION_TEST = 'second'")
583+
584+
# Each namespace should have its own constant
585+
assert_equal "first", @n::ISOLATION_TEST
586+
assert_equal "second", n2::ISOLATION_TEST
587+
588+
# Constants should not interfere with each other
589+
assert_not_equal @n::ISOLATION_TEST, n2::ISOLATION_TEST
590+
end
591+
592+
def test_eval_with_variables
593+
pend unless Namespace.enabled?
594+
595+
# Test local variable access (should work within the eval context)
596+
result = @n.eval("x = 10; y = 20; x + y")
597+
assert_equal 30, result
598+
end
599+
600+
def test_eval_error_handling
601+
pend unless Namespace.enabled?
602+
603+
# Test syntax error
604+
assert_raise(SyntaxError) { @n.eval("1 +") }
605+
606+
# Test name error
607+
assert_raise(NameError) { @n.eval("undefined_variable") }
608+
609+
# Test that namespace is properly restored after error
610+
begin
611+
@n.eval("raise RuntimeError, 'test error'")
612+
rescue RuntimeError
613+
# Should be able to continue using the namespace
614+
result = @n.eval("2 + 2")
615+
assert_equal 4, result
616+
end
617+
end
536618
end

0 commit comments

Comments
 (0)