Provide an API to access documentation metadata at compile time · Issue #8095 · elixir-lang/elixir · GitHub
Skip to content

Provide an API to access documentation metadata at compile time #8095

Description

@hauleth

Environment

  • Elixir & Erlang/OTP versions (elixir --version): 1.7.2
  • Operating system: macOS

Current behavior

I wanted to create macro that would allow me to remove boilerplate over generating structures for events in my application. To document such structures I wanted to use @doc to simulate feeling that these are normal parts of the external module instead of being separate modules, ex.:

defmodule MyApp.Event do
  import Events

  @moduledoc false

  @doc """
  Creation of new submission
  """
  @doc deprecated: "Foo"
  defevent SubmissionCreated,
    id: any(),
    name: String.t(),
    author: [map()]
end

While basic implementation is dumb easy documentation is quite challenging. What I have achieved is:

defmodule Events do
  @moduledoc false

  defmacro defevent(module, fields \\ []) do
    keys = Keyword.keys(fields)

    quote do
      docs = Module.get_attribute(__MODULE__, :doc)
      deprecated = case Module.get_attribute(__MODULE__, :deprecated) do
        nil -> []
        value -> [deprecated: value]
      end

      {set, _} = :elixir_module.data_tables(__MODULE__)
      metadata = case :ets.lookup(set, {:doc, :meta}) do
        [{{:doc, :meta}, metadata, _}] -> deprecated ++ Keyword.new(metadata)
        [] -> deprecated
      end

      defmodule unquote(module) do
        if docs, do: Module.put_attribute(__MODULE__, :moduledoc, docs)
        if metadata != [], do: @moduledoc metadata

        @type t :: %__MODULE__{unquote_splicing(fields)}

        defstruct unquote(keys)
      end

      :elixir_module.delete_definition_attributes(__ENV__, nil, nil, nil, nil, nil)
    end
  end
end

Which is fairly ok, except of the part where I need manually get data from ETS for documentation metadata. It cannot be mitigated by using Module.get_attribute/2 as it explicitly requires atom as a second argument while metadata are stored under {:doc, :meta}.

Expected behavior

Somehow allow fetching documentation metadata in macros. This would allow macro writers to utilise that metadata in some way like example above.

Activity

  1. fertapric commented on Aug 16, 2018

    @fertapric
    Member

    I would add "at compile time" to the title, because once compiled I think you can access the documentation using Code.fetch_docs/1 :)

  2. changed the title [-]There is no way to programatically access to documentation metadata[/-] [+]Provide an API to access documentation metadata at compile time[/+] on Aug 16, 2018
  3. josevalim commented on Aug 16, 2018

    @josevalim
    Member

    The documentation metadata and related fields is stored as private fields on the table, that's why you can't access it and you shouldn't rely on the compiler internals for this. :)

    The other thing is that, even if we provide a way to access metadata, then you would also need a way to remove the documentation because there is no actual function definition. It feels like the best way to go here is to use your own attribute, such as @eventdoc, especially because you won't pretend that you are defining a function when it is actually a module.

  4. josevalim commented on Aug 16, 2018

    @josevalim
    Member

    The proper solution here would be to make @doc and friends an accumulated attribute, so accessing @doc would return all previously set values:

    @doc "foo"
    @doc bar: 1
    @doc #=> [{2, bar: 1}, {1, "foo"}]

    This would also allow us to get rid of special cases in both Module and Kernel. The trouble with doing this now is that it will break code that expects @doc to always be a binary (or similar). So we can only do it on Elixir 2.0.

  5. added this to the v2.0.0 milestone on Aug 16, 2018
  6. hauleth commented on Aug 16, 2018

    @hauleth
    ContributorAuthor

    @josevalim I agree that in this particular situation it makes sense to use custom attribute instead, but imagine something like that:

    defmacro defrecord(name, tag \\ nil, kv) do
      quote do
        def unquote(name)(), do: …
        def unquote(name)(values), do: …
        def unquote(name)(record, values), do: …
      end
    end

    And while you maybe do not want to share most docs between them then currently supported tags makes sense for them (like :since or :deprecated).

  7. mguimas commented on May 9, 2019

    @mguimas
    Contributor

    @josevalim Instead of this

    @doc "foo"
    @doc bar: 1
    @doc #=> [{2, bar: 1}, {1, "foo"}]
    

    could you consider this

    @doc "foo"
    @doc bar: 1
    @doc #=> [doc: "foo", bar: 1]
    

    ?

  8. josevalim commented on Jun 23, 2020

    @josevalim
    Member

    @mguimas unfortunately a metadata can be named :doc, so that would be ambiguous, we will probably use a tuple-based format, such as {"doc", meta: "data"}, or a map: %{doc: "...", metadata: [...]}.

  9. hauleth commented on Jun 23, 2020

    @hauleth
    ContributorAuthor

    @josevalim I would vote for the tuple + keyword list so:

    @doc "foo:
    @doc bar: 1
    @doc bar: 2
    

    Would result with:

    {"foo", [bar: 1, bar: 2]}
  10. mguimas commented on Jun 23, 2020

    @mguimas
    Contributor

    @josevalim I believe that a good data structure is one that makes it is easy to use Enum to traverse the information associated with the @doc entries.

    So in the example

    @doc :foo
    @doc bar: 1
    @doc bar: 2
    @doc :bar
    

    perhaps a good solution is

    [:foo, {:bar, 1}, {:bar, 2}, :bar]

    which respects not only the contents, but also the order of the doc entries.

  11. Nezteb commented on Dec 22, 2023

    @Nezteb
    Contributor

    @hauleth @josevalim It's been 3 years; I don't think this issue has been resolved, but I figured I'd check in with both of you to verify. 😄

  12. wojtekmach commented on Dec 22, 2023

    @wojtekmach
    Member

    It’s not. It’s marked for Elixir v2.0.

  13. Nezteb commented on Dec 22, 2023

    @Nezteb
    Contributor

    It’s marked for Elixir v2.0.

    Ah my bad. I'm on mobile and didn't scroll down to see the addition of the 2.0 milestone. 🤦‍♂️

    Disregard me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions