Add rubocop-performance gem.

This commit is contained in:
Mike McQuaid 2019-04-30 08:44:10 +01:00
parent 1d334e270e
commit 738f4689a5
No known key found for this signature in database
GPG Key ID: 48A898132FD8EE70
32 changed files with 2491 additions and 0 deletions

View File

@ -20,5 +20,6 @@ gem "backports"
gem "concurrent-ruby"
gem "mechanize"
gem "plist"
gem "rubocop-performance"
gem "rubocop-rspec"
gem "ruby-macho"

View File

@ -87,6 +87,8 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.6)
rubocop-performance (1.1.0)
rubocop (>= 0.67.0)
rubocop-rspec (1.32.0)
rubocop (>= 0.60.0)
ruby-macho (2.2.0)
@ -128,6 +130,7 @@ DEPENDENCIES
rspec-retry
rspec-wait
rubocop
rubocop-performance
rubocop-rspec
ruby-macho
simplecov

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'rubocop/performance'
require_relative 'rubocop/performance/version'
require_relative 'rubocop/performance/inject'
RuboCop::Performance::Inject.defaults!
require_relative 'rubocop/cop/performance_cops'

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies places where `caller[n]`
# can be replaced by `caller(n..n).first`.
#
# @example
# # bad
# caller[1]
# caller.first
# caller_locations[1]
# caller_locations.first
#
# # good
# caller(2..2).first
# caller(1..1).first
# caller_locations(2..2).first
# caller_locations(1..1).first
class Caller < Cop
MSG_BRACE = 'Use `%<method>s(%<n>d..%<n>d).first`' \
' instead of `%<method>s[%<m>d]`.'.freeze
MSG_FIRST = 'Use `%<method>s(%<n>d..%<n>d).first`' \
' instead of `%<method>s.first`.'.freeze
def_node_matcher :slow_caller?, <<-PATTERN
{
(send nil? {:caller :caller_locations})
(send nil? {:caller :caller_locations} int)
}
PATTERN
def_node_matcher :caller_with_scope_method?, <<-PATTERN
{
(send #slow_caller? :first)
(send #slow_caller? :[] int)
}
PATTERN
def on_send(node)
return unless caller_with_scope_method?(node)
add_offense(node)
end
private
def message(node)
method_name = node.receiver.method_name
caller_arg = node.receiver.first_argument
n = caller_arg ? int_value(caller_arg) : 1
if node.method_name == :[]
m = int_value(node.first_argument)
n += m
format(MSG_BRACE, n: n, m: m, method: method_name)
else
format(MSG_FIRST, n: n, method: method_name)
end
end
def int_value(node)
node.children[0]
end
end
end
end
end

View File

@ -0,0 +1,177 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# Reordering `when` conditions with a splat to the end
# of the `when` branches can improve performance.
#
# Ruby has to allocate memory for the splat expansion every time
# that the `case` `when` statement is run. Since Ruby does not support
# fall through inside of `case` `when`, like some other languages do,
# the order of the `when` branches should not matter. By placing any
# splat expansions at the end of the list of `when` branches we will
# reduce the number of times that memory has to be allocated for
# the expansion. The exception to this is if multiple of your `when`
# conditions can be true for any given condition. A likely scenario for
# this defining a higher level when condition to override a condition
# that is inside of the splat expansion.
#
# This is not a guaranteed performance improvement. If the data being
# processed by the `case` condition is normalized in a manner that favors
# hitting a condition in the splat expansion, it is possible that
# moving the splat condition to the end will use more memory,
# and run slightly slower.
#
# @example
# # bad
# case foo
# when *condition
# bar
# when baz
# foobar
# end
#
# case foo
# when *[1, 2, 3, 4]
# bar
# when 5
# baz
# end
#
# # good
# case foo
# when baz
# foobar
# when *condition
# bar
# end
#
# case foo
# when 1, 2, 3, 4
# bar
# when 5
# baz
# end
class CaseWhenSplat < Cop
include Alignment
include RangeHelp
MSG = 'Reordering `when` conditions with a splat to the end ' \
'of the `when` branches can improve performance.'.freeze
ARRAY_MSG = 'Pass the contents of array literals ' \
'directly to `when` conditions.'.freeze
def on_case(case_node)
when_conditions = case_node.when_branches.flat_map(&:conditions)
splat_offenses(when_conditions).reverse_each do |condition|
range = condition.parent.loc.keyword.join(condition.source_range)
variable, = *condition
message = variable.array_type? ? ARRAY_MSG : MSG
add_offense(condition.parent, location: range, message: message)
end
end
def autocorrect(when_node)
lambda do |corrector|
if needs_reorder?(when_node)
reorder_condition(corrector, when_node)
else
inline_fix_branch(corrector, when_node)
end
end
end
private
def replacement(conditions)
reordered = conditions.partition(&:splat_type?).reverse
reordered.flatten.map(&:source).join(', ')
end
def inline_fix_branch(corrector, when_node)
conditions = when_node.conditions
range = range_between(conditions[0].loc.expression.begin_pos,
conditions[-1].loc.expression.end_pos)
corrector.replace(range, replacement(conditions))
end
def reorder_condition(corrector, when_node)
when_branches = when_node.parent.when_branches
return if when_branches.one?
corrector.remove(when_branch_range(when_node))
corrector.insert_after(when_branches.last.source_range,
reordering_correction(when_node))
end
def reordering_correction(when_node)
new_condition = replacement(when_node.conditions)
if same_line?(when_node, when_node.body)
new_condition_with_then(when_node, new_condition)
else
new_branch_without_then(when_node, new_condition)
end
end
def when_branch_range(when_node)
next_branch =
when_node.parent.when_branches[when_node.branch_index + 1]
range_between(when_node.source_range.begin_pos,
next_branch.source_range.begin_pos)
end
def new_condition_with_then(node, new_condition)
"\n#{indent_for(node)}when " \
"#{new_condition} then #{node.body.source}"
end
def new_branch_without_then(node, new_condition)
"\n#{indent_for(node)}when #{new_condition}" \
"\n#{indent_for(node.body)}#{node.body.source}"
end
def indent_for(node)
' ' * node.loc.column
end
def splat_offenses(when_conditions)
found_non_splat = false
offenses = when_conditions.reverse.map do |condition|
found_non_splat ||= non_splat?(condition)
next if non_splat?(condition)
condition if found_non_splat
end
offenses.compact
end
def non_splat?(condition)
variable, = *condition
(condition.splat_type? && variable.array_type?) ||
!condition.splat_type?
end
def needs_reorder?(when_node)
following_branches =
when_node.parent.when_branches[(when_node.branch_index + 1)..-1]
following_branches.any? do |when_branch|
when_branch.conditions.any? do |condition|
non_splat?(condition)
end
end
end
end
end
end
end

View File

@ -0,0 +1,108 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies places where a case-insensitive string comparison
# can better be implemented using `casecmp`.
#
# @example
# # bad
# str.downcase == 'abc'
# str.upcase.eql? 'ABC'
# 'abc' == str.downcase
# 'ABC'.eql? str.upcase
# str.downcase == str.downcase
#
# # good
# str.casecmp('ABC').zero?
# 'abc'.casecmp(str).zero?
class Casecmp < Cop
MSG = 'Use `%<good>s` instead of `%<bad>s`.'.freeze
CASE_METHODS = %i[downcase upcase].freeze
def_node_matcher :downcase_eq, <<-PATTERN
(send
$(send _ ${:downcase :upcase})
${:== :eql? :!=}
${str (send _ {:downcase :upcase} ...) (begin str)})
PATTERN
def_node_matcher :eq_downcase, <<-PATTERN
(send
{str (send _ {:downcase :upcase} ...) (begin str)}
${:== :eql? :!=}
$(send _ ${:downcase :upcase}))
PATTERN
def_node_matcher :downcase_downcase, <<-PATTERN
(send
$(send _ ${:downcase :upcase})
${:== :eql? :!=}
$(send _ ${:downcase :upcase}))
PATTERN
def on_send(node)
return unless downcase_eq(node) || eq_downcase(node)
return unless (parts = take_method_apart(node))
_, _, arg, variable = parts
good_method = build_good_method(arg, variable)
add_offense(
node,
message: format(MSG, good: good_method, bad: node.source)
)
end
def autocorrect(node)
return unless (parts = take_method_apart(node))
receiver, method, arg, variable = parts
correction(node, receiver, method, arg, variable)
end
private
def take_method_apart(node)
if downcase_downcase(node)
receiver, method, rhs = *node
arg, = *rhs
elsif downcase_eq(node)
receiver, method, arg = *node
elsif eq_downcase(node)
arg, method, receiver = *node
else
return
end
variable, = *receiver
[receiver, method, arg, variable]
end
def correction(node, _receiver, method, arg, variable)
lambda do |corrector|
corrector.insert_before(node.loc.expression, '!') if method == :!=
replacement = build_good_method(arg, variable)
corrector.replace(node.loc.expression, replacement)
end
end
def build_good_method(arg, variable)
# We want resulting call to be parenthesized
# if arg already includes one or more sets of parens, don't add more
# or if method call already used parens, again, don't add more
if arg.send_type? || !parentheses?(arg)
"#{variable.source}.casecmp(#{arg.source}).zero?"
else
"#{variable.source}.casecmp#{arg.source}.zero?"
end
end
end
end
end
end

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop is used to identify usages of
# @example
# # bad
# array = ["a", "b", "c"]
# array.compact.flatten.map { |x| x.downcase }
#
# Each of these methods (`compact`, `flatten`, `map`) will generate a
# new intermediate array that is promptly thrown away. Instead it is
# faster to mutate when we know it's safe.
#
# @example
# # good.
# array = ["a", "b", "c"]
# array.compact!
# array.flatten!
# array.map! { |x| x.downcase }
# array
class ChainArrayAllocation < Cop
include RangeHelp
# These methods return a new array but only sometimes. They must be
# called with an argument. For example:
#
# [1,2].first # => 1
# [1,2].first(1) # => [1]
#
RETURN_NEW_ARRAY_WHEN_ARGS = ':first :last :pop :sample :shift '.freeze
# These methods return a new array only when called without a block.
RETURNS_NEW_ARRAY_WHEN_NO_BLOCK = ':zip :product '.freeze
# These methods ALWAYS return a new array
# after they're called it's safe to mutate the the resulting array
ALWAYS_RETURNS_NEW_ARRAY = ':* :+ :- :collect :compact :drop '\
':drop_while :flatten :map :reject ' \
':reverse :rotate :select :shuffle :sort ' \
':take :take_while :transpose :uniq ' \
':values_at :| '.freeze
# These methods have a mutation alternative. For example :collect
# can be called as :collect!
HAS_MUTATION_ALTERNATIVE = ':collect :compact :flatten :map :reject '\
':reverse :rotate :select :shuffle :sort '\
':uniq '.freeze
MSG = 'Use unchained `%<method>s!` and `%<second_method>s!` '\
'(followed by `return array` if required) instead of chaining '\
'`%<method>s...%<second_method>s`.'.freeze
def_node_matcher :flat_map_candidate?, <<-PATTERN
{
(send (send _ ${#{RETURN_NEW_ARRAY_WHEN_ARGS}} {int lvar ivar cvar gvar}) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
(send (block (send _ ${#{ALWAYS_RETURNS_NEW_ARRAY} }) ...) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
(send (send _ ${#{ALWAYS_RETURNS_NEW_ARRAY + RETURNS_NEW_ARRAY_WHEN_NO_BLOCK}} ...) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
}
PATTERN
def on_send(node)
flat_map_candidate?(node) do |fm, sm, _|
range = range_between(
node.loc.dot.begin_pos,
node.source_range.end_pos
)
add_offense(
node,
location: range,
message: format(MSG, method: fm, second_method: sm)
)
end
end
end
end
end
end

View File

@ -0,0 +1,122 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies places where `sort { |a, b| a.foo <=> b.foo }`
# can be replaced by `sort_by(&:foo)`.
# This cop also checks `max` and `min` methods.
#
# @example
# # bad
# array.sort { |a, b| a.foo <=> b.foo }
# array.max { |a, b| a.foo <=> b.foo }
# array.min { |a, b| a.foo <=> b.foo }
# array.sort { |a, b| a[:foo] <=> b[:foo] }
#
# # good
# array.sort_by(&:foo)
# array.sort_by { |v| v.foo }
# array.sort_by do |var|
# var.foo
# end
# array.max_by(&:foo)
# array.min_by(&:foo)
# array.sort_by { |a| a[:foo] }
class CompareWithBlock < Cop
include RangeHelp
MSG = 'Use `%<compare_method>s_by%<instead>s` instead of ' \
'`%<compare_method>s { |%<var_a>s, %<var_b>s| %<str_a>s ' \
'<=> %<str_b>s }`.'.freeze
def_node_matcher :compare?, <<-PATTERN
(block
$(send _ {:sort :min :max})
(args (arg $_a) (arg $_b))
$send)
PATTERN
def_node_matcher :replaceable_body?, <<-PATTERN
(send
(send (lvar %1) $_method $...)
:<=>
(send (lvar %2) _method $...))
PATTERN
def on_block(node)
compare?(node) do |send, var_a, var_b, body|
replaceable_body?(body, var_a, var_b) do |method, args_a, args_b|
return unless slow_compare?(method, args_a, args_b)
range = compare_range(send, node)
add_offense(
node,
location: range,
message: message(send, method, var_a, var_b, args_a)
)
end
end
end
def autocorrect(node)
lambda do |corrector|
send, var_a, var_b, body = compare?(node)
method, arg, = replaceable_body?(body, var_a, var_b)
replacement =
if method == :[]
"#{send.method_name}_by { |a| a[#{arg.first.source}] }"
else
"#{send.method_name}_by(&:#{method})"
end
corrector.replace(compare_range(send, node),
replacement)
end
end
private
def slow_compare?(method, args_a, args_b)
return false unless args_a == args_b
if method == :[]
return false unless args_a.size == 1
key = args_a.first
return false unless %i[sym str int].include?(key.type)
else
return false unless args_a.empty?
end
true
end
# rubocop:disable Metrics/MethodLength
def message(send, method, var_a, var_b, args)
compare_method = send.method_name
if method == :[]
key = args.first
instead = " { |a| a[#{key.source}] }"
str_a = "#{var_a}[#{key.source}]"
str_b = "#{var_b}[#{key.source}]"
else
instead = "(&:#{method})"
str_a = "#{var_a}.#{method}"
str_b = "#{var_b}.#{method}"
end
format(MSG, compare_method: compare_method,
instead: instead,
var_a: var_a,
var_b: var_b,
str_a: str_a,
str_b: str_b)
end
# rubocop:enable Metrics/MethodLength
def compare_range(send, node)
range_between(send.loc.selector.begin_pos, node.loc.end.end_pos)
end
end
end
end
end

View File

@ -0,0 +1,102 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop is used to identify usages of `count` on an `Enumerable` that
# follow calls to `select` or `reject`. Querying logic can instead be
# passed to the `count` call.
#
# @example
# # bad
# [1, 2, 3].select { |e| e > 2 }.size
# [1, 2, 3].reject { |e| e > 2 }.size
# [1, 2, 3].select { |e| e > 2 }.length
# [1, 2, 3].reject { |e| e > 2 }.length
# [1, 2, 3].select { |e| e > 2 }.count { |e| e.odd? }
# [1, 2, 3].reject { |e| e > 2 }.count { |e| e.even? }
# array.select(&:value).count
#
# # good
# [1, 2, 3].count { |e| e > 2 }
# [1, 2, 3].count { |e| e < 2 }
# [1, 2, 3].count { |e| e > 2 && e.odd? }
# [1, 2, 3].count { |e| e < 2 && e.even? }
# Model.select('field AS field_one').count
# Model.select(:value).count
#
# `ActiveRecord` compatibility:
# `ActiveRecord` will ignore the block that is passed to `count`.
# Other methods, such as `select`, will convert the association to an
# array and then run the block on the array. A simple work around to
# make `count` work with a block is to call `to_a.count {...}`.
#
# Example:
# Model.where(id: [1, 2, 3].select { |m| m.method == true }.size
#
# becomes:
#
# Model.where(id: [1, 2, 3]).to_a.count { |m| m.method == true }
class Count < Cop
include SafeMode
include RangeHelp
MSG = 'Use `count` instead of `%<selector>s...%<counter>s`.'.freeze
def_node_matcher :count_candidate?, <<-PATTERN
{
(send (block $(send _ ${:select :reject}) ...) ${:count :length :size})
(send $(send _ ${:select :reject} (:block_pass _)) ${:count :length :size})
}
PATTERN
def on_send(node)
return if rails_safe_mode?
count_candidate?(node) do |selector_node, selector, counter|
return unless eligible_node?(node)
range = source_starting_at(node) do
selector_node.loc.selector.begin_pos
end
add_offense(node,
location: range,
message: format(MSG, selector: selector,
counter: counter))
end
end
def autocorrect(node)
selector_node, selector, _counter = count_candidate?(node)
selector_loc = selector_node.loc.selector
return if selector == :reject
range = source_starting_at(node) { |n| n.loc.dot.begin_pos }
lambda do |corrector|
corrector.remove(range)
corrector.replace(selector_loc, 'count')
end
end
private
def eligible_node?(node)
!(node.parent && node.parent.block_type?)
end
def source_starting_at(node)
begin_pos = if block_given?
yield node
else
node.source_range.begin_pos
end
range_between(begin_pos, node.source_range.end_pos)
end
end
end
end
end

View File

@ -0,0 +1,110 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop is used to identify usages of
# `select.first`, `select.last`, `find_all.first`, and `find_all.last`
# and change them to use `detect` instead.
#
# @example
# # bad
# [].select { |item| true }.first
# [].select { |item| true }.last
# [].find_all { |item| true }.first
# [].find_all { |item| true }.last
#
# # good
# [].detect { |item| true }
# [].reverse.detect { |item| true }
#
# `ActiveRecord` compatibility:
# `ActiveRecord` does not implement a `detect` method and `find` has its
# own meaning. Correcting ActiveRecord methods with this cop should be
# considered unsafe.
class Detect < Cop
include SafeMode
MSG = 'Use `%<prefer>s` instead of ' \
'`%<first_method>s.%<second_method>s`.'.freeze
REVERSE_MSG = 'Use `reverse.%<prefer>s` instead of ' \
'`%<first_method>s.%<second_method>s`.'.freeze
def_node_matcher :detect_candidate?, <<-PATTERN
{
(send $(block (send _ {:select :find_all}) ...) ${:first :last} $...)
(send $(send _ {:select :find_all} ...) ${:first :last} $...)
}
PATTERN
def on_send(node)
return if rails_safe_mode?
detect_candidate?(node) do |receiver, second_method, args|
return unless args.empty?
return unless receiver
receiver, _args, body = *receiver if receiver.block_type?
return if accept_first_call?(receiver, body)
register_offense(node, receiver, second_method)
end
end
def autocorrect(node)
receiver, first_method = *node
replacement = if first_method == :last
"reverse.#{preferred_method}"
else
preferred_method
end
first_range = receiver.source_range.end.join(node.loc.selector)
receiver, _args, _body = *receiver if receiver.block_type?
lambda do |corrector|
corrector.remove(first_range)
corrector.replace(receiver.loc.selector, replacement)
end
end
private
def accept_first_call?(receiver, body)
caller, _first_method, args = *receiver
# check that we have usual block or block pass
return true if body.nil? && (args.nil? || !args.block_pass_type?)
lazy?(caller)
end
def register_offense(node, receiver, second_method)
_caller, first_method, _args = *receiver
range = receiver.loc.selector.join(node.loc.selector)
message = second_method == :last ? REVERSE_MSG : MSG
formatted_message = format(message, prefer: preferred_method,
first_method: first_method,
second_method: second_method)
add_offense(node, location: range, message: formatted_message)
end
def preferred_method
config.for_cop('Style/CollectionMethods') \
['PreferredMethods']['detect'] || 'detect'
end
def lazy?(node)
return false unless node
receiver, method, _args = *node
method == :lazy && !receiver.nil?
end
end
end
end
end

View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop checks for double `#start_with?` or `#end_with?` calls
# separated by `||`. In some cases such calls can be replaced
# with an single `#start_with?`/`#end_with?` call.
#
# @example
# # bad
# str.start_with?("a") || str.start_with?(Some::CONST)
# str.start_with?("a", "b") || str.start_with?("c")
# str.end_with?(var1) || str.end_with?(var2)
#
# # good
# str.start_with?("a", Some::CONST)
# str.start_with?("a", "b", "c")
# str.end_with?(var1, var2)
class DoubleStartEndWith < Cop
MSG = 'Use `%<receiver>s.%<method>s(%<combined_args>s)` ' \
'instead of `%<original_code>s`.'.freeze
def on_or(node)
receiver,
method,
first_call_args,
second_call_args = process_source(node)
return unless receiver && second_call_args.all?(&:pure?)
combined_args = combine_args(first_call_args, second_call_args)
add_offense_for_double_call(node, receiver, method, combined_args)
end
def autocorrect(node)
_receiver, _method,
first_call_args, second_call_args = process_source(node)
combined_args = combine_args(first_call_args, second_call_args)
first_argument = first_call_args.first.loc.expression
last_argument = second_call_args.last.loc.expression
range = first_argument.join(last_argument)
lambda do |corrector|
corrector.replace(range, combined_args)
end
end
private
def process_source(node)
if check_for_active_support_aliases?
check_with_active_support_aliases(node)
else
two_start_end_with_calls(node)
end
end
def combine_args(first_call_args, second_call_args)
(first_call_args + second_call_args).map(&:source).join(', ')
end
def add_offense_for_double_call(node, receiver, method, combined_args)
msg = format(MSG, receiver: receiver.source,
method: method,
combined_args: combined_args,
original_code: node.source)
add_offense(node, message: msg)
end
def check_for_active_support_aliases?
cop_config['IncludeActiveSupportAliases']
end
def_node_matcher :two_start_end_with_calls, <<-PATTERN
(or
(send $_recv [{:start_with? :end_with?} $_method] $...)
(send _recv _method $...))
PATTERN
def_node_matcher :check_with_active_support_aliases, <<-PATTERN
(or
(send $_recv
[{:start_with? :starts_with? :end_with? :ends_with?} $_method]
$...)
(send _recv _method $...))
PATTERN
end
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies unnecessary use of a regex where `String#end_with?`
# would suffice.
#
# @example
# # bad
# 'abc'.match?(/bc\Z/)
# 'abc' =~ /bc\Z/
# 'abc'.match(/bc\Z/)
#
# # good
# 'abc'.end_with?('bc')
class EndWith < Cop
MSG = 'Use `String#end_with?` instead of a regex match anchored to ' \
'the end of the string.'.freeze
SINGLE_QUOTE = "'".freeze
def_node_matcher :redundant_regex?, <<-PATTERN
{(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_end?) (regopt)))
(send (regexp (str $#literal_at_end?) (regopt)) {:match :=~} $_)}
PATTERN
def literal_at_end?(regex_str)
# is this regexp 'literal' in the sense of only matching literal
# chars, rather than using metachars like . and * and so on?
# also, is it anchored at the end of the string?
regex_str =~ /\A(?:#{LITERAL_REGEX})+\\z\z/
end
def on_send(node)
return unless redundant_regex?(node)
add_offense(node)
end
def autocorrect(node)
redundant_regex?(node) do |receiver, regex_str|
receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
regex_str = regex_str[0..-3] # drop \Z anchor
regex_str = interpret_string_escapes(regex_str)
lambda do |corrector|
new_source = receiver.source + '.end_with?(' +
to_string_literal(regex_str) + ')'
corrector.replace(node.source_range, new_source)
end
end
end
end
end
end
end

View File

@ -0,0 +1,97 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# Do not compute the size of statically sized objects.
#
# @example
# # String methods
# # bad
# 'foo'.size
# %q[bar].count
# %(qux).length
#
# # Symbol methods
# # bad
# :fred.size
# :'baz'.length
#
# # Array methods
# # bad
# [1, 2, thud].count
# %W(1, 2, bar).size
#
# # Hash methods
# # bad
# { a: corge, b: grault }.length
#
# # good
# foo.size
# bar.count
# qux.length
#
# # good
# :"#{fred}".size
# CONST = :baz.length
#
# # good
# [1, 2, *thud].count
# garply = [1, 2, 3]
# garly.size
#
# # good
# { a: corge, **grault }.length
# waldo = { a: corge, b: grault }
# waldo.size
#
class FixedSize < Cop
MSG = 'Do not compute the size of statically sized objects.'.freeze
def_node_matcher :counter, <<-MATCHER
(send ${array hash str sym} {:count :length :size} $...)
MATCHER
def on_send(node)
return if allowed_parent?(node.parent)
counter(node) do |var, arg|
return if allowed_variable?(var) || allowed_argument?(arg)
add_offense(node)
end
end
private
def allowed_variable?(var)
contains_splat?(var) || contains_double_splat?(var)
end
def allowed_argument?(arg)
arg && non_string_argument?(arg.first)
end
def allowed_parent?(node)
node && (node.casgn_type? || node.block_type?)
end
def contains_splat?(node)
return unless node.array_type?
node.each_child_node(:splat).any?
end
def contains_double_splat?(node)
return unless node.hash_type?
node.each_child_node(:kwsplat).any?
end
def non_string_argument?(node)
node && !node.str_type?
end
end
end
end
end

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop is used to identify usages of
#
# @example
# # bad
# [1, 2, 3, 4].map { |e| [e, e] }.flatten(1)
# [1, 2, 3, 4].collect { |e| [e, e] }.flatten(1)
#
# # good
# [1, 2, 3, 4].flat_map { |e| [e, e] }
# [1, 2, 3, 4].map { |e| [e, e] }.flatten
# [1, 2, 3, 4].collect { |e| [e, e] }.flatten
class FlatMap < Cop
include RangeHelp
MSG = 'Use `flat_map` instead of `%<method>s...%<flatten>s`.'.freeze
FLATTEN_MULTIPLE_LEVELS = ' Beware, `flat_map` only flattens 1 level ' \
'and `flatten` can be used to flatten ' \
'multiple levels.'.freeze
def_node_matcher :flat_map_candidate?, <<-PATTERN
(send (block $(send _ ${:collect :map}) ...) ${:flatten :flatten!} $...)
PATTERN
def on_send(node)
flat_map_candidate?(node) do |map_node, first_method, flatten, params|
flatten_level, = *params.first
if cop_config['EnabledForFlattenWithoutParams'] && !flatten_level
offense_for_levels(node, map_node, first_method, flatten)
elsif flatten_level == 1
offense_for_method(node, map_node, first_method, flatten)
end
end
end
def autocorrect(node)
map_node, _first_method, _flatten, params = flat_map_candidate?(node)
flatten_level, = *params.first
return unless flatten_level
range = range_between(node.loc.dot.begin_pos,
node.source_range.end_pos)
lambda do |corrector|
corrector.remove(range)
corrector.replace(map_node.loc.selector, 'flat_map')
end
end
private
def offense_for_levels(node, map_node, first_method, flatten)
message = MSG + FLATTEN_MULTIPLE_LEVELS
register_offense(node, map_node, first_method, flatten, message)
end
def offense_for_method(node, map_node, first_method, flatten)
register_offense(node, map_node, first_method, flatten, MSG)
end
def register_offense(node, map_node, first_method, flatten, message)
range = range_between(map_node.loc.selector.begin_pos,
node.loc.expression.end_pos)
add_offense(node,
location: range,
message: format(message, method: first_method,
flatten: flatten))
end
end
end
end
end

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop checks for inefficient searching of keys and values within
# hashes.
#
# `Hash#keys.include?` is less efficient than `Hash#key?` because
# the former allocates a new array and then performs an O(n) search
# through that array, while `Hash#key?` does not allocate any array and
# performs a faster O(1) search for the key.
#
# `Hash#values.include?` is less efficient than `Hash#value?`. While they
# both perform an O(n) search through all of the values, calling `values`
# allocates a new array while using `value?` does not.
#
# @example
# # bad
# { a: 1, b: 2 }.keys.include?(:a)
# { a: 1, b: 2 }.keys.include?(:z)
# h = { a: 1, b: 2 }; h.keys.include?(100)
#
# # good
# { a: 1, b: 2 }.key?(:a)
# { a: 1, b: 2 }.has_key?(:z)
# h = { a: 1, b: 2 }; h.key?(100)
#
# # bad
# { a: 1, b: 2 }.values.include?(2)
# { a: 1, b: 2 }.values.include?('garbage')
# h = { a: 1, b: 2 }; h.values.include?(nil)
#
# # good
# { a: 1, b: 2 }.value?(2)
# { a: 1, b: 2 }.has_value?('garbage')
# h = { a: 1, b: 2 }; h.value?(nil)
#
class InefficientHashSearch < Cop
def_node_matcher :inefficient_include?, <<-PATTERN
(send (send $_ {:keys :values}) :include? _)
PATTERN
def on_send(node)
inefficient_include?(node) do |receiver|
return if receiver.nil?
add_offense(node)
end
end
def autocorrect(node)
lambda do |corrector|
# Replace `keys.include?` or `values.include?` with the appropriate
# `key?`/`value?` method.
corrector.replace(
node.loc.expression,
"#{autocorrect_hash_expression(node)}."\
"#{autocorrect_method(node)}(#{autocorrect_argument(node)})"
)
end
end
private
def message(node)
"Use `##{autocorrect_method(node)}` instead of "\
"`##{current_method(node)}.include?`."
end
def autocorrect_method(node)
case current_method(node)
when :keys then use_long_method ? 'has_key?' : 'key?'
when :values then use_long_method ? 'has_value?' : 'value?'
end
end
def current_method(node)
node.receiver.method_name
end
def use_long_method
preferred_config = config.for_all_cops['Style/PreferredHashMethods']
preferred_config &&
preferred_config['EnforcedStyle'] == 'long' &&
preferred_config['Enabled']
end
def autocorrect_argument(node)
node.arguments.first.source
end
def autocorrect_hash_expression(node)
node.receiver.receiver.source
end
end
end
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop checks for `OpenStruct.new` calls.
# Instantiation of an `OpenStruct` invalidates
# Ruby global method cache as it causes dynamic method
# definition during program runtime.
# This could have an effect on performance,
# especially in case of single-threaded
# applications with multiple `OpenStruct` instantiations.
#
# @example
# # bad
# class MyClass
# def my_method
# OpenStruct.new(my_key1: 'my_value1', my_key2: 'my_value2')
# end
# end
#
# # good
# class MyClass
# MyStruct = Struct.new(:my_key1, :my_key2)
# def my_method
# MyStruct.new('my_value1', 'my_value2')
# end
# end
#
class OpenStruct < Cop
MSG = 'Consider using `Struct` over `OpenStruct` ' \
'to optimize the performance.'.freeze
def_node_matcher :open_struct, <<-PATTERN
(send (const {nil? cbase} :OpenStruct) :new ...)
PATTERN
def on_send(node)
open_struct(node) do |method|
add_offense(node, location: :selector, message: format(MSG, method))
end
end
end
end
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies uses of `Range#include?`, which iterates over each
# item in a `Range` to see if a specified item is there. In contrast,
# `Range#cover?` simply compares the target item with the beginning and
# end points of the `Range`. In a great majority of cases, this is what
# is wanted.
#
# This cop is `Safe: false` by default because `Range#include?` and
# `Range#cover?` are not equivalent behaviour.
#
# @example
# # bad
# ('a'..'z').include?('b') # => true
#
# # good
# ('a'..'z').cover?('b') # => true
#
# # Example of a case where `Range#cover?` may not provide
# # the desired result:
#
# ('a'..'z').cover?('yellow') # => true
class RangeInclude < Cop
MSG = 'Use `Range#cover?` instead of `Range#include?`.'.freeze
# TODO: If we traced out assignments of variables to their uses, we
# might pick up on a few more instances of this issue
# Right now, we only detect direct calls on a Range literal
# (We don't even catch it if the Range is in double parens)
def_node_matcher :range_include, <<-PATTERN
(send {irange erange (begin {irange erange})} :include? ...)
PATTERN
def on_send(node)
return unless range_include(node)
add_offense(node, location: :selector)
end
def autocorrect(node)
->(corrector) { corrector.replace(node.loc.selector, 'cover?') }
end
end
end
end
end

View File

@ -0,0 +1,93 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies the use of a `&block` parameter and `block.call`
# where `yield` would do just as well.
#
# @example
# # bad
# def method(&block)
# block.call
# end
# def another(&func)
# func.call 1, 2, 3
# end
#
# # good
# def method
# yield
# end
# def another
# yield 1, 2, 3
# end
class RedundantBlockCall < Cop
MSG = 'Use `yield` instead of `%<argname>s.call`.'.freeze
YIELD = 'yield'.freeze
OPEN_PAREN = '('.freeze
CLOSE_PAREN = ')'.freeze
SPACE = ' '.freeze
def_node_matcher :blockarg_def, <<-PATTERN
{(def _ (args ... (blockarg $_)) $_)
(defs _ _ (args ... (blockarg $_)) $_)}
PATTERN
def_node_search :blockarg_calls, <<-PATTERN
(send (lvar %1) :call ...)
PATTERN
def_node_search :blockarg_assigned?, <<-PATTERN
(lvasgn %1 ...)
PATTERN
def on_def(node)
blockarg_def(node) do |argname, body|
next unless body
calls_to_report(argname, body).each do |blockcall|
add_offense(blockcall, message: format(MSG, argname: argname))
end
end
end
# offenses are registered on the `block.call` nodes
def autocorrect(node)
_receiver, _method, *args = *node
new_source = String.new(YIELD)
unless args.empty?
new_source += if parentheses?(node)
OPEN_PAREN
else
SPACE
end
new_source << args.map(&:source).join(', ')
end
new_source << CLOSE_PAREN if parentheses?(node) && !args.empty?
->(corrector) { corrector.replace(node.source_range, new_source) }
end
private
def calls_to_report(argname, body)
return [] if blockarg_assigned?(body, argname)
calls = to_enum(:blockarg_calls, body, argname)
return [] if calls.any? { |call| args_include_block_pass?(call) }
calls
end
def args_include_block_pass?(blockcall)
_receiver, _call, *args = *blockcall
args.any?(&:block_pass_type?)
end
end
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies the use of `Regexp#match` or `String#match`, which
# returns `#<MatchData>`/`nil`. The return value of `=~` is an integral
# index/`nil` and is more performant.
#
# @example
# # bad
# do_something if str.match(/regex/)
# while regex.match('str')
# do_something
# end
#
# # good
# method(str =~ /regex/)
# return value unless regex =~ 'str'
class RedundantMatch < Cop
MSG = 'Use `=~` in places where the `MatchData` returned by ' \
'`#match` will not be used.'.freeze
# 'match' is a fairly generic name, so we don't flag it unless we see
# a string or regexp literal on one side or the other
def_node_matcher :match_call?, <<-PATTERN
{(send {str regexp} :match _)
(send !nil? :match {str regexp})}
PATTERN
def_node_matcher :only_truthiness_matters?, <<-PATTERN
^({if while until case while_post until_post} equal?(%0) ...)
PATTERN
def on_send(node)
return unless match_call?(node) &&
(!node.value_used? || only_truthiness_matters?(node)) &&
!(node.parent && node.parent.block_type?)
add_offense(node)
end
def autocorrect(node)
# Regexp#match can take a second argument, but this cop doesn't
# register an offense in that case
return unless node.first_argument.regexp_type?
new_source =
node.receiver.source + ' =~ ' + node.first_argument.source
->(corrector) { corrector.replace(node.source_range, new_source) }
end
end
end
end
end

View File

@ -0,0 +1,183 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies places where `Hash#merge!` can be replaced by
# `Hash#[]=`.
#
# @example
# hash.merge!(a: 1)
# hash.merge!({'key' => 'value'})
# hash.merge!(a: 1, b: 2)
class RedundantMerge < Cop
AREF_ASGN = '%<receiver>s[%<key>s] = %<value>s'.freeze
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'.freeze
WITH_MODIFIER_CORRECTION = <<-RUBY.strip_indent
%<keyword>s %<condition>s
%<leading_space>s%<indent>s%<body>s
%<leading_space>send
RUBY
def_node_matcher :redundant_merge_candidate, <<-PATTERN
(send $!nil? :merge! [(hash $...) !kwsplat_type?])
PATTERN
def_node_matcher :modifier_flow_control?, <<-PATTERN
[{if while until} modifier_form?]
PATTERN
def on_send(node)
each_redundant_merge(node) do |redundant_merge_node|
add_offense(redundant_merge_node)
end
end
def autocorrect(node)
redundant_merge_candidate(node) do |receiver, pairs|
new_source = to_assignments(receiver, pairs).join("\n")
if node.parent && pairs.size > 1
correct_multiple_elements(node, node.parent, new_source)
else
correct_single_element(node, new_source)
end
end
end
private
def message(node)
redundant_merge_candidate(node) do |receiver, pairs|
assignments = to_assignments(receiver, pairs).join('; ')
format(MSG, prefer: assignments, current: node.source)
end
end
def each_redundant_merge(node)
redundant_merge_candidate(node) do |receiver, pairs|
next if non_redundant_merge?(node, receiver, pairs)
yield node
end
end
def non_redundant_merge?(node, receiver, pairs)
non_redundant_pairs?(receiver, pairs) ||
kwsplat_used?(pairs) ||
non_redundant_value_used?(receiver, node)
end
def non_redundant_pairs?(receiver, pairs)
pairs.size > 1 && !receiver.pure? || pairs.size > max_key_value_pairs
end
def kwsplat_used?(pairs)
pairs.any?(&:kwsplat_type?)
end
def non_redundant_value_used?(receiver, node)
node.value_used? &&
!EachWithObjectInspector.new(node, receiver).value_used?
end
def correct_multiple_elements(node, parent, new_source)
if modifier_flow_control?(parent)
new_source = rewrite_with_modifier(node, parent, new_source)
node = parent
else
padding = "\n#{leading_spaces(node)}"
new_source.gsub!(/\n/, padding)
end
->(corrector) { corrector.replace(node.source_range, new_source) }
end
def correct_single_element(node, new_source)
->(corrector) { corrector.replace(node.source_range, new_source) }
end
def to_assignments(receiver, pairs)
pairs.map do |pair|
key, value = *pair
key = key.sym_type? && pair.colon? ? ":#{key.source}" : key.source
format(AREF_ASGN, receiver: receiver.source,
key: key,
value: value.source)
end
end
def rewrite_with_modifier(node, parent, new_source)
indent = ' ' * indent_width
padding = "\n#{indent + leading_spaces(node)}"
new_source.gsub!(/\n/, padding)
format(WITH_MODIFIER_CORRECTION, keyword: parent.loc.keyword.source,
condition: parent.condition.source,
leading_space: leading_spaces(node),
indent: indent,
body: new_source).chomp
end
def leading_spaces(node)
node.source_range.source_line[/\A\s*/]
end
def indent_width
@config.for_cop('IndentationWidth')['Width'] || 2
end
def max_key_value_pairs
Integer(cop_config['MaxKeyValuePairs'])
end
# A utility class for checking the use of values within an
# `each_with_object` call.
class EachWithObjectInspector
extend NodePattern::Macros
def initialize(node, receiver)
@node = node
@receiver = unwind(receiver)
end
def value_used?
return false unless eligible_receiver? && second_argument
receiver.loc.name.source == second_argument.loc.name.source
end
private
attr_reader :node, :receiver
def eligible_receiver?
receiver.respond_to?(:lvar_type?) && receiver.lvar_type?
end
def second_argument
parent = node.parent
parent = parent.parent if parent.begin_type?
@second_argument ||= each_with_object_node(parent)
end
def unwind(receiver)
while receiver.respond_to?(:send_type?) && receiver.send_type?
receiver, = *receiver
end
receiver
end
def_node_matcher :each_with_object_node, <<-PATTERN
(block (send _ :each_with_object _) (args _ $_) ...)
PATTERN
end
end
end
end
end

View File

@ -0,0 +1,265 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# In Ruby 2.4, `String#match?`, `Regexp#match?`, and `Symbol#match?`
# have been added. The methods are faster than `match`.
# Because the methods avoid creating a `MatchData` object or saving
# backref.
# So, when `MatchData` is not used, use `match?` instead of `match`.
#
# @example
# # bad
# def foo
# if x =~ /re/
# do_something
# end
# end
#
# # bad
# def foo
# if x !~ /re/
# do_something
# end
# end
#
# # bad
# def foo
# if x.match(/re/)
# do_something
# end
# end
#
# # bad
# def foo
# if /re/ === x
# do_something
# end
# end
#
# # good
# def foo
# if x.match?(/re/)
# do_something
# end
# end
#
# # good
# def foo
# if !x.match?(/re/)
# do_something
# end
# end
#
# # good
# def foo
# if x =~ /re/
# do_something(Regexp.last_match)
# end
# end
#
# # good
# def foo
# if x.match(/re/)
# do_something($~)
# end
# end
#
# # good
# def foo
# if /re/ === x
# do_something($~)
# end
# end
class RegexpMatch < Cop
extend TargetRubyVersion
minimum_target_ruby_version 2.4
# Constants are included in this list because it is unlikely that
# someone will store `nil` as a constant and then use it for comparison
TYPES_IMPLEMENTING_MATCH = %i[const regexp str sym].freeze
MSG =
'Use `match?` instead of `%<current>s` when `MatchData` ' \
'is not used.'.freeze
def_node_matcher :match_method?, <<-PATTERN
{
(send _recv :match _)
(send _recv :match _ (int ...))
}
PATTERN
def_node_matcher :match_operator?, <<-PATTERN
(send !nil? {:=~ :!~} !nil?)
PATTERN
def_node_matcher :match_threequals?, <<-PATTERN
(send (regexp (str _) {(regopt) (regopt _)}) :=== !nil?)
PATTERN
def match_with_lvasgn?(node)
return false unless node.match_with_lvasgn_type?
regexp, _rhs = *node
regexp.to_regexp.named_captures.empty?
end
MATCH_NODE_PATTERN = <<-PATTERN.freeze
{
#match_method?
#match_operator?
#match_threequals?
#match_with_lvasgn?
}
PATTERN
def_node_matcher :match_node?, MATCH_NODE_PATTERN
def_node_search :search_match_nodes, MATCH_NODE_PATTERN
def_node_search :last_matches, <<-PATTERN
{
(send (const nil? :Regexp) :last_match)
(send (const nil? :Regexp) :last_match _)
({back_ref nth_ref} _)
(gvar #match_gvar?)
}
PATTERN
def on_if(node)
check_condition(node.condition)
end
def on_case(node)
return if node.condition
node.each_when do |when_node|
when_node.each_condition do |condition|
check_condition(condition)
end
end
end
def autocorrect(node)
lambda do |corrector|
if match_method?(node)
corrector.replace(node.loc.selector, 'match?')
elsif match_operator?(node) || match_threequals?(node)
recv, oper, arg = *node
correct_operator(corrector, recv, arg, oper)
elsif match_with_lvasgn?(node)
recv, arg = *node
correct_operator(corrector, recv, arg)
end
end
end
private
def check_condition(cond)
match_node?(cond) do
return if last_match_used?(cond)
add_offense(cond)
end
end
def message(node)
format(MSG, current: node.loc.selector.source)
end
def last_match_used?(match_node)
scope_root = scope_root(match_node)
body = scope_root ? scope_body(scope_root) : match_node.ancestors.last
return true if match_node.parent.if_type? &&
match_node.parent.modifier_form?
match_node_pos = match_node.loc.expression.begin_pos
next_match_pos = next_match_pos(body, match_node_pos, scope_root)
range = match_node_pos..next_match_pos
find_last_match(body, range, scope_root)
end
def next_match_pos(body, match_node_pos, scope_root)
node = search_match_nodes(body).find do |match|
match.loc.expression.begin_pos > match_node_pos &&
scope_root(match) == scope_root
end
node ? node.loc.expression.begin_pos : Float::INFINITY
end
def find_last_match(body, range, scope_root)
last_matches(body).find do |ref|
ref_pos = ref.loc.expression.begin_pos
range.cover?(ref_pos) &&
scope_root(ref) == scope_root
end
end
def scope_body(node)
children = node.children
case node.type
when :module
children[1]
when :defs
children[3]
else
children[2]
end
end
def scope_root(node)
node.each_ancestor.find do |ancestor|
ancestor.def_type? ||
ancestor.defs_type? ||
ancestor.class_type? ||
ancestor.module_type?
end
end
def match_gvar?(sym)
%i[
$~
$MATCH
$PREMATCH
$POSTMATCH
$LAST_PAREN_MATCH
$LAST_MATCH_INFO
].include?(sym)
end
def correct_operator(corrector, recv, arg, oper = nil)
op_range = correction_range(recv, arg)
if TYPES_IMPLEMENTING_MATCH.include?(recv.type)
corrector.replace(op_range, '.match?(')
elsif TYPES_IMPLEMENTING_MATCH.include?(arg.type)
corrector.replace(op_range, '.match?(')
swap_receiver_and_arg(corrector, recv, arg)
else
corrector.replace(op_range, '&.match?(')
end
corrector.insert_after(arg.loc.expression, ')')
corrector.insert_before(recv.loc.expression, '!') if oper == :!~
end
def swap_receiver_and_arg(corrector, recv, arg)
corrector.replace(recv.loc.expression, arg.source)
corrector.replace(arg.loc.expression, recv.source)
end
def correction_range(recv, arg)
buffer = processed_source.buffer
op_begin_pos = recv.loc.expression.end_pos
op_end_pos = arg.loc.expression.begin_pos
Parser::Source::Range.new(buffer, op_begin_pos, op_end_pos)
end
end
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop is used to identify usages of `reverse.each` and
# change them to use `reverse_each` instead.
#
# @example
# # bad
# [].reverse.each
#
# # good
# [].reverse_each
class ReverseEach < Cop
include RangeHelp
MSG = 'Use `reverse_each` instead of `reverse.each`.'.freeze
UNDERSCORE = '_'.freeze
def_node_matcher :reverse_each?, <<-MATCHER
(send $(send _ :reverse) :each)
MATCHER
def on_send(node)
reverse_each?(node) do |receiver|
location_of_reverse = receiver.loc.selector.begin_pos
end_location = node.loc.selector.end_pos
range = range_between(location_of_reverse, end_location)
add_offense(node, location: range)
end
end
def autocorrect(node)
->(corrector) { corrector.replace(node.loc.dot, UNDERSCORE) }
end
end
end
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop is used to identify usages of `count` on an
# `Array` and `Hash` and change them to `size`.
#
# @example
# # bad
# [1, 2, 3].count
#
# # bad
# {a: 1, b: 2, c: 3}.count
#
# # good
# [1, 2, 3].size
#
# # good
# {a: 1, b: 2, c: 3}.size
#
# # good
# [1, 2, 3].count { |e| e > 2 }
# TODO: Add advanced detection of variables that could
# have been assigned to an array or a hash.
class Size < Cop
MSG = 'Use `size` instead of `count`.'.freeze
def on_send(node)
return unless eligible_node?(node)
add_offense(node, location: :selector)
end
def autocorrect(node)
->(corrector) { corrector.replace(node.loc.selector, 'size') }
end
private
def eligible_node?(node)
return false unless node.method?(:count) && !node.arguments?
eligible_receiver?(node.receiver) && !allowed_parent?(node.parent)
end
def eligible_receiver?(node)
return false unless node
array?(node) || hash?(node)
end
def allowed_parent?(node)
node && node.block_type?
end
def array?(node)
return true if node.array_type?
return false unless node.send_type?
_, constant = *node.receiver
constant == :Array || node.method_name == :to_a
end
def hash?(node)
return true if node.hash_type?
return false unless node.send_type?
_, constant = *node.receiver
constant == :Hash || node.method_name == :to_h
end
end
end
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies unnecessary use of a regex where
# `String#start_with?` would suffice.
#
# @example
# # bad
# 'abc'.match?(/\Aab/)
# 'abc' =~ /\Aab/
# 'abc'.match(/\Aab/)
#
# # good
# 'abc'.start_with?('ab')
class StartWith < Cop
MSG = 'Use `String#start_with?` instead of a regex match anchored to ' \
'the beginning of the string.'.freeze
SINGLE_QUOTE = "'".freeze
def_node_matcher :redundant_regex?, <<-PATTERN
{(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_start?) (regopt)))
(send (regexp (str $#literal_at_start?) (regopt)) {:match :=~} $_)}
PATTERN
def literal_at_start?(regex_str)
# is this regexp 'literal' in the sense of only matching literal
# chars, rather than using metachars like `.` and `*` and so on?
# also, is it anchored at the start of the string?
# (tricky: \s, \d, and so on are metacharacters, but other characters
# escaped with a slash are just literals. LITERAL_REGEX takes all
# that into account.)
regex_str =~ /\A\\A(?:#{LITERAL_REGEX})+\z/
end
def on_send(node)
return unless redundant_regex?(node)
add_offense(node)
end
def autocorrect(node)
redundant_regex?(node) do |receiver, regex_str|
receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
regex_str = regex_str[2..-1] # drop \A anchor
regex_str = interpret_string_escapes(regex_str)
lambda do |corrector|
new_source = receiver.source + '.start_with?(' +
to_string_literal(regex_str) + ')'
corrector.replace(node.source_range, new_source)
end
end
end
end
end
end
end

View File

@ -0,0 +1,173 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies places where `gsub` can be replaced by
# `tr` or `delete`.
#
# @example
# # bad
# 'abc'.gsub('b', 'd')
# 'abc'.gsub('a', '')
# 'abc'.gsub(/a/, 'd')
# 'abc'.gsub!('a', 'd')
#
# # good
# 'abc'.gsub(/.*/, 'a')
# 'abc'.gsub(/a+/, 'd')
# 'abc'.tr('b', 'd')
# 'a b c'.delete(' ')
class StringReplacement < Cop
include RangeHelp
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'.freeze
DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/.freeze
DELETE = 'delete'.freeze
TR = 'tr'.freeze
BANG = '!'.freeze
SINGLE_QUOTE = "'".freeze
def_node_matcher :string_replacement?, <<-PATTERN
(send _ {:gsub :gsub!}
${regexp str (send (const nil? :Regexp) {:new :compile} _)}
$str)
PATTERN
def on_send(node)
string_replacement?(node) do |first_param, second_param|
return if accept_second_param?(second_param)
return if accept_first_param?(first_param)
offense(node, first_param, second_param)
end
end
def autocorrect(node)
_string, _method, first_param, second_param = *node
first_source, = first_source(first_param)
second_source, = *second_param
unless first_param.str_type?
first_source = interpret_string_escapes(first_source)
end
replacement_method =
replacement_method(node, first_source, second_source)
replace_method(node, first_source, second_source, first_param,
replacement_method)
end
def replace_method(node, first, second, first_param, replacement)
lambda do |corrector|
corrector.replace(node.loc.selector, replacement)
unless first_param.str_type?
corrector.replace(first_param.source_range,
to_string_literal(first))
end
if second.empty? && first.length == 1
remove_second_param(corrector, node, first_param)
end
end
end
private
def accept_second_param?(second_param)
second_source, = *second_param
second_source.length > 1
end
def accept_first_param?(first_param)
first_source, options = first_source(first_param)
return true if first_source.nil?
unless first_param.str_type?
return true if options
return true unless first_source =~ DETERMINISTIC_REGEX
# This must be done after checking DETERMINISTIC_REGEX
# Otherwise things like \s will trip us up
first_source = interpret_string_escapes(first_source)
end
first_source.length != 1
end
def offense(node, first_param, second_param)
first_source, = first_source(first_param)
unless first_param.str_type?
first_source = interpret_string_escapes(first_source)
end
second_source, = *second_param
message = message(node, first_source, second_source)
add_offense(node, location: range(node), message: message)
end
def first_source(first_param)
case first_param.type
when :regexp
source_from_regex_literal(first_param)
when :send
source_from_regex_constructor(first_param)
when :str
first_param.children.first
end
end
def source_from_regex_literal(node)
regex, options = *node
source, = *regex
options, = *options
[source, options]
end
def source_from_regex_constructor(node)
_const, _init, regex = *node
case regex.type
when :regexp
source_from_regex_literal(regex)
when :str
source, = *regex
source
end
end
def range(node)
range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
end
def replacement_method(node, first_source, second_source)
replacement = if second_source.empty? && first_source.length == 1
DELETE
else
TR
end
"#{replacement}#{BANG if node.bang_method?}"
end
def message(node, first_source, second_source)
replacement_method =
replacement_method(node, first_source, second_source)
format(MSG, prefer: replacement_method, current: node.method_name)
end
def method_suffix(node)
node.loc.end ? node.loc.end.source : ''
end
def remove_second_param(corrector, node, first_param)
end_range = range_between(first_param.source_range.end_pos,
node.source_range.end_pos)
corrector.replace(end_range, method_suffix(node))
end
end
end
end
end

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop checks for .times.map calls.
# In most cases such calls can be replaced
# with an explicit array creation.
#
# @example
# # bad
# 9.times.map do |i|
# i.to_s
# end
#
# # good
# Array.new(9) do |i|
# i.to_s
# end
class TimesMap < Cop
MESSAGE = 'Use `Array.new(%<count>s)` with a block ' \
'instead of `.times.%<map_or_collect>s`'.freeze
MESSAGE_ONLY_IF = 'only if `%<count>s` is always 0 or more'.freeze
def on_send(node)
check(node)
end
def on_block(node)
check(node)
end
def autocorrect(node)
map_or_collect, count = times_map_call(node)
replacement =
"Array.new(#{count.source}" \
"#{map_or_collect.arguments.map { |arg| ", #{arg.source}" }.join})"
lambda do |corrector|
corrector.replace(map_or_collect.loc.expression, replacement)
end
end
private
def check(node)
times_map_call(node) do |map_or_collect, count|
add_offense(node, message: message(map_or_collect, count))
end
end
def message(map_or_collect, count)
template = if count.literal?
MESSAGE + '.'
else
"#{MESSAGE} #{MESSAGE_ONLY_IF}."
end
format(template,
count: count.source,
map_or_collect: map_or_collect.method_name)
end
def_node_matcher :times_map_call, <<-PATTERN
{(block $(send (send $!nil? :times) {:map :collect}) ...)
$(send (send $!nil? :times) {:map :collect} (block_pass ...))}
PATTERN
end
end
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# In Ruby 2.3 or later, use unary plus operator to unfreeze a string
# literal instead of `String#dup` and `String.new`.
# Unary plus operator is faster than `String#dup`.
#
# Note: `String.new` (without operator) is not exactly the same as `+''`.
# These differ in encoding. `String.new.encoding` is always `ASCII-8BIT`.
# However, `(+'').encoding` is the same as script encoding(e.g. `UTF-8`).
# So, if you expect `ASCII-8BIT` encoding, disable this cop.
#
# @example
# # bad
# ''.dup
# "something".dup
# String.new
# String.new('')
# String.new('something')
#
# # good
# +'something'
# +''
class UnfreezeString < Cop
extend TargetRubyVersion
minimum_target_ruby_version 2.3
MSG = 'Use unary plus to get an unfrozen string literal.'.freeze
def_node_matcher :dup_string?, <<-PATTERN
(send {str dstr} :dup)
PATTERN
def_node_matcher :string_new?, <<-PATTERN
{
(send (const nil? :String) :new {str dstr})
(send (const nil? :String) :new)
}
PATTERN
def on_send(node)
add_offense(node) if dup_string?(node) || string_new?(node)
end
end
end
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop identifies places where `URI::Parser.new`
# can be replaced by `URI::DEFAULT_PARSER`.
#
# @example
# # bad
# URI::Parser.new
#
# # good
# URI::DEFAULT_PARSER
#
class UriDefaultParser < Cop
MSG = 'Use `%<double_colon>sURI::DEFAULT_PARSER` instead of ' \
'`%<double_colon>sURI::Parser.new`.'.freeze
def_node_matcher :uri_parser_new?, <<-PATTERN
(send
(const
(const ${nil? cbase} :URI) :Parser) :new)
PATTERN
def on_send(node)
return unless uri_parser_new?(node) do |captured_value|
double_colon = captured_value ? '::' : ''
message = format(MSG, double_colon: double_colon)
add_offense(node, message: message)
end
end
def autocorrect(node)
lambda do |corrector|
double_colon = uri_parser_new?(node) ? '::' : ''
corrector.replace(
node.loc.expression, "#{double_colon}URI::DEFAULT_PARSER"
)
end
end
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module RuboCop
# RuboCop included the performance cops directly before version 1.0.0.
# We can remove them to avoid warnings about redefining constants.
module Cop
remove_const('Performance') if const_defined?('Performance')
end
end
require_relative 'performance/caller'
require_relative 'performance/case_when_splat'
require_relative 'performance/casecmp'
require_relative 'performance/compare_with_block'
require_relative 'performance/count'
require_relative 'performance/detect'
require_relative 'performance/double_start_end_with'
require_relative 'performance/end_with'
require_relative 'performance/fixed_size'
require_relative 'performance/flat_map'
require_relative 'performance/inefficient_hash_search'
require_relative 'performance/open_struct'
require_relative 'performance/range_include'
require_relative 'performance/redundant_block_call'
require_relative 'performance/redundant_match'
require_relative 'performance/redundant_merge'
require_relative 'performance/regexp_match'
require_relative 'performance/reverse_each'
require_relative 'performance/size'
require_relative 'performance/start_with'
require_relative 'performance/string_replacement'
require_relative 'performance/times_map'
require_relative 'performance/unfreeze_string'
require_relative 'performance/uri_default_parser'
require_relative 'performance/chain_array_allocation'

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module RuboCop
# RuboCop Performance project namespace
module Performance
PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
CONFIG = YAML.safe_load(CONFIG_DEFAULT.read).freeze
private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module RuboCop
module Performance
# Because RuboCop doesn't yet support plugins, we have to monkey patch in a
# bit of our configuration.
module Inject
def self.defaults!
path = CONFIG_DEFAULT.to_s
hash = ConfigLoader.send(:load_yaml_configuration, path)
config = Config.new(hash, path)
puts "configuration from #{path}" if ConfigLoader.debug?
config = ConfigLoader.merge_with_default(config, path)
ConfigLoader.instance_variable_set(:@default_configuration, config)
end
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module RuboCop
module Performance
module Version
STRING = '1.1.0'.freeze
end
end
end