%% ===================================================================== %% 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 <richardc@it.uu.se> %% @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.