Skip to content

Commit

Permalink
Migrate to Code.fetch_docs/1 (#7828)
Browse files Browse the repository at this point in the history
  • Loading branch information
lackac authored and josevalim committed Jul 4, 2018
1 parent f84eff9 commit caa90b5
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 144 deletions.
12 changes: 9 additions & 3 deletions lib/elixir/lib/behaviour.ex
Expand Up @@ -97,14 +97,20 @@ defmodule Behaviour do
end

def __behaviour__(:docs) do
for {tuple, line, kind, docs} <- Code.get_docs(__MODULE__, :callback_docs) do
{:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(__MODULE__)

for {{kind, name, arity}, line, _, doc, _} <- docs, kind in [:callback, :macrocallback] do
case kind do
:callback -> {tuple, line, :def, docs}
:macrocallback -> {tuple, line, :defmacro, docs}
:callback -> {{name, arity}, line, :def, __behaviour__doc_value(doc)}
:macrocallback -> {{name, arity}, line, :defmacro, __behaviour__doc_value(doc)}
end
end
end

defp __behaviour__doc_value(:none), do: nil
defp __behaviour__doc_value(:hidden), do: false
defp __behaviour__doc_value(%{"en" => doc}), do: doc

import unquote(__MODULE__)
end
end
Expand Down
14 changes: 10 additions & 4 deletions lib/elixir/lib/code.ex
Expand Up @@ -1069,15 +1069,17 @@ defmodule Code do
@spec fetch_docs(module) ::
{:docs_v1, anno, beam_language, format, module_doc :: doc, metadata,
docs :: [{{kind, name, arity}, anno, signature, doc, metadata}]}
| {:error, :module_not_found | :docs_chunk_not_found}
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
| future_formats
when anno: :erl_anno.anno(),
beam_language: atom,
format: binary,
doc: %{binary => binary} | :none | :hidden,
kind: atom,
name: atom,
signature: [binary],
metadata: map
metadata: map,
future_formats: term
@since "1.7.0"
def fetch_docs(module) when is_atom(module) do
case :code.get_object_code(module) do
Expand All @@ -1095,10 +1097,14 @@ defmodule Code do
defp do_fetch_docs(bin_or_path) do
case :beam_lib.chunks(bin_or_path, [@docs_chunk]) do
{:ok, {_module, [{@docs_chunk, bin}]}} ->
:erlang.binary_to_term(bin)
try do
:erlang.binary_to_term(bin)
rescue
_ -> {:error, {:invalid_chunk, bin}}
end

{:error, :beam_lib, {:missing_chunk, _, @docs_chunk}} ->
{:error, :docs_chunk_not_found}
{:error, :chunk_not_found}
end
end

Expand Down
15 changes: 12 additions & 3 deletions lib/elixir/lib/kernel/typespec.ex
Expand Up @@ -23,11 +23,20 @@ defmodule Kernel.Typespec do
end

@doc false
@deprecated "Use Code.fetch_docs/1 instead"
def beam_typedocs(module) when is_atom(module) or is_binary(module) do
IO.warn("Kernel.Typespec.beam_typedocs/1 is deprecated, please use Code.get_docs/2 instead")
case Code.fetch_docs(module) do
{:docs_v1, _, _, _, _, _, docs} ->
for {{:type, name, arity}, _, _, doc, _} <- docs do
case doc do
:none -> {{name, arity}, nil}
:hidden -> {{name, arity}, false}
%{"en" => doc_string} -> {{name, arity}, doc_string}
end
end

if docs = Code.get_docs(module, :type_docs) do
for {tuple, _, _, doc} <- docs, do: {tuple, doc}
{:error, _} ->
nil
end
end

Expand Down
4 changes: 2 additions & 2 deletions lib/elixir/pages/Writing Documentation.md
Expand Up @@ -121,6 +121,6 @@ Elixir warns if a private function has a `@doc` attribute and discards its conte

Private functions may still need internal documentation for maintainers, though. That can be accomplished with code comments.

## Code.get_docs/2
## Code.fetch_docs/1

Elixir stores documentation inside pre-defined chunks in the bytecode. It can be accessed from Elixir by using the `Code.get_docs/2` function. This also means documentation is only accessed when required and not when modules are loaded by the Virtual Machine. The only downside is that modules defined in-memory, like the ones defined in IEx, cannot have their documentation accessed as they do not have their bytecode written to disk.
Elixir stores documentation inside pre-defined chunks in the bytecode. It can be accessed from Elixir by using the `Code.fetch_docs/1` function. This also means documentation is only accessed when required and not when modules are loaded by the Virtual Machine. The only downside is that modules defined in-memory, like the ones defined in IEx, cannot have their documentation accessed as they do not have their bytecode written to disk.
2 changes: 1 addition & 1 deletion lib/elixir/test/elixir/kernel/docs_test.exs
Expand Up @@ -38,7 +38,7 @@ defmodule Kernel.DocsTest do
end
)

assert Code.fetch_docs(WithoutDocs) == {:error, :docs_chunk_not_found}
assert Code.fetch_docs(WithoutDocs) == {:error, :chunk_not_found}
after
Code.compiler_options(docs: true)
end
Expand Down
49 changes: 30 additions & 19 deletions lib/ex_unit/lib/ex_unit/doc_test.ex
Expand Up @@ -427,39 +427,50 @@ defmodule ExUnit.DocTest do
## Extraction of the tests

defp extract(module) do
all_docs = Code.get_docs(module, :all)
case Code.fetch_docs(module) do
{:docs_v1, anno, _, _, moduledoc, _, docs} ->
line = :erl_anno.line(anno)
extract_from_moduledoc({line, moduledoc}, module) ++ extract_from_docs(docs, module)

unless all_docs do
raise Error,
module: module,
message:
"could not retrieve the documentation for module #{inspect(module)}. " <>
"The module was not compiled with documentation or its BEAM file cannot be accessed"
{:error, reason} ->
raise Error,
module: module,
message:
"could not retrieve the documentation for module #{inspect(module)}. " <>
explain_docs_error(reason)
end
end

moduledocs = extract_from_moduledoc(all_docs[:moduledoc], module)
defp explain_docs_error(:module_not_found),
do: "The BEAM file of the module cannot be accessed"

docs =
for doc <- all_docs[:docs],
doc <- extract_from_doc(doc, module),
do: doc
defp explain_docs_error(:chunk_not_found),
do: "The module was not compiled with documentation"

moduledocs ++ docs
end
defp explain_docs_error({:invalid_chunk, _}),
do: "The documentation chunk in the module is invalid"

defp extract_from_moduledoc({_, doc}, _module) when doc in [false, nil], do: []
defp extract_from_moduledoc({_, doc}, _module) when doc in [:none, :hidden], do: []

defp extract_from_moduledoc({line, doc}, module) do
defp extract_from_moduledoc({line, %{"en" => doc}}, module) do
for test <- extract_tests(line, doc, module) do
normalize_test(test, :moduledoc)
end
end

defp extract_from_doc({_, _, _, _, doc}, _module) when doc in [false, nil], do: []
defp extract_from_docs(docs, module) do
for doc <- docs, doc <- extract_from_doc(doc, module), do: doc
end

defp extract_from_doc({{kind, _, _}, _, _, doc, _}, _module)
when kind not in [:function, :macro] or doc in [:none, :hidden],
do: []

defp extract_from_doc({{_, name, arity}, anno, _, %{"en" => doc}, _}, module) do
line = :erl_anno.line(anno)

defp extract_from_doc({fa, line, _, _, doc}, module) do
for test <- extract_tests(line, doc, module) do
normalize_test(test, fa)
normalize_test(test, {name, arity})
end
end

Expand Down
53 changes: 30 additions & 23 deletions lib/iex/lib/iex/autocomplete.ex
Expand Up @@ -409,7 +409,7 @@ defmodule IEx.Autocomplete do
not ensure_loaded?(mod) ->
[]

docs = Code.get_docs(mod, :docs) ->
docs = get_docs(mod, [:function, :macro]) ->
exports(mod)
|> Kernel.--(default_arg_functions_with_doc_false(docs))
|> Enum.reject(&hidden_fun?(&1, docs))
Expand All @@ -424,8 +424,8 @@ defmodule IEx.Autocomplete do
not ensure_loaded?(mod) ->
[]

docs = Code.get_docs(mod, :type_docs) ->
Enum.map(docs, &elem(&1, 0))
docs = get_docs(mod, [:type]) ->
Enum.map(docs, &extract_name_and_arity/1)

true ->
exports(mod)
Expand All @@ -437,43 +437,50 @@ defmodule IEx.Autocomplete do
not ensure_loaded?(mod) ->
[]

docs = Code.get_docs(mod, :callback_docs) ->
Enum.map(docs, &elem(&1, 0))
docs = get_docs(mod, [:callback, :macrocallback]) ->
Enum.map(docs, &extract_name_and_arity/1)

true ->
exports(mod)
end
end

defp get_docs(mod, kinds) do
case Code.fetch_docs(mod) do
{:docs_v1, _, _, _, _, _, docs} ->
for {{kind, _, _}, _, _, _, _} = doc <- docs, kind in kinds, do: doc

{:error, _} ->
nil
end
end

defp extract_name_and_arity({{_, name, arity}, _, _, _, _}), do: {name, arity}

defp default_arg_functions_with_doc_false(docs) do
for {{fun_name, arity}, _, _, args, false} <- docs,
count = count_defaults(args),
for {{_, fun_name, arity}, _, signature, :hidden, _} <- docs,
count = count_defaults(signature),
count > 0,
new_arity <- (arity - count)..arity,
do: {fun_name, new_arity}
end

defp count_defaults(args) do
Enum.count(args, &match?({:\\, _, _}, &1))
defp count_defaults(signature) do
signature
|> Stream.flat_map(&Regex.scan(~r/ \\\\ /, &1))
|> Enum.count()
end

defp hidden_fun?(fun, docs) do
case List.keyfind(docs, fun, 0) do
nil ->
underscored_fun?(fun)

{_, _, _, _, false} ->
true

{fun, _, _, _, nil} ->
underscored_fun?(fun)

{_, _, _, _, _} ->
false
defp hidden_fun?({name, arity}, docs) do
case Enum.find(docs, &match?({{_, ^name, ^arity}, _, _, _, _}, &1)) do
nil -> underscored_fun?(name)
{_, _, _, :hidden, _} -> true
{_, _, _, :none, _} -> underscored_fun?(name)
{_, _, _, _, _} -> false
end
end

defp underscored_fun?({name, _}), do: hd(Atom.to_charlist(name)) == ?_
defp underscored_fun?(name), do: hd(Atom.to_charlist(name)) == ?_

defp ensure_loaded?(Elixir), do: false
defp ensure_loaded?(mod), do: Code.ensure_loaded?(mod)
Expand Down
7 changes: 3 additions & 4 deletions lib/iex/lib/iex/info.ex
Expand Up @@ -45,10 +45,9 @@ defimpl IEx.Info, for: Atom do

defp info_module(mod) do
extra =
if Code.get_docs(mod, :moduledoc) do
"Use h(#{inspect(mod)}) to access its documentation.\n"
else
""
case Code.fetch_docs(mod) do
{:docs_v1, _, _, _, %{}, _, _} -> "Use h(#{inspect(mod)}) to access its documentation.\n"
_ -> ""
end

mod_info = mod.module_info()
Expand Down

0 comments on commit caa90b5

Please sign in to comment.