Skip to content

Commit 448064e

Browse files
authored
Add equiv handling for types and callbacks for Erlang (#1873)
1 parent 7d63b37 commit 448064e

File tree

4 files changed

+102
-34
lines changed

4 files changed

+102
-34
lines changed

Diff for: lib/ex_doc/language/erlang.ex

+48-22
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,36 @@ defmodule ExDoc.Language.Erlang do
6969
end
7070
end
7171

72+
defp equiv_data(module, file, line, metadata, prefix \\ "") do
73+
case metadata[:equiv] do
74+
nil ->
75+
nil
76+
77+
equiv when is_binary(equiv) ->
78+
## We try to parse the equiv in order to link to the target
79+
with {:ok, toks, _} <- :erl_scan.string(:unicode.characters_to_list(equiv <> ".")),
80+
{:ok, [{:call, _, {:atom, _, func}, args}]} <- :erl_parse.parse_exprs(toks) do
81+
"Equivalent to [`#{equiv}`](`#{prefix}#{func}/#{length(args)}`)."
82+
else
83+
{:ok, [{:op, _, :/, {:atom, _, _}, {:integer, _, _}}]} ->
84+
"Equivalent to `#{prefix}#{equiv}`."
85+
86+
_ ->
87+
"Equivalent to `#{equiv}`."
88+
end
89+
|> ExDoc.DocAST.parse!("text/markdown")
90+
91+
equiv ->
92+
ExDoc.Utils.warn("invalid equiv #{inspect(equiv)}",
93+
file: file,
94+
line: line,
95+
module: module
96+
)
97+
98+
nil
99+
end
100+
end
101+
72102
defp function_data(name, arity, _doc_content, module_data, metadata) do
73103
specs =
74104
case Map.fetch(module_data.private.specs, {name, arity}) do
@@ -88,26 +118,7 @@ defmodule ExDoc.Language.Erlang do
88118
{file, line} = Source.get_function_location(module_data, {name, arity})
89119

90120
%{
91-
doc_fallback: fn ->
92-
case metadata[:equiv] do
93-
nil ->
94-
nil
95-
96-
equiv when is_binary(equiv) ->
97-
## We try to parse the equiv in order to link to the target
98-
with {:ok, toks, _} <- :erl_scan.string(:unicode.characters_to_list(equiv <> ".")),
99-
{:ok, [{:call, _, {:atom, _, func}, args}]} <- :erl_parse.parse_exprs(toks) do
100-
"Equivalent to [`#{equiv}`](`#{func}/#{length(args)}`)"
101-
else
102-
_ -> "Equivalent to `#{equiv}`"
103-
end
104-
|> ExDoc.DocAST.parse!("text/markdown")
105-
106-
equiv ->
107-
IO.warn("invalid equiv: #{inspect(equiv)}", [])
108-
nil
109-
end
110-
end,
121+
doc_fallback: fn -> equiv_data(module_data.module, file, line, metadata) end,
111122
extra_annotations: [],
112123
source_line: line,
113124
source_file: file,
@@ -117,7 +128,7 @@ defmodule ExDoc.Language.Erlang do
117128

118129
@impl true
119130
def callback_data(entry, module_data) do
120-
{{_kind, name, arity}, anno, signature, _doc, _metadata} = entry
131+
{{_kind, name, arity}, anno, signature, _doc, metadata} = entry
121132

122133
extra_annotations =
123134
if {name, arity} in module_data.private.optional_callbacks, do: ["optional"], else: []
@@ -132,6 +143,15 @@ defmodule ExDoc.Language.Erlang do
132143
end
133144

134145
%{
146+
doc_fallback: fn ->
147+
equiv_data(
148+
module_data.module,
149+
Source.anno_file(anno),
150+
Source.anno_line(anno),
151+
metadata,
152+
"c:"
153+
)
154+
end,
135155
source_line: Source.anno_line(anno),
136156
source_file: Source.anno_file(anno),
137157
signature: signature,
@@ -142,11 +162,14 @@ defmodule ExDoc.Language.Erlang do
142162

143163
@impl true
144164
def type_data(entry, module_data) do
145-
{{kind, name, arity}, anno, signature, _doc, _metadata} = entry
165+
{{kind, name, arity}, anno, signature, _doc, metadata} = entry
146166

147167
case Source.get_type_from_module_data(module_data, name, arity) do
148168
%{} = map ->
149169
%{
170+
doc_fallback: fn ->
171+
equiv_data(module_data.module, map.source_file, map.source_line, metadata, "t:")
172+
end,
150173
type: map.type,
151174
source_line: map.source_line,
152175
source_file: map.source_file,
@@ -157,6 +180,9 @@ defmodule ExDoc.Language.Erlang do
157180

158181
nil ->
159182
%{
183+
doc_fallback: fn ->
184+
equiv_data(module_data.module, nil, Source.anno_line(anno), metadata, "t:")
185+
end,
160186
type: kind,
161187
source_line: Source.anno_line(anno),
162188
spec: nil,

Diff for: lib/ex_doc/retriever.ex

+10-2
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,9 @@ defmodule ExDoc.Retriever do
341341
annotations_for_docs.(metadata) ++
342342
callback_data.extra_annotations ++ annotations_from_metadata(metadata, module_metadata)
343343

344-
doc_ast = doc_ast(content_type, source_doc, file: doc_file, line: doc_line + 1)
344+
doc_ast =
345+
doc_ast(content_type, source_doc, file: doc_file, line: doc_line + 1) ||
346+
doc_fallback(callback_data)
345347

346348
group =
347349
GroupMatcher.match_function(
@@ -401,7 +403,9 @@ defmodule ExDoc.Retriever do
401403
annotations_from_metadata(metadata, module_metadata) ++
402404
type_data.extra_annotations
403405

404-
doc_ast = doc_ast(content_type, source_doc, file: doc_file, line: doc_line + 1)
406+
doc_ast =
407+
doc_ast(content_type, source_doc, file: doc_file, line: doc_line + 1) ||
408+
doc_fallback(type_data)
405409

406410
group =
407411
GroupMatcher.match_function(
@@ -429,6 +433,10 @@ defmodule ExDoc.Retriever do
429433

430434
## General helpers
431435

436+
defp doc_fallback(data) do
437+
data[:doc_fallback] && data.doc_fallback.()
438+
end
439+
432440
defp nil_or_name(name, arity) do
433441
if name == nil do
434442
"nil/#{arity}"

Diff for: lib/mix/tasks/docs.ex

+3-1
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,9 @@ defmodule Mix.Tasks.Docs do
245245
Functions and callbacks inside a module can also be organized in groups.
246246
This is done via the `:groups_for_docs` configuration which is a
247247
keyword list of group titles and filtering functions that receive the
248-
documentation metadata of functions as argument.
248+
documentation metadata of functions as argument. The metadata received will also
249+
contain `:module`, `:name`, `:arity` and `:kind` to help identify which entity is
250+
currently being processed.
249251
250252
For example, imagine that you have an API client library with a large surface
251253
area for all the API endpoints you need to support. It would be helpful to

Diff for: test/ex_doc/retriever/erlang_test.exs

+41-9
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,17 @@ defmodule ExDoc.Retriever.ErlangTest do
3333
erlc(c, :mod, ~S"""
3434
-module(mod).
3535
-moduledoc("mod docs.").
36-
-export([function1/0, function2/0]).
36+
-export([function1/0, function2/1, equiv_function2/0]).
3737
3838
-doc("function1/0 docs.").
3939
-spec function1() -> atom().
4040
function1() -> ok.
4141
42-
-doc("function2/0 docs.").
43-
function2() -> ok.
42+
-doc("function2/1 docs.").
43+
function2(Opts) -> Opts.
44+
45+
-doc #{ equiv => function2([{test, args}]) }.
46+
equiv_function2() -> function2([{test, args}]).
4447
""")
4548

4649
{[mod], []} = Retriever.docs_from_modules([:mod], %ExDoc.Config{})
@@ -49,7 +52,7 @@ defmodule ExDoc.Retriever.ErlangTest do
4952
deprecated: nil,
5053
moduledoc_line: 2,
5154
moduledoc_file: moduledoc_file,
52-
docs: [function1, function2],
55+
docs: [equiv_function2, function1, function2],
5356
docs_groups: [:Types, :Callbacks, :Functions],
5457
group: nil,
5558
id: "mod",
@@ -93,11 +96,18 @@ defmodule ExDoc.Retriever.ErlangTest do
9396
"function1() -> <a href=\"https://www.erlang.org/doc/man/erlang.html#type-atom\">atom</a>()."
9497

9598
%ExDoc.FunctionNode{
96-
id: "function2/0"
99+
id: "function2/1"
97100
} = function2
98101

99-
assert DocAST.to_string(function2.doc) =~ "function2/0 docs."
102+
assert DocAST.to_string(function2.doc) =~ "function2/1 docs."
100103
assert function2.specs == []
104+
105+
%ExDoc.FunctionNode{
106+
id: "equiv_function2/0"
107+
} = equiv_function2
108+
109+
assert DocAST.to_string(equiv_function2.doc) =~
110+
~r'Equivalent to <a href="`function2/1`"><code[^>]+>function2\(\[\{test, args\}\]\).*\.'
101111
end
102112

103113
test "module included files", c do
@@ -222,6 +232,9 @@ defmodule ExDoc.Retriever.ErlangTest do
222232
-doc("callback1/0 docs.").
223233
-callback callback1() -> atom().
224234
235+
-doc #{ equiv => callback1() }.
236+
-callback equiv_callback1() -> atom().
237+
225238
-doc("optional_callback1/0 docs.").
226239
-callback optional_callback1() -> atom().
227240
@@ -230,7 +243,7 @@ defmodule ExDoc.Retriever.ErlangTest do
230243

231244
config = %ExDoc.Config{source_url_pattern: "%{path}:%{line}"}
232245
{[mod], []} = Retriever.docs_from_modules([:mod], config)
233-
[callback1, optional_callback1] = mod.docs
246+
[callback1, equiv_callback1, optional_callback1] = mod.docs
234247

235248
assert callback1.id == "c:callback1/0"
236249
assert callback1.type == :callback
@@ -242,6 +255,16 @@ defmodule ExDoc.Retriever.ErlangTest do
242255
assert Erlang.autolink_spec(hd(callback1.specs), current_kfa: {:callback, :callback1, 0}) ==
243256
"callback1() -> <a href=\"https://www.erlang.org/doc/man/erlang.html#type-atom\">atom</a>()."
244257

258+
assert equiv_callback1.id == "c:equiv_callback1/0"
259+
assert equiv_callback1.type == :callback
260+
assert equiv_callback1.annotations == []
261+
assert equiv_callback1.group == :Callbacks
262+
263+
assert DocAST.to_string(equiv_callback1.doc) =~
264+
~r'Equivalent to <a href="`c:callback1/0`"><code[^>]+>callback1().*\.'
265+
266+
assert Path.basename(equiv_callback1.source_url) == "mod.erl:7"
267+
245268
assert optional_callback1.id == "c:optional_callback1/0"
246269
assert optional_callback1.type == :callback
247270
assert optional_callback1.group == :Callbacks
@@ -251,18 +274,21 @@ defmodule ExDoc.Retriever.ErlangTest do
251274
test "types", c do
252275
erlc(c, :mod, ~S"""
253276
-module(mod).
254-
-export_type([type1/0, opaque1/0]).
277+
-export_type([type1/0, equiv_type1/0, opaque1/0]).
255278
256279
-doc("type1/0 docs.").
257280
-type type1() :: atom().
258281
282+
-doc #{ equiv => type1/1 }.
283+
-type equiv_type1() :: atom().
284+
259285
-doc("opaque1/0 docs.").
260286
-opaque opaque1() :: atom().
261287
""")
262288

263289
config = %ExDoc.Config{source_url_pattern: "%{path}:%{line}"}
264290
{[mod], []} = Retriever.docs_from_modules([:mod], config)
265-
[opaque1, type1] = mod.typespecs
291+
[equiv_type1, opaque1, type1] = mod.typespecs
266292

267293
assert opaque1.id == "t:opaque1/0"
268294
assert opaque1.type == :opaque
@@ -281,6 +307,12 @@ defmodule ExDoc.Retriever.ErlangTest do
281307

282308
assert type1.spec |> Erlang.autolink_spec(current_kfa: {:type, :type1, 0}) ==
283309
"type1() :: <a href=\"https://www.erlang.org/doc/man/erlang.html#type-atom\">atom</a>()."
310+
311+
assert equiv_type1.id == "t:equiv_type1/0"
312+
assert equiv_type1.type == :type
313+
assert equiv_type1.group == :Types
314+
assert equiv_type1.signature == "equiv_type1()"
315+
assert equiv_type1.doc |> DocAST.to_string() =~ ~r'Equivalent to .*t:type1/1.*\.'
284316
end
285317

286318
test "records", c do

0 commit comments

Comments
 (0)