aboutsummaryrefslogtreecommitdiffstats
path: root/lib/edoc/src/edoc_tags.erl
diff options
context:
space:
mode:
authorErlang/OTP <[email protected]>2009-11-20 14:54:40 +0000
committerErlang/OTP <[email protected]>2009-11-20 14:54:40 +0000
commit84adefa331c4159d432d22840663c38f155cd4c1 (patch)
treebff9a9c66adda4df2106dfd0e5c053ab182a12bd /lib/edoc/src/edoc_tags.erl
downloadotp-84adefa331c4159d432d22840663c38f155cd4c1.tar.gz
otp-84adefa331c4159d432d22840663c38f155cd4c1.tar.bz2
otp-84adefa331c4159d432d22840663c38f155cd4c1.zip
The R13B03 release.OTP_R13B03
Diffstat (limited to 'lib/edoc/src/edoc_tags.erl')
-rw-r--r--lib/edoc/src/edoc_tags.erl373
1 files changed, 373 insertions, 0 deletions
diff --git a/lib/edoc/src/edoc_tags.erl b/lib/edoc/src/edoc_tags.erl
new file mode 100644
index 0000000000..1f2cb99c75
--- /dev/null
+++ b/lib/edoc/src/edoc_tags.erl
@@ -0,0 +1,373 @@
+%% =====================================================================
+%% 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 2001-2003 Richard Carlsson
+%% @author Richard Carlsson <[email protected]>
+%% @see edoc
+%% @end
+%% =====================================================================
+
+%% @doc EDoc tag scanning.
+
+%% TODO: tag/macro for including the code of a function as `<pre>'-text.
+%% TODO: consider new tag: @license text
+
+-module(edoc_tags).
+
+-export([tags/0, tags/1, tag_names/0, tag_parsers/0, scan_lines/2,
+ filter_tags/3, check_tags/4, parse_tags/4]).
+
+-import(edoc_report, [report/4, warning/4, error/3]).
+
+-include("edoc.hrl").
+-include("edoc_types.hrl").
+
+
+%% Tags are described by {Name, Parser, Flags}.
+%% Name = atom()
+%% Parser = text | xml | (Text,Line,Where) -> term()
+%% Flags = [Flag]
+%% Flag = module | function | package | overview | single
+%%
+%% Note that the pseudo-tag '@clear' is not listed here.
+%% (Cf. the function 'filter_tags'.)
+%%
+%% Rejected tag suggestions:
+%% - @keywords (never up to date; free text search is better)
+%% - @uses [modules] (never up to date; false dependencies)
+%% - @maintainer (never up to date; duplicates author info)
+%% - @contributor (unnecessary; mention in normal documentation)
+%% - @creator (unnecessary; already have copyright/author)
+%% - @history (never properly updated; use version control etc.)
+%% - @category (useless; superseded by keywords or free text search)
+
+tags() ->
+ All = [module,footer,function,package,overview],
+ [{author, fun parse_contact/4, [module,package,overview]},
+ {copyright, text, [module,package,overview,single]},
+ {deprecated, xml, [module,function,package,single]},
+ {doc, xml, [module,function,package,overview,single]},
+ {docfile, fun parse_file/4, All},
+ {'end', text, All},
+ {equiv, fun parse_expr/4, [function,single]},
+ {headerfile, fun parse_header/4, All},
+ {hidden, text, [module,function,single]},
+ {param, fun parse_param/4, [function]},
+ {private, text, [module,function,single]},
+ {reference, xml, [module,footer,package,overview]},
+ {returns, xml, [function,single]},
+ {see, fun parse_see/4, [module,function,package,overview]},
+ {since, text, [module,function,package,overview,single]},
+ {spec, fun parse_spec/4, [function,single]},
+ {throws, fun parse_throws/4, [function,single]},
+ {title, text, [overview,single]},
+ {'TODO', xml, All},
+ {todo, xml, All},
+ {type, fun parse_typedef/4, [module,footer,function]},
+ {version, text, [module,package,overview,single]}].
+
+aliases('TODO') -> todo;
+aliases(return) -> returns;
+aliases(T) -> T.
+
+%% Selecting tags based on flags.
+tags(Flag) ->
+ [T || {T,_,Fs} <- tags(), lists:member(Flag, Fs)].
+
+%% The set of known tags.
+tag_names() ->
+ [T || {T,_,_} <- tags()].
+
+%% The pairs of tags and their parsers.
+tag_parsers() ->
+ [{T,F} || {T,F,_} <- tags()].
+
+
+%% Scanning lines of comment text.
+
+scan_lines(Ss, L) ->
+ lists:reverse(scan_lines(Ss, L, [])).
+
+scan_lines([S | Ss], L, As) ->
+ scan_lines(S, Ss, L, As);
+scan_lines([], _L, As) ->
+ As.
+
+%% Looking for a leading '@', skipping whitespace.
+%% Also accept "TODO:" at start of line as equivalent to "@TODO".
+
+scan_lines([$\s | Cs], Ss, L, As) -> scan_lines(Cs, Ss, L, As);
+scan_lines([$\t | Cs], Ss, L, As) -> scan_lines(Cs, Ss, L, As);
+scan_lines([$@ | Cs], Ss, L, As) -> scan_tag(Cs, Ss, L, As, []);
+scan_lines(("TODO:"++_)=Cs, Ss, L, As) -> scan_tag(Cs, Ss, L, As, []);
+scan_lines(_, Ss, L, As) -> scan_lines(Ss, L + 1, As).
+
+%% Scanning chars following '@', accepting only nonempty valid names.
+%% See edoc_lib:is_name/1 for details on what is a valid name. In tags
+%% we also allow the initial letter to be uppercase or underscore.
+
+scan_tag([C | Cs], Ss, L, As, Ts) when C >= $a, C =< $z ->
+ scan_tag_1(Cs, Ss, L, As, [C | Ts]);
+scan_tag([C | Cs], Ss, L, As, Ts) when C >= $A, C =< $Z ->
+ scan_tag_1(Cs, Ss, L, As, [C | Ts]);
+scan_tag([C | Cs], Ss, L, As, Ts) when C >= $\300, C =< $\377,
+ C =/= $\327, C =/= $\367 ->
+ scan_tag_1(Cs, Ss, L, As, [C | Ts]);
+scan_tag([$_ | Cs], Ss, L, As, Ts) ->
+ scan_tag_1(Cs, Ss, L, As, [$_ | Ts]);
+scan_tag(_Cs, Ss, L, As, _Ts) ->
+ scan_lines(Ss, L + 1, As). % not a valid name
+
+scan_tag_1([C | Cs], Ss, L, As, Ts) when C >= $a, C =< $z ->
+ scan_tag_1(Cs, Ss, L, As, [C | Ts]);
+scan_tag_1([C | Cs], Ss, L, As, Ts) when C >= $A, C =< $Z ->
+ scan_tag_1(Cs, Ss, L, As, [C | Ts]);
+scan_tag_1([C | Cs], Ss, L, As, Ts) when C >= $0, C =< $9 ->
+ scan_tag_1(Cs, Ss, L, As, [C | Ts]);
+scan_tag_1([C | Cs], Ss, L, As, Ts) when C >= $\300, C =< $\377,
+ C =/= $\327, C =/= $\367 ->
+ scan_tag_1(Cs, Ss, L, As, [C | Ts]);
+scan_tag_1([$_ | Cs], Ss, L, As, Ts) ->
+ scan_tag_1(Cs, Ss, L, As, [$_ | Ts]);
+scan_tag_1(Cs, Ss, L, As, Ts) ->
+ scan_tag_2(Cs, Ss, L, As, {Ts, L}).
+
+%% Check that the tag is followed by whitespace, linebreak, or colon.
+
+scan_tag_2([$\s | Cs], Ss, L, As, T) ->
+ scan_tag_lines(Ss, T, [Cs], L + 1, As);
+scan_tag_2([$\t | Cs], Ss, L, As, T) ->
+ scan_tag_lines(Ss, T, [Cs], L + 1, As);
+scan_tag_2([$: | Cs], Ss, L, As, T) ->
+ scan_tag_lines(Ss, T, [Cs], L + 1, As);
+scan_tag_2([], Ss, L, As, T) ->
+ scan_tag_lines(Ss, T, [[]], L + 1, As);
+scan_tag_2(_, Ss, L, As, _T) ->
+ scan_lines(Ss, L + 1, As).
+
+%% Scanning lines after a tag is found.
+
+scan_tag_lines([S | Ss], T, Ss1, L, As) ->
+ scan_tag_lines(S, S, Ss, T, Ss1, L, As);
+scan_tag_lines([], {Ts, L1}, Ss1, _L, As) ->
+ [make_tag(Ts, L1, Ss1) | As].
+
+%% Collecting tag text lines until end of comment or next tagged line.
+
+scan_tag_lines([$\s | Cs], S, Ss, T, Ss1, L, As) ->
+ scan_tag_lines(Cs, S, Ss, T, Ss1, L, As);
+scan_tag_lines([$\t | Cs], S, Ss, T, Ss1, L, As) ->
+ scan_tag_lines(Cs, S, Ss, T, Ss1, L, As);
+scan_tag_lines([$@, C | _Cs], S, Ss, {Ts, L1}, Ss1, L, As)
+ when C >= $a, C =< $z ->
+ scan_lines(S, Ss, L, [make_tag(Ts, L1, Ss1) | As]);
+scan_tag_lines([$@, C | _Cs], S, Ss, {Ts, L1}, Ss1, L, As)
+ when C >= $A, C =< $Z ->
+ scan_lines(S, Ss, L, [make_tag(Ts, L1, Ss1) | As]);
+scan_tag_lines([$@, C | _Cs], S, Ss, {Ts, L1}, Ss1, L, As)
+ when C >= $\300, C =< $\377, C =/= $\327, C =/= $\367 ->
+ scan_lines(S, Ss, L, [make_tag(Ts, L1, Ss1) | As]);
+scan_tag_lines("TODO:"++_, S, Ss, {Ts, L1}, Ss1, L, As) ->
+ scan_lines(S, Ss, L, [make_tag(Ts, L1, Ss1) | As]);
+scan_tag_lines(_Cs, S, Ss, T, Ss1, L, As) ->
+ scan_tag_lines(Ss, T, [S | Ss1], L + 1, As).
+
+make_tag(Cs, L, Ss) ->
+ #tag{name = aliases(list_to_atom(lists:reverse(Cs))),
+ line = L,
+ data = append_lines(lists:reverse(Ss))}.
+
+%% Flattening lines of text and inserting line breaks.
+
+append_lines([L]) -> L;
+append_lines([L | Ls]) -> L ++ [$\n | append_lines(Ls)];
+append_lines([]) -> [].
+
+%% Filtering out unknown tags.
+
+filter_tags(Ts, Tags, Where) ->
+ filter_tags(Ts, Tags, Where, []).
+
+filter_tags([#tag{name = clear} | Ts], Tags, Where, _Ts1) ->
+ filter_tags(Ts, Tags, Where);
+filter_tags([#tag{name = N, line = L} = T | Ts], Tags, Where, Ts1) ->
+ case sets:is_element(N, Tags) of
+ true ->
+ filter_tags(Ts, Tags, Where, [T | Ts1]);
+ false ->
+ warning(L, Where, "tag @~s not recognized.", [N]),
+ filter_tags(Ts, Tags, Where, Ts1)
+ end;
+filter_tags([], _, _, Ts) ->
+ lists:reverse(Ts).
+
+%% Check occurrances of tags.
+
+check_tags(Ts, Allow, Single, Where) ->
+ check_tags(Ts, Allow, Single, Where, false, sets:new()).
+
+check_tags([#tag{name = T, line = L} | Ts], Allow, Single, Where, Error, Seen) ->
+ case sets:is_element(T, Seen) of
+ true ->
+ case sets:is_element(T, Single) of
+ false ->
+ check_tags(Ts, Allow, Single, Where, Error, Seen);
+ true ->
+ report(L, Where, "multiple @~s tag.", [T]),
+ check_tags(Ts, Allow, Single, Where, true, Seen)
+ end;
+ false ->
+ Seen1 = sets:add_element(T, Seen),
+ case sets:is_element(T, Allow) of
+ true ->
+ check_tags(Ts, Allow, Single, Where, Error, Seen1);
+ false ->
+ report(L, Where, "tag @~s not allowed here.", [T]),
+ check_tags(Ts, Allow, Single, Where, true, Seen1)
+ end
+ end;
+check_tags([], _, _, _, Error, _) ->
+ Error.
+
+
+%% Parses tag contents for specific tags.
+
+parse_tags(Ts, How, Env, Where) ->
+ parse_tags(Ts, How, Env, Where, []).
+
+parse_tags([#tag{name = Name} = T | Ts], How, Env, Where, Ts1) ->
+ case dict:fetch(Name, How) of
+ text ->
+ parse_tags(Ts, How, Env, Where, [T | Ts1]);
+ xml ->
+ [T1] = parse_tag(T, fun parse_xml/4, Env, Where),
+ parse_tags(Ts, How, Env, Where, [T1 | Ts1]);
+ F when is_function(F) ->
+ Ts2 = parse_tag(T, F, Env, Where),
+ parse_tags(Ts, How, Env, Where, lists:reverse(Ts2, Ts1))
+ end;
+parse_tags([], _How, _Env, _Where, Ts) ->
+ lists:reverse(Ts).
+
+parse_tag(T, F, Env, Where) ->
+ case catch {ok, F(T#tag.data, T#tag.line, Env, Where)} of
+ {ok, Data} ->
+ [T#tag{data = Data}];
+ {expand, Ts} ->
+ Ts;
+ {error, L, Error} ->
+ error(L, Where, Error),
+ exit(error);
+ {'EXIT', R} -> exit(R);
+ Other -> throw(Other)
+ end.
+
+%% parser functions for the built-in content types. They also perform
+%% some sanity checks on the results.
+
+parse_xml(Data, Line, _Env, _Where) ->
+ edoc_wiki:parse_xml(Data, Line).
+
+parse_see(Data, Line, _Env, _Where) ->
+ edoc_parser:parse_see(Data, Line).
+
+parse_expr(Data, Line, _Env, _Where) ->
+ edoc_lib:parse_expr(Data, Line).
+
+parse_spec(Data, Line, _Env, {_, {F, A}} = _Where) ->
+ Spec = edoc_parser:parse_spec(Data, Line),
+ #t_spec{name = N, type = #t_fun{args = As}} = Spec,
+ if length(As) /= A ->
+ throw_error(Line, "@spec arity does not match.");
+ true ->
+ case N of
+ undefined ->
+ Spec#t_spec{name = #t_name{module = [], name = F}};
+ #t_name{module = [], name = F} ->
+ Spec;
+ _ ->
+ throw_error(Line, "@spec name does not match.")
+ end
+ end.
+
+parse_param(Data, Line, _Env, {_, {_F, _A}} = _Where) ->
+ edoc_parser:parse_param(Data, Line).
+
+parse_throws(Data, Line, _Env, {_, {_F, _A}} = _Where) ->
+ edoc_parser:parse_throws(Data, Line).
+
+parse_contact(Data, Line, _Env, _Where) ->
+ case edoc_lib:parse_contact(Data, Line) of
+ {"", "", _URI} ->
+ throw_error(Line, "must specify name or e-mail.");
+ Info ->
+ Info
+ end.
+
+parse_typedef(Data, Line, _Env, _Where) ->
+ Def = edoc_parser:parse_typedef(Data, Line),
+ {#t_typedef{name = #t_name{name = T}}, _} = Def,
+ case edoc_types:is_predefined(T) of
+ true ->
+ throw_error(Line, {"redefining built-in type '~w'.", [T]});
+ false ->
+ Def
+ end.
+
+parse_file(Data, Line, Env, _Where) ->
+ case edoc_lib:parse_expr(Data, Line) of
+ {string, _, File0} ->
+ File = edoc_lib:strip_space(File0),
+ case edoc_extract:file(File, module, Env, []) of
+ {ok, Ts} ->
+ throw({expand, Ts});
+ {error, R} ->
+ throw_error(Line, {read_file, File, R})
+ end;
+ _ ->
+ throw_error(Line, file_not_string)
+ end.
+
+parse_header(Data, Line, Env, {Where, _}) ->
+ parse_header(Data, Line, Env, Where);
+parse_header(Data, Line, Env, Where) when is_list(Where) ->
+ case edoc_lib:parse_expr(Data, Line) of
+ {string, _, File} ->
+ Dir = filename:dirname(Where),
+ Path = Env#env.includes ++ [Dir],
+ case edoc_lib:find_file(Path, "", File) of
+ "" ->
+ throw_error(Line, {file_not_found, File});
+ File1 ->
+ Ts = edoc_extract:header(File1, Env, []),
+ throw({expand, Ts})
+ end;
+ _ ->
+ throw_error(Line, file_not_string)
+ end.
+
+throw_error(L, {read_file, File, R}) ->
+ throw_error(L, {"error reading file '~s': ~w",
+ [edoc_lib:filename(File), R]});
+throw_error(L, {file_not_found, F}) ->
+ throw_error(L, {"file not found: ~s", [F]});
+throw_error(L, file_not_string) ->
+ throw_error(L, "expected file name as a string");
+throw_error(L, D) ->
+ throw({error, L, D}).