summaryrefslogtreecommitdiff
path: root/lib/pry/commands/edit
diff options
context:
space:
mode:
authorJohn Mair <jrmair@gmail.com>2012-12-31 05:15:25 +0100
committerJohn Mair <jrmair@gmail.com>2012-12-31 05:18:30 +0100
commit4cfb0dccae6727db56a8e6afef5fb6c4809b62a3 (patch)
tree7095220a0b2df775ca5a388d7272541012bf8ff5 /lib/pry/commands/edit
parent621e01e3f0d4ce111e36ab48b2b97312d5576c74 (diff)
downloadpry-4cfb0dccae6727db56a8e6afef5fb6c4809b62a3.tar.gz
Merge remaining `edit-method` functionality into `edit`
edit now accepts the -p (runtime patch) switch, and can patch methods. Coming soon: patching of classes and commands. edit-method command has been left in codebase until 100% sure 'edit' is a full replacement.
Diffstat (limited to 'lib/pry/commands/edit')
-rw-r--r--lib/pry/commands/edit/method_patcher.rb106
1 files changed, 106 insertions, 0 deletions
diff --git a/lib/pry/commands/edit/method_patcher.rb b/lib/pry/commands/edit/method_patcher.rb
new file mode 100644
index 00000000..55d81021
--- /dev/null
+++ b/lib/pry/commands/edit/method_patcher.rb
@@ -0,0 +1,106 @@
+class Pry
+ class Command::Edit
+ class MethodPatcher
+ attr_accessor :method_object
+ attr_accessor :target
+ attr_accessor :_pry_
+
+ def initialize(method_object, target, _pry_)
+ @method_object = method_object
+ @target = target
+ @_pry_ = _pry_
+ end
+
+ # perform the patch
+ def perform_patch
+ lines = method_object.source.lines.to_a
+ lines[0] = definition_line_for_owner(lines[0])
+ source = wrap_for_nesting(wrap_for_owner(Pry::Editor.edit_tempfile_with_content(lines)))
+
+ if method_object.alias?
+ with_method_transaction do
+ _pry_.evaluate_ruby source
+ Pry.binding_for(method_object.owner).eval("alias #{method_object.name} #{original_name}")
+ end
+ else
+ _pry_.evaluate_ruby source
+ end
+ end
+
+ private
+
+ # Run some code ensuring that at the end target#meth_name will not have changed.
+ #
+ # When we're redefining aliased methods we will overwrite the method at the
+ # unaliased name (so that super continues to work). By wrapping that code in a
+ # transation we make that not happen, which means that alias_method_chains, etc.
+ # continue to work.
+ #
+ # @param [String] meth_name The method name before aliasing
+ # @param [Module] target The owner of the method
+ def with_method_transaction
+ target = Pry.binding_for(target)
+ temp_name = "__pry_#{method_object.original_name}__"
+
+ target.eval("alias #{temp_name} #{method_object.original_name}")
+ yield
+ target.eval("alias #{method_object.original_name} #{temp_name}")
+ ensure
+ target.eval("undef #{temp_name}") rescue nil
+ end
+
+ # Update the definition line so that it can be eval'd directly on the Method's
+ # owner instead of from the original context.
+ #
+ # In particular this takes `def self.foo` and turns it into `def foo` so that we
+ # don't end up creating the method on the singleton class of the singleton class
+ # by accident.
+ #
+ # This is necessarily done by String manipulation because we can't find out what
+ # syntax is needed for the argument list by ruby-level introspection.
+ #
+ # @param String The original definition line. e.g. def self.foo(bar, baz=1)
+ # @return String The new definition line. e.g. def foo(bar, baz=1)
+ def definition_line_for_owner(line)
+ if line =~ /^def (?:.*?\.)?#{Regexp.escape(method_object.original_name)}(?=[\(\s;]|$)/
+ "def #{method_object.original_name}#{$'}"
+ else
+ raise CommandError, "Could not find original `def #{method_object.original_name}` line to patch."
+ end
+ end
+
+ # Update the source code so that when it has the right owner when eval'd.
+ #
+ # This (combined with definition_line_for_owner) is backup for the case that
+ # wrap_for_nesting fails, to ensure that the method will stil be defined in
+ # the correct place.
+ #
+ # @param [String] source The source to wrap
+ # @return [String]
+ def wrap_for_owner(source)
+ Thread.current[:__pry_owner__] = method_object.owner
+ source = "Thread.current[:__pry_owner__].class_eval do\n#{source}\nend"
+ end
+
+ # Update the new source code to have the correct Module.nesting.
+ #
+ # This method uses syntactic analysis of the original source file to determine
+ # the new nesting, so that we can tell the difference between:
+ #
+ # class A; def self.b; end; end
+ # class << A; def b; end; end
+ #
+ # The resulting code should be evaluated in the TOPLEVEL_BINDING.
+ #
+ # @param [String] source The source to wrap.
+ # @return [String]
+ def wrap_for_nesting(source)
+ nesting = Pry::Code.from_file(method_object.source_file).nesting_at(method_object.source_line)
+
+ (nesting + [source] + nesting.map{ "end" } + [""]).join("\n")
+ rescue Pry::Indent::UnparseableNestingError => e
+ source
+ end
+ end
+ end
+end