/
util.cr
248 lines (218 loc) · 7.28 KB
/
util.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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# Utility module for Ameba's rules.
module Ameba::AST::Util
# Returns tuple with two bool flags:
#
# 1. is *node* a literal?
# 2. can *node* be proven static?
protected def literal_kind?(node) : {Bool, Bool}
case node
when Crystal::NilLiteral,
Crystal::BoolLiteral,
Crystal::NumberLiteral,
Crystal::CharLiteral,
Crystal::StringLiteral,
Crystal::SymbolLiteral,
Crystal::RegexLiteral,
Crystal::ProcLiteral,
Crystal::MacroLiteral
{true, true}
when Crystal::RangeLiteral
{true, static_literal?(node.from) &&
static_literal?(node.to)}
when Crystal::ArrayLiteral,
Crystal::TupleLiteral
{true, node.elements.all? do |element|
static_literal?(element)
end}
when Crystal::HashLiteral
{true, node.entries.all? do |entry|
static_literal?(entry.key) &&
static_literal?(entry.value)
end}
when Crystal::NamedTupleLiteral
{true, node.entries.all? do |entry|
static_literal?(entry.value)
end}
else
{false, false}
end
end
# Returns `true` if current `node` is a static literal, `false` otherwise.
def static_literal?(node) : Bool
is_literal, is_static = literal_kind?(node)
is_literal && is_static
end
# Returns `true` if current `node` is a dynamic literal, `false` otherwise.
def dynamic_literal?(node) : Bool
is_literal, is_static = literal_kind?(node)
is_literal && !is_static
end
# Returns `true` if current `node` is a literal, `false` otherwise.
def literal?(node) : Bool
is_literal, _ = literal_kind?(node)
is_literal
end
# Returns `true` if current `node` is a `Crystal::Path`
# matching given *name*, `false` otherwise.
def path_named?(node, name) : Bool
node.is_a?(Crystal::Path) &&
name == node.names.join("::")
end
# Returns a source code for the current node.
# This method uses `node.location` and `node.end_location`
# to determine and cut a piece of source of the node.
def node_source(node, code_lines)
loc, end_loc = node.location, node.end_location
return unless loc && end_loc
source_between(loc, end_loc, code_lines)
end
# Returns the source code from *loc* to *end_loc* (inclusive).
def source_between(loc, end_loc, code_lines) : String?
line, column = loc.line_number - 1, loc.column_number - 1
end_line, end_column = end_loc.line_number - 1, end_loc.column_number - 1
node_lines = code_lines[line..end_line]
first_line, last_line = node_lines[0]?, node_lines[-1]?
return if first_line.nil? || last_line.nil?
return if first_line.size < column # compiler reports incorrect location
node_lines[0] = first_line.sub(0...column, "")
if line == end_line # one line
end_column = end_column - column
last_line = node_lines[0]
end
return if last_line.size < end_column + 1
node_lines[-1] = last_line.sub(end_column + 1...last_line.size, "")
node_lines.join('\n')
end
# Returns `true` if node is a flow command, `false` otherwise.
# Node represents a flow command if it is a control expression,
# or special call node that interrupts execution (i.e. raise, exit, abort).
def flow_command?(node, in_loop)
case node
when Crystal::Return
true
when Crystal::Break, Crystal::Next
in_loop
when Crystal::Call
raise?(node) || exit?(node) || abort?(node)
else
false
end
end
# Returns `true` if node is a flow expression, `false` if not.
# Node represents a flow expression if it is full-filled by a flow command.
#
# For example, this node is a flow expression, because each branch contains
# a flow command `return`:
#
# ```
# if a > 0
# return :positive
# elsif a < 0
# return :negative
# else
# return :zero
# end
# ```
#
# This node is a not a flow expression:
#
# ```
# if a > 0
# return :positive
# end
# ```
#
# That's because not all branches return(i.e. `else` is missing).
def flow_expression?(node, in_loop = false)
return true if flow_command? node, in_loop
case node
when Crystal::If, Crystal::Unless
flow_expressions? [node.then, node.else], in_loop
when Crystal::BinaryOp
flow_expression? node.left, in_loop
when Crystal::Case
flow_expressions? [node.whens, node.else].flatten, in_loop
when Crystal::ExceptionHandler
flow_expressions? [node.else || node.body, node.rescues].flatten, in_loop
when Crystal::While, Crystal::Until
flow_expression? node.body, in_loop
when Crystal::Rescue, Crystal::When
flow_expression? node.body, in_loop
when Crystal::Expressions
node.expressions.any? { |exp| flow_expression? exp, in_loop }
else
false
end
end
private def flow_expressions?(nodes, in_loop)
nodes.all? { |exp| flow_expression? exp, in_loop }
end
# Returns `true` if node represents `raise` method call.
def raise?(node)
node.is_a?(Crystal::Call) &&
node.name == "raise" && node.args.size == 1 && node.obj.nil?
end
# Returns `true` if node represents `exit` method call.
def exit?(node)
node.is_a?(Crystal::Call) &&
node.name == "exit" && node.args.size <= 1 && node.obj.nil?
end
# Returns `true` if node represents `abort` method call.
def abort?(node)
node.is_a?(Crystal::Call) &&
node.name == "abort" && node.args.size <= 2 && node.obj.nil?
end
# Returns `true` if node represents a loop.
def loop?(node)
case node
when Crystal::While, Crystal::Until
true
when Crystal::Call
node.name == "loop" && node.args.size == 0 && node.obj.nil?
else
false
end
end
# Returns the exp code of a control expression.
# Wraps implicit tuple literal with curly brackets (e.g. multi-return).
def control_exp_code(node : Crystal::ControlExpression, code_lines)
return unless exp = node.exp
return unless exp_code = node_source(exp, code_lines)
return exp_code unless exp.is_a?(Crystal::TupleLiteral) && exp_code[0] != '{'
return unless exp_start = exp.elements.first.location
return unless exp_end = exp.end_location
"{#{source_between(exp_start, exp_end, code_lines)}}"
end
# Returns `nil` if *node* does not contain a name.
def name_location(node)
if loc = node.name_location
return loc
end
return node.var.location if node.is_a?(Crystal::TypeDeclaration) ||
node.is_a?(Crystal::UninitializedVar)
return unless node.responds_to?(:name) && (name = node.name)
return unless name.is_a?(Crystal::ASTNode)
name.location
end
# Returns zero if *node* does not contain a name.
def name_size(node)
unless (size = node.name_size).zero?
return size
end
return 0 unless node.responds_to?(:name) && (name = node.name)
case name
when Crystal::ASTNode then name.name_size
when Crystal::Token::Kind then name.to_s.size # Crystal::MagicConstant
else name.size
end
end
# Returns `nil` if *node* does not contain a name.
#
# NOTE: Use this instead of `Crystal::Call#name_end_location` to avoid an
# off-by-one error.
def name_end_location(node)
return unless loc = name_location(node)
return if (size = name_size(node)).zero?
loc.adjust(column_number: size - 1)
end
end