summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFernando Tapia Rico <fertapric@gmail.com>2018-12-15 08:51:28 +0100
committerJosé Valim <jose.valim@gmail.com>2018-12-15 08:51:28 +0100
commitdadb412cefc2209d0c74aeda092d6bbf7ebd80ae (patch)
tree911c9049d89666f235cbcc8693d3ec706b125f3b
parent93ceb95e762023191eb107dc06935be0f5ce7dbc (diff)
downloadelixir-dadb412cefc2209d0c74aeda092d6bbf7ebd80ae.tar.gz
Use typespec info in IEx.Helpers.b/1 (#8514)
The previous approach was using the docs chunk as the source of truth. Because of that, Protocols or Erlang/OTP modules were not showing callback information.
-rw-r--r--lib/iex/lib/iex/introspection.ex114
-rw-r--r--lib/iex/test/iex/helpers_test.exs65
2 files changed, 125 insertions, 54 deletions
diff --git a/lib/iex/lib/iex/introspection.ex b/lib/iex/lib/iex/introspection.ex
index 6630f3641..bd5e2f0a8 100644
--- a/lib/iex/lib/iex/introspection.ex
+++ b/lib/iex/lib/iex/introspection.ex
@@ -287,7 +287,7 @@ defmodule IEx.Introspection do
result =
for {^function, arity} <- exports,
(if docs do
- find_doc(docs, function, arity)
+ find_doc_with_content(docs, function, arity)
else
get_spec(module, function, arity) != []
end) do
@@ -364,7 +364,7 @@ defmodule IEx.Introspection do
spec = get_spec(mod, fun, arity)
cond do
- doc_tuple = find_doc(docs, fun, arity) ->
+ doc_tuple = find_doc_with_content(docs, fun, arity) ->
print_fun(mod, doc_tuple, spec)
:ok
@@ -388,15 +388,17 @@ defmodule IEx.Introspection do
end
defp has_callback?(mod, fun) do
- mod
- |> get_docs([:callback, :macrocallback])
- |> Enum.any?(&match?({_, ^fun, _}, elem(&1, 0)))
+ case get_callback_docs(mod, &match?({_, ^fun, _}, elem(&1, 0))) do
+ {:ok, [_ | _]} -> true
+ _ -> false
+ end
end
defp has_callback?(mod, fun, arity) do
- mod
- |> get_docs([:callback, :macrocallback])
- |> Enum.any?(&match?({_, ^fun, ^arity}, elem(&1, 0)))
+ case get_callback_docs(mod, &match?({_, ^fun, ^arity}, elem(&1, 0))) do
+ {:ok, [_ | _]} -> true
+ _ -> false
+ end
end
defp has_type?(mod, fun) do
@@ -423,16 +425,22 @@ defmodule IEx.Introspection do
defp extract_name_and_arity({{_, name, arity}, _, _, _, _}), do: {name, arity}
+ defp find_doc_with_content(docs, function, arity) do
+ doc = find_doc(docs, function, arity)
+ if doc != nil and has_content?(doc), do: doc
+ end
+
+ defp has_content?({_, _, _, :hidden, _}), do: false
+ defp has_content?({{_, name, _}, _, _, :none, _}), do: hd(Atom.to_charlist(name)) != ?_
+ defp has_content?({_, _, _, _, _}), do: true
+
defp find_doc(nil, _fun, _arity) do
nil
end
defp find_doc(docs, fun, arity) do
- doc =
- Enum.find(docs, &match?({_, ^fun, ^arity}, elem(&1, 0))) ||
- find_doc_defaults(docs, fun, arity)
-
- if doc != nil and has_content?(doc), do: doc
+ Enum.find(docs, &match?({_, ^fun, ^arity}, elem(&1, 0))) ||
+ find_doc_defaults(docs, fun, arity)
end
defp find_doc_defaults(docs, function, min) do
@@ -445,10 +453,6 @@ defmodule IEx.Introspection do
end)
end
- defp has_content?({_, _, _, :hidden, _}), do: false
- defp has_content?({{_, name, _}, _, _, :none, _}), do: hd(Atom.to_charlist(name)) != ?_
- defp has_content?({_, _, _, _, _}), do: true
-
defp print_fun(mod, {{kind, fun, arity}, _line, signature, doc, metadata}, spec) do
if callback_module = doc == :none and callback_module(mod, fun, arity) do
filter = &match?({_, ^fun, ^arity}, elem(&1, 0))
@@ -466,19 +470,10 @@ defmodule IEx.Introspection do
defp kind_to_def(:macro), do: :defmacro
defp callback_module(mod, fun, arity) do
- predicate = &match?({{^fun, ^arity}, _}, &1)
-
mod.module_info(:attributes)
|> Keyword.get_values(:behaviour)
|> Stream.concat()
- |> Enum.find(&Enum.any?(get_callbacks(&1), predicate))
- end
-
- defp get_callbacks(module) do
- case Typespec.fetch_callbacks(module) do
- {:ok, callbacks} -> callbacks
- :error -> []
- end
+ |> Enum.find(&has_callback?(&1, fun, arity))
end
defp get_spec(module, name, arity) do
@@ -504,9 +499,6 @@ defmodule IEx.Introspection do
:no_beam ->
no_beam(mod)
- :no_docs ->
- no_docs(mod)
-
{:ok, []} ->
puts_error("No callbacks for #{inspect(mod)} were found")
@@ -524,7 +516,6 @@ defmodule IEx.Introspection do
case get_callback_docs(mod, filter) do
:no_beam -> no_beam(mod)
- :no_docs -> no_docs(mod)
{:ok, []} -> docs_not_found("#{inspect(mod)}.#{fun}")
{:ok, docs} -> Enum.each(docs, &print_typespec/1)
end
@@ -537,7 +528,6 @@ defmodule IEx.Introspection do
case get_callback_docs(mod, filter) do
:no_beam -> no_beam(mod)
- :no_docs -> no_docs(mod)
{:ok, []} -> docs_not_found("#{inspect(mod)}.#{fun}/#{arity}")
{:ok, docs} -> Enum.each(docs, &print_typespec/1)
end
@@ -557,26 +547,54 @@ defmodule IEx.Introspection do
:error ->
:no_beam
- _ when is_nil(docs) ->
- :no_docs
-
{:ok, callbacks} ->
docs =
- docs
+ callbacks
+ |> Enum.map(&translate_callback/1)
|> Enum.filter(filter)
- |> Enum.map(fn
- {{:macrocallback, fun, arity}, _, _, doc, metadata} ->
- macro = {:"MACRO-#{fun}", arity + 1}
- {format_callback(:macrocallback, fun, macro, callbacks), doc, metadata}
-
- {{kind, fun, arity}, _, _, doc, metadata} ->
- {format_callback(kind, fun, {fun, arity}, callbacks), doc, metadata}
+ |> Enum.sort()
+ |> Enum.flat_map(fn {{_, function, arity}, _specs} = callback ->
+ case find_doc(docs, function, arity) do
+ nil -> [{format_callback(callback), :none, %{}}]
+ {_, _, _, :hidden, _} -> []
+ {_, _, _, doc, metadata} -> [{format_callback(callback), doc, metadata}]
+ end
end)
{:ok, docs}
end
end
+ defp translate_callback({{name, arity}, specs}) do
+ case Atom.to_string(name) do
+ "MACRO-" <> macro_name ->
+ # The typespec of a macrocallback differs from the one expressed
+ # via @macrocallback:
+ #
+ # * The function name is prefixed with "MACRO-"
+ # * The arguments contain an additional first argument: the caller
+ # * The arity is increased by 1
+ #
+ specs =
+ Enum.map(specs, fn {:type, line1, :fun, [{:type, line2, :product, [_ | args]}, spec]} ->
+ {:type, line1, :fun, [{:type, line2, :product, args}, spec]}
+ end)
+
+ {{:macrocallback, String.to_atom(macro_name), arity - 1}, specs}
+
+ _ ->
+ {{:callback, name, arity}, specs}
+ end
+ end
+
+ defp format_callback({{kind, name, _arity}, specs}) do
+ Enum.map(specs, fn spec ->
+ Typespec.spec_to_quoted(name, spec)
+ |> Macro.prewalk(&drop_macro_env/1)
+ |> format_typespec(kind, 0)
+ end)
+ end
+
defp add_optional_callback_docs(docs, mod) do
optional_callbacks =
if Code.ensure_loaded?(mod) and function_exported?(mod, :behaviour_info, 1) do
@@ -596,16 +614,6 @@ defmodule IEx.Introspection do
format_typespec(callbacks, :optional_callbacks, 0)
end
- defp format_callback(kind, name, key, callbacks) do
- {_, specs} = List.keyfind(callbacks, key, 0)
-
- Enum.map(specs, fn spec ->
- Typespec.spec_to_quoted(name, spec)
- |> Macro.prewalk(&drop_macro_env/1)
- |> format_typespec(kind, 0)
- end)
- end
-
defp drop_macro_env({name, meta, [{:::, _, [_, {{:., _, [Macro.Env, :t]}, _, _}]} | args]}),
do: {name, meta, args}
diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs
index 233f3c38b..07828b49e 100644
--- a/lib/iex/test/iex/helpers_test.exs
+++ b/lib/iex/test/iex/helpers_test.exs
@@ -611,7 +611,7 @@ defmodule IEx.HelpersTest do
end
describe "b" do
- test "lists all callbacks for a module" do
+ test "lists all callbacks for an Elixir module" do
assert capture_io(fn -> b(Mix) end) == "No callbacks for Mix were found\n"
assert capture_io(fn -> b(NoMix) end) == "Could not load module NoMix, got: nofile\n"
@@ -622,6 +622,43 @@ defmodule IEx.HelpersTest do
"""
end
+ test "lists all callbacks for an Erlang module" do
+ output = capture_io(fn -> b(:gen_server) end)
+
+ assert output =~ "@callback handle_cast(request :: term(), state :: term()) ::"
+ assert output =~ "@callback handle_info(info :: :timeout | term(), state :: term()) ::"
+ assert output =~ "@callback init(args :: term()) ::"
+ end
+
+ test "lists all macrocallbacks for a module" do
+ filename = "macrocallbacks.ex"
+
+ content = """
+ defmodule Macrocallbacks do
+ @macrocallback test(:foo) :: integer
+ end
+ """
+
+ with_file(filename, content, fn ->
+ assert c(filename, ".") == [Macrocallbacks]
+
+ assert capture_io(fn -> b(Macrocallbacks) end) =~
+ "@macrocallback test(:foo) :: integer()\n\n"
+ end)
+ after
+ cleanup_modules([Macrocallbacks])
+ end
+
+ test "lists all callbacks for a protocol" do
+ assert capture_io(fn -> b(Enumerable) end) =~ """
+ @callback count(t()) :: {:ok, non_neg_integer()} | {:error, module()}
+
+ @callback member?(t(), term()) :: {:ok, boolean()} | {:error, module()}
+
+ @callback reduce(t(), acc(), reducer()) :: result()
+ """
+ end
+
test "lists callback with multiple clauses" do
filename = "multiple_clauses_callback.ex"
@@ -656,6 +693,9 @@ defmodule IEx.HelpersTest do
assert capture_io(fn -> b(Exception.message() / 1) end) ==
"@callback message(t()) :: String.t()\n\n"
+
+ assert capture_io(fn -> b(:gen_server.handle_cast() / 2) end) =~
+ "@callback handle_cast(request :: term(), state :: term()) ::"
end
test "prints callback documentation metadata" do
@@ -703,6 +743,29 @@ defmodule IEx.HelpersTest do
after
cleanup_modules([OptionalCallbacks])
end
+
+ test "does not print docs for @doc false callbacks" do
+ filename = "hidden_callbacks.ex"
+
+ content = """
+ defmodule HiddenCallbacks do
+ @doc false
+ @callback hidden_callback() :: integer
+
+ @doc false
+ @macrocallback hidden_macrocallback() :: integer
+ end
+ """
+
+ with_file(filename, content, fn ->
+ assert c(filename, ".") == [HiddenCallbacks]
+
+ assert capture_io(fn -> b(HiddenCallbacks) end) =~
+ "No callbacks for HiddenCallbacks were found\n"
+ end)
+ after
+ cleanup_modules([HiddenCallbacks])
+ end
end
describe "t" do