289 lines
11 KiB
Erlang
289 lines
11 KiB
Erlang
%%% @doc
|
|
%%% Analyze erlang-related files and compilation data using EPP, in order to
|
|
%%% build complete and accurate DAGs
|
|
%%% @end
|
|
-module(rebar_compiler_epp).
|
|
-export([deps/2, resolve_module/2]).
|
|
%% cache (a la code path storage, but for dependencies not in code path)
|
|
-export([ensure_started/0, flush/0, resolve_source/2]).
|
|
-export([init/1, handle_call/3, handle_cast/2]).
|
|
%% remove when OTP 19 support is no longer needed
|
|
-export([handle_info/2, terminate/2, code_change/3]).
|
|
|
|
-behaviour(gen_server).
|
|
|
|
-include_lib("kernel/include/file.hrl").
|
|
|
|
-include("rebar.hrl").
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% Basic File Handling %%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
%% Find all Erlang code dependencies for a given file
|
|
-spec deps(file:filename_all(), Opts) -> Attributes when
|
|
Opts :: [Opt, ...],
|
|
Opt :: {includes, [file:filename_all()]}
|
|
| {macros, [file:filename_all()]},
|
|
%% following are all required, OTP-18 don't like it though
|
|
Attributes :: #{include => [file:filename_all()],
|
|
missing_include_file => [file:filename_all()],
|
|
missing_include_lib => [file:filename_all()],
|
|
behaviour => [atom()],
|
|
parse_transform => [atom()],
|
|
is_behaviour => boolean()}.
|
|
deps(File, Opts) ->
|
|
{EppOpts, ExtraOpts} = split_opts(Opts),
|
|
{ok, Forms} = epp:parse_file(File, EppOpts),
|
|
normalize(handle_forms(Forms, default_attrs(), ExtraOpts)).
|
|
|
|
%% Find the path matching a given erlang module
|
|
resolve_module(Mod, Paths) ->
|
|
ModStr = atom_to_list(Mod),
|
|
try
|
|
[throw(P) || P <- Paths, ModStr =:= filename:basename(P, ".erl")],
|
|
{error, not_found}
|
|
catch
|
|
Path -> {ok, Path}
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% Cache for deps %%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
-spec ensure_started() -> ok.
|
|
ensure_started() ->
|
|
case whereis(?MODULE) of
|
|
undefined ->
|
|
case gen_server:start({local, ?MODULE}, ?MODULE, [], []) of
|
|
{ok, _Pid} ->
|
|
ok;
|
|
{error, {already_started, _Pid}} ->
|
|
ok
|
|
end;
|
|
Pid when is_pid(Pid) ->
|
|
ok
|
|
end.
|
|
|
|
flush() ->
|
|
gen_server:cast(?MODULE, flush).
|
|
|
|
%% @doc Resolves "Name" erl module to a path, given list of paths to search.
|
|
%% Caches result for subsequent requests.
|
|
-spec resolve_source(atom() | file:filename_all(), [file:filename_all()]) -> {true, file:filename_all()} | false.
|
|
resolve_source(Name, Dirs) when is_atom(Name) ->
|
|
gen_server:call(?MODULE, {resolve, atom_to_list(Name) ++ ".erl", Dirs}, infinity);
|
|
resolve_source(Name, Dirs) when is_list(Name) ->
|
|
gen_server:call(?MODULE, {resolve, Name, Dirs}, infinity).
|
|
|
|
-record(state, {
|
|
%% filesystem cache, denormalised
|
|
fs = #{} :: #{file:filename_all() => [file:filename_all()]},
|
|
%% map of module name => abs path
|
|
resolved = #{} :: #{file:filename_all() => file:filename_all()}
|
|
}).
|
|
|
|
init([]) ->
|
|
{ok, #state{}}.
|
|
|
|
handle_call({resolve, Name, Dirs}, _From, #state{fs = Fs, resolved = Res} = State) ->
|
|
case maps:find(Name, Res) of
|
|
{ok, Found} ->
|
|
{reply, Found, State};
|
|
error ->
|
|
{Resolved, NewFs} = resolve(Name, Fs, Dirs),
|
|
{reply, Resolved, State#state{resolved = Res#{Name => Resolved}, fs = NewFs}}
|
|
end.
|
|
|
|
handle_cast(flush, _State) ->
|
|
{noreply, #state{}}.
|
|
|
|
resolve(_Name, Fs, []) ->
|
|
{false, Fs};
|
|
resolve(Name, Fs, [Dir | Tail]) ->
|
|
{NewFs, Files} = list_directory(Dir, Fs),
|
|
case lists:keyfind(Name, 2, Files) of
|
|
{FullDir, _} ->
|
|
{{true, filename:join(FullDir, Name)}, NewFs};
|
|
false ->
|
|
resolve(Name, NewFs, Tail)
|
|
end.
|
|
|
|
%% list_directory/2 caches files in the directory and all subdirectories,
|
|
%% to support the behaviour of looking for source files in
|
|
%% subdirectories of src/* folder.
|
|
%% This may introduce weird dependencies for cases when CT
|
|
%% test cases contain test data with files named the same
|
|
%% as requested behaviour/parse_transforms, but let's hope
|
|
%% it won't happen for many projects. If it does, in fact,
|
|
%% it won't cause any damage, just extra unexpected recompiles.
|
|
list_directory(Dir, Cache) ->
|
|
case maps:find(Dir, Cache) of
|
|
{ok, Files} ->
|
|
{Cache, Files};
|
|
error ->
|
|
case file:list_dir(Dir) of
|
|
{ok, DirFiles} ->
|
|
%% create a full list of *.erl files under Dir.
|
|
{NewFs, Files} = lists:foldl(
|
|
fun (File, {DirCache, Files} = Acc) ->
|
|
FullName = filename:join(Dir, File),
|
|
case filelib:is_dir(FullName) of
|
|
true ->
|
|
%% We assume the include paths carry all recursive directories
|
|
%% so we don't need this resolution to be recursive.
|
|
Acc;
|
|
false ->
|
|
%% ignore all but *.erl files
|
|
case filename:extension(File) =:= ".erl" of
|
|
true ->
|
|
{DirCache, [{Dir, File} | Files]};
|
|
false ->
|
|
Acc
|
|
end
|
|
end
|
|
end,
|
|
{Cache, []}, DirFiles),
|
|
{NewFs#{Dir => Files}, Files};
|
|
{error, Reason} ->
|
|
?DIAGNOSTIC("Failed to list ~s, ~p", [Dir, Reason]),
|
|
{Cache, []}
|
|
end
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%
|
|
%%% OTP 19 %%%
|
|
handle_info(_Request, State) ->
|
|
{noreply, State}.
|
|
|
|
terminate(_Reason, _State) ->
|
|
ok.
|
|
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
%%%%%%%%%%%%%%%
|
|
%%% PRIVATE %%%
|
|
%%%%%%%%%%%%%%%
|
|
|
|
default_attrs() ->
|
|
#{include => [],
|
|
missing_include_file => [],
|
|
missing_include_lib => [],
|
|
behaviour => [],
|
|
parse_transform => [],
|
|
is_behaviour => false}.
|
|
|
|
normalize(Map) ->
|
|
#{include := Incl,
|
|
missing_include_file := InclF,
|
|
missing_include_lib := InclL,
|
|
behaviour := Behaviour,
|
|
parse_transform := PTrans} = Map,
|
|
Map#{include => lists:usort(Incl),
|
|
missing_include_file => lists:usort(InclF),
|
|
missing_include_lib => lists:usort(InclL),
|
|
behaviour => lists:usort(Behaviour),
|
|
parse_transform => lists:usort(PTrans)}.
|
|
|
|
handle_forms([File|Forms], Map, Opts) ->
|
|
lists:foldl(fun(Form, M) -> handle_form(Form, M, Opts) end,
|
|
Map, drop_self_file(File, Forms)).
|
|
|
|
drop_self_file(_, []) ->
|
|
[];
|
|
drop_self_file({attribute, _, file, {Path,_}} = File,
|
|
[{attribute,_, file, {Path,_}} | Rest]) ->
|
|
drop_self_file(File, Rest);
|
|
drop_self_file(File, [Keep|Rest]) ->
|
|
[Keep | drop_self_file(File, Rest)].
|
|
|
|
%% Included files (both libs and direct includes);
|
|
%% There are also references to the module's own file declaration
|
|
%% in there, but this is dropped by `drop_self_file/2' and assumed
|
|
%% to be gone here.
|
|
handle_form({attribute, _Line, file, {Path, Ln}}, Map, Opts) ->
|
|
%% Some people think they're funny and they go include attributes
|
|
%% like:
|
|
%% -file("fake/file.hrl", Ln).
|
|
%% Which are expanded to the very clause we have here, which in
|
|
%% turn is impossible to distinguish from actual included files
|
|
%% once checked through epp. The way we work around that here
|
|
%% is to check if the path is absolute, and if so, keep it in since
|
|
%% epp has expanded it; otherwise consider it to be a failed include.
|
|
%% This is not perfect but we can't do much more without touching the
|
|
%% disk and hopefully nobody else in the community has relied on this
|
|
%% thing.
|
|
case filename:absname(Path) of
|
|
Path ->
|
|
maps:update_with(include, fun(L) -> [Path|L] end, [Path], Map);
|
|
_ -> % argh!
|
|
handle_form({error, {Ln, {epp, {include, file, Path}}}}, Map, Opts)
|
|
end;
|
|
%% Include files that EPP couldn't resolve
|
|
handle_form({error, {_Line, epp, {include, file, Name}}}, Map, _Opts) ->
|
|
maps:update_with(missing_include_file, fun(L) -> [Name|L] end, [Name], Map);
|
|
handle_form({error, {_Line, epp, {include, lib, Path}}}, Map, Opts) ->
|
|
%% This file might still exist in the regular paths not in
|
|
%% code:lib_dir, which depend on options we pass to this module.
|
|
%% recursively seek it, and add it to the paths to expand here.
|
|
case find_include_with_opts(Path, Opts) of
|
|
{ok, File} ->
|
|
%% we can't go and figure out the contents within that include
|
|
%% file because we'd need its own compiler opts and app opts
|
|
%% to do it safely. Tracking that file is still better
|
|
%% than nothing though.
|
|
maps:update_with(include, fun(L) -> [File|L] end, [File], Map);
|
|
{error, not_found} ->
|
|
maps:update_with(missing_include_lib, fun(L) -> [Path|L] end, [Path], Map)
|
|
end;
|
|
%% Behaviour implementation declaration
|
|
handle_form({attribute, _Line, behaviour, Name}, Map, _Opts) ->
|
|
maps:update_with(behaviour, fun(L) -> [Name|L] end, [Name], Map);
|
|
handle_form({attribute, _Line, behavior, Name}, Map, _Opts) ->
|
|
maps:update_with(behaviour, fun(L) -> [Name|L] end, [Name], Map);
|
|
%% Extract parse transforms
|
|
handle_form({attribute, Line, compile, Attr}, Map, _Opts) when not is_list(Attr) ->
|
|
handle_form({attribute, Line, compile, [Attr]}, Map, _Opts);
|
|
handle_form({attribute, _Line, compile, Attrs}, Map, _Opts) ->
|
|
Mods = [case T of
|
|
{_, {M,_}} -> M;
|
|
{_, M} -> M
|
|
end || T <- proplists:lookup_all(parse_transform, Attrs)],
|
|
maps:update_with(parse_transform, fun(L) -> Mods++L end, Mods, Map);
|
|
%% Current style behaviour specification declaration
|
|
handle_form({attribute, _Line, callback, _}, Map, _Opts) ->
|
|
Map#{is_behaviour => true};
|
|
%% Old style behaviour specification, both spellings supported
|
|
%% The function needs to be exported, but we skip over that logic
|
|
%% for now.
|
|
handle_form({function, _Line, behaviour_info, 1, _}, Map, _Opts) ->
|
|
Map#{is_behaviour => true};
|
|
handle_form({function, _Line, behavior_info, 1, _}, Map, _Opts) ->
|
|
Map#{is_behaviour => true};
|
|
%% Skip the rest
|
|
handle_form(_, Map, _Opts) ->
|
|
Map.
|
|
|
|
split_opts(Opts) ->
|
|
%% Extra Opts are options we added to palliate to issues we had
|
|
%% with resolving include_libs and other things in EPP.
|
|
lists:partition(
|
|
fun({OptName, _}) ->
|
|
not lists:member(OptName, [include_libs, parse_transforms])
|
|
end,
|
|
Opts
|
|
).
|
|
|
|
find_include_with_opts(Path, Opts) ->
|
|
InclPaths = proplists:get_value(include_libs, Opts, []),
|
|
find_include_lib(InclPaths, Path).
|
|
|
|
find_include_lib([], _) ->
|
|
{error, not_found};
|
|
find_include_lib([H|T], File) ->
|
|
Abs = filename:join([H, File]),
|
|
case filelib:is_regular(Abs) of
|
|
true -> {ok, Abs};
|
|
false -> find_include_lib(T, File)
|
|
end.
|