File: source.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 (274 lines) | stat: -rw-r--r-- 8,574 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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
defmodule ExDoc.Language.Source do
  @moduledoc false

  def anno_line(line) when is_integer(line), do: abs(line)
  def anno_line(anno), do: anno |> :erl_anno.line() |> abs()

  def anno_file(anno) do
    case :erl_anno.file(anno) do
      :undefined ->
        nil

      file ->
        String.Chars.to_string(file)
    end
  end

  @doc """
  Get abstract code and basedir for a module

  The abstract code has been traversed so that all records in types
  have had their fields in-lined.
  """
  def get_abstract_code(module) do
    with {^module, binary, _file} <- :code.get_object_code(module),
         {:ok, {_, [{:abstract_code, {_vsn, abst_code}}]}} <-
           :beam_lib.chunks(binary, [:abstract_code]) do
      expand_records_in_types(abst_code)
    else
      _ -> nil
    end
  end

  defp expand_records_in_types(abst_code) do
    # Find all records in ast and collect any fields with type declarations
    records =
      filtermap_ast(abst_code, nil, fn
        {:attribute, anno, :record, {name, fields}} ->
          {name,
           fields
           |> Enum.flat_map(fn
             {:typed_record_field, record_field, type} ->
               [{:type, anno, :field_type, [elem(record_field, 2), type]}]

             record_field when elem(record_field, 0) == :record_field ->
               [{:type, anno, :field_type, [elem(record_field, 2), {:type, anno, :term, []}]}]

             _ ->
               []
           end)}

        _ ->
          nil
      end)
      |> Map.new()

    # Expand records in all specs, callbacks, types, opaques and nominals
    filtermap_ast(abst_code, nil, fn
      {:attribute, anno, kind, {mfa, ast}} when kind in [:spec, :callback] ->
        ast = Enum.map(ast, &expand_records(&1, records))
        {:attribute, anno, kind, {mfa, ast}}

      {:attribute, anno, type, {name, ast, args}} when type in [:opaque, :nominal, :type] ->
        {:attribute, anno, type, {name, expand_records(ast, records), args}}

      otherwise ->
        otherwise
    end)
  end

  defp expand_records(types, records) when is_list(types) do
    Enum.map(types, &expand_records(&1, records))
  end

  defp expand_records({:ann_type, anno, [name, type]}, records) do
    {:ann_type, anno, [name, expand_records(type, records)]}
  end

  # When we encounter a record, we fetch the type definitions in the record and
  # merge then with the type. If there are duplicates we take the one in the type
  # declaration
  defp expand_records({:type, anno, :record, [{:atom, _, record} = name | args]}, records) do
    args =
      (args ++ Map.get(records, record, []))
      |> Enum.uniq_by(fn {:type, _, :field_type, [{:atom, _, name} | _]} -> name end)

    # We delete the record from the map so that recursive
    # record definitions are not expanded.
    records = Map.delete(records, record)

    {:type, anno, :record, expand_records([name | args], records)}
  end

  defp expand_records({type, anno, what, args}, records) when type in [:type, :user_type] do
    {:type, anno, what, expand_records(args, records)}
  end

  defp expand_records({:remote_type, anno, [m, t, args]}, records) do
    {:remote_type, anno, [m, t, expand_records(args, records)]}
  end

  defp expand_records(otherwise, _records) do
    otherwise
  end

  @doc """
  Fetches the basedir of a module.

  The basedir is the cwd of the Elixir/Erlang compiler when compiling the module.
  All `-file` attributes in the module is relative to this directory.
  """
  def fetch_basedir!(abst_code, module) do
    # We look for the first -file attribute to see what the source file that
    # was compiled is called. Both Erlang and Elixir places one at the top.
    filename =
      Enum.find_value(abst_code, fn
        {:attribute, _anno, :file, {filename, _line}} ->
          filename

        _ ->
          nil
      end) || raise "could not find base directory for #{inspect(module)}"

    # The first -file attribute will be either relative or absolute
    # depending on whether the compiler was called with an absolute
    # or relative path.
    if Path.type(filename) == :relative do
      # If the compiler was called with a relative path, then any other
      # relative -file attribute will be relative to the same directory.
      # We use `module_info(:compile)[:source]` to get an absolute path
      # to the source file and calculate the basedir from that
      compile_source =
        cond do
          source = module.module_info(:compile)[:source] ->
            source

          module in :erlang.pre_loaded() ->
            {:ok, {_, [compile_info: compile_info]}} =
              :beam_lib.chunks(
                Application.app_dir(:erts, "ebin/#{module}.beam")
                |> String.to_charlist(),
                [:compile_info]
              )

            compile_info[:source]

          true ->
            # This happens only when building docs for Makeup itself,
            # as some of its modules may have been loaded from the escript.
            filename
        end

      compile_source
      |> String.Chars.to_string()
      |> Path.absname()
      |> Path.split()
      |> Enum.drop(Path.split(filename) |> Enum.count() |> Kernel.*(-1))
      |> Path.join()
    else
      # If an absolute path was used, then any relative -file attribute
      # is relative to the directory of the source file
      Path.dirname(filename)
    end
  end

  def fetch_module_location!(abst_code, source_basedir, module) do
    find_ast(abst_code, source_basedir, fn
      {:attribute, anno, :module, ^module} ->
        {anno_file(anno), anno_line(anno)}

      _ ->
        nil
    end) || raise "could not find module definition for #{inspect(module)}"
  end

  def fetch_function_location!(module_data, {name, arity}) do
    find_ast(module_data.private.abst_code, module_data.source_basedir, fn
      {:function, anno, ^name, ^arity, _} -> {anno_file(anno), anno_line(anno)}
      _ -> nil
    end) || raise "could not find function definition for #{name}/#{arity}"
  end

  # Returns a map of {name, arity} => spec.
  def get_specs(abst_code, source_basedir) do
    filtermap_ast(abst_code, source_basedir, fn
      {:attribute, _anno, :spec, {name, _types}} = spec ->
        {name, spec}

      _ ->
        nil
    end)
    |> Map.new()
  end

  def fetch_type!(module_data, name, arity) do
    find_ast(module_data.private.abst_code, module_data.source_basedir, fn
      {:attribute, anno, type, {^name, _, args} = spec} = attr ->
        if type in [:nominal, :opaque, :type] and length(args) == arity do
          %{
            type: type,
            spec: spec,
            attr: attr,
            source_file: anno_file(anno),
            source_line: anno_line(anno)
          }
        end

      _ ->
        nil
    end) || raise "could not find type definition for #{name}/#{arity}"
  end

  def get_callbacks(abst_code, source_basedir) do
    filtermap_ast(abst_code, source_basedir, fn
      {:attribute, _anno, :callback, {name, _types}} = callback ->
        {name, callback}

      _ ->
        nil
    end)
    |> Map.new()
  end

  def get_optional_callbacks(module, :behaviour) do
    module.behaviour_info(:optional_callbacks)
  rescue
    FunctionClauseError -> []
  end

  def get_optional_callbacks(_module, _type), do: []

  defp find_ast(ast, source_basedir, fun) do
    filtermap_ast(ast, source_basedir, fun) |> List.first()
  end

  # The file which a form belongs to is decided by the previous :file
  # attribute in the AST. The :file can be either relative, or absolute
  # depending on how the file was included. So when traversing the AST
  # we need to keep track of the :file attributes and update the anno
  # with the correct file.
  defp filtermap_ast(ast, source_basedir, fun) do
    Enum.reduce(ast, {nil, []}, fn
      {:attribute, _anno, :file, {filename, _line}} = entry, {_file, acc} ->
        {if Path.type(filename) == :relative && source_basedir do
           Path.join(source_basedir, filename)
         else
           filename
         end,
         if entry = fun.(entry) do
           [entry | acc]
         else
           acc
         end}

      entry, {file, acc} ->
        anno =
          if file && source_basedir do
            :erl_anno.set_file(file, elem(entry, 1))
          else
            elem(entry, 1)
          end

        if entry = fun.(put_elem(entry, 1, anno)) do
          {file, [entry | acc]}
        else
          {file, acc}
        end

      _, file_acc ->
        file_acc
    end)
    |> elem(1)
    |> Enum.reverse()
  end
end