Skip to content

Commit 01474e0

Browse files
committed
Type checking of try
1 parent d0f7c03 commit 01474e0

File tree

4 files changed

+116
-97
lines changed

4 files changed

+116
-97
lines changed

lib/elixir/lib/module/types/expr.ex

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -241,46 +241,51 @@ defmodule Module.Types.Expr do
241241
|> dynamic_unless_static(stack)
242242
end
243243

244-
# TODO: case expr do pat -> expr end
245244
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do
246-
{_expr_type, context} = of_expr(case_expr, stack, context)
245+
{expr_type, context} = of_expr(case_expr, stack, context)
247246

248247
clauses
249-
|> of_clauses(stack, {none(), context})
248+
|> of_clauses(stack, [expr_type], {none(), context})
250249
|> dynamic_unless_static(stack)
251250
end
252251

253252
# TODO: fn pat -> expr end
254253
def of_expr({:fn, _meta, clauses}, stack, context) do
255-
{_acc, context} = of_clauses(clauses, stack, {none(), context})
254+
[{:->, _, [args, _]} | _] = clauses
255+
expected = Enum.map(args, fn _ -> dynamic() end)
256+
{_acc, context} = of_clauses(clauses, stack, expected, {none(), context})
256257
{fun(), context}
257258
end
258259

259-
@try_blocks [:do, :after]
260-
@try_clause_blocks [:catch, :else]
260+
def of_expr({:try, _meta, [[do: body] ++ blocks]}, stack, context) do
261+
{body_type, context} = of_expr(body, stack, context)
262+
initial = if Keyword.has_key?(blocks, :else), do: none(), else: body_type
261263

262-
# TODO: try do expr end
263-
def of_expr({:try, _meta, [blocks]}, stack, context) do
264-
context =
265-
Enum.reduce(blocks, context, fn
266-
{:rescue, clauses}, context ->
267-
Enum.reduce(clauses, context, fn
268-
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, context ->
269-
of_rescue(var, exceptions, body, expr, [], meta, stack, context)
270-
271-
{:->, meta, [[var], body]}, context ->
272-
of_rescue(var, [], body, var, [:anonymous_rescue], meta, stack, context)
273-
end)
264+
blocks
265+
|> Enum.reduce({initial, context}, fn
266+
{:rescue, clauses}, acc_context ->
267+
Enum.reduce(clauses, acc_context, fn
268+
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} ->
269+
{type, context} = of_rescue(var, exceptions, body, expr, [], meta, stack, context)
270+
{union(type, acc), context}
271+
272+
{:->, meta, [[var], body]}, {acc, context} ->
273+
hint = [:anonymous_rescue]
274+
{type, context} = of_rescue(var, [], body, var, hint, meta, stack, context)
275+
{union(type, acc), context}
276+
end)
274277

275-
{block, body}, context when block in @try_blocks ->
276-
of_expr_context(body, stack, context)
278+
{:after, body}, {acc, context} ->
279+
{_type, context} = of_expr(body, stack, context)
280+
{acc, context}
277281

278-
{block, clauses}, context when block in @try_clause_blocks ->
279-
{_, context} = of_clauses(clauses, stack, {none(), context})
280-
context
281-
end)
282+
{:catch, clauses}, acc_context ->
283+
of_clauses(clauses, stack, [atom([:error, :exit, :throw]), dynamic()], acc_context)
282284

283-
{dynamic(), context}
285+
{:else, clauses}, acc_context ->
286+
of_clauses(clauses, stack, [body_type], acc_context)
287+
end)
288+
|> dynamic_unless_static(stack)
284289
end
285290

286291
def of_expr({:receive, _meta, [blocks]}, stack, context) do
@@ -290,7 +295,7 @@ defmodule Module.Types.Expr do
290295
{acc, context}
291296

292297
{:do, clauses}, {acc, context} ->
293-
of_clauses(clauses, stack, {acc, context})
298+
of_clauses(clauses, stack, [dynamic()], {acc, context})
294299

