summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAaron Patterson <[email protected]>2025-06-25 10:36:57 -0700
committerAaron Patterson <[email protected]>2025-06-26 10:18:14 -0700
commit3d5619c8b1a76626e0991d758b71afc549829c38 (patch)
tree191b2610d4dbec921c86e97ee991617836dfb7c7
parent242343ff801e35d19d81ec9d4ff3c32a36c00f06 (diff)
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]
-rw-r--r--namespace.c18
-rw-r--r--test/ruby/test_namespace.rb82
2 files changed, 100 insertions, 0 deletions
diff --git a/namespace.c b/namespace.c
index 24e4b92ac4..dd7d21c380 100644
--- a/namespace.c
+++ b/namespace.c
@@ -859,6 +859,23 @@ rb_namespace_require_relative(VALUE namespace, VALUE fname)
return rb_ensure(rb_require_relative_entrypoint, fname, namespace_both_pop, (VALUE)&arg);
}
+static VALUE
+rb_namespace_eval_string(VALUE str)
+{
+ return rb_eval_string(RSTRING_PTR(str));
+}
+
+static VALUE
+rb_namespace_eval(VALUE namespace, VALUE str)
+{
+ rb_thread_t *th = GET_THREAD();
+
+ StringValue(str);
+
+ namespace_push(th, namespace);
+ return rb_ensure(rb_namespace_eval_string, str, namespace_pop, (VALUE)th);
+}
+
static int namespace_experimental_warned = 0;
void
@@ -1061,6 +1078,7 @@ Init_Namespace(void)
rb_define_method(rb_cNamespace, "load", rb_namespace_load, -1);
rb_define_method(rb_cNamespace, "require", rb_namespace_require, 1);
rb_define_method(rb_cNamespace, "require_relative", rb_namespace_require_relative, 1);
+ rb_define_method(rb_cNamespace, "eval", rb_namespace_eval, 1);
rb_define_method(rb_cNamespace, "inspect", rb_namespace_inspect, 0);
diff --git a/test/ruby/test_namespace.rb b/test/ruby/test_namespace.rb
index f13063be48..cd59306867 100644
--- a/test/ruby/test_namespace.rb
+++ b/test/ruby/test_namespace.rb
@@ -533,4 +533,86 @@ class TestNamespace < Test::Unit::TestCase
assert !$LOADED_FEATURES.include?(File.join(namespace_dir, 'blank1.rb'))
assert !$LOADED_FEATURES.include?(File.join(namespace_dir, 'blank2.rb'))
end
+
+ def test_eval_basic
+ pend unless Namespace.enabled?
+
+ # Test basic evaluation
+ result = @n.eval("1 + 1")
+ assert_equal 2, result
+
+ # Test string evaluation
+ result = @n.eval("'hello ' + 'world'")
+ assert_equal "hello world", result
+ end
+
+ def test_eval_with_constants
+ pend unless Namespace.enabled?
+
+ # Define a constant in the namespace via eval
+ @n.eval("TEST_CONST = 42")
+ assert_equal 42, @n::TEST_CONST
+
+ # Constant should not be visible in main namespace
+ assert_raise(NameError) { TEST_CONST }
+ end
+
+ def test_eval_with_classes
+ pend unless Namespace.enabled?
+
+ # Define a class in the namespace via eval
+ @n.eval("class TestClass; def hello; 'from namespace'; end; end")
+
+ # Class should be accessible in the namespace
+ instance = @n::TestClass.new
+ assert_equal "from namespace", instance.hello
+
+ # Class should not be visible in main namespace
+ assert_raise(NameError) { TestClass }
+ end
+
+ def test_eval_isolation
+ pend unless Namespace.enabled?
+
+ # Create another namespace
+ n2 = Namespace.new
+
+ # Define different constants in each namespace
+ @n.eval("ISOLATION_TEST = 'first'")
+ n2.eval("ISOLATION_TEST = 'second'")
+
+ # Each namespace should have its own constant
+ assert_equal "first", @n::ISOLATION_TEST
+ assert_equal "second", n2::ISOLATION_TEST
+
+ # Constants should not interfere with each other
+ assert_not_equal @n::ISOLATION_TEST, n2::ISOLATION_TEST
+ end
+
+ def test_eval_with_variables
+ pend unless Namespace.enabled?
+
+ # Test local variable access (should work within the eval context)
+ result = @n.eval("x = 10; y = 20; x + y")
+ assert_equal 30, result
+ end
+
+ def test_eval_error_handling
+ pend unless Namespace.enabled?
+
+ # Test syntax error
+ assert_raise(SyntaxError) { @n.eval("1 +") }
+
+ # Test name error
+ assert_raise(NameError) { @n.eval("undefined_variable") }
+
+ # Test that namespace is properly restored after error
+ begin
+ @n.eval("raise RuntimeError, 'test error'")
+ rescue RuntimeError
+ # Should be able to continue using the namespace
+ result = @n.eval("2 + 2")
+ assert_equal 4, result
+ end
+ end
end