%% ===================================================================== %% 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 %% %% 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. %% %% Alternatively, you may use this file under the terms of the GNU Lesser %% General Public License (the "LGPL") as published by the Free Software %% Foundation; either version 2.1, or (at your option) any later version. %% If you wish to allow use of your version of this file only under the %% terms of the LGPL, you should delete the provisions above and replace %% them with the notice and other provisions required by the LGPL; see %% . If you do not delete the provisions %% above, a recipient may use your version of this file under the terms of %% either the Apache License or the LGPL. %% %% @copyright 2001-2003 Richard Carlsson %% @author Richard Carlsson %% @see edoc %% @end %% ===================================================================== %% @doc EDoc documentation extraction. -module(edoc_extract). -export([source/3, source/4, source/5, header/3, header/4, header/5, file/4, text/4]). -import(edoc_report, [report/3, warning/3]). %% %% @headerfile "edoc.hrl" (disabled until it can be made private) -include("edoc.hrl"). -type filename() :: file:filename(). -type proplist() :: proplists:proplist(). -type context() :: module | footer | function | overview | single. %% @doc Like {@link source/5}, but reads the syntax tree and the %% comments from the specified file. %% %% @see edoc:read_comments/2 %% @see edoc:read_source/2 %% @see source/4 -spec source(File, Env, Opts) -> R when File :: filename(), Env :: edoc:env(), Opts :: proplist(), R :: {module(), edoc:edoc_module()} | {module(), edoc:edoc_module(), [edoc:entry()]}. source(File, Env, Opts) -> Forms = edoc:read_source(File, Opts), Comments = edoc:read_comments(File, Opts), source(Forms, Comments, File, Env, Opts). %% @doc Like {@link source/4}, but first inserts the given comments in %% the syntax trees. The syntax trees must contain valid position %% information. (Cf. {@link edoc:read_comments/2}.) %% %% @see edoc:read_comments/2 %% @see edoc:read_source/2 %% @see source/3 %% @see source/4 %% @see //syntax_tools/erl_recomment -spec source(Forms, Comments, File, Env, Opts) -> R when Forms :: erl_syntax:forms(), Comments :: [edoc:comment()], File :: filename(), Env :: edoc:env(), Opts :: proplist(), R :: {module(), edoc:edoc_module()} | {module(), edoc:edoc_module(), [edoc:entry()]}. source(Forms, Comments, File, Env, Opts) when is_list(Forms) -> Forms1 = erl_syntax:form_list(Forms), source(Forms1, Comments, File, Env, Opts); source(Forms, Comments, File, Env, Opts) -> Tree = erl_recomment:quick_recomment_forms(Forms, Comments), TypeDocs = find_type_docs(Forms, Comments, Env, File), source1(Tree, File, Env, Opts, TypeDocs). %% @doc Extracts EDoc documentation from commented source code syntax %% trees. The given `Forms' must be a single syntax tree of %% type `form_list', or a list of syntax trees representing %% "program forms" (cf. {@link edoc:read_source/2}. %% `Env' is an environment created by {@link %% edoc_lib:get_doc_env/3}. The `File' argument is used for %% error reporting and output file name generation only. %% %% See {@link edoc:get_doc/2} for descriptions of the `def', %% `hidden', `private', and `todo' options. %% %% @see edoc:read_comments/2 %% @see edoc:read_source/2 %% @see source/5 %% @see //syntax_tools/erl_recomment %% Note that the actual module name found in the source file will be %% used for generating the documentation, creating relative links, etc. %% INHERIT-OPTIONS: add_macro_defs/3 %% INHERIT-OPTIONS: edoc_data:module/4 -spec source(Forms, File, Env, Opts) -> R when Forms :: erl_syntax:forms(), File :: filename(), Env :: edoc:env(), Opts :: proplist(), R :: {module(), edoc:edoc_module()} | {module(), edoc:edoc_module(), [edoc:entry()]}. source(Forms, File, Env, Opts) when is_list(Forms) -> source(erl_syntax:form_list(Forms), File, Env, Opts); source(Tree, File0, Env, Opts) -> TypeDocs = find_type_docs(Tree, [], Env, File0), source1(Tree, File0, Env, Opts, TypeDocs). %% Forms0 and Comments is used for extracting Erlang type documentation. source1(Tree, File0, Env, Opts, TypeDocs) -> Forms = preprocess_forms(Tree), File = edoc_lib:filename(File0), Module = get_module_info(Tree, File), {Header, Footer, Entries} = collect(Forms, Module), Name = Module#module.name, Env1 = Env#env{module = Name, root = ""}, Env2 = add_macro_defs(module_macros(Env1), Opts, Env1), Entries1 = get_tags([Header, Footer | Entries], Env2, File, TypeDocs), Entries2 = edoc_specs:add_type_data(Entries1, Opts, File, Module), edoc_tags:check_types(Entries2, Opts, File), Data = edoc_data:module(Module, Entries2, Env2, Opts), %% If `return_entries' is given return the EDoc internal representation of tags / code forms. case proplists:is_defined(return_entries, Opts) of true -> {Name, Data, Entries2}; false -> {Name, Data} end. %% @doc Similar to {@link header/5}, but reads the syntax tree and the %% comments from the specified file. %% %% @see edoc:read_comments/2 %% @see edoc:read_source/2 %% @see header/4 -spec header(File, Env, Opts) -> edoc:entry_data() when File :: filename(), Env :: edoc:env(), Opts :: proplist(). header(File, Env, Opts) -> Forms = edoc:read_source(File), Comments = edoc:read_comments(File), header(Forms, Comments, File, Env, Opts). %% @doc Similar to {@link header/4}, but first inserts the given %% comments in the syntax trees. The syntax trees must contain valid %% position information. (Cf. {@link edoc:read_comments/2}.) %% %% @see header/3 %% @see header/4 %% @see //syntax_tools/erl_recomment -spec header(Forms, Comments, File, Env, Opts) -> edoc:entry_data() when Forms :: erl_syntax:forms(), Comments :: [edoc:comment()], File :: filename(), Env :: edoc:env(), Opts :: proplist(). header(Forms, Comments, File, Env, Opts) when is_list(Forms) -> Forms1 = erl_syntax:form_list(Forms), header(Forms1, Comments, File, Env, Opts); header(Forms, Comments, File, Env, Opts) -> Tree = erl_recomment:quick_recomment_forms(Forms, Comments), header(Tree, File, Env, Opts). %% @doc Extracts EDoc documentation from commented header file syntax %% trees. Similar to {@link source/5}, but ignores any documentation %% that occurs before a module declaration or a function definition. %% (Warning messages are printed if content may be ignored.) `Env' is %% assumed to already be set up with a suitable module context. %% %% @see header/5 %% @see //syntax_tools/erl_recomment -spec header(Forms, File, Env, Opts) -> edoc:entry_data() when Forms :: erl_syntax:forms(), File :: filename(), Env :: edoc:env(), Opts :: proplist(). header(Forms, File, Env, Opts) when is_list(Forms) -> header(erl_syntax:form_list(Forms), File, Env, Opts); header(Tree, File0, Env, _Opts) -> Forms = preprocess_forms(Tree), File = edoc_lib:filename(File0), Module = #module{name = Env#env.module}, % a dummy module record %% We take only "footer" tags, i.e., any kind of definition will %% kill all the information above it up to that point. Then we call %% this the 'header' to make error reports make better sense. {Header, Footer, Entries} = collect(Forms, Module), if Header#entry.data /= {[],[],[]} -> warning(File, "documentation before module declaration is ignored by @headerfile", []); true -> ok end, if Entries /= [] -> warning(File, "documentation before function definitions is ignored by @headerfile", []); true -> ok end, [Entry] = get_tags([Footer#entry{name = header}], Env, File), Entry#entry.data. %% NEW-OPTIONS: def %% DEFER-OPTIONS: source/4 add_macro_defs(Defs0, Opts, Env) -> Defs = proplists:append_values(def, Opts), edoc_macros:check_defs(Defs), Env#env{macros = Defs ++ Defs0 ++ Env#env.macros}. %% @doc Reads a text file and returns the list of tags in the file. Any %% lines of text before the first tag are ignored. `Env' is an %% environment created by {@link edoc_lib:get_doc_env/3}. Upon error, %% `Reason' is an atom returned from the call to {@link %% //kernel/file:read_file/1} or the atom 'invalid_unicode'. %% %% See {@link text/4} for options. %% INHERIT-OPTIONS: text/4 -spec file(File, Context, Env, Opts) -> {ok, Tags} | {error, Reason} when File :: filename(), Context :: context(), Env :: edoc:env(), Opts :: proplist(), Tags :: [term()], Reason :: term(). file(File, Context, Env, Opts) -> case file:read_file(File) of {ok, Bin} -> Enc = edoc_lib:read_encoding(File,[{in_comment_only, false}]), case catch unicode:characters_to_list(Bin, Enc) of String when is_list(String) -> {ok, text(String, Context, Env, Opts, File)}; _ -> {error, invalid_unicode} end; {error, _} = Error -> Error end. %% @doc Returns the list of tags in the text. Any lines of text before %% the first tag are ignored. `Env' is an environment created by {@link %% edoc_lib:get_doc_env/3}. %% %% See {@link source/4} for a description of the `def' option. %% INHERIT-OPTIONS: add_macro_defs/3 %% DEFER-OPTIONS: source/4 -spec text(Text, Context, Env, Opts) -> Tags when Text :: string(), Context :: context(), Env :: edoc:env(), Opts :: proplist(), Tags :: [term()]. text(Text, Context, Env, Opts) -> text(Text, Context, Env, Opts, ""). text(Text, Context, Env, Opts, Where) -> Env1 = add_macro_defs(file_macros(Context, Env), Opts, Env), Cs = edoc_lib:lines(Text), Ts0 = edoc_tags:scan_lines(Cs, 1), Tags = sets:from_list(edoc_tags:tag_names()), Ts1 = edoc_tags:filter_tags(Ts0, Tags, Where), Single = sets:from_list(edoc_tags:tags(single)), Allow = sets:from_list(edoc_tags:tags(Context)), case edoc_tags:check_tags(Ts1, Allow, Single, Where) of true -> exit(error); false -> Ts2 = edoc_macros:expand_tags(Ts1, Env1, Where), How = dict:from_list(edoc_tags:tag_parsers()), edoc_tags:parse_tags(Ts2, How, Env1, Where) end. %% @doc Initialises a module-info record with data about the module %% represented by the list of forms. Exports are guaranteed to exist in %% the set of defined names. -spec get_module_info(Forms, File) -> edoc:module_meta() when Forms :: erl_syntax:forms(), File :: filename(). get_module_info(Forms, File) -> L = case catch {ok, erl_syntax_lib:analyze_forms(Forms)} of {ok, L1} -> L1; syntax_error -> report(File, "syntax error in input.", []), exit(error); {'EXIT', R} -> exit(R); R -> throw(R) end, {Name, Vars} = case lists:keyfind(module, 1, L) of {module, N} when is_atom(N) -> {N, none}; {module, {N, _}=Mod} when is_atom(N) -> Mod; _ -> report(File, "module name missing.", []), exit(error) end, Functions = ordsets:from_list(get_list_keyval(functions, L)), Exports = ordsets:from_list(get_list_keyval(exports, L)), Attributes = ordsets:from_list(get_list_keyval(attributes, L)), Records = get_list_keyval(records, L), Encoding = edoc_lib:read_encoding(File, []), #module{name = Name, parameters = Vars, functions = Functions, exports = ordsets:intersection(Exports, Functions), attributes = Attributes, records = Records, encoding = Encoding, file = File}. get_list_keyval(Key, L) -> case lists:keyfind(Key, 1, L) of {Key, As} -> ordsets:from_list(As); _ -> [] end. %% @doc Preprocessing: copies any precomments on forms to standalone %% comments, and removes "invisible" forms from the list. -spec preprocess_forms(Forms) -> Forms when Forms :: erl_syntax:forms(). preprocess_forms(Tree) -> preprocess_forms_1(erl_syntax:form_list_elements( erl_syntax:flatten_form_list(Tree))). preprocess_forms_1([F | Fs]) -> case erl_syntax:get_precomments(F) of [] -> preprocess_forms_2(F, Fs); Cs -> Cs ++ preprocess_forms_2(F, Fs) end; preprocess_forms_1([]) -> []. preprocess_forms_2(F, Fs) -> case erl_syntax_lib:analyze_form(F) of comment -> [F | preprocess_forms_1(Fs)]; {function, _} -> [F | preprocess_forms_1(Fs)]; {attribute, {module, _}} -> [F | preprocess_forms_1(Fs)]; text -> [F | preprocess_forms_1(Fs)]; {attribute, {record, _}} -> [F | preprocess_forms_1(Fs)]; {attribute, {N, _}} -> case edoc_specs:is_tag(N) of true -> [F | preprocess_forms_1(Fs)]; false -> preprocess_forms_1(Fs) end; _ -> preprocess_forms_1(Fs) end. %% This collects the data for the header and the functions of the %% module. Note that the list of forms is assumed to have been %% preprocessed first, so that all "invisible" forms are removed, and %% the only interesting comments are those that are standalone comments %% in the list. collect(Fs, Mod) -> Acc = #{comments => [], callbacks => [], specs => [], types => [], records => [], functions => [], header => undefined}, collect(Fs, Acc, Mod). collect([F | Fs], Acc, Mod) -> #{comments := Cs, types := Ts, records := Rs, header := Header} = Acc, case erl_syntax_lib:analyze_form(F) of comment -> collect(Fs, store(comments, F, Acc), Mod); {function, Name} -> L = get_line(F), Export = ordsets:is_element(Name, Mod#module.exports), Args = parameters(erl_syntax:function_clauses(F)), Function = #entry{name = Name, args = Args, line = L, export = Export, data = {comment_text(Cs), [], [], Ts, Rs}}, NewAcc = Acc#{comments := [], types := [], records := []}, collect(Fs, store(functions, Function, NewAcc), Mod); {attribute, {module, _}} when Header =:= undefined -> L = get_line(F), NewAcc = Acc#{comments := [], specs := [], types := [], records := []}, NewHeader = #entry{name = module, line = L, data = {comment_text(Cs), [], [], Ts, Rs}}, collect(Fs, store(header, NewHeader, NewAcc), Mod); {attribute, {record, {_Name, Fields}}} -> case is_typed_record(Fields) of true -> collect(Fs, store(records, F, Acc), Mod); false -> collect(Fs, Acc, Mod) end; {attribute, {N, _}} -> case edoc_specs:tag(N) of callback -> collect(Fs, store(callbacks, F, Acc), Mod); spec -> collect(Fs, store(specs, F, Acc), Mod); type -> collect(Fs, store(types, F, Acc), Mod); unknown -> %% Drop current seen comments. NewAcc = Acc#{comments := [], specs := [], types := []}, collect(Fs, NewAcc, Mod) end; _ -> %% Drop current seen comments. NewAcc = Acc#{comments := [], specs := [], types := [], records := []}, collect(Fs, NewAcc, Mod) end; collect([], Acc, Mod) -> #{comments := Cs, callbacks := Cbs, specs := Ss, types := Ts, records := Rs, functions := As, header := Header} = Acc, Footer = #entry{name = footer, data = {comment_text(Cs), Cbs, [], Ts, Rs}}, As1 = lists:reverse(As), As2 = insert_specs(As1, Ss, Mod), if Header =:= undefined -> {#entry{name = module, data = {[],[],[],[],[]}}, Footer, As2}; true -> {Header, Footer, As2} end. store(header, Value, Acc) -> Acc#{header := Value}; store(Key, Value, Acc) -> maps:update_with(Key, fun (Vs) -> [Value | Vs] end, Acc). is_typed_record([]) -> false; is_typed_record([{_, {_, Type}} | Fs]) -> Type =/= none orelse is_typed_record(Fs). %% Returns a list of simplified comment information (position and text) %% for a list of abstract comments. The order of elements is reversed. comment_text(Cs) -> comment_text(Cs, []). comment_text([C | Cs], Ss) -> L = get_line(C), comment_text(Cs, [#comment{line = L, text = [remove_percent_chars(S) || S <- erl_syntax:comment_text(C)]} | Ss]); comment_text([], Ss) -> Ss. get_line(Tree) -> Anno = erl_syntax:get_pos(Tree), erl_anno:line(Anno). insert_specs(As, Ss, Mod) -> ModName = Mod#module.name, SpecList = [ {spec_fun_arity(ModName, S), [S]} || S <- Ss ], Specs = maps:from_list(SpecList), %% Assert that we've not skipped redundant specs for the same {Fun, Arity}. %% This should never happen, as such a module would not compile. case length(SpecList) == maps:size(Specs) of true -> ok; false -> error_redundant_specs(Mod, SpecList, Specs) end, insert_specs_(ModName, As, Specs). error_redundant_specs(Mod, SpecList, Specs) -> [{RedundantMFA, [Form]} | _] = lists:sort(SpecList) -- lists:sort(maps:to_list(Specs)), {_, Line, _, _} = erl_syntax:revert(Form), {_, F, A} = RedundantMFA, edoc_report:error(Line, {Mod#module.file, {F, A}}, "Redundant -spec attribute found. Try setting {preprocess, true}."), erlang:exit({redundant_spec, RedundantMFA}). insert_specs_(_, [], _) -> []; insert_specs_(ModName, [#entry{} = A | As], Specs) -> #entry{name = {F, Arity}, data = {Cs, Cbs, _, Ts, Rs}} = A, Ss = maps:get({ModName, F, Arity}, Specs, []), [ A#entry{data = {Cs, Cbs, Ss, Ts, Rs}} | insert_specs_(ModName, As, Specs) ]. spec_fun_arity(ModName, Form) -> case erl_syntax:revert(Form) of {attribute, _, spec, {{F, A}, _}} -> %% -spec F(Args...) -> ... {ModName, F, A}; {attribute, _, spec, {{M, F, A}, _}} -> %% -spec M:F(Args...) -> ... {M, F, A} end. %% @doc Replaces leading `%' characters by spaces. For example, `"%%% %% foo" -> "\s\s\s foo"', but `"% % foo" -> "\s % foo"', since the %% second `%' is preceded by whitespace. -spec remove_percent_chars(string()) -> string(). remove_percent_chars([$% | Cs]) -> [$\s | remove_percent_chars(Cs)]; remove_percent_chars(Cs) -> Cs. %% Extracting possible parameter names from Erlang clause patterns. The %% atom '_' is used when no name can be found. (Better names are made up %% later, when we also may have typespecs available; see edoc_data.) parameters(Clauses) -> select_names([find_names(Ps) || Ps <- patterns(Clauses)]). patterns(Cs) -> edoc_lib:transpose([erl_syntax:clause_patterns(C) || C <- Cs]). find_names(Ps) -> find_names(Ps, []). find_names([P | Ps], Ns) -> case erl_syntax:type(P) of variable -> find_names(Ps, [tidy_name(erl_syntax:variable_name(P)) | Ns]); match_expr -> %% Right-hand side gets priority over left-hand side! %% Note that the list is reversed afterwards. P1 = erl_syntax:match_expr_pattern(P), P2 = erl_syntax:match_expr_body(P), find_names([P1, P2 | Ps], Ns); list -> P1 = erl_syntax:list_tail(P), find_names([P1 | Ps], Ns); record_expr -> A = erl_syntax:record_expr_type(P), AtomName = erl_syntax:atom_name(A), Atom = list_to_atom(AtomName), case AtomName =:= lists:flatten(io_lib:write_atom(Atom)) of true -> N = list_to_atom(capitalize(AtomName)), find_names(Ps, [N | Ns]); false -> find_names(Ps, Ns) end; infix_expr -> %% this can only be a '++' operation P1 = erl_syntax:infix_expr_right(P), find_names([P1 | Ps], Ns); _ -> find_names(Ps, Ns) end; find_names([], Ns) -> lists:reverse(Ns). select_names(Ls) -> select_names(Ls, [], sets:new()). select_names([Ns | Ls], As, S) -> A = select_name(Ns, S), select_names(Ls, [A | As], sets:add_element(A, S)); select_names([], As, _) -> lists:reverse(As). select_name([A | Ns], S) -> case sets:is_element(A, S) of true -> select_name(Ns, S); false -> A end; select_name([], _S) -> '_'. %% Strip leading underscore characters from parameter names. If the %% result does not begin with an uppercase character, we add a single %% leading underscore. If the result would be empty, the atom '_' is %% returned. tidy_name(A) -> case atom_to_list(A) of [$_ | Cs] -> list_to_atom(tidy_name_1(Cs)); _ -> A end. tidy_name_1([$_ | Cs]) -> tidy_name_1(Cs); tidy_name_1([C | _]=Cs) when C >= $A, C =< $Z -> Cs; tidy_name_1([C | _]=Cs) when C >= $\300, C =< $\336, C =/= $\327-> Cs; tidy_name_1(Cs) -> [$_ | Cs]. %% Change initial character from lowercase to uppercase. capitalize([C | Cs]) when C >= $a, C =< $z -> [C - 32 | Cs]; capitalize([C | Cs]) when C >= $\340, C =< $\376, C /= $\367 -> [C - 32 | Cs]; capitalize(Cs) -> Cs. %% Collects the tags belonging to each entry, checks them, expands %% macros and parses the content. -record(tags, {names,single,module,function,footer}). %-type tags() :: #tags{names :: sets:set(atom()), % single :: sets:set(atom()), % module :: sets:set(atom()), % footer :: sets:set(atom()), % function :: sets:set(atom())}. get_tags(Es, Env, File) -> get_tags(Es, Env, File, dict:new()). get_tags(Es, Env, File, TypeDocs) -> %% Cache this stuff for quick lookups. Tags = #tags{names = sets:from_list(edoc_tags:tag_names()), single = sets:from_list(edoc_tags:tags(single)), module = sets:from_list(edoc_tags:tags(module)), footer = sets:from_list(edoc_tags:tags(footer)), function = sets:from_list(edoc_tags:tags(function))}, How = dict:from_list(edoc_tags:tag_parsers()), get_tags(Es, Tags, Env, How, File, TypeDocs). get_tags([#entry{name = Name, data = {Cs,Cbs,Specs,Types,Records}} = E | Es], Tags, Env, How, File, TypeDocs) -> Where = {File, Name}, Ts0 = scan_tags(Cs), {Ts1,Specs1} = select_spec(Ts0, Where, Specs), Ts2 = check_tags(Ts1, Tags, Where), Ts3 = edoc_macros:expand_tags(Ts2, Env, Where), Ts4 = edoc_tags:parse_tags(Ts3, How, Env, Where), Ts = selected_specs(Specs1, Ts4), ETypes = [edoc_specs:type(Type, TypeDocs) || Type <- Types ++ Records], Callbacks = get_callbacks(Name, Cbs, TypeDocs), [E#entry{data = Ts ++ ETypes ++ Callbacks} | get_tags(Es, Tags, Env, How, File, TypeDocs)]; get_tags([], _, _, _, _, _) -> []. get_callbacks(_EntryName, CbForms, TypeDocs) -> [ callback(F, TypeDocs) || F <- CbForms ]. callback(F, TypeDocs) -> {attribute,_,callback,{NA,_}} = Attr = erl_syntax:revert(F), Doc = case dict:find({callback, NA}, TypeDocs) of error -> none; {ok, D} -> D end, #tag{name = callback, line = erl_syntax:get_pos(F), origin = code, data = {NA, Doc}, form = Attr}. %% Scanning a list of separate comments for tags. scan_tags([#comment{line = L, text = Ss} | Es]) -> edoc_tags:scan_lines(Ss, L) ++ scan_tags(Es); scan_tags([]) -> []. %% Check the set of found tags (depending on context). %% Completely unknown tags are filtered out with a warning. check_tags(Ts0, Tags, Where) -> Ts = edoc_tags:filter_tags(Ts0, Tags#tags.names, Where), case check_tags_1(Ts, Tags, Where) of false -> Ts; true -> exit(error) end. check_tags_1(Ts, Tags, {_, module} = Where) -> Allow = Tags#tags.module, Single = Tags#tags.single, edoc_tags:check_tags(Ts, Allow, Single, Where); check_tags_1(Ts, Tags, {_, footer} = Where) -> Allow = Tags#tags.footer, Single = Tags#tags.single, edoc_tags:check_tags(Ts, Allow, Single, Where); check_tags_1(Ts, Tags, Where) -> Allow = Tags#tags.function, Single = Tags#tags.single, edoc_tags:check_tags(Ts, Allow, Single, Where). %% `Specs', that is `-spec' attributes, take precedence before any `@spec' tags. %% @see selected_specs/2 -define(function(_F, _A), {_F, _A}). select_spec(Ts, {_, ?function(_F, _A)}, Specs) -> case Specs of [] -> {Ts, []}; [_|_] -> {Ts, Specs} end; select_spec(Ts, _Where, _Specs) -> {Ts,[]}. -undef(function). skip_specs(Ts) -> [ T || T = #tag{name = N} <- Ts, N /= spec ]. %% If a `-spec' attribute is present, it takes precedence over `@spec' tags. selected_specs([], Ts) -> Ts; selected_specs([F], Ts) -> [edoc_specs:spec(F) | skip_specs(Ts)]. %% Macros for modules module_macros(Env) -> [{module, atom_to_list(Env#env.module)}] ++ edoc_macros:std_macros(Env). %% Macros for reading auxiliary edoc-files file_macros(_Context, Env) -> edoc_macros:std_macros(Env). %% @doc Extracts what will be documentation of Erlang types. %% Returns a dict of {Name, Doc} where Name is {TypeName, Arity}. %% %% The idea is to mimic how the @type tag works. %% Using @type: %%```@type t() = t1(). Some docs of t/0; %% Further docs of t/0.''' %% The same thing using -type: %%```-type t() :: t1(). % Some docs of t/0; %% Further docs of t/0.''' find_type_docs(Forms0, Comments, Env, File) -> Tree = erl_recomment:recomment_forms(Forms0, Comments), Forms = preprocess_forms(Tree), Env1 = add_macro_defs(edoc_macros:std_macros(Env), [], Env), F = fun(C, Line) -> find_fun(C, Line, Env1, File) end, edoc_specs:docs(Forms, F). find_fun(C0, Line, Env, File) -> C1 = comment_text(C0), Text = lists:append([C#comment.text || C <- C1]), Comm = #comment{line = Line, text = Text}, [Tag | _] = scan_tags([Comm]), [Tag1] = edoc_macros:expand_tags([Tag], Env, File), Tag1.