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

Add 'Style/UnpackFirst' cop #5643

Merged
merged 1 commit into from
Mar 19, 2018
Merged

Add 'Style/UnpackFirst' cop #5643

merged 1 commit into from
Mar 19, 2018

Conversation

bdewater
Copy link
Contributor

@bdewater bdewater commented Mar 7, 2018

This is a shorter way of getting the data you will need most of the time when unpacking a string. It was introduced in Ruby 2.4: https://ruby-doc.org/core-2.4.0/String.html#method-i-unpack1

While unpack1 is also faster (it skips creating the intermediate array object), I consider it to be more style related like Style/SymbolProc. The bad form runs 3+ million iterations per second on my laptop, which is not a performance bottleneck for most Ruby apps.


Before submitting the PR make sure the following are checked:

  • Wrote [good commit messages]
  • Feature branch is up-to-date with master (if not - rebase it).
  • Squashed related commits together.
  • Added tests.
  • Added an entry to the Changelog if the new code introduces user-observable changes. See changelog entry format.
  • The PR relates to only one subject with a clear title
    and description in grammatically correct, complete sentences.
  • Run rake default or rake parallel. It executes all tests and RuboCop for itself, and generates the documentation.

@bdewater bdewater force-pushed the unpack-cop branch 5 times, most recently from db347b0 to 9664d49 Compare March 9, 2018 15:24

minimum_target_ruby_version 2.4

MSG = 'Use `unpack1(%<format>s)` instead of '\
Copy link
Collaborator

Choose a reason for hiding this comment

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

The message might be more informative with the receiver of unpack in it.

@bbatsov
Copy link
Collaborator

bbatsov commented Mar 12, 2018

The cop looks good overall. I wonder of the generic name Unpack shouldn't be changed with UnpackFirst, though.

@bdewater bdewater force-pushed the unpack-cop branch 2 times, most recently from b13f0a2 to 01e22fe Compare March 12, 2018 13:45
@bdewater bdewater changed the title Add 'Style/Unpack' cop Add 'Style/UnpackFirst' cop Mar 12, 2018
@bdewater
Copy link
Contributor Author

Thanks for the feedback. Agreed on both points and just pushed the changes 😄

@Drenmi
Copy link
Collaborator

Drenmi commented Mar 12, 2018

There's one (not very commonly used) syntax that's unaccounted for. Accessing like so:

foo.[](0)

Not sure if it's worth accounting for this in the first version, unless it can be easily handled. 🙂

@bdewater
Copy link
Contributor Author

TIL! I'll look at adding at later today.

@rrosenblum
Copy link
Contributor

@Drenmi Other cops like Performance/Sample do not currently support checking the style foo.[](0). I am starting to see that style used more with safe navigation, foo&.[](0). Is it worth adding support for that style? Regardless, it seems like a good candidate for a new style cop.

@bdewater
Copy link
Contributor Author

@Drenmi I added that syntax (and found some others 😄) but was not able to get the message completely right 🤔

@rrosenblum
Copy link
Contributor

While unpack1 is also faster, I consider it to be more style related like Style/SymbolProc. The bad form runs 1+ million iterations per second on my laptop, which is not a performance bottleneck for most Ruby apps.

It is a tough call between classifying as style vs performance some times. Like you said, this unlikely to be a performance bottleneck, but there is a clear performance gain in using unpack1. Regardless of how we classify this cop, I encourage you to add an example to https://github.com/JuanitoFatas/fast-ruby

[2] pry(main)> FOO_BAR = 'foo bar'.freeze
[5] pry(main)> Benchmark.ips do |x|
[5] pry(main)*   x.report('unpack[0]') { FOO_BAR.unpack('Z*Z*')[0] }
[5] pry(main)*   x.report('unpack1') { FOO_BAR.unpack1('Z*Z*') }
[5] pry(main)*   x.compare!
[5] pry(main)* end
Warming up --------------------------------------
           unpack[0]   169.518k i/100ms
             unpack1   218.699k i/100ms
Calculating -------------------------------------
           unpack[0]      3.356M (± 5.3%) i/s -     16.782M in   5.015638s
             unpack1      5.533M (± 5.5%) i/s -     27.775M in   5.036752s

Comparison:
             unpack1:  5532904.7 i/s
           unpack[0]:  3355751.7 i/s - 1.65x  slower

MSG = 'Use `String#unpack1(%<format>s)` instead of '\
'`String#unpack(%<format>s)%<method>s`.'.freeze

def_node_matcher :unpack_and_first_element?, <<-PATTERN
Copy link
Contributor

Choose a reason for hiding this comment

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

Thoughts for later: I wonder if we could capture this into a reusable form to make it easier for other cops to also identify that they are grabbing the first element from an array. Performance/Sample also looks for grabbing the first element, but is missing checks for at and take.

@rrosenblum
Copy link
Contributor

@bdewater Here is a fix for the messaging, https://gist.github.com/rrosenblum/240d8343c4aa7377a73f5170ddbaf6c3.

@bdewater bdewater force-pushed the unpack-cop branch 2 times, most recently from 215a9ec to 3e3cd67 Compare March 15, 2018 16:01
@bdewater
Copy link
Contributor Author

@rrosenblum thank you very much! 🙇🏼‍♂️ Learned something new today 😄 I believe this is now ready to be merged.

@@ -2010,6 +2010,12 @@ Style/UnneededPercentQ:
StyleGuide: '#percent-q'
Enabled: true

Style/UnpackFirst:
Description: >-
Checks for accessing the first element of unpack's result
Copy link
Contributor

Choose a reason for hiding this comment

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

unpack and unpack1 should be surrounded by back-ticks (`).

module RuboCop
module Cop
module Style
# This cop checks for accessing the first element of *unpack* which can
Copy link
Contributor

Choose a reason for hiding this comment

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

Use back-ticks instead of asterisks around unpack and unpack1.


def autocorrect(node)
lambda do |corrector|
corrector.remove(node.loc.dot) if node.method_name == :first
Copy link
Contributor

Choose a reason for hiding this comment

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

There are no tests for the auto-correction of take, slice, and at. I think the auto-correction is going to produce incorrect code when one of those is used.


minimum_target_ruby_version 2.4

MSG = 'Use `String#unpack1(%<format>s)` instead of '\
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not <%format>s.unpack1? That reads better to me. Same for the other part of the message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because format is the argument to unpack(1), not the string to be unpacked.

I'm happy to leave the String prefix off, but I thought that was the suggestion you made in your earlier comment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I meant I wanted the message to say something like Use foo.unpack1instead offoo...`. When using the actual code that you're checking in the messages they are easier to understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I updated the message to show this.

This is a shorter way of getting the data you will need most of the time when unpacking a string.

While unpack1 is also faster (it skips creating the intermediate array object), I consider it to be more style related like Style/SymbolProc. The bad form runs 3+ million iterations per second, which is not a performance bottleneck for most Ruby apps.
This was referenced Mar 21, 2018
@greysteil
Copy link
Contributor

Guys, so sorry for the cross-reference spam above. Only realised it was happening today (was due to a change we made last week) and fixed it immediately in dependabot/dependabot-core@2bd343b. Unfortunately there's no way for us to delete the old cross-references, so they'll stay there as a reminder of our mistake 😞

@bbatsov
Copy link
Collaborator

bbatsov commented Mar 23, 2018

@greysteil No worries!

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.

5 participants