aboutsummaryrefslogtreecommitdiffstats
path: root/lib/edoc/src/edoc_data.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/edoc/src/edoc_data.erl')
-rw-r--r--lib/edoc/src/edoc_data.erl545
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.