forked from rubocop/rubocop-rails
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcompact_blank.rb
More file actions
132 lines (114 loc) · 4.63 KB
/
compact_blank.rb
File metadata and controls
132 lines (114 loc) · 4.63 KB
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
# frozen_string_literal: true
module RuboCop
module Cop
module Rails
# Checks if collection can be blank-compacted with `compact_blank`.
#
# @safety
# It is unsafe by default because false positives may occur in the
# blank check of block arguments to the receiver object.
#
# For example, `[[1, 2], [3, nil]].reject { |first, second| second.blank? }` and
# `[[1, 2], [3, nil]].compact_blank` are not compatible. The same is true for `blank?`.
# This will work fine when the receiver is a hash object.
#
# And `compact_blank!` has different implementations for `Array`, `Hash`, and
# `ActionController::Parameters`.
# `Array#compact_blank!`, `Hash#compact_blank!` are equivalent to `delete_if(&:blank?)`.
# If the cop makes a mistake, autocorrected code may get unexpected behavior.
#
# @example
#
# # bad
# collection.reject(&:blank?)
# collection.reject { |_k, v| v.blank? }
# collection.select(&:present?)
# collection.select { |_k, v| v.present? }
# collection.filter(&:present?)
# collection.filter { |_k, v| v.present? }
#
# # good
# collection.compact_blank
#
# # bad
# collection.delete_if(&:blank?) # Same behavior as `Array#compact_blank!` and `Hash#compact_blank!`
# collection.delete_if { |_k, v| v.blank? } # Same behavior as `Array#compact_blank!` and `Hash#compact_blank!`
# collection.keep_if(&:present?) # Same behavior as `Array#compact_blank!` and `Hash#compact_blank!`
# collection.keep_if { |_k, v| v.present? } # Same behavior as `Array#compact_blank!` and `Hash#compact_blank!`
#
# # good
# collection.compact_blank!
#
class CompactBlank < Base
include RangeHelp
extend AutoCorrector
extend TargetRailsVersion
MSG = 'Use `%<preferred_method>s` instead.'
RESTRICT_ON_SEND = %i[reject delete_if select filter keep_if].freeze
DESTRUCTIVE_METHODS = %i[delete_if keep_if].freeze
minimum_target_rails_version 6.1
def_node_matcher :reject_with_block?, <<~PATTERN
(block
(send _ {:reject :delete_if})
$(args ...)
(send
$(lvar _) :blank?))
PATTERN
def_node_matcher :reject_with_block_pass?, <<~PATTERN
(send _ {:reject :delete_if}
(block_pass
(sym :blank?)))
PATTERN
def_node_matcher :select_with_block?, <<~PATTERN
(block
(send _ {:select :filter :keep_if})
$(args ...)
(send
$(lvar _) :present?))
PATTERN
def_node_matcher :select_with_block_pass?, <<~PATTERN
(send _ {:select :filter :keep_if}
(block-pass
(sym :present?)))
PATTERN
def on_send(node)
return if target_ruby_version < 2.6 && node.method?(:filter)
return unless bad_method?(node)
range = offense_range(node)
preferred_method = preferred_method(node)
add_offense(range, message: format(MSG, preferred_method: preferred_method)) do |corrector|
corrector.replace(range, preferred_method)
end
end
private
def bad_method?(node)
return true if reject_with_block_pass?(node)
return true if select_with_block_pass?(node)
arguments, receiver_in_block = reject_with_block?(node.parent) || select_with_block?(node.parent)
if arguments
return use_single_value_block_argument?(arguments, receiver_in_block) ||
use_hash_value_block_argument?(arguments, receiver_in_block)
end
false
end
def use_single_value_block_argument?(arguments, receiver_in_block)
arguments.length == 1 && arguments[0].source == receiver_in_block.source
end
def use_hash_value_block_argument?(arguments, receiver_in_block)
arguments.length == 2 && arguments[1].source == receiver_in_block.source
end
def offense_range(node)
end_pos = if node.parent&.block_type? && node.parent&.send_node == node
node.parent.source_range.end_pos
else
node.source_range.end_pos
end
range_between(node.loc.selector.begin_pos, end_pos)
end
def preferred_method(node)
DESTRUCTIVE_METHODS.include?(node.method_name) ? 'compact_blank!' : 'compact_blank'
end
end
end
end
end