forked from rubocop/rubocop
/
if_unless_modifier.rb
163 lines (136 loc) · 5.07 KB
/
if_unless_modifier.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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Checks for `if` and `unless` statements that would fit on one line if
# written as modifier `if`/`unless`. The cop also checks for modifier
# `if`/`unless` lines that exceed the maximum line length.
#
# The maximum line length is configured in the `Layout/LineLength`
# cop. The tab size is configured in the `IndentationWidth` of the
# `Layout/IndentationStyle` cop.
#
# @example
# # bad
# if condition
# do_stuff(bar)
# end
#
# unless qux.empty?
# Foo.do_something
# end
#
# do_something_in_a_method_with_a_long_name(arg) if long_condition
#
# # good
# do_stuff(bar) if condition
# Foo.do_something unless qux.empty?
#
# if long_condition
# do_something_in_a_method_with_a_long_name(arg)
# end
class IfUnlessModifier < Base
include StatementModifier
include LineLengthHelp
include IgnoredPattern
extend AutoCorrector
MSG_USE_MODIFIER = 'Favor modifier `%<keyword>s` usage when having a ' \
'single-line body. Another good alternative is ' \
'the usage of control flow `&&`/`||`.'
MSG_USE_NORMAL =
'Modifier form of `%<keyword>s` makes the line too long.'
def on_if(node)
msg = if single_line_as_modifier?(node) && !named_capture_in_condition?(node)
MSG_USE_MODIFIER
elsif too_long_due_to_modifier?(node)
MSG_USE_NORMAL
end
return unless msg
add_offense(node.loc.keyword, message: format(msg, keyword: node.keyword)) do |corrector|
autocorrect(corrector, node)
end
end
private
def autocorrect(corrector, node)
replacement = if node.modifier_form?
to_normal_form(node)
else
to_modifier_form(node)
end
corrector.replace(node, replacement)
end
def too_long_due_to_modifier?(node)
node.modifier_form? && too_long_single_line?(node) &&
!another_statement_on_same_line?(node)
end
def ignored_patterns
config.for_cop('Layout/LineLength')['IgnoredPatterns'] || []
end
def too_long_single_line?(node)
return false unless max_line_length
range = node.source_range
return false unless range.first_line == range.last_line
return false unless line_length_enabled_at_line?(range.first_line)
line = range.source_line
return false if line_length(line) <= max_line_length
too_long_line_based_on_config?(range, line)
end
def too_long_line_based_on_config?(range, line)
return false if matches_ignored_pattern?(line)
too_long = too_long_line_based_on_ignore_cop_directives?(range, line)
return too_long unless too_long == :undetermined
too_long_line_based_on_allow_uri?(line)
end
def too_long_line_based_on_ignore_cop_directives?(range, line)
if ignore_cop_directives? && directive_on_source_line?(range.line - 1)
return line_length_without_directive(line) > max_line_length
end
:undetermined
end
def too_long_line_based_on_allow_uri?(line)
if allow_uri?
uri_range = find_excessive_uri_range(line)
return false if uri_range && allowed_uri_position?(line, uri_range)
end
true
end
def line_length_enabled_at_line?(line)
processed_source.comment_config
.cop_enabled_at_line?('Layout/LineLength', line)
end
def named_capture_in_condition?(node)
node.condition.match_with_lvasgn_type?
end
def non_eligible_node?(node)
non_simple_if_unless?(node) ||
node.chained? ||
node.nested_conditional? ||
super
end
def non_simple_if_unless?(node)
node.ternary? || node.elsif? || node.else?
end
def another_statement_on_same_line?(node)
line_no = node.source_range.last_line
# traverse the AST upwards until we find a 'begin' node
# we want to look at the following child and see if it is on the
# same line as this 'if' node
while node && !node.begin_type?
index = node.sibling_index
node = node.parent
end
node && (sibling = node.children[index + 1]) &&
sibling.source_range.first_line == line_no
end
def to_normal_form(node)
indentation = ' ' * node.source_range.column
<<~RUBY.chomp
#{node.keyword} #{node.condition.source}
#{indentation} #{node.body.source}
#{indentation}end
RUBY
end
end
end
end
end