metaprogramming/instance_exec/reading_a_string_no_parens.rb

tests/_examples/metaprogramming/instance_exec/reading_a_string_no_parens.rb in the metorex repository.

# The same run written without parentheses where they can be left off.
#
# `instance_eval` handed a String runs it against the receiver: the code reads
# constants from the receiver's singleton class and class before the scopes
# the caller was written in, it sees the caller's own locals, and the file and
# line it is counted from are the ones it was given.

module ReceiverScope
  FOO = :receiver_scope

  class Parent
    FOO = :parent
  end

  class Receiver < Parent
    FOO = :receiver

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

  class PlainReceiver < Parent
  end
end

module CallerScope
  FOO = :caller_scope

  class Caller
    FOO = :caller

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

p CallerScope::Caller.new.read ReceiverScope::Receiver.new
p CallerScope::Caller.new.read ReceiverScope::PlainReceiver.new

# The code sees the caller's locals, and a name it assigns is the caller's own.
held = nil
Object.new.instance_eval "held = :assigned"
p held

# The file and the line it is counted from are the ones it was given.
refused = begin
  Object.new.instance_eval "raise 'from the string'", "a_file", 10
rescue RuntimeError => held
  held
end
p refused.backtrace.first

# With none given, the code is counted as written where the call was made.
where = Object.new.instance_eval "[__FILE__, __LINE__]"
p where[0].start_with? "(eval at "
p where[1]

# A block keeps the scopes it was written in.
class BlockScope
  @@noted = :block_scope

  def block
    -> * { @@noted }
  end
end

class OtherCaller
  @@noted = :other_caller

  def read(receiver, block)
    receiver.instance_eval(&block)
  end
end

p OtherCaller.new.read(Object.new, BlockScope.new.block)

Output:

:singleton_class
:caller
:assigned
"a_file:10:in '<main>'"
true
1
:block_scope