Skip to content

Commit

Permalink
[Fix rubocop#77] Add new Performance/BindCall cop
Browse files Browse the repository at this point in the history
Fixes rubocop#77.

This PR adds new `Performance/BindCall` cop.

In Ruby 2.7, `UnboundMethod#bind_call` have been added.

- https://bugs.ruby-lang.org/issues/15955
- ruby/ruby@83c6a1e

The `bind_call(obj, args, ...)` method is faster than
`bind(obj).call(args, ...)`.

```console
% ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin17]

% gem i benchmark-ips
(snip)

% cat bench.rb
require 'benchmark/ips'

Benchmark.ips do |x|
  umethod = String.instance_method(:start_with?)

  x.report('bind.call') { umethod.bind('hello, world').call('hello') }
  x.report('bind_call') { umethod.bind_call('hello, world', 'hello') }

  x.compare!
end

% ruby bench.rb
Warming up --------------------------------------
           bind.call   122.167k i/100ms
           bind_call   189.749k i/100ms
Calculating -------------------------------------
           bind.call      1.795M (± 1.7%) i/s -      9.040M in   5.039135s
           bind_call      3.756M (± 2.2%) i/s -     18.785M in   5.004573s

Comparison:
           bind_call:  3755510.3 i/s
           bind.call:  1794560.4 i/s - 2.09x  slower
```
  • Loading branch information
koic committed Jan 7, 2020
1 parent 224bf96 commit d1c52cd
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## master (unreleased)

### New features

* [#77](https://github.com/rubocop-hq/rubocop-performance/issues/77): Add new `Performance/BindCall` cop. ([@koic][])

## 1.5.2 (2019-12-25)

### Bug fixes
Expand Down
5 changes: 5 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# This is the default configuration file.

Performance/BindCall:
Description: 'Use `bind_call(obj, args, ...)` instead of `bind(obj).call(args, ...)`.'
Enabled: true
VersionAdded: '1.6'

Performance/Caller:
Description: >-
Use `caller(n..n)` instead of `caller`.
Expand Down
79 changes: 79 additions & 0 deletions lib/rubocop/cop/performance/bind_call.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Performance
# In Ruby 2.7, `UnboundMethod#bind_call` have been added.
#
# This cop identifies places where `bind(obj).call(args, ...)`
# can be replaced by `bind_call(obj, args, ...)`.
#
# The `bind_call(obj, args, ...)` method is faster than
# `bind(obj).call(args, ...)`.
#
# @example
# # bad
# umethod.bind(obj).call(foo, bar)
#
# # good
# umethod.bind_call(obj, foo, bar)
#
class BindCall < Cop
include RangeHelp
extend TargetRubyVersion

minimum_target_ruby_version 2.7

MSG = 'Use `bind_call(%<bind_arg>s, %<call_args>s)` instead of ' \
'`bind(%<bind_arg>s).call(%<call_args>s)`.'

def_node_matcher :bind_with_call_method?, <<~PATTERN
(send
$(send
(send nil? _) :bind
$(...)) :call
$...)
PATTERN

def on_send(node)
bind_with_call_method?(node) do |receiver, bind_arg, call_args_node|
range = correction_range(receiver, node)

call_args = build_call_args(call_args_node)

msg = format(MSG, bind_arg: bind_arg.source, call_args: call_args)

add_offense(node, location: range, message: msg)
end
end

def autocorrect(node)
receiver, bind_arg, call_args_node = bind_with_call_method?(node)

range = correction_range(receiver, node)

call_args = build_call_args(call_args_node)

replacement_method = "bind_call(#{bind_arg.source}, #{call_args})"

lambda do |corrector|
corrector.replace(range, replacement_method)
end
end

private

def correction_range(receiver, node)
location_of_bind = receiver.loc.selector.begin_pos
location_of_call = node.loc.end.end_pos

range_between(location_of_bind, location_of_call)
end

def build_call_args(call_args_node)
call_args_node.map(&:source).join(', ')
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/performance_cops.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require_relative 'performance/bind_call'
require_relative 'performance/caller'
require_relative 'performance/case_when_splat'
require_relative 'performance/casecmp'
Expand Down
1 change: 1 addition & 0 deletions manual/cops.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!-- START_COP_LIST -->
#### Department [Performance](cops_performance.md)

* [Performance/BindCall](cops_performance.md#performancebindcall)
* [Performance/Caller](cops_performance.md#performancecaller)
* [Performance/CaseWhenSplat](cops_performance.md#performancecasewhensplat)
* [Performance/Casecmp](cops_performance.md#performancecasecmp)
Expand Down
24 changes: 24 additions & 0 deletions manual/cops_performance.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Performance

## Performance/BindCall

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Enabled | Yes | Yes | 1.6 | -

In Ruby 2.7, `UnboundMethod#bind_call` have been added.

This cop identifies places where `bind(obj).call(args, ...)`
can be replaced by `bind_call(obj, args, ...)`.

The `bind_call(obj, args, ...)` method is faster than
`bind(obj).call(args, ...)`.

### Examples

```ruby
# bad
umethod.bind(obj).call(foo, bar)

# good
umethod.bind_call(obj, foo, bar)
```

## Performance/Caller

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
Expand Down
54 changes: 54 additions & 0 deletions spec/rubocop/cop/performance/bind_call_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Performance::BindCall, :config do
subject(:cop) { described_class.new(config) }

context 'TargetRubyVersion <= 2.6', :ruby26 do
it 'does not register an offense when using `bind(obj).call(args, ...)`' do
expect_no_offenses(<<~RUBY)
umethod.bind(obj).call(foo, bar)
RUBY
end
end

context 'TargetRubyVersion >= 2.7', :ruby27 do
it 'registers an offense when using `bind(obj).call(args, ...)`' do
expect_offense(<<~RUBY)
umethod.bind(obj).call(foo, bar)
^^^^^^^^^^^^^^^^^^^^^^^^ Use `bind_call(obj, foo, bar)` instead of `bind(obj).call(foo, bar)`.
RUBY

expect_correction(<<~RUBY)
umethod.bind_call(obj, foo, bar)
RUBY
end

it 'registers an offense when the argument of `bind` method is a string' do
expect_offense(<<~RUBY)
umethod.bind('obj').call(foo, bar)
^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `bind_call('obj', foo, bar)` instead of `bind('obj').call(foo, bar)`.
RUBY

expect_correction(<<~RUBY)
umethod.bind_call('obj', foo, bar)
RUBY
end

it 'registers an offense when a argument of `call` method is a string' do
expect_offense(<<~RUBY)
umethod.bind(obj).call('foo', bar)
^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `bind_call(obj, 'foo', bar)` instead of `bind(obj).call('foo', bar)`.
RUBY

expect_correction(<<~RUBY)
umethod.bind_call(obj, 'foo', bar)
RUBY
end

it 'does not register an offense when using `bind_call(obj, args, ...)`' do
expect_no_offenses(<<~RUBY)
umethod.bind_call(obj, foo, bar)
RUBY
end
end
end

0 comments on commit d1c52cd

Please sign in to comment.