Permalink
Browse files

Make monkey_patching less global and threadsafe

  • Loading branch information...
1 parent b8ff414 commit aa99aa25581697f4e789a5a0c3ff0d19cc2668d4 @ConradIrwin ConradIrwin committed Dec 4, 2010
Showing with 72 additions and 16 deletions.
  1. +47 −16 lib/ampex.rb
  2. +25 −0 spec/ampex_spec.rb
View
@@ -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
@@ -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
View
@@ -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
@@ -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.