Add rubocop-performance gem.
This commit is contained in:
parent
1d334e270e
commit
738f4689a5
@ -20,5 +20,6 @@ gem "backports"
|
||||
gem "concurrent-ruby"
|
||||
gem "mechanize"
|
||||
gem "plist"
|
||||
gem "rubocop-performance"
|
||||
gem "rubocop-rspec"
|
||||
gem "ruby-macho"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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