/
safe_navigation_chain.rb
99 lines (89 loc) · 2.95 KB
/
safe_navigation_chain.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
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# The safe navigation operator returns nil if the receiver is
# nil. If you chain an ordinary method call after a safe
# navigation operator, it raises NoMethodError. We should use a
# safe navigation operator after a safe navigation operator.
# This cop checks for the problem outlined above.
#
# @example
#
# # bad
#
# x&.foo.bar
# x&.foo + bar
# x&.foo[bar]
#
# @example
#
# # good
#
# x&.foo&.bar
# x&.foo || bar
class SafeNavigationChain < Base
include NilMethods
extend AutoCorrector
extend TargetRubyVersion
minimum_target_ruby_version 2.3
MSG = 'Do not chain ordinary method call after safe navigation operator.'
# @!method bad_method?(node)
def_node_matcher :bad_method?, <<~PATTERN
{
(send $(csend ...) $_ ...)
(send $({block numblock} (csend ...) ...) $_ ...)
}
PATTERN
def on_send(node)
bad_method?(node) do |safe_nav, method|
return if nil_methods.include?(method)
method_chain = method_chain(node)
location =
Parser::Source::Range.new(node.source_range.source_buffer,
safe_nav.source_range.end_pos,
method_chain.source_range.end_pos)
add_offense(location) do |corrector|
autocorrect(corrector, offense_range: location, send_node: method_chain)
end
end
end
private
# @param [Parser::Source::Range] offense_range
# @param [RuboCop::AST::SendNode] send_node
# @return [String]
def add_safe_navigation_operator(offense_range:, send_node:)
source = \
if send_node.method?(:[]) || send_node.method?(:[]=)
format(
'%<method_name>s(%<arguments>s)',
arguments: send_node.arguments.map(&:source).join(', '),
method_name: send_node.method_name
)
else
offense_range.source.dup
end
source.prepend('.') unless send_node.dot?
source.prepend('&')
end
# @param [RuboCop::Cop::Corrector] corrector
# @param [Parser::Source::Range] offense_range
# @param [RuboCop::AST::SendNode] send_node
def autocorrect(corrector, offense_range:, send_node:)
corrector.replace(
offense_range,
add_safe_navigation_operator(
offense_range: offense_range,
send_node: send_node
)
)
end
def method_chain(node)
chain = node
chain = chain.parent if chain.send_type? && chain.parent&.call_type?
chain
end
end
end
end
end