rebar3/src/rebar_compiler_epp.erl

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.