diff options
author | Hans Bolinder <[email protected]> | 2010-06-22 09:42:44 +0200 |
---|---|---|
committer | Hans Bolinder <[email protected]> | 2011-03-10 12:57:37 +0100 |
commit | 8052b98f596db048467c0c57cbaac1d3a27687ad (patch) | |
tree | 144fcf14894c8a5c7df3778d7b1d91fa6a72a1cb /lib/edoc/src/edoc_specs.erl | |
parent | cc5885e81da81dc52bd7890ff3612a48d2f4a9f2 (diff) | |
download | otp-8052b98f596db048467c0c57cbaac1d3a27687ad.tar.gz otp-8052b98f596db048467c0c57cbaac1d3a27687ad.tar.bz2 otp-8052b98f596db048467c0c57cbaac1d3a27687ad.zip |
Make Erlang specifications and types available in EDoc
It is now possible to use Erlang specifications and types in EDoc
documentation. Erlang specifications and types will be used unless
there is also a function specification (@spec) or a type alias (@type)
with the same name. In the current implementation the placement of
-spec matters: it should be placed where the @spec would otherwise
have been placed.
Not all Erlang types are included in the documentation, but only those
exported by some export_type declaration or used by some documented
Erlang specification (-spec).
There is currently no support for overloaded Erlang specifications.
The syntax definitions of EDoc have been augmented to cope with most
of the Erlang types. (But we recommend that Erlang types should be
used instead.)
edoc:read_source() takes one new option, report_missing_types.
edoc_layout:module() takes one new option, pretty_printer.
Diffstat (limited to 'lib/edoc/src/edoc_specs.erl')
-rw-r--r-- | lib/edoc/src/edoc_specs.erl | 603 |
1 files changed, 603 insertions, 0 deletions
diff --git a/lib/edoc/src/edoc_specs.erl b/lib/edoc/src/edoc_specs.erl new file mode 100644 index 0000000000..45016ef85a --- /dev/null +++ b/lib/edoc/src/edoc_specs.erl @@ -0,0 +1,603 @@ +% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2011. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% + +%% @doc EDoc interface to Erlang specifications and types. + +-module(edoc_specs). + +-export([type/2, spec/2, dummy_spec/1, docs/2]). + +-export([add_data/4, tag/1, is_tag/1]). + +-include("edoc.hrl"). +-include("edoc_types.hrl"). + +-type proplist() :: [proplists:property()]. +-type syntaxTree() :: erl_syntax:syntaxTree(). + +-define(TOP_TYPE, term). + +%% +%% Exported functions +%% + +-spec type(Form::syntaxTree(), TypeDocs::dict()) -> #tag{}. + +%% @doc Convert an Erlang type to EDoc representation. +%% TypeDocs is a dict of {Name, Doc}. +%% Note: #t_typedef.name is set to {record, R} for record types. +type(Form, TypeDocs) -> + {Name, Data0} = erl_syntax_lib:analyze_wild_attribute(Form), + type = tag(Name), + {TypeName, Type, Args, Doc} = + case Data0 of + {{record, R}, Fs, []} -> + L = erl_syntax:get_pos(Form), + {{record, R}, {type, L, record, [{atom,L,R} | Fs]}, [], ""}; + {N,T,As} -> + Doc0 = + case dict:find({N, length(As)}, TypeDocs) of + {ok, Doc1} -> + Doc1; + error -> + "" + end, + {#t_name{name = N}, T, As, Doc0} + end, + #tag{name = type, line = element(2, Type), + origin = code, + data = {#t_typedef{name = TypeName, + args = d2e(Args), + type = d2e(opaque2abstr(Name, Type))}, + Doc}}. + +-spec spec(Form::syntaxTree(), ClauseN::pos_integer()) -> #tag{}. + +%% @doc Convert an Erlang spec to EDoc representation. +spec(Form, Clause) -> + {Name, _Arity, TypeSpecs} = get_spec(Form), + TypeSpec = lists:nth(Clause, TypeSpecs), + #tag{name = spec, line = element(2, TypeSpec), + origin = code, + data = aspec(d2e(TypeSpec), Name)}. + +-spec dummy_spec(Form::syntaxTree()) -> #tag{}. + +%% @doc Create a #tag{} record where data is a string with the name of +%% the given Erlang spec and an empty list of arguments. +dummy_spec(Form) -> + {#t_name{name = Name}, Arity, TypeSpecs} = get_spec(Form), + As = string:join(lists:duplicate(Arity, "_X"), ","), + S = lists:flatten(io_lib:format("~p(~s) -> true\n", [Name, As])), + #tag{name = spec, line = element(2, hd(TypeSpecs)), + origin = code, data = S}. + +-spec docs(Forms::[syntaxTree()], CommentFun) -> dict() when + CommentFun :: fun(([syntaxTree()], Line :: term()) -> #tag{}). + +%% @doc Find comments after -type/-opaque declarations. +%% Postcomments "inside" the type are skipped. +docs(Forms, CommentFun) -> + find_type_docs(Forms, [], CommentFun). + +-type entry() :: #entry{}. +-type module_info() :: #module{}. +-type entries() :: [entry()]. +-spec add_data(Entries::entries(), Options::proplist(), + File::file:filename(), Module::module_info()) -> entries(). + +%% @doc Create tags a la EDoc for Erlang specifications and types. +%% Exported types and types used (indirectly) by Erlang specs are +%% added to the entries. +add_data(Entries, Opts, File, Module) -> + TypeDefs0 = espec_types(Entries), + TypeTable = ets:new(etypes, [ordered_set]), + Es1 = expand_records(Entries, TypeDefs0, TypeTable, Opts, File, Module), + Es = [use_tags(E, TypeTable) || E <- Es1], + true = ets:delete(TypeTable), + Es. + +%% +%% Local functions +%% + +aspec(#t_spec{}=Spec, Name) -> + Spec#t_spec{name = Name}; +aspec(Type, Name) -> + #t_spec{name = Name, type = Type}. + +get_spec(Form) -> + {spec, Data0} = erl_syntax_lib:analyze_wild_attribute(Form), + case Data0 of + {{F,A}, D} -> + {#t_name{name = F}, A, D}; + {{M,F,A}, D} -> + {#t_name{module = M, name = F}, A, D} + end. + +find_type_docs([], Cs, _Fun) -> + dict:from_list(Cs); +find_type_docs([F | Fs], Cs, Fun) -> + try get_name_and_last_line(F) of + {Name, LastTypeLine} -> + C0 = erl_syntax:comment(["% @type f(). "]), + C1 = erl_syntax:set_pos(C0, LastTypeLine), + %% Postcomments before the dot after the typespec are ignored. + C2 = [C1 | [C || + C <- erl_syntax:get_postcomments(F), + get_line(erl_syntax:get_pos(C)) >= LastTypeLine]], + C3 = collect_comments(Fs, LastTypeLine), + #tag{data = Doc0} = Fun(lists:reverse(C2 ++ C3), LastTypeLine), + case strip(Doc0) of % Strip away "f(). \n" + "" -> + find_type_docs(Fs, Cs, Fun); + Doc -> + W = edoc_wiki:parse_xml(Doc, LastTypeLine), + find_type_docs(Fs, [{Name, W}|Cs], Fun) + end + catch _:_ -> + find_type_docs(Fs, Cs, Fun) + end. + +collect_comments([], _Line) -> + []; +collect_comments([F | Fs], Line) -> + L1 = get_line(erl_syntax:get_pos(F)), + if + L1 =:= Line + 1; + L1 =:= Line -> % a separate postcomment + case is_comment(F) of + true -> + [F | collect_comments(Fs, L1)]; + false -> + [] + end; + true -> + [] + end. +%% Note: there is a creepy bug concerning an include file terminated +%% by a -type attribute and the include statement is followed by a +%% comment (which is not meant to be documentation of the type). + +is_comment(F) -> + erl_syntax_lib:analyze_form(F) =:= comment. + +strip("") -> + ""; +strip([$\n | S]) -> + S; +strip([_ | S]) -> + strip(S). + +%% Find the type name and the greatest line number of a type spec. +%% Should use syntax_tools but this has to do for now. +get_name_and_last_line(F) -> + {Name, Data} = erl_syntax_lib:analyze_wild_attribute(F), + type = edoc_specs:tag(Name), + Attr = {attribute, erl_syntax:get_pos(F), Name, Data}, + Ref = make_ref(), + Fun = fun(L) -> {Ref, get_line(L)} end, + TypeName = case Data of + {N, _T, As} when is_atom(N) -> % skip records + {N, length(As)} + end, + Line = gll(erl_lint:modify_line(Attr, Fun), Ref), + {TypeName, Line}. + +gll({Ref, Line}, Ref) -> + Line; +gll([], _Ref) -> + 0; +gll(List, Ref) when is_list(List) -> + lists:max([gll(E, Ref) || E <- List]); +gll(Tuple, Ref) when is_tuple(Tuple) -> + gll(tuple_to_list(Tuple), Ref); +gll(_, _) -> + 0. + +get_line(Pos) -> + {line, Line} = erl_scan:attributes_info(Pos, line), + Line. + +%% Collect all Erlang types. Types in comments (@type) shadow Erlang +%% types (-spec/-opaque). +espec_types(Entries) -> + Tags = get_all_tags(Entries), + CommTs = [type_name(T) || + #tag{name = type, origin = comment}=T <- Tags], + CT = sets:from_list(CommTs), + [T || #tag{name = Name, origin = code}=T <- Tags, + tag(Name) =:= type, + not sets:is_element(type_name(T), CT)]. + +get_all_tags(Es) -> + lists:flatmap(fun (#entry{data = Ts}) -> Ts end, Es). + +%% Turns an opaque type into an abstract datatype. +%% Note: top level annotation is ignored. +opaque2abstr(opaque, _T) -> undefined; +opaque2abstr(type, T) -> T. + +%% Replaces the parameters extracted from the source (by +%% edoc_extract:parameters/1) by annotations and variable names, using +%% the source parameters as default values +%% Selects seen types (exported types, types used by specs), +%% skips records and unused types. +use_tags(#entry{data = Ts}=E, TypeTable) -> + use_tags(Ts, E, TypeTable, []). + +use_tags([], E, _TypeTable, NTs) -> + E#entry{data = lists:reverse(NTs)}; +use_tags([#tag{origin = code}=T | Ts], E, TypeTable, NTs) -> + case tag(T#tag.name) of + spec -> + Args = params(T, E#entry.args), + use_tags(Ts, E#entry{args = Args}, TypeTable, [T | NTs]); + type -> + TypeName = type_name(T), + case ets:lookup(TypeTable, TypeName) of + [{{{record,_},_},_,_}] -> + use_tags(Ts, E, TypeTable, NTs); + [{_,_,not_seen}] -> + use_tags(Ts, E, TypeTable, NTs); + [] -> + use_tags(Ts, E, TypeTable, NTs); + [{TypeName, Tag, seen}] -> + use_tags(Ts, E, TypeTable, [Tag | NTs]) + end + end; +use_tags([T | Ts], E, TypeTable, NTs) -> + use_tags(Ts, E, TypeTable, [T | NTs]). + +params(#tag{name = spec, data=#t_spec{type = #t_fun{args = As}}}, Default) -> + parms(As, Default). + +parms([], []) -> + []; +parms([A | As], [D | Ds]) -> + [param(A, D) | parms(As, Ds)]. + +param(#t_list{type = Type}, Default) -> + param(Type, Default); +param(#t_paren{type = Type}, Default) -> + param(Type, Default); +param(#t_nonempty_list{type = Type}, Default) -> + param(Type, Default); +param(#t_record{name = #t_atom{val = Name}}, _Default) -> + list_to_atom(capitalize(atom_to_list(Name))); +param(T, Default) -> + arg_name(?t_ann(T), Default). + +capitalize([C | Cs]) when C >= $a, C =< $z -> [C - 32 | Cs]; +capitalize(Cs) -> Cs. + +%% Like edoc_types:arg_name/1 +arg_name([], Default) -> + Default; +arg_name([A | As], Default) -> + case is_name(A) of + true -> A; + false -> arg_name(As, Default) + end. + +is_name(A) -> + is_atom(A). + +d2e({ann_type,_,[V, T0]}) -> + %% Note: the -spec/-type syntax allows annotations everywhere, but + %% EDoc does not. The fact that the annotation is added to the + %% type here does not necessarily mean that it will be used by the + %% layout module. + T = d2e(T0), + ?add_t_ann(T, element(3, V)); +d2e({type,_,no_return,[]}) -> + #t_type{name = #t_name{name = none}}; +d2e({remote_type,_,[{atom,_,M},{atom,_,F},Ts0]}) -> + Ts = d2e(Ts0), + typevar_anno(#t_type{name = #t_name{module = M, name = F}, args = Ts}, Ts); +d2e({type,_,'fun',[{type,_,product,As0},Ran0]}) -> + Ts = [Ran|As] = d2e([Ran0|As0]), + %% Assume that the linter has checked type variables. + typevar_anno(#t_fun{args = As, range = Ran}, Ts); +d2e({type,_,'fun',[A0={type,_,any},Ran0]}) -> + Ts = [A, Ran] = d2e([A0, Ran0]), + typevar_anno(#t_fun{args = [A], range = Ran}, Ts); +d2e({type,_,'fun',[]}) -> + #t_type{name = #t_name{name = function}, args = []}; +d2e({type,_,any}) -> + #t_var{name = '...'}; % Kludge... not a type variable! +d2e({type,_,nil,[]}) -> + #t_nil{}; +d2e({paren_type,_,[T]}) -> + #t_paren{type = d2e(T)}; +d2e({type,_,list,[T0]}) -> + T = d2e(T0), + typevar_anno(#t_list{type = T}, [T]); +d2e({type,_,nonempty_list,[T0]}) -> + T = d2e(T0), + typevar_anno(#t_nonempty_list{type = T}, [T]); +d2e({type,_,bounded_fun,[T,Gs]}) -> + [F0|Defs] = d2e([T|Gs]), + F = ?set_t_ann(F0, lists:keydelete(type_variables, 1, ?t_ann(F0))), + %% Assume that the linter has checked type variables. + #t_spec{type = typevar_anno(F, [F0]), defs = Defs}; +d2e({type,_,range,[V1,V2]}) -> + {integer,_,I1} = erl_eval:partial_eval(V1), + {integer,_,I2} = erl_eval:partial_eval(V2), + #t_integer_range{from = I1, to = I2}; +d2e({type,_,constraint,[Sub,Ts0]}) -> + case {Sub,Ts0} of + {{atom,_,is_subtype},[{var,_,N},T0]} -> + Ts = [T] = d2e([T0]), + #t_def{name = #t_var{name = N}, type = typevar_anno(T, Ts)}; + {{atom,_,is_subtype},[ST0,T0]} -> + %% Should not happen. + Ts = [ST,T] = d2e([ST0,T0]), + #t_def{name = ST, type = typevar_anno(T, Ts)}; + _ -> + throw_error(element(2, Sub), "cannot handle guard", []) + end; +d2e({type,_,union,Ts0}) -> + Ts = d2e(Ts0), + typevar_anno(#t_union{types = Ts}, Ts); +d2e({type,_,tuple,any}) -> + #t_type{name = #t_name{name = tuple}, args = []}; +d2e({type,_,binary,[Base,Unit]}) -> + #t_binary{base_size = element(3, Base), + unit_size = element(3, Unit)}; +d2e({type,_,tuple,Ts0}) -> + Ts = d2e(Ts0), + typevar_anno(#t_tuple{types = Ts}, Ts); +d2e({type,_,record,[Name|Fs0]}) -> + Atom = #t_atom{val = element(3, Name)}, + Fs = d2e(Fs0), + typevar_anno(#t_record{name = Atom, fields = Fs}, Fs); +d2e({type,_,field_type,[Name,Type0]}) -> + Type = d2e(Type0), + typevar_anno(#t_field{name = #t_atom{val = element(3, Name)}, type = Type}, + [Type]); +d2e({typed_record_field,{record_field,L,Name},Type}) -> + d2e({type,L,field_type,[Name,Type]}); +d2e({typed_record_field,{record_field,L,Name,_E},Type}) -> + d2e({type,L,field_type,[Name,Type]}); +d2e({record_field,L,_Name,_E}=F) -> + d2e({typed_record_field,F,{type,L,any,[]}}); % Maybe skip... +d2e({record_field,L,_Name}=F) -> + d2e({typed_record_field,F,{type,L,any,[]}}); % Maybe skip... +d2e({type,_,Name,Types0}) -> + Types = d2e(Types0), + typevar_anno(#t_type{name = #t_name{name = Name}, args = Types}, Types); +d2e({var,_,'_'}) -> + #t_type{name = #t_name{name = ?TOP_TYPE}}; +d2e({var,_,TypeName}) -> + TypeVar = ordsets:from_list([TypeName]), + T = #t_var{name = TypeName}, + %% Annotate type variables with the name of the variable. + %% Doing so will stop edoc_layout (and possibly other layout modules) + %% from using the argument name from the source or to invent a new name. + T1 = ?add_t_ann(T, {type_variables, TypeVar}), + ?add_t_ann(T1, TypeName); +d2e(L) when is_list(L) -> + [d2e(T) || T <- L]; +d2e({atom,_,A}) -> + #t_atom{val = A}; +d2e(undefined = U) -> % opaque + U; +d2e(Expr) -> + {integer,_,I} = erl_eval:partial_eval(Expr), + #t_integer{val = I}. + +%% A type annotation (a tuple; neither an atom nor a list). +typevar_anno(Type, Ts) -> + Vs = typevars(Ts), + case ordsets:to_list(Vs) of + [] -> Type; + _ -> ?add_t_ann(Type, {type_variables, Vs}) + end. + +typevars(Ts) -> + ordsets:union(get_typevars(Ts)). + +get_typevars(Ts) -> + [Vs || T <- Ts, T =/= undefined, {type_variables, Vs} <- ?t_ann(T)]. + +-record(parms, {tab, warn, file, line}). + +%% Expands record references. Explicitly given record fields are kept, +%% but otherwise the fields from the record definition are substituted +%% for the reference. The reason is that there are no record types. +%% It is recommended to introduce types like "r() :: r{}" and then use +%% r() everywhere. The right hand side, r{}, is expanded in order to +%% show all fields. +%% Returns updated types in the ETS table DT. +expand_records(Entries, TypeDefs, DT, Opts, File, Module) -> + TypeList = [{type_name(T), T, not_seen} || T <- TypeDefs], + true = ets:insert(DT, TypeList), + Warn = proplists:get_value(report_missing_type, Opts, + ?REPORT_MISSING_TYPE) =:= true, + P = #parms{tab = DT, warn = Warn, file = File, line = 0}, + ExportedTypes = [Name || + {export_type,Ts} <- Module#module.attributes, + is_list(Ts), + {N,I} <- Ts, + ets:member(DT, Name = {#t_name{name = N}, I})], + _ = lists:foreach(fun({N,A}) -> true = seen_type(N, A, P) + end, ExportedTypes), + entries(Entries, P, Opts). + +entries([E0 | Es], P, Opts) -> + E = case edoc_data:hidden_filter([E0], Opts) of + [] -> + E0; + [_] -> + E0#entry{data = specs(E0#entry.data, P)} + end, + [E | entries(Es, P, Opts)]; +entries([], _P, _Opts) -> + []. + +specs([#tag{line = L, name = spec, origin = code, data = Spec}=Tag0 | Tags], + P0) -> + #t_spec{type = Type0, defs = Defs0} = Spec, + P = P0#parms{line = L}, + Type = xrecs(Type0, P), + Defs = xrecs(Defs0, P), + Tag = Tag0#tag{data = Spec#t_spec{type = Type, defs = Defs}}, + [Tag | specs(Tags, P)]; +specs([Tag | Tags], P) -> + [Tag | specs(Tags, P)]; +specs([], _P) -> + []. + +xrecs(#t_def{type = Type0}=T, P) -> + Type = xrecs(Type0, P), + T#t_def{type = Type}; +xrecs(#t_type{name = Name, args = Args0}=T, P) -> + Args = xrecs(Args0, P), + NArgs = length(Args), + true = seen_type(Name, NArgs, P), + T#t_type{args = Args}; +xrecs(#t_var{}=T, _P) -> + T; +xrecs(#t_fun{args = Args0, range = Range0}=T, P) -> + Args = xrecs(Args0, P), + Range = xrecs(Range0, P), + T#t_fun{args = Args, range = Range}; +xrecs(#t_tuple{types = Types0}=T, P) -> + Types = xrecs(Types0, P), + T#t_tuple{types = Types}; +xrecs(#t_list{type = Type0}=T, P) -> + Type = xrecs(Type0, P), + T#t_list{type = Type}; +xrecs(#t_nil{}=T, _P) -> + T; +xrecs(#t_paren{type = Type0}=T, P) -> + Type = xrecs(Type0, P), + T#t_paren{type = Type}; +xrecs(#t_nonempty_list{type = Type0}=T, P) -> + Type = xrecs(Type0, P), + T#t_nonempty_list{type = Type}; +xrecs(#t_atom{}=T, _P) -> + T; +xrecs(#t_integer{}=T, _P) -> + T; +xrecs(#t_integer_range{}=T, _P) -> + T; +xrecs(#t_binary{}=T, _P) -> + T; +xrecs(#t_float{}=T, _P) -> + T; +xrecs(#t_union{types = Types0}=T, P) -> + Types = xrecs(Types0, P), + T#t_union{types = Types}; +xrecs(#t_record{fields = Fields0}=T, P) -> + Fields1 = xrecs(Fields0, P), + #t_record{name = #t_atom{val = Name}} = T, + RName = {record, Name}, + true = seen_type(RName, 0, P), + Fields = select_fields(Fields1, RName, P#parms.tab), + T#t_record{fields = Fields}; +xrecs(#t_field{type = Type0}=T, P) -> + Type = xrecs(Type0, P), + T#t_field{type = Type}; +xrecs(undefined=T, _P) -> % opaque + T; +xrecs([]=T, _P) -> + T; +xrecs([E0 | Es0], P) -> + [xrecs(E0, P) | xrecs(Es0, P)]. + +seen_type(N, NArgs, P) -> + TypeName = {N, NArgs}, + #parms{tab = DT} = P, + case {ets:lookup(DT, TypeName), N} of + {[{TypeName, _, seen}], _} -> + true; + {[{TypeName, TagType, not_seen}], _} when N#t_name.module =:= [] -> + expand_datatype(TagType, proper_type, DT, P); + {[{TypeName, TagType, not_seen}], {record, _}} -> + expand_datatype(TagType, record_type, DT, P); + {[], {record, R}} -> + #parms{warn = W, line = L, file = File} = P, + [edoc_report:warning(L, File, "reference to untyped record ~w", + [R]) || W], + ets:insert(DT, {TypeName, fake, seen}); + {[], _} -> % External type or missing type. + true + end. + +expand_datatype(Tag0, Kind, DT, P0) -> + #tag{line = L, data = {T0, Doc}} = Tag0, + #t_typedef{type = Type0, defs = []} = T0, + TypeName = type_name(Tag0), + true = ets:update_element(DT, TypeName, {3, seen}), + P = P0#parms{line = L}, + Type = case Kind of + record_type -> + #t_record{fields = Fields0} = Type0, + Fields = xrecs(Fields0, P), + Type0#t_record{fields = Fields}; + proper_type -> + xrecs(Type0, P) + end, + Tag = Tag0#tag{data={T0#t_typedef{type=Type}, Doc}}, + ets:insert(DT, {TypeName, Tag, seen}). + +select_fields(Fields, Name, DT) -> + RecordName = {Name, 0}, + case ets:lookup(DT, RecordName) of + [{RecordName, fake, seen}] -> + Fields; + [{RecordName, #tag{data = {T, _Doc}}, seen}] -> + #t_typedef{args = [], type = #t_record{fields = Fs}, defs = []}=T, + [find_field(F, Fields) || F <- Fs] + end. + +find_field(F, Fs) -> + case lists:keyfind(F#t_field.name, #t_field.name, Fs) of + false -> F; + NF -> NF + end. + +type_name(#tag{name = type, + data = {#t_typedef{name = Name, args = As},_}}) -> + {Name, length(As)}. + +%% @doc Return `true' if `Tag' is one of the specification and type +%% attribute tags recognized by the Erlang compiler. + +-spec is_tag(Tag::atom()) -> boolean(). + +is_tag(opaque) -> true; +is_tag(spec) -> true; +is_tag(type) -> true; +is_tag(_) -> false. + +%% @doc Return the kind of the attribute tag. + +-type tag_kind() :: 'type' | 'spec' | 'unknown'. +-spec tag(Tag::atom()) -> tag_kind(). + +tag(opaque) -> type; +tag(spec) -> spec; +tag(type) -> type; +tag(_) -> unknown. + +throw_error(Line, S, A) -> + edoc_report:error(Line, "", io_lib:format(S, A)), + throw(error). |