295300
{:after, [{:->, meta, [[timeout], body]}]}, {acc, context} ->
296301
{timeout_type, context} = of_expr(timeout, stack, context)
@@ -313,7 +318,7 @@ defmodule Module.Types.Expr do
313318
context = Enum.reduce(opts, context, &for_option(&1, stack, &2))
314319

315320
if Keyword.has_key?(opts, :reduce) do
316-
{_, context} = of_clauses(block, stack, {none(), context})
321+
{_, context} = of_clauses(block, stack, [dynamic()], {none(), context})
317322
{dynamic(), context}
318323
else
319324
{_type, context} = of_expr(block, stack, context)
@@ -431,7 +436,7 @@ defmodule Module.Types.Expr do
431436
context
432437
end
433438

434-
of_expr_context(body, stack, context)
439+
of_expr(body, stack, context)
435440
end
436441

437442
## Comprehensions
@@ -456,15 +461,18 @@ defmodule Module.Types.Expr do
456461
end
457462

458463
defp for_clause(expr, stack, context) do
459-
of_expr_context(expr, stack, context)
464+
{_type, context} = of_expr(expr, stack, context)
465+
context
460466
end
461467

462468
defp for_option({:into, expr}, stack, context) do
463-
of_expr_context(expr, stack, context)
469+
{_type, context} = of_expr(expr, stack, context)
470+
context
464471
end
465472

466473
defp for_option({:reduce, expr}, stack, context) do
467-
of_expr_context(expr, stack, context)
474+
{_type, context} = of_expr(expr, stack, context)
475+
context
468476
end
469477

470478
defp for_option({:uniq, _}, _stack, context) do
@@ -482,15 +490,17 @@ defmodule Module.Types.Expr do
482490
end
483491

484492
defp with_clause(expr, stack, context) do
485-
of_expr_context(expr, stack, context)
493+
{_type, context} = of_expr(expr, stack, context)
494+
context
486495
end
487496

488497
defp with_option({:do, body}, stack, context) do
489-
of_expr_context(body, stack, context)
498+
{_type, context} = of_expr(body, stack, context)
499+
context
490500
end
491501

492502
defp with_option({:else, clauses}, stack, context) do
493-
{_, context} = of_clauses(clauses, stack, {none(), context})
503+
{_, context} = of_clauses(clauses, stack, [dynamic()], {none(), context})
494504
context
495505
end
496506

@@ -522,7 +532,7 @@ defmodule Module.Types.Expr do
522532
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
523533
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
524534

