Skip to content

Commit 7452530

Browse files
committed
Type check Integer.to_string and String.to_integer
1 parent 8d60093 commit 7452530

File tree

3 files changed

+165
-26
lines changed

3 files changed

+165
-26
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,10 @@ defmodule Module.Types.Expr do
298298
# TODO: We cannot return the unions of functions. Do we forbid this?
299299
# Do we check it is always the same return type? Do we simply say it is a function?
300300
{mods, context} = Of.modules(remote_type, name, arity, expr, meta, stack, context)
301-
context = Enum.reduce(mods, context, &Of.remote(&1, name, arity, meta, stack, &2))
301+
302+
context =
303+
Enum.reduce(mods, context, &(Of.remote(&1, name, arity, meta, stack, &2) |> elem(1)))
304+
302305
{fun(), context}
303306
end
304307

@@ -334,7 +337,7 @@ defmodule Module.Types.Expr do
334337
else
335338
# If the exception cannot be found or is invalid,
336339
# we call Of.remote/5 to emit a warning.
337-
context = Of.remote(exception, :__struct__, 0, meta, stack, context)
340+
{_, context} = Of.remote(exception, :__struct__, 0, meta, stack, context)
338341
{error_type(), context}
339342
end
340343
end)

lib/elixir/lib/module/types/of.ex

Lines changed: 133 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ defmodule Module.Types.Of do
190190
Returns `__info__(:struct)` information about a struct.
191191
"""
192192
def struct_info(struct, meta, stack, context) do
193-
context = remote(struct, :__struct__, 0, meta, stack, context)
193+
{_, context} = remote(struct, :__struct__, 0, meta, stack, context)
194194

195195
info =
196196
struct.__info__(:struct) ||
@@ -293,8 +293,73 @@ defmodule Module.Types.Of do
293293
context
294294
end
295295

296+
## Modules
297+
298+
@doc """
299+
Returns the modules.
300+
301+
The call information is used on report reporting.
302+
"""
303+
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
304+
case atom_fetch(type) do
305+
{_, mods} ->
306+
{mods, context}
307+
308+
:error ->
309+
warning = {:badmodule, expr, type, fun, arity, hints, context}
310+
{[], error(warning, meta, stack, context)}
311+
end
312+
end
313+
296314
## Remotes
297315

316+
# Define strong arrows found in the standard library.
317+
# A strong arrow means that, if a type outside of its
318+
# domain is given, an error is raised. We are also
319+
# ensuring that domains for the same function have
320+
# no overlaps.
321+
322+
for {mod, fun, clauses} <- [
323+
{:erlang, :binary_to_integer, [{[binary()], integer()}]},
324+
{:erlang, :integer_to_binary, [{[integer()], binary()}]}
325+
] do
326+
[{args, _return} | _others] = clauses
327+
328+
defp strong_remote(unquote(mod), unquote(fun), unquote(length(args))),
329+
do: unquote(Macro.escape(clauses))
330+
end
331+
332+
defp strong_remote(_mod, _fun, _arity), do: []
333+
334+
@doc """
335+
Checks a module is a valid remote.
336+
337+
It returns either a tuple with the remote information and the context.
338+
The remote information may be one of:
339+
340+
* `:none` - no typing information found.
341+
342+
* `{:infer, clauses}` - clauses from inferences. You must check all
343+
all clauses and return the union between them. They are dynamic
344+
and they can only be converted into arrows by computing the union
345+
of all arguments.
346+
347+
* `{:strong, clauses}` - clauses from signatures. So far these are
348+
strong arrows with non-overlapping domains. If you find one matching
349+
clause, you can stop looking for others.
350+
351+
"""
352+
def remote(module, fun, arity, meta, stack, context) when is_atom(module) do
353+
if Keyword.get(meta, :runtime_module, false) do
354+
{:none, context}
355+
else
356+
case strong_remote(module, fun, arity) do
357+
[] -> {:none, check_export(module, fun, arity, meta, stack, context)}
358+
clauses -> {{:strong, clauses}, context}
359+
end
360+
end
361+
end
362+
298363
# TODO: Implement element without a literal index
299364

300365
def apply(:erlang, :element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
@@ -407,36 +472,37 @@ defmodule Module.Types.Of do
407472
apply(mod, name, args_types, expr, stack, context)
408473

409474
false ->
410-
context = remote(mod, name, arity, elem(expr, 1), stack, context)
411-
{dynamic(), context}
475+
{info, context} = remote(mod, name, arity, elem(expr, 1), stack, context)
476+
477+
case apply_remote(info, args_types) do
478+
{:ok, type} ->
479+
{type, context}
480+
481+
{:error, clauses} ->
482+
error = {:badapply, expr, args_types, clauses, context}
483+
{error_type(), error(error, elem(expr, 1), stack, context)}
484+
end
412485
end
413486
end
414487

415-
@doc """
416-
Returns the modules for a given call.
417-
"""
418-
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
419-
case atom_fetch(type) do
420-
{_, mods} ->
421-
{mods, context}
488+
defp apply_remote(:none, _args_types) do
489+
{:ok, dynamic()}
490+
end
422491

423-
:error ->
424-
warning = {:badmodule, expr, type, fun, arity, hints, context}
425-
{[], error(warning, meta, stack, context)}
426-
end
492+
defp apply_remote({:strong, clauses}, args_types) do
493+
Enum.find_value(clauses, {:error, clauses}, fn {expected, return} ->
494+
if zip_compatible?(args_types, expected) do
495+
{:ok, return}
496+
end
497+
end)
427498
end
428499

429-
@doc """
430-
Checks a module is a valid remote.
431-
"""
432-
def remote(module, fun, arity, meta, stack, context) when is_atom(module) do
433-
if Keyword.get(meta, :runtime_module, false) do
434-
context
435-
else
436-
check_export(module, fun, arity, meta, stack, context)
437-
end
500+
defp zip_compatible?([actual | actuals], [expected | expecteds]) do
501+
compatible?(actual, expected) and zip_compatible?(actuals, expecteds)
438502
end
439503

504+
defp zip_compatible?([], []), do: true
505+
440506
defp check_export(module, fun, arity, meta, stack, context) do
441507
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
442508
{:ok, mode, :def, reason} ->
@@ -726,6 +792,31 @@ defmodule Module.Types.Of do
726792
}
727793
end
728794

795+
def format_diagnostic({:badapply, expr, args_types, clauses, context}) do
796+
traces = collect_traces(expr, context)
797+
798+
%{
799+
details: %{typing_traces: traces},
800+
message:
801+
IO.iodata_to_binary([
802+
"""
803+
incompatible types given to #{format_mfa(expr)}:
804+
805+
#{expr_to_string(expr) |> indent(4)}
806+
807+
expected types:
808+
809+
#{clauses_args_to_quoted_string(clauses) |> indent(4)}
810+
811+
but got types:
812+
813+
#{args_to_quoted_string(args_types) |> indent(4)}
814+
""",
815+
format_traces(traces)
816+
])
817+
}
818+
end
819+
729820
def format_diagnostic({:mismatched_comparison, expr, context}) do
730821
traces = collect_traces(expr, context)
731822

@@ -845,6 +936,25 @@ defmodule Module.Types.Of do
845936
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
846937
end
847938

939+
defp clauses_args_to_quoted_string([{args, _return}]) do
940+
args_to_quoted_string(args)
941+
end
942+
943+
defp args_to_quoted_string([arg]) do
944+
to_quoted_string(arg)
945+
end
946+
947+
defp args_to_quoted_string(args) do
948+
{:_, [], Enum.map(args, &to_quoted/1)}
949+
|> Code.Formatter.to_algebra()
950+
|> Inspect.Algebra.format(98)
951+
|> IO.iodata_to_binary()
952+
|> case do
953+
"_(\n" <> _ = multiple_lines -> binary_slice(multiple_lines, 1..-1//1)
954+
single_line -> binary_slice(single_line, 2..-2//1)
955+
end
956+
end
957+
848958
defp empty_if(condition, content) do
849959
if condition, do: "", else: content
850960
end

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,32 @@ defmodule Module.Types.ExprTest do
658658
end
659659
end
660660

661+
describe "apply" do
662+
test "Integer.to_string/1" do
663+
assert typewarn!([x = :foo], Integer.to_string(x)) ==
664+
{dynamic(),
665+
~l"""
666+
incompatible types given to Integer.to_string/1:
667+
668+
Integer.to_string(x)
669+
670+
expected types:
671+
672+
integer()
673+
674+
but got types:
675+
676+
dynamic(:foo)
677+
678+
where "x" was given the type:
679+
680+
# type: dynamic(:foo)
681+
# from: types_test.ex:LINE-2
682+
x = :foo
683+
"""}
684+
end
685+
end
686+
661687
describe "try" do
662688
test "warns on undefined exceptions" do
663689
assert typewarn!(
@@ -682,7 +708,7 @@ defmodule Module.Types.ExprTest do
682708
end
683709

684710
test "defines unions of exceptions in rescue" do
685-
# TODO: check via the actual return type instead
711+
# TODO: we are validating this through the exception but we should actually check the returned type
686712
assert typewarn!(
687713
try do
688714
:ok

0 commit comments

Comments
 (0)