Skip to content

Commit df0553b

Browse files
committed
Do not raise false positive on not is_struct checks, closes #15183
1 parent 3452e6a commit df0553b

2 files changed

Lines changed: 48 additions & 26 deletions

File tree

lib/elixir/lib/module/types/pattern.ex

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,18 @@ defmodule Module.Types.Pattern do
12581258
end
12591259
end
12601260

1261+
@dynamic_fail dynamic(atom([:fail]))
1262+
1263+
# `x or :fail` is a pattern used in Elixir to make guards fail.
1264+
# For example, `not is_struct(x, Foo)` checks if `Foo` is an atom
1265+
# or fails, but in the negation case `:fail` becomes a possible
1266+
# return type leading to a violation, we don't want. So we mark
1267+
# `:fail` as dynamic as its whole purpose is to cause failures.
1268+
defp of_remote(:orelse, [left, :fail], _call, expected, stack, context) do
1269+
{type, context} = of_guard(left, expected, left, stack, context)
1270+
{union(type, @dynamic_fail), context}
1271+
end
1272+
12611273
defp of_remote(fun, _args, call, expected, stack, context)
12621274
when fun in [:and, :or, :andalso, :orelse] do
12631275
{both_domain, abort_domain, always_rhs?} =
@@ -1280,38 +1292,43 @@ defmodule Module.Types.Pattern do
12801292
# For example, if the expected type is true for andalso, then it can
12811293
# only be true if both clauses are executed, so we know the first
12821294
# argument has to be true and the second has to be expected.
1283-
if subtype?(expected, both_domain) do
1284-
of_logical_all([left | right], true, both_domain, abort_domain, stack, context)
1285-
else
1286-
cond_context = enable_conditional_mode(context)
1295+
cond do
1296+
subtype?(expected, both_domain) ->
1297+
of_logical_all([left | right], true, both_domain, abort_domain, stack, context)
12871298

1288-
# Compute the sure types, which are stored directly in the context
1289-
{_type, context} = of_guard(left, boolean(), left, stack, context)
1299+
right == [] ->
1300+
of_guard(left, expected, left, stack, context)
12901301

1291-
# andalso/orelse may not execute the rhs, so we cannot get sure types from it
1292-
context =
1293-
case always_rhs? do
1294-
true ->
1295-
Enum.reduce(right, context, fn expr, context ->
1296-
{_, context} = of_guard(expr, boolean(), expr, stack, context)
1297-
context
1298-
end)
1302+
true ->
1303+
cond_context = enable_conditional_mode(context)
12991304

1300-
false ->
1301-
context
1302-
end
1305+
# Compute the sure types, which are stored directly in the context
1306+
{_type, context} = of_guard(left, boolean(), left, stack, context)
13031307

1304-
{type, vars_conds} =
1305-
of_logical_cond([left | right], none(), [], expected, stack, cond_context)
1308+
# andalso/orelse may not execute the rhs, so we cannot get sure types from it
1309+
context =
1310+
case always_rhs? do
1311+
true ->
1312+
Enum.reduce(right, context, fn expr, context ->
1313+
{_, context} = of_guard(expr, boolean(), expr, stack, context)
1314+
context
1315+
end)
13061316

1307-
# We will be precise if all branches changed the same variable
1308-
context =
1309-
update_in(context.pattern_info.vars, fn
1310-
false -> false
1311-
vars -> Of.all_same_conditional_vars?(vars_conds) and vars
1312-
end)
1317+
false ->
1318+
context
1319+
end
1320+
1321+
{type, vars_conds} =
1322+
of_logical_cond([left | right], none(), [], expected, stack, cond_context)
1323+
1324+
# We will be precise if all branches changed the same variable
1325+
context =
1326+
update_in(context.pattern_info.vars, fn
1327+
false -> false
1328+
vars -> Of.all_same_conditional_vars?(vars_conds) and vars
1329+
end)
13131330

1314-
{type, Of.reduce_conditional_vars(vars_conds, call, stack, context)}
1331+
{type, Of.reduce_conditional_vars(vars_conds, call, stack, context)}
13151332
end
13161333
end
13171334

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,11 @@ defmodule Module.Types.PatternTest do
645645
test "is_struct/1" do
646646
assert typecheck!([x], is_struct(x), x) == dynamic(open_map(__struct__: atom()))
647647
assert typecheck!([x], is_struct(x, URI), x) == dynamic(open_map(__struct__: atom([URI])))
648+
649+
assert typecheck!([x], not is_struct(x), x)
650+
|> equal?(dynamic(negation(open_map(__struct__: atom()))))
651+
652+
assert typecheck!([x], not is_struct(x, URI), x) == dynamic()
648653
end
649654

650655
test "is_binary/1" do

0 commit comments

Comments
 (0)