/
exception.cr
400 lines (331 loc) · 10.8 KB
/
exception.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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
require "../exception"
require "../types"
module Crystal
class TypeException < CodeError
include ErrorFormat
getter node
property inner : CodeError?
getter line_number : Int32?
getter column_number : Int32
getter size : Int32
def color=(color)
@color = !!color
inner.try &.color=(color)
end
def error_trace=(error_trace)
@error_trace = !!error_trace
inner.try &.error_trace=(error_trace)
end
def warning=(warning)
super
inner.try &.warning=(warning)
end
def self.for_node(node, message, inner = nil)
location = node.name_location || node.location
if location
ex = new message, location.line_number, location.column_number, location.filename, node.name_size, inner
wrap_macro_expression(ex, location)
else
new message, nil, 0, nil, 0, inner
end
end
def initialize(message, @line_number, @column_number : Int32, @filename, @size, @inner = nil)
@error_trace = true
# If the inner exception is a macro raise, we replace this exception's
# message with that message. In this way the error message will
# look like a regular message produced by the compiler, and not
# because of an incorrect macro expansion.
if inner.is_a?(MacroRaiseException)
message = inner.message
@inner = nil
end
super(message)
end
def self.new(message : String)
new message, nil, 0, nil, 0
end
def self.new(message : String, location : Location)
ex = new message, location.line_number, location.column_number, location.filename, 0
wrap_macro_expression(ex, location)
end
protected def self.wrap_macro_expression(ex, location)
filename = location.filename
if filename.is_a?(VirtualFile) && (expanded_location = filename.expanded_location)
ex = TypeException.new "expanding macro", expanded_location.line_number, expanded_location.column_number, expanded_location.filename, 0, ex
end
ex
end
def to_json_single(json)
json.object do
json.field "file", true_filename
json.field "line", @line_number
json.field "column", @column_number
json.field "size", @size
json.field "message", @message
end
if inner = @inner
inner.to_json_single(json)
end
end
def inspect_with_backtrace(io : IO) : Nil
to_s(io)
backtrace?.try &.each do |frame|
io.print " from "
io.puts frame
end
io.flush
end
def to_s_with_source(io : IO, source)
append_to_s io, source
end
def append_to_s(io : IO, source)
inner = @inner
unless @error_trace || inner.is_a? MethodTraceException
if inner && inner.has_location?
return inner.append_to_s(io, source)
end
end
# If the inner exception has no location it means that they came from virtual nodes.
# In that case, get the deepest error message and only show that.
if inner && !inner.has_location?
msg = deepest_error_message.to_s
else
msg = @message.to_s
end
error_message_lines = msg.lines
unless @error_trace || @warning
io << colorize("Showing last frame. Use --error-trace for full trace.").dim
io << "\n\n"
end
if body = error_body(source, default_message)
io << body
io << '\n'
end
unless error_message_lines.empty?
io << error_headline(error_message_lines.shift)
io << remaining error_message_lines
end
if inner
return if inner.is_a? MethodTraceException && !inner.has_message?
return unless inner.has_location?
io << "\n\n"
io << '\n' unless inner.is_a? MethodTraceException
inner.append_to_s io, source
end
end
def default_message
if line_number = @line_number
"#{@warning ? "warning" : "error"} in line #{@line_number}"
end
end
def error_headline(msg)
return "Warning: #{msg}" if @warning
if (inner = @inner) && !inner.is_a? MethodTraceException? && inner.has_location?
colorize("Error: #{msg}").yellow
else
colorize("Error: #{msg}").yellow.bold
end
end
def has_location?
if @inner.try &.has_location?
true
else
@filename || @line_number
end
end
def deepest_error_message
if inner = @inner
inner.deepest_error_message
else
@message
end
end
end
class MethodTraceException < CodeError
def initialize(@owner : Type?, @trace : Array(ASTNode), @nil_reason : NilReason?, @show : Bool)
super(nil)
end
def has_location?
true
end
def to_json_single(json)
end
def to_s_with_source(io : IO, source)
append_to_s(io, source)
end
def has_trace?
@trace.any?(&.location)
end
def has_message?
@nil_reason || has_trace? && @show
end
def append_to_s(io : IO, source)
nil_reason = @nil_reason
if !@show
if nil_reason
print_nil_reason(nil_reason, io)
if has_trace? || nil_reason.try(&.nodes)
io.puts
io.puts
end
end
return
end
if has_trace?
io << "#{@owner} trace:"
@trace.each do |node|
print_with_location node, io
end
end
return unless nil_reason
if has_trace?
io.puts
io.puts
end
print_nil_reason(nil_reason, io)
if nil_reason_nodes = nil_reason.nodes
nil_reason_nodes.each do |node|
print_with_location node, io
end
end
end
def print_nil_reason(nil_reason, io)
case nil_reason.reason
when :used_before_initialized
io << "Instance variable '#{nil_reason.name}' was used before it was initialized in one of the 'initialize' methods, rendering it nilable"
when :used_self_before_initialized
io << "'self' was used before initializing instance variable '#{nil_reason.name}', rendering it nilable"
when :initialized_in_rescue
io << "Instance variable '#{nil_reason.name}' is initialized inside a begin-rescue, so it can potentially be left uninitialized if an exception is raised and rescued"
else
# TODO: we should probably change nil_reason to be an enum so we don't need this else branch
end
end
def print_with_location(node, io)
location = node.location
return unless location
filename = location.filename
line_number = location.line_number
case filename
when VirtualFile
lines = filename.source.lines.to_a
filename = "macro #{filename.macro.name} (in #{filename.macro.location.try &.filename}:#{filename.macro.location.try &.line_number})"
when String
lines = File.read_lines(filename) if File.file?(filename)
else
return
end
io << "\n\n"
io << " "
io << relative_filename(filename) << ':' << line_number
io << "\n\n"
return unless lines
line = lines[line_number - 1]
name_location = node.name_location
name_size = node.name_size
io << " "
io << replace_leading_tabs_with_spaces(line.chomp)
io.puts
return unless name_location
io << " "
io << (" " * (name_location.column_number - 1))
with_color.green.bold.surround(io) do
io << '^'
if name_size > 0
io << ("~" * (name_size - 1)) if name_size
end
end
end
def deepest_error_message
nil
end
end
class FrozenTypeException < TypeException
end
class UndefinedMacroMethodError < TypeException
end
class MacroRaiseException < TypeException
end
class SkipMacroException < ::Exception
getter expanded_before_skip : String
getter macro_expansion_pragmas : Hash(Int32, Array(Lexer::LocPragma))?
def initialize(@expanded_before_skip, @macro_expansion_pragmas)
super()
end
end
class Program
def undefined_global_variable(node, similar_name)
common = String.build do |str|
str << "can't infer the type of global variable '#{node.name}'"
if similar_name
str << '\n'
str << colorize(" (did you mean #{similar_name}?)").yellow.bold.to_s
end
end
msg = String.build do |str|
str << common
str << "\n\n"
str << undefined_variable_message("global", node.name)
str << "\n\n"
str << common
end
node.raise msg
end
def undefined_class_variable(node, owner, similar_name)
common = String.build do |str|
str << "can't infer the type of class variable '#{node.name}' of #{owner.devirtualize}"
if similar_name
str << '\n'
str << "Did you mean '#{similar_name}'?"
end
end
msg = String.build do |str|
str << common
str << "\n\n"
str << undefined_variable_message("class", node.name)
str << "\n\n"
str << common
end
node.raise msg
end
def undefined_instance_variable(node, owner, similar_name)
common = String.build do |str|
str << "can't infer the type of instance variable '#{node.name}' of #{owner.devirtualize}"
if similar_name
str << '\n'
str << "Did you mean '#{similar_name}'?"
end
end
msg = String.build do |str|
str << common
str << "\n\n"
str << undefined_variable_message("instance", node.name)
str << "\n\n"
str << common
end
node.raise msg
end
def undefined_variable_message(kind, example_name)
<<-MSG
The type of a #{kind} variable, if not declared explicitly with
`#{example_name} : Type`, is inferred from assignments to it across
the whole program.
The assignments must look like this:
1. `#{example_name} = 1` (or other literals), inferred to the literal's type
2. `#{example_name} = Type.new`, type is inferred to be Type
3. `#{example_name} = Type.method`, where `method` has a return type
annotation, type is inferred from it
4. `#{example_name} = arg`, with 'arg' being a method argument with a
type restriction 'Type', type is inferred to be Type
5. `#{example_name} = arg`, with 'arg' being a method argument with a
default value, type is inferred using rules 1, 2 and 3 from it
6. `#{example_name} = uninitialized Type`, type is inferred to be Type
7. `#{example_name} = LibSome.func`, and `LibSome` is a `lib`, type
is inferred from that fun.
8. `LibSome.func(out #{example_name})`, and `LibSome` is a `lib`, type
is inferred from that fun argument.
Other assignments have no effect on its type.
MSG
end
end
end