diff options
Diffstat (limited to 'lib/edoc/src/edoc_data.erl')
-rw-r--r-- | lib/edoc/src/edoc_data.erl | 545 |
1 files changed, 545 insertions, 0 deletions
diff --git a/lib/edoc/src/edoc_data.erl b/lib/edoc/src/edoc_data.erl new file mode 100644 index 0000000000..124f8eb9a1 --- /dev/null +++ b/lib/edoc/src/edoc_data.erl @@ -0,0 +1,545 @@ +%% ===================================================================== +%% This library is free software; you can redistribute it and/or modify +%% it under the terms of the GNU Lesser General Public License as +%% published by the Free Software Foundation; either version 2 of the +%% License, or (at your option) any later version. +%% +%% This library is distributed in the hope that it will be useful, but +%% WITHOUT ANY WARRANTY; without even the implied warranty of +%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%% Lesser General Public License for more details. +%% +%% You should have received a copy of the GNU Lesser General Public +%% License along with this library; if not, write to the Free Software +%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +%% USA +%% +%% $Id$ +%% +%% @private +%% @copyright 2003 Richard Carlsson +%% @author Richard Carlsson <[email protected]> +%% @see edoc +%% @end +%% ===================================================================== + +%% @doc Building the EDoc external data structure. See the file +%% <a href="../priv/edoc.dtd">`edoc.dtd'</a> for details. + +-module(edoc_data). + +-export([module/4, package/4, overview/4, type/2]). + +-include("edoc.hrl"). + +%% TODO: report multiple definitions of the same type in the same module. +%% TODO: check that variables in @equiv are found in the signature +%% TODO: copy types from target (if missing) when using @equiv + +%% <!ELEMENT module (args?, description?, author*, copyright?, +%% version?, since?, deprecated?, see*, reference*, +%% todo?, behaviour*, callbacks?, typedecls?, +%% functions)> +%% <!ATTLIST module +%% name CDATA #REQUIRED +%% private NMTOKEN(yes | no) #IMPLIED +%% hidden NMTOKEN(yes | no) #IMPLIED +%% root CDATA #IMPLIED> +%% <!ELEMENT args (arg*)> +%% <!ELEMENT arg (argName, description?)> +%% <!ELEMENT argName (#PCDATA)> +%% <!ELEMENT description (briefDescription, fullDescription?)> +%% <!ELEMENT briefDescription (#PCDATA)> +%% <!ELEMENT fullDescription (#PCDATA)> +%% <!ELEMENT author EMPTY> +%% <!ATTLIST author +%% name CDATA #REQUIRED +%% email CDATA #IMPLIED +%% website CDATA #IMPLIED> +%% <!ELEMENT version (#PCDATA)> +%% <!ELEMENT since (#PCDATA)> +%% <!ELEMENT copyright (#PCDATA)> +%% <!ELEMENT deprecated (description)> +%% <!ELEMENT see (#PCDATA)> +%% <!ATTLIST see +%% name CDATA #REQUIRED +%% href CDATA #IMPLIED> +%% <!ELEMENT reference (#PCDATA)> +%% <!ELEMENT todo (#PCDATA)> +%% <!ELEMENT behaviour (#PCDATA)> +%% <!ATTLIST behaviour +%% href CDATA #IMPLIED> +%% <!ELEMENT callbacks (callback+)> +%% <!ELEMENT typedecls (typedecl+)> +%% <!ELEMENT typedecl (typedef, description?)> +%% <!ELEMENT functions (function+)> + +%% NEW-OPTIONS: private, hidden, todo +%% DEFER-OPTIONS: edoc_extract:source/4 + +module(Module, Entries, Env, Opts) -> + Name = atom_to_list(Module#module.name), + HeaderEntry = get_entry(module, Entries), + HeaderTags = HeaderEntry#entry.data, + AllTags = get_all_tags(Entries), + Functions = function_filter(Entries, Opts), + Out = {module, ([{name, Name}, + {root, Env#env.root}] + ++ case is_private(HeaderTags) of + true -> [{private, "yes"}]; + false -> [] + end + ++ case is_hidden(HeaderTags) of + true -> [{hidden, "yes"}]; + false -> [] + end), + (module_args(Module#module.parameters) + ++ behaviours(Module#module.attributes, Env) + ++ get_doc(HeaderTags) + ++ authors(HeaderTags) + ++ get_version(HeaderTags) + ++ get_since(HeaderTags) + ++ get_copyright(HeaderTags) + ++ get_deprecated(HeaderTags) + ++ sees(HeaderTags, Env) + ++ references(HeaderTags) + ++ todos(HeaderTags, Opts) + ++ [{typedecls, types(AllTags, Env)}, + {functions, functions(Functions, Env, Opts)} + | callbacks(Functions, Module, Env, Opts)]) + }, + xmerl_lib:expand_element(Out). + +get_all_tags(Es) -> + lists:flatmap(fun (#entry{data = Ts}) -> Ts end, Es). + +is_private(Ts) -> + get_tags(private, Ts) =/= []. + +description([]) -> + []; +description(Desc) -> + ShortDesc = edoc_lib:get_first_sentence(Desc), + [{description, + [{briefDescription, ShortDesc}, + {fullDescription, Desc}]}]. + +module_args(none) -> + []; +module_args(Vs) -> + [{args, [{arg, [{argName, [atom_to_list(V)]}]} || V <- Vs]}]. + +types(Tags, Env) -> + [{typedecl, [{label, edoc_types:to_label(Def)}], + [edoc_types:to_xml(Def, Env)] ++ description(Doc)} + || #tag{name = type, data = {Def, Doc}} <- Tags]. + +functions(Es, Env, Opts) -> + [function(N, As, Export, Ts, Env, Opts) + || #entry{name = {_,_}=N, args = As, export = Export, data = Ts} + <- Es]. + +function_filter(Es, Opts) -> + Private = proplists:get_bool(private, Opts), + Hidden = proplists:get_bool(hidden, Opts), + [E || E <- Es, function_filter(E, Private, Hidden)]. + +%% Note that only entries whose names have the form {_,_} are functions. +function_filter(#entry{name = {_,_}, export = Export, data = Ts}, + Private, Hidden) -> + ((Export andalso not is_private(Ts)) orelse Private) + andalso ((not is_hidden(Ts)) orelse Hidden); +function_filter(_, _, _) -> + false. + +is_hidden(Ts) -> + get_tags(hidden, Ts) =/= []. + +callbacks(Es, Module, Env, Opts) -> + case lists:any(fun (#entry{name = {behaviour_info, 1}}) -> true; + (_) -> false + end, + Es) of + true -> + try (Module#module.name):behaviour_info(callbacks) of + Fs -> + Fs1 = [{F,A} || {F,A} <- Fs, is_atom(F), is_integer(A)], + if Fs1 =:= [] -> + []; + true -> + [{callbacks, + [callback(F, Env, Opts) || F <- Fs1]}] + end + catch + _:_ -> [] + end; + false -> [] + end. + +%% <!ELEMENT callback EMPTY> +%% <!ATTLIST callback +%% name CDATA #REQUIRED +%% arity CDATA #REQUIRED> + +callback({N, A}, _Env, _Opts) -> + {callback, [{name, atom_to_list(N)}, + {arity, integer_to_list(A)}], + []}. + +%% <!ELEMENT function (args, typespec?, returns?, throws?, equiv?, +%% description?, since?, deprecated?, see*, todo?)> +%% <!ATTLIST function +%% name CDATA #REQUIRED +%% arity CDATA #REQUIRED +%% exported NMTOKEN(yes | no) #REQUIRED +%% label CDATA #IMPLIED> +%% <!ELEMENT args (arg*)> +%% <!ELEMENT arg (argName, description?)> +%% <!ELEMENT argName (#PCDATA)> +%% <!ELEMENT returns (description)> +%% <!ELEMENT throws (type, localdef*)> +%% <!ELEMENT equiv (expr, see?)> +%% <!ELEMENT expr (#PCDATA)> + +function({N, A}, As, Export, Ts, Env, Opts) -> + {Args, Ret, Spec} = signature(Ts, As, Env), + {function, [{name, atom_to_list(N)}, + {arity, integer_to_list(A)}, + {exported, case Export of + true -> "yes"; + false -> "no" + end}, + {label, edoc_refs:to_label(edoc_refs:function(N, A))}], + [{args, [{arg, [{argName, [atom_to_list(A)]}] ++ description(D)} + || {A, D} <- Args]}] + ++ Spec + ++ case Ret of + [] -> []; + _ -> [{returns, description(Ret)}] + end + ++ get_throws(Ts, Env) + ++ get_equiv(Ts, Env) + ++ get_doc(Ts) + ++ get_since(Ts) + ++ get_deprecated(Ts, N, A, Env) + ++ sees(Ts, Env) + ++ todos(Ts, Opts) + }. + +get_throws(Ts, Env) -> + case get_tags(throws, Ts) of + [Throws] -> + Type = Throws#tag.data, + [edoc_types:to_xml(Type, Env)]; + [] -> + [] + end. + +get_equiv(Ts, Env) -> + case get_tags(equiv, Ts) of + [Equiv] -> + Expr = Equiv#tag.data, + See = case get_expr_ref(Equiv#tag.data) of + none -> []; + Ref -> + [see(Ref, [edoc_refs:to_string(Ref)], Env)] + end, + [{equiv, [{expr, [erl_prettypr:format(Expr)]} | See]}]; + [] -> + [] + end. + +get_doc(Ts) -> + case get_tags(doc, Ts) of + [T] -> + description(T#tag.data); + [] -> + [] + end. + +get_copyright(Ts) -> + get_pcdata_tag(copyright, Ts). + +get_version(Ts) -> + get_pcdata_tag(version, Ts). + +get_since(Ts) -> + get_pcdata_tag(since, Ts). + +get_pcdata_tag(Tag, Ts) -> + case get_tags(Tag, Ts) of + [T] -> + [{Tag, [T#tag.data]}]; + [] -> + [] + end. + +%% Deprecation declarations for xref: +%% +%% -deprecated(Info). +%% Info = Spec | [Spec] +%% Spec = module | {F,A} | {F,A,Details}} +%% Details = next_version | next_major_release | eventually +%% (EXTENSION: | string() | {M1,F1,A1}} +%% TODO: use info from '-deprecated(...)' (xref-)declarations. + +get_deprecated(Ts) -> + case get_tags(deprecated, Ts) of + [T] -> + [{deprecated, description(T#tag.data)}]; + [] -> + [] + end. + +get_deprecated(Ts, F, A, Env) -> + case get_deprecated(Ts) of + [] -> + M = Env#env.module, + case otp_internal:obsolete(M, F, A) of + {Tag, Text} when Tag =:= deprecated; Tag =:= removed -> + deprecated([Text]); + {Tag, Repl, _Rel} when Tag =:= deprecated; Tag =:= removed -> + deprecated(Repl, Env); + _ -> + [] + end; + Es -> + Es + end. + +deprecated(Repl, Env) -> + {Text, Ref} = replacement_function(Env#env.module, Repl), + Desc = ["Use ", {a, href(Ref, Env), [{code, [Text]}]}, " instead."], + deprecated(Desc). + +deprecated(Desc) -> + [{deprecated, description(Desc)}]. + +replacement_function(M0, {M,F,A}) when is_list(A) -> + %% refer to the largest listed arity - the most general version + replacement_function(M0, {M,F,lists:last(lists:sort(A))}); +replacement_function(M, {M,F,A}) -> + {io_lib:fwrite("~w/~w", [F, A]), edoc_refs:function(F, A)}; +replacement_function(_, {M,F,A}) -> + {io_lib:fwrite("~w:~w/~w", [M, F, A]), edoc_refs:function(M, F, A)}. + +get_expr_ref(Expr) -> + case catch {ok, erl_syntax_lib:analyze_application(Expr)} of + {ok, {F, A}} when is_atom(F), is_integer(A) -> + edoc_refs:function(F, A); + {ok, {M, {F, A}}} when is_atom(M), is_atom(F), is_integer(A) -> + edoc_refs:function(M, F, A); + _ -> + none + end. + +authors(Ts) -> + [author(Info) || #tag{data = Info} <- get_tags(author, Ts)]. + +%% <!ATTLIST author +%% name CDATA #REQUIRED +%% email CDATA #IMPLIED +%% website CDATA #IMPLIED> + +author({Name, Mail, URI}) -> + %% At least one of Name and Mail must be nonempty in the tag. + {author, ([{name, if Name =:= "" -> Mail; + true -> Name + end}] + ++ if Mail =:= "" -> + case lists:member($@, Name) of + true -> [{email, Name}]; + false -> [] + end; + true -> [{email, Mail}] + end + ++ if URI =:= "" -> []; + true -> [{website, URI}] + end), []}. + +behaviours(As, Env) -> + [{behaviour, href(edoc_refs:module(B), Env), [atom_to_list(B)]} + || {behaviour, B} <- As, is_atom(B)]. + +sees(Tags, Env) -> + Ts = get_tags(see, Tags), + Rs = lists:keysort(1, [Data || #tag{data = Data} <- Ts]), + [see(Ref, XML, Env) || {Ref, XML} <- Rs]. + +see(Ref, [], Env) -> + see(Ref, [edoc_refs:to_string(Ref)], Env); +see(Ref, XML, Env) -> + {see, [{name, edoc_refs:to_string(Ref)}] ++ href(Ref, Env), XML}. + +href(Ref, Env) -> + [{href, edoc_refs:get_uri(Ref, Env)}] + ++ case edoc_refs:is_top(Ref, Env) of + true -> + [{target, "_top"}]; + false -> + [] + end. + +references(Tags) -> + [{reference, XML} || #tag{data = XML} <- get_tags(reference, Tags)]. + +todos(Tags, Opts) -> + case proplists:get_bool(todo, Opts) of + true -> + [{todo, XML} || #tag{data = XML} <- get_tags('todo', Tags)]; + false -> + [] + end. + +signature(Ts, As, Env) -> + case get_tags(spec, Ts) of + [T] -> + Spec = T#tag.data, + R = merge_returns(Spec, Ts), + As0 = edoc_types:arg_names(Spec), + Ds0 = edoc_types:arg_descs(Spec), + %% choose names in spec before names in code + P = dict:from_list(params(Ts)), + As1 = merge_args(As0, As, Ds0, P), + %% check_params(As1, P), + Spec1 = edoc_types:set_arg_names(Spec, [A || {A,_} <- As1]), + {As1, R, [edoc_types:to_xml(Spec1, Env)]}; + [] -> + S = sets:new(), + {[{A, ""} || A <- fix_argnames(As, S, 1)], [], []} + end. + +params(Ts) -> + [T#tag.data || T <- get_tags(param, Ts)]. + +%% check_params(As, P) -> +%% case dict:keys(P) -- [N || {N,_} <- As] of +%% [] -> ok; +%% Ps -> error %% TODO: report @param declarations with no match +%% end. + +merge_returns(Spec, Ts) -> + case get_tags(returns, Ts) of + [] -> + case edoc_types:range_desc(Spec) of + "" -> []; + Txt -> [Txt] + end; + [T] -> T#tag.data + end. + +%% Names are chosen from the first list (the specification) if possible. +%% Descriptions specified with @param (in P dict) override descriptions +%% from the spec (in Ds). + +merge_args(As, As1, Ds, P) -> + merge_args(As, As1, Ds, [], P, sets:new(), 1). + +merge_args(['_' | As], ['_' | As1], [D | Ds], Rs, P, S, N) -> + merge_args(As, As1, Ds, Rs, P, S, N, make_name(N, S), D); +merge_args(['_' | As], [A | As1], [D | Ds], Rs, P, S, N) -> + merge_args(As, As1, Ds, Rs, P, S, N, A, D); +merge_args([A | As], [_ | As1], [D | Ds], Rs, P, S, N) -> + merge_args(As, As1, Ds, Rs, P, S, N, A, D); +merge_args([], [], [], Rs, _P, _S, _N) -> + lists:reverse(Rs). + +merge_args(As, As1, Ds, Rs, P, S, N, A, D0) -> + D = case dict:find(A, P) of + {ok, D1} -> D1; + error when D0 =:= [] -> []; % no description + error -> [D0] % a simple-xml text element + end, + merge_args(As, As1, Ds, [{A, D} | Rs], P, + sets:add_element(A, S), N + 1). + +fix_argnames(['_' | As], S, N) -> + A = make_name(N, S), + [A | fix_argnames(As, sets:add_element(A, S), N + 1)]; +fix_argnames([A | As], S, N) -> + [A | fix_argnames(As, sets:add_element(A, S), N + 1)]; +fix_argnames([], _S, _N) -> + []. + +make_name(N, S) -> + make_name(N, S, "X"). + +make_name(N, S, Base) -> + A = list_to_atom(Base ++ integer_to_list(N)), + case sets:is_element(A, S) of + true -> + make_name(N, S, Base ++ "x"); + false -> + A + end. + +get_entry(Name, [#entry{name = Name} = E | _Es]) -> E; +get_entry(Name, [_ | Es]) -> get_entry(Name, Es). + +get_tags(Tag, [#tag{name = Tag} = T | Ts]) -> [T | get_tags(Tag, Ts)]; +get_tags(Tag, [_ | Ts]) -> get_tags(Tag, Ts); +get_tags(_, []) -> []. + +%% --------------------------------------------------------------------- + +type(T, Env) -> + xmerl_lib:expand_element({type, [edoc_types:to_xml(T, Env)]}). + +%% <!ELEMENT package (description?, author*, copyright?, version?, +%% since?, deprecated?, see*, reference*, todo?, +%% modules)> +%% <!ATTLIST package +%% name CDATA #REQUIRED +%% root CDATA #IMPLIED> +%% <!ELEMENT modules (module+)> + +package(Package, Tags, Env, Opts) -> + Env1 = Env#env{package = Package, + root = edoc_refs:relative_package_path('', Package)}, + xmerl_lib:expand_element(package_1(Package, Tags, Env1, Opts)). + +package_1(Package, Tags, Env, Opts) -> + {package, [{root, Env#env.root}], + ([{packageName, [atom_to_list(Package)]}] + ++ get_doc(Tags) + ++ authors(Tags) + ++ get_copyright(Tags) + ++ get_version(Tags) + ++ get_since(Tags) + ++ get_deprecated(Tags) + ++ sees(Tags, Env) + ++ references(Tags) + ++ todos(Tags, Opts)) + }. + +%% <!ELEMENT overview (title, description?, author*, copyright?, version?, +%% since?, see*, reference*, todo?, packages, modules)> +%% <!ATTLIST overview +%% root CDATA #IMPLIED> +%% <!ELEMENT title (#PCDATA)> + +overview(Title, Tags, Env, Opts) -> + Env1 = Env#env{package = '', + root = ""}, + xmerl_lib:expand_element(overview_1(Title, Tags, Env1, Opts)). + +overview_1(Title, Tags, Env, Opts) -> + {overview, [{root, Env#env.root}], + ([{title, [get_title(Tags, Title)]}] + ++ get_doc(Tags) + ++ authors(Tags) + ++ get_copyright(Tags) + ++ get_version(Tags) + ++ get_since(Tags) + ++ sees(Tags, Env) + ++ references(Tags) + ++ todos(Tags, Opts)) + }. + +get_title(Ts, Default) -> + case get_tags(title, Ts) of + [T] -> + T#tag.data; + [] -> + Default + end. |