Skip to content

Commit

Permalink
[GR-18163] Fix constant lookup in #instance_eval called with String
Browse files Browse the repository at this point in the history
PullRequest: truffleruby/3592
  • Loading branch information
andrykonchin committed Jan 25, 2023
2 parents 8bb04d8 + 8d39eb3 commit f911355
Show file tree
Hide file tree
Showing 15 changed files with 453 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Bug fixes:
* Fix `Marshal.dump` when big Integer (that cannot be expressed with 4 bytes) is serialized (#2790, @andrykonchin).
* Fix `Array#pack` and accept `Numeric` values when `Float` is expected (#2815, @andrykonchin).
* Fix `\P{}` matching in regular expressions (#2798, @andrykonchin).
* Fix constants lookup when `BasicObject#instance_eval` method is called with a String (#2810, @andrykonchin).

Compatibility:

Expand Down
228 changes: 225 additions & 3 deletions spec/ruby/core/basicobject/fixtures/classes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,231 @@ module InstExecIncluded
include InstExec
end

module InstEvalCVar
instance_eval { @@count = 2 }
module InstEval
module CVar
module Get
class ReceiverScope
@@cvar = :value_defined_in_receiver_scope
end

class BlockDefinitionScope
@@cvar = :value_defined_in_block_definition_scope

def block
->(*) { @@cvar }
end
end

class CallerScope
@@cvar = :value_defined_in_caller_scope

def get_class_variable_with_string(obj)
obj.instance_eval("@@cvar")
end

def get_class_variable_with_block(obj, block)
obj.instance_eval(&block)
end
end

class CallerWithoutCVarScope
def get_class_variable_with_string(obj)
obj.instance_eval("@@cvar")
end
end

ReceiverWithCVarDefinedInSingletonClass = Class.new.new.tap do |obj|
obj.singleton_class.class_variable_set(:@@cvar, :value_defined_in_receiver_singleton_class)
end
end

module Set
class ReceiverScope
end

class BlockDefinitionScope
def self.get_class_variable
@@cvar
end

def block_to_assign(value)
->(*) { @@cvar = value }
end
end

class CallerScope
def self.get_class_variable
@@cvar
end

def set_class_variable_with_string(obj, value)
obj.instance_eval("@@cvar=#{value.inspect}")
end

def set_class_variable_with_block(obj, block)
obj.instance_eval(&block)
end
end
end
end
end

module InstEval
module Constants
module ConstantInReceiverSingletonClass
module ReceiverScope
FOO = :ReceiverScope

class ReceiverParent
FOO = :ReceiverParent
end

class Receiver < ReceiverParent
FOO = :Receiver

def initialize
self.singleton_class.const_set(:FOO, :singleton_class)
end
end
end

module CallerScope
FOO = :CallerScope

class CallerParent
FOO = :CallerParent
end

class Caller < CallerParent
FOO = :Caller

def get_constant_with_string(receiver)
receiver.instance_eval("FOO")
end
end
end
end

module ConstantInReceiverClass
module ReceiverScope
FOO = :ReceiverScope

class ReceiverParent
FOO = :ReceiverParent
end

class Receiver < ReceiverParent
FOO = :Receiver
end
end

module CallerScope
FOO = :CallerScope

class CallerParent
FOO = :CallerParent
end

class Caller < CallerParent
FOO = :Caller

def get_constant_with_string(receiver)
receiver.instance_eval("FOO")
end
end
end
end

module ConstantInCallerClass
module ReceiverScope
FOO = :ReceiverScope

class ReceiverParent
FOO = :ReceiverParent
end

class Receiver < ReceiverParent
# FOO is not declared in a receiver class
end
end

module CallerScope
FOO = :CallerScope

class CallerParent
FOO = :CallerParent
end

class Caller < CallerParent
FOO = :Caller

def get_constant_with_string(receiver)
receiver.instance_eval("FOO")
end
end
end
end

module ConstantInCallerOuterScopes
module ReceiverScope
FOO = :ReceiverScope

class ReceiverParent
FOO = :ReceiverParent
end

class Receiver < ReceiverParent
# FOO is not declared in a receiver class
end
end

module CallerScope
FOO = :CallerScope

class CallerParent
FOO = :CallerParent
end

class Caller < CallerParent
# FOO is not declared in a caller class

def get_constant_with_string(receiver)
receiver.instance_eval("FOO")
end
end
end
end

module ConstantInReceiverParentClass
module ReceiverScope
FOO = :ReceiverScope

class ReceiverParent
FOO = :ReceiverParent
end

class Receiver < ReceiverParent
# FOO is not declared in a receiver class
end
end

module CallerScope
# FOO is not declared in a caller outer scopes

class CallerParent
FOO = :CallerParent
end

class Caller < CallerParent
# FOO is not declared in a caller class

def get_constant_with_string(receiver)
receiver.instance_eval("FOO")
end
end
end
end
end
end

class InstEvalConst
Expand All @@ -26,7 +249,6 @@ class InstEvalConst
module InstEvalOuter
module Inner
obj = InstEvalConst.new
X_BY_STR = obj.instance_eval("INST_EVAL_CONST_X") rescue nil
X_BY_BLOCK = obj.instance_eval { INST_EVAL_CONST_X } rescue nil
end
end
Expand Down
117 changes: 101 additions & 16 deletions spec/ruby/core/basicobject/instance_eval_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ def foo
BasicObjectSpecs::IVars.new.instance_eval("@secret").should == 99
end

it "raises TypeError for frozen objects when tries to set receiver's instance variables" do
-> { nil.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen NilClass: nil")
-> { true.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen TrueClass: true")
-> { false.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen FalseClass: false")
-> { 1.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen Integer: 1")
-> { :symbol.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen Symbol: :symbol")

obj = Object.new
obj.freeze
-> { obj.instance_eval { @foo = 42 } }.should raise_error(FrozenError)
end

it "treats block-local variables as local to the block" do
prc = instance_eval <<-CODE
proc do |x, prc|
Expand All @@ -105,11 +117,6 @@ def foo
prc.call(false, prc).should == 1
end

it "sets class variables in the receiver" do
BasicObjectSpecs::InstEvalCVar.class_variables.should include(:@@count)
BasicObjectSpecs::InstEvalCVar.send(:class_variable_get, :@@count).should == 2
end

it "makes the receiver metaclass the scoped class when used with a string" do
obj = Object.new
obj.instance_eval %{
Expand All @@ -119,8 +126,52 @@ class B; end
obj.singleton_class.const_get(:B).should be_an_instance_of(Class)
end

it "gets constants in the receiver if a string given" do
BasicObjectSpecs::InstEvalOuter::Inner::X_BY_STR.should == 2
describe "constants lookup when a String given" do
it "looks in the receiver singleton class first" do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::CallerScope::Caller.new

caller.get_constant_with_string(receiver).should == :singleton_class
end

ruby_version_is ""..."3.1" do
it "looks in the caller scope next" do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::CallerScope::Caller.new

caller.get_constant_with_string(receiver).should == :Caller
end
end

ruby_version_is "3.1" do
it "looks in the receiver class next" do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::CallerScope::Caller.new

caller.get_constant_with_string(receiver).should == :Receiver
end
end

it "looks in the caller class next" do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::CallerScope::Caller.new

caller.get_constant_with_string(receiver).should == :Caller
end

it "looks in the caller outer scopes next" do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::CallerScope::Caller.new

caller.get_constant_with_string(receiver).should == :CallerScope
end

it "looks in the receiver class hierarchy next" do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::CallerScope::Caller.new

caller.get_constant_with_string(receiver).should == :ReceiverParent
end
end

it "doesn't get constants in the receiver if a block given" do
Expand All @@ -136,17 +187,51 @@ class B; end
end.should raise_error(TypeError)
end

quarantine! do # Not clean, leaves cvars lying around to break other specs
it "scopes class var accesses in the caller when called on an Integer" do
# Integer can take instance vars
Integer.class_eval "@@__tmp_instance_eval_spec = 1"
(defined? @@__tmp_instance_eval_spec).should be_nil
describe "class variables lookup" do
it "gets class variables in the caller class when called with a String" do
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerScope.new

caller.get_class_variable_with_string(receiver).should == :value_defined_in_caller_scope
end

it "gets class variables in the block definition scope when called with a block" do
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerScope.new
block = BasicObjectSpecs::InstEval::CVar::Get::BlockDefinitionScope.new.block

caller.get_class_variable_with_block(receiver, block).should == :value_defined_in_block_definition_scope
end

it "sets class variables in the caller class when called with a String" do
receiver = BasicObjectSpecs::InstEval::CVar::Set::ReceiverScope.new
caller = BasicObjectSpecs::InstEval::CVar::Set::CallerScope.new

caller.set_class_variable_with_string(receiver, 1)
BasicObjectSpecs::InstEval::CVar::Set::CallerScope.get_class_variable.should == 1
end

it "sets class variables in the block definition scope when called with a block" do
receiver = BasicObjectSpecs::InstEval::CVar::Set::ReceiverScope.new
caller = BasicObjectSpecs::InstEval::CVar::Set::CallerScope.new
block = BasicObjectSpecs::InstEval::CVar::Set::BlockDefinitionScope.new.block_to_assign(1)

caller.set_class_variable_with_block(receiver, block)
BasicObjectSpecs::InstEval::CVar::Set::BlockDefinitionScope.get_class_variable.should == 1
end

it "does not have access to class variables in the receiver class when called with a String" do
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new
-> { caller.get_class_variable_with_string(receiver) }.should raise_error(NameError, /uninitialized class variable @@cvar/)
end

@@__tmp_instance_eval_spec = 2
1.instance_eval { @@__tmp_instance_eval_spec }.should == 2
Integer.__send__(:remove_class_variable, :@@__tmp_instance_eval_spec)
it "does not have access to class variables in the receiver's singleton class when called with a String" do
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverWithCVarDefinedInSingletonClass
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new
-> { caller.get_class_variable_with_string(receiver) }.should raise_error(NameError, /uninitialized class variable @@cvar/)
end
end
end

it "raises a TypeError when defining methods on numerics" do
-> do
Expand Down
Loading

0 comments on commit f911355

Please sign in to comment.