rubocop-rails 2.32.0 → 2.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/default.yml +47 -3
- data/lib/rubocop/cop/mixin/active_record_helper.rb +1 -1
- data/lib/rubocop/cop/mixin/index_method.rb +4 -0
- data/lib/rubocop/cop/rails/action_controller_flash_before_render.rb +4 -2
- data/lib/rubocop/cop/rails/delegate.rb +4 -4
- data/lib/rubocop/cop/rails/duplicate_association.rb +1 -1
- data/lib/rubocop/cop/rails/duplicate_scope.rb +2 -2
- data/lib/rubocop/cop/rails/env.rb +57 -0
- data/lib/rubocop/cop/rails/env_local.rb +50 -26
- data/lib/rubocop/cop/rails/environment_comparison.rb +56 -48
- data/lib/rubocop/cop/rails/exit.rb +7 -4
- data/lib/rubocop/cop/rails/file_path.rb +2 -2
- data/lib/rubocop/cop/rails/find_by.rb +1 -1
- data/lib/rubocop/cop/rails/find_by_or_assignment_memoization.rb +124 -0
- data/lib/rubocop/cop/rails/helper_instance_variable.rb +16 -17
- data/lib/rubocop/cop/rails/http_status_name_consistency.rb +80 -0
- data/lib/rubocop/cop/rails/index_with.rb +5 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +7 -0
- data/lib/rubocop/cop/rails/order_arguments.rb +84 -0
- data/lib/rubocop/cop/rails/output.rb +3 -0
- data/lib/rubocop/cop/rails/output_safety.rb +3 -1
- data/lib/rubocop/cop/rails/pluck.rb +6 -3
- data/lib/rubocop/cop/rails/presence.rb +67 -18
- data/lib/rubocop/cop/rails/read_write_attribute.rb +1 -1
- data/lib/rubocop/cop/rails/redirect_back_or_to.rb +99 -0
- data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +3 -3
- data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +1 -1
- data/lib/rubocop/cop/rails/save_bang.rb +2 -2
- data/lib/rubocop/cop/rails/transaction_exit_statement.rb +4 -1
- data/lib/rubocop/cop/rails/where_exists.rb +5 -5
- data/lib/rubocop/cop/rails_cops.rb +5 -0
- data/lib/rubocop/rails/version.rb +1 -1
- metadata +9 -4
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Rails
|
|
6
|
+
# Avoid memoizing `find_by` results with `||=`.
|
|
7
|
+
#
|
|
8
|
+
# It is common to see code that attempts to memoize `find_by` result by `||=`,
|
|
9
|
+
# but `find_by` may return `nil`, in which case it is not memoized as intended.
|
|
10
|
+
#
|
|
11
|
+
# NOTE: Respecting the object shapes introduced in Ruby 3.2, instance variables used
|
|
12
|
+
# for memoization that are initialized at object creation are ignored.
|
|
13
|
+
#
|
|
14
|
+
# @safety
|
|
15
|
+
# This cop is unsafe because detected `find_by` may not be Active Record's method,
|
|
16
|
+
# or the code may have a different purpose than memoization.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# # bad - exclusively doing memoization
|
|
20
|
+
# def current_user
|
|
21
|
+
# @current_user ||= User.find_by(id: session[:user_id])
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# def current_user
|
|
26
|
+
# return @current_user if defined?(@current_user)
|
|
27
|
+
#
|
|
28
|
+
# @current_user = User.find_by(id: session[:user_id])
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# # bad - method contains other code
|
|
32
|
+
# def current_user
|
|
33
|
+
# @current_user ||= User.find_by(id: session[:user_id])
|
|
34
|
+
# @current_user.do_something
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# # good
|
|
38
|
+
# def current_user
|
|
39
|
+
# if defined?(@current_user)
|
|
40
|
+
# @current_user
|
|
41
|
+
# else
|
|
42
|
+
# @current_user = User.find_by(id: session[:user_id])
|
|
43
|
+
# end
|
|
44
|
+
# @current_user.do_something
|
|
45
|
+
# end
|
|
46
|
+
class FindByOrAssignmentMemoization < Base
|
|
47
|
+
extend AutoCorrector
|
|
48
|
+
|
|
49
|
+
MSG = 'Avoid memoizing `find_by` results with `||=`.'
|
|
50
|
+
|
|
51
|
+
RESTRICT_ON_SEND = %i[find_by].freeze
|
|
52
|
+
|
|
53
|
+
def_node_matcher :find_by_or_assignment_memoization, <<~PATTERN
|
|
54
|
+
(or_asgn
|
|
55
|
+
(ivasgn $_)
|
|
56
|
+
$(send _ :find_by ...)
|
|
57
|
+
)
|
|
58
|
+
PATTERN
|
|
59
|
+
|
|
60
|
+
# When a method body contains only memoization, the correction can be more succinct.
|
|
61
|
+
def on_def(node)
|
|
62
|
+
find_by_or_assignment_memoization(node.body) do |variable_name, find_by|
|
|
63
|
+
next if instance_variable_assigned?(variable_name)
|
|
64
|
+
|
|
65
|
+
add_offense(node.body) do |corrector|
|
|
66
|
+
corrector.replace(
|
|
67
|
+
node.body,
|
|
68
|
+
<<~RUBY.rstrip
|
|
69
|
+
return #{variable_name} if defined?(#{variable_name})
|
|
70
|
+
|
|
71
|
+
#{variable_name} = #{find_by.source}
|
|
72
|
+
RUBY
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
correct_to_regular_method_definition(corrector, node) if node.endless?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def on_send(node)
|
|
81
|
+
assignment_node = node.parent
|
|
82
|
+
|
|
83
|
+
find_by_or_assignment_memoization(assignment_node) do |variable_name, find_by|
|
|
84
|
+
next if assignment_node.each_ancestor(:if).any? || instance_variable_assigned?(variable_name)
|
|
85
|
+
|
|
86
|
+
add_offense(assignment_node) do |corrector|
|
|
87
|
+
corrector.replace(
|
|
88
|
+
assignment_node,
|
|
89
|
+
<<~RUBY.rstrip
|
|
90
|
+
if defined?(#{variable_name})
|
|
91
|
+
#{variable_name}
|
|
92
|
+
else
|
|
93
|
+
#{variable_name} = #{find_by.source}
|
|
94
|
+
end
|
|
95
|
+
RUBY
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def instance_variable_assigned?(instance_variable_name)
|
|
104
|
+
initialize_methods.any? do |def_node|
|
|
105
|
+
def_node.each_descendant(:ivasgn).any? do |asgn_node|
|
|
106
|
+
asgn_node.name == instance_variable_name
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def initialize_methods
|
|
112
|
+
@initialize_methods ||= processed_source.ast.each_descendant(:def).select { |node| node.method?(:initialize) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def correct_to_regular_method_definition(corrector, node)
|
|
116
|
+
range = node.loc.assignment.join(node.body.source_range.begin)
|
|
117
|
+
|
|
118
|
+
corrector.replace(range, "\n")
|
|
119
|
+
corrector.insert_after(node, "\nend")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -13,7 +13,7 @@ module RuboCop
|
|
|
13
13
|
# variable, consider moving the behavior elsewhere, for
|
|
14
14
|
# example to a model, decorator or presenter.
|
|
15
15
|
#
|
|
16
|
-
# Provided that a class
|
|
16
|
+
# Provided that an instance variable belongs to a class,
|
|
17
17
|
# an offense will not be registered.
|
|
18
18
|
#
|
|
19
19
|
# @example
|
|
@@ -28,38 +28,37 @@ module RuboCop
|
|
|
28
28
|
# end
|
|
29
29
|
#
|
|
30
30
|
# # good
|
|
31
|
-
#
|
|
32
|
-
#
|
|
31
|
+
# module ButtonHelper
|
|
32
|
+
# class Welcome
|
|
33
|
+
# def initialize(text:)
|
|
34
|
+
# @text = text
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# def welcome(**)
|
|
39
|
+
# render Welcome.new(**)
|
|
40
|
+
# end
|
|
33
41
|
# end
|
|
42
|
+
#
|
|
34
43
|
class HelperInstanceVariable < Base
|
|
35
44
|
MSG = 'Do not use instance variables in helpers.'
|
|
36
45
|
|
|
37
|
-
def_node_matcher :form_builder_class?, <<~PATTERN
|
|
38
|
-
(const
|
|
39
|
-
(const
|
|
40
|
-
(const {nil? cbase} :ActionView) :Helpers) :FormBuilder)
|
|
41
|
-
PATTERN
|
|
42
|
-
|
|
43
46
|
def on_ivar(node)
|
|
44
|
-
return if
|
|
47
|
+
return if instance_variable_belongs_to_class?(node)
|
|
45
48
|
|
|
46
49
|
add_offense(node)
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
def on_ivasgn(node)
|
|
50
|
-
return if node.parent.or_asgn_type? ||
|
|
53
|
+
return if node.parent.or_asgn_type? || instance_variable_belongs_to_class?(node)
|
|
51
54
|
|
|
52
55
|
add_offense(node.loc.name)
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
private
|
|
56
59
|
|
|
57
|
-
def
|
|
58
|
-
node.each_ancestor(:class)
|
|
59
|
-
return true if form_builder_class?(class_node.parent_class)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
false
|
|
60
|
+
def instance_variable_belongs_to_class?(node)
|
|
61
|
+
node.each_ancestor(:class).any?
|
|
63
62
|
end
|
|
64
63
|
end
|
|
65
64
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Rails
|
|
6
|
+
# Enforces consistency by using the current HTTP status names.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# render json: { error: "Invalid data" }, status: :unprocessable_entity
|
|
11
|
+
# head :payload_too_large
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# render json: { error: "Invalid data" }, status: :unprocessable_content
|
|
15
|
+
# head :content_too_large
|
|
16
|
+
#
|
|
17
|
+
class HttpStatusNameConsistency < Base
|
|
18
|
+
extend AutoCorrector
|
|
19
|
+
|
|
20
|
+
requires_gem 'rack', '>= 3.1.0'
|
|
21
|
+
|
|
22
|
+
RESTRICT_ON_SEND = %i[render redirect_to head assert_response assert_redirected_to].freeze
|
|
23
|
+
|
|
24
|
+
PREFERRED_STATUSES = {
|
|
25
|
+
unprocessable_entity: :unprocessable_content,
|
|
26
|
+
payload_too_large: :content_too_large
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
MSG = 'Prefer `:%<preferred>s` over `:%<current>s`.'
|
|
30
|
+
|
|
31
|
+
def_node_matcher :status_method_call, <<~PATTERN
|
|
32
|
+
{
|
|
33
|
+
(send nil? {:render :redirect_to} _ $hash)
|
|
34
|
+
(send nil? {:render :redirect_to} $hash)
|
|
35
|
+
(send nil? {:head :assert_response} $_ ...)
|
|
36
|
+
(send nil? :assert_redirected_to _ $hash ...)
|
|
37
|
+
(send nil? :assert_redirected_to $hash ...)
|
|
38
|
+
}
|
|
39
|
+
PATTERN
|
|
40
|
+
|
|
41
|
+
def_node_matcher :status_hash_value, <<~PATTERN
|
|
42
|
+
(hash <(pair (sym :status) $_) ...>)
|
|
43
|
+
PATTERN
|
|
44
|
+
|
|
45
|
+
def on_send(node)
|
|
46
|
+
status_method_call(node) do |status_node|
|
|
47
|
+
if status_node.hash_type?
|
|
48
|
+
# Handle hash arguments like { status: :unprocessable_entity }
|
|
49
|
+
status_hash_value(status_node) do |status_value|
|
|
50
|
+
check_status_name_consistency(status_value)
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
# Handle positional arguments like head :unprocessable_entity
|
|
54
|
+
check_status_name_consistency(status_node)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def check_status_name_consistency(node)
|
|
62
|
+
if node.sym_type? && PREFERRED_STATUSES.key?(node.value)
|
|
63
|
+
current_status = node.value
|
|
64
|
+
preferred_status = PREFERRED_STATUSES[current_status]
|
|
65
|
+
|
|
66
|
+
message = format(MSG, current: current_status, preferred: preferred_status)
|
|
67
|
+
|
|
68
|
+
add_offense(node, message: message) do |corrector|
|
|
69
|
+
corrector.replace(node, ":#{preferred_status}")
|
|
70
|
+
end
|
|
71
|
+
else
|
|
72
|
+
node.children.each do |child|
|
|
73
|
+
check_status_name_consistency(child) if child.is_a?(Parser::AST::Node)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -8,6 +8,11 @@ module RuboCop
|
|
|
8
8
|
# an enumerable into a hash where the keys are the original elements.
|
|
9
9
|
# Rails provides the `index_with` method for this purpose.
|
|
10
10
|
#
|
|
11
|
+
# @safety
|
|
12
|
+
# This cop is marked as unsafe autocorrection, because `nil.to_h` returns {}
|
|
13
|
+
# but `nil.with_index` throws `NoMethodError`. Therefore, autocorrection is not
|
|
14
|
+
# compatible if the receiver is nil.
|
|
15
|
+
#
|
|
11
16
|
# @example
|
|
12
17
|
# # bad
|
|
13
18
|
# [1, 2, 3].each_with_object({}) { |el, h| h[el] = foo(el) }
|
|
@@ -178,6 +178,7 @@ module RuboCop
|
|
|
178
178
|
(pair (sym :inverse_of) nil)
|
|
179
179
|
PATTERN
|
|
180
180
|
|
|
181
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
181
182
|
def on_send(node)
|
|
182
183
|
recv, arguments = association_recv_arguments(node)
|
|
183
184
|
return unless arguments
|
|
@@ -192,9 +193,11 @@ module RuboCop
|
|
|
192
193
|
return unless scope?(arguments) || options_requiring_inverse_of?(options)
|
|
193
194
|
|
|
194
195
|
return if options_contain_inverse_of?(options)
|
|
196
|
+
return if dynamic_options?(options) && options.none? { |opt| inverse_of_nil_option?(opt) }
|
|
195
197
|
|
|
196
198
|
add_offense(node.loc.selector, message: message(options))
|
|
197
199
|
end
|
|
200
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
198
201
|
|
|
199
202
|
def scope?(arguments)
|
|
200
203
|
!ignore_scopes? && arguments.any?(&:block_type?)
|
|
@@ -216,6 +219,10 @@ module RuboCop
|
|
|
216
219
|
end
|
|
217
220
|
end
|
|
218
221
|
|
|
222
|
+
def dynamic_options?(options)
|
|
223
|
+
options.any? { |option| option&.kwsplat_type? }
|
|
224
|
+
end
|
|
225
|
+
|
|
219
226
|
def options_contain_inverse_of?(options)
|
|
220
227
|
options.any? { |opt| inverse_of_option?(opt) }
|
|
221
228
|
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Rails
|
|
6
|
+
# Prefer symbol arguments over strings in `order` method.
|
|
7
|
+
#
|
|
8
|
+
# @safety
|
|
9
|
+
# Cop is unsafe because the receiver might not be an Active Record query.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# User.order('name')
|
|
14
|
+
# User.order('name DESC')
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# User.order(:name)
|
|
18
|
+
# User.order(name: :desc)
|
|
19
|
+
#
|
|
20
|
+
class OrderArguments < Base
|
|
21
|
+
extend AutoCorrector
|
|
22
|
+
|
|
23
|
+
MSG = 'Prefer `%<prefer>s` instead.'
|
|
24
|
+
|
|
25
|
+
RESTRICT_ON_SEND = %i[order].freeze
|
|
26
|
+
|
|
27
|
+
def_node_matcher :string_order, <<~PATTERN
|
|
28
|
+
(call _ :order (str $_value)+)
|
|
29
|
+
PATTERN
|
|
30
|
+
|
|
31
|
+
ORDER_EXPRESSION_REGEX = /\A(\w+) ?(asc|desc)?\z/i.freeze
|
|
32
|
+
|
|
33
|
+
def on_send(node)
|
|
34
|
+
return unless (current_expressions = string_order(node))
|
|
35
|
+
return unless (preferred_expressions = replacement(current_expressions))
|
|
36
|
+
|
|
37
|
+
offense_range = find_offense_range(node)
|
|
38
|
+
add_offense(offense_range, message: format(MSG, prefer: preferred_expressions)) do |corrector|
|
|
39
|
+
corrector.replace(offense_range, preferred_expressions)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
alias on_csend on_send
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def find_offense_range(node)
|
|
47
|
+
node.first_argument.source_range.join(node.last_argument.source_range)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def replacement(order_expressions)
|
|
51
|
+
order_arguments = order_expressions.flat_map { |expr| expr.split(',') }
|
|
52
|
+
order_arguments.map! { |arg| extract_column_and_direction(arg.strip) }
|
|
53
|
+
|
|
54
|
+
return if order_arguments.any?(&:nil?)
|
|
55
|
+
return if order_arguments.any? { |column_name, _| positional_column?(column_name) }
|
|
56
|
+
|
|
57
|
+
convert_to_preferred_arguments(order_arguments).join(', ')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def convert_to_preferred_arguments(order_expressions)
|
|
61
|
+
use_hash = false
|
|
62
|
+
order_expressions.map do |column, direction|
|
|
63
|
+
if direction == :asc && !use_hash
|
|
64
|
+
":#{column}"
|
|
65
|
+
else
|
|
66
|
+
use_hash = true
|
|
67
|
+
"#{column}: :#{direction}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def positional_column?(column_name)
|
|
73
|
+
column_name.match?(/\A\d+\z/)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def extract_column_and_direction(order_expression)
|
|
77
|
+
return unless (column, direction = ORDER_EXPRESSION_REGEX.match(order_expression)&.captures)
|
|
78
|
+
|
|
79
|
+
[column.downcase, direction&.downcase&.to_sym || :asc]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -38,9 +38,11 @@ module RuboCop
|
|
|
38
38
|
...)
|
|
39
39
|
PATTERN
|
|
40
40
|
|
|
41
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
41
42
|
def on_send(node)
|
|
42
43
|
return if node.parent&.call_type? || node.block_node
|
|
43
44
|
return if !output?(node) && !io_output?(node)
|
|
45
|
+
return if node.arguments.any? { |arg| arg.type?(:hash, :block_pass) }
|
|
44
46
|
|
|
45
47
|
range = offense_range(node)
|
|
46
48
|
|
|
@@ -48,6 +50,7 @@ module RuboCop
|
|
|
48
50
|
corrector.replace(range, 'Rails.logger.debug')
|
|
49
51
|
end
|
|
50
52
|
end
|
|
53
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
51
54
|
|
|
52
55
|
private
|
|
53
56
|
|
|
@@ -84,7 +84,9 @@ module RuboCop
|
|
|
84
84
|
private
|
|
85
85
|
|
|
86
86
|
def non_interpolated_string?(node)
|
|
87
|
-
|
|
87
|
+
return false unless (receiver = node.receiver)
|
|
88
|
+
|
|
89
|
+
receiver.str_type? || (receiver.dstr_type? && receiver.children.all?(&:str_type?))
|
|
88
90
|
end
|
|
89
91
|
|
|
90
92
|
def looks_like_rails_html_safe?(node)
|
|
@@ -27,6 +27,9 @@ module RuboCop
|
|
|
27
27
|
# end
|
|
28
28
|
# ----
|
|
29
29
|
#
|
|
30
|
+
# If a method call has no receiver, like `do_something { users.map { |user| user[:foo] }`,
|
|
31
|
+
# it is not considered part of an iteration and will be detected.
|
|
32
|
+
#
|
|
30
33
|
# @safety
|
|
31
34
|
# This cop is unsafe because model can use column aliases.
|
|
32
35
|
#
|
|
@@ -59,9 +62,9 @@ module RuboCop
|
|
|
59
62
|
(any_block (call _ {:map :collect}) $_argument (send lvar :[] $_key))
|
|
60
63
|
PATTERN
|
|
61
64
|
|
|
62
|
-
# rubocop:disable Metrics/AbcSize
|
|
65
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
63
66
|
def on_block(node)
|
|
64
|
-
return if node.each_ancestor(:any_block).
|
|
67
|
+
return if node.each_ancestor(:any_block).first&.receiver
|
|
65
68
|
|
|
66
69
|
pluck_candidate?(node) do |argument, key|
|
|
67
70
|
next if key.regexp_type? || !use_one_block_argument?(argument)
|
|
@@ -79,7 +82,7 @@ module RuboCop
|
|
|
79
82
|
register_offense(node, key)
|
|
80
83
|
end
|
|
81
84
|
end
|
|
82
|
-
# rubocop:enable Metrics/AbcSize
|
|
85
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
83
86
|
alias on_numblock on_block
|
|
84
87
|
alias on_itblock on_block
|
|
85
88
|
|
|
@@ -37,38 +37,65 @@ module RuboCop
|
|
|
37
37
|
#
|
|
38
38
|
# # good
|
|
39
39
|
# a.presence || b
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# # bad
|
|
43
|
+
# a.present? ? a.foo : nil
|
|
44
|
+
#
|
|
45
|
+
# # bad
|
|
46
|
+
# !a.present? ? nil : a.foo
|
|
47
|
+
#
|
|
48
|
+
# # bad
|
|
49
|
+
# a.blank? ? nil : a.foo
|
|
50
|
+
#
|
|
51
|
+
# # bad
|
|
52
|
+
# !a.blank? ? a.foo : nil
|
|
53
|
+
#
|
|
54
|
+
# # good
|
|
55
|
+
# a.presence&.foo
|
|
56
|
+
#
|
|
57
|
+
# # good
|
|
58
|
+
# a.present? ? a[1] : nil
|
|
59
|
+
#
|
|
60
|
+
# # good
|
|
61
|
+
# a[:key] = value if a.present?
|
|
62
|
+
#
|
|
63
|
+
# # good
|
|
64
|
+
# a.present? ? a > 1 : nil
|
|
65
|
+
# a <= 0 if a.present?
|
|
40
66
|
class Presence < Base
|
|
41
67
|
include RangeHelp
|
|
42
68
|
extend AutoCorrector
|
|
43
69
|
|
|
44
70
|
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
|
|
71
|
+
INDEX_ACCESS_METHODS = %i[[] []=].freeze
|
|
45
72
|
|
|
46
73
|
def_node_matcher :redundant_receiver_and_other, <<~PATTERN
|
|
47
74
|
{
|
|
48
75
|
(if
|
|
49
|
-
(send $_recv :present?)
|
|
50
|
-
_recv
|
|
76
|
+
{(send $_recv :blank?) (send (send $_recv :present?) :!)}
|
|
51
77
|
$!begin
|
|
78
|
+
_recv
|
|
52
79
|
)
|
|
53
80
|
(if
|
|
54
|
-
(send $_recv :blank?)
|
|
55
|
-
$!begin
|
|
81
|
+
{(send $_recv :present?) (send (send $_recv :blank?) :!)}
|
|
56
82
|
_recv
|
|
83
|
+
$!begin
|
|
57
84
|
)
|
|
58
85
|
}
|
|
59
86
|
PATTERN
|
|
60
87
|
|
|
61
|
-
def_node_matcher :
|
|
88
|
+
def_node_matcher :redundant_receiver_and_chain, <<~PATTERN
|
|
62
89
|
{
|
|
63
90
|
(if
|
|
64
|
-
(send (send $_recv :present?) :!)
|
|
65
|
-
|
|
66
|
-
_recv
|
|
91
|
+
{(send $_recv :blank?) (send (send $_recv :present?) :!)}
|
|
92
|
+
{nil? nil_type?}
|
|
93
|
+
$(send _recv ...)
|
|
67
94
|
)
|
|
68
95
|
(if
|
|
69
|
-
(send (send $_recv :blank?) :!)
|
|
70
|
-
_recv
|
|
71
|
-
|
|
96
|
+
{(send $_recv :present?) (send (send $_recv :blank?) :!)}
|
|
97
|
+
$(send _recv ...)
|
|
98
|
+
{nil? nil_type?}
|
|
72
99
|
)
|
|
73
100
|
}
|
|
74
101
|
PATTERN
|
|
@@ -82,18 +109,26 @@ module RuboCop
|
|
|
82
109
|
register_offense(node, receiver, other)
|
|
83
110
|
end
|
|
84
111
|
|
|
85
|
-
|
|
86
|
-
return if
|
|
112
|
+
redundant_receiver_and_chain(node) do |receiver, chain|
|
|
113
|
+
return if ignore_chain_node?(chain) || receiver.nil?
|
|
87
114
|
|
|
88
|
-
|
|
115
|
+
register_chain_offense(node, receiver, chain)
|
|
89
116
|
end
|
|
90
117
|
end
|
|
91
118
|
|
|
92
119
|
private
|
|
93
120
|
|
|
94
121
|
def register_offense(node, receiver, other)
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
replacement = replacement(receiver, other, node.left_sibling)
|
|
123
|
+
add_offense(node, message: message(node, replacement)) do |corrector|
|
|
124
|
+
corrector.replace(node, replacement)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def register_chain_offense(node, receiver, chain)
|
|
129
|
+
replacement = chain_replacement(receiver, chain, node.left_sibling)
|
|
130
|
+
add_offense(node, message: message(node, replacement)) do |corrector|
|
|
131
|
+
corrector.replace(node, replacement)
|
|
97
132
|
end
|
|
98
133
|
end
|
|
99
134
|
|
|
@@ -105,8 +140,12 @@ module RuboCop
|
|
|
105
140
|
node&.type?(:if, :rescue, :while)
|
|
106
141
|
end
|
|
107
142
|
|
|
108
|
-
def
|
|
109
|
-
|
|
143
|
+
def ignore_chain_node?(node)
|
|
144
|
+
index_access_method?(node) || node.assignment? || node.arithmetic_operation? || node.comparison_method?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def message(node, replacement)
|
|
148
|
+
prefer = replacement.gsub(/^\s*|\n/, '')
|
|
110
149
|
current = current(node).gsub(/^\s*|\n/, '')
|
|
111
150
|
format(MSG, prefer: prefer, current: current)
|
|
112
151
|
end
|
|
@@ -146,6 +185,16 @@ module RuboCop
|
|
|
146
185
|
def method_range(node)
|
|
147
186
|
range_between(node.source_range.begin_pos, node.first_argument.source_range.begin_pos - 1)
|
|
148
187
|
end
|
|
188
|
+
|
|
189
|
+
def chain_replacement(receiver, chain, left_sibling)
|
|
190
|
+
replaced = "#{receiver.source}.presence&.#{chain.method_name}"
|
|
191
|
+
replaced += "(#{chain.arguments.map(&:source).join(', ')})" if chain.arguments?
|
|
192
|
+
left_sibling ? "(#{replaced})" : replaced
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def index_access_method?(node)
|
|
196
|
+
INDEX_ACCESS_METHODS.include?(node.method_name)
|
|
197
|
+
end
|
|
149
198
|
end
|
|
150
199
|
end
|
|
151
200
|
end
|
|
@@ -66,7 +66,7 @@ module RuboCop
|
|
|
66
66
|
return false unless enclosing_method
|
|
67
67
|
|
|
68
68
|
shadowing_method_name = first_arg.value.to_s
|
|
69
|
-
shadowing_method_name
|
|
69
|
+
shadowing_method_name += '=' if node.method?(:write_attribute)
|
|
70
70
|
enclosing_method.method?(shadowing_method_name)
|
|
71
71
|
end
|
|
72
72
|
|