525-
defp of_clauses(clauses, stack, acc_context) do
535+
defp of_clauses(clauses, stack, _expected, acc_context) do
526536
Enum.reduce(clauses, acc_context, fn {:->, meta, [head, body]}, {acc, context} ->
527537
{patterns, guards} = extract_head(head)
528538
{_types, context} = Pattern.of_head(patterns, guards, meta, stack, context)
@@ -545,11 +555,6 @@ defmodule Module.Types.Expr do
545555
defp flatten_when({:when, _meta, [left, right]}), do: [left | flatten_when(right)]
546556
defp flatten_when(other), do: [other]
547557

548-
defp of_expr_context(expr, stack, context) do
549-
{_type, context} = of_expr(expr, stack, context)
550-
context
551-
end
552-
553558
defp map_put!(map_type, key, value_type) do
554559
case map_put(map_type, key, value_type) do
555560
{:ok, descr} -> descr

lib/elixir/src/elixir_clauses.erl

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,15 @@ clause(Meta, Kind, _Fun, _, _, E) ->
197197

198198
head(Meta, [{'when', WhenMeta, [_ | _] = All}], S, E) ->
199199
{Args, Guard} = elixir_utils:split_last(All),
200-
{EArgs, SA, EA} = match(fun elixir_expand:expand_args/3, Meta, Args, S, S, E),
201-
{EGuard, SG, EG} = guard(Guard, SA, EA#{context := guard}),
202-
{[{'when', WhenMeta, EArgs ++ [EGuard]}], SG, EG#{context := nil}};
200+
guarded_head(Meta, WhenMeta, Args, Guard, S, E);
203201
head(Meta, Args, S, E) ->
204202
match(fun elixir_expand:expand_args/3, Meta, Args, S, S, E).
205203

204+
guarded_head(Meta, WhenMeta, Args, Guard, S, E) ->
205+
{EArgs, SA, EA} = match(fun elixir_expand:expand_args/3, Meta, Args, S, S, E),
206+
{EGuard, SG, EG} = guard(Guard, SA, EA#{context := guard}),
207+
{[{'when', WhenMeta, EArgs ++ [EGuard]}], SG, EG#{context := nil}}.
208+
206209
guard({'when', Meta, [Left, Right]}, S, E) ->
207210
{ELeft, SL, EL} = guard(Left, S, E),
208211
{ERight, SR, ER} = guard(Right, SL, EL),
@@ -400,8 +403,12 @@ expand_clauses_with_stacktrace(Meta, Fun, Clauses, S, E) ->
400403
expand_catch(Meta, [{'when', _, [_, _, _, _ | _]}], _, E) ->
401404
Error = {wrong_number_of_args_for_clause, "one or two args", origin(Meta, 'try'), 'catch'},
402405
file_error(Meta, E, ?MODULE, Error);
403-
expand_catch(Meta, [_] = Args, S, E) ->
404-
head(Meta, Args, S, E);
406+
expand_catch(Meta, [{'when', WhenMeta, [Arg1, Arg2, Guard]}], S, E) ->
407+
guarded_head(Meta, WhenMeta, [Arg1, Arg2], Guard, S, E);
408+
expand_catch(Meta, [{'when', WhenMeta, [Arg1, Guard]}], S, E) ->
409+
guarded_head(Meta, WhenMeta, [Arg1], Guard, S, E);
410+
expand_catch(Meta, [Arg], S, E) ->
411+
head(Meta, [throw, Arg], S, E);
405412
expand_catch(Meta, [_, _] = Args, S, E) ->
406413
head(Meta, Args, S, E);
407414
expand_catch(Meta, _, _, E) ->

lib/elixir/src/elixir_erl_try.erl

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,7 @@ reduce_clauses([], Acc, OldStack, SAcc, _S) ->
2020
{lists:reverse(Acc), SAcc#elixir_erl{stacktrace=OldStack}}.
2121

2222
each_clause({'catch', Meta, Raw, Expr}, S) ->
23-
{Args, Guards} = elixir_utils:extract_splat_guards(Raw),
24-
25-
Match =
26-
case Args of
27-
[X] -> [throw, X];
28-
[X, Y] -> [X, Y]
29-
end,
23+
{Match, Guards} = elixir_utils:extract_splat_guards(Raw),
3024

3125
{{clause, Line, [TKind, TMatches], TGuards, TBody}, TS} =
3226
elixir_erl_clauses:clause(?ann(Meta), fun elixir_erl_pass:translate_args/3, Match, Expr, Guards, S),

lib/elixir/test/elixir/module/types/expr_test.exs

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ defmodule Module.Types.ExprTest do
147147
{dynamic() |> union(atom([nil])), "URI.unknown/1 is undefined or private"}
148148

149149
assert typewarn!(try(do: :ok, after: URI.unknown("foo"))) ==
150-
{dynamic(), "URI.unknown/1 is undefined or private"}
150+
{atom([:ok]), "URI.unknown/1 is undefined or private"}
151151

152152
# Check it also emits over a union
153153
assert typewarn!(
@@ -1063,6 +1063,38 @@ defmodule Module.Types.ExprTest do
10631063
end
10641064

10651065
describe "try" do
1066+
test "returns unions of all clauses" do
1067+
assert typecheck!(
1068+
try do
1069+
:do
1070+
rescue
1071+
_ -> :rescue
1072+
catch
1073+
:caught -> :caught1
1074+
:throw, :caught -> :caught2
1075+
after
1076+
:not_used
1077+
end
1078+
) == atom([:do, :caught1, :caught2, :rescue])
1079+
1080+
assert typecheck!(
1081+
[x],
1082+
try do
1083+
x
1084+
rescue
1085+
_ -> :rescue
1086+
catch
1087+
:caught -> :caught1
1088+
:throw, :caught -> :caught2
1089+
after
1090+
:not_used
1091+
else
1092+
:match -> :else1
1093+
_ -> :else2
1094+
end
1095+
) == atom([:caught1, :caught2, :rescue, :else1, :else2])
1096+
end
1097+
10661098
test "warns on undefined exceptions" do
10671099
assert typewarn!(
10681100
try do
@@ -1071,7 +1103,7 @@ defmodule Module.Types.ExprTest do
10711103
e in UnknownError -> e
10721104
end
10731105
) ==
1074-
{dynamic(),
1106+
{dynamic() |> union(atom([:ok])),
10751107
"struct UnknownError is undefined (module UnknownError is not available or is yet to be defined). " <>
10761108
"Make sure the module name is correct and has been specified in full (or that an alias has been defined)"}
10771109

@@ -1082,67 +1114,48 @@ defmodule Module.Types.ExprTest do
10821114
e in Enumerable -> e
10831115
end
10841116
) ==
1085-
{dynamic(),
1117+
{dynamic() |> union(atom([:ok])),
10861118
"struct Enumerable is undefined (there is such module but it does not define a struct)"}
10871119
end
10881120

10891121
test "defines unions of exceptions in rescue" do
1090-
# TODO: we are validating the type through the exception but we should actually check the returned type
1091-
assert typeerror!(
1122+
assert typecheck!(
10921123
try do
1093-
:ok
1124+
raise "oops"
10941125
rescue
1095-
e in [SyntaxError, RuntimeError] ->
1096-
e.unknown
1126+
e in [RuntimeError, ArgumentError] ->
1127+
e
10971128
end
10981129
) ==
1099-
~l"""
1100-
unknown key .unknown in expression:
1101-
1102-
e.unknown
1103-
1104-
where "e" was given the type:
1105-
1106-
# type: %RuntimeError{__exception__: true, message: term()} or
1107-
%SyntaxError{
1108-
__exception__: true,
1109-
column: term(),
1110-
description: term(),
1111-
file: term(),
1112-
line: term(),
1113-
snippet: term()
1114-
}
1115-
# from: types_test.ex:LINE-4
1116-
rescue e in [SyntaxError, RuntimeError] ->
1117-
"""
1130+
union(
1131+
closed_map(
1132+
__struct__: atom([ArgumentError]),
1133+
__exception__: atom([true]),
1134+
message: term()
1135+
),
1136+
closed_map(
1137+
__struct__: atom([RuntimeError]),
1138+
__exception__: atom([true]),
1139+
message: term()
1140+
)
1141+
)
11181142
end
11191143

11201144
test "defines an open map of two fields in anonymous rescue" do
1121-
# TODO: we are validating the type through the exception but we should actually check the returned type
1122-
assert typeerror!(
1145+
assert typecheck!(
11231146
try do
1124-
:ok
1147+
raise "oops"
11251148
rescue
1126-
e -> e.message
1149+
e -> e
11271150
end
11281151
) ==
1129-
~l"""
1130-
unknown key .message in expression:
1131-
1132-
e.message
1133-
1134-
where "e" was given the type:
1135-
1136-
# type: %{..., __exception__: true, __struct__: atom()}
1137-
# from: types_test.ex:LINE-3
1138-
rescue e ->
1139-
1140-
#{hints(:anonymous_rescue)}
1141-
"""
1152+
open_map(
1153+
__struct__: atom(),
1154+
__exception__: atom([true])
1155+
)
11421156
end
11431157

11441158
test "matches on stacktrace" do
1145-
# TODO: we are validating the type through the exception but we should actually check the returned type
11461159
assert typeerror!(
11471160
try do
11481161
:ok

0 commit comments

Comments
 (0)