Skip to content

Commit ca82388

Browse files
lackacjosevalim
authored andcommitted
Show documentation metadata in IEx (only since and deprecated for now) (#7886)
1 parent b33dd12 commit ca82388

File tree

4 files changed

+151
-22
lines changed

4 files changed

+151
-22
lines changed

lib/elixir/lib/io/ansi/docs.ex

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ defmodule IO.ANSI.Docs do
77
@doc """
88
The default options used by this module.
99
10-
The supported values are:
10+
The supported keys are:
1111
1212
* `:enabled` - toggles coloring on and off (true)
1313
* `:doc_bold` - bold text (bright)
1414
* `:doc_code` - code blocks (cyan)
1515
* `:doc_headings` - h1, h2, h3, h4, h5, h6 headings (yellow)
16+
* `:doc_metadata` - documentation metadata keys (yellow)
1617
* `:doc_inline_code` - inline code (cyan)
1718
* `:doc_table_heading` - the style for table headings
1819
* `:doc_title` - top level heading (reverse, yellow)
@@ -29,6 +30,7 @@ defmodule IO.ANSI.Docs do
2930
doc_bold: [:bright],
3031
doc_code: [:cyan],
3132
doc_headings: [:yellow],
33+
doc_metadata: [:yellow],
3234
doc_inline_code: [:cyan],
3335
doc_table_heading: [:reverse],
3436
doc_title: [:reverse, :yellow],
@@ -53,6 +55,39 @@ defmodule IO.ANSI.Docs do
5355
newline_after_block()
5456
end
5557

58+
@doc """
59+
Prints documentation metadata (only `since` and `deprecated` for now).
60+
61+
See `default_options/0` for docs on the supported options.
62+
"""
63+
@spec print_metadata(map, keyword) :: :ok
64+
def print_metadata(metadata, options \\ []) when is_map(metadata) do
65+
options = Keyword.merge(default_options(), options)
66+
print_each_metadata(metadata, options) && IO.write("\n")
67+
end
68+
69+
@metadata_filter [:deprecated, :since]
70+
71+
defp print_each_metadata(metadata, options) do
72+
Enum.reduce(metadata, false, fn
73+
{key, value}, _printed when is_binary(value) and key in @metadata_filter ->
74+
label = metadata_label(key, options)
75+
indent = String.duplicate(" ", length_without_escape(label, 0) + 1)
76+
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true)
77+
78+
_metadata, printed ->
79+
printed
80+
end)
81+
end
82+
83+
defp metadata_label(key, options) do
84+
if options[:enabled] do
85+
"#{color(:doc_metadata, options)}#{key}:#{IO.ANSI.reset()}"
86+
else
87+
"#{key}:"
88+
end
89+
end
90+
5691
@doc """
5792
Prints the documentation body.
5893

lib/elixir/test/elixir/io/ansi/docs_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ defmodule IO.ANSI.DocsTest do
88
capture_io(fn -> IO.ANSI.Docs.print_heading(str, []) end) |> String.trim_trailing()
99
end
1010

11+
def format_metadata(map) do
12+
capture_io(fn -> IO.ANSI.Docs.print_metadata(map, []) end)
13+
end
14+
1115
def format(str) do
1216
capture_io(fn -> IO.ANSI.Docs.print(str, []) end) |> String.trim_trailing()
1317
end
@@ -19,6 +23,12 @@ defmodule IO.ANSI.DocsTest do
1923
assert String.contains?(result, " wibble ")
2024
end
2125

26+
test "metadata is formatted" do
27+
result = format_metadata(%{since: "1.2.3", deprecated: "Use that other one", author: "Alice"})
28+
assert result == "\e[33mdeprecated:\e[0m Use that other one\n\e[33msince:\e[0m 1.2.3\n\n"
29+
assert format_metadata(%{author: "Alice"}) == ""
30+
end
31+
2232
test "first level heading is converted" do
2333
result = format("# wibble\n\ntext\n")
2434
assert result == "\e[33m# wibble\e[0m\n\e[0m\ntext\n\e[0m"

lib/iex/lib/iex/introspection.ex

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,8 @@ defmodule IEx.Introspection do
250250
case Code.ensure_loaded(module) do
251251
{:module, _} ->
252252
case Code.fetch_docs(module) do
253-
{:docs_v1, _, _, _, %{} = doc, _, _} ->
254-
print_doc(inspect(module), [], doc)
253+
{:docs_v1, _, _, _, %{} = doc, metadata, _} ->
254+
print_doc(inspect(module), [], doc, metadata)
255255

256256
{:docs_v1, _, _, _, _, _, _} ->
257257
docs_not_found(inspect(module))
@@ -376,7 +376,7 @@ defmodule IEx.Introspection do
376376

377377
is_nil(docs) and spec != [] ->
378378
message = %{"en" => "Module was compiled without docs. Showing only specs."}
379-
print_doc("#{inspect(mod)}.#{fun}/#{arity}", spec, message)
379+
print_doc("#{inspect(mod)}.#{fun}/#{arity}", spec, message, %{})
380380
:ok
381381

382382
is_nil(docs) ->
@@ -449,7 +449,7 @@ defmodule IEx.Introspection do
449449
defp has_content?({{_, name, _}, _, _, :none, _}), do: hd(Atom.to_charlist(name)) != ?_
450450
defp has_content?({_, _, _, _, _}), do: true
451451

452-
defp print_fun(mod, {{kind, fun, arity}, _line, signature, doc, _meta}, spec) do
452+
defp print_fun(mod, {{kind, fun, arity}, _line, signature, doc, metadata}, spec) do
453453
if callback_module = doc == :none and callback_module(mod, fun, arity) do
454454
filter = &match?({_, ^fun, ^arity}, elem(&1, 0))
455455

@@ -458,7 +458,7 @@ defmodule IEx.Introspection do
458458
_ -> nil
459459
end
460460
else
461-
print_doc("#{kind_to_def(kind)} #{Enum.join(signature, " ")}", spec, doc)
461+
print_doc("#{kind_to_def(kind)} #{Enum.join(signature, " ")}", spec, doc, metadata)
462462
end
463463
end
464464

@@ -513,7 +513,7 @@ defmodule IEx.Introspection do
513513
{:ok, docs} ->
514514
docs
515515
|> add_optional_callback_docs(mod)
516-
|> Enum.each(fn {definition, _} -> IO.puts(definition) end)
516+
|> Enum.each(fn {definition, _, _} -> IO.puts(definition) end)
517517
end
518518

519519
dont_display_result()
@@ -565,12 +565,12 @@ defmodule IEx.Introspection do
565565
docs
566566
|> Enum.filter(filter)
567567
|> Enum.map(fn
568-
{{:macrocallback, fun, arity}, _, _, doc, _} ->
568+
{{:macrocallback, fun, arity}, _, _, doc, metadata} ->
569569
macro = {:"MACRO-#{fun}", arity + 1}
570-
{format_callback(:macrocallback, fun, macro, callbacks), doc}
570+
{format_callback(:macrocallback, fun, macro, callbacks), doc, metadata}
571571

572-
{{kind, fun, arity}, _, _, doc, _} ->
573-
{format_callback(kind, fun, {fun, arity}, callbacks), doc}
572+
{{kind, fun, arity}, _, _, doc, metadata} ->
573+
{format_callback(kind, fun, {fun, arity}, callbacks), doc, metadata}
574574
end)
575575

576576
{:ok, docs}
@@ -588,7 +588,7 @@ defmodule IEx.Introspection do
588588
if optional_callbacks == [] do
589589
docs
590590
else
591-
docs ++ [{format_optional_callbacks(optional_callbacks), ""}]
591+
docs ++ [{format_optional_callbacks(optional_callbacks), "", %{}}]
592592
end
593593
end
594594

@@ -637,8 +637,8 @@ defmodule IEx.Introspection do
637637
{:ok, types} ->
638638
printed =
639639
for {_, {^type, _, args}} = typespec <- types do
640-
doc = {format_type(typespec), type_doc(module, type, length(args))}
641-
print_typespec(doc)
640+
type_doc(module, type, length(args), typespec)
641+
|> print_typespec()
642642
end
643643

644644
if printed == [] do
@@ -657,8 +657,8 @@ defmodule IEx.Introspection do
657657
{:ok, types} ->
658658
printed =
659659
for {_, {^type, _, args}} = typespec <- types, length(args) == arity do
660-
doc = {format_type(typespec), type_doc(module, type, arity)}
661-
print_typespec(doc)
660+
type_doc(module, type, arity, typespec)
661+
|> print_typespec()
662662
end
663663

664664
if printed == [] do
@@ -674,12 +674,12 @@ defmodule IEx.Introspection do
674674
dont_display_result()
675675
end
676676

677-
defp type_doc(module, type, arity) do
677+
defp type_doc(module, type, arity, typespec) do
678678
if docs = get_docs(module, [:type]) do
679-
{_, _, _, content, _} = Enum.find(docs, &match?({:type, ^type, ^arity}, elem(&1, 0)))
680-
content
679+
{_, _, _, content, metadata} = Enum.find(docs, &match?({:type, ^type, ^arity}, elem(&1, 0)))
680+
{format_type(typespec), content, metadata}
681681
else
682-
:none
682+
{format_type(typespec), :none, %{}}
683683
end
684684
end
685685

@@ -717,27 +717,31 @@ defmodule IEx.Introspection do
717717
IEx.color(:doc_inline_code, left) <> " " <> right
718718
end
719719

720-
defp print_doc(heading, types, doc) do
720+
defp print_doc(heading, types, doc, metadata) do
721721
doc = translate_doc(doc) || ""
722722

723723
if opts = IEx.Config.ansi_docs() do
724724
IO.ANSI.Docs.print_heading(heading, opts)
725725
IO.write(types)
726+
IO.ANSI.Docs.print_metadata(metadata, opts)
726727
IO.ANSI.Docs.print(doc, opts)
727728
else
728729
IO.puts("* #{heading}\n")
729730
IO.write(types)
731+
IO.ANSI.Docs.print_metadata(metadata, enabled: false)
730732
IO.puts(doc)
731733
end
732734
end
733735

734-
defp print_typespec({types, doc}) do
736+
defp print_typespec({types, doc, metadata}) do
735737
IO.puts(types)
736738
doc = translate_doc(doc)
737739

738740
if opts = IEx.Config.ansi_docs() do
741+
IO.ANSI.Docs.print_metadata(metadata, opts)
739742
doc && IO.ANSI.Docs.print(doc, opts)
740743
else
744+
IO.ANSI.Docs.print_metadata(metadata, enabled: false)
741745
doc && IO.puts(doc)
742746
end
743747
end

lib/iex/test/iex/helpers_test.exs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,38 @@ defmodule IEx.HelpersTest do
403403
"No documentation for Kernel.__info__ was found\n"
404404
end
405405

406+
test "prints documentation metadata" do
407+
content = """
408+
defmodule Sample do
409+
@moduledoc "Sample module"
410+
@moduledoc deprecated: "Use OtherSample", since: "1.2.3", authors: ["Alice", "Bob"]
411+
@doc "With metadata"
412+
@doc since: "1.2.3", author: "Alice"
413+
@deprecated "Use OtherSample.with_metadata/0"
414+
def with_metadata(), do: 0
415+
@doc "Without metadata"
416+
def without_metadata(), do: 1
417+
end
418+
"""
419+
420+
filename = "sample.ex"
421+
422+
with_file(filename, content, fn ->
423+
assert c(filename, ".") == [Sample]
424+
425+
assert capture_io(fn -> h(Sample) end) ==
426+
"* Sample\n\ndeprecated: Use OtherSample\nsince: 1.2.3\n\nSample module\n"
427+
428+
assert capture_io(fn -> h(Sample.with_metadata()) end) ==
429+
"* def with_metadata()\n\ndeprecated: Use OtherSample.with_metadata/0\nsince: 1.2.3\n\nWith metadata\n"
430+
431+
assert capture_io(fn -> h(Sample.without_metadata()) end) ==
432+
"* def without_metadata()\n\nWithout metadata\n"
433+
end)
434+
after
435+
cleanup_modules([Sample])
436+
end
437+
406438
test "considers underscored functions without docs by default" do
407439
content = """
408440
defmodule Sample do
@@ -593,6 +625,27 @@ defmodule IEx.HelpersTest do
593625
"@callback message(t()) :: String.t()\n\n"
594626
end
595627

628+
test "prints callback documentation metadata" do
629+
filename = "callback_with_metadata.ex"
630+
631+
content = """
632+
defmodule CallbackWithMetadata do
633+
@doc "callback"
634+
@doc since: "1.2.3", deprecated: "Use handle_test/1", purpose: :test
635+
@callback test(:foo) :: integer
636+
end
637+
"""
638+
639+
with_file(filename, content, fn ->
640+
assert c(filename, ".") == [CallbackWithMetadata]
641+
642+
assert capture_io(fn -> b(CallbackWithMetadata.test()) end) ==
643+
"@callback test(:foo) :: integer()\n\ndeprecated: Use handle_test/1\nsince: 1.2.3\n\ncallback\n"
644+
end)
645+
after
646+
cleanup_modules([CallbackWithMetadata])
647+
end
648+
596649
test "prints optional callback" do
597650
filename = "optional_callbacks.ex"
598651

@@ -666,6 +719,33 @@ defmodule IEx.HelpersTest do
666719
after
667720
cleanup_modules([TypeSample])
668721
end
722+
723+
test "prints type documentation metadata" do
724+
content = """
725+
defmodule TypeSample do
726+
@typedoc "An id with description."
727+
@typedoc since: "1.2.3", deprecated: "Use t/0", purpose: :test
728+
@type id_with_desc :: {number, String.t}
729+
end
730+
"""
731+
732+
filename = "typesample.ex"
733+
734+
with_file(filename, content, fn ->
735+
assert c(filename, ".") == [TypeSample]
736+
737+
assert capture_io(fn -> t(TypeSample.id_with_desc()) end) == """
738+
@type id_with_desc() :: {number(), String.t()}
739+
740+
deprecated: Use t/0
741+
since: 1.2.3
742+
743+
An id with description.
744+
"""
745+
end)
746+
after
747+
cleanup_modules([TypeSample])
748+
end
669749
end
670750

671751
describe "v" do

0 commit comments

Comments
 (0)