Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

repl.rb: guard against negative overhang #1771

Merged
merged 2 commits into from
Oct 4, 2018
Merged

Conversation

DamienRobert
Copy link
Contributor

@DamienRobert DamienRobert commented Aug 25, 2018

When copy pasting into pry a code like this

foo do
	bar #note the tab here
end

then @indent.indent(val) replace the tab by two spaces, and so
overhang = original_val.length - indented_val.length is negative,
which yields an Exception in whitespace = ' ' * overhang.

Guard against this by taking the max with 0.

@kyrylo
Copy link
Member

kyrylo commented Sep 22, 2018

Hey @DamienRobert, thanks for the patch.

I noticed your code snippet is not valid Ruby. Is this on purpose? The description makes sense, however when I tried copy-pasting it in my pry but I don't see any difference between master and this patch. It still looks broken:

[1] pry(main)> def aaa
[1] pry(main)*   foo do
        bar #note the tab here
[1] pry(main)*     bar #note the tab herehere
[1] pry(main)*     donedone
[1] pry(main)*   end

That said, I don't see any exceptions. I'm running it on OSX. Is there any extra information to be shared?

@kyrylo
Copy link
Member

kyrylo commented Sep 22, 2018

I forgot to mention that if you use iTerm2, there's a cool feature, which lets you convert tabs to spaces:

1

@DamienRobert
Copy link
Contributor Author

@kyrylo sorry, the code should be correct ruby, replace the 'done' by 'end'. I guess I wrote the commit message after writing some shell code...

Its strange that you don't get any exception, when I copy paste the snippet (even though it is not valid code) into pry, I get:
/data/dams/opt/pkgmgr/gems/gems/pry-0.11.3/lib/pry/indent.rb:392:in `*': negative argument (ArgumentError)

@kyrylo
Copy link
Member

kyrylo commented Sep 24, 2018

What's the Ruby version you're using?

@kyrylo
Copy link
Member

kyrylo commented Sep 26, 2018

Even when I paste the corrected version of the snippet, I still see broken output. If this is a bug on older Rubies, we probably need a test to confirm that.

@DamienRobert
Copy link
Contributor Author

DamienRobert commented Sep 26, 2018

@kyrylo I use ruby 2.5.1p57, but this is definitively not a bug from ruby, but from pry; when overhang is negative, ' ' * overhang will always give "negative argument (ArgumentError)"

This pr is not about correcting the output, but preventing an exception in pry (which is very annoying since I need to relaunch pry and reload everything if it happens). The exception only occurs when Pry.config.correct_indent is true (and also Pry.config.auto_indent). Maybe this is not enabled in your config?

To correct the output, one would need to replace original_val.length by the space it takes into the screen, ie count every character as one except tabs which count up to the nearest multiple of 8. (Of course there are complications: wide unicode characters should count for 2, anssi sequences for 0, but we only would need a upper bound, it does not matter if correct_indent insert more white space than necessary.)

But I think it is more urgent to prevent the exception than to correct the displayed output.

Copy link
Member

@kyrylo kyrylo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add a test for this? It's not obvious from the code how this can occur. If it's unachievable, I'd be inclined to have a comment instead.

lib/pry/repl.rb Outdated
@@ -107,7 +107,7 @@ def read
if output.tty? && pry.config.correct_indent && Pry::Helpers::BaseHelpers.use_ansi_codes?
output.print @indent.correct_indentation(
current_prompt, indented_val,
original_val.length - indented_val.length
[0, original_val.length - indented_val.length].max
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not trying to be nitpicky here but this code is executed frequently, therefore it's better to make it more performant. I believe if we used a temp variable with an if check it would accomplish that goal. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, but I am not sure I understand your review. This line is executed only once for every read, so I don't know how I could use a temp variable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for not being clear enough. I think we could put the result of evaluation of original_val.length - indented_val.length into a temp variable, then check if it’s larger than 0. If it is, then we return it. If not, then we return nil. With this approach there’s no need to allocate an array. Does it make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, indeed a benchmark shows that an if is slightly faster, but allocating a temporary variable is actually slower:

require 'benchmark'
array = (1..1000000).map { rand(10)-5 }
Benchmark.bmbm do |x|
  x.report("array") { array.each {|x| [x,0].max }}
  x.report("if")  { array.each {|x| x >= 0 ? x : 0  }}
  x.report("if temp var")  { array.each {|x| y=x; y=0 if y <= 0  }}             
end
                 user          system       total         real

array 0.048741 0.000000 0.048741 (0.048804)
if 0.047802 0.000000 0.047802 (0.047877)
if temp var 0.052751 0.000000 0.052751 (0.052818)

Anyway I don't really think microoptimising this code is really important (especially since it follows an io operation), so I changed my version to your suggestion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like you are right. Thanks for running this benchmark and sorry for the nitpicky comment.

@kyrylo
Copy link
Member

kyrylo commented Sep 28, 2018

I am sorry, I got confused and mixed issues together. Unfortunately, I still cannot replicate this error even when I explicitly enable correct_indent and auto_indent. Either way, I can see how this could be an issue, so it's better to patch that.

@DamienRobert
Copy link
Contributor Author

Sure no problem! So your review is about another pr right?
I would have liked to understand why you cannot replicate this error. But in any case this patch is pretty safe and won't do any harm (and in my case fix an exception!)

@kyrylo
Copy link
Member

kyrylo commented Sep 29, 2018

So your review is about another pr right?

My review is still applicable to this PR, this stands the same :)

I would have liked to understand why you cannot replicate this error.

Heh, same here. I'll try to upgrade my Ruby to see if it happens but either way the review is still applicable.

@DamienRobert
Copy link
Contributor Author

DamienRobert commented Sep 29, 2018

So your review is about another pr right?
My review is still applicable to this PR, this stands the same :)

Ok, I replied to your review, but I am not sure what you meant.

Heh, same here. I'll try to upgrade my Ruby to see if it happens but either way the review is still applicable.

What does the following do?

i=Pry::Indent.new
i.indent("def foo") #so that i.@indent_level=1
orig="\t'bar'"
indented=i.indent(orig) # => "  'bar'"
overhang=orig.length - indented.length # overhang = -1

This should replicate a case where overhang will be equal to -1.

By the way when you paste the snippet, do you paste it line by line or at once?
If you paste it line by line, due to pry auto indent feature the negative overhang won't happen.

To simulate a full paste, you can use this snippet

require 'pry'
class Repl < Pry::REPL
  def read_line(_prompt)
    <<-EOS
foo do
	bar #note the tab here
end
    EOS
  end
end
 Repl.new(Pry.new, {}).send(:read) #=> ArgumentError: negative argument

@kyrylo
Copy link
Member

kyrylo commented Oct 1, 2018

What does the following do? By the way when you paste the snippet, do you paste it line by line or at once?

I can now reproduce the problem this way. I used to paste at once but perhaps it depends on how your terminal handles this. I run iTerm 2 but even Terminal.app doesn't show any errors. Either way, thanks for the patch.

Have you considered this comment of mine?

Would it be possible to add a test for this? It's not obvious from the code how this can occur. If it's unachievable, I'd be inclined to have a comment instead.

You basically wrote that test in your last comment, we just need to put that to the test file 😄

P.S. Could you also rebase this on master? I see the "out-of-date branch" message by GitHub here, similarly to your other PR.

Damien Robert added 2 commits October 3, 2018 22:09
When copy pasting into pry a code like this
~~~ ruby
foo do
	bar #note the tab here
done
~~~
then `@indent.indent(val)` replace the tab by two spaces, and so
`overhang = original_val.length - indented_val.length` is negative,
which yields an Exception in `whitespace = ' ' * overhang`.

Guard against this by taking the max with 0.
@DamienRobert
Copy link
Contributor Author

DamienRobert commented Oct 3, 2018

I can now reproduce the problem this way. I used to paste at once but perhaps it depends on how your terminal handles this. I run iTerm 2 but even Terminal.app doesn't show any errors. Either way, thanks for the patch.

Ok, so it seems that these terminals split the paste on new lines, interesting...

You basically wrote that test in your last comment, we just need to put that to the test file smile

I used the ReplTester helper, but I had to do some convolutions (like mock tty?) to force the problematic code path to be taken. I tried and this test does fails without my fix.

P.S. Could you also rebase this on master? I see the "out-of-date branch" message by GitHub here, similarly to your other PR.

Done.

it "should raise no exception when indented with a tab" do
ReplTester.start(correct_indent: true, auto_indent: true) do
output=@pry.config.output
def output.tty?; true; end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we mock using RSpec?
If I am not mistaken, this test modifies the outside environment, which we'd want to prevent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we mock using RSpec?

We could, but there is no reason to, since we don't want to test tty? here.

If I am not mistaken, this test modifies the outside environment, which we'd want to prevent.

No, ReplTester.start invokes redirect_pry_io input, output where output is (essentially) a StringIO.new. So this is a temp object that only lives during the duration of the ReplTester block, and I am only adding to its singleton class, so this won't change the outside environment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't necessarily agree with this reasoning but I have to admit it's not a big deal.

Copy link
Member

@kyrylo kyrylo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. Could you rebase, please? Otherwise, I'll have to hit the "update branch" button, which will create a new junk commit. Happy to merge as soon as this is rebased.

@kyrylo
Copy link
Member

kyrylo commented Oct 4, 2018

Never mind, it turned out to be a setting in the project which requires PRs to be rebased on latest master. I disabled this check because it's very impractical (I have to bug contributors to rebase).

Thanks for your work 👍

@kyrylo kyrylo merged commit e575670 into pry:master Oct 4, 2018
kyrylo added a commit that referenced this pull request Oct 4, 2018
@DamienRobert
Copy link
Contributor Author

Yes I had rebased, but you pushed a new commit in the meantime :) Thanks for merging!

@DamienRobert DamienRobert deleted the fix_indent branch October 4, 2018 13:45
kyrylo added a commit that referenced this pull request Oct 6, 2018
kyrylo added a commit that referenced this pull request Oct 12, 2018
kyrylo added a commit that referenced this pull request Oct 15, 2018
kyrylo added a commit that referenced this pull request Oct 17, 2018
kyrylo added a commit that referenced this pull request Oct 19, 2018
kyrylo added a commit that referenced this pull request Oct 19, 2018
kyrylo added a commit that referenced this pull request Oct 19, 2018
kyrylo added a commit that referenced this pull request Oct 20, 2018
kyrylo added a commit that referenced this pull request Oct 21, 2018
kyrylo added a commit that referenced this pull request Oct 28, 2018
kyrylo added a commit that referenced this pull request Oct 28, 2018
kyrylo added a commit that referenced this pull request Nov 1, 2018
kyrylo added a commit that referenced this pull request Nov 3, 2018
kyrylo added a commit that referenced this pull request Nov 3, 2018
kyrylo added a commit that referenced this pull request Nov 4, 2018
netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this pull request Dec 17, 2018
pkgsr change:
* Remove @Prefix@ from ALTERNATIVES file.

### [v0.12.2][v0.12.2] (November 12, 2018)

#### Bug fixes

* Restore removed deprecations, which were removed by accident due to a bad
  rebase.

### [v0.12.1][v0.12.1] (November 12, 2018)

#### Bug fixes

* Stopped creating a new hash each time `Pry::Prompt#[]` is invoked
  ([#1855](pry/pry#1855))
* Fixed `less` pager not working when it's available
  ([#1861](pry/pry#1861))

### [v0.12.0][v0.12.0] (November 5, 2018)

#### Major changes

* Dropped support for Rubinius ([#1785](pry/pry#1785))

#### Features

* Added a new command, `clear-screen`, that clears the content of the screen Pry
  is running in regardless of platform (Windows or UNIX-like)
  ([#1723](pry/pry#1723))
* Added a new command, `gem-stat`, that prints gem statistics such as gem
  dependencies and downloads ([#1707](pry/pry#1707))
* Added support for nested exceptions for the `wtf` command
  ([#1791](pry/pry#1791))
* Added support for dynamic prompt names
  ([#1833](pry/pry#1833))

  ```rb
  # pryrc
  Pry.config.prompt_name = Pry.lazy { rand(100) }

  # Session
  [1] 80(main)>
  [2] 87(main)>
  [3] 30(main)>
  ```
* Added support for XDG Base Directory Specification
  ([#1609](pry/pry#1609),
  [#1844](pry/pry#1844),
  ([#1848](pry/pry#1848)))
* Removed the `simple-prompt`. Use `change-prompt simple` instead. The
  `list-prompt` command was removed and embedded as `change-prompt --list`
  ([#1849](pry/pry#1849))

#### API changes

* The following methods started accepting the new optional `config` parameter
  ([#1809](pry/pry#1809)):
  * `Pry::Helpers.tablify(things, line_length, config = Pry.config)`
  * `Pry::Helpers.tablify_or_one_line(heading, things, config = Pry.config)`
  * `Pry::Helpers.tablify_to_screen_width(things, options, config = Pry.config)`
  * `Pry::Helpers::Table.new(items, args, config = Pry.config)`

  You are expected to pass a session-local `_pry_.config` instead of the global
  one.

* Added new method `Pry::Config.assign`, for creating a Config non-recursively
  ([#1725](pry/pry#1725))
* Added `Pry.lazy`, which is a helper method for values that need to be
  calculated dynamically. Currently, only `config.prompt_name` supports it
  ([#1833](pry/pry#1833))
* `Pry::Prompt` responds to `.[]`, `.all` & `.add` now. The `Pry::Prompt.add`
  method must be used for implementing custom prompts. See the API in the
  documentation for the class ([#1846](pry/pry#1846))

#### Breaking changes

* Deleted the `Pry::Helpers::Text.bright_default` alias for
  `Pry::Helpers::Text.bold` ([#1795](pry/pry#1795))
* `Pry::Helpers.tablify_to_screen_width(things, options, config = Pry.config)`
  requires `options` or `nil` in place of them.
* `Pry::Helpers::Table.new(items, args, config = Pry.config)` requires `args`
  or `nil` in place of them.
* Completely revamped `Pry::HistoryArray`
  ([#1818](pry/pry#1818)).
  * It's been renamed to `Pry::Ring`
    ([#1817](pry/pry#1817))
  * The implementation has changed and as result, the following methods were
    removed:
    * `Pry::Ring#length` (use `Pry::Ring#count` instead)
    * `#empty?`, `#each`, `#inspect`, `#pop!`, `#to_h`
  * To access old Enumerable methods convert the ring to Array with `#to_a`
  * Fixed indexing for elements (e.g. `_pry_.input_ring[0]` always return some
    element and not `nil`)
* Renamed `Pry.config.prompt_safe_objects` to `Pry.config.prompt_safe_contexts`
* Removed deprecated `Pry::CommandSet#before_command` &
  `Pry::CommandSet#after_command` ([#1838](pry/pry#1838))

#### Deprecations

* Deprecated `_pry_.input_array` & `_pry_.output_array` in favour of
  `_pry_.input_ring` & `_pry_.output_ring` respectively
  ([#1814](pry/pry#1814))
* Deprecated `Pry::Command#text`. Please use `#black`, `#white`, etc. directly
  instead (as you would with helper functions from `BaseHelpers` and
  `CommandHelpers`) ([#1701](pry/pry#1701))
* Deprecated `_pry_.input_array` & `_pry_.output_array` in favour of
  `_pry_.input_ring` and `_pry_.output_ring` respectively
  ([#1817](pry/pry#1817))
* Deprecated `Pry::Platform`. Use `Pry::Helpers::Platform` instead. Note that
  `Pry::Helpers::BaseHelpers` still includes the `Platform` methods but emits a
  warning. You must switch to `Pry::Helpers::Platform` in your code
  ([#1838](pry/pry#1838),
  ([#1845](pry/pry#1845)))
* Deprecated `Pry::Prompt::MAP`. You should use `Pry::Prompt.all` instead to
  access the same map ([#1846](pry/pry#1846))

#### Bug fixes

* Fixed a bug where `cd Hash.new` reported `self` as an instance of Pry::Config
  in the prompt ([#1725](pry/pry#1725))
* Silenced the `Could not find files for the given pattern(s)` error message
  coming from `where` on Windows, when `less` or another pager is not installed
  ([#1767](pry/pry#1767))
* Fixed possible double loading of Pry plugins' `cli.rb` on Ruby (>= 2.4) due to
  [the `realpath` changes while invoking
  `require`](https://bugs.ruby-lang.org/issues/10222)
  ([#1762](pry/pry#1762),
  [#1774](pry/pry#1762))
* Fixed `NoMethodError` on code objects that have a comment but no source when
  invoking `show-source` ([#1779](pry/pry#1779))
* Fixed `negative argument (ArgumentError)` upon pasting code with tabs, which
  used to confuse automatic indentation
  ([#1771](pry/pry#1771))
* Fixed Pry not being able to load history on Ruby 2.4.4+ when it contains the
  null character ([#1789](pry/pry#1789))
* Fixed Pry raising errors on `cd`'ing into some objects that redefine
  `method_missing` and `respond_to?`
  ([#1811](pry/pry#1811))
* Fixed bug when indentation leaves parts of input after pressing enter when
  Readline is enabled with mode indicators for vi mode
  ([#1813](pry/pry#1813),
  [#1820](pry/pry#1820),
  [#1825](pry/pry#1825))
* Fixed `edit` not writing to history
  ([#1749](pry/pry#1749))

#### Other changes

* Deprecated the `Data` constant to match Ruby 2.5 in the `ls` command
  ([#1731](pry/pry#1731))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants