Analyze conditional orelse and when
This commit is contained in:
@@ -74,9 +74,22 @@ defmodule Module.Types.Of do
|
||||
Returns `true` if there was a refinement, `false` otherwise.
|
||||
"""
|
||||
def refine_body_var({_, meta, _}, type, expr, stack, context) do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
|
||||
def refine_body_var(version, type, expr, stack, context)
|
||||
when is_integer(version) or is_reference(version) do
|
||||
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
|
||||
|
||||
context =
|
||||
case context.conditional_vars do
|
||||
%{} = conditional_vars ->
|
||||
%{context | conditional_vars: Map.put(conditional_vars, version, true)}
|
||||
|
||||
nil ->
|
||||
context
|
||||
end
|
||||
|
||||
if gradual?(old_type) and type not in [term(), dynamic()] and not is_map_key(data, :errored) do
|
||||
case compatible_intersection(old_type, type) do
|
||||
{:ok, new_type} when new_type != old_type ->
|
||||
@@ -104,8 +117,11 @@ defmodule Module.Types.Of do
|
||||
use compatibility.
|
||||
"""
|
||||
def refine_head_var({_, meta, _}, type, expr, stack, context) do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
refine_head_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
|
||||
def refine_head_var(version, type, expr, stack, context)
|
||||
when is_integer(version) or is_reference(version) do
|
||||
case context.vars do
|
||||
%{^version => %{errored: true}} ->
|
||||
{:ok, error_type(), context}
|
||||
|
||||
@@ -420,10 +420,8 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
They behave like guards, so we need to take into account their scope.
|
||||
"""
|
||||
def of_size(:match, arg, expr, stack, %{pattern_info: pattern_info} = context) do
|
||||
context = init_guard_info(context)
|
||||
{type, context} = of_guard(arg, integer(), expr, stack, context)
|
||||
{type, %{context | pattern_info: pattern_info}}
|
||||
def of_size(:match, arg, expr, stack, context) do
|
||||
of_guard(arg, integer(), expr, stack, context)
|
||||
end
|
||||
|
||||
def of_size(:guard, arg, expr, stack, context) do
|
||||
@@ -755,34 +753,35 @@ defmodule Module.Types.Pattern do
|
||||
context
|
||||
end
|
||||
|
||||
defp of_guards([guard], stack, context) do
|
||||
{type, context} = of_guard(guard, stack, context)
|
||||
maybe_badguard(type, guard, stack, context)
|
||||
end
|
||||
|
||||
defp of_guards(guards, stack, context) do
|
||||
# TODO: This match? is temporary until we support multiple guards
|
||||
single? = match?([_], guards)
|
||||
context = init_guard_info(context, single?)
|
||||
return = if single?, do: @atom_true, else: term()
|
||||
cond_context = %{context | conditional_vars: %{}}
|
||||
|
||||
context =
|
||||
Enum.reduce(guards, context, fn guard, context ->
|
||||
{type, context} = of_guard(guard, return, guard, stack, context)
|
||||
|
||||
if never_true?(type) do
|
||||
error = {:badguard, type, guard, context}
|
||||
error(__MODULE__, error, error_meta(guard, stack), stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
{vars_conds, context} =
|
||||
Enum.map_reduce(guards, context, fn guard, context ->
|
||||
{type, %{vars: vars, conditional_vars: cond_vars}} = of_guard(guard, stack, cond_context)
|
||||
{{vars, cond_vars}, maybe_badguard(type, guard, stack, context)}
|
||||
end)
|
||||
|
||||
{_, context} = pop_guard_info(context)
|
||||
context
|
||||
when_expr = Enum.reduce(guards, {:_, [], []}, &{:when, [], [&2, &1]})
|
||||
of_cond_vars(vars_conds, when_expr, stack, context)
|
||||
end
|
||||
|
||||
defp init_guard_info(context, check_domain? \\ true) do
|
||||
%{context | pattern_info: {check_domain?}}
|
||||
defp maybe_badguard(type, guard, stack, context) do
|
||||
if never_true?(type) do
|
||||
error = {:badguard, type, guard, context}
|
||||
error(__MODULE__, error, error_meta(guard, stack), stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp pop_guard_info(%{pattern_info: pattern_info} = context) do
|
||||
{pattern_info, %{context | pattern_info: nil}}
|
||||
defp of_guard(guard, stack, context) do
|
||||
of_guard(guard, @atom_true, guard, stack, context)
|
||||
end
|
||||
|
||||
# :atom
|
||||
@@ -871,10 +870,7 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
# var
|
||||
def of_guard(var, expected, expr, stack, context) when is_var(var) do
|
||||
case context.pattern_info do
|
||||
{true} -> Of.refine_body_var(var, expected, expr, stack, context)
|
||||
{false} -> {Of.var(var, context), context}
|
||||
end
|
||||
Of.refine_body_var(var, expected, expr, stack, context)
|
||||
end
|
||||
|
||||
defp of_remote(fun, _meta, [left, right], call, expected, stack, context)
|
||||
@@ -888,29 +884,10 @@ defmodule Module.Types.Pattern do
|
||||
# For example, if the expected type is true for andalso, then it can
|
||||
# only be true if both clauses are executed, so we know the first
|
||||
# argument has to be true and the second has to be expected.
|
||||
{left_domain, right_domain, surely_rhs?} =
|
||||
if subtype?(expected, both_domain) do
|
||||
{both_domain, expected, true}
|
||||
else
|
||||
{boolean(), term(), false}
|
||||
end
|
||||
|
||||
{left_type, context} = of_guard(left, left_domain, call, stack, context)
|
||||
|
||||
{right_type, context} =
|
||||
if surely_rhs? do
|
||||
of_guard(right, right_domain, call, stack, context)
|
||||
else
|
||||
%{pattern_info: pattern_info} = context
|
||||
context = %{context | pattern_info: {false}}
|
||||
{type, context} = of_guard(right, right_domain, call, stack, context)
|
||||
{type, %{context | pattern_info: pattern_info}}
|
||||
end
|
||||
|
||||
if compatible?(left_type, abort_domain) do
|
||||
{union(abort_domain, right_type), context}
|
||||
if subtype?(expected, both_domain) do
|
||||
of_logical_both(left, both_domain, right, expected, abort_domain, call, stack, context)
|
||||
else
|
||||
{right_type, context}
|
||||
of_logical_cond(left, right, expected, abort_domain, call, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -924,6 +901,58 @@ defmodule Module.Types.Pattern do
|
||||
Apply.remote_apply(info, :erlang, fun, args_types, call, stack, context)
|
||||
end
|
||||
|
||||
defp of_logical_both(left, left_domain, right, right_domain, to_abort, call, stack, context) do
|
||||
{left_type, context} = of_guard(left, left_domain, call, stack, context)
|
||||
{right_type, context} = of_guard(right, right_domain, call, stack, context)
|
||||
|
||||
if disjoint?(left_type, to_abort) do
|
||||
{right_type, context}
|
||||
else
|
||||
{union(to_abort, right_type), context}
|
||||
end
|
||||
end
|
||||
|
||||
defp of_logical_cond(left, right, expected, to_abort, call, stack, context) do
|
||||
cond_context = %{context | conditional_vars: %{}}
|
||||
|
||||
# First we do pass to find the surely types, which are stored directly in the context
|
||||
{_left_type, context} = of_guard(left, boolean(), call, stack, context)
|
||||
|
||||
# Now we find the conditional ones
|
||||
{left_type, left_context} = of_guard(left, expected, call, stack, cond_context)
|
||||
{right_type, right_context} = of_guard(right, expected, call, stack, cond_context)
|
||||
|
||||
%{vars: left_vars, conditional_vars: left_cond} = left_context
|
||||
%{vars: right_vars, conditional_vars: right_cond} = right_context
|
||||
vars_conds = [{left_vars, left_cond}, {right_vars, right_cond}]
|
||||
context = of_cond_vars(vars_conds, call, stack, context)
|
||||
|
||||
if disjoint?(left_type, to_abort) do
|
||||
{right_type, context}
|
||||
else
|
||||
{union(to_abort, right_type), context}
|
||||
end
|
||||
end
|
||||
|
||||
defp of_cond_vars([{vars, cond} | vars_conds], expr, stack, context) do
|
||||
Enum.reduce(Map.keys(cond), context, fn version, context ->
|
||||
if Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
|
||||
%{^version => %{type: type}} = vars
|
||||
|
||||
type =
|
||||
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
|
||||
%{^version => %{type: type}} = vars
|
||||
union(acc, type)
|
||||
end)
|
||||
|
||||
{_, context} = Of.refine_body_var(version, type, expr, stack, context)
|
||||
context
|
||||
else
|
||||
context
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
def format_diagnostic({:badguard, type, expr, context}) do
|
||||
|
||||
@@ -473,6 +473,67 @@ defmodule Module.Types.PatternTest do
|
||||
"""
|
||||
end
|
||||
|
||||
test "when checks" do
|
||||
assert typecheck!([x], is_binary(x) when is_atom(x), x) == dynamic(union(binary(), atom()))
|
||||
|
||||
assert typecheck!([x], is_binary(x) when map_size(x) >= 0, x) ==
|
||||
dynamic(union(binary(), open_map()))
|
||||
|
||||
assert typecheck!([x], tuple_size(x) >= 0 when map_size(x) >= 0, x) ==
|
||||
dynamic(union(tuple(), open_map()))
|
||||
|
||||
assert typecheck!([x, y], is_binary(x) when is_atom(y), {x, y}) ==
|
||||
dynamic(tuple([term(), term()]))
|
||||
end
|
||||
|
||||
test "conditional checks" do
|
||||
assert typecheck!([x], is_binary(x) or is_atom(x), x) == dynamic(union(binary(), atom()))
|
||||
|
||||
assert typecheck!([x], is_binary(x) or map_size(x) >= 0, x) ==
|
||||
dynamic(union(binary(), open_map()))
|
||||
|
||||
assert typecheck!([x, y], is_binary(x) or is_atom(y), {x, y}) ==
|
||||
dynamic(tuple([term(), term()]))
|
||||
|
||||
assert typecheck!([x], not (is_pid(x) and is_atom(x)), x) |> equal?(dynamic(term()))
|
||||
|
||||
assert typecheck!([x, y], not (is_pid(x) and is_atom(y)), {x, y}) ==
|
||||
dynamic(tuple([term(), term()]))
|
||||
|
||||
# Error
|
||||
assert typeerror!([x], is_pid(x) and is_atom(x), x) == ~l"""
|
||||
this guard will never succeed:
|
||||
|
||||
is_pid(x) and is_atom(x)
|
||||
|
||||
because it returns type:
|
||||
|
||||
false
|
||||
|
||||
where "x" was given the type:
|
||||
|
||||
# type: pid()
|
||||
# from: types_test.ex:LINE
|
||||
is_pid(x)
|
||||
"""
|
||||
|
||||
assert typeerror!([x], (is_binary(x) or is_atom(x)) and is_pid(x), x) == ~l"""
|
||||
this guard will never succeed:
|
||||
|
||||
(is_binary(x) or is_atom(x)) and is_pid(x)
|
||||
|
||||
because it returns type:
|
||||
|
||||
false
|
||||
|
||||
where "x" was given the type:
|
||||
|
||||
# type: dynamic(atom() or binary())
|
||||
# from: types_test.ex:LINE
|
||||
is_binary(x) or is_atom(x)
|
||||
"""
|
||||
end
|
||||
|
||||
test "domain checks" do
|
||||
# Regular domain check
|
||||
assert typecheck!([x], length(x) == 3, x) == dynamic(list(term()))
|
||||
|
||||
@@ -136,10 +136,13 @@ defmodule TypeHelper do
|
||||
|
||||
{ast, _, _} = :elixir_expand.expand(fun, :elixir_env.env_to_ex(env), env)
|
||||
{:fn, _, [{:->, _, [[{:when, _, args}], body]}]} = ast
|
||||
{patterns, guards} = Enum.split(args, -1)
|
||||
{patterns, guards, body}
|
||||
{patterns, [guards]} = Enum.split(args, -1)
|
||||
{patterns, flatten_when(guards), body}
|
||||
end
|
||||
|
||||
defp flatten_when({:when, _meta, [left, right]}), do: [left | flatten_when(right)]
|
||||
defp flatten_when(other), do: [other]
|
||||
|
||||
defp new_stack(mode) do
|
||||
cache =
|
||||
if mode == :infer do
|
||||
|
||||
@@ -1034,15 +1034,6 @@ defmodule ExUnit.AssertionsTest do
|
||||
"This should raise an error" = error.message
|
||||
end
|
||||
|
||||
test "flunk with wrong argument type" do
|
||||
flunk(["flunk takes a binary, not a list"])
|
||||
flunk("This should never be tested")
|
||||
rescue
|
||||
error ->
|
||||
"no function clause matching in ExUnit.Assertions.flunk/1" =
|
||||
FunctionClauseError.message(error)
|
||||
end
|
||||
|
||||
test "AssertionError.message/1 is nicely formatted" do
|
||||
assert :a = :b
|
||||
rescue
|
||||
|
||||
Reference in New Issue
Block a user