From e9e4e24ef71e80ed5896d3bea5e4f03a5ba8bcb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 29 Dec 2024 19:45:10 +0100 Subject: [PATCH] Track __info__(:struct) more precisely Closes #14127. --- lib/elixir/lib/module/parallel_checker.ex | 13 ++- lib/elixir/lib/module/types/apply.ex | 88 +++++++++++-------- .../test/elixir/module/types/expr_test.exs | 14 ++- 3 files changed, 68 insertions(+), 47 deletions(-) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index fc9788f1d0..87d81c8220 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -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 diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 40f535e7a3..ef9e5f3d1c 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -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([ diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index d4c3461303..2373e686c1 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -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()]))