forked from rubocop/rubocop-performance
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathredundant_equality_comparison_block.rb
137 lines (116 loc) · 4.72 KB
/
redundant_equality_comparison_block.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# Checks for uses `Enumerable#all?`, `Enumerable#any?`, `Enumerable#one?`,
# and `Enumerable#none?` are compared with `===` or similar methods in block.
#
# By default, `Object#===` behaves the same as `Object#==`, but this
# behavior is appropriately overridden in subclass. For example,
# `Range#===` returns `true` when argument is within the range.
#
# This cop has `AllowRegexpMatch` option and it is true by default because
# `regexp.match?('string')` often used in block changes to the opposite result:
#
# [source,ruby]
# ----
# [/pattern/].all? { |regexp| regexp.match?('pattern') } # => true
# [/pattern/].all? { |regexp| regexp =~ 'pattern' } # => true
# [/pattern/].all?('pattern') # => false
# ----
#
# @safety
# This cop is unsafe because `===` and `==` do not always behave the same.
#
# @example
# # bad
# items.all? { |item| pattern === item }
# items.all? { |item| item == other }
# items.all? { |item| item.is_a?(Klass) }
# items.all? { |item| item.kind_of?(Klass) }
#
# # good
# items.all?(pattern)
# items.all?(Klass)
#
# @example AllowRegexpMatch: true (default)
#
# # good
# items.all? { |item| item =~ pattern }
# items.all? { |item| item.match?(pattern) }
#
# @example AllowRegexpMatch: false
#
# # bad
# items.all? { |item| item =~ pattern }
# items.all? { |item| item.match?(pattern) }
#
class RedundantEqualityComparisonBlock < Base
extend AutoCorrector
extend TargetRubyVersion
minimum_target_ruby_version 2.5
MSG = 'Use `%<prefer>s` instead of block.'
TARGET_METHODS = %i[all? any? one? none?].freeze
COMPARISON_METHODS = %i[== === is_a? kind_of?].freeze
REGEXP_METHODS = %i[=~ match?].freeze
IS_A_METHODS = %i[is_a? kind_of?].freeze
def on_block(node)
return unless TARGET_METHODS.include?(node.method_name)
return unless one_block_argument?(node.arguments)
block_argument = node.arguments.first
block_body = node.body
return unless use_equality_comparison_block?(block_body)
return if same_block_argument_and_is_a_argument?(block_body, block_argument)
return unless (new_argument = new_argument(block_argument, block_body))
range = offense_range(node)
prefer = "#{node.method_name}(#{new_argument})"
add_offense(range, message: format(MSG, prefer: prefer)) do |corrector|
corrector.replace(range, prefer)
end
end
private
def one_block_argument?(block_arguments)
block_arguments.one? && !block_arguments.source.include?(',')
end
def use_equality_comparison_block?(block_body)
return false unless block_body.send_type?
method_name = block_body.method_name
COMPARISON_METHODS.include?(method_name) || (!allow_regexp_match? && REGEXP_METHODS.include?(method_name))
end
def same_block_argument_and_is_a_argument?(block_body, block_argument)
if block_body.method?(:===)
block_argument.source != block_body.children[2].source
elsif IS_A_METHODS.include?(block_body.method_name)
block_argument.source == block_body.first_argument.source
else
false
end
end
def new_argument(block_argument, block_body)
if block_argument.source == block_body.receiver.source
rhs = block_body.first_argument
return if use_block_argument_in_method_argument_of_operand?(block_argument, rhs)
rhs.source
elsif block_argument.source == block_body.first_argument.source
lhs = block_body.receiver
return if use_block_argument_in_method_argument_of_operand?(block_argument, lhs)
lhs.source
end
end
def use_block_argument_in_method_argument_of_operand?(block_argument, operand)
return false unless operand.send_type?
arguments = operand.arguments
arguments.inject(arguments.map(&:source)) do |operand_sources, argument|
operand_sources + argument.each_descendant(:lvar).map(&:source)
end.any?(block_argument.source)
end
def offense_range(node)
node.send_node.loc.selector.join(node.source_range.end)
end
def allow_regexp_match?
cop_config.fetch('AllowRegexpMatch', true)
end
end
end
end
end