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]
This commit is contained in:
Aaron Patterson 2025-06-25 10:36:57 -07:00 committed by Aaron Patterson
parent 242343ff80
commit 3d5619c8b1
2 changed files with 100 additions and 0 deletions

View file

@ -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);

View file

@ -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