diff options
author | Aaron Patterson <[email protected]> | 2025-06-25 10:36:57 -0700 |
---|---|---|
committer | Aaron Patterson <[email protected]> | 2025-06-26 10:18:14 -0700 |
commit | 3d5619c8b1a76626e0991d758b71afc549829c38 (patch) | |
tree | 191b2610d4dbec921c86e97ee991617836dfb7c7 | |
parent | 242343ff801e35d19d81ec9d4ff3c32a36c00f06 (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.c | 18 | ||||
-rw-r--r-- | test/ruby/test_namespace.rb | 82 |
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 |