Add rubocop-performance gem.
This commit is contained in:
parent
1d334e270e
commit
738f4689a5
@ -20,5 +20,6 @@ gem "backports"
|
|||||||
gem "concurrent-ruby"
|
gem "concurrent-ruby"
|
||||||
gem "mechanize"
|
gem "mechanize"
|
||||||
gem "plist"
|
gem "plist"
|
||||||
|
gem "rubocop-performance"
|
||||||
gem "rubocop-rspec"
|
gem "rubocop-rspec"
|
||||||
gem "ruby-macho"
|
gem "ruby-macho"
|
||||||
|
|||||||
@ -87,6 +87,8 @@ GEM
|
|||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 1.6)
|
unicode-display_width (>= 1.4.0, < 1.6)
|
||||||
|
rubocop-performance (1.1.0)
|
||||||
|
rubocop (>= 0.67.0)
|
||||||
rubocop-rspec (1.32.0)
|
rubocop-rspec (1.32.0)
|
||||||
rubocop (>= 0.60.0)
|
rubocop (>= 0.60.0)
|
||||||
ruby-macho (2.2.0)
|
ruby-macho (2.2.0)
|
||||||
@ -128,6 +130,7 @@ DEPENDENCIES
|
|||||||
rspec-retry
|
rspec-retry
|
||||||
rspec-wait
|
rspec-wait
|
||||||
rubocop
|
rubocop
|
||||||
|
rubocop-performance
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
ruby-macho
|
ruby-macho
|
||||||
simplecov
|
simplecov
|
||||||
|
|||||||
@ -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'
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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'
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module RuboCop
|
||||||
|
module Performance
|
||||||
|
module Version
|
||||||
|
STRING = '1.1.0'.freeze
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
x
Reference in New Issue
Block a user