File 5401-compiler-implementation-of-EEP-59.patch of Package erlang
From f2f48e329827b1750a39cced3cd4a27183e944af Mon Sep 17 00:00:00 2001
From: Kiko Fernandez-Reyes <kiko@erlang.org>
Date: Fri, 15 Sep 2023 16:55:47 +0200
Subject: [PATCH] compiler: implementation of EEP-59
Implementation of EEP-59 - Documentation Attributes
- Documentation attributes are added to the binary beam file, following
format of [EEP-48](https://www.erlang.org/eeps/eep-0048), via
`+beam_docs` compiler flag
- Warnings related to documentation attributes are dealt with in the
`beam_docs.erl` instead of adding them to `erl_lint.erl`
*Example 1*
```erlang
-module(warn_missing_doc).
-export([test/0, test/1, test/2]).
-export_type([test/0, test/1]).
-type test() :: ok.
-type test(N) :: N.
-callback test() -> ok.
-include("warn_missing_doc.hrl").
test() -> ok.
test(N) -> N.
```
Using the compiler flag `warn_missing_doc` will raise a warning when
doc. attributes are missing in exported functions, types, and callbacks.
*Example 2*
```erlang
-module(doc_with_file).
-export([main/1]).
-moduledoc {file, "README"}.
-doc {file, "FUN"}.
-spec main(Var) -> foo(Var).
main(X) ->
X.
```
`moduledoc`s and `doc`s may refer to external files to be embedded.
*Example 3 - Warnings and Types*
```erlang
-export([uses_public/0]).
-export_type([public/0]).
-doc false.
-type hidden_type() :: integer().
-type intermediate() :: hidden_type().
-type public() :: intermediate().
-spec uses_public() -> public().
uses_public() ->
ok.
```
Compiler warns about exported functions whose specs refer to hidden
types. In the example above, the `hidden_type()` is set as `hidden`
either via `-doc false` or `-doc hidden` and `public() :> intermediate()
:> hidden_type()`. When documentation attributes mark a type as hidden,
they won't be part of the documentation. Thus, the warning that the
`hidden_type()` is not part of the documentation, yet used in an
exported function.
---
bootstrap/lib/compiler/ebin/beam_doc.beam | Bin 0 -> 17580 bytes
bootstrap/lib/compiler/ebin/compile.beam | Bin 39052 -> 39736 bytes
.../lib/kernel/ebin/erl_erts_errors.beam | Bin 23652 -> 23660 bytes
bootstrap/lib/kernel/ebin/group.beam | Bin 16816 -> 17964 bytes
bootstrap/lib/kernel/ebin/prim_tty.beam | Bin 22544 -> 22564 bytes
bootstrap/lib/stdlib/ebin/argparse.beam | Bin 22908 -> 22908 bytes
bootstrap/lib/stdlib/ebin/beam_lib.beam | Bin 19240 -> 19424 bytes
bootstrap/lib/stdlib/ebin/c.beam | Bin 18820 -> 19204 bytes
bootstrap/lib/stdlib/ebin/edlin.beam | Bin 16156 -> 16248 bytes
bootstrap/lib/stdlib/ebin/edlin_context.beam | Bin 11864 -> 11884 bytes
bootstrap/lib/stdlib/ebin/edlin_expand.beam | Bin 26256 -> 26296 bytes
bootstrap/lib/stdlib/ebin/edlin_key.beam | Bin 3940 -> 3972 bytes
bootstrap/lib/stdlib/ebin/epp.beam | Bin 32072 -> 33824 bytes
bootstrap/lib/stdlib/ebin/erl_lint.beam | Bin 91388 -> 92028 bytes
bootstrap/lib/stdlib/ebin/erl_parse.beam | Bin 179332 -> 179996 bytes
erts/preloaded/src/Makefile | 2 +-
lib/compiler/doc/src/compile.xml | 29 +
lib/compiler/src/Makefile | 1 +
lib/compiler/src/beam_doc.erl | 1073 +++++++++++++++++
lib/compiler/src/compile.erl | 81 +-
lib/compiler/src/compiler.app.src | 3 +-
lib/compiler/test/Makefile | 1 +
lib/compiler/test/beam_doc_SUITE.erl | 603 +++++++++
lib/compiler/test/beam_doc_SUITE_data/FUN | 3 +
lib/compiler/test/beam_doc_SUITE_data/README | 3 +
lib/compiler/test/beam_doc_SUITE_data/TYPES | 3 +
.../all_string_formats.erl | 20 +
.../test/beam_doc_SUITE_data/callback.erl | 69 ++
.../test/beam_doc_SUITE_data/deprecated.erl | 46 +
.../beam_doc_SUITE_data/doc_with_file.erl | 25 +
.../doc_with_file_error.erl | 24 +
.../test/beam_doc_SUITE_data/docformat.erl | 18 +
.../docmodule_with_doc_attributes.erl | 36 +
.../test/beam_doc_SUITE_data/equiv.erl | 10 +
.../test/beam_doc_SUITE_data/export_all.erl | 31 +
.../test/beam_doc_SUITE_data/folder/FILE | 3 +
.../folder/doc_with_file.hrl | 1 +
.../beam_doc_SUITE_data/hide_moduledoc.erl | 15 +
.../beam_doc_SUITE_data/hide_moduledoc2.erl | 25 +
.../beam_doc_SUITE_data/private_types.erl | 65 +
.../singleton_docformat.erl | 21 +
.../beam_doc_SUITE_data/singleton_meta.erl | 17 +
.../test/beam_doc_SUITE_data/singletondoc.erl | 19 +
.../singletonmoduledoc.erl | 7 +
.../test/beam_doc_SUITE_data/skip_doc.erl | 19 +
.../test/beam_doc_SUITE_data/slogan.erl | 73 ++
.../test/beam_doc_SUITE_data/spec.erl | 24 +
.../beam_doc_SUITE_data/spec_switch_order.erl | 54 +
.../beam_doc_SUITE_data/types_and_opaques.erl | 147 +++
.../beam_doc_SUITE_data/types_and_opaques.hrl | 2 +
.../beam_doc_SUITE_data/user_defined_type.erl | 3 +
.../beam_doc_SUITE_data/user_defined_type.hrl | 2 +
.../beam_doc_SUITE_data/warn_missing_doc.erl | 14 +
.../beam_doc_SUITE_data/warn_missing_doc.hrl | 2 +
lib/compiler/test/compile_SUITE.erl | 27 +-
.../test/compile_SUITE_data/simple-basic1.mk | 2 +-
.../test/compile_SUITE_data/simple-basic2.mk | 2 +-
.../test/compile_SUITE_data/simple-missing.mk | 2 +-
.../test/compile_SUITE_data/simple-phony.mk | 4 +-
.../test/compile_SUITE_data/simple-target1.mk | 2 +-
.../test/compile_SUITE_data/simple-target2.mk | 2 +-
.../test/compile_SUITE_data/simple.erl | 4 +
.../test/compile_SUITE_data/unicode-0.md | 1 +
.../priv/bin/validate_links.escript | 13 +-
lib/stdlib/doc/src/beam_lib.xml | 12 +-
lib/stdlib/src/beam_lib.erl | 24 +-
lib/stdlib/src/epp.erl | 78 +-
lib/stdlib/src/erl_lint.erl | 87 +-
lib/stdlib/src/erl_parse.yrl | 41 +
lib/stdlib/src/stdlib.app.src | 2 +-
lib/stdlib/test/beam_lib_SUITE.erl | 2 +-
lib/stdlib/test/epp_SUITE.erl | 53 +-
lib/stdlib/test/erl_lint_SUITE.erl | 104 ++
make/otp.mk.in | 2 +-
system/doc/reference_manual/Makefile | 3 +
system/doc/reference_manual/documentation.md | 414 +++++++
system/doc/reference_manual/modules.xml | 66 +-
system/doc/reference_manual/part.xml | 1 +
system/doc/reference_manual/xmlfiles.mk | 1 +
79 files changed, 3460 insertions(+), 83 deletions(-)
create mode 100644 bootstrap/lib/compiler/ebin/beam_doc.beam
create mode 100644 lib/compiler/src/beam_doc.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/FUN
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/README
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/TYPES
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/all_string_formats.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/callback.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/deprecated.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/doc_with_file.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/doc_with_file_error.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/docformat.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/docmodule_with_doc_attributes.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/equiv.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/export_all.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/folder/FILE
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/folder/doc_with_file.hrl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc2.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/private_types.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/singleton_docformat.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/singleton_meta.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/singletondoc.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/singletonmoduledoc.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/skip_doc.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/slogan.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/spec.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/spec_switch_order.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.hrl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/user_defined_type.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/user_defined_type.hrl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.erl
create mode 100644 lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.hrl
create mode 100644 lib/compiler/test/compile_SUITE_data/unicode-0.md
create mode 100644 system/doc/reference_manual/documentation.md
diff --git a/erts/preloaded/src/Makefile b/erts/preloaded/src/Makefile
index 1994aa1302..4d742b06dc 100644
--- a/erts/preloaded/src/Makefile
+++ b/erts/preloaded/src/Makefile
@@ -84,7 +84,7 @@ KERNEL_SRC=$(ERL_TOP)/lib/kernel/src
KERNEL_INCLUDE=$(ERL_TOP)/lib/kernel/include
STDLIB_INCLUDE=$(ERL_TOP)/lib/stdlib/include
-ERL_COMPILE_FLAGS += +debug_info -I$(KERNEL_SRC) -I$(KERNEL_INCLUDE)
+ERL_COMPILE_FLAGS += +debug_info +no_docs -I$(KERNEL_SRC) -I$(KERNEL_INCLUDE)
ifeq ($(ERL_DETERMINISTIC),yes)
ERL_COMPILE_FLAGS += +deterministic
diff --git a/lib/compiler/doc/src/compile.xml b/lib/compiler/doc/src/compile.xml
index d7c5e34dd2..b601baf07d 100644
--- a/lib/compiler/doc/src/compile.xml
+++ b/lib/compiler/doc/src/compile.xml
@@ -146,6 +146,16 @@
exception at runtime).</p>
</item>
+ <tag><c>no_docs</c><marker id="beam_docs"/></tag>
+ <item>
+ <p>The compiler by default extracts <seeguide marker="system/reference_manual:documentation">documentation</seeguide> from
+ <seeguide marker="system/reference_manual:modules#documentation-attributes"><c>-doc</c> attributes</seeguide>
+ and places them in the <seetype marker="stdlib:beam_lib#chunkid"><c>Docs</c> chunk</seetype> according to <seeguide marker="kernel:eep48_chapter">EEP-48</seeguide>.
+ </p>
+ <p>This option switches off the placement of <seeguide marker="system/reference_manual:modules#documentation-attributes"><c>-doc</c> attributes</seeguide>
+ in the <seetype marker="stdlib:beam_lib#chunkid"><c>Docs</c> chunk</seetype></p>.
+ </item>
+
<tag><c>binary</c></tag>
<item>
<p>The compiler returns the object code in a
@@ -875,6 +885,25 @@ module.beam: module.erl \
warnings.</p>
</item>
+ <tag><c>warn_missing_doc</c><marker id="warn_missing_doc"/></tag>
+ <item>
+ <p>By default, warnings are not emitted when <c>-doc</c>
+ attribute for an exported function is not given. Use this
+ option to turn on this kind of warning.</p>
+ </item>
+
+ <tag><c>nowarn_hidden_doc</c> | <c>{nowarn_hidden_doc,NAs}</c>
+ <marker id="nowarn_hidden_doc"/></tag>
+ <item>
+ <p>By default, warnings are emitted when <c>-doc false</c>
+ attribute is set on a <seeguide marker="system/reference_manual:documentation#What-is-visible-versus-hidden">callback or referenced type</seeguide>. You can set
+ <c>nowarn_hidden_doc</c> to suppress all those warnings,
+ or <c>{nowarn_hidden_doc, NAs}</c> to suppress specific
+ callbacks or types. <c>NAs</c> is a tuple <c>{Name, Arity}</c>
+ or a list of such tuples.
+ </p>
+ </item>
+
<tag><c>warn_missing_spec</c></tag>
<item>
<p>By default, warnings are not emitted when a specification
diff --git a/lib/compiler/src/Makefile b/lib/compiler/src/Makefile
index a33d14f2d5..297f6b1253 100644
--- a/lib/compiler/src/Makefile
+++ b/lib/compiler/src/Makefile
@@ -56,6 +56,7 @@ MODULES = \
beam_dict \
beam_digraph \
beam_disasm \
+ beam_doc \
beam_flatten \
beam_jump \
beam_listing \
diff --git a/lib/compiler/src/beam_doc.erl b/lib/compiler/src/beam_doc.erl
new file mode 100644
index 0000000000..cf2726bbe5
--- /dev/null
+++ b/lib/compiler/src/beam_doc.erl
@@ -0,0 +1,1073 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2023-2028. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%% Purpose : Generate documentation as per EEP-48
+%%
+%% Pass to generate EEP-48 format for beam files.
+%%
+%% Example:
+%%
+%% 1> compile:file(test).
+%%
+
+-module(beam_doc).
+
+-feature(maybe_expr, enable).
+
+-export([main/4, format_error/1]).
+
+-import(lists, [foldl/3, all/2, map/2, filter/2, reverse/1, join/2, filtermap/2,
+ uniq/2, member/2, flatten/1]).
+
+-include_lib("kernel/include/eep48.hrl").
+
+-moduledoc false.
+
+-define(DEFAULT_MODULE_DOC_LOC, 1).
+-define(DEFAULT_FORMAT, <<"text/markdown">>).
+
+
+-record(docs, {%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %%
+ %% PREPROCESSOR FIELDS
+ %%
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %%
+ %% These fields are used in a first pass to preprocess the AST.
+ %% The fields are considered the source of truth.
+ %%
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+ cwd :: file:filename(), % Cwd
+ filename :: file:filename(),
+ curr_filename :: file:filename(),
+ opts :: [opt()],
+
+ module :: module(),
+ deprecated = #{} :: map(),
+
+ docformat = ?DEFAULT_FORMAT :: binary(),
+ moduledoc = {?DEFAULT_MODULE_DOC_LOC, none} :: {integer() | erl_anno:anno(), none | map() | hidden},
+ moduledoc_meta = none :: none | #{ otp_doc_vsn => tuple() },
+
+ %% tracks exported functions from multiple `-export([...])`
+ exported_functions = sets:new() :: sets:set({FunName :: atom(), Arity :: non_neg_integer()}),
+
+ %% tracks exported type from multiple `-export_type([...])`
+ exported_types = sets:new() :: sets:set({TypeName :: atom(), Arity :: non_neg_integer()}),
+
+ %% tracks type_defs to point to their creation annotation. used for throwing warnings
+ %% about type definitions that are unreachable
+ type_defs = #{} :: #{{TypeName :: atom(), Arity :: non_neg_integer()} := erl_anno:anno()},
+
+ %% helper field to track hidden types
+ hidden_types = sets:new() :: sets:set({Name :: atom(), Arity :: non_neg_integer()}),
+
+ %% user defined types that need to be shown in the documentation. these are types that are not
+ %% exported but that the documentation needs to show because exported functions referred to them.
+ user_defined_types = sets:new() :: sets:set({TypeName :: atom(), Arity :: non_neg_integer()}),
+
+ %% used to report warnings of types in exported functions where the types
+ %% may have been set to hidden with a documentation attribute.
+ types_from_exported_funs = #{} :: #{{TypeName :: atom(), Arity :: non_neg_integer()} := [erl_anno:anno()]},
+
+ %% tracks the reachable type graph, i.e., type dependencies
+ type_dependency = digraph:new() :: digraph:graph(),
+
+ %% track any records found so that we can track
+ records = #{} :: #{ atom() => term() },
+
+ % keeps track of `-compile(export_all)`
+ export_all = false :: boolean(),
+
+ %% slogans: used to create slogans from it.
+ slogans = #{} :: #{{FunName :: atom(), Arity :: non_neg_integer()}
+ => {FunName :: atom(),
+ ListOfVars :: [atom()],
+ Arity :: non_neg_integer()}},
+
+ %% populates all function / types, callbacks. it is updated on an ongoing basis
+ %% since a doc attribute `doc ...` is not known in a first pass to be attached
+ %% to a function / type / callback.
+ docs = #{} :: #{{Attribute :: function | type | opaque | callback,
+ FunName :: atom(),
+ Arity :: non_neg_integer()}
+ =>
+ {Status :: none | {hidden, erl_anno:anno()} | set,
+ Documentation :: none | {DocText :: unicode:chardata(), Anno :: erl_anno:anno()},
+ Meta :: map()}},
+
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %%
+ %% DOCUMENTATION TRACKING
+ %%
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %%
+ %% Documentation attributes of the form `-doc ...` are not known
+ %% to be attached to the callback / function / type until reading
+ %% the next line. The following fields keep track of this state.
+ %% As soon as this state is known to be attached to a type / callback/ function,
+ %% this state should be saved in the `docs` field, which is a mapping
+ %% of {function(), arity()} => {...} e.g. contains hidden fields, lines,
+ %% documentation text, etc.
+ %%
+ %% one cannot rely on the fields below to keep track of documentation,
+ %% as Erlang allows pretty unstructure code.
+ %%
+ %% e.g.,
+ %%
+ %% -doc false.
+ %% -spec foo() -> ok.
+ %%
+ %% -spec bar() -> ok.
+ %%
+ %% -doc #{author => "X"}.
+ %% -doc foo() -> ok.
+ %%
+ %% thus, after reading a terminal AST node (spec, type, fun declaration, opaque, callback),
+ %% the intermediate state saveed in the fields below needs to be
+ %% saved in the `docs` field.
+
+ hidden_status = none :: none | hidden,
+
+ % Function/type/callback local doc. either none of some string was added
+ %% Stateful since the documentation is a two-step process.
+ %% First the documentation is entered, and the next terminal item (callback, fun, or type)
+ %% determines to which element the documentation gets attached to.
+ %%
+ %% When getting to a terminal item, the documentation and its status gets attached
+ %% to a terminal item in the global map `docs`.
+ doc = none :: none
+ | {DocText :: unicode:chardata(), Anno :: erl_anno:anno()} ,
+
+ %% track if the doc was never added (none), marked hidden (-doc hidden)
+ %% or entered (-doc "..."). If entered, doc_status = set, and doc = "...".
+ %% this field is needed because one we do the following:
+ %%
+ %% -doc hidden.
+ %% -doc "This is a hidden function".
+ %%
+ %% Alternatively, one can merge `doc` and `doc_status` as:
+ %%
+ %% doc = none | {hidden, "" | none} | "".
+ %%
+ %% The order in which `-doc hidden.` and `-doc "documentation here"` is written
+ %% is not defined, so one cannot assume that the following order:
+ %%
+ %% -doc "This is a hidden function".
+ %% -doc hidden.
+ %%
+ %% Because of this, we use two fields to keep track of documentation.
+ doc_status = none :: none | {hidden, erl_anno:anno()} | set,
+
+ % Function/type/callback local meta.
+ %% exported => boolean(), keeps track of types that are private but used in public functions
+ %% thus, they must be considered as exported for documentation purposes.
+ %% only useful when processing types. thus, it must be remove from functions and callbacks.
+ %% Stateful, need to be fixed as docs.
+ meta = #{exported => false} :: map(),
+
+ %% on analysing the AST, and upon finding a spec of a exported
+ %% function, the types from the spec are added to the field
+ %% below. if the function to which the spec belongs to is hidden,
+ %% we purge types from this field. if the function to which the
+ %% specs belong to are not hidden, they are added to
+ %% user_defined_types. Essentially, `last_read_user_types` is a
+ %% queue that accumulates types until they can be promoted to
+ %% `user_defined_types` or purged (removed).
+ %%
+ %% RATIONALE / DESIGN
+ %%
+ %% this field keeps track of these types until we reach the
+ %% function definition, which means that we already know if the
+ %% function sets `-doc false.`. upon having this information, we
+ %% can discard the user defined types when the function uses
+ %% `-doc false.` (hidden), since that means that the function
+ %% should not be displayed in the docs. if the function is not
+ %% hidden, we add the user defined types to the field
+ %% `user_defined_types` as these can be (non-)exported types. if
+ %% the types are exported, the docs will show the type
+ %% definition. if the types are not exported, the type definition
+ %% will be shown as not exported.
+ last_read_user_types = #{},
+
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %%
+ %% RESULT
+ %%
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+ ast_fns = [] :: list(),
+ ast_types = [] :: list(),
+ ast_callbacks = [] :: list(),
+ warnings = [] :: warnings()
+ }).
+
+-type internal_docs() :: #docs{}.
+-type opt() :: warn_missing_doc | nowarn_hidden_doc | {nowarn_hidden_doc, {atom(), arity()}}.
+-type kfa() :: {Kind :: function | type | callback, Name :: atom(), Arity :: arity()}.
+-type warnings() :: [{file:filename(),
+ [{erl_anno:location(), beam_doc, warning()}]}].
+-type warning() :: {missing_doc, kfa()} | missing_moduledoc |
+ {hidden_type_used_in_exported_fun | hidden_callback, {Name :: atom(), arity()}}.
+
+
+-doc "
+Transforms an Erlang abstract syntax form into EEP-48 documentation format.
+".
+-spec main(file:filename(), file:filename(), [erl_parse:abstract_form()], [opt()]) ->
+ {ok, #docs_v1{}, warnings()}.
+main(Dirname, Filename, AST, CmdLineOpts) ->
+ Opts = extract_opts(AST, CmdLineOpts),
+ State0 = new_state(Dirname, Filename, Opts),
+ State1 = preprocessing(AST, State0),
+ Docs = extract_documentation(AST, State1),
+ {ModuleDocAnno, ModuleDoc} = Docs#docs.moduledoc,
+ DocV1 = #docs_v1{},
+ Result = DocV1#docs_v1{ format = Docs#docs.docformat,
+ anno = ModuleDocAnno,
+ metadata = Docs#docs.moduledoc_meta,
+ module_doc = ModuleDoc,
+ docs = process_docs(Docs) },
+ {ok, Result, Docs#docs.warnings }.
+
+extract_opts(AST, CmdLineOpts) ->
+ CompileOpts = lists:flatten([C || {attribute,_,compile,C} <- AST]),
+ CompileOpts ++ CmdLineOpts.
+
+-spec format_error(warning()) -> io_lib:chars().
+format_error({hidden_type_used_in_exported_fun, {Type, Arity}}) ->
+ io_lib:format("hidden type '~p/~p' used in exported function",
+ [Type, Arity]);
+format_error({hidden_callback, {Name, Arity}}) ->
+ io_lib:format("hidden callback '~p/~p' used", [Name, Arity]);
+format_error({missing_doc, {Kind, Name, Arity}}) ->
+ io_lib:format("missing -doc for ~w ~tw/~w", [Kind, Name, Arity]);
+format_error(missing_moduledoc) ->
+ io_lib:format("missing -moduledoc", []).
+
+process_docs(#docs{ast_callbacks = AstCallbacks, ast_fns = AstFns, ast_types = AstTypes}) ->
+ AstTypes ++ AstCallbacks ++ AstFns.
+
+
+preprocessing(AST, State) ->
+ PreprocessingFuns = fun (AST0, State0) ->
+ Funs = [% Order matters
+ fun extract_deprecated/2,
+ fun extract_exported_types0/2, % done
+ fun extract_slogan_from_spec0/2,%done
+ fun track_documentation/2, %must be before upsert_documentation_from_terminal_item/2
+ fun upsert_documentation_from_terminal_item/2,
+ fun extract_docformat0/2, %done
+ fun extract_moduledoc0/2, %done
+ fun extract_module_meta/2, %done
+ fun extract_exported_funs/2, %done
+ fun extract_file/2, %done
+ fun extract_record/2,
+ fun extract_hidden_types0/2, %done
+ fun extract_type_defs0/2, %done
+ fun extract_type_dependencies/2],
+ foldl(fun (F, State1) -> F(AST0, State1) end, State0, Funs)
+ end,
+ foldl(PreprocessingFuns, State, AST).
+
+extract_deprecated({attribute, Anno, deprecated, {F, A}}, State) ->
+ extract_deprecated({attribute, Anno, deprecated, {F, A, undefined}}, State);
+extract_deprecated({attribute, _, deprecated, {F, A, Reason}}, State) ->
+ Deprecations = (State#docs.deprecated)#{ {function, F, A} => Reason },
+ State#docs{ deprecated = Deprecations };
+extract_deprecated({attribute, Anno, deprecated_type, {F, A}}, State) ->
+ extract_deprecated({attribute, Anno, deprecated_type, {F, A, undefined}}, State);
+extract_deprecated({attribute, _, deprecated_type, {F, A, Reason}}, State) ->
+ Deprecations = (State#docs.deprecated)#{ {type, F, A} => Reason },
+ State#docs{ deprecated = Deprecations };
+extract_deprecated(_, State) ->
+ State.
+
+extract_exported_types0({attribute,_ANNO,export_type,ExportedTypes}, State) ->
+ update_export_types(State, ExportedTypes);
+extract_exported_types0({attribute,_ANNO,module, Module}, State) ->
+ State#docs{ module = Module };
+extract_exported_types0({attribute,_ANNO,compile, export_all}, State) ->
+ update_export_all(State, true);
+extract_exported_types0(_AST, State) ->
+ State.
+
+extract_slogan_from_spec0({attribute, Anno, Tag, Form}, State) when Tag =:= spec; Tag =:= callback ->
+ maybe
+ {Name, Arity, Args} = extract_args_from_spec(Form),
+ true ?= is_list(Args),
+
+ Vars = foldl(fun (_, false) -> false;
+ ({var, _, Var}, Vars) -> [Var | Vars];
+ ({ann_type, _, [{var, _, Var} | _]}, Vars) -> [Var | Vars];
+ (_, _) -> false
+ end, [], Args),
+
+ true ?= is_list(Vars),
+ Arity ?= length(Vars),
+ update_slogan0(State, Anno, {Name, reverse(Vars), Arity})
+ else
+ _ ->
+ State
+ end;
+extract_slogan_from_spec0(_, State) ->
+ State.
+
+%%
+%% extract arguments for the slogan from the spec.
+%% does not accept multi-clause callbacks / specs due to the ambiguity
+%% of which spec clause to choose.
+%%
+extract_args_from_spec({{Name, Arity}, Types}) ->
+ case Types of
+ [{type, _, 'fun', [{type, _, product, Args}, _Return]}] ->
+ {Name, Arity, Args};
+ [{type, _, bounded_fun, [Args, _Constraints]}] ->
+ extract_args_from_spec({{Name, Arity}, [Args]});
+ _ ->
+ {Name, Arity, false}
+ end;
+extract_args_from_spec({{_Mod, Name, Arity}, Types}) ->
+ extract_args_from_spec({{Name, Arity}, Types}).
+
+update_slogan0(#docs{slogans = Slogans}=State, _Anno, {FunName, Vars, Arity}=Slogan)
+ when is_atom(FunName) andalso is_list(Vars) andalso is_number(Arity) ->
+ State#docs{slogans = Slogans#{{FunName, Arity} => Slogan}}.
+
+
+%% Documentation tracking is a two-step (stateful phase).
+%% First phase (this one) saves documentation attributes to fields
+%% until reaching a terminal element where the docs are gathered globally.
+track_documentation({attribute, _Anno, doc, Meta0}, State) when is_map(Meta0) ->
+ Meta1 = case Meta0 of
+ #{ equiv := {call,_,_Equiv,_Args}=Equiv} ->
+ Meta0#{ equiv := unicode:characters_to_binary(erl_pp:expr(Equiv)) };
+ #{ equiv := {Func,Arity}} ->
+ Meta0#{ equiv := unicode:characters_to_binary(io_lib:format("~p/~p",[Func,Arity])) };
+ _ ->
+ Meta0
+ end,
+ State1 = update_meta(State, Meta1),
+ update_doc(State1, none);
+track_documentation({attribute, Anno, doc, DocStatus}, State)
+ when DocStatus =:= hidden; DocStatus =:= false ->
+ update_docstatus(State, {hidden, set_file_anno(Anno, State)});
+track_documentation({attribute, Anno, doc, Doc}, State) when is_list(Doc) ->
+ update_doc(State, {Doc, Anno});
+track_documentation({attribute, Anno, doc, Doc}, State) when is_binary(Doc) ->
+ update_doc(State, {unicode:characters_to_list(Doc), Anno});
+track_documentation(_, State) ->
+ State.
+
+upsert_documentation_from_terminal_item({function, _Anno, F, Arity, _}, State) ->
+ upsert_documentation(function, F, Arity, State);
+upsert_documentation_from_terminal_item({attribute, _Anno, TypeOrOpaque, {TypeName, _TypeDef, TypeArgs}},State)
+ when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+ Arity = length(fun_to_varargs(TypeArgs)),
+ upsert_documentation(type, TypeName, Arity, State);
+upsert_documentation_from_terminal_item({attribute, _Anno, callback, {{CB, Arity}, _Form}}, State) ->
+ upsert_documentation(callback, CB, Arity, State);
+upsert_documentation_from_terminal_item(_, State) ->
+ State.
+
+upsert_documentation(Tag, Name, Arity, State) when Tag =:= function;
+ Tag =:= type;
+ Tag =:= opaque;
+ Tag =:= callback ->
+ Docs = State#docs.docs,
+ State1 = case maps:get({Tag, Name, Arity}, Docs, none) of
+ none ->
+ Status = State#docs.doc_status,
+ Doc = State#docs.doc,
+ Meta = State#docs.meta,
+ State#docs{docs = Docs#{{Tag, Name, Arity} => {Status, Doc, Meta}}};
+ {Status, Documentation, Meta} ->
+ Status1 = upsert_state(Status, State#docs.doc_status),
+ Doc = upsert_doc(Documentation, State#docs.doc),
+ Meta1 = upsert_meta(Meta, State#docs.meta),
+ State#docs{docs = Docs#{{Tag, Name, Arity} := {Status1, Doc, Meta1}}}
+ end,
+ reset_state(State1).
+
+%% Keep status unless there is a change.
+upsert_state({hidden, _}=Hidden, _) ->
+ Hidden;
+upsert_state(Status, none) ->
+ Status;
+upsert_state(_Status, Tag) ->
+ case Tag of
+ {hidden, _} ->
+ Tag;
+ set ->
+ Tag
+ end.
+
+upsert_doc(Documentation, none) ->
+ Documentation;
+upsert_doc(_, Documentation) ->
+ Documentation.
+
+upsert_meta(Meta0, Meta1) ->
+ maps:merge(Meta0, Meta1).
+
+
+extract_docformat0({attribute, _ModuleDocAnno, moduledoc, MetaFormat}, State) when is_map(MetaFormat) ->
+ case maps:get(format, MetaFormat, not_found) of
+ not_found -> State;
+ Format when is_list(Format) -> State#docs{docformat = unicode:characters_to_binary(Format)};
+ Format when is_binary(Format) -> State#docs{docformat = Format}
+ end;
+extract_docformat0(_, State) ->
+ State.
+
+%%
+%% Sets module documentation attributes
+%%
+extract_moduledoc0({attribute, ModuleDocAnno, moduledoc, false}, State) ->
+ extract_moduledoc0({attribute, ModuleDocAnno, moduledoc, hidden}, State);
+extract_moduledoc0({attribute, ModuleDocAnno, moduledoc, hidden}, State) ->
+ State#docs{moduledoc = {ModuleDocAnno, create_module_doc(hidden)}};
+extract_moduledoc0({attribute, ModuleDocAnno, moduledoc, ModuleDoc}, State) when is_list(ModuleDoc) ->
+ Doc = unicode:characters_to_binary(string:trim(ModuleDoc)),
+ State#docs{moduledoc = {set_file_anno(ModuleDocAnno, State), create_module_doc(Doc)}};
+extract_moduledoc0(_, State) ->
+ State.
+
+
+extract_module_meta({attribute, _ModuleDocAnno, moduledoc, MetaDoc}, State) when is_map(MetaDoc) ->
+ State#docs{moduledoc_meta = maps:merge(State#docs.moduledoc_meta, MetaDoc)};
+extract_module_meta(_, State) ->
+ State.
+
+extract_exported_funs({attribute,_ANNO,export,ExportedFuns}, State) ->
+ update_export_funs(State, ExportedFuns);
+extract_exported_funs(_, State) ->
+ State.
+
+
+%% Sets the filename based on the module
+extract_file({attribute, _Anno, file, {Filename, _A}}, State) ->
+ update_filename(State, Filename);
+extract_file(_, State) ->
+ State.
+
+extract_record({attribute, Anno, record, {Name, Fields}}, State) ->
+ TypeFields = filtermap(
+ fun({typed_record_field, RecordField, Type}) ->
+ {true, {type, Anno, field_type, [element(3, RecordField), Type]}};
+ (_) ->
+ false
+ end, Fields),
+ State#docs{ records = (State#docs.records)#{ Name => TypeFields } };
+extract_record(_, State) ->
+ State.
+
+%%
+%% Extracts types with documentation attribute set to `hidden` or `false`.
+%%
+%% E.g.:
+%%
+%% -doc hidden.
+%% -type foo() :: integer().
+%%
+extract_hidden_types0({attribute, _Anno, doc, DocStatus}, State) when
+ DocStatus =:= hidden; DocStatus =:= false ->
+ State#docs{hidden_status = hidden};
+extract_hidden_types0({attribute, _Anno, doc, _}, State) ->
+ State;
+extract_hidden_types0({attribute, _Anno, TypeOrOpaque, {Name, _Type, Args}}, #docs{hidden_status = hidden,
+ hidden_types = HiddenTypes}=State)
+ when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+ State#docs{hidden_status = none,
+ hidden_types = sets:add_element({Name, length(Args)}, HiddenTypes)};
+extract_hidden_types0(_, State) ->
+ State#docs{hidden_status = none}.
+
+
+%%
+%% Adds type definitions / user-defined types to the state
+%%
+%% Necessary to provide warnings using the mapping
+%% #{{TypeName, length(Args)} => Anno}.
+%%
+extract_type_defs0({attribute, Anno, TypeOrOpaque, {TypeName, _TypeDef, TypeArgs}}, #docs{type_defs = TypeDefs}=State)
+ when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+ Args = fun_to_varargs(TypeArgs),
+ Type = {TypeName, length(Args)},
+ State#docs{type_defs = TypeDefs#{Type => Anno}};
+extract_type_defs0(_, State) ->
+ State.
+
+%%
+%% Creates a reachable type graph.
+%%
+%% Given a type `-type X(Args) :: Args2.`, `X` is a vertex that
+%% connects with vertices from Args and Args, creating a reachable
+%% type graph.
+%%
+extract_type_dependencies({attribute, _Anno, TypeOrOpaque, {TypeName, TypeDef, TypeArgs}},
+ #docs{type_dependency = TypeDependency}=State)
+ when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+ Types = extract_user_types([TypeArgs, TypeDef], State),
+ Type = {TypeName, length(TypeArgs)},
+ digraph:add_vertex(TypeDependency, Type),
+ _ = [begin
+ digraph:add_vertex(TypeDependency, TypeAndArity),
+ digraph:add_edge(TypeDependency, Type, TypeAndArity)
+ end || TypeAndArity <- maps:keys(Types)],
+ State;
+extract_type_dependencies(_, State) ->
+ State.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% Helper functions
+%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+
+-spec create_module_doc(ModuleDoc :: binary() | atom()) -> map().
+create_module_doc(ModuleDoc) when is_atom(ModuleDoc) ->
+ ModuleDoc;
+create_module_doc(ModuleDoc) when not is_atom(ModuleDoc) ->
+ create_module_doc(<<"en">>, ModuleDoc).
+
+-spec create_module_doc(Lang :: binary(), ModuleDoc :: binary()) -> map().
+create_module_doc(Lang, ModuleDoc) ->
+ #{Lang => ModuleDoc}.
+
+-spec new_state(Dirname :: file:filename(), Filename :: file:filename(),
+ Opts :: [opt()]) -> internal_docs().
+new_state(Dirname, Filename, Opts) ->
+ DocsV1 = #docs_v1{},
+ reset_state(#docs{cwd = Dirname, filename = Filename,
+ curr_filename = Filename, opts = Opts,
+ moduledoc_meta = DocsV1#docs_v1.metadata}).
+
+
+-spec reset_state(State :: internal_docs()) -> internal_docs().
+reset_state(State) ->
+ State#docs{doc = none,
+ doc_status = none,
+ meta = #{exported => false}}.
+
+update_docstatus(State, V) ->
+ State#docs{doc_status = V}.
+
+
+update_ast(function, #docs{ast_fns=AST}=State, Fn) ->
+ State#docs{ast_fns = [Fn | AST]};
+update_ast(Type,#docs{ast_types=AST}=State, Fn) when Type =:= type; Type =:= opaque->
+ State#docs{ast_types = [Fn | AST]};
+update_ast(callback, #docs{ast_callbacks = AST}=State, Fn) ->
+ State#docs{ast_callbacks = [Fn | AST]}.
+
+-spec update_meta(State :: internal_docs(), Meta :: map()) -> internal_docs().
+update_meta(#docs{meta = Meta0}=State, Meta1) ->
+ State#docs{meta = maps:merge(Meta0, Meta1)}.
+
+-spec update_user_defined_types({type | callback | function, term(), integer()},
+ State :: internal_docs()) -> internal_docs().
+update_user_defined_types({_Attr, _F, _A}=Key,
+ #docs{user_defined_types = UserDefinedTypes,
+ last_read_user_types = LastAddedTypes}=State) ->
+ Docs = State#docs.docs,
+ case maps:get(Key, Docs, none) of
+ {{hidden, _Anno}, _, _} ->
+ State#docs{last_read_user_types = #{}};
+ _ ->
+ State#docs{user_defined_types = sets:union(UserDefinedTypes, sets:from_list(maps:keys(LastAddedTypes))),
+ last_read_user_types = #{}}
+ end.
+
+-spec update_doc(State :: internal_docs(), Doc) -> internal_docs() when
+ Doc :: {unicode:chardata(), erl_anno:anno()} | atom().
+update_doc(#docs{doc_status = DocStatus}=State, Doc0) ->
+ %% The exported := true only applies to types and should be ignored for functions.
+ %% This is because we need to export private types that are used on public
+ %% functions, or the documentation will create dead links.
+ State1 = update_docstatus(State, set_doc_status(DocStatus)),
+ State2 = update_meta(State1, #{exported => true}),
+ case Doc0 of
+ none ->
+ State2;
+ {Doc, Anno} ->
+ State2#docs{doc = {string:trim(Doc), set_file_anno(Anno, State2)}}
+ end.
+
+set_file_anno(Anno, State) ->
+ case {State#docs.curr_filename, erl_anno:file(Anno)} of
+ {ModuleName, undefined} when ModuleName =/= "",
+ ModuleName =/= State#docs.filename ->
+ erl_anno:set_file(ModuleName, Anno);
+ _ ->
+ Anno
+ end.
+
+%% Sets the doc status from `none` to `set`.
+%% Leave unchanged if the status was already set to something.
+%%
+set_doc_status(none) ->
+ set;
+set_doc_status(Other) ->
+ Other.
+
+-spec update_filename(State :: internal_docs(), ModuleName :: unicode:chardata()) -> internal_docs().
+update_filename(#docs{}=State, ModuleName) ->
+ State#docs{curr_filename = ModuleName}.
+
+-spec update_export_funs(State :: internal_docs(), proplists:proplist()) -> internal_docs().
+update_export_funs(State, ExportedFuns) ->
+ ExportedFuns1 = sets:union(State#docs.exported_functions, sets:from_list(ExportedFuns)),
+ State#docs{exported_functions = ExportedFuns1}.
+
+-spec update_export_types(State :: internal_docs(), proplists:proplist()) -> internal_docs().
+update_export_types(State, ExportedTypes) ->
+ ExportedTypes1 = sets:union(State#docs.exported_types, sets:from_list(ExportedTypes)),
+ State#docs{exported_types = ExportedTypes1}.
+
+update_export_all(State, ExportAll) ->
+ State#docs{ export_all = ExportAll }.
+
+remove_exported_type_info(Key, #docs{docs = Docs}=State) ->
+ {Status, Doc, Meta} = maps:get(Key, Docs),
+ Docs1 = maps:update(Key, {Status, Doc, maps:remove(exported, Meta)}, Docs),
+ State#docs{docs = Docs1}.
+
+extract_documentation(AST, State) ->
+ State1 = foldl(fun extract_documentation0/2, State, AST),
+ State2 = purge_types_not_used_from_exported_functions(State1),
+ State3 = purge_unreachable_types(State2),
+ warnings(AST, State3).
+
+%%
+%% purges types that are not used in exported functions.
+%% the type dependency field in docs does not keep track of which type
+%% is used in a public function, it simply connects all types.
+%% types of hidden functions may exist in the reachable type graph
+%% and they should be ignored unless reachable from
+purge_types_not_used_from_exported_functions(#docs{user_defined_types = UserDefinedTypes}=State) ->
+ AstTypes = filter(fun ({{_, F, A}, _Anno, _Slogan, _Doc, #{exported := Exported}}) ->
+ sets:is_element({F, A}, UserDefinedTypes) orelse Exported
+ end, State#docs.ast_types),
+ State#docs{ast_types = AstTypes }.
+
+purge_unreachable_types(#docs{types_from_exported_funs = TypesFromExportedFuns,
+ type_dependency = TypeDependency}=State) ->
+ SetTypesFromExportedFns = sets:from_list(maps:keys(TypesFromExportedFuns)),
+ SetTypes = sets:union(SetTypesFromExportedFns, State#docs.exported_types),
+ ReachableTypes = digraph_utils:reachable(sets:to_list(SetTypes), TypeDependency),
+ ReachableSet = sets:from_list(ReachableTypes),
+ AstTypes = filter(fun ({{_, F, A}, _Anno, _Slogan, _Doc, #{exported := Exported}}) ->
+ sets:is_element({F, A}, ReachableSet) orelse Exported
+ end, State#docs.ast_types),
+ State#docs{ast_types = AstTypes }.
+
+warnings(_AST, State) ->
+ WarnFuns = [fun warn_hidden_types_used_in_public_fns/1,
+ fun warn_missing_docs/1,
+ fun warn_missing_moduledoc/1,
+ fun warn_hidden_callback/1
+ ],
+ foldl(fun (W, State0) -> W(State0) end, State, WarnFuns).
+
+warn_missing_docs(State) ->
+ DocNodes = process_docs(State),
+ foldl(fun warn_missing_docs/2, State, DocNodes).
+
+warn_hidden_callback(State) ->
+ L = maps:to_list(State#docs.docs),
+ NoWarn = flatten(proplists:get_all_values(nowarn_hidden_doc, State#docs.opts)),
+ case member(true, NoWarn) of
+ false ->
+ foldl(fun ({{callback, Name, Arity},{{hidden, Anno}, _, _}}, State0) ->
+ case member({Name, Arity}, NoWarn) of
+ false ->
+ Warning = {hidden_callback, {Name, Arity}},
+ W = create_warning(Anno, Warning, State0),
+ State0#docs{ warnings = [W | State0#docs.warnings] };
+ true ->
+ State0
+ end;
+ (_, State0) ->
+ State0
+ end, State, L);
+ true ->
+ State
+ end.
+
+%% hidden types with `-doc hidden.` or `-doc false.`, which are public (inside
+%% `export_type([])`), and used in public functions, they do not make sense. It
+%% is a type that is not documented (due to hidden property), visible in the
+%% docs (because it is in export_type), and reference / used by a public
+%% function cannot be used.
+%% A type that is hidden, private, and used in an exported function will be documented
+%% by the doc generation showing the internal type structure.
+warn_hidden_types_used_in_public_fns(#docs{types_from_exported_funs = TypesFromExportedFuns,
+ type_dependency = TypeDependency,
+ type_defs = TypeDefs}=State) ->
+ NoWarn = flatten(proplists:get_all_values(nowarn_hidden_doc, State#docs.opts)),
+ case member(true, NoWarn) of
+ false ->
+ HiddenTypes = State#docs.hidden_types,
+ Types = maps:keys(TypesFromExportedFuns),
+ ReachableTypes = digraph_utils:reachable(Types, TypeDependency),
+ ReachableSet = sets:from_list(ReachableTypes),
+ Warnings = sets:intersection(HiddenTypes, ReachableSet),
+ FilteredWarnings = sets:filter(fun(Key) -> not member(Key, NoWarn) end, Warnings),
+ WarningsWithAnno = sets:map(fun (Key) ->
+ Anno = maps:get(Key, TypeDefs),
+ Warn = {hidden_type_used_in_exported_fun, Key},
+ create_warning(Anno, Warn, State)
+ end, FilteredWarnings),
+ State#docs{warnings = State#docs.warnings ++ sets:to_list(WarningsWithAnno) };
+ true ->
+ State
+ end.
+
+create_warning(Anno, Warning, State) ->
+ Filename = erl_anno_file(Anno, State),
+ Location = erl_anno:location(Anno),
+ {Filename, [{Location, ?MODULE, Warning}]}.
+
+warn_missing_docs({KFA, Anno, _, Doc, _}, State) ->
+ case proplists:get_value(warn_missing_doc, State#docs.opts, false) of
+ true when Doc =:= none ->
+ Warning = {missing_doc, KFA},
+ State#docs{ warnings = [create_warning(Anno, Warning, State) | State#docs.warnings] };
+ _false ->
+ State
+ end.
+
+warn_missing_moduledoc(State) ->
+ {_, ModuleDoc} = State#docs.moduledoc,
+ case proplists:get_value(warn_missing_doc, State#docs.opts, false) of
+ true when ModuleDoc =:= none ->
+ Anno = erl_anno:new(?DEFAULT_MODULE_DOC_LOC),
+ Warning = missing_moduledoc,
+ State#docs{ warnings = [create_warning(Anno, Warning, State) | State#docs.warnings] };
+ _false ->
+ State
+ end.
+
+%%
+%% Extracts documentation
+%%
+%% This algorithm may use temporal state to keep track of documentation.
+%% Example: By looking at `-doc ...` one cannot know whether the doc
+%% is attached to a function, type, callback.
+extract_documentation0({attribute, _Anno, file, {Filename, _A}}, State) ->
+ update_filename(State, Filename);
+extract_documentation0({attribute, _Anno, spec, _}=AST, State) ->
+ extract_documentation_spec(AST, State);
+extract_documentation0({function, _Anno, F, A, _Body}=AST, State) ->
+ State1 = remove_exported_type_info({function, F, A}, State),
+ extract_documentation_from_funs(AST, State1);
+extract_documentation0({attribute, _Anno, TypeOrOpaque, _}=AST,State)
+ when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+ extract_documentation_from_type(AST, State);
+extract_documentation0({attribute, _Anno, callback, {{CB, A}, _Form}}=AST, State) ->
+ State1 = remove_exported_type_info({callback, CB, A}, State),
+ extract_documentation_from_cb(AST, State1);
+extract_documentation0(_, State) ->
+ State.
+
+
+extract_documentation_spec({attribute, Anno, spec, {{Name,Arity}, SpecTypes}}, #docs{exported_functions = ExpFuns}=State) ->
+%% this is because public functions may use private types and these private
+%% types need to be included in the beam and documentation.
+ case sets:is_element({Name, Arity}, ExpFuns) orelse State#docs.export_all of
+ true ->
+ add_user_types(Anno, SpecTypes, State);
+ false ->
+ State
+ end;
+extract_documentation_spec({attribute, Anno, spec, {{_Mod, Name,Arity}, SpecTypes}}, State) ->
+ extract_documentation_spec({attribute, Anno, spec, {{Name,Arity}, SpecTypes}}, State).
+
+add_user_types(_Anno, SpecTypes, State) ->
+ Types = extract_user_types(SpecTypes, State),
+ State1 = set_types_used_in_public_funs(State, Types),
+ set_last_read_user_types(State1, Types).
+
+%% pre: only call this function to add types from external functions.
+set_types_used_in_public_funs(#docs{types_from_exported_funs = TypesFromExportedFuns}=State, Types) ->
+ Combiner = fun (_Key, Value1, Value2) -> Value1 ++ Value2 end,
+ Types0 = maps:merge_with(Combiner, TypesFromExportedFuns, Types),
+ State#docs{types_from_exported_funs = Types0}.
+
+set_last_read_user_types(#docs{}=State, Types) ->
+ State#docs{last_read_user_types = Types}.
+
+extract_user_types(Args, #docs{ records = Records }) ->
+ {Types, _Records} = extract_user_types(Args, {maps:new(), Records}),
+ Types;
+extract_user_types(Types, Acc) when is_list(Types) ->
+ foldl(fun extract_user_types/2, Acc, Types);
+extract_user_types({ann_type, _, [_Name, Type]}, Acc) ->
+ extract_user_types(Type, Acc);
+extract_user_types({type, _, 'fun', Args}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({type, _, map, Args}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({type, _,record,[{atom, _, Name} | Args]}, {Acc, Records}) ->
+ NewArgs = uniq(fun({type, _, field_type, [{atom, _, FieldName} | _]}) ->
+ FieldName
+ end, Args ++ maps:get(Name, Records, [])),
+ extract_user_types(NewArgs, {Acc, maps:remove(Name, Records)});
+extract_user_types({remote_type,_,[_ModuleName,_TypeName,Args]}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({type, _, tuple, Args}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({type, _,union, Args}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({user_type, Anno, Name, Args}, {Acc, Records}) ->
+ %% append user type and continue iterating through lists in case of other
+ %% user-defined types to be added
+ Fun = fun (Value) -> [Anno | Value] end,
+ Acc1 = maps:update_with({Name, length(Args)}, Fun, [Anno], Acc),
+ extract_user_types(Args, {Acc1, Records});
+extract_user_types({type, _, bounded_fun, Args}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({type,_,product,Args}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({type,_,constraint,[{atom,_,is_subtype},Args]}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({type, _, map_field_assoc, Args}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({type, _, map_field_exact, Args}, Acc) ->
+ extract_user_types(Args, Acc);
+extract_user_types({type,_,field_type,[_Name, Type]}, Acc) ->
+ extract_user_types(Type, Acc);
+extract_user_types({type, _,_BuiltIn, Args}, Acc) when is_list(Args)->
+ %% Handles built-in types such as 'list', 'nil' 'range'.
+ extract_user_types(Args, Acc);
+extract_user_types(_Else, Acc) ->
+ Acc.
+
+extract_documentation_from_type({attribute, Anno, TypeOrOpaque, {TypeName, _TypeDef, TypeArgs}=Types},
+ #docs{docs = Docs, exported_types=ExpTypes}=State)
+ when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+ Args = fun_to_varargs(TypeArgs),
+ Key = {type, TypeName, length(TypeArgs)},
+
+ % we assume it exists because a first pass must have added it
+ {Status, Doc, Meta} = maps:get(Key, Docs),
+
+ State0 = add_last_read_user_type(Anno, Types, State),
+ Type = {TypeName, length(Args)},
+
+ Docs1 = maps:update(Key, {Status, Doc, Meta#{exported := sets:is_element(Type, ExpTypes)}}, Docs),
+ State2 = State0#docs {docs = Docs1},
+ State3 = gen_doc_with_slogan({type, Anno, TypeName, length(Args), Args}, State2),
+ add_type_defs(Type, State3).
+
+add_type_defs(Type, #docs{type_defs = TypeDefs, ast_types = [{_KFA, Anno, _Slogan, _Doc, _Meta} | _]}=State) ->
+ State#docs{type_defs = TypeDefs#{Type => Anno}}.
+
+
+add_last_read_user_type(_Anno, {_TypeName, TypeDef, TypeArgs}, State) ->
+ Types = extract_user_types([TypeArgs, TypeDef], State),
+ set_last_read_user_types(State, Types).
+
+%% NOTE: Terminal elements for the documentation, such as `-type`, `-opaque`, `-callback`,
+%% and functions always need to reset the state when they finish, so that new
+%% new AST items start with a clean slate.
+extract_documentation_from_funs({function, Anno, F, A, [{clause, _, ClauseArgs, _, _}]},
+ #docs{exported_functions = ExpFuns}=State) ->
+ case (sets:is_element({F, A}, ExpFuns) orelse State#docs.export_all) of
+ true ->
+ gen_doc_with_slogan({function, Anno, F, A, ClauseArgs}, State);
+ false ->
+ reset_state(State)
+ end;
+extract_documentation_from_funs({function, _Anno0, F, A, _Body}=AST,
+ #docs{exported_functions=ExpFuns}=State) ->
+ {Doc1, Anno1} = fetch_doc_and_anno(State, AST),
+ case sets:is_element({F, A}, ExpFuns) orelse State#docs.export_all of
+ true ->
+ {Slogan, DocsWithoutSlogan} = extract_slogan(Doc1, State, F, A),
+ AttrBody = {function, F, A},
+ gen_doc(Anno1, AttrBody, Slogan, DocsWithoutSlogan, State);
+ false ->
+ reset_state(State)
+ end.
+
+extract_documentation_from_cb({attribute, Anno, callback, {{CB, A}, Form}}, State) ->
+ %% adds user types as part of possible types that need to be exported
+ State2 = add_user_types(Anno, Form, State),
+ Args = case Form of
+ [Fun] ->
+ fun_to_varargs(Fun);
+ _ -> %% multi-clause
+ Form
+ end,
+ gen_doc_with_slogan({callback, Anno, CB, A, Args}, State2).
+
+%% Generates documentation
+-spec gen_doc(Anno, AttrBody, Slogan, Docs, State) -> Response when
+ Anno :: erl_anno:anno(),
+ AttrBody :: {function | type | callback, term(), integer()},
+ Slogan :: unicode:chardata(),
+ Docs :: none | hidden | unicode:chardata() | #{ <<_:16>> => unicode:chardata() },
+ State :: internal_docs(),
+ Response :: internal_docs().
+gen_doc(Anno, AttrBody, Slogan, Docs, State) when not is_atom(Docs), not is_map(Docs) ->
+ gen_doc(Anno, AttrBody, Slogan, #{ <<"en">> => unicode:characters_to_binary(string:trim(Docs)) }, State);
+gen_doc(Anno, {Attr, _F, _A}=AttrBody, Slogan, Docs, #docs{docs=DocsMap}=State) ->
+ {_Status, _Doc, Meta} = maps:get(AttrBody, DocsMap),
+ Result = {AttrBody, Anno, [unicode:characters_to_binary(Slogan)], Docs,
+ maybe_add_deprecation(AttrBody, Meta, State)},
+ State1 = update_user_defined_types(AttrBody, State),
+ reset_state(update_ast(Attr, State1, Result)).
+
+erl_anno_file(Anno, State) ->
+ case erl_anno:file(Anno) of
+ undefined ->
+ State#docs.filename;
+ FN -> FN
+ end.
+
+maybe_add_deprecation(_KNA, #{ deprecated := Deprecated } = Meta, _State) ->
+ Meta#{ deprecated := unicode:characters_to_binary(Deprecated) };
+maybe_add_deprecation({Kind, Name, Arity}, Meta, #docs{ module = Module,
+ deprecated = Deprecations }) ->
+ maybe
+ error ?= maps:find({Kind, Name, Arity}, Deprecations),
+ error ?= maps:find({Kind, Name, '_'}, Deprecations),
+ error ?= maps:find({Kind, '_', Arity}, Deprecations),
+ error ?= maps:find({Kind, '_', '_'}, Deprecations),
+ Meta
+ else
+ {ok, Value} ->
+ Text =
+ if Kind =:= function ->
+ erl_lint:format_error({deprecated, {Module,Name,Arity},
+ info_string(Value)});
+ Kind =:= type ->
+ erl_lint:format_error({deprecated_type, {Module,Name,Arity},
+ info_string(Value)})
+ end,
+ Meta#{ deprecated => unicode:characters_to_binary(Text) }
+ end.
+
+%% Copies from lib/stdlib/scripts/update_deprecations
+info_string(undefined) ->
+ "see the documentation for details";
+info_string(next_version) ->
+ "will be removed in the next version. "
+ "See the documentation for details";
+info_string(next_major_release) ->
+ "will be removed in the next major release. "
+ "See the documentation for details";
+info_string(eventually) ->
+ "will be removed in a future release. "
+ "See the documentation for details";
+info_string(String) when is_list(String) ->
+ String.
+
+%% Generates the documentation inferring the slogan from the documentation.
+gen_doc_with_slogan({Attr, _Anno0, F, A, Args}=AST, State) ->
+ {Doc1, Anno1} = fetch_doc_and_anno(State, AST),
+ {Slogan, DocsWithoutSlogan} = extract_slogan(Doc1, State, F, A, Args),
+ AttrBody = {Attr, F, A},
+ gen_doc(Anno1, AttrBody, Slogan, DocsWithoutSlogan, State).
+
+fetch_doc_and_anno(#docs{docs = DocsMap}=State, {Attr, Anno0, F, A, _Args}) ->
+ %% a first pass guarantees that DocsMap cannot be empty
+ {DocStatus, Doc, _Meta} = maps:get({Attr, F, A}, DocsMap),
+ case {DocStatus, Doc} of
+ {{hidden, Anno}, _} -> {hidden, Anno};
+ {_, none} -> {none, set_file_anno(Anno0, State)};
+ {_, {Doc1, Anno}} -> {Doc1, Anno}
+ end.
+
+-spec fun_to_varargs(tuple() | term()) -> list(term()).
+fun_to_varargs({type, _, bounded_fun, [T|_]}) ->
+ fun_to_varargs(T);
+fun_to_varargs({type, _, 'fun', [{type,_,product,Args}|_] }) ->
+ map(fun fun_to_varargs/1, Args);
+fun_to_varargs({ann_type, _, [Name|_]}) ->
+ Name;
+fun_to_varargs({var,_,_} = Name) ->
+ Name;
+fun_to_varargs(Else) ->
+ Else.
+
+extract_slogan(Doc, State, F, A) ->
+ extract_slogan(Doc, State, F, A, [invalid]).
+extract_slogan(Doc, State, F, A, Args) ->
+ %% order of the strategy matters
+ StrategyOrder = [fun slogan_strategy_doc_attr/5,
+ fun slogan_strategy_spec/5,
+ fun slogan_strategy_args/5,
+ fun slogan_strategy_default/5],
+
+ %% selection alg. tries strategy until one strategy
+ %% returns value =/= false
+ SloganSelection = fun (Fun, false) -> Fun(Doc, State, F, A, Args);
+ (_F, Acc) -> Acc
+ end,
+ foldl(SloganSelection, false, StrategyOrder).
+
+
+slogan_strategy_doc_attr(Doc, _State, F, A, _Args) ->
+ maybe
+ false ?= Doc =:= none orelse Doc =:= hidden,
+ [MaybeSlogan | Rest] = string:split(Doc, "\n"),
+ {ok, Toks, _} ?= erl_scan:string(unicode:characters_to_list([MaybeSlogan,"."])),
+ {ok, [{call,_,{atom,_,F},SloganArgs}]} ?= erl_parse:parse_exprs(Toks),
+ A ?= length(SloganArgs),
+ {MaybeSlogan, Rest}
+ else
+ _ ->
+ false
+ end.
+
+slogan_strategy_spec(Doc, State, F, A, _Args) ->
+ case maps:get({F, A}, State#docs.slogans, none) of
+ {F, Vars, A} ->
+ VarString = join(", ",[atom_to_list(Var) || Var <- Vars]),
+ Slogan = unicode:characters_to_list(io_lib:format("~p(~s)", [F, VarString])),
+ {Slogan, Doc};
+ none ->
+ false
+ end.
+
+slogan_strategy_args(Doc, _State, F, _A, Args) ->
+ case all(fun is_var_without_underscore/1, Args) of
+ true ->
+ {extract_slogan_from_args(F, Args), Doc};
+ false ->
+ false
+ end.
+
+slogan_strategy_default(Doc, _State, F, A, _Args) ->
+ {io_lib:format("~p/~p",[F,A]), Doc}.
+
+
+is_var_without_underscore({var, _, N}) ->
+ N =/= '_';
+is_var_without_underscore(_) ->
+ false.
+
+extract_slogan_from_args(F, Args) ->
+ io_lib:format("~p(~ts)",[F, join(", ",[string:trim(atom_to_list(Arg),leading,"_") || {var, _, Arg} <- Args])]).
diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl
index d4e693aee0..43d55bb5d9 100644
--- a/lib/compiler/src/compile.erl
+++ b/lib/compiler/src/compile.erl
@@ -340,19 +340,19 @@ format_error_reason(Class, Reason, Stack) ->
erl_error:format_exception(Class, Reason, Stack, Opts).
%% The compile state record.
--record(compile, {filename="" :: file:filename(),
- dir="" :: file:filename(),
- base="" :: file:filename(),
- ifile="" :: file:filename(),
- ofile="" :: file:filename(),
- module=[] :: module() | [],
- abstract_code=[] :: abstract_code(), %Abstract code for debugger.
- options=[] :: [option()], %Options for compilation
- mod_options=[] :: [option()], %Options for module_info
- encoding=none :: none | epp:source_encoding(),
- errors=[] :: errors(),
- warnings=[] :: warnings(),
- extra_chunks=[] :: [{binary(), binary()}]}).
+-record(compile, {filename="" :: file:filename(),
+ dir="" :: file:filename(),
+ base="" :: file:filename(),
+ ifile="" :: file:filename(),
+ ofile="" :: file:filename(),
+ module=[] :: module() | [],
+ abstract_code=[] :: abstract_code(), %Abstract code for debugger.
+ options=[] :: [option()], %Options for compilation
+ mod_options=[] :: [option()], %Options for module_info
+ encoding=none :: none | epp:source_encoding(),
+ errors=[] :: errors(),
+ warnings=[] :: warnings(),
+ extra_chunks=[] :: [{binary(), binary()}]}).
internal({forms,Forms}, Opts0) ->
{_,Ps} = passes(forms, Opts0),
@@ -812,6 +812,8 @@ standard_passes() ->
{iff,'dpp',{listing,"pp"}},
?pass(lint_module),
+ {unless,no_docs,?pass(beam_docs)},
+ ?pass(remove_doc_attributes),
{iff,'P',{src_listing,"P"}},
{iff,'to_pp',{done,"P"}},
@@ -821,7 +823,9 @@ standard_passes() ->
abstr_passes(AbstrStatus) ->
case AbstrStatus of
- non_verified_abstr -> [{unless, no_lint, ?pass(lint_module)}];
+ non_verified_abstr -> [{unless, no_lint, ?pass(lint_module)},
+ {unless,no_docs,?pass(beam_docs)},
+ ?pass(remove_doc_attributes)];
verified_abstr -> []
end ++
[
@@ -1027,17 +1031,20 @@ parse_module(_Code, St) ->
Ret
end.
-do_parse_module(DefEncoding, #compile{ifile=File,options=Opts,dir=Dir}=St) ->
+deterministic_filename(#compile{ifile=File,options=Opts}) ->
SourceName0 = proplists:get_value(source, Opts, File),
- SourceName = case member(deterministic, Opts) of
- true ->
- filename:basename(SourceName0);
- false ->
- case member(absolute_source, Opts) of
- true -> paranoid_absname(SourceName0);
- false -> SourceName0
- end
- end,
+ case member(deterministic, Opts) of
+ true ->
+ filename:basename(SourceName0);
+ false ->
+ case member(absolute_source, Opts) of
+ true -> paranoid_absname(SourceName0);
+ false -> SourceName0
+ end
+ end.
+
+do_parse_module(DefEncoding, #compile{ifile=File,options=Opts,dir=Dir}=St) ->
+ SourceName = deterministic_filename(St),
StartLocation = case with_columns(Opts) of
true ->
{1,1};
@@ -1589,6 +1596,31 @@ core_inline_module(Code0, #compile{options=Opts}=St) ->
save_abstract_code(Code, St) ->
{ok,Code,St#compile{abstract_code=erl_parse:anno_to_term(Code)}}.
+-define(META_DOC_CHUNK, <<"Docs">>).
+
+
+%% Adds documentation attributes to extra_chunks (beam file)
+beam_docs(Code, #compile{dir = Dir, options = Options,
+ extra_chunks = ExtraChunks }=St) ->
+ SourceName = deterministic_filename(St),
+ {ok, Docs, Ws} = beam_doc:main(Dir, SourceName, Code, Options),
+ MetaDocs = [{?META_DOC_CHUNK, term_to_binary(Docs)} | ExtraChunks],
+ {ok, Code, St#compile{extra_chunks = MetaDocs,
+ warnings = St#compile.warnings ++ Ws}}.
+
+%% Strips documentation attributes from the code
+remove_doc_attributes(Code, St) ->
+ {ok, [Attr || Attr <- Code, not is_doc_attribute(Attr)], St}.
+
+
+is_doc_attribute(Attr) ->
+ case Attr of
+ {attribute, _Anno, DocAttr, _Meta}
+ when DocAttr =:= doc; DocAttr =:= moduledoc; DocAttr =:= docformat ->
+ true;
+ _ -> false
+ end.
+
debug_info(#compile{module=Module,ofile=OFile}=St) ->
{DebugInfo,Opts2} = debug_info_chunk(St),
case member(encrypt_debug_info, Opts2) of
@@ -2123,6 +2155,7 @@ pre_load() ->
beam_clean,
beam_dict,
beam_digraph,
+ beam_doc,
beam_flatten,
beam_jump,
beam_kernel_to_ssa,
diff --git a/lib/compiler/src/compiler.app.src b/lib/compiler/src/compiler.app.src
index 71588c0826..3190b43468 100644
--- a/lib/compiler/src/compiler.app.src
+++ b/lib/compiler/src/compiler.app.src
@@ -31,6 +31,7 @@
beam_dict,
beam_digraph,
beam_disasm,
+ beam_doc,
beam_flatten,
beam_jump,
beam_kernel_to_ssa,
diff --git a/lib/compiler/test/Makefile b/lib/compiler/test/Makefile
index 43c7ccec7a..925f6ba969 100644
--- a/lib/compiler/test/Makefile
+++ b/lib/compiler/test/Makefile
@@ -12,6 +12,7 @@ MODULES= \
beam_bounds_SUITE \
beam_validator_SUITE \
beam_disasm_SUITE \
+ beam_doc_SUITE \
beam_except_SUITE \
beam_jump_SUITE \
beam_reorder_SUITE \
diff --git a/lib/compiler/test/beam_doc_SUITE.erl b/lib/compiler/test/beam_doc_SUITE.erl
new file mode 100644
index 0000000000..217698c13b
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE.erl
@@ -0,0 +1,603 @@
+
+-module(beam_doc_SUITE).
+-export([all/0, groups/0, init_per_group/2, end_per_group/2, singleton_moduledoc/1, singleton_doc/1,
+ docmodule_with_doc_attributes/1, hide_moduledoc/1, docformat/1,
+ singleton_docformat/1, singleton_meta/1, slogan/1,
+ types_and_opaques/1, callback/1, hide_moduledoc2/1,
+ private_types/1, export_all/1, equiv/1, spec/1, deprecated/1, warn_missing_doc/1,
+ doc_with_file/1, doc_with_file_error/1, all_string_formats/1,
+ docs_from_ast/1, spec_switch_order/1, user_defined_type/1, skip_doc/1]).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("kernel/include/eep48.hrl").
+-include_lib("stdlib/include/assert.hrl").
+
+-define(get_name(), atom_to_list(?FUNCTION_NAME)).
+
+all() ->
+ [{group, documentation_generation_tests}, doc_with_file].
+
+groups() ->
+ [{documentation_generation_tests, [parallel], documentation_generation_tests()}].
+
+init_per_group(_, Config) ->
+ Config.
+
+end_per_group(_, _Config) ->
+ ok.
+
+documentation_generation_tests() ->
+ [singleton_moduledoc,
+ singleton_doc,
+ docmodule_with_doc_attributes,
+ hide_moduledoc,
+ hide_moduledoc2,
+ docformat,
+ singleton_docformat,
+ singleton_meta,
+ slogan,
+ types_and_opaques,
+ callback,
+ private_types,
+ export_all,
+ equiv,
+ spec,
+ deprecated,
+ warn_missing_doc,
+ doc_with_file_error,
+ all_string_formats,
+ spec_switch_order,
+ docs_from_ast,
+ user_defined_type,
+ skip_doc
+ ].
+
+singleton_moduledoc(Conf) ->
+ ModuleName = "singletonmoduledoc",
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+
+ Mime = <<"text/markdown">>,
+ ModuleDoc = #{<<"en">> => <<"Moduledoc test module">>},
+ {ok, {docs_v1, _,_, Mime,ModuleDoc, _,_}} = code:get_doc(ModName),
+ ok.
+
+singleton_doc(Conf) ->
+ ModuleName = "singletondoc",
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ Mime = <<"text/markdown">>,
+ Doc = #{<<"en">> => <<"Doc test module">>},
+ FooDoc = #{<<"en">> => <<"Tests multi-clauses">>},
+ {ok, {docs_v1, 1,_, Mime, none, _,
+ [{{function, foo,1},_, [<<"foo(ok)">>], FooDoc, _},
+ {{function, main,0},_, [<<"main()">>], Doc, _}]}} = code:get_doc(ModName),
+ ok.
+
+docmodule_with_doc_attributes(Conf) ->
+ ModuleName = "docmodule_with_doc_attributes",
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ Mime = <<"text/markdown">>,
+ ModuleDoc = #{<<"en">> => <<"Moduledoc test module">>},
+ Doc = #{<<"en">> => <<"Doc test module">>},
+ FileDocs = #{<<"en">> => <<"# README\n\nThis is a test">>},
+ {ok, #docs_v1{ anno = ModuleAnno,
+ beam_language = erlang,
+ format = Mime,
+ module_doc = ModuleDoc,
+ metadata = #{},
+ docs = Docs
+ }} = code:get_doc(ModName),
+
+
+ [{{function,no_docs_multi,1},NoDocsMultiAnno,[<<"no_docs_multi/1">>],none,#{}},
+ {{function,with_file_docs,0},FileDocsAnno, [<<"with_file_docs()">>],FileDocs,#{}},
+ {{function,no_docs,0},NoDocsAnno, [<<"no_docs()">>],none,#{}},
+ {{function,ok,0}, OkAnno, [<<"ok()">>],none,#{authors := "Someone"}},
+ {{function, main,_},MainAnno, _, Doc, _}] = Docs,
+
+ ?assertEqual(5, erl_anno:line(ModuleAnno)),
+ ?assertEqual(10, erl_anno:line(MainAnno)),
+ ?assertEqual(18, erl_anno:line(OkAnno)),
+ ?assertEqual(21, erl_anno:line(NoDocsAnno)),
+ ?assertEqual(1, erl_anno:line(FileDocsAnno)),
+ ?assertEqual("README", filename:basename(erl_anno:file(FileDocsAnno))),
+ ?assertEqual(28, erl_anno:line(NoDocsMultiAnno)),
+
+ ok.
+
+hide_moduledoc(Conf) ->
+ {ok, ModName} = default_compile_file(Conf, "hide_moduledoc"),
+ {ok, {docs_v1, _,_, _Mime, hidden, _,
+ [{{function, main, 0}, _, [<<"main()">>],
+ #{ <<"en">> := <<"Doc test module">> }, #{}}]}} = code:get_doc(ModName),
+ ok.
+
+%% TODO: crashes
+hide_moduledoc2(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ {ok, {docs_v1, _,_, _Mime, hidden, _,
+ [{{function,handle_call,1},{19,2},[<<"handle_call/1">>],hidden,#{}},
+ {{function, main, 0}, _, [<<"main()">>], hidden, #{}}]}} = code:get_doc(ModName),
+ ok.
+
+docformat(Conf) ->
+ {ok, ModName} = default_compile_file(Conf, "docformat"),
+ ModuleDoc = #{<<"en">> => <<"Moduledoc test module">>},
+ Meta = #{format => "text/asciidoc",
+ deprecated => "Use something else",
+ otp_doc_vsn => {1,0,0},
+ since => "1.0"},
+ Doc = #{<<"en">> => <<"Doc test module">>},
+ {ok, {docs_v1, _,_, <<"text/asciidoc">>, ModuleDoc, Meta,
+ [{{function, main,_},_, _, Doc, _}]}} = code:get_doc(ModName),
+ ok.
+
+singleton_docformat(Conf) ->
+ {ok, ModName} = default_compile_file(Conf, "singleton_docformat"),
+ ModuleDoc = #{<<"en">> => <<"Moduledoc test module">>},
+ Meta = #{format => <<"text/asciidoc">>,
+ deprecated => "Use something else",
+ otp_doc_vsn => {1,0,0},
+ since => "1.0"},
+ Doc = #{<<"en">> => <<"Doc test module\n\nMore info here">>},
+ FunMeta = #{ authors => [<<"Beep Bop">>], equiv => <<"main/3">> },
+ {ok, {docs_v1, _,erlang, <<"text/asciidoc">>, ModuleDoc, Meta,
+ [{{function, main,0},_, [<<"main()">>], Doc, FunMeta}]}} = code:get_doc(ModName),
+ ok.
+
+singleton_meta(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ Meta = #{ authors => [<<"Beep Bop">>], equiv => <<"main/3">>},
+ DocMain1 = #{<<"en">> => <<"Returns always ok.">>},
+ {ok, {docs_v1, _,erlang, <<"text/markdown">>, none, _,
+ [{{function, main1,0},_, [<<"main1()">>], DocMain1, #{equiv := <<"main(_)">>}},
+ {{function, main,0},_, [<<"main()">>], none, Meta}]}}
+ = code:get_doc(ModName),
+ ok.
+
+slogan(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ Doc = #{<<"en">> => <<"Returns ok.">>},
+ BarDoc = #{ <<"en">> => <<"foo()\nNot a slogan since foo =/= bar">> },
+ NoSloganDoc = #{ <<"en">> => <<"Not a slogan\n\nTests slogans in multi-clause">>},
+ {ok, {docs_v1, _,_, _, none, _,
+ [Connect, MulticlauseSloganIgnored, SpecNoDocSlogan, NoDocSlogan,
+ Slogan2, Slogan1, NoSlogan, Bar, Main]}} = code:get_doc(ModName),
+
+ {{function,connect,2},_,
+ [<<"connect(TCPSocket, TLSOptions)">>],none,#{equiv := <<"connect/3">>,since := <<"OTP R14B">>}} = Connect,
+ {{function,spec_multiclause_slogan_ignored,1},_,[<<"spec_multiclause_slogan_ignored(X)">>],none,#{}} = MulticlauseSloganIgnored,
+ {{function, spec_no_doc_slogan, 1}, _, [<<"spec_no_doc_slogan(Y)">>], none, #{}} = SpecNoDocSlogan,
+ {{function, no_doc_slogan, 1}, _, [<<"no_doc_slogan(X)">>], none, #{}}= NoDocSlogan,
+ {{function, spec_slogan, 2}, _, [<<"spec_slogan(Y, Z)">>], _, #{}} = Slogan2,
+ {{function, spec_slogan, 1}, _, [<<"spec_slogan(Y)">>], _, #{}} = Slogan1,
+ {{function, no_slogan,1},_,[<<"no_slogan/1">>], NoSloganDoc, #{}} = NoSlogan,
+ {{function, bar,0},_,[<<"bar()">>], BarDoc, #{}} = Bar,
+ {{function, main,1},_,[<<"main(Foo)">>], Doc, #{}} = Main,
+ ok.
+
+types_and_opaques(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName, Warnings} = default_compile_file(Conf, ModuleName, [return_warnings]),
+ TypeDoc = #{<<"en">> => <<"Represents the name of a person.">>},
+ GenericsDoc = #{<<"en">> => <<"Tests generics">>},
+ OpaqueDoc = #{<<"en">> =>
+ <<"Represents the name of a person that cannot be named.">>},
+ MaybeOpaqueDoc = #{<<"en">> => <<"mmaybe(X) ::= nothing | X.\n\nRepresents a maybe type.">>},
+ MaybeMeta = #{ authors => "Someone else", exported => true },
+ NaturalNumberMeta = #{since => "1.0", equiv => <<"non_neg_integer/0">>, exported => true},
+
+ {ok, {docs_v1, _,_, _, none, _,
+ [%% Type Definitions
+ Public, Intermediate, HiddenNoWarnType, HiddenType, OtherPrivateType, MyPrivateType,
+ MyMap, StateEnter, CallbackMode,CallbackResult, EncodingFunc, Three,
+ Two, One, Hidden, HiddenFalse, MMaybe, Unnamed, Param,NatNumber, Name,
+ HiddenIncludedType,
+ %% Functions
+ UsesPublic, Ignore, MapFun, PrivateEncoding, Foo
+ ]}} = code:get_doc(ModName),
+
+ {{type,public,0},{128,2},[<<"public()">>],none,#{exported := true}} = Public,
+ {{type,intermediate,0},{127,2},[<<"intermediate()">>],none,#{exported := false}} = Intermediate,
+ {{type,hidden_nowarn_type,0},{123,2},[<<"hidden_nowarn_type()">>],hidden,#{exported := false}} = HiddenNoWarnType,
+ {{type,hidden_type,0},{120,2},[<<"hidden_type()">>],hidden,#{exported := false}} = HiddenType,
+ {{type,my_other_private_type,0},MyOtherPrivateTypeLine,
+ [<<"my_other_private_type()">>],none,#{exported := false}} = OtherPrivateType,
+ {{type,my_private_type,0},MyPrivateTypeLine,
+ [<<"my_private_type()">>],none,#{exported := false}} = MyPrivateType,
+ {{type,mymap,0},MyMapLine,[<<"mymap()">>],none,#{exported := false}} = MyMap,
+ {{type,state_enter,0},StateEnterLine,[<<"state_enter()">>],none,#{exported := false}}=StateEnter,
+ {{type,callback_mode,0},CallbackModeLine, [<<"callback_mode()">>],none,#{exported := false}} = CallbackMode,
+ {{type,callback_mode_result,0},CallbackResultLine,
+ [<<"callback_mode_result()">>],none,#{exported := true}} = CallbackResult,
+ {{type,encoding_func,0},_,[<<"encoding_func()">>],none,#{exported := false}} = EncodingFunc,
+ {{type,three,0},_,[<<"three()">>],none,#{exported := false}} = Three,
+ {{type,two,0},_,[<<"two()">>],none,#{exported := false}} = Two,
+ {{type,one,0},_,[<<"one()">>],none,#{exported := false}} = One,
+ {{type,hidden,0},_,[<<"hidden()">>],hidden,#{exported := true}} = Hidden,
+ {{type,hidden_false,0},_,[<<"hidden_false()">>],hidden,
+ #{exported := true, authors := "Someone else"}} = HiddenFalse,
+ {{type, mmaybe,1},_,[<<"mmaybe(X)">>], MaybeOpaqueDoc, MaybeMeta} = MMaybe,
+ {{type, unnamed,0},{30,2},[<<"unnamed()">>], OpaqueDoc,
+ #{equiv := <<"non_neg_integer()">>, exported := true}} = Unnamed,
+ {{type, param,1},_,[<<"param(X)">>], GenericsDoc,
+ #{equiv := <<"madeup()">>, exported := true}} = Param,
+ {{type, natural_number,0},_,[<<"natural_number()">>], none, NaturalNumberMeta} = NatNumber,
+ {{type, name,1},_,[<<"name(_)">>], TypeDoc, #{exported := true}} = Name,
+ {{type, hidden_included_type, 0}, _, _, hidden, #{exported := false }} = HiddenIncludedType,
+
+ {{function,uses_public,0},{131,1},[<<"uses_public()">>],none,#{}} = UsesPublic,
+ {{function,ignore_type_from_hidden_fun,0},_,[<<"ignore_type_from_hidden_fun()">>],hidden,#{}} = Ignore,
+ {{function,map_fun,0},_,[<<"map_fun()">>],none,#{}} = MapFun,
+ {{function,private_encoding_func,2},_,[<<"private_encoding_func(Data, Options)">>],none,#{}} = PrivateEncoding,
+ {{function,foo,0},_,[<<"foo()">>],none,#{}} = Foo,
+
+ ?assertEqual(106, erl_anno:line(MyOtherPrivateTypeLine)),
+ ?assertEqual(105, erl_anno:line(MyPrivateTypeLine)),
+ ?assertEqual(102, erl_anno:line(MyMapLine)),
+ ?assertEqual(99, erl_anno:line(StateEnterLine)),
+ ?assertEqual(98, erl_anno:line(CallbackModeLine)),
+ ?assertEqual(96, erl_anno:line(CallbackResultLine)),
+
+ [{File, Ws}, {HrlFile, HrlWs}] = Warnings,
+ ?assertEqual("types_and_opaques.erl", filename:basename(File)),
+ ?assertEqual({{120,2}, beam_doc,
+ {hidden_type_used_in_exported_fun,{hidden_type,0}}}, lists:nth(4, Ws)),
+
+ ?assertEqual("types_and_opaques.hrl", filename:basename(HrlFile)),
+ ?assertEqual({{1,2}, beam_doc,
+ {hidden_type_used_in_exported_fun,{hidden_included_type,0}}}, lists:nth(1, HrlWs)),
+
+ {ok, ModName, [_]} =
+ default_compile_file(Conf, ModuleName, [return_warnings, nowarn_hidden_doc, nowarn_unused_type]),
+
+ ok.
+
+callback(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName, [{File,Warnings}]} =
+ default_compile_file(Conf, ModuleName, [return_warnings, report]),
+ Doc = #{<<"en">> => <<"Callback fn that always returns ok.">>},
+ ImpCallback = #{<<"en">> => <<"This is a test">>},
+ FunctionDoc = #{<<"en">> => <<"all_ok()\n\nCalls all_ok/0">>},
+ ChangeOrder = #{<<"en">> => <<"Test changing order">>},
+ {ok, {docs_v1, _,_, _, none, _,
+ [{{callback,nowarn,1},{39,2},[<<"nowarn(Arg)">>],hidden,#{}},
+ {{callback,warn,0},{36,2},[<<"warn()">>],hidden,#{}},
+ {{callback,bounded,1},_,[<<"bounded(X)">>],none,#{}},
+ {{callback,multi,1},_,[<<"multi(Argument)">>],
+ #{ <<"en">> := <<"A multiclause callback with slogan docs">> },#{}},
+ {{callback,multi_no_slogan,1},_,[<<"multi_no_slogan/1">>],none,#{}},
+ {{callback,ann,1},_,[<<"ann(X)">>],none,#{}},
+ {{callback,param,1},_,[<<"param(X)">>],none,#{}},
+ {{callback, change_order,0},_,[<<"change_order()">>], ChangeOrder,
+ #{equiv := <<"ok()">>}},
+ {{callback, all_ok,0},_,[<<"all_ok()">>], Doc, #{}},
+ {{function, main2,0},_,[<<"main2()">>], #{<<"en">> := <<"Second main">>},
+ #{equiv := <<"main()">>}},
+ {{function, main,0},_,[<<"main()">>], FunctionDoc, #{}},
+ {{function, all_ok,0},_, [<<"all_ok()">>],ImpCallback,
+ #{equiv := <<"ok/0">>}}
+ ]}} = code:get_doc(ModName),
+
+ ?assertEqual("callback.erl", filename:basename(File)),
+ io:format("Warnings: ~p~n", [Warnings]),
+ ?assertEqual(1, length(Warnings)),
+ ?assertMatch({{36,2},beam_doc,{hidden_callback,{warn,0}}}, lists:nth(1, Warnings)),
+
+ {ok, ModName, []} =
+ default_compile_file(Conf, ModuleName, [return_warnings, report, nowarn_hidden_doc]),
+
+ ok.
+
+private_types(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+
+ {ok, {docs_v1, _,_, _, none, _,
+ [
+ %% Types
+ RemoteTypeT, TupleT, RecordAT, RecordInlineT,
+ MapValue2T, MapKey2T, MapValueT, MapKeyT,
+ FunRet2T, FunRetT, FunT, Complex, BoundedRetT,
+ ArgT, BoundedArgT, Private, HiddenExportT, PrivateCBT,
+ PublicT, PrivateT,
+ %% Callbacks
+ CBar,
+ %% Functions
+ Bounded, HiddenTypeExposed, Hidden, Bar]}} = code:get_doc(ModName),
+
+ ?assertMatch({{type,remote_type_t,1}, _, _, none, #{exported := false}},RemoteTypeT),
+ ?assertMatch({{type,tuple_t,0}, _, _, none, #{exported := false}},TupleT),
+ ?assertMatch({{type,record_a_t,0}, _, _, none, #{exported := false}},RecordAT),
+ ?assertMatch({{type,record_inline_t,0}, _, _, none, #{exported := false}},RecordInlineT),
+ ?assertMatch({{type,map_value_2_t,0}, _, _, none, #{exported := false}},MapValue2T),
+ ?assertMatch({{type,map_key_2_t,0}, _, _, none, #{exported := false}},MapKey2T),
+ ?assertMatch({{type,map_value_t,0}, _, _, none, #{exported := false}},MapValueT),
+ ?assertMatch({{type,map_key_t,0}, _, _, none, #{exported := false}},MapKeyT),
+ ?assertMatch({{type,fun_ret_2_t,0}, _, _, none, #{exported := false}},FunRet2T),
+ ?assertMatch({{type,fun_ret_t,0}, _, _, none, #{exported := false}},FunRetT),
+ ?assertMatch({{type,fun_t,0}, _, _, none, #{exported := false}},FunT),
+ ?assertMatch({{type,complex,1}, _, _, none, #{exported := true}},Complex),
+ ?assertMatch({{type,bounded_ret_t,0}, _, _, none, #{exported := false}},BoundedRetT),
+ ?assertMatch({{type,arg_t,0}, _, _, none, #{exported := false}},ArgT),
+ ?assertMatch({{type,bounded_arg_t,0}, _, _, none, #{exported := false}},BoundedArgT),
+ ?assertMatch({{type,private,0}, {28,2}, [<<"private()">>], hidden, #{exported := false}},Private),
+ ?assertMatch({{type,hidden_export_t,0},_,[<<"hidden_export_t()">>],hidden,#{exported := true}},HiddenExportT),
+ ?assertMatch({{type,private_cb_t,0},_,_,none,#{exported := false}},PrivateCBT),
+ ?assertMatch({{type,public_t,0},_, [<<"public_t()">>], none,#{ exported := true}},PublicT),
+ ?assertMatch({{type,private_t,0},_, [<<"private_t()">>], none,#{ exported := false}},PrivateT),
+ ?assertMatch({{callback,bar,1},_,_,none,#{}},CBar),
+ ?assertMatch({{function,bounded,2},_,_,none,#{}},Bounded),
+ ?assertMatch({{function,hidden_type_exposed,0},{32,1},[<<"hidden_type_exposed()">>],none,#{}},HiddenTypeExposed),
+ ?assertMatch({{function,hidden,0},_,[<<"hidden()">>],hidden,#{}},Hidden),
+ ?assertMatch({{function,bar,0},_,[<<"bar()">>],none,#{}},Bar),
+
+ ok.
+
+
+export_all(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ ImpCallback = #{<<"en">> => <<"This is a test">>},
+ FunctionDoc = #{<<"en">> => <<"all_ok()\n\nCalls all_ok/0">>},
+ {ok, {docs_v1, _,_, _, none, _,
+ [{{function, main2,0},_,[<<"main2()">>], #{<<"en">> := <<"Second main">>},
+ #{equiv := <<"main()">>}},
+ {{function, main,0},_,[<<"main()">>], FunctionDoc, #{}},
+ {{function, all_ok,0},_, [<<"all_ok()">>],ImpCallback,
+ #{equiv := <<"ok/0">>}}
+ ]}} = code:get_doc(ModName),
+ ok.
+
+equiv(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ {ok, {docs_v1, _,_, _, none, _,
+ [{{function, main, 2},_,[<<"main(A, B)">>], none,
+ #{ }},
+ {{function, main, 1},_,[<<"main(A)">>], none,
+ #{ equiv := <<"main(A, 1)">> }}
+ ]}} = code:get_doc(ModName),
+ ok.
+
+spec(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ {ok, {docs_v1, _,_, _, none, _,
+ [{{type,no,0},_,[<<"no()">>],none,#{exported := false}},
+ {{type,yes,0},_,[<<"yes()">>],none,#{exported := false}},
+ {{callback,me,1},_,[<<"me/1">>],none,#{}},
+ {{function,baz,1},_,[<<"baz(X)">>],none,#{}},
+ {{function,foo,1},_,[<<"foo(X)">>],none,#{}}]}} = code:get_doc(ModName),
+ ok.
+
+user_defined_type(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ {ok, {docs_v1, _,_, _, none, _, []}} = code:get_doc(ModName),
+ ok.
+
+deprecated(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+ {ok, {docs_v1, _,_, _, none, _,
+ [{{type,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"the type deprecated:test(_) is deprecated; Deprecation reason">>}},
+ {{type,test,0},_,[<<"test()">>],none,#{deprecated := <<"the type deprecated:test() is deprecated; see the documentation for details">>}},
+ {{callback,test,0},_,[<<"test()">>],none,#{deprecated := <<"Meta reason">>}},
+ {{function,test,2},_,[<<"test(N, M)">>],none,#{deprecated := <<"Meta reason">>}},
+ {{function,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"deprecated:test/1 is deprecated; Deprecation reason">>}},
+ {{function,test,0},_,[<<"test()">>],none,#{deprecated := <<"deprecated:test/0 is deprecated; see the documentation for details">>}}]}} =
+ code:get_doc(ModName),
+
+ {ok, ModName} = default_compile_file(Conf, ModuleName, [{d,'TEST_WILDCARD'},
+ {d, 'REASON', next_major_release}]),
+ {ok, {docs_v1, _,_, _, none, _,
+ [{{type,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"the type deprecated:test(_) is deprecated; see the documentation for details">>}},
+ {{type,test,0},_,[<<"test()">>],none,#{deprecated := <<"the type deprecated:test() is deprecated; see the documentation for details">>}},
+ {{callback,test,0},_,[<<"test()">>],none,#{deprecated := <<"Meta reason">>}},
+ {{function,test,2},_,[<<"test(N, M)">>],none,#{deprecated := <<"Meta reason">>}},
+ {{function,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"deprecated:test/1 is deprecated; will be removed in the next major release. See the documentation for details">>}},
+ {{function,test,0},_,[<<"test()">>],none,#{deprecated := <<"deprecated:test/0 is deprecated; see the documentation for details">>}}]}} =
+ code:get_doc(ModName),
+
+ {ok, ModName} = default_compile_file(Conf, ModuleName, [{d,'ALL_WILDCARD'},
+ {d,'REASON',next_version},
+ {d,'TREASON',eventually}]),
+ {ok, {docs_v1, _,_, _, none, _,
+ [{{type,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"the type deprecated:test(_) is deprecated; will be removed in a future release. See the documentation for details">>}},
+ {{type,test,0},_,[<<"test()">>],none,#{deprecated := <<"the type deprecated:test() is deprecated; see the documentation for details">>}},
+ {{callback,test,0},_,[<<"test()">>],none,#{deprecated := <<"Meta reason">>}},
+ {{function,test,2},_,[<<"test(N, M)">>],none,#{deprecated := <<"Meta reason">>}},
+ {{function,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"deprecated:test/1 is deprecated; will be removed in the next version. See the documentation for details">>}},
+ {{function,test,0},_,[<<"test()">>],none,#{deprecated := <<"deprecated:test/0 is deprecated; see the documentation for details">>}}]}} =
+ code:get_doc(ModName),
+ ok.
+
+warn_missing_doc(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName, [{File,Warnings}, {HrlFile, HrlWarnings}]} =
+ default_compile_file(Conf, ModuleName, [return_warnings, warn_missing_doc, report]),
+
+ {ok, {docs_v1, _,_, _, none, _,
+ [{{type,test,1},_,[<<"test(N)">>],none,_},
+ {{type,test,0},_,[<<"test()">>],none,_},
+ {{callback,test,0},_,[<<"test()">>],none,_},
+ {{function,test,1},_,[<<"test(N)">>],none,_},
+ {{function,test,0},_,[<<"test()">>],none,_},
+ {{function,test,2},_,[<<"test(N, M)">>],none,_}]}
+ } = code:get_doc(ModName),
+
+ ?assertEqual("warn_missing_doc.erl", filename:basename(File)),
+ ?assertEqual(6, length(Warnings)),
+ ?assertMatch({1, beam_doc, missing_moduledoc}, lists:nth(1, Warnings)),
+ ?assertMatch({{6,2}, beam_doc, {missing_doc, {type,test,0}}}, lists:nth(2, Warnings)),
+ ?assertMatch({{7,2}, beam_doc, {missing_doc, {type,test,1}}}, lists:nth(3, Warnings)),
+ ?assertMatch({{9,2}, beam_doc, {missing_doc, {callback,test,0}}}, lists:nth(4, Warnings)),
+ ?assertMatch({{13,1}, beam_doc, {missing_doc, {function,test,0}}}, lists:nth(5, Warnings)),
+ ?assertMatch({{14,1}, beam_doc, {missing_doc, {function,test,1}}}, lists:nth(6, Warnings)),
+
+ ?assertEqual("warn_missing_doc.hrl", filename:basename(HrlFile)),
+ ?assertEqual(1, length(HrlWarnings)),
+ ?assertMatch({{2,1}, beam_doc, {missing_doc, {function,test,2}}}, lists:nth(1, HrlWarnings)),
+
+ ok.
+
+doc_with_file(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, Cwd} = file:get_cwd(),
+ try
+ ok = file:set_cwd(proplists:get_value(data_dir, Conf)),
+ {ok, ModName} = default_compile_file(Conf, ModuleName, [{i, "./folder"}]),
+ {ok, {docs_v1, ModuleAnno,_, _, #{<<"en">> := <<"# README\n\nThis is a test">>}, _,
+ [{{type,bar,1},_,[<<"bar(X)">>],none,#{exported := false}},
+ {{type,foo,1},_,[<<"foo(X)">>],none,#{exported := true}},
+ {{type,private_type_exported,0},_,[<<"private_type_exported()">>],
+ #{<<"en">> := <<"# TYPES\n\nTest">>}, #{exported := false}},
+ {{function,main2,1},Main2Anno,[<<"main2(I)">>],
+ #{<<"en">> := <<"# File\n\ntesting fetching docs from other folders">>}, #{}},
+ {{function,main,1},_,[<<"main(Var)">>],
+ #{<<"en">> := <<"# Fun\n\nTest importing function">>},#{}}]}} = code:get_doc(ModName),
+
+ ?assertEqual(1, erl_anno:line(ModuleAnno)),
+ ?assertEqual(1, erl_anno:line(Main2Anno)),
+ ?assertEqual("./folder/FILE", erl_anno:file(Main2Anno)),
+ ok
+ after
+ ok = file:set_cwd(Cwd)
+ end.
+
+doc_with_file_error(Conf) ->
+ ModuleName = ?get_name(),
+ {error,
+ [{_,
+ [{{6,2},epp,{moduledoc,file,"doesnotexist"}},
+ {{8,2},epp,{doc,file,"doesnotexist"}},
+ {{11,2},epp,{doc,file,"doesnotexist"}}]}] = Errors, []} = default_compile_file(Conf, ModuleName),
+ [[Mod:format_error(Error) || {_Loc, Mod, Error} <- Errs] || {_File, Errs} <- Errors],
+ {error, _, []} = default_compile_file(Conf, ModuleName, [report]),
+ ok.
+
+all_string_formats(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+
+ {ok, {docs_v1, _ModuleAnno,_, _, #{<<"en">> := <<"Moduledoc test module">>}, _,
+ [
+ {{function,six,0},_,_, #{<<"en">> := <<"all_string_formats-all_string_formats">>}, #{}},
+ {{function,five,0},_,_, #{<<"en">> := <<"all_string_formats-Doc module">>}, #{}},
+ {{function,four,0},_,_, #{<<"en">> := <<"Doc test mödule"/utf8>>}, #{}},
+ {{function,three,0},_,_, #{<<"en">> := <<"Doctestmodule">>}, #{}},
+ {{function,two,0},_,_, #{<<"en">> := <<"Doc test module">>}, #{}},
+ {{function,one,0},_,_, #{<<"en">> := <<"Doc test module">>}, #{}}
+ ]}} = code:get_doc(ModName),
+ ok.
+
+spec_switch_order(Conf) ->
+ ModuleName = ?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName),
+
+ {ok, {docs_v1, _ModuleAnno,_, _, _, _,
+ [NotFalse, Other, Bar, Foo]}} = code:get_doc(ModName),
+ {{function,not_false,0}, {53,1}, [<<"not_false()">>], none,#{}} = NotFalse,
+ {{function,other,0},{37,2},[<<"other()">>],hidden,#{}} = Other,
+ {{function,bar,1},{31,2},[<<"bar(X)">>],hidden,#{}} = Bar,
+ {{function,foo,1}, {23, 2}, [<<"foo(Var)">>], #{ <<"en">> := <<"Foo does X">> }, #{}} = Foo.
+
+skip_doc(Conf) ->
+ ModuleName =?get_name(),
+ {ok, ModName} = default_compile_file(Conf, ModuleName, [no_docs]),
+
+ {ok,{docs_v1,0,erlang,<<"application/erlang+html">>,none,
+ #{generated := true,
+ otp_doc_vsn := {1,0,0}},
+ [{{function,main,0},{8,1},[<<"main/0">>],none,#{}},
+ {{function,foo,1},{16,1},[<<"foo/1">>],none,#{}}]}} = code:get_doc(ModName),
+
+ {ok, _ModName} = compile_file(Conf, ModuleName, [report, return_errors, no_docs]),
+ {error, missing} = code:get_doc(ModName),
+ ok.
+
+
+docs_from_ast(_Conf) ->
+ Code = """
+ -module(test).
+ -moduledoc "moduledoc".
+ -export([main/0]).
+ -doc "main".
+ main() -> ok.
+ """,
+
+ {ok, test, BeamCode} = compile:forms(scan_and_parse(Code),[debug_info]),
+ {ok, {test, [{documentation, Docs }]}} = beam_lib:chunks(BeamCode, [documentation]),
+
+ ?assertMatch(
+ #docs_v1{ module_doc = #{ <<"en">> := <<"moduledoc">> },
+ anno = 2,
+ docs = [{{function,main,0}, 4, _, #{ <<"en">> := <<"main">> }, _}]},
+ Docs),
+
+ check_no_doc_attributes(BeamCode),
+
+ {ok, test, BeamCodeWSource} = compile:forms(scan_and_parse(Code),[beam_docs, debug_info, {source, "test.erl"}]),
+ {ok, {test, [{documentation, DocsWSource }]}} = beam_lib:chunks(BeamCodeWSource, [documentation]),
+
+ ?assertMatch(
+ #docs_v1{ module_doc = #{ <<"en">> := <<"moduledoc">> },
+ anno = 2,
+ docs = [{{function,main,0}, 4,
+ _, #{ <<"en">> := <<"main">> }, _}]},
+ DocsWSource),
+ check_no_doc_attributes(BeamCodeWSource),
+ ok.
+
+scan_and_parse(Code) ->
+ {ok, Toks, _} = erl_scan:string(Code),
+ parse(Toks).
+
+parse([]) -> [];
+parse(Toks) ->
+ {Form, [Dot | Rest]} = lists:splitwith(fun(E) -> element(1,E) =/= dot end, Toks),
+ {ok, F} = erl_parse:parse_form(Form ++ [Dot]),
+ [F | parse(Rest)].
+
+compile_file(Conf, ModuleName, ExtraOpts) ->
+ ErlModName = ModuleName ++ ".erl",
+ Filename = filename:join(proplists:get_value(data_dir, Conf), ErlModName),
+ io:format("Compiling: ~ts~n~p~n",[Filename, ExtraOpts]),
+ case compile:file(Filename, ExtraOpts) of
+ Res when element(1, Res) =:= ok ->
+ ModName = element(2, Res),
+ case lists:search(fun (X) -> X =:= no_docs end, ExtraOpts) of
+ false when length(ExtraOpts) > 0 ->
+ check_no_doc_attributes(code:which(ModName)),
+ Res;
+ _ ->
+ Res
+ end;
+ Else ->
+ Else
+ end.
+
+default_compile_file(Conf, ModuleName) ->
+ default_compile_file(Conf, ModuleName, []).
+default_compile_file(Conf, ModuleName, ExtraOpts) ->
+ compile_file(Conf, ModuleName, [report, return_errors, debug_info] ++ ExtraOpts).
+
+
+%% Verify that all doc and moduledoc attributes are stripped from debug_info
+check_no_doc_attributes(Mod) ->
+ {ok, {_ModName,
+ [{debug_info,
+ {debug_info_v1,erl_abstract_code,
+ {AST, Opts}}}]}} = beam_lib:chunks(Mod, [debug_info]),
+ false = lists:search(
+ fun(E) ->
+ element(1,E) == attribute
+ andalso
+ (element(3,E) == doc orelse element(3,E) == moduledoc)
+ end, AST),
+ false = lists:member(no_docs, Opts),
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/FUN b/lib/compiler/test/beam_doc_SUITE_data/FUN
new file mode 100644
index 0000000000..ace17b0007
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/FUN
@@ -0,0 +1,3 @@
+# Fun
+
+Test importing function
diff --git a/lib/compiler/test/beam_doc_SUITE_data/README b/lib/compiler/test/beam_doc_SUITE_data/README
new file mode 100644
index 0000000000..935d425318
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/README
@@ -0,0 +1,3 @@
+# README
+
+This is a test
diff --git a/lib/compiler/test/beam_doc_SUITE_data/TYPES b/lib/compiler/test/beam_doc_SUITE_data/TYPES
new file mode 100644
index 0000000000..4b5f08ac26
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/TYPES
@@ -0,0 +1,3 @@
+# TYPES
+
+Test
diff --git a/lib/compiler/test/beam_doc_SUITE_data/all_string_formats.erl b/lib/compiler/test/beam_doc_SUITE_data/all_string_formats.erl
new file mode 100644
index 0000000000..c567fc046f
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/all_string_formats.erl
@@ -0,0 +1,20 @@
+-module(all_string_formats).
+
+-export([one/0,two/0,three/0,four/0,five/0,six/0]).
+
+-moduledoc """
+ Moduledoc test module
+ """.
+
+-doc ~S"Doc test module".
+one() -> ok.
+-doc ~B"Doc test module".
+two() -> ok.
+-doc <<"Doc","test","modul",$e>>.
+three() -> ok.
+-doc <<"Doc test mödule"/utf8>>.
+four() -> ok.
+-doc <<?MODULE_STRING, "-Doc module">>.
+five() -> ok.
+-doc ?MODULE_STRING "-" ?MODULE_STRING.
+six() -> ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/callback.erl b/lib/compiler/test/beam_doc_SUITE_data/callback.erl
new file mode 100644
index 0000000000..1f0bf1aae0
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/callback.erl
@@ -0,0 +1,69 @@
+-module(callback).
+
+%% -doc "
+%% This should be ignored
+%% ".
+%% -behaviour(gen_server).
+
+-export([all_ok/0, main/0, main2/0]).
+
+-doc "
+Callback fn that always returns ok.
+".
+-callback all_ok() -> ok.
+
+-doc "
+Test changing order
+".
+-doc #{equiv => ok()}.
+-callback change_order() -> Order :: boolean().
+
+-callback param(X) -> X.
+-callback ann(X :: integer()) -> Y :: integer().
+
+-callback multi_no_slogan(X :: integer()) -> X :: integer();
+ (X :: atom()) -> X :: atom().
+
+
+-doc "multi(Argument)
+
+A multiclause callback with slogan docs".
+-callback multi(X :: integer()) -> X :: integer();
+ (X :: atom()) -> X :: atom().
+
+-callback bounded(X) -> integer() when X :: integer().
+
+-doc hidden.
+-callback warn() -> ok.
+
+-doc hidden.
+-compile({nowarn_hidden_doc, nowarn/1}).
+-callback nowarn(Arg :: atom()) -> ok.
+
+
+-doc #{equiv => ok/0}.
+-doc "
+This is a test
+".
+all_ok() ->
+ all_ok().
+
+-doc #{equiv => main()}.
+-spec main() -> ok.
+-doc "
+all_ok()
+
+Calls all_ok/0
+".
+main() ->
+ all_ok().
+
+-doc #{equiv => main()}.
+-doc "
+main2()
+
+Second main
+".
+-spec main2() -> ok.
+main2() ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/deprecated.erl b/lib/compiler/test/beam_doc_SUITE_data/deprecated.erl
new file mode 100644
index 0000000000..a2edfec1b0
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/deprecated.erl
@@ -0,0 +1,46 @@
+-module(deprecated).
+
+-export([test/0, test/1, test/2]).
+-export_type([test/0, test/1]).
+
+-ifndef(REASON).
+-define(REASON,"Deprecation reason").
+-endif.
+
+-ifndef(TREASON).
+-define(TREASON,"Deprecation reason").
+-endif.
+
+-doc #{ deprecated => "Meta reason" }.
+-callback test() -> ok.
+
+-ifdef(TEST_WILDCARD).
+-deprecated({test, '_', ?REASON}).
+-else.
+-ifdef(ALL_WILDCARD).
+-deprecated({'_', '_', ?REASON}).
+-else.
+-deprecated({test, 1, ?REASON}).
+-endif.
+-endif.
+-deprecated({test, 0}).
+
+-ifdef(TEST_WILDCARD).
+-deprecated_type({test, '_'}).
+-else.
+-ifdef(ALL_WILDCARD).
+-deprecated_type({'_', '_', ?TREASON}).
+-else.
+-deprecated_type({test, 1, ?TREASON}).
+-endif.
+-endif.
+-deprecated_type({test, 0}).
+-type test() :: ok.
+-type test(N) :: N.
+
+test() -> ok.
+test(N) -> N.
+
+-doc #{ deprecated => "Meta reason" }.
+test(N,M) -> N + M.
+
diff --git a/lib/compiler/test/beam_doc_SUITE_data/doc_with_file.erl b/lib/compiler/test/beam_doc_SUITE_data/doc_with_file.erl
new file mode 100644
index 0000000000..ff4c0040a8
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/doc_with_file.erl
@@ -0,0 +1,25 @@
+-module(doc_with_file).
+
+-export([main/1, main2/1]).
+-export_type([foo/1]).
+
+-moduledoc {file, "README"}.
+
+-doc {file, "TYPES"}.
+-type private_type_exported() :: integer().
+
+-doc {file, "FUN"}.
+-spec main(Var) -> foo(Var).
+main(X) ->
+ X.
+
+-include("doc_with_file.hrl").
+-spec main2( I :: integer()) -> bar(I :: integer()).
+main2(X) when is_atom(X) ->
+ X;
+main2(X) ->
+ X.
+
+
+-type foo(X) :: { X, private_type_exported()}.
+-type bar(X) :: foo({X, private_type_exported()}).
diff --git a/lib/compiler/test/beam_doc_SUITE_data/doc_with_file_error.erl b/lib/compiler/test/beam_doc_SUITE_data/doc_with_file_error.erl
new file mode 100644
index 0000000000..baa7034cc6
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/doc_with_file_error.erl
@@ -0,0 +1,24 @@
+-module(doc_with_file_error).
+
+-export([main/1, main2/1]).
+-export_type([foo/1]).
+
+-moduledoc {file, "doesnotexist"}.
+
+-doc {file, "doesnotexist"}.
+-type private_type_exported() :: integer().
+
+-doc {file, "doesnotexist"}.
+-spec main(Var) -> foo(Var).
+main(X) ->
+ X.
+
+-doc(({file, "folder/doesnotexist"})).
+-spec main2( I :: integer()) -> bar(I :: integer()).
+main2(X) when is_atom(X) ->
+ X;
+main2(X) ->
+ X.
+
+-type foo(X) :: { X, private_type_exported()}.
+-type bar(X) :: foo({X, private_type_exported()}).
diff --git a/lib/compiler/test/beam_doc_SUITE_data/docformat.erl b/lib/compiler/test/beam_doc_SUITE_data/docformat.erl
new file mode 100644
index 0000000000..40b1bd3762
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/docformat.erl
@@ -0,0 +1,18 @@
+-module(docformat).
+
+-export([main/0]).
+
+
+-moduledoc #{since => "1.0"}.
+-moduledoc #{deprecated => "Use something else"}.
+-moduledoc #{format => "text/asciidoc"}.
+-moduledoc "
+Moduledoc test module
+".
+
+
+-doc "
+Doc test module
+".
+main() ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/docmodule_with_doc_attributes.erl b/lib/compiler/test/beam_doc_SUITE_data/docmodule_with_doc_attributes.erl
new file mode 100644
index 0000000000..7d1dbe1d4d
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/docmodule_with_doc_attributes.erl
@@ -0,0 +1,36 @@
+-module(docmodule_with_doc_attributes).
+
+-export([main/0, ok/0, no_docs/0, no_docs_multi/1, with_file_docs/0]).
+
+-moduledoc "
+Moduledoc test module
+".
+
+
+-doc "
+Doc test module
+".
+main() ->
+ ok().
+
+
+-doc #{authors => "Someone"}.
+ok() ->
+ no_docs().
+
+no_docs() ->
+ ok.
+
+-doc {file, "README"}.
+with_file_docs() ->
+ ok.
+
+no_docs_multi(a) ->
+ a;
+no_docs_multi(A) ->
+ private_multi(A).
+
+private_multi(a) ->
+ a;
+private_multi(A) ->
+ A.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/equiv.erl b/lib/compiler/test/beam_doc_SUITE_data/equiv.erl
new file mode 100644
index 0000000000..a111066356
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/equiv.erl
@@ -0,0 +1,10 @@
+-module(equiv).
+
+-export([main/1, main/2]).
+
+-doc #{ equiv => main(A, 1) }.
+main(A) ->
+ main(A, 1).
+
+main(A, B) ->
+ {A, B}.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/export_all.erl b/lib/compiler/test/beam_doc_SUITE_data/export_all.erl
new file mode 100644
index 0000000000..13d4e7d39c
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/export_all.erl
@@ -0,0 +1,31 @@
+-module(export_all).
+
+-compile(export_all).
+
+
+-doc #{equiv => ok/0}.
+-doc "
+This is a test
+".
+all_ok() ->
+ all_ok().
+
+-doc #{equiv => main()}.
+-spec main() -> ok.
+-doc "
+all_ok()
+
+Calls all_ok/0
+".
+main() ->
+ all_ok().
+
+-doc #{equiv => main()}.
+-doc "
+main2()
+
+Second main
+".
+-spec main2() -> ok.
+main2() ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/folder/FILE b/lib/compiler/test/beam_doc_SUITE_data/folder/FILE
new file mode 100644
index 0000000000..efb71e584a
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/folder/FILE
@@ -0,0 +1,3 @@
+# File
+
+testing fetching docs from other folders
diff --git a/lib/compiler/test/beam_doc_SUITE_data/folder/doc_with_file.hrl b/lib/compiler/test/beam_doc_SUITE_data/folder/doc_with_file.hrl
new file mode 100644
index 0000000000..a9d40b48df
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/folder/doc_with_file.hrl
@@ -0,0 +1 @@
+-doc({file, "FILE"}).
diff --git a/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc.erl b/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc.erl
new file mode 100644
index 0000000000..2d02658e24
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc.erl
@@ -0,0 +1,15 @@
+-module(hide_moduledoc).
+
+-export([main/0]).
+
+-moduledoc false.
+
+-doc "
+Doc test module
+".
+main() ->
+ ok().
+
+-doc #{since => "1.0"}.
+ok() ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc2.erl b/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc2.erl
new file mode 100644
index 0000000000..12bea08d52
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc2.erl
@@ -0,0 +1,25 @@
+-module(hide_moduledoc2).
+
+-export([main/0, handle_call/1]).
+
+-moduledoc hidden.
+
+-doc "
+Doc test module
+".
+-doc hidden.
+main() ->
+ ok().
+
+-doc #{since => "1.0"}.
+-doc hidden.
+ok() ->
+ ok.
+
+-doc false.
+-spec handle_call('which' | {'add',atom()} | {'delete',atom()}) ->
+ {'reply', 'ok' | [atom()]}.
+
+handle_call({add,_Address}=A) -> A;
+handle_call({delete,_Address}=D) -> D;
+handle_call(which) -> which.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/private_types.erl b/lib/compiler/test/beam_doc_SUITE_data/private_types.erl
new file mode 100644
index 0000000000..a490dc29f1
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/private_types.erl
@@ -0,0 +1,65 @@
+-module(private_types).
+
+-export([bar/0, hidden/0, hidden_type_exposed/0, bounded/2]).
+-export_type([public_t/0, hidden_export_t/0, complex/1]).
+
+-type private_t() :: integer(). %% In chunk because referred to by exported bar/0
+-type public_t() :: integer(). %% In chunk because exported
+-type private_cb_t() :: integer(). %% In chunk because referred to by callback
+-type local_t() :: integer(). %% Not in chunk because only referred by non-exported function
+
+-doc false.
+-type hidden_export_t() :: integer(). %% In chunk because exported
+
+-callback bar(private_cb_t()) -> ok.
+
+-spec bar() -> private_t().
+bar() -> baz().
+
+-spec baz() -> local_t().
+baz() -> 1.
+
+-type hidden_t() :: integer(). %% Not in chunk because only referred to by hidden function
+
+-doc false.
+-spec hidden() -> hidden_t().
+hidden() -> 1.
+
+-doc false.
+-type private() :: integer().
+
+-spec hidden_type_exposed() -> private().
+hidden_type_exposed() -> 1.
+
+-type bounded_arg_t() :: integer().
+-type arg_t() :: integer().
+-type bounded_ret_t() :: integer().
+-spec bounded(A :: arg_t(), B) -> C
+ when B :: bounded_arg_t(),
+ C :: bounded_ret_t().
+bounded(A, B) -> A + B.
+
+-record(r,{ a :: record_a_t(), f :: record_f_t(), rec :: #r{} }). %% We have a recusive type to make sure we handle that
+-type complex(A) ::
+ [fun((fun_t()) -> fun_ret_t()) |
+ fun((...) -> fun_ret_2_t()) |
+ #{ map_key_t() := map_value_t(),
+ map_key_2_t() => map_value_2_t() } |
+ #r{ f :: record_inline_t() } |
+ {tuple_t()} |
+ maps:iterator_order(remote_type_t(A)) |
+ [] | 1 .. 2
+ ].
+
+-type fun_t() :: integer().
+-type fun_ret_t() :: integer().
+-type fun_ret_2_t() :: integer().
+-type map_key_t() :: integer().
+-type map_value_t() :: integer().
+-type map_key_2_t() :: integer().
+-type map_value_2_t() :: integer().
+-type record_inline_t() :: integer().
+-type record_a_t() :: integer().
+-type record_f_t() :: integer(). %% Should not be included as #r{ f } overrides it
+-type tuple_t() :: integer().
+-type remote_type_t(A) :: A.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/singleton_docformat.erl b/lib/compiler/test/beam_doc_SUITE_data/singleton_docformat.erl
new file mode 100644
index 0000000000..7aab513844
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/singleton_docformat.erl
@@ -0,0 +1,21 @@
+-module(singleton_docformat).
+
+-export([main/0]).
+
+-moduledoc #{format => <<"text/asciidoc">>,
+ since => "1.0",
+ deprecated => "Use something else"}.
+-moduledoc "
+Moduledoc test module
+".
+
+
+-doc #{ authors => [<<"Beep Bop">>] }.
+-doc #{ equiv => main/3 }.
+-doc "
+Doc test module
+
+More info here
+".
+main() ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/singleton_meta.erl b/lib/compiler/test/beam_doc_SUITE_data/singleton_meta.erl
new file mode 100644
index 0000000000..aa4bb6fb30
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/singleton_meta.erl
@@ -0,0 +1,17 @@
+-module(singleton_meta).
+
+-export([main/0, main1/0]).
+
+-doc #{ authors => [<<"Beep Bop">>] }.
+-doc #{ equiv => main/3 }.
+main() ->
+ main1().
+
+-doc (#{ equiv => main(_) }).
+-doc "
+main1()
+
+Returns always ok.
+".
+main1() ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/singletondoc.erl b/lib/compiler/test/beam_doc_SUITE_data/singletondoc.erl
new file mode 100644
index 0000000000..ed5b349a9d
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/singletondoc.erl
@@ -0,0 +1,19 @@
+-module(singletondoc).
+
+-export([main/0, foo/1]).
+
+-doc "
+Doc test module
+".
+main() ->
+ ok.
+
+-doc "
+foo(ok)
+
+Tests multi-clauses
+".
+foo(X) when is_atom(X) ->
+ X;
+foo(_) ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/singletonmoduledoc.erl b/lib/compiler/test/beam_doc_SUITE_data/singletonmoduledoc.erl
new file mode 100644
index 0000000000..cd81012d08
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/singletonmoduledoc.erl
@@ -0,0 +1,7 @@
+-module(singletonmoduledoc).
+
+-export([]).
+
+-moduledoc "
+Moduledoc test module
+".
diff --git a/lib/compiler/test/beam_doc_SUITE_data/skip_doc.erl b/lib/compiler/test/beam_doc_SUITE_data/skip_doc.erl
new file mode 100644
index 0000000000..afb0323104
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/skip_doc.erl
@@ -0,0 +1,19 @@
+-module(skip_doc).
+
+-export([main/0, foo/1]).
+
+-doc "
+Doc test module
+".
+main() ->
+ ok.
+
+-doc "
+foo(ok)
+
+Tests multi-clauses
+".
+foo(X) when is_atom(X) ->
+ X;
+foo(_) ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/slogan.erl b/lib/compiler/test/beam_doc_SUITE_data/slogan.erl
new file mode 100644
index 0000000000..45f5aca908
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/slogan.erl
@@ -0,0 +1,73 @@
+-module(slogan).
+
+-export([main/1,
+ bar/0,
+ no_slogan/1,
+ spec_slogan/1,
+ spec_slogan/2,
+ no_doc_slogan/1,
+ spec_no_doc_slogan/1,
+ spec_multiclause_slogan_ignored/1,
+ connect/2
+ ]).
+
+-doc "
+main(Foo)
+
+Returns ok.
+".
+-spec main(X :: integer()) -> ok.
+main(_X) ->
+ ok.
+
+-doc "
+foo()
+Not a slogan since foo =/= bar
+".
+bar() ->
+ ok.
+
+-doc "
+Not a slogan
+
+Tests slogans in multi-clause
+".
+-spec no_slogan(atom()) -> atom();
+ (term()) -> ok.
+no_slogan(X) when is_atom(X) ->
+ X;
+no_slogan(_X) ->
+ ok.
+
+-spec spec_slogan(Y :: integer()) -> integer() | ok.
+-doc "Not a slogan".
+spec_slogan(_X) -> ok.
+
+-spec spec_slogan(Y :: integer(), Z :: integer()) -> integer() | ok.
+-doc "Not a slogan".
+spec_slogan(_X, _Y) -> _X + _Y.
+
+no_doc_slogan(X) -> X.
+
+-spec spec_no_doc_slogan(Y) -> Y.
+spec_no_doc_slogan(X) ->
+ X.
+
+
+-spec spec_multiclause_slogan_ignored(Y) -> Y;
+ (Z) -> Z when Z :: integer().
+spec_multiclause_slogan_ignored(X) ->
+ X.
+
+
+-doc(#{equiv => connect/3}).
+-doc(#{since => <<"OTP R14B">>}).
+-spec connect(TCPSocket, TLSOptions) ->
+ {ok, sslsocket} |
+ {error, reason} |
+ {option_not_a_key_value_tuple, any()} when
+ TCPSocket :: socket,
+ TLSOptions :: [tls_client_option].
+
+connect(Socket, SslOptions) ->
+ {Socket, SslOptions}.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/spec.erl b/lib/compiler/test/beam_doc_SUITE_data/spec.erl
new file mode 100644
index 0000000000..df0331b1ce
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/spec.erl
@@ -0,0 +1,24 @@
+-module(spec).
+
+-type yes() :: integer().
+-type no() :: atom().
+
+-export([foo/1, baz/1]).
+
+-record(name, {field = undefined :: atom()}).
+
+-callback me(X :: yes()) -> no();
+ (no()) -> yes();
+ (term()) -> #name{ }.
+
+-spec spec:foo(yes()) -> {yes(), yes()} | yes();
+ (no()) -> no().
+foo(X) ->
+ _ = #name{field = none},
+ X.
+
+
+-spec baz(Z) -> Z when Z :: yes();
+ (no()) -> no().
+baz(X) ->
+ X.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/spec_switch_order.erl b/lib/compiler/test/beam_doc_SUITE_data/spec_switch_order.erl
new file mode 100644
index 0000000000..9aa9262474
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/spec_switch_order.erl
@@ -0,0 +1,54 @@
+-module(spec_switch_order).
+
+-export([foo/1, bar/1, other/0, not_false/0]).
+
+-doc hidden.
+-spec foo(integer()) -> mytype().
+
+
+
+-doc "
+bar(Var)
+".
+-spec bar(integer()) -> another_type().
+
+
+
+
+-type mytype() :: ok.
+-type another_type() :: ok.
+
+
+
+-doc "
+foo(Var)
+
+Foo does X
+".
+foo(_X) ->
+ ok.
+
+-doc hidden.
+bar(_X) ->
+ ok.
+
+
+%% Ordering issue
+-doc false.
+-spec other() -> ok.
+%% docs #{ {other, 0} => {hidden, none, #{}}
+%% need to reset fields
+
+-spec not_false() -> ok.
+%% create entry with current fields
+%% reset state
+
+-doc #{}.
+other() ->
+ ok.
+%% fetch or create entry with for other
+%% update unset fields
+%% we need to differenciate between unset and set with default
+
+not_false() ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.erl b/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.erl
new file mode 100644
index 0000000000..4512f66a4d
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.erl
@@ -0,0 +1,147 @@
+-module(types_and_opaques).
+
+-export([foo/0, private_encoding_func/2,map_fun/0,ignore_type_from_hidden_fun/0]).
+
+-export_type([name/1,unnamed/0, mmaybe/1, callback_mode_result/0]).
+
+-export([uses_public/0]).
+-export_type([public/0]).
+
+-include("types_and_opaques.hrl").
+
+-doc "
+name(_)
+
+Represents the name of a person.
+".
+-type name(_Ignored) :: string().
+
+-doc #{since => "1.0"}.
+-doc #{equiv => non_neg_integer/0}.
+-type natural_number() :: non_neg_integer().
+
+-doc "
+Tests generics
+".
+-doc #{equiv => madeup()}.
+-type param(X) :: {X, integer(), Y :: string()}.
+
+-doc #{equiv => non_neg_integer()}.
+-doc "
+unnamed()
+
+Represents the name of a person that cannot be named.
+".
+-opaque unnamed() :: name(integer()).
+
+
+
+-export_type([natural_number/0, param/1]).
+
+
+
+-doc #{ authors => "Someone else" }.
+-doc "
+mmaybe(X) ::= nothing | X.
+
+Represents a maybe type.
+".
+-opaque mmaybe(X) :: nothing | X.
+
+-opaque non_exported() :: atom().
+
+-type not_exported_either() :: atom().
+
+-doc hidden.
+-doc #{ authors => "Someone else" }.
+-type hidden_false() :: atom().
+
+-doc false.
+-doc "
+Here is ok.
+".
+-type hidden() :: hidden_false().
+
+
+
+-export_type([hidden_false/0, hidden/0]).
+
+
+
+-type one() :: 1.
+-type two() :: one().
+-type three() :: two().
+-type four() :: three().
+
+-spec foo() -> three().
+foo() -> 1.
+
+
+-type encoding_func() :: fun((non_neg_integer()) -> boolean()).
+
+-spec private_encoding_func(Data, Options) -> AbsTerm when
+ Data :: term(),
+ Options :: Location | [Option],
+ Option :: {encoding, Encoding}
+ | {line, Line}
+ | {location, Location},
+ Encoding :: 'latin1' | 'unicode' | 'utf8' | 'none' | encoding_func(),
+ Line :: erl_anno:line(),
+ Location :: erl_anno:location(),
+ AbsTerm :: term().
+private_encoding_func(_, _) ->
+ ok.
+
+
+-type callback_mode_result() ::
+ callback_mode() | [callback_mode() | state_enter()].
+-type callback_mode() :: 'state_functions' | 'handle_event_function'.
+-type state_enter() :: 'state_enter'.
+
+
+-type mymap() :: #{ foo => my_private_type(),
+ bar := my_other_private_type()}.
+
+-type my_private_type() :: integer().
+-type my_other_private_type() :: non_neg_integer().
+
+-spec map_fun() -> mymap().
+map_fun() ->
+ ok.
+
+
+-doc false.
+-spec ignore_type_from_hidden_fun() -> four().
+ignore_type_from_hidden_fun() ->
+ ok.
+
+%% Type below should be a warning, since it is refered
+%% by a public function or type and the inner type is hidden.
+-doc false.
+-type hidden_type() :: integer().
+%% Test suppression of hidden type warning.
+-doc false.
+-type hidden_nowarn_type() :: integer().
+-compile({nowarn_hidden_doc, [hidden_nowarn_type/0]}).
+
+-type intermediate() :: hidden_type() | hidden_included_type() | hidden_nowarn_type().
+-type public() :: intermediate().
+
+-spec uses_public() -> public().
+uses_public() ->
+ qux().
+
+-doc false.
+-doc "
+Hidden function with doc attribute
+".
+qux() ->
+ qux2().
+
+
+-doc "
+Hidden function with doc attribute
+".
+-doc false.
+qux2() ->
+ ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.hrl b/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.hrl
new file mode 100644
index 0000000000..eda452edab
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.hrl
@@ -0,0 +1,2 @@
+-doc false.
+-type hidden_included_type() :: integer().
diff --git a/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.erl b/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.erl
new file mode 100644
index 0000000000..6d5b745b39
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.erl
@@ -0,0 +1,3 @@
+-module(user_defined_type).
+
+-include("user_defined_type.hrl").
diff --git a/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.hrl b/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.hrl
new file mode 100644
index 0000000000..63356724c9
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.hrl
@@ -0,0 +1,2 @@
+-type foo() :: integer().
+-type foo_dependent() :: foo().
diff --git a/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.erl b/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.erl
new file mode 100644
index 0000000000..d2b6c8f38f
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.erl
@@ -0,0 +1,14 @@
+-module(warn_missing_doc).
+
+-export([test/0, test/1, test/2]).
+-export_type([test/0, test/1]).
+
+-type test() :: ok.
+-type test(N) :: N.
+
+-callback test() -> ok.
+
+-include("warn_missing_doc.hrl").
+
+test() -> ok.
+test(N) -> N.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.hrl b/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.hrl
new file mode 100644
index 0000000000..5b702b9078
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.hrl
@@ -0,0 +1,2 @@
+-doc #{ }.
+test(N,M) -> N + M.
diff --git a/lib/compiler/test/compile_SUITE.erl b/lib/compiler/test/compile_SUITE.erl
index 2c6f260618..8093b731c3 100644
--- a/lib/compiler/test/compile_SUITE.erl
+++ b/lib/compiler/test/compile_SUITE.erl
@@ -408,15 +408,22 @@ makedep(Config) when is_list(Config) ->
%% Generate dependencies and compile normally at the same time.
GeneratedHrl = filename:join(PrivDir, "generated.hrl"),
- ok = file:write_file(GeneratedHrl, ""),
- {ok,simple} = compile:file(Simple, [report,makedep_side_effect,
- {makedep_output,Target},
- {i,PrivDir}|IncludeOptions]),
- {ok,Mf9} = file:read_file(Target),
- BasicMf3 = iolist_to_binary([string:trim(BasicMf2), " ", filename:join(PrivDir, "generated.hrl"), "\n"]),
- BasicMf3 = makedep_canonicalize_result(Mf9, DataDir),
- error = compile:file(Simple, [report,makedep_side_effect,
- {makedep_output,PrivDir}|IncludeOptions]),
+ GeneratedDoc = filename:join(proplists:get_value(data_dir, Config), "foo.md"),
+ try
+ ok = file:write_file(GeneratedHrl, ""),
+ ok = file:write_file(GeneratedDoc, ""),
+ {ok,simple} = compile:file(Simple, [report,makedep_side_effect,
+ {makedep_output,Target},
+ {i,PrivDir}|IncludeOptions]),
+ {ok,Mf9} = file:read_file(Target),
+ BasicMf3 = iolist_to_binary([string:trim(BasicMf2), " $(srcdir)/foo.md ", filename:join(PrivDir, "generated.hrl"), "\n"]),
+ BasicMf3 = makedep_canonicalize_result(Mf9, DataDir),
+ error = compile:file(Simple, [report,makedep_side_effect,
+ {makedep_output,PrivDir}|IncludeOptions])
+ after
+ ok = file:delete(GeneratedHrl),
+ ok = file:delete(GeneratedDoc)
+ end,
%% Cover generation of long lines that must be split.
CompileModule = filename:join(code:lib_dir(compiler), "src/compile.erl"),
@@ -432,7 +439,6 @@ makedep(Config) when is_list(Config) ->
error = compile:file(Simple, [report,makedep,{makedep_output,a_bad_output_device}]),
ok = file:delete(Target),
- ok = file:delete(GeneratedHrl),
ok = file:del_dir(filename:dirname(Target)),
ok.
@@ -707,7 +713,6 @@ encrypted_abstr_1(Simple, Target) ->
erpc:call(
Node,
fun() ->
- {ok,OldCwd} = file:get_cwd(),
ok = file:set_cwd(TargetDir),
error = compile:file(Simple, [encrypt_debug_info,report]),
diff --git a/lib/compiler/test/compile_SUITE_data/simple-basic1.mk b/lib/compiler/test/compile_SUITE_data/simple-basic1.mk
index 4073fa82d0..53bec35f96 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-basic1.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-basic1.mk
@@ -1 +1 @@
-simple.beam: $(srcdir)/simple.erl
+simple.beam: $(srcdir)/simple.erl $(srcdir)/unicode-0.md
diff --git a/lib/compiler/test/compile_SUITE_data/simple-basic2.mk b/lib/compiler/test/compile_SUITE_data/simple-basic2.mk
index 761d1d9582..98406b8083 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-basic2.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-basic2.mk
@@ -1 +1 @@
-simple.beam: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl
+simple.beam: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl
diff --git a/lib/compiler/test/compile_SUITE_data/simple-missing.mk b/lib/compiler/test/compile_SUITE_data/simple-missing.mk
index b13d44ec36..38fccfd82e 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-missing.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-missing.mk
@@ -1 +1 @@
-simple.beam: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl generated.hrl
+simple.beam: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl generated.hrl
diff --git a/lib/compiler/test/compile_SUITE_data/simple-phony.mk b/lib/compiler/test/compile_SUITE_data/simple-phony.mk
index c84bcedd2a..fb049c1db7 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-phony.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-phony.mk
@@ -1,3 +1,5 @@
-simple.beam: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl
+simple.beam: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl
+
+$(srcdir)/unicode-0.md:
$(srcdir)/include/simple.hrl:
diff --git a/lib/compiler/test/compile_SUITE_data/simple-target1.mk b/lib/compiler/test/compile_SUITE_data/simple-target1.mk
index dd9fa0d6e5..8b31c4c696 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-target1.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-target1.mk
@@ -1 +1 @@
-$target: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl
+$target: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl
diff --git a/lib/compiler/test/compile_SUITE_data/simple-target2.mk b/lib/compiler/test/compile_SUITE_data/simple-target2.mk
index a5fc6f461d..f41b9a066c 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-target2.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-target2.mk
@@ -1 +1 @@
-$$target: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl
+$$target: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl
diff --git a/lib/compiler/test/compile_SUITE_data/simple.erl b/lib/compiler/test/compile_SUITE_data/simple.erl
index 9385d101e0..5129e6e204 100644
--- a/lib/compiler/test/compile_SUITE_data/simple.erl
+++ b/lib/compiler/test/compile_SUITE_data/simple.erl
@@ -28,6 +28,7 @@
test() ->
passed.
+-doc {file, "unicode-0.md"}.
unicode() ->
{"это",'спутник'}.
@@ -37,6 +38,9 @@ unicode() ->
-ifdef(need_foo).
-include("simple.hrl").
+-ifdef(include_generated).
+-doc {file, "foo.md"}.
+-endif.
foo() ->
{?included_value, ?foo_value}.
diff --git a/lib/compiler/test/compile_SUITE_data/unicode-0.md b/lib/compiler/test/compile_SUITE_data/unicode-0.md
new file mode 100644
index 0000000000..b95d9f74a1
--- /dev/null
+++ b/lib/compiler/test/compile_SUITE_data/unicode-0.md
@@ -0,0 +1 @@
+Small test docs
diff --git a/lib/erl_docgen/priv/bin/validate_links.escript b/lib/erl_docgen/priv/bin/validate_links.escript
index d1bccfa3fc..0ea9b3730c 100755
--- a/lib/erl_docgen/priv/bin/validate_links.escript
+++ b/lib/erl_docgen/priv/bin/validate_links.escript
@@ -210,13 +210,12 @@ parse_mod2app(Filename) ->
validate_links({Filename, Links}, CachedFiles) ->
%% io:format("~s ~p~n",[Links]),
- lists:foreach(
- fun({LinkType, TypeLinks}) ->
- lists:foreach(
- fun({Line,Link}) ->
- validate_link(Filename, LinkType, Line, Link, CachedFiles)
- end, TypeLinks)
- end, maps:to_list(maps:filter(fun(Key,_) -> not is_atom(Key) end,Links))).
+ maps:foreach(fun(LinkType, TypeLinks) when not is_atom(LinkType) ->
+ lists:foreach(fun({Line,Link}) ->
+ validate_link(Filename, LinkType, Line, Link, CachedFiles)
+ end, TypeLinks);
+ (_, _) -> ok
+ end, Links).
validate_link(Filename, LinkType, Line, [{"marker",Marker}], CachedFiles) ->
validate_link(Filename, LinkType, Line, Marker, CachedFiles);
validate_link(Filename, "seemfa", Line, Link, CachedFiles) ->
diff --git a/lib/stdlib/doc/src/beam_lib.xml b/lib/stdlib/doc/src/beam_lib.xml
index e743741ea8..0d2a6586cf 100644
--- a/lib/stdlib/doc/src/beam_lib.xml
+++ b/lib/stdlib/doc/src/beam_lib.xml
@@ -52,6 +52,7 @@
<item><c>labeled_exports ("ExpT")</c></item>
<item><c>labeled_locals ("LocT")</c></item>
<item><c>locals ("LocT")</c></item>
+ <item><c>documentation ("Docs")</c></item>
</list>
</description>
@@ -200,7 +201,7 @@ io:fwrite("~s~n", [erl_prettypr:format(erl_syntax:form_list(AC))]).</code>
<datatype>
<name name="chunkid"/>
<desc>
- <p>"Attr" | "CInf" | "Dbgi" | "ExpT" | "ImpT" | "LocT" | "AtU8"</p>
+ <p><c>"Attr" | "CInf" | "Dbgi" | "ExpT" | "ImpT" | "LocT" | "AtU8" | "Docs"</c></p>
</desc>
</datatype>
<datatype>
@@ -236,6 +237,15 @@ io:fwrite("~s~n", [erl_prettypr:format(erl_syntax:form_list(AC))]).</code>
is automatically computed from the <c>debug_info</c> chunk.</p>
</desc>
</datatype>
+ <datatype>
+ <name name="docs"/>
+ <desc>
+ <p>
+ <seeguide marker="kernel:eep48_chapter#the--docs--format">
+ EEP-48 documentation format</seeguide>
+ </p>
+ </desc>
+ </datatype>
<datatype>
<name name="forms"/>
</datatype>
diff --git a/lib/stdlib/src/beam_lib.erl b/lib/stdlib/src/beam_lib.erl
index 31be277643..8ee2a737c7 100644
--- a/lib/stdlib/src/beam_lib.erl
+++ b/lib/stdlib/src/beam_lib.erl
@@ -20,6 +20,8 @@
-module(beam_lib).
-behaviour(gen_server).
+-include_lib("kernel/include/eep48.hrl").
+
%% Avoid warning for local function error/1 clashing with autoimported BIF.
-compile({no_auto_import,[error/1]}).
%% Avoid warning for local function error/2 clashing with autoimported BIF.
@@ -71,19 +73,21 @@
-type label() :: integer().
-type chunkid() :: nonempty_string(). % approximation of the strings below
-%% "Abst" | "Dbgi" | "Attr" | "CInf" | "ExpT" | "ImpT" | "LocT" | "Atom" | "AtU8".
+%% "Abst" | "Dbgi" | "Attr" | "CInf" | "ExpT" | "ImpT" | "LocT" | "Atom" | "AtU8" | "Docs"
-type chunkname() :: 'abstract_code' | 'debug_info'
| 'attributes' | 'compile_info'
| 'exports' | 'labeled_exports'
| 'imports' | 'indexed_imports'
| 'locals' | 'labeled_locals'
- | 'atoms'.
+ | 'atoms' | 'documentation'.
-type chunkref() :: chunkname() | chunkid().
-type attrib_entry() :: {Attribute :: atom(), [AttributeValue :: term()]}.
-type compinfo_entry() :: {InfoKey :: atom(), term()}.
-type labeled_entry() :: {Function :: atom(), arity(), label()}.
+-type docs() :: #docs_v1{}.
+
-type chunkdata() :: {chunkid(), dataB()}
| {'abstract_code', abst_code()}
| {'debug_info', debug_info()}
@@ -95,7 +99,8 @@
| {'indexed_imports', [{index(), module(), Function :: atom(), arity()}]}
| {'locals', [{atom(), arity()}]}
| {'labeled_locals', [labeled_entry()]}
- | {'atoms', [{integer(), atom()}]}.
+ | {'atoms', [{integer(), atom()}]}
+ | {'documentation', docs()}.
%% Error reasons
-type info_rsn() :: {'chunk_too_big', file:filename(),
@@ -744,6 +749,18 @@ chunk_to_data(debug_info=Id, Chunk, File, _Cs, AtomTable, Mod) ->
{AtomTable, {Id, anno_from_term(Term)}}
end
end;
+chunk_to_data(documentation=Id, Chunk, File, _Cs, AtomTable, _Mod) ->
+ try
+ case binary_to_term(Chunk) of
+ #docs_v1{} = Term ->
+ {AtomTable, {Id, Term}};
+ _ ->
+ error({invalid_chunk, File, chunk_name_to_id(Id, File)})
+ end
+ catch
+ error:badarg ->
+ error({invalid_chunk, File, chunk_name_to_id(Id, File)})
+ end;
chunk_to_data(abstract_code=Id, Chunk, File, _Cs, AtomTable, Mod) ->
%% Before Erlang/OTP 20.0.
case Chunk of
@@ -793,6 +810,7 @@ chunk_name_to_id(attributes, _) -> "Attr";
chunk_name_to_id(abstract_code, _) -> "Abst";
chunk_name_to_id(debug_info, _) -> "Dbgi";
chunk_name_to_id(compile_info, _) -> "CInf";
+chunk_name_to_id(documentation, _) -> "Docs";
chunk_name_to_id(Other, File) ->
error({unknown_chunk, File, Other}).
diff --git a/lib/stdlib/src/epp.erl b/lib/stdlib/src/epp.erl
index 2b531c8869..826a763893 100644
--- a/lib/stdlib/src/epp.erl
+++ b/lib/stdlib/src/epp.erl
@@ -27,9 +27,12 @@
-export([default_encoding/0, encoding_to_string/1,
read_encoding_from_binary/1, read_encoding_from_binary/2,
set_encoding/1, set_encoding/2, read_encoding/1, read_encoding/2]).
+
-export([interpret_file_attribute/1]).
-export([normalize_typed_record_fields/1,restore_typed_record_fields/1]).
+-include_lib("kernel/include/file.hrl").
+
%%------------------------------------------------------------------------
-export_type([source_encoding/0]).
@@ -234,6 +237,10 @@ format_error({circular,M,A}) ->
io_lib:format("circular macro '~ts/~p'", [M,A]);
format_error({include,W,F}) ->
io_lib:format("can't find include ~s \"~ts\"", [W,F]);
+format_error({Tag, invalid, Alternative}) when Tag =:= moduledoc; Tag =:= doc ->
+ io_lib:format("invalid ~s tag, only ~s allowed", [Tag, Alternative]);
+format_error({Tag, W, Filename}) when Tag =:= moduledoc; Tag =:= doc ->
+ io_lib:format("can't find ~s ~s \"~ts\"", [Tag, W, Filename]);
format_error({illegal,How,What}) ->
io_lib:format("~s '-~s'", [How,What]);
format_error({illegal_function,Macro}) ->
@@ -932,6 +939,12 @@ scan_toks([{'-',_Lh},{atom,_Ld,warning}=Warn|Toks], From, St) ->
scan_err_warn(Toks, Warn, From, leave_prefix(St));
scan_toks([{'-',_Lh},{atom,_Li,include}=Inc|Toks], From, St) ->
scan_include(Toks, Inc, From, St);
+scan_toks([{'-',_Lh},{atom,_Ld,D}=Doc | [{'(', _},{'{',_} | _] = Toks], From, St)
+ when D =:= doc; D =:= moduledoc ->
+ scan_filedoc(coalesce_strings(Toks), Doc, From, St);
+scan_toks([{'-',_Lh},{atom,_Ld,D}=Doc | [{'{',_} | _] = Toks], From, St)
+ when D =:= doc; D =:= moduledoc ->
+ scan_filedoc(coalesce_strings(Toks), Doc, From, St);
scan_toks([{'-',_Lh},{atom,_Li,include_lib}=IncLib|Toks], From, St) ->
scan_include_lib(Toks, IncLib, From, St);
scan_toks([{'-',_Lh},{atom,_Li,ifdef}=IfDef|Toks], From, St) ->
@@ -978,14 +991,73 @@ scan_toks(Toks0, From, St) ->
wait_req_scan(St)
end.
+%% First we parse either ({file, "filename"}) or {file, "filename"} and
+%% return proper errors if syntax is incorrect. Only literal strings are allowed.
+scan_filedoc([{'(', _},{'{',_}, {atom, _,file},
+ {',', _}, {string, _, _} = DocFilename,
+ {'}', _},{')',_},{dot,_} = Dot], DocType, From, St) ->
+ scan_filedoc_content(DocFilename, Dot, DocType, From, St);
+scan_filedoc([{'(', _},{'{',_}, {atom, _,file} | _] = Toks, DocType, From, St) ->
+ T = find_mismatch(['(','{',atom,',',string,'}',')',dot], Toks, DocType),
+ epp_reply(From, {error,{loc(T),epp,{bad,DocType}}}),
+ wait_req_scan(St);
+scan_filedoc([{'(', _},{'{',_}, T | _], DocType, From, St) ->
+ epp_reply(From, {error,{loc(T),epp,{DocType, invalid, file}}}),
+ wait_req_scan(St);
+scan_filedoc([{'{',_}, {atom, _,file},
+ {',', _}, {string, _, _} = DocFilename,
+ {'}', _},{dot,_} = Dot], DocType, From, St) ->
+ scan_filedoc_content(DocFilename, Dot, DocType, From, St);
+scan_filedoc([{'{',_}, {atom, _,file} | _] = Toks, {atom,_,DocType}, From, St) ->
+ T = find_mismatch(['{',{atom, file},',',string,'}',dot], Toks, DocType),
+ epp_reply(From, {error,{loc(T),epp,{bad,DocType}}}),
+ wait_req_scan(St);
+scan_filedoc([{'{',_}, T | _], {atom,_,DocType}, From, St) ->
+ epp_reply(From, {error,{loc(T),epp,{DocType, invalid, file}}}),
+ wait_req_scan(St).
+
+%% Reads the content of the file and rewrites the AST as if
+%% the content had been written in-place.
+scan_filedoc_content({string, _A, DocFilename}, Dot,
+ {atom,DocLoc,Doc}, From, #epp{name = CurrentFilename} = St) ->
+ %% The head of the path is the dir where the current file is
+ Cwd = hd(St#epp.path),
+ case file:path_open([Cwd], DocFilename, [read, binary]) of
+ {ok, NewF, Pname} ->
+ case file:read_file_info(NewF) of
+ {ok, #file_info{ size = Sz }} ->
+ {ok, Bin} = file:read(NewF, Sz),
+ ok = file:close(NewF),
+ StartLoc = start_loc(St#epp.location),
+ %% Enter a new file for this doc entry
+ enter_file_reply(From, Pname, erl_anno:new(StartLoc), StartLoc,
+ code, St#epp.deterministic),
+ epp_reply(From, {ok,
+ [{'-',StartLoc}, {atom, StartLoc, Doc}]
+ ++ [{string, StartLoc, unicode:characters_to_list(Bin)}, {dot,StartLoc}]}),
+ %% Restore the previous file
+ enter_file_reply(From, CurrentFilename,
+ erl_anno:new(loc(Dot)), loc(Dot), code,
+ St#epp.deterministic),
+ wait_req_scan(St);
+ {error, _} ->
+ ok = file:close(NewF),
+ epp_reply(From, {error,{DocLoc,epp,{Doc, file, DocFilename}}}),
+ wait_req_scan(St)
+ end;
+ {error, _} ->
+ epp_reply(From, {error,{DocLoc,epp,{Doc, file, DocFilename}}}),
+ wait_req_scan(St)
+ end.
+
%% Determine whether we have passed the prefix where a -feature
%% directive is allowed.
in_prefix({atom, _, Atom}) ->
%% These directives are allowed inside the prefix
lists:member(Atom, ['module', 'feature',
'if', 'else', 'elif', 'endif', 'ifdef', 'ifndef',
- 'define', 'undef',
- 'include', 'include_lib']);
+ 'define', 'undef', 'include', 'include_lib',
+ 'moduledoc', 'doc']);
in_prefix(_T) ->
false.
@@ -1937,6 +2009,8 @@ find_mismatch([var_or_atom|Tags], [{var,_A,_V}=T|Ts], _T0) ->
find_mismatch(Tags, Ts, T);
find_mismatch([var_or_atom|Tags], [{atom,_A,_N}=T|Ts], _T0) ->
find_mismatch(Tags, Ts, T);
+find_mismatch([{Tag,Value}|Tags], [{Tag,_A,Value}=T|Ts], _T0) ->
+ find_mismatch(Tags, Ts, T);
find_mismatch(_, Ts, T0) ->
no_match(Ts, T0).
diff --git a/lib/stdlib/src/erl_lint.erl b/lib/stdlib/src/erl_lint.erl
index 766aade3a0..4c57859c74 100644
--- a/lib/stdlib/src/erl_lint.erl
+++ b/lib/stdlib/src/erl_lint.erl
@@ -174,7 +174,9 @@ value_option(Flag, Default, On, OnVal, Off, OffVal, Opts) ->
bvt = none :: 'none' | [any()], %Variables in binary pattern
gexpr_context = guard %Context of guard expression
:: gexpr_context(),
- load_nif=false :: boolean() %true if calls erlang:load_nif/2
+ load_nif=false :: boolean(), %true if calls erlang:load_nif/2
+ doc_defined = {false, none} :: {boolean(), term()},
+ moduledoc_defined = {false, none} :: {boolean(), term()}
}).
-type lint_state() :: #lint{}.
@@ -250,6 +252,8 @@ format_error(multiple_on_loads) ->
"more than one on_load attribute";
format_error({bad_on_load_arity,{F,A}}) ->
io_lib:format("function ~tw/~w has wrong arity (must be 0)", [F,A]);
+format_error({Tag, duplicate_doc_attribute, Ann}) ->
+ io_lib:format("redefining documentation attribute (~p) previously set at line ~p", [Tag, Ann]);
format_error({undefined_on_load,{F,A}}) ->
io_lib:format("function ~tw/~w undefined", [F,A]);
format_error(nif_inline) ->
@@ -889,23 +893,73 @@ attribute_state({attribute,Aa,behaviour,Behaviour}, St) ->
St#lint{behaviour=St#lint.behaviour ++ [{Aa,Behaviour}]};
attribute_state({attribute,Aa,behavior,Behaviour}, St) ->
St#lint{behaviour=St#lint.behaviour ++ [{Aa,Behaviour}]};
-attribute_state({attribute,A,type,{TypeName,TypeDef,Args}}, St) ->
- type_def(type, A, TypeName, TypeDef, Args, St);
-attribute_state({attribute,A,opaque,{TypeName,TypeDef,Args}}, St) ->
- type_def(opaque, A, TypeName, TypeDef, Args, St);
+attribute_state({attribute,A,type,{TypeName,TypeDef,Args}}=AST, St) ->
+ St1 = untrack_doc(AST, St),
+ type_def(type, A, TypeName, TypeDef, Args, St1);
+attribute_state({attribute,A,opaque,{TypeName,TypeDef,Args}}=AST, St) ->
+ St1 = untrack_doc(AST, St),
+ type_def(opaque, A, TypeName, TypeDef, Args, St1);
attribute_state({attribute,A,spec,{Fun,Types}}, St) ->
spec_decl(A, Fun, Types, St);
-attribute_state({attribute,A,callback,{Fun,Types}}, St) ->
- callback_decl(A, Fun, Types, St);
+attribute_state({attribute,A,callback,{Fun,Types}}=AST, St) ->
+ St1 =untrack_doc(AST, St),
+ callback_decl(A, Fun, Types, St1);
attribute_state({attribute,A,optional_callbacks,Es}, St) ->
optional_callbacks(A, Es, St);
attribute_state({attribute,A,on_load,Val}, St) ->
on_load(A, Val, St);
+attribute_state({attribute, _A, DocAttr, Doc}=AST, St)
+ when is_list(Doc) andalso (DocAttr =:= moduledoc orelse DocAttr =:= doc) ->
+ track_doc(AST, St);
attribute_state({attribute,_A,_Other,_Val}, St) -> % Ignore others
St;
attribute_state(Form, St) ->
function_state(Form, St#lint{state=function}).
+
+%% -doc "
+%% Tracks whether we have read a documentation attribute string multiple times.
+%% Terminal elements that reset the state of the documentation attribute tracking
+%% are:
+
+%% - function,
+%% - opaque,
+%% - type
+%% - callback
+
+%% These terminal elements are also the only ones where one should place
+%% documentation attributes.
+%% ".
+track_doc({attribute, A, Tag, Doc}=_AST, #lint{}=St)
+ when is_list(Doc) andalso (Tag =:= moduledoc orelse Tag =:= doc) ->
+ case get_doc_attr(Tag, St) of
+ {true, Ann} -> add_error(A, {Tag, duplicate_doc_attribute, erl_anno:line(Ann)}, St);
+ {false, _} -> update_doc_attr(Tag, A, St)
+ end;
+track_doc(_AST, St) ->
+ St.
+
+%%
+%% Helper functions to track documentation attributes
+%%
+get_doc_attr(moduledoc, #lint{moduledoc_defined = Moduledoc}) -> Moduledoc;
+get_doc_attr(doc, #lint{doc_defined = Doc}) -> Doc.
+
+update_doc_attr(moduledoc, A, #lint{}=St) ->
+ St#lint{moduledoc_defined = {true, A}};
+update_doc_attr(doc, A, #lint{}=St) ->
+ St#lint{doc_defined = {true, A}}.
+
+%% -doc "
+%% Reset the tracking of a documentation attribute.
+
+%% That is, assume that a terminal object was reached, thus we need to reset
+%% the state so that the linter understands that we have not seen any other
+%% documentation attribute.
+%% ".
+untrack_doc(_AST, St) ->
+ St#lint{doc_defined = {false, none}}.
+
%% function_state(Form, State) ->
%% State'
%% Allow for record, type and opaque type definitions and spec
@@ -914,18 +968,25 @@ attribute_state(Form, St) ->
function_state({attribute,A,record,{Name,Fields}}, St) ->
record_def(A, Name, Fields, St);
-function_state({attribute,A,type,{TypeName,TypeDef,Args}}, St) ->
- type_def(type, A, TypeName, TypeDef, Args, St);
-function_state({attribute,A,opaque,{TypeName,TypeDef,Args}}, St) ->
- type_def(opaque, A, TypeName, TypeDef, Args, St);
+function_state({attribute,A,type,{TypeName,TypeDef,Args}}=AST, St) ->
+ St1 = untrack_doc(AST, St),
+ type_def(type, A, TypeName, TypeDef, Args, St1);
+function_state({attribute,A,opaque,{TypeName,TypeDef,Args}}=AST, St) ->
+ St1 = untrack_doc(AST, St),
+ type_def(opaque, A, TypeName, TypeDef, Args, St1);
function_state({attribute,A,spec,{Fun,Types}}, St) ->
spec_decl(A, Fun, Types, St);
+function_state({attribute,_A,doc,_Val}=AST, St) ->
+ track_doc(AST, St);
+function_state({attribute,_A,moduledoc,_Val}=AST, St) ->
+ track_doc(AST, St);
function_state({attribute,_A,dialyzer,_Val}, St) ->
St;
function_state({attribute,Aa,Attr,_Val}, St) ->
add_error(Aa, {attribute,Attr}, St);
-function_state({function,Anno,N,A,Cs}, St) ->
- function(Anno, N, A, Cs, St);
+function_state({function,Anno,N,A,Cs}=AST, St) ->
+ St1 = untrack_doc(AST, St),
+ function(Anno, N, A, Cs, St1);
function_state({eof,Location}, St) -> eof(Location, St).
%% eof(LastLocation, State) ->
diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl
index 7f21253a55..c1c2ce434e 100644
--- a/lib/stdlib/src/erl_parse.yrl
+++ b/lib/stdlib/src/erl_parse.yrl
@@ -1418,6 +1418,47 @@ build_attribute({atom,Aa,file}, Val) ->
{attribute,Aa,file,{Name,Line}};
[Other|_] -> error_bad_decl(Other, file)
end;
+build_attribute({atom,Aa,Attr}, Val) when Attr =:= doc; Attr =:= moduledoc ->
+ case Val of
+ [{atom,_,Value}] when is_boolean(Value) ->
+ {attribute,Aa,Attr,Value};
+ [{atom,_,hidden=Value}] ->
+ {attribute,Aa,Attr,Value};
+ [{string,_,Value}] ->
+ {attribute,Aa,Attr,Value};
+ [{bin,_, _} = Bin] ->
+ case term(Bin) of
+ Value when is_binary(Value) ->
+ {attribute,Aa,Attr,Value};
+ _Else ->
+ error_bad_decl(Bin, doc)
+ end;
+ [{map,_,Pairs} = Expr] ->
+ Value =
+ try
+ maps:from_list(
+ lists:map(
+ fun({map_field_assoc,_,K,V}) ->
+ case normalise(K) of
+ equiv when Attr =:= doc, element(1, V) =:= call ->
+ {equiv, V};
+ NormalK ->
+ {NormalK, normalise(attribute_farity(V))}
+ end;
+ (E) ->
+ throw({badarg, E})
+ end, Pairs))
+ catch {badarg,E} ->
+ ret_abstr_err(E, "bad attribute");
+ _:_ ->
+ ret_abstr_err(Expr, "bad attribute")
+ end,
+ {attribute,Aa,Attr,Value};
+ [{tuple,_,[{atom,_,file},{string,_,Value}]}] ->
+ {attribute,Aa,Attr,{file,Value}};
+ [Other|_] ->
+ error_bad_decl(Other, doc)
+ end;
build_attribute({atom,Aa,Attr}, Val) ->
case Val of
[Expr0] ->
diff --git a/lib/stdlib/test/beam_lib_SUITE.erl b/lib/stdlib/test/beam_lib_SUITE.erl
index ef3a615699..11c0253ccb 100644
--- a/lib/stdlib/test/beam_lib_SUITE.erl
+++ b/lib/stdlib/test/beam_lib_SUITE.erl
@@ -453,7 +453,7 @@ strip_add_chunks(Conf) when is_list(Conf) ->
compare_chunks(B1, NB1, NBId1),
%% Keep all the extra chunks
- ExtraChunks = ["Abst", "Dbgi", "Attr", "CInf", "LocT", "Atom"],
+ ExtraChunks = ["Abst", "Dbgi", "Attr", "CInf", "Docs", "LocT", "Atom"],
{ok, {simple, AB1}} = beam_lib:strip(B1, ExtraChunks),
ABId1 = chunk_ids(AB1),
true = length(BId1) == length(ABId1),
diff --git a/lib/stdlib/test/epp_SUITE.erl b/lib/stdlib/test/epp_SUITE.erl
index 90b1712d14..b440f83be5 100644
--- a/lib/stdlib/test/epp_SUITE.erl
+++ b/lib/stdlib/test/epp_SUITE.erl
@@ -30,7 +30,8 @@
test_error/1, test_warning/1, otp_14285/1,
test_if/1,source_name/1,otp_16978/1,otp_16824/1,scan_file/1,file_macro/1,
deterministic_include/1, nondeterministic_include/1,
- gh_8268/1
+ gh_8268/1,
+ moduledoc_include/1
]).
-export([epp_parse_erl_form/2]).
@@ -73,7 +73,8 @@ all() ->
encoding, extends, function_macro, test_error, test_warning,
otp_14285, test_if, source_name, otp_16978, otp_16824, scan_file, file_macro,
deterministic_include, nondeterministic_include,
- gh_8268].
+ gh_8268,
+ moduledoc_include].
groups() ->
[{upcase_mac, [], [upcase_mac_1, upcase_mac_2]},
@@ -127,6 +127,48 @@ file_macro(Config) when is_list(Config) ->
"Other source" = FileA = FileB,
ok.
+moduledoc_include(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ ModuleFileContent = <<"-module(moduledoc).
+
+ -moduledoc {file, \"README.md\"}.
+
+ -export([]).
+ ">>,
+ DocFileContent = <<"# README
+
+ This file is a test
+ ">>,
+ CreateFile = fun (Dir, File, Content) ->
+ Dirname = filename:join([PrivDir, Dir]),
+ ok = create_dir(Dirname),
+ Filename = filename:join([Dirname, File]),
+ ok = file:write_file(Filename, Content),
+ Filename
+ end,
+
+ %% positive test: checks that all works as expected
+ ModuleName = CreateFile("module_attr", "moduledoc.erl", ModuleFileContent),
+ DocName = CreateFile("module_attr", "README.md", DocFileContent),
+ {ok, List} = epp:parse_file(ModuleName, []),
+ {attribute, _, moduledoc, ModuleDoc} = lists:keyfind(moduledoc, 3, List),
+ ?assertEqual({ok, unicode:characters_to_binary(ModuleDoc)}, file:read_file(DocName)),
+
+ %% negative test: checks that we produce an expected error
+ ModuleErrContent = binary:replace(ModuleFileContent, <<"README">>, <<"NotExistingFile">>),
+ ModuleErrName = CreateFile("module_attr", "moduledoc_err.erl", ModuleErrContent),
+ {ok, ListErr} = epp:parse_file(ModuleErrName, []),
+ {error,{_,epp,{moduledoc,file, "NotExistingFile.md"}}} = lists:keyfind(error, 1, ListErr),
+
+ ok.
+
+create_dir(Dir) ->
+ case file:make_dir(Dir) of
+ ok -> ok;
+ {error, eexist} -> ok;
+ _ -> error
+ end.
+
deterministic_include(Config) when is_list(Config) ->
DataDir = proplists:get_value(data_dir, Config),
File = filename:join(DataDir, "deterministic_include.erl"),
@@ -930,11 +972,12 @@ scan_file(Config) when is_list(Config) ->
[FileForm1, ModuleForm, ExportForm,
FileForm2, FileForm3, FunctionForm,
{eof,_}] = Toks,
- [{'-',_}, {atom,_,file}, {'(',_} | _ ] = FileForm1,
+ [{'-',_}, {atom,_,file}, {'(',_} | _ ] = FileForm1,
[{'-',_}, {atom,_,module}, {'(',_} | _ ] = ModuleForm,
[{'-',_}, {atom,_,export}, {'(',_} | _ ] = ExportForm,
- [{'-',_}, {atom,_,file}, {'(',_} | _ ] = FileForm2,
- [{'-',_}, {atom,_,file}, {'(',_} | _ ] = FileForm3,
+ [{'-',_}, {atom,_,file}, {'(',_} | _ ] = FileForm2,
+ [{'-',_}, {atom,_,file}, {'(',_} | _ ] = FileForm3,
+ [{atom,_,ok}, {'(',_} | _] = FunctionForm,
ok.
macs(Epp) ->
diff --git a/lib/stdlib/test/erl_lint_SUITE.erl b/lib/stdlib/test/erl_lint_SUITE.erl
index 64cf5643f7..c54c1bd00d 100644
--- a/lib/stdlib/test/erl_lint_SUITE.erl
+++ b/lib/stdlib/test/erl_lint_SUITE.erl
@@ -36,6 +36,7 @@
-export([all/0, suite/0, groups/0]).
-export([singleton_type_var_errors/1,
+ documentation_attributes/1,
unused_vars_warn_basic/1,
unused_vars_warn_lc/1,
unused_vars_warn_rec/1,
@@ -116,6 +117,7 @@ all() ->
eep49,
redefined_builtin_type,
singleton_type_var_errors,
+ documentation_attributes,
match_float_zero].
groups() ->
@@ -909,6 +911,108 @@ unused_import(Config) when is_list(Config) ->
[] = run(Config, Ts),
ok.
+documentation_attributes(Config) when is_list(Config) ->
+ Ts = [{error_moduledoc,
+ <<"-moduledoc \"\"\"
+ Error
+ \"\"\".
+ -import(lists, []).
+
+ -moduledoc \"\"\"
+ Duplicate entry
+ \"\"\".
+ main() -> error.
+ ">>,
+ [],
+ {errors,[{{6,15},erl_lint,{moduledoc,duplicate_doc_attribute,1}}], []}},
+
+
+ {error_doc_import,
+ <<"-doc \"\"\"
+ Error
+ \"\"\".
+ -import(lists, []).
+
+ -doc \"\"\"
+ Duplicate entry
+ \"\"\".
+ main() -> error.
+ ">>,
+ [],
+ {errors,[{{6,15},erl_lint,{doc,duplicate_doc_attribute,1}}], []}},
+
+ {error_doc_export,
+ <<"-doc \"\"\"
+ Error
+ \"\"\".
+ -export([]).
+
+ -doc \"\"\"
+ Duplicate entry
+ \"\"\".
+ main() -> error.
+ ">>,
+ [],
+ {errors,[{{6,16},erl_lint,{doc,duplicate_doc_attribute,1}}], []}},
+
+ {error_doc_export_type,
+ <<"-doc \"\"\"
+ Error
+ \"\"\".
+ -export_type([]).
+
+ -doc \"\"\"
+ Duplicate entry
+ \"\"\".
+ main() -> error.
+ ">>,
+ [],
+ {errors,[{{6,16},erl_lint,{doc,duplicate_doc_attribute,1}}], []}},
+
+ {error_doc_include,
+ <<"-doc \"\"\"
+ Error
+ \"\"\".
+ -include_lib(\"common_test/include/ct.hrl\").
+
+ -doc \"\"\"
+ Duplicate entry
+ \"\"\".
+ main() -> error.
+ ">>,
+ [],
+ {errors,[{{6,16},erl_lint,{doc,duplicate_doc_attribute,1}}], []}},
+
+ {error_doc_behaviour,
+ <<"-doc \"\"\"
+ Error
+ \"\"\".
+ -behaviour(gen_server).
+
+ -doc \"\"\"
+ Duplicate entry
+ \"\"\".
+ main() -> error.
+ ">>,
+ [],
+ {error,[{{6,16},erl_lint,{doc,duplicate_doc_attribute,1}}],
+ [{{4,16},erl_lint,{undefined_behaviour_func,{handle_call,3},gen_server}},
+ {{4,16},erl_lint,{undefined_behaviour_func,{handle_cast,2},gen_server}},
+ {{4,16},erl_lint,{undefined_behaviour_func,{init,1},gen_server}}]}},
+
+ {ok_doc_in_wrong_position,
+ <<"-doc \"\"\"
+ Bad position, that gets attached to function.
+ We do not report this as an error.
+ \"\"\".
+ -include_lib(\"common_test/include/ct.hrl\").
+
+ main() -> ok.
+ ">>, [], []}
+ ],
+ [] = run(Config, Ts),
+ ok.
+
%% Test singleton type variables
singleton_type_var_errors(Config) when is_list(Config) ->
Ts = [{singleton_error1,
diff --git a/make/otp.mk.in b/make/otp.mk.in
index 1ed554ee76..88a7d14b33 100644
--- a/make/otp.mk.in
+++ b/make/otp.mk.in
@@ -104,7 +104,7 @@ endif
ifdef BOOTSTRAP
ERL_COMPILE_FLAGS += +slim
else
- ERL_COMPILE_FLAGS += +debug_info
+ ERL_COMPILE_FLAGS += +debug_info +no_docs
endif
ifeq ($(ERL_DETERMINISTIC),yes)
ERL_COMPILE_FLAGS += +deterministic
diff --git a/system/doc/reference_manual/Makefile b/system/doc/reference_manual/Makefile
index c51496ca5d..932be45e8d 100644
--- a/system/doc/reference_manual/Makefile
+++ b/system/doc/reference_manual/Makefile
@@ -94,6 +94,9 @@ clean clean_docs:
rm -f $(TOP_PDF_FILE) $(TOP_PDF_FILE:%.pdf=%.fo)
rm -f errs core *~
+$(XMLDIR)/%.xml: %.md $(ERL_TOP)/make/emd2exml
+ $(ERL_TOP)/make/emd2exml $< $@
+
# ----------------------------------------------------
# Release Target
# ----------------------------------------------------
diff --git a/system/doc/reference_manual/documentation.md b/system/doc/reference_manual/documentation.md
new file mode 100644
index 0000000000..7eb351204e
--- /dev/null
+++ b/system/doc/reference_manual/documentation.md
@@ -0,0 +1,414 @@
+# Documentation
+
+Documentation in Erlang is done through the `-moduledoc` and `-doc` [attributes][]. For example:
+
+ -module(math).
+ -moduledoc """
+ A module for basic arithmetic.
+ """.
+
+ -export([add/2]).
+
+ -doc "Adds two numbers together."
+ add(One, Two) -> One + Two.
+
+The `-moduledoc` attribute has to be located before the first `-doc` attribute or
+function declaration. It documents the overall purpose of the module.
+
+The `-doc` attribute always precedes the [function][] or [attribute][attributes] it documents.
+The attributes that can be documented are [user-defined types][] (`-type` and `-opaque`) and
+[behaviour module attributes][] (`-callback`).
+
+By default the format used for documentation attributes is [Markdown][wikipedia]
+but that can be changed by setting [module documentation metadata](#moduledoc-metadata).
+
+A good starting point to writing Markdown is [Basic writing and formatting syntax][github].
+
+For details on what is allowed to be part of the `-moduledoc` and `-doc` attributes, see
+[Documentation Attributes][doc_attrs].
+
+`-doc` attributes have been available since Erlang/OTP 27.
+
+[attributes]: system/reference_manual:modules#module-attributes
+[function]: system/reference_manual:functions
+[user-defined types]: system/reference_manual:typespec#type-declarations-of-user-defined-types
+[behaviour module attributes]: system/reference_manual:modules#behaviour-module-attribute
+[Earmark]: https://github.com/robertdober/earmark_parser
+[wikipedia]: https://en.wikipedia.org/wiki/Markdown
+[github]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax
+[doc_attrs]: system/reference_manual:modules#documentation-attributes
+
+## Documentation metadata
+
+It is possible to add metadata to the documentation entry. You do this by adding
+a `-moduledoc` or `-doc` attribute with a map as argument. For example:
+
+ -module(math).
+ -moduledoc """
+ A module for basic arithmetic.
+ """.
+ -moduledoc #{ since => "1.0" }.
+
+ -export([add/2]).
+
+ -doc "Adds two number together."
+ -doc(#{ since => "1.0" }).
+ add(One, Two) -> One + Two.
+
+The metadata is used by documentation tools to provide extra information to
+the user. There can be multiple metadata documentation entries, in which case
+the maps will be merged with the latest taking precedence if there are
+duplicate keys. Example:
+
+ -doc "Adds two number together."
+ -doc #{ since => "1.0", author => "Joe" }.
+ -doc #{ since => "2.0" }.
+ add(One, Two) -> One + Two.
+
+This will result in a metadata entry of `#{ since => "2.0", author => "Joe" }`.
+
+The keys and values in the metadata map can be any type, but it is recommended
+that only [atoms][] are used for keys and [strings][] for the values.
+
+[atoms]: data_types#atom
+[strings]: data_types#string
+
+## External documentation files
+
+The `-moduledoc` and `-doc` can also be placed in external files. To do so use
+`-doc {file, "path/to/doc.md"}` to point to the documentation. The path used is
+relative to the file where the `-doc` attribute is located. For example:
+
+ %% doc/add.md
+ Adds two numbers together
+
+and
+
+ %% src/math.erl
+ -doc({file, "../doc/add.md"}).
+ add(One, Two) -> One + Two.
+
+## Documenting a module
+
+The module description should include details on how to use the API
+and examples of the different functions working together. Here is a
+good place to use images and other diagrams to better show the usage
+of the module. Instead of writing a long text in the `moduledoc`
+attribute, it could be better to break it out into an external page.
+
+The `moduledoc` attribute should start with a short paragraph
+describing the module and then go into greater details. For example:
+
+ -module(math).
+ -moduledoc """
+ A module for basic arithmetic.
+
+ This module can be used to add and subtract values. For example:
+
+ ```
+ 1> math:subtract(math:add(2, 3), 1).
+ 4
+ ```
+ """.
+
+### Moduledoc metadata
+
+There are three reserved metadata keys for `-moduledoc`:
+
+- `since` - Shows in which version of the application the module was added.
+- `deprecated` - Shows a text in the documentation explaining that it is deprecated
+ and what to use instead.
+- `format` - The format to use for all documentation in this module.
+ The default is `text/markdown`.
+ It should be written using the [mime type][] of the format.
+
+Example:
+
+ -moduledoc {file, "../doc/math.asciidoc"}.
+ -moduledoc #{ since => "0.1", format => "text/asciidoc" }.
+ -moduledoc #{ deprecated => "Use the stdlib math module instead." }.
+
+[mime type]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
+
+## Documenting functions, user-defined types, and callbacks
+
+Functions, types, and callbacks can be documented using the `-doc` attribute.
+Each entry should start with a short paragraph describing the purpose of entity,
+and then go into greater detail in needed.
+
+It is not recommended to include images or diagrams in this documentation as
+it is used by IDEs and [c:h/1][] to show the documentation to the user.
+
+For example:
+
+ -doc """
+ A number that can be used by the math module.
+
+ We use a special number here so that we know
+ that this number comes from this module.
+ """.
+ -opaque number() :: {math, erlang:number()}.
+
+ -doc """
+ Adds two number together.
+
+ ### Example:
+
+ ```
+ 1> math:add(math:number(1), math:number(2)).
+ {number, 3}
+ ```
+ """.
+ -spec add(number(), number()) -> number().
+ add({number, One}, {number, Two}) -> {number, One + Two}.
+
+[c:h/1]: seemfa/stdlib:c#h/1
+
+### Doc metadata
+
+There are four reserved metadata keys for `-doc`:
+
+- `since => unicode:chardata()` - Shows which version of the application the module
+ was added.
+- `deprecated => unicode:chardata()` - Shows a text in the documentation explaining
+ that it is deprecated and what to use instead. The compiler will automatically
+ insert this key if there is a `-deprecated` attribute marking a function as deprecated.
+- `equiv => unicode:chardata()` - Notes that this function is equivalent to another function
+ in this module. The equivalence can be described using either `Func/Arity` or `Func(Args)`.
+ For example:
+
+ -doc #{ equiv => add/3 }.
+ add(One, Two) -> add(One, Two, []).
+ add(One, Two, Options) -> ...
+
+ or
+
+ -doc #{ equiv => add(One, Two, []) }.
+ -spec add(One :: number(), Two :: number()) -> number().
+ add(One, Two) -> add(One, Two, []).
+ add(One, Two, Options) -> ...
+
+ The entry into the [EEP-48][] doc chunk metadata is the value converted to a string.
+
+- `exported => boolean()` - A [boolean/0][] signifying if the entry is `exported`
+ or not. For any `-type` attribute this value is automatically set by the compiler
+ and should not be set by the user.
+
+[boolean/0]: seetype/erts:erlang#boolean
+
+### Doc slogans
+
+The doc slogan is a short text shown to describe the function and its arguments.
+By default it is determined by looking at the names of the arguments in the `-spec` or
+function. For example:
+
+ add(One, Two) -> One + Two.
+
+ -spec sub(One :: integer(), Two :: integer()) -> integer().
+ sub(X, Y) -> X - Y.
+
+will have a slogan of `add(One, Two)` and `sub(One, Two)`.
+
+For types or callbacks, the slogan is derived from the type or callback specification.
+For example:
+
+ -type number(Value) :: {number, Value}.
+ %% slogan will be `number(Value)`
+
+ -opaque number() :: {number, number()}.
+ %% slogan will be `number()`
+
+ -callback increment(In :: number()) -> Out.
+ %% slogan will be `increment(In)`
+
+ -callback increment(In) -> Out when
+ In :: number().
+ %% slogan will be `increment(In)`
+
+If it is not possible to "easily" figure out a nice slogan from the code, the
+MFA syntax is used instead. For example: `add/2`, `number/1`, `increment/1`
+
+It is possible to supply a custom slogan by placing it as the first line of
+the `-doc` attribute. The provided slogan must be in the form of a function
+declaration up until the `->`. For example:
+
+ -doc """
+ add(One, Two)
+
+ Adds two numbers.
+ """.
+ add(A, B) -> A + B.
+
+Will create the slogan `add(One, Two)`. The slogan will be removed from the
+documentation string, so in the example above only the text `"Adds two numbers"`
+will be part of the documentation. This works for functions, types, and callbacks.
+
+## Links in Markdown
+
+When writing documentation in Markdown, links are automatically found in any
+inline code segment that looks like an MFA. For example:
+
+ -doc "See `sub/2` for more details".
+
+will create a link to the `sub/2` function in the current module if it exists.
+One can also use `` `sub/2` `` as the link target. For example:
+
+ -doc "See [subtract](`sub/2`) for more details".
+ -doc "See [`sub/2`] for more details".
+ -doc """
+ See [subtract] for more details
+
+ [subtract]: `sub/2`
+ """.
+ -doc """
+ See [subtract][1] for more details
+
+ [1]: `sub/2`
+ """.
+
+The above examples result in the same link being created.
+
+The link can also other entities:
+
+- `remote functions` - Use `module:function/arity` syntax.
+
+ Example:
+
+ -doc "See `math:sub/2` for more details".
+
+- `modules` - Write the module with a `m` prefix. Use anchors to
+ jump to a specific place in the module.
+
+ Example:
+
+ -doc "See `m:math` for more details".
+ -doc "See `m:math#anchor` for more details".
+
+- `types` - Use the same syntax as for local/remote function but add a `t` prefix.
+
+ Example:
+
+ -doc "See `t:number/0` for more details".
+ -doc "See `t:math:number/0` for more details".
+
+- `callbacks` - Use the same syntax as for local/remote function but add a `c` prefix.
+
+ Example:
+
+ -doc "See `c:increment/0` for more details".
+ -doc "See `c:math:increment/0` for more details".
+
+- `extra pages` - For extra pages in the current application use a normal link,
+ for example "`[release notes](notes.md)`".
+ For extra pages in another application use the `e` prefix and state which
+ application the page belongs to. One can also use anchors to jump to a specific
+ place in the page.
+
+ Example:
+
+ -doc "See `e:stdlib:unicode_usage` for more details".
+ -doc "See `e:stdlib:unicode_usage#notes-about-raw-filenames` for more details".
+
+## What is visible versus hidden?
+
+An Erlang [application][] normally consists of various public and private modules. That is,
+modules that should be used by other applications and modules that should not. By default
+all modules in an application are visible, but by setting `-moduledoc false.`
+specific modules can be hidden from being listed as part of the available API.
+
+An Erlang [module][] consists of public and private functions and type attributes.
+By default, all exported functions, exported types and callbacks are considered
+visible and part of the modules public API. In addition, any non-exported
+type that is referred to by any other visible type attribute is also visible,
+but not considered to be part of the public API. For example:
+
+ -export([example/0]).
+
+ -type private() :: one.
+ -spec example() -> private().
+ example() -> one.
+
+in the above code, the function `example/0` is exported and it referenced the
+un-exported type `private/0`. Therefore both `example/0` and `private/0` will
+be marked as visible. The `private/0` type will have the metadata field `exported`
+set to `false` to show that it is not part of the public API.
+
+If you want to make a visible entity hidden you need to set the `-doc` attribute to
+`false`. Let's revisit out previous example:
+
+ -export([example/0]).
+
+ -type private() :: one.
+ -spec example() -> private().
+ -doc false.
+ example() -> one.
+
+The function `example/0` is exported but explicitly marked as hidden; therefore
+both `example/0` and `private/0` will be hidden.
+
+Any documentation added to an automatically hidden entity
+(non-exported function or type) is ignored and will generate a
+warning. Such functions can be documented using comments.
+
+[application]: seeerl/kernel:application
+[module]: modules
+
+## Compiling and getting documentation
+
+The Erlang compiler has support for compiling the documentation into [EEP-48][]
+documentation chunks by passing the [beam_docs][] flag to [compile:file/1][], or
+`+beam_docs` to [erlc][].
+
+The documentation can then be retrieved using [code:get_doc/1][], or viewed using the
+shell built-in command [h()][c:h/1]. For example:
+
+ 1> h(math).
+
+ math
+
+ A module for basic arithmetic.
+
+ 2> h(math, add).
+
+ add(One, Two)
+
+ Adds two numbers together.
+
+[EEP-48]: kernel:eep48_chapter
+[compile:file/1]: seemfa/compiler:compile#file/1
+[beam_docs]: seeerl/compiler:compile#beam_docs
+[erlc]: seecom/erts:erlc
+[code:get_doc/1]: seemfa/kernel:code#get_doc/1
+
+## Using ExDoc to generate HTML/ePub documentation
+
+[ExDoc][] has built-in support to generate documentation from Markdown. The simplest
+way to use it is by using the [rebar3_ex_doc][] plugin. To setup a rebar3 project to
+use [ExDoc][] to generate documentation add the following to your `rebar3.config`.
+
+ %% Enable the plugin
+ {plugins, [rebar3_ex_doc]}.
+
+ %% Configure the compiler to emit documentation
+ {profiles, [{docs, [{erl_opts, [beam_docs]}]}]}.
+
+ {ex_doc, [
+ {extras, ["README.md"]},
+ {main, "README.md"},
+ {source_url, "https://github.com/namespace/your_app"}
+ ]}.
+
+When configured you can run `rebar3 ex_doc` and the documentation will be generated to
+`doc/index.html`. For more details and options see the [rebar3_ex_doc][] documentation.
+
+You can also download the [release escript bundle][ex_doc_escript] from github and
+run it from the command line. The documentation for using the escript is
+found by running `ex_doc --help`.
+
+If you are writing documentation that will be using [ExDoc][] to generate HTML/ePub
+it is highly recommended to read its documentation.
+
+[ExDoc]: https://hexdocs.pm/ex_doc/
+[rebar3_ex_doc]: https://hexdocs.pm/rebar3_ex_doc
+[ex_doc_escript]: https://github.com/elixir/ex_doc/releases/latest
+[Earmark]: https://hexdocs.pm/earmark_parser
diff --git a/system/doc/reference_manual/modules.xml b/system/doc/reference_manual/modules.xml
index 360345d4ad..955374ecea 100644
--- a/system/doc/reference_manual/modules.xml
+++ b/system/doc/reference_manual/modules.xml
@@ -104,6 +104,18 @@ fact(0) -> % |
functions from. <c>Functions</c> is a list similar as for
<c>export</c>.</p>
</item>
+ <tag><c>-moduledoc(Documentation).</c> or <c>-moduledoc Documentation.</c></tag>
+ <item>
+ <p>The user documentation for this module. The allowed values for
+ <c>Documentation</c> are the same as for
+ <seeguide marker="#documentation-attributes"><c>-doc</c></seeguide>.
+ </p>
+ <p>See the
+ <seeguide marker="documentation">Documentation</seeguide>
+ guide in the Erlang Reference Manual for more details about how
+ to use <c>-moduledoc</c>.
+ </p>
+ </item>
<tag><c>-compile(Options).</c></tag>
<item>
<p>Compiler options. <c>Options</c> is a single option
@@ -247,6 +259,54 @@ behaviour_info(callbacks) -> Callbacks.</pre>
which is not to be further updated.
</p>
</section>
+
+ <section>
+ <title>Documentation attributes</title>
+ <p>The module attribute <c>-doc(Documentation)</c> is used to provide user
+ documentation for a function/type/callback: </p>
+ <pre>
+-doc("Example documentation").
+example() -> ok.
+ </pre>
+ <p>The attribute should be placed just before the entity it documents.The parenthesis are
+ optional around <c>Documentation</c>. The allowed values for <c>Documentation</c> are:</p>
+ <taglist>
+ <tag><seeguide marker="data_types#string">literal string</seeguide> or <seeguide marker="expressions#unicode-segments">utf-8 encoded binary string</seeguide></tag>
+ <item><p>The string documenting the entity. Any literal string is allowed,
+ so both <seeguide marker="data_types#tqstring">triple quoted strings</seeguide>
+ and <seeguide marker="data_types#sigil">sigils</seeguide> that translate to literal
+ strings can be used. The following examples are equivalent:</p>
+ <code>
+-doc("Example \"docs\"").
+-doc(<<"Example \"docs\""/utf8>>).
+-doc ~S/Example "docs"/.
+-doc """
+ Example "docs"
+ """
+-doc ~B|Example "docs"|.
+ </code>
+ <p>For clarity it is recommended to use either normal <c>"strings"</c> or
+ triple quoted strings for documentation attributes.</p>
+ </item>
+ <tag><c>{file, </c><seetype marker="kernel:file#filename"><c>file:filename()</c></seetype><c>}</c></tag>
+ <item>Read the contents of filename and use that as the documentation string.</item>
+ <tag><c>false</c></tag>
+ <item>Set the current entity as hidden, that is, it should not be listed as an
+ available function and has no documentation.</item>
+ <tag><c>Metadata :: </c><seetype marker="erts:erlang#map"><c>map()</c></seetype></tag>
+ <item>
+ <p>
+ Metadata about the current entity. Some of the keys in the
+ metadata have a special meaning. See <seeguide marker="system/reference_manual:documentation#moduledoc-metadata">Moduledoc metadata</seeguide> and <seeguide marker="system/reference_manual:documentation#doc-metadata">Doc metadata</seeguide> for more details.
+ </p>
+ </item>
+ </taglist>
+ <p>It is possible to have multiple Metadata doc attributes per entity, but only a single
+ documentation string entry is allowed.</p>
+ <p>See the <seeguide marker="documentation">Documentation</seeguide>
+ guide in the Erlang Reference Manual for more details.
+ </p>
+ </section>
</section>
<section>
@@ -364,12 +424,6 @@ behaviour_info(callbacks) -> Callbacks.</pre>
all NIF functions in the module.</p>
</item>
- <tag><c>native</c></tag>
- <item>
- <p>Return <c>true</c> if the module has native compiled code.
- Return <c>false</c> otherwise. In a system compiled without HiPE
- support, the result is always <c>false</c></p>
- </item>
</taglist>
</section>
</section>
diff --git a/system/doc/reference_manual/part.xml b/system/doc/reference_manual/part.xml
index ec2e3e0306..abee9fba9e 100644
--- a/system/doc/reference_manual/part.xml
+++ b/system/doc/reference_manual/part.xml
@@ -34,6 +34,7 @@
<xi:include href="patterns.xml"/>
<xi:include href="modules.xml"/>
<xi:include href="functions.xml"/>
+ <xi:include href="documentation.xml"/>
<xi:include href="typespec.xml"/>
<xi:include href="opaques.xml"/>
<xi:include href="expressions.xml"/>
diff --git a/system/doc/reference_manual/xmlfiles.mk b/system/doc/reference_manual/xmlfiles.mk
index 8e2af09699..740b070ded 100644
--- a/system/doc/reference_manual/xmlfiles.mk
+++ b/system/doc/reference_manual/xmlfiles.mk
@@ -23,6 +23,7 @@ REF_MAN_CHAPTER_FILES = \
patterns.xml \
modules.xml \
functions.xml \
+ documentation.xml \
expressions.xml \
macros.xml \
records.xml \
--
2.35.3