1
0
Fork 0
mirror of https://github.com/pry/pry.git synced 2022-11-09 12:35:05 -05:00

Make object path resolution more robust (fix #957)

This fixes #957 and should make object path resolution more predictable
in general. Instead of splitting the path on "/" before doing any
parsing, we use `StringScanner` and `complete_expression?` to scan
through the string looking for complete slash-delimited Ruby
expressions.

It also turned out that separating the code for handling "-" from the
path-resolution code simplified things a lot. It doesn't really make
sense for "-" to be in there anyway, since paths like "foo/-/bar" don't
mean anything.
This commit is contained in:
Ryan Fitzgerald 2014-04-28 00:08:23 -07:00
parent 2467871169
commit a9a49ee8a3
4 changed files with 76 additions and 64 deletions

View file

@ -21,9 +21,19 @@ class Pry
def process
state.old_stack ||= []
stack, state.old_stack =
ObjectPath.new(arg_string, _pry_.binding_stack, state.old_stack).resolve
_pry_.binding_stack = stack if stack
if arg_string.strip == "-"
unless state.old_stack.empty?
_pry_.binding_stack, state.old_stack = state.old_stack, _pry_.binding_stack
end
else
stack = ObjectPath.new(arg_string, _pry_.binding_stack).resolve
if stack && stack != _pry_.binding_stack
state.old_stack = _pry_.binding_stack
_pry_.binding_stack = stack
end
end
end
end

View file

@ -60,8 +60,7 @@ class Pry::InputCompleter
if path.call.empty?
target = options[:target]
else
target, _ = Pry::ObjectPath.new(path.call, @pry.binding_stack).resolve
target = target.last
target = Pry::ObjectPath.new(path.call, @pry.binding_stack).resolve.last
end
begin

View file

@ -11,79 +11,72 @@ class Pry
# Object paths are mostly relevant in the context of the `cd` command.
# @see https://github.com/pry/pry/wiki/State-navigation
class ObjectPath
SPECIAL_TERMS = ["", "::", ".", ".."]
# @param [String] path_string The object path expressed as a string.
# @param [Array<Binding>] current_stack The current state of the binding
# stack.
# @param [Array<Binding>] old_stack The previous state of the binding
# stack, if applicable.
def initialize(path_string, current_stack, old_stack=[])
def initialize(path_string, current_stack)
@path_string = path_string
@current_stack = current_stack
@old_stack = old_stack
end
# @return [Array(Array<Binding>, Array<Binding>)] an array
# containing two elements, the new binding stack and the old binding
# stack.
# @return [Array<Binding>] a new stack resulting from applying the given
# path to the current stack.
def resolve
# Extract command arguments. Delete blank arguments like " ", but
# don't delete empty strings like "".
path = @path_string.split(/\//).delete_if { |a| a =~ /\A\s+\z/ }
stack = @current_stack.dup
state_old_stack = @old_stack
scanner = StringScanner.new(@path_string.strip)
stack = @current_stack.dup
# Special case when we only get a single "/", return to root.
if path.empty?
state_old_stack = stack.dup unless @old_stack.empty?
stack = [stack.first]
end
begin
next_segment = ""
path.each_with_index do |context, i|
begin
case context.chomp
when ""
state_old_stack = stack.dup
stack = [stack.first]
when "::"
state_old_stack = stack.dup
stack.push(TOPLEVEL_BINDING)
when "."
next
when ".."
unless stack.size == 1
# Don't rewrite old_stack if we're in complex expression
# (e.g.: `cd 1/2/3/../4).
state_old_stack = stack.dup if path.first == ".."
stack.pop
end
when "-"
unless @old_stack.empty?
# Interchange current stack and old stack with each other.
stack, state_old_stack = state_old_stack, stack
end
loop do
# Scan for as long as we don't see a slash
next_segment << scanner.scan(/[^\/]*/)
if complete?(next_segment) || scanner.eos?
scanner.getch # consume the slash
break
else
state_old_stack = stack.dup if i == 0
stack.push(Pry.binding_for(stack.last.eval(context)))
end
rescue RescuableException => e
# Restore old stack to its initial values.
state_old_stack = @old_stack
msg = [
"Bad object path: #{@path_string}.",
"Failed trying to resolve: #{context}.",
e.inspect
].join(' ')
CommandError.new(msg).tap do |err|
err.set_backtrace e.backtrace
raise err
next_segment << scanner.getch # append the slash
end
end
end
[stack, state_old_stack]
case next_segment.chomp
when ""
stack = [stack.first]
when "::"
stack.push(TOPLEVEL_BINDING)
when "."
next
when ".."
stack.pop unless stack.size == 1
else
stack.push(Pry.binding_for(stack.last.eval(next_segment)))
end
rescue RescuableException => e
return handle_failure(next_segment, e)
end until scanner.eos?
stack
end
private
def complete?(segment)
SPECIAL_TERMS.include?(segment) || Pry::Code.complete_expression?(segment)
end
def handle_failure(context, err)
msg = [
"Bad object path: #{@path_string.inspect}",
"Failed trying to resolve: #{context.inspect}",
"Exception: #{err.inspect}"
].join("\n")
raise CommandError.new(msg).tap { |e|
e.set_backtrace err.backtrace
}
end
end
end

View file

@ -240,6 +240,16 @@ describe 'cd' do
@t.assert_binding_stack [@o]
end
it 'can cd into an expression containing a string with slashes in it' do
@t.eval 'cd ["http://google.com"]'
@t.eval('self').should == ["http://google.com"]
end
it 'can cd into an expression with division in it' do
@t.eval 'cd (10/2)/even?'
@t.eval('self').should == false
end
# Regression test for ticket #516.
# FIXME: This is actually broken.
# it 'should be able to cd into the Object BasicObject' do