Skip to content
This repository has been archived by the owner on Dec 5, 2023. It is now read-only.

Commit

Permalink
Make monkey_patching less global and threadsafe
Browse files Browse the repository at this point in the history
  • Loading branch information
ConradIrwin committed Dec 4, 2010
1 parent b8ff414 commit aa99aa2
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 16 deletions.
63 changes: 47 additions & 16 deletions lib/ampex.rb
Expand Up @@ -28,7 +28,7 @@ def initialize(&block)
#
def method_missing(name, *args, &block)
mv = Metavariable.new { |x| @to_proc.call(x).send(name, *args, &block) }
Metavariable.temporarily_monkeypatch(args.last.class, mv) if name.to_s =~ /[^!=<>]=$/
Metavariable.temporarily_monkeypatch(args.last, :to_proc) { mv.to_proc } if name.to_s =~ /[^!=<>]=$/
mv
end

Expand All @@ -49,26 +49,57 @@ def method_missing(name, *args, &block)
# :two.to_proc _/ and un-patch here
# ary.map &_
#
# There is a risk if someone uses an amp-less X, and assigns something with to_proc
# (most likely a symbol), and then uses .map(&:to_i), as &:to_i will return the
# behaviour of their metavariable.
# We go to some lengths to ensure that, providing the & and the X are adjacent,
# it's not possible to get different behaviour in the rest of the program; despite
# the temporary mutation of potentially global state.
#
# There are other things that might notice us doing this, if people are listening
# on various method_added hooks, or have overridden class_eval, etc. But I'm not
# too worried.
# We can't really do anything if the & has been split from the X, consider:
#
def self.temporarily_monkeypatch(klass, mv)
klass.class_eval do
# assigner = (X[0] = :to_i)
# assigner == :to_i
# # => true
# [1,2,3].map(&:to_i)
# # => NoMethodError: undefined method `[]=' for 1:Fixnum
#
# Just strongly encourage use of:
# assigner = lambda &X = :to_i
# assigner == :to_i
# # => false
# [1,2,3].map(&:to_i)
# # => [1,2,3]
#
def self.temporarily_monkeypatch(instance, method_name, &block)

Thread.exclusive do
@monkey_patch_count = @monkey_patch_count ? @monkey_patch_count + 1 : 0
stashed_method_name = :"#{method_name}_without_metavariable_#{@monkey_patch_count}"
thread = Thread.current

# Try to get a handle on the object's singleton class, but fall back to using
# its actual class where that is not possible (i.e. for numbers and symbols)
klass = (class << instance; self; end) rescue instance.class
klass.class_eval do

alias_method(stashed_method_name, method_name) rescue nil
define_method(method_name) do

todo = block

Thread.exclusive do
if self.equal?(instance) && thread.equal?(Thread.current)

alias_method(:to_proc_without_metavariable, :to_proc) rescue nil
define_method(:to_proc) do
klass.class_eval do
klass.class_eval do
undef_method(method_name)
alias_method(method_name, stashed_method_name) rescue nil
undef_method(stashed_method_name) rescue nil
end

undef :to_proc
alias_method(:to_proc, :to_proc_without_metavariable) rescue nil
undef :to_proc_without_metavariable rescue nil
else
todo = method(stashed_method_name)
end
end

mv.to_proc
todo.call
end
end
end
Expand Down
25 changes: 25 additions & 0 deletions spec/ampex_spec.rb
Expand Up @@ -48,10 +48,29 @@ def intercept(b)
[{}].each(&X[intercept(b)] = b).should == [{NoMethodError => b}]
end

it "should not be possible to intercept #to_proc in an interrupting thread" do
X[0] = :inspect
b = []
Thread.new { b << [1,2,3].map(&:inspect) }.join
b.should == [["1","2", "3"]]
[].map(&:inspect)
end

it "should preserve existing #to_proc" do
[{}].each(&X[:to_a] = :to_a).map(&:to_a).should == [[[:to_a, :to_a]]]
end

it "should preserve existing #to_proc in an object's singleton class" do
a = Object.new
class << a
def to_proc; lambda { 3 }; end
end

[1].map(&a).should == [3]
[{1 => 2}].each(&X[1] = 3).should == [{1 => 3}]
[1].map(&a).should == [3]
end

it "should only evaluate arguments once" do
@counted = 0
def count
Expand Down Expand Up @@ -82,4 +101,10 @@ def assigner(key, value); lambda &X[key] = value; end
[{}].map(&:inspect).should == ["{}"]
end

it "should not be perturbed by an ampless X" do
X[0] = 1
[{1 => 2}].each(&X[1] = 3).should == [{1 => 3}]
[].map(&1)
end

end

0 comments on commit aa99aa2

Please sign in to comment.