New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate to Code.fetch_docs/1
#7828
Conversation
It seems to me that while the approach of parsing the docs chunk always on the caller side does the job, it leads to a lot of repetition. There are currently two implementations that look up specific kind of docs, two or three variations on how to interpret doc values ( I think that we should either add a @josevalim thoughts? |
lib/iex/lib/iex/introspection.ex
Outdated
defp translate_doc(nil), do: nil | ||
defp translate_doc(:none), do: nil | ||
defp translate_doc(:hidden), do: nil | ||
defp translate_doc(binary) when is_binary(binary), do: binary |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When can we hit this clause?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From line 379
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we make it be %{"en" => message}
on line 379 then? Or too much hassle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, if we can avoid nil
and use :none
instead... but I am not sure how feasible is that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exactly the symptom. Currently the code is in two minds. Do we keep the atoms used by the chunk and let clients deal with that or translate them early on into idiomatic Elixir? I'm not sure which I would prefer, but certainly not both. :)
The current implementation was mostly a mechanical replacement of Code.get_docs/1
. I think with spending more time on it we could refactor the IEx related bits more. Moving some things like count_defaults
to the chunk while sharing some code between IEx.Autocomplete
and IEx.Introspection
.
Good point. I have seen some repetitions:
Did I miss some cases? |
I agree, if it were only these I wouldn't mind the verbosity. Other repetitions:
I'm mostly concerned about scattering |
While looking for ways to remove repetition I also noticed that we could handle two more cases in case :beam_lib.chunks(bin_or_path, [@docs_chunk]) do
{:ok, {_module, [{@docs_chunk, bin}]}} ->
case :erlang.binary_to_term(bin) do
{:docs_v1, _, _, _, _, _, _} = chunk ->
chunk
tuple when is_tuple(tuple) and is_atom(elem(tuple, 0)) ->
{:error, {:unsupported_version, elem(tuple, 0)}}
unknown ->
{:error, {:unrecognized_chunk, unknown}}
end
{:error, :beam_lib, {:missing_chunk, _, @docs_chunk}} ->
{:error, :docs_chunk_not_found}
end This would also ensure that we're adhering to the spec we promised. |
I think we should move to :none and :hidden because they are clearer and
more idiomatic than nil and false.
Regarding the defaults, the plan was to indeed at them to the metadata. So
I would leave the duplication for now and when we make it easier to define
the metadata from the Elixir side we can revisit it.
--
*José Valimwww.plataformatec.com.br
<http://www.plataformatec.com.br/>Founder and Director of R&D*
|
Also let’s go ahead with the fetch_docs change. :)
--
*José Valimwww.plataformatec.com.br
<http://www.plataformatec.com.br/>Founder and Director of R&D*
|
@josevalim ok, let's live with the duplication for now. I completed the outstanding changes. This frees us to move forward and deal with the removal of |
lib/ex_unit/lib/ex_unit/doc_test.ex
Outdated
|
||
moduledocs = extract_from_moduledoc(all_docs[:moduledoc], module) | ||
defp explain_docs_error(:module_not_found), | ||
do: "The BEAM file of the modoule cannot be accessed" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo “modoule”
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 | ||
end | ||
|
||
defp find_doc_defaults(docs, function, min) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to get rid of that elem(doc, 2)
what do you think about?
defp find_doc_defaults(docs, function, min) do
Enum.find(docs, fn
{{_, ^function, arity}, _, signature, _, _} when arity > min ->
defaults = count_defaults(signature)
arity <= min + defaults
_ ->
false
end)
end
lib/elixir/lib/code.ex
Outdated
{:docs_v1, _, _, _, _, _, _} = chunk -> | ||
chunk | ||
|
||
tuple when is_tuple(tuple) and is_atom(elem(tuple, 0)) -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, now looking at the code, I don't think we should return this. The idea of a version being supported or not should be up to the caller and not up to us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@josevalim I see, you want to make fetch_docs
transparent and put the burden of knowing what's in the chunks entirely on the caller.
With this change and the other about rescuing binary_to_term
I'm thinking that we should also revisit the typespec for Code.fetch_docs/1
. We can't really promise to return the current spec unless we match on the result of binary_to_term
. What should be the approach for the spec in this case? I would like to make the spec more useful than simply @spec fetch_docs(module) :: term | {:error, term}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can try:
@spec fetch_docs(module) :: {:docs_v1, ...} | {:error, term} | future_formats when future_formats: term
But this may have negative implications in dialyzer. We can give it a try though.
lib/elixir/lib/code.ex
Outdated
tuple when is_tuple(tuple) and is_atom(elem(tuple, 0)) -> | ||
{:error, {:unsupported_version, elem(tuple, 0)}} | ||
|
||
unknown -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the binary_to_term
could fail altogether, maybe we should do this:
try do
:erlang.binary_to_term(binary)
rescue
_ -> {:error, {:invalid_chunk, binary}}
end
Maybe somebody could put invalid data into the chunk but, if they do that, they deserve the errors they will get (which is true for any other chunk). :)
else | ||
"" | ||
case Code.fetch_docs(mod) do | ||
{:docs_v1, _, _, _, %{}, _, _} -> "Use h(#{inspect(mod)}) to access its documentation.\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe here the opposite would be better?
{:error, _} -> ""
_ -> "Use h(#{inspect(mod)}) to access its documentation.\n"
This way if we add new versions in the future we won't need to change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The module doc elem can also be :none
or :hidden
in which case h/1
won't be able to return any documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see, thank you.
lib/mix/lib/mix/task.ex
Outdated
@@ -124,9 +124,10 @@ defmodule Mix.Task do | |||
""" | |||
@spec moduledoc(task_module) :: String.t() | nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the typespec we always return nil
. Maybe we could get away with returning nil
for false
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I should've looked at the typespec.
@lackac I have dropped some final comments. :) |
lib/elixir/lib/kernel/typespec.ex
Outdated
@@ -24,10 +24,20 @@ defmodule Kernel.Typespec do | |||
|
|||
@doc false | |||
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") | |||
IO.warn("Kernel.Typespec.beam_typedocs/1 is deprecated, please use Code.fetch_docs/1 instead") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we migrate to the @deprecated
attribute here while we're at it?
lib/ex_unit/lib/ex_unit/doc_test.ex
Outdated
@@ -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, line, _, _, moduledoc, _, docs} -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is line
always going to be a line number? The spec of Code.get_docs
defines it as :erl_anno.anno()
which might have more data in it. Should we use :erl_anno.line(anno)
to get the line here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would certainly be safer. However, in doc tests we know that it's just a line since it was generated by the Elixir compiler where we don't include more information. But since that could change I think it's best to go with your advice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, lets go with :erl_anno.line/1
.
Enum.count(args, &match?({:\\, _, _}, &1)) | ||
defp count_defaults(signature) do | ||
signature | ||
|> Stream.flat_map(&Regex.scan(~r/ \\\\ /, &1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that's reliable - I could imagine code like this that would miscalculate here:
iex(3)> quote(do: def foo(bar \\ ~S| \\ |)) |> Macro.to_string
"def(foo(bar \\\\ ~S\" \\\\ \"))"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just temporary until we move the number of defaults to metadata. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're right. We discussed this earlier:
I wrote:
I think we could replace [
count_defaults
] by adding something likedefaults: 2
ordefaults: [:year, :options]
to the metadata inelixir_erl
Regarding the defaults, the plan was to indeed at them to the metadata. So
I would leave the duplication for now and when we make it easier to define
the metadata from the Elixir side we can revisit it.
I would look at that after we remove ExDc
and we have a chance to refactor the chunk generation and how and what we store about docs in ETS during compilation. I think we can live with this edge case in autocompletion until then.
@@ -122,11 +122,12 @@ defmodule Mix.Task do | |||
|
|||
Returns the moduledoc or `nil`. | |||
""" | |||
@spec moduledoc(task_module) :: String.t() | nil | |||
@spec moduledoc(task_module) :: String.t() | nil | false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently the spec was wrong, hence the build failure. I Fixed it and updated the spec. If the build is green tomorrow morning I will go ahead and merge this. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I was about to push the same thing. :)
❤️ 💚 💙 💛 💜 |
Related to #7198.