Track __info__(:struct) more precisely

Closes #14127.
This commit is contained in:
José Valim
2024-12-29 19:55:46 +01:00
parent f300c7ee96
commit e9e4e24ef7
3 changed files with 68 additions and 47 deletions
+6 -7
View File
@@ -209,7 +209,8 @@ defmodule Module.ParallelChecker do
"""
@spec fetch_export(cache(), module(), atom(), arity()) ::
{:ok, mode(), binary() | nil, {:infer, [term()]} | :none}
| {:error, :function | :module}
| :badmodule
| {:badfunction, mode()}
def fetch_export({checker, table}, module, fun, arity) do
case :ets.lookup(table, module) do
[] ->
@@ -217,12 +218,12 @@ defmodule Module.ParallelChecker do
fetch_export({checker, table}, module, fun, arity)
[{_key, false}] ->
{:error, :module}
:badmodule
[{_key, mode}] ->
case :ets.lookup(table, {module, {fun, arity}}) do
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
[] -> {:error, :function}
[] -> {:badfunction, mode}
end
end
end
@@ -419,7 +420,7 @@ defmodule Module.ParallelChecker do
end
defp info_exports(module) do
{:elixir, behaviour_exports(module) ++ [{:__info__, 1} | module.__info__(:functions)]}
{:elixir, behaviour_exports(module) ++ module.__info__(:functions)}
rescue
_ -> {:erlang, module.module_info(:exports)}
end
@@ -432,8 +433,7 @@ defmodule Module.ParallelChecker do
defp cache_from_module_map(table, map) do
exports =
[{:__info__, 1}] ++
behaviour_exports(map) ++
behaviour_exports(map) ++
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
@@ -458,7 +458,6 @@ defmodule Module.ParallelChecker do
)
end)
:ets.insert(table, {{module, {:__info__, 1}}, nil, :none})
:ets.insert(table, {module, :elixir})
end
+50 -38
View File
@@ -31,6 +31,7 @@ defmodule Module.Types.Apply do
end
fas = list(tuple([atom(), integer()]))
struct_info = list(closed_map(default: if_set(term()), field: atom()))
shared_info = [
attributes: list(tuple([atom(), list(term())])),
@@ -40,31 +41,32 @@ defmodule Module.Types.Apply do
module: atom()
]
module_info = [functions: fas, nifs: fas] ++ shared_info
elixir_info =
[
deprecated: list(tuple([tuple([atom(), integer()]), binary()])),
exports_md5: binary(),
functions: fas,
macros: fas,
struct: struct_info |> union(atom([nil]))
] ++ shared_info
infos =
%{
behaviour_info: [
callbacks: fas,
optional_callbacks: fas
],
module_info: [functions: fas, nifs: fas] ++ shared_info,
__info__:
[
deprecated: list(tuple([tuple([atom(), integer()]), binary()])),
exports_md5: binary(),
functions: fas,
macros: fas,
struct:
list(closed_map(default: if_set(term()), field: atom()))
|> union(atom([nil]))
] ++ shared_info,
# TODO: Move this to a type signature in the long term
__protocol__: [
module: atom(),
functions: fas,
consolidated?: boolean(),
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))
]
}
[
# We have a special key that tracks if something is a struct or not
{{:__info__, true}, Keyword.put(elixir_info, :struct, struct_info)},
{{:__info__, false}, Keyword.put(elixir_info, :struct, atom([nil]))},
{:__info__, elixir_info},
{:behaviour_info, callbacks: fas, optional_callbacks: fas},
{:module_info, module_info},
# TODO: Move this to a type signature declared by `defprotocol` (or perhaps part of the behaviour)
{:__protocol__,
module: atom(),
functions: fas,
consolidated?: boolean(),
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))}
]
for {name, clauses} <- infos do
domain = atom(Keyword.keys(clauses))
@@ -76,7 +78,7 @@ defmodule Module.Types.Apply do
end
defp signature(:module_info, 0) do
{:strong, nil, [{[], unquote(Macro.escape(kw.(infos.module_info)))}]}
{:strong, nil, [{[], unquote(Macro.escape(kw.(module_info)))}]}
end
defp signature(_, _), do: :none
@@ -512,10 +514,20 @@ defmodule Module.Types.Apply do
info = if info == :none, do: signature(fun, arity), else: info
{info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
{:error, type} ->
{:badfunction, :elixir} when fun == :__info__ and arity == 1 ->
key =
cond do
not Code.ensure_loaded?(module) -> :__info__
module.__info__(:struct) != nil -> {:__info__, true}
true -> {:__info__, false}
end
{signature(key, arity), context}
error ->
context =
if warn_undefined?(module, fun, arity, stack) do
warn(__MODULE__, {:undefined, type, module, fun, arity}, meta, stack, context)
warn(__MODULE__, {:undefined, error, module, fun, arity}, meta, stack, context)
else
context
end
@@ -525,14 +537,6 @@ defmodule Module.Types.Apply do
end
end
defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do
if reason do
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
else
context
end
end
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
case :otp_internal.obsolete(module, fun, arity) do
{:deprecated, string} when is_list(string) ->
@@ -549,6 +553,14 @@ defmodule Module.Types.Apply do
end
end
defp check_deprecated(_, module, fun, arity, reason, meta, stack, context) do
if reason do
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
else
context
end
end
defp builtin_module?(module) do
is_map_key(builtin_modules(), module)
end
@@ -952,7 +964,7 @@ defmodule Module.Types.Apply do
}
end
def format_diagnostic({:undefined, :module, module, fun, arity}) do
def format_diagnostic({:undefined, :badmodule, module, fun, arity}) do
top =
if fun == :__struct__ and arity == 0 do
"struct #{inspect(module)}"
@@ -973,7 +985,7 @@ defmodule Module.Types.Apply do
}
end
def format_diagnostic({:undefined, :function, module, :__struct__, 0}) do
def format_diagnostic({:undefined, {:badfunction, _}, module, :__struct__, 0}) do
%{
message:
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
@@ -981,7 +993,7 @@ defmodule Module.Types.Apply do
}
end
def format_diagnostic({:undefined, :function, module, fun, arity}) do
def format_diagnostic({:undefined, {:badfunction, _}, module, fun, arity}) do
%{
message:
IO.iodata_to_binary([
@@ -1436,13 +1436,13 @@ defmodule Module.Types.ExprTest do
describe "info" do
test "__info__/1" do
assert typecheck!([x], x.__info__(:functions)) == list(tuple([atom(), integer()]))
assert typecheck!(GenServer.__info__(:functions)) == list(tuple([atom(), integer()]))
assert typewarn!(:string.__info__(:functions)) ==
{dynamic(), ":string.__info__/1 is undefined or private"}
assert typecheck!([x], x.__info__(:functions)) == list(tuple([atom(), integer()]))
assert typeerror!([x], x.__info__(:whatever)) |> strip_ansi() =~ """
incompatible types given to __info__/1:
@@ -1454,6 +1454,16 @@ defmodule Module.Types.ExprTest do
"""
end
test "__info__/1 for struct information" do
assert typecheck!(GenServer.__info__(:struct)) == atom([nil])
assert typecheck!(URI.__info__(:struct)) ==
list(closed_map(default: if_set(term()), field: atom()))
assert typecheck!([x], x.__info__(:struct)) ==
list(closed_map(default: if_set(term()), field: atom())) |> union(atom([nil]))
end
test "behaviour_info/1" do
assert typecheck!([x], x.behaviour_info(:callbacks)) == list(tuple([atom(), integer()]))