File: refs.ex

package info (click to toggle)
elixir-ex-doc 0.35.1%2Bdfsg-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,784 kB
  • sloc: javascript: 2,848; makefile: 15; xml: 12; sh: 5
file content (192 lines) | stat: -rw-r--r-- 4,888 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
defmodule ExDoc.Refs do
  @moduledoc false

  # A read-through cache of documentation references.
  #
  # A given ref is always associated with a module. If we don't have a ref
  # in the cache we fetch the module's docs chunk and fill in the cache.
  #
  # If the module does not have the docs chunk, we fetch it's functions,
  # callbacks and types from other sources.

  @typep entry() :: {ref(), visibility()}

  @typep ref() ::
           {:module, module()}
           | {kind(), module(), name :: atom(), arity()}
  @typep kind() :: :function | :callback | :type
  @typep visibility() :: :hidden | :public | :undefined | :partial

  @name __MODULE__

  use GenServer

  @spec start_link(any()) :: GenServer.on_start()
  def start_link(arg) do
    GenServer.start_link(__MODULE__, arg, name: @name)
  end

  @spec init(any()) :: {:ok, nil}
  def init(_) do
    :ets.new(@name, [:named_table, :public, :set])
    {:ok, nil}
  end

  @spec clear() :: :ok
  def clear() do
    :ets.delete_all_objects(@name)
    :ok
  end

  @spec get_visibility(ref()) :: visibility()
  def get_visibility(ref) do
    case lookup(ref) do
      {:ok, visibility} ->
        visibility

      :error ->
        case fetch(ref) do
          {:ok, visibility} -> visibility
          :error -> :undefined
        end
    end
  end

  @spec insert([entry()]) :: :ok
  def insert(entries) do
    true = :ets.insert(@name, entries)
    :ok
  end

  @spec insert_from_chunk(module, tuple()) :: :ok
  def insert_from_chunk(module, result) do
    module
    |> fetch_entries(result)
    |> insert()

    :ok
  end

  defp lookup(ref) do
    case :ets.lookup(@name, ref) do
      [{^ref, visibility}] -> {:ok, visibility}
      [] -> :error
    end
  rescue
    _ -> :error
  end

  defp fetch({:module, module} = ref) do
    insert_from_chunk(module, Code.fetch_docs(module))
    lookup(ref)
  end

  defp fetch({_kind, module, _name, _arity} = ref) do
    get_visibility({:module, module})
    lookup(ref)
  end

  defp fetch_entries(module, result) do
    case result do
      {:docs_v1, _, _, _, module_doc, _, docs} ->
        module_visibility = visibility(module_doc)

        [{{:module, module}, module_visibility}] ++
          for {{kind, name, arity}, _, _, doc, metadata} <- docs,
              ref_kind = to_ref_kind(kind),
              visibility = visibility(module_doc, {ref_kind, name, doc}),
              arity <- (arity - (metadata[:defaults] || 0))..arity do
            {{ref_kind, module, name, arity}, visibility}
          end

      {:error, _reason} ->
        with true <- :code.which(module) != :non_existing,
             true <- Code.ensure_loaded?(module) do
          # We say it is limited because the types may not actually be available in the beam file.
          [{{:module, module}, :limited}] ++
            to_refs(exports(module), module, :function) ++
            to_refs(callbacks(module), module, :callback) ++
            to_refs(types(module, [:type, :opaque, :nominal]), module, :type)
        else
          _ ->
            [{{:module, module}, :undefined}]
        end
    end
  end

  defguardp has_no_docs(doc) when doc == :none or doc == %{}

  defp starts_with_underscore?(name), do: match?([?_ | _], Atom.to_charlist(name))

  defp visibility(:hidden),
    do: :hidden

  defp visibility(_module_doc),
    do: :public

  defp visibility(_module_doc, {kind, _name, _doc})
       when kind not in [:callback, :function, :type],
       do: raise(ArgumentError, "Unknown kind #{inspect(kind)}")

  defp visibility(:hidden, {_kind, _name, _doc}),
    do: :hidden

  defp visibility(_, {_kind, _name, :hidden}),
    do: :hidden

  defp visibility(_, {kind, name, doc}) when has_no_docs(doc) do
    cond do
      kind in [:callback, :type] ->
        :public

      kind == :function and starts_with_underscore?(name) ->
        :hidden

      kind == :function ->
        :public
    end
  end

  defp visibility(_, {_, _, _}) do
    :public
  end

  defp to_ref_kind(:macro), do: :function
  defp to_ref_kind(:macrocallback), do: :callback
  defp to_ref_kind(other), do: other

  defp exports(module) do
    if function_exported?(module, :__info__, 1) do
      module.__info__(:functions) ++ module.__info__(:macros)
    else
      module.module_info(:exports)
    end
  end

  defp callbacks(module) do
    if function_exported?(module, :behaviour_info, 1) do
      module.behaviour_info(:callbacks)
    else
      []
    end
  end

  defp types(module, kind_list) do
    case Code.Typespec.fetch_types(module) do
      {:ok, list} ->
        for {kind, {name, _, args}} <- list,
            kind in kind_list do
          {name, length(args)}
        end

      :error ->
        []
    end
  end

  defp to_refs(list, module, kind, visibility \\ :public) do
    for {name, arity} <- list do
      {{kind, module, name, arity}, visibility}
    end
  end
end