Skip to content

Commit e9e4e24

Browse files
committed
Track __info__(:struct) more precisely
Closes #14127.
1 parent f300c7e commit e9e4e24

File tree

3 files changed

+68
-47
lines changed

3 files changed

+68
-47
lines changed

lib/elixir/lib/module/parallel_checker.ex

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,20 +209,21 @@ defmodule Module.ParallelChecker do
209209
"""
210210
@spec fetch_export(cache(), module(), atom(), arity()) ::
211211
{:ok, mode(), binary() | nil, {:infer, [term()]} | :none}
212-
| {:error, :function | :module}
212+
| :badmodule
213+
| {:badfunction, mode()}
213214
def fetch_export({checker, table}, module, fun, arity) do
214215
case :ets.lookup(table, module) do
215216
[] ->
216217
cache_module({checker, table}, module)
217218
fetch_export({checker, table}, module, fun, arity)
218219

219220
[{_key, false}] ->
220-
{:error, :module}
221+
:badmodule
221222

222223
[{_key, mode}] ->
223224
case :ets.lookup(table, {module, {fun, arity}}) do
224225
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
225-
[] -> {:error, :function}
226+
[] -> {:badfunction, mode}
226227
end
227228
end
228229
end
@@ -419,7 +420,7 @@ defmodule Module.ParallelChecker do
419420
end
420421

421422
defp info_exports(module) do
422-
{:elixir, behaviour_exports(module) ++ [{:__info__, 1} | module.__info__(:functions)]}
423+
{:elixir, behaviour_exports(module) ++ module.__info__(:functions)}
423424
rescue
424425
_ -> {:erlang, module.module_info(:exports)}
425426
end
@@ -432,8 +433,7 @@ defmodule Module.ParallelChecker do
432433

433434
defp cache_from_module_map(table, map) do
434435
exports =
435-
[{:__info__, 1}] ++
436-
behaviour_exports(map) ++
436+
behaviour_exports(map) ++
437437
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
438438

439439
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
@@ -458,7 +458,6 @@ defmodule Module.ParallelChecker do
458458
)
459459
end)
460460

461-
:ets.insert(table, {{module, {:__info__, 1}}, nil, :none})
462461
:ets.insert(table, {module, :elixir})
463462
end
464463

lib/elixir/lib/module/types/apply.ex

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ defmodule Module.Types.Apply do
3131
end
3232

3333
fas = list(tuple([atom(), integer()]))
34+
struct_info = list(closed_map(default: if_set(term()), field: atom()))
3435

3536
shared_info = [
3637
attributes: list(tuple([atom(), list(term())])),
@@ -40,31 +41,32 @@ defmodule Module.Types.Apply do
4041
module: atom()
4142
]
4243

44+
module_info = [functions: fas, nifs: fas] ++ shared_info
45+
46+
elixir_info =
47+
[
48+
deprecated: list(tuple([tuple([atom(), integer()]), binary()])),
49+
exports_md5: binary(),
50+
functions: fas,
51+
macros: fas,
52+
struct: struct_info |> union(atom([nil]))
53+
] ++ shared_info
54+
4355
infos =
44-
%{
45-
behaviour_info: [
46-
callbacks: fas,
47-
optional_callbacks: fas
48-
],
49-
module_info: [functions: fas, nifs: fas] ++ shared_info,
50-
__info__:
51-
[
52-
deprecated: list(tuple([tuple([atom(), integer()]), binary()])),
53-
exports_md5: binary(),
54-
functions: fas,
55-
macros: fas,
56-
struct:
57-
list(closed_map(default: if_set(term()), field: atom()))
58-
|> union(atom([nil]))
59-
] ++ shared_info,
60-
# TODO: Move this to a type signature in the long term
61-
__protocol__: [
62-
module: atom(),
63-
functions: fas,
64-
consolidated?: boolean(),
65-
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))
66-
]
67-
}
56+
[
57+
# We have a special key that tracks if something is a struct or not
58+
{{:__info__, true}, Keyword.put(elixir_info, :struct, struct_info)},
59+
{{:__info__, false}, Keyword.put(elixir_info, :struct, atom([nil]))},
60+
{:__info__, elixir_info},
61+
{:behaviour_info, callbacks: fas, optional_callbacks: fas},
62+
{:module_info, module_info},
63+
# TODO: Move this to a type signature declared by `defprotocol` (or perhaps part of the behaviour)
64+
{:__protocol__,
65+
module: atom(),
66+
functions: fas,
67+
consolidated?: boolean(),
68+
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))}
69+
]
6870

6971
for {name, clauses} <- infos do
7072
domain = atom(Keyword.keys(clauses))
@@ -76,7 +78,7 @@ defmodule Module.Types.Apply do
7678
end
7779

7880
defp signature(:module_info, 0) do
79-
{:strong, nil, [{[], unquote(Macro.escape(kw.(infos.module_info)))}]}
81+
{:strong, nil, [{[], unquote(Macro.escape(kw.(module_info)))}]}
8082
end
8183

8284
defp signature(_, _), do: :none
@@ -512,10 +514,20 @@ defmodule Module.Types.Apply do
512514
info = if info == :none, do: signature(fun, arity), else: info
513515
{info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
514516

515-
{:error, type} ->
517+
{:badfunction, :elixir} when fun == :__info__ and arity == 1 ->
518+
key =
519+
cond do
520+
not Code.ensure_loaded?(module) -> :__info__
521+
module.__info__(:struct) != nil -> {:__info__, true}
522+
true -> {:__info__, false}
523+
end
524+
525+
{signature(key, arity), context}
526+
527+
error ->
516528
context =
517529
if warn_undefined?(module, fun, arity, stack) do
518-
warn(__MODULE__, {:undefined, type, module, fun, arity}, meta, stack, context)
530+
warn(__MODULE__, {:undefined, error, module, fun, arity}, meta, stack, context)
519531
else
520532
context
521533
end
@@ -525,14 +537,6 @@ defmodule Module.Types.Apply do
525537
end
526538
end
527539

528-
defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do
529-
if reason do
530-
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
531-
else
532-
context
533-
end
534-
end
535-
536540
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
537541
case :otp_internal.obsolete(module, fun, arity) do
538542
{:deprecated, string} when is_list(string) ->
@@ -549,6 +553,14 @@ defmodule Module.Types.Apply do
549553
end
550554
end
551555

556+
defp check_deprecated(_, module, fun, arity, reason, meta, stack, context) do
557+
if reason do
558+
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
559+
else
560+
context
561+
end
562+
end
563+
552564
defp builtin_module?(module) do
553565
is_map_key(builtin_modules(), module)
554566
end
@@ -952,7 +964,7 @@ defmodule Module.Types.Apply do
952964
}
953965
end
954966

955-
def format_diagnostic({:undefined, :module, module, fun, arity}) do
967+
def format_diagnostic({:undefined, :badmodule, module, fun, arity}) do
956968
top =
957969
if fun == :__struct__ and arity == 0 do
958970
"struct #{inspect(module)}"
@@ -973,15 +985,15 @@ defmodule Module.Types.Apply do
973985
}
974986
end
975987

976-
def format_diagnostic({:undefined, :function, module, :__struct__, 0}) do
988+
def format_diagnostic({:undefined, {:badfunction, _}, module, :__struct__, 0}) do
977989
%{
978990
message:
979991
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
980992
group: true
981993
}
982994
end
983995

984-
def format_diagnostic({:undefined, :function, module, fun, arity}) do
996+
def format_diagnostic({:undefined, {:badfunction, _}, module, fun, arity}) do
985997
%{
986998
message:
987999
IO.iodata_to_binary([

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,13 +1436,13 @@ defmodule Module.Types.ExprTest do
14361436

14371437
describe "info" do
14381438
test "__info__/1" do
1439-
assert typecheck!([x], x.__info__(:functions)) == list(tuple([atom(), integer()]))
1440-
14411439
assert typecheck!(GenServer.__info__(:functions)) == list(tuple([atom(), integer()]))
14421440

14431441
assert typewarn!(:string.__info__(:functions)) ==
14441442
{dynamic(), ":string.__info__/1 is undefined or private"}
14451443

1444+
assert typecheck!([x], x.__info__(:functions)) == list(tuple([atom(), integer()]))
1445+
14461446
assert typeerror!([x], x.__info__(:whatever)) |> strip_ansi() =~ """
14471447
incompatible types given to __info__/1:
14481448
@@ -1454,6 +1454,16 @@ defmodule Module.Types.ExprTest do
14541454
"""
14551455
end
14561456

1457+
test "__info__/1 for struct information" do
1458+
assert typecheck!(GenServer.__info__(:struct)) == atom([nil])
1459+
1460+
assert typecheck!(URI.__info__(:struct)) ==
1461+
list(closed_map(default: if_set(term()), field: atom()))
1462+
1463+
assert typecheck!([x], x.__info__(:struct)) ==
1464+
list(closed_map(default: if_set(term()), field: atom())) |> union(atom([nil]))
1465+
end
1466+
14571467
test "behaviour_info/1" do
14581468
assert typecheck!([x], x.behaviour_info(:callbacks)) == list(tuple([atom(), integer()]))
14591469

0 commit comments

Comments
 (0)