/
guard_clause.cr
186 lines (156 loc) · 4.68 KB
/
guard_clause.cr
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
module Ameba::Rule::Style
# Use a guard clause instead of wrapping the code inside a conditional
# expression
#
# ```
# # bad
# def test
# if something
# work
# end
# end
#
# # good
# def test
# return unless something
#
# work
# end
#
# # also good
# def test
# work if something
# end
#
# # bad
# if something
# raise "exception"
# else
# ok
# end
#
# # good
# raise "exception" if something
# ok
#
# # bad
# if something
# foo || raise("exception")
# else
# ok
# end
#
# # good
# foo || raise("exception") if something
# ok
# ```
#
# YAML configuration example:
#
# ```
# Style/GuardClause:
# Enabled: true
# ```
class GuardClause < Base
include AST::Util
properties do
enabled false
description "Check for conditionals that can be replaced with guard clauses"
end
MSG = "Use a guard clause (`%s`) instead of wrapping the " \
"code inside a conditional expression."
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::Assign,
]
end
def test(source, node : Crystal::Def)
final_expression =
if (body = node.body).is_a?(Crystal::Expressions)
body.last
else
body
end
case final_expression
when Crystal::If, Crystal::Unless
check_ending_if(source, final_expression)
end
end
def test(source, node : Crystal::If | Crystal::Unless)
return if accepted_form?(source, node, ending: false)
case
when guard_clause = guard_clause(node.then)
parent, conditional_keyword = node.then, keyword(node)
when guard_clause = guard_clause(node.else)
parent, conditional_keyword = node.else, opposite_keyword(node)
end
return unless guard_clause && parent && conditional_keyword
guard_clause_source = guard_clause_source(source, guard_clause, parent)
report_issue(source, node, guard_clause_source, conditional_keyword)
end
private def check_ending_if(source, node)
return if accepted_form?(source, node, ending: true)
report_issue(source, node, "return", opposite_keyword(node))
end
private def report_issue(source, node, scope_exiting_keyword, conditional_keyword)
return unless keyword_loc = node.location
return unless cond_code = node_source(node.cond, source.lines)
keyword_end_loc = keyword_loc.adjust(column_number: keyword(node).size - 1)
example = "#{scope_exiting_keyword} #{conditional_keyword} #{cond_code}"
# TODO: check if example is too long for single line
if node.else.is_a?(Crystal::Nop)
return unless end_end_loc = node.end_location
end_loc = end_end_loc.adjust(column_number: {{ 1 - "end".size }})
issue_for keyword_loc, keyword_end_loc, MSG % example do |corrector|
replacement = "#{scope_exiting_keyword} #{conditional_keyword}"
corrector.replace(keyword_loc, keyword_end_loc, replacement)
corrector.remove(end_loc, end_end_loc)
end
else
issue_for keyword_loc, keyword_end_loc, MSG % example
end
end
private def keyword(node : Crystal::If)
"if"
end
private def keyword(node : Crystal::Unless)
"unless"
end
private def opposite_keyword(node : Crystal::If)
"unless"
end
private def opposite_keyword(node : Crystal::Unless)
"if"
end
private def accepted_form?(source, node, ending)
return true if node.is_a?(Crystal::If) && node.ternary?
return true unless cond_loc = node.cond.location
return true unless cond_end_loc = node.cond.end_location
return true unless cond_loc.line_number == cond_end_loc.line_number
return true unless (then_loc = node.then.location).nil? || cond_loc < then_loc
if ending
!node.else.is_a?(Crystal::Nop)
else
return true if node.else.is_a?(Crystal::Nop)
return true unless code = node_source(node, source.lines)
code.starts_with?("elsif")
end
end
private def guard_clause(node)
node = node.right if node.is_a?(Crystal::BinaryOp)
return unless location = node.location
return unless end_location = node.end_location
return unless location.line_number == end_location.line_number
case node
when Crystal::Call
node if node.obj.nil? && node.name == "raise"
when Crystal::Return, Crystal::Break, Crystal::Next
node
end
end
def guard_clause_source(source, guard_clause, parent)
node = parent.is_a?(Crystal::BinaryOp) ? parent : guard_clause
node_source(node, source.lines)
end
end
end