Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:Ledest:erlang:26
erlang
2701-compiler-implementation-of-EEP-59.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 2701-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 @@ -32,7 +32,8 @@ test_if/1,source_name/1,otp_16978/1,otp_16824/1,scan_file/1,file_macro/1, string_concat_warning/1, deterministic_include/1, nondeterministic_include/1, - gh_8268/1 + gh_8268/1, + moduledoc_include/1 ]). -export([epp_parse_erl_form/2]). @@ -78,7 +79,8 @@ all() -> otp_14285, test_if, source_name, otp_16978, otp_16824, scan_file, file_macro, string_concat_warning, 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"), @@ -932,12 +932,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,_,ok}, {'(',_} | _ ] = FunctionForm, + [{'-',_}, {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() -> redefined_builtin_type, tilde_k, singleton_type_var_errors, + documentation_attributes, match_float_zero, undefined_module, update_literal]. @@ -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 PRIMARY_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
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor