Skip to content

Commit

Permalink
import_type in edoc
Browse files Browse the repository at this point in the history
  • Loading branch information
ilya-klyuchnikov committed Oct 16, 2023
1 parent d1e27c1 commit b568c79
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 88 deletions.
3 changes: 2 additions & 1 deletion lib/edoc/src/edoc.hrl
Expand Up @@ -51,7 +51,8 @@
attributes = [],
records = [],
encoding = latin1,
file}).
file,
imported_types = #{}}).

-record(env, {module = [],
root = "",
Expand Down
34 changes: 19 additions & 15 deletions lib/edoc/src/edoc_extract.erl
Expand Up @@ -132,7 +132,8 @@ source1(Tree, File0, Env, Opts, TypeDocs) ->
Env1 = Env#env{module = Name,
root = ""},
Env2 = add_macro_defs(module_macros(Env1), Opts, Env1),
Entries1 = get_tags([Header, Footer | Entries], Env2, File, TypeDocs),
Imp = Module#module.imported_types,
Entries1 = get_tags([Header, Footer | Entries], Env2, File, TypeDocs, Imp),
Entries2 = edoc_specs:add_type_data(Entries1, Opts, File, Module),
edoc_tags:check_types(Entries2, Opts, File),
Data = edoc_data:module(Module, Entries2, Env2, Opts),
Expand Down Expand Up @@ -211,7 +212,8 @@ header(Tree, File0, Env, _Opts) ->
warning(File, "documentation before function definitions is ignored by @headerfile", []);
true -> ok
end,
[Entry] = get_tags([Footer#entry{name = header}], Env, File),
Imp = #{},
[Entry] = get_tags([Footer#entry{name = header}], Env, File, Imp),
Entry#entry.data.

%% NEW-OPTIONS: def
Expand Down Expand Up @@ -323,14 +325,16 @@ get_module_info(Forms, File) ->
Attributes = ordsets:from_list(get_list_keyval(attributes, L)),
Records = get_list_keyval(records, L),
Encoding = edoc_lib:read_encoding(File, []),
ImportedTypes = #{T => M || {import_type, {M, Ts}} <- Attributes, T <- Ts},
#module{name = Name,
parameters = Vars,
functions = Functions,
exports = ordsets:intersection(Exports, Functions),
attributes = Attributes,
records = Records,
encoding = Encoding,
file = File}.
file = File,
imported_types = ImportedTypes}.

get_list_keyval(Key, L) ->
case lists:keyfind(Key, 1, L) of
Expand Down Expand Up @@ -622,32 +626,32 @@ capitalize(Cs) -> Cs.
% footer :: sets:set(atom()),
% function :: sets:set(atom())}.

get_tags(Es, Env, File) ->
get_tags(Es, Env, File, dict:new()).
get_tags(Es, Env, File, Imp) ->
get_tags(Es, Env, File, dict:new(), Imp).

get_tags(Es, Env, File, TypeDocs) ->
get_tags(Es, Env, File, TypeDocs, Imp) ->
%% Cache this stuff for quick lookups.
Tags = #tags{names = sets:from_list(edoc_tags:tag_names()),
single = sets:from_list(edoc_tags:tags(single)),
module = sets:from_list(edoc_tags:tags(module)),
footer = sets:from_list(edoc_tags:tags(footer)),
function = sets:from_list(edoc_tags:tags(function))},
How = dict:from_list(edoc_tags:tag_parsers()),
get_tags(Es, Tags, Env, How, File, TypeDocs).
get_tags(Es, Tags, Env, How, File, TypeDocs, Imp).

get_tags([#entry{name = Name, data = {Cs,Cbs,Specs,Types,Records}} = E | Es],
Tags, Env, How, File, TypeDocs) ->
Tags, Env, How, File, TypeDocs, Imp) ->
Where = {File, Name},
Ts0 = scan_tags(Cs),
{Ts1,Specs1} = select_spec(Ts0, Where, Specs),
Ts2 = check_tags(Ts1, Tags, Where),
Ts3 = edoc_macros:expand_tags(Ts2, Env, Where),
Ts4 = edoc_tags:parse_tags(Ts3, How, Env, Where),
Ts = selected_specs(Specs1, Ts4),
ETypes = [edoc_specs:type(Type, TypeDocs) || Type <- Types ++ Records],
Ts = selected_specs(Specs1, Ts4, Imp),
ETypes = [edoc_specs:type(Type, TypeDocs, Imp) || Type <- Types ++ Records],
Callbacks = get_callbacks(Name, Cbs, TypeDocs),
[E#entry{data = Ts ++ ETypes ++ Callbacks} | get_tags(Es, Tags, Env, How, File, TypeDocs)];
get_tags([], _, _, _, _, _) ->
[E#entry{data = Ts ++ ETypes ++ Callbacks} | get_tags(Es, Tags, Env, How, File, TypeDocs, Imp)];
get_tags([], _, _, _, _, _, _) ->
[].

get_callbacks(_EntryName, CbForms, TypeDocs) ->
Expand Down Expand Up @@ -713,10 +717,10 @@ skip_specs(Ts) ->
[ T || T = #tag{name = N} <- Ts, N /= spec ].

%% If a `-spec' attribute is present, it takes precedence over `@spec' tags.
selected_specs([], Ts) ->
selected_specs([], Ts, _) ->
Ts;
selected_specs([F], Ts) ->
[edoc_specs:spec(F) | skip_specs(Ts)].
selected_specs([F], Ts, Imp) ->
[edoc_specs:spec(F, Imp) | skip_specs(Ts)].

%% Macros for modules

Expand Down
142 changes: 72 additions & 70 deletions lib/edoc/src/edoc_specs.erl
Expand Up @@ -21,7 +21,7 @@

-module(edoc_specs).

-export([type/2, spec/1, dummy_spec/1, docs/2]).
-export([type/3, spec/2, dummy_spec/1, docs/2]).

-export([add_type_data/4, tag/1, is_tag/1]).

Expand All @@ -36,12 +36,12 @@
%% Exported functions
%%

-spec type(Form::syntaxTree(), TypeDocs::dict:dict()) -> #tag{}.
-spec type(Form::syntaxTree(), TypeDocs::dict:dict(), map()) -> #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) ->
type(Form, TypeDocs, Imp) ->
{Name, Data0} = analyze_type_attribute(Form),
{TypeName, Type, Args, Doc} =
case Data0 of
Expand All @@ -64,19 +64,19 @@ type(Form, TypeDocs) ->
#tag{name = type, line = get_line(element(2, Type)),
origin = code,
data = {#t_typedef{name = TypeName,
args = d2e(Args),
type = d2e(opaque2abstr(Name, Type))},
args = d2e(Args, Imp),
type = d2e(opaque2abstr(Name, Type), Imp)},
Doc},
form = Form}.

-spec spec(Form::syntaxTree()) -> #tag{}.
-spec spec(Form::syntaxTree(), map()) -> #tag{}.

%% @doc Convert an Erlang spec to EDoc representation.
spec(Form) ->
spec(Form, Imp) ->
{Name, _Arity, TypeSpecs} = get_spec(Form),
#tag{name = spec, line = get_line(element(2, lists:nth(1, TypeSpecs))),
origin = code,
data = [aspec(d2e(TypeSpec), Name) || TypeSpec <- TypeSpecs],
data = [aspec(d2e(TypeSpec, Imp), Name) || TypeSpec <- TypeSpecs],
form = Form}.

-spec dummy_spec(Form::syntaxTree()) -> #tag{}.
Expand Down Expand Up @@ -329,133 +329,135 @@ arg_name([A | As], Default) ->
is_name(A) ->
is_atom(A).

d2e(T) ->
d2e(T, 0).
d2e(T, Imp) ->
d2e(T, 0, Imp).

d2e({ann_type,_,[V, T0]}, Prec) ->
d2e({ann_type,_,[V, T0]}, Prec, Imp) ->
%% 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.
{_L,P,R} = erl_parse:type_inop_prec('::'),
T1 = d2e(T0, R),
T1 = d2e(T0, R, Imp),
T = ?add_t_ann(T1, element(3, V)),
maybe_paren(P, Prec, T); % the only necessary call to maybe_paren()
d2e({remote_type,_,[{atom,_,M},{atom,_,F},Ts0]}, _Prec) ->
Ts = d2e(Ts0),
d2e({remote_type,_,[{atom,_,M},{atom,_,F},Ts0]}, _Prec, Imp) ->
Ts = d2e(Ts0, Imp),
typevar_anno(#t_type{name = #t_name{module = M, name = F}, args = Ts}, Ts);
d2e({type,_,'fun',[{type,_,product,As0},Ran0]}, _Prec) ->
Ts = [Ran|As] = d2e([Ran0|As0]),
d2e({type,_,'fun',[{type,_,product,As0},Ran0]}, _Prec, Imp) ->
Ts = [Ran|As] = d2e([Ran0|As0], Imp),
%% Assume that the linter has checked type variables.
typevar_anno(#t_fun{args = As, range = Ran}, Ts);
d2e({type,_,'fun',[A0={type,_,any},Ran0]}, _Prec) ->
Ts = [A, Ran] = d2e([A0, Ran0]),
d2e({type,_,'fun',[A0={type,_,any},Ran0]}, _Prec, Imp) ->
Ts = [A, Ran] = d2e([A0, Ran0], Imp),
typevar_anno(#t_fun{args = [A], range = Ran}, Ts);
d2e({type,_,'fun',[]}, _Prec) ->
d2e({type,_,'fun',[]}, _Prec, _Imp) ->
#t_type{name = #t_name{name = function}, args = []};
d2e({type,_,any}, _Prec) ->
d2e({type,_,any}, _Prec, _Imp) ->
#t_var{name = '...'}; % Kludge... not a type variable!
d2e({type,_,nil,[]}, _Prec) ->
d2e({type,_,nil,[]}, _Prec, _Imp) ->
#t_nil{};
d2e({paren_type,_,[T]}, Prec) ->
d2e(T, Prec);
d2e({type,_,list,[T0]}, _Prec) ->
T = d2e(T0),
d2e({paren_type,_,[T]}, Prec, Imp) ->
d2e(T, Prec, Imp);
d2e({type,_,list,[T0]}, _Prec, Imp) ->
T = d2e(T0, Imp),
typevar_anno(#t_list{type = T}, [T]);
d2e({type,_,nonempty_list,[T0]}, _Prec) ->
T = d2e(T0),
d2e({type,_,nonempty_list,[T0]}, _Prec, Imp) ->
T = d2e(T0, Imp),
typevar_anno(#t_nonempty_list{type = T}, [T]);
d2e({type,_,bounded_fun,[T,Gs]}, _Prec) ->
[F0|Defs] = d2e([T|Gs]),
d2e({type,_,bounded_fun,[T,Gs]}, _Prec, Imp) ->
[F0|Defs] = d2e([T|Gs], Imp),
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]}, Prec) ->
d2e({type,_,range,[V1,V2]}, Prec, _Imp) ->
{_L,P,_R} = erl_parse:type_inop_prec('..'),
{integer,_,I1} = erl_eval:partial_eval(V1),
{integer,_,I2} = erl_eval:partial_eval(V2),
T0 = #t_integer_range{from = I1, to = I2},
maybe_paren(P, Prec, T0);
d2e({type,_,constraint,[Sub,Ts0]}, _Prec) ->
d2e({type,_,constraint,[Sub,Ts0]}, _Prec, Imp) ->
case {Sub,Ts0} of
{{atom,_,is_subtype},[{var,_,N},T0]} ->
Ts = [T] = d2e([T0]),
Ts = [T] = d2e([T0], Imp),
#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]),
Ts = [ST,T] = d2e([ST0,T0], Imp),
#t_def{name = ST, type = typevar_anno(T, Ts)};
_ ->
throw_error(get_line(element(2, Sub)), "cannot handle guard", [])
end;
d2e({type,_,union,Ts0}, Prec) ->
d2e({type,_,union,Ts0}, Prec, Imp) ->
{_L,P,R} = erl_parse:type_inop_prec('|'),
Ts = d2e(Ts0, R),
Ts = d2e(Ts0, R, Imp),
T = maybe_paren(P, Prec, #t_union{types = Ts}),
typevar_anno(T, Ts);
d2e({type,_,tuple,any}, _Prec) ->
d2e({type,_,tuple,any}, _Prec, _Imp) ->
#t_type{name = #t_name{name = tuple}, args = []};
d2e({type,_,binary,[Base,Unit]}, _Prec) ->
d2e({type,_,binary,[Base,Unit]}, _Prec, _Imp) ->
{integer,_,B} = erl_eval:partial_eval(Base),
{integer,_,U} = erl_eval:partial_eval(Unit),
#t_binary{base_size = B, unit_size = U};
d2e({type,_,map,any}, _Prec) ->
d2e({type,_,map,any}, _Prec, _Imp) ->
#t_type{name = #t_name{name = map}, args = []};
d2e({type,_,map,Es}, _Prec) ->
#t_map{types = d2e(Es) };
d2e({type,_,map_field_assoc,[K,V]}, Prec) ->
T = #t_map_field{assoc_type = assoc, k_type = d2e(K), v_type=d2e(V) },
d2e({type,_,map,Es}, _Prec, Imp) ->
#t_map{types = d2e(Es, Imp) };
d2e({type,_,map_field_assoc,[K,V]}, Prec, Imp) ->
T = #t_map_field{assoc_type = assoc, k_type = d2e(K, Imp), v_type=d2e(V, Imp) },
{P,_R} = erl_parse:type_preop_prec('#'),
maybe_paren(P, Prec, T);
d2e({type,_,map_field_exact,[K,V]}, Prec) ->
T = #t_map_field{assoc_type = exact, k_type = d2e(K), v_type=d2e(V) },
d2e({type,_,map_field_exact,[K,V]}, Prec, Imp) ->
T = #t_map_field{assoc_type = exact, k_type = d2e(K, Imp), v_type=d2e(V, Imp) },
{P,_R} = erl_parse:type_preop_prec('#'),
maybe_paren(P, Prec, T);
d2e({type,_,tuple,Ts0}, _Prec) ->
Ts = d2e(Ts0),
d2e({type,_,tuple,Ts0}, _Prec, Imp) ->
Ts = d2e(Ts0, Imp),
typevar_anno(#t_tuple{types = Ts}, Ts);
d2e({type,_,record,[Name|Fs0]}, Prec) ->
d2e({type,_,record,[Name|Fs0]}, Prec, Imp) ->
Atom = #t_atom{val = element(3, Name)},
Fs = d2e(Fs0),
Fs = d2e(Fs0, Imp),
{P,_R} = erl_parse:type_preop_prec('#'),
T = maybe_paren(P, Prec, #t_record{name = Atom, fields = Fs}),
typevar_anno(T, Fs);
d2e({type,_,field_type,[Name,Type0]}, Prec) ->
d2e({type,_,field_type,[Name,Type0]}, Prec, Imp) ->
{_L,P,R} = erl_parse:type_inop_prec('::'),
Type = maybe_paren(P, Prec, d2e(Type0, R)),
Type = maybe_paren(P, Prec, d2e(Type0, R, Imp)),
T = #t_field{name = #t_atom{val = element(3, Name)}, type = Type},
typevar_anno(T, [Type]);
d2e({typed_record_field,{record_field,L,Name},Type}, Prec) ->
d2e({type,L,field_type,[Name,Type]}, Prec);
d2e({typed_record_field,{record_field,L,Name,_E},Type}, Prec) ->
d2e({type,L,field_type,[Name,Type]}, Prec);
d2e({record_field,L,_Name,_E}=F, Prec) ->
d2e({typed_record_field,F,{type,L,any,[]}}, Prec); % Maybe skip...
d2e({record_field,L,_Name}=F, Prec) ->
d2e({typed_record_field,F,{type,L,any,[]}}, Prec); % Maybe skip...
d2e({type,_,Name,Types0}, _Prec) ->
Types = d2e(Types0),
d2e({typed_record_field,{record_field,L,Name},Type}, Prec, Imp) ->
d2e({type,L,field_type,[Name,Type]}, Prec, Imp);
d2e({typed_record_field,{record_field,L,Name,_E},Type}, Prec, Imp) ->
d2e({type,L,field_type,[Name,Type]}, Prec, Imp);
d2e({record_field,L,_Name,_E}=F, Prec, Imp) ->
d2e({typed_record_field,F,{type,L,any,[]}}, Prec, Imp); % Maybe skip...
d2e({record_field,L,_Name}=F, Prec, Imp) ->
d2e({typed_record_field,F,{type,L,any,[]}}, Prec, Imp); % Maybe skip...
d2e({type,_,Name,Types0}, _Prec, Imp) ->
Types = d2e(Types0, Imp),
typevar_anno(#t_type{name = #t_name{name = Name}, args = Types}, Types);
d2e({user_type,_,Name,Types0}, _Prec) ->
Types = d2e(Types0),
typevar_anno(#t_type{name = #t_name{name = Name}, args = Types}, Types);
d2e({var,_,'_'}, _Prec) ->
d2e({user_type,_,Name,Types0}, _Prec, Imp) ->
Arity = length(Types0),
Mod = maps:get({Name, Arity}, Imp, []),
Types = d2e(Types0, Imp),
typevar_anno(#t_type{name = #t_name{module = Mod, name = Name}, args = Types}, Types);
d2e({var,_,'_'}, _Prec, _Imp) ->
#t_type{name = #t_name{name = ?TOP_TYPE}};
d2e({var,_,TypeName}, _Prec) ->
d2e({var,_,TypeName}, _Prec, _Imp) ->
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, Prec) when is_list(L) ->
[d2e(T, Prec) || T <- L];
d2e({atom,_,A}, _Prec) ->
d2e(L, Prec, Imp) when is_list(L) ->
[d2e(T, Prec, Imp) || T <- L];
d2e({atom,_,A}, _Prec, _Imp) ->
#t_atom{val = A};
d2e(undefined = U, _Prec) -> % opaque
d2e(undefined = U, _Prec, _Imp) -> % opaque
U;
d2e(Expr, _Prec) ->
d2e(Expr, _Prec, _Imp) ->
{integer,_,I} = erl_eval:partial_eval(Expr),
#t_integer{val = I}.

Expand Down
16 changes: 14 additions & 2 deletions lib/edoc/test/edoc_SUITE.erl
Expand Up @@ -25,13 +25,13 @@
%% Test cases
-export([app/1,appup/1,build_std/1,build_map_module/1,otp_12008/1,
build_app/1, otp_14285/1, infer_module_app_test/1,
module_with_feature/1]).
module_with_feature/1, module_with_import_type/1]).

suite() -> [{ct_hooks,[ts_install_cth]}].

all() ->
[app,appup,build_std,build_map_module,otp_12008, build_app, otp_14285,
infer_module_app_test, module_with_feature].
infer_module_app_test, module_with_feature, module_with_import_type].

groups() ->
[].
Expand Down Expand Up @@ -170,3 +170,15 @@ module_with_feature(Config) ->
PreprocessOpts = [{preprocess, true}, {dir, PrivDir}],
ok = edoc:files([Source], PreprocessOpts),
ok.

module_with_import_type(Config) ->
DataDir = ?config(data_dir, Config),
PrivDir = ?config(priv_dir, Config),
F1 = filename:join(DataDir, "export_type.erl"),
F2 = filename:join(DataDir, "import_type.erl"),
ok = edoc:files([F1, F2], [{dir, PrivDir}]),
ImportTypeHtmlFile = filename:join(PrivDir, "import_type.html"),
true = filelib:is_regular(ImportTypeHtmlFile),
{ok, Html} = file:read_file(ImportTypeHtmlFile),
{_, _} = binary:match(Html, <<"export_type:my_binary()">>),
ok.

0 comments on commit b568c79

Please sign in to comment.