2017-02-23 17:38:56 +01:00
|
|
|
#!/usr/bin/env escript
|
2017-01-16 15:14:33 +01:00
|
|
|
%% -*- erlang -*-
|
|
|
|
%%! -pa ebin
|
|
|
|
|
|
|
|
-record(state, {run_hooks = dict:new(),
|
|
|
|
run_fold_hooks = dict:new(),
|
|
|
|
hooked_funs = dict:new(),
|
|
|
|
mfas = dict:new(),
|
|
|
|
specs = dict:new(),
|
|
|
|
module :: module(),
|
|
|
|
file :: filename:filename()}).
|
|
|
|
|
|
|
|
main([Dir]) ->
|
|
|
|
State =
|
|
|
|
filelib:fold_files(
|
|
|
|
Dir, ".+\.[eh]rl\$", false,
|
|
|
|
fun(FileIn, Res) ->
|
|
|
|
case get_forms(FileIn) of
|
|
|
|
{ok, Forms} ->
|
|
|
|
Tree = erl_syntax:form_list(Forms),
|
|
|
|
Mod = list_to_atom(filename:rootname(filename:basename(FileIn))),
|
|
|
|
Acc0 = analyze_form(Tree, Res#state{module = Mod, file = FileIn}),
|
|
|
|
erl_syntax_lib:fold(
|
|
|
|
fun(Form, Acc) ->
|
|
|
|
case erl_syntax:type(Form) of
|
|
|
|
application ->
|
|
|
|
case erl_syntax_lib:analyze_application(Form) of
|
|
|
|
{ejabberd_hooks, {run, N}}
|
|
|
|
when N == 2; N == 3 ->
|
|
|
|
analyze_run_hook(Form, Acc);
|
|
|
|
{ejabberd_hooks, {run_fold, N}}
|
|
|
|
when N == 3; N == 4 ->
|
|
|
|
analyze_run_fold_hook(Form, Acc);
|
|
|
|
{ejabberd_hooks, {add, N}}
|
|
|
|
when N == 4; N == 5 ->
|
|
|
|
analyze_run_fun(Form, Acc);
|
|
|
|
{gen_iq_handler, {add_iq_handler, 6}} ->
|
|
|
|
analyze_iq_handler(Form, Acc);
|
|
|
|
_ ->
|
|
|
|
Acc
|
|
|
|
end;
|
|
|
|
attribute ->
|
|
|
|
case catch erl_syntax_lib:analyze_attribute(Form) of
|
|
|
|
{spec, _} ->
|
|
|
|
analyze_type_spec(Form, Acc);
|
|
|
|
_ ->
|
|
|
|
Acc
|
|
|
|
end;
|
|
|
|
_ ->
|
|
|
|
Acc
|
|
|
|
end
|
|
|
|
end, Acc0, Tree);
|
|
|
|
_Err ->
|
|
|
|
Res
|
|
|
|
end
|
|
|
|
end, #state{}),
|
|
|
|
report_orphaned_funs(State),
|
|
|
|
RunDeps = build_deps(State#state.run_hooks, State#state.hooked_funs),
|
|
|
|
RunFoldDeps = build_deps(State#state.run_fold_hooks, State#state.hooked_funs),
|
|
|
|
emit_module(RunDeps, RunFoldDeps, State#state.specs, Dir, hooks_type_test).
|
|
|
|
|
|
|
|
analyze_form(_Form, State) ->
|
|
|
|
%% case catch erl_syntax_lib:analyze_forms(Form) of
|
|
|
|
%% Props when is_list(Props) ->
|
|
|
|
%% M = State#state.module,
|
|
|
|
%% MFAs = lists:foldl(
|
|
|
|
%% fun({F, A}, Acc) ->
|
|
|
|
%% dict:append({M, F}, A, Acc)
|
|
|
|
%% end, State#state.mfas,
|
|
|
|
%% proplists:get_value(functions, Props, [])),
|
|
|
|
%% State#state{mfas = MFAs};
|
|
|
|
%% _ ->
|
|
|
|
%% State
|
|
|
|
%% end.
|
|
|
|
State.
|
|
|
|
|
|
|
|
analyze_run_hook(Form, State) ->
|
|
|
|
[Hook|Tail] = erl_syntax:application_arguments(Form),
|
|
|
|
case atom_value(Hook, State) of
|
|
|
|
undefined ->
|
|
|
|
State;
|
|
|
|
HookName ->
|
|
|
|
Args = case Tail of
|
|
|
|
[_Host, Args0] -> Args0;
|
|
|
|
[Args0] ->
|
|
|
|
Args0
|
|
|
|
end,
|
|
|
|
Arity = erl_syntax:list_length(Args),
|
|
|
|
Hooks = dict:store({HookName, Arity},
|
|
|
|
{State#state.file, erl_syntax:get_pos(Hook)},
|
|
|
|
State#state.run_hooks),
|
|
|
|
State#state{run_hooks = Hooks}
|
|
|
|
end.
|
|
|
|
|
|
|
|
analyze_run_fold_hook(Form, State) ->
|
|
|
|
[Hook|Tail] = erl_syntax:application_arguments(Form),
|
|
|
|
case atom_value(Hook, State) of
|
|
|
|
undefined ->
|
|
|
|
State;
|
|
|
|
HookName ->
|
|
|
|
Args = case Tail of
|
|
|
|
[_Host, _Val, Args0] -> Args0;
|
|
|
|
[_Val, Args0] -> Args0
|
|
|
|
end,
|
|
|
|
Arity = erl_syntax:list_length(Args) + 1,
|
|
|
|
Hooks = dict:store({HookName, Arity},
|
|
|
|
{State#state.file, erl_syntax:get_pos(Form)},
|
|
|
|
State#state.run_fold_hooks),
|
|
|
|
State#state{run_fold_hooks = Hooks}
|
|
|
|
end.
|
|
|
|
|
|
|
|
analyze_run_fun(Form, State) ->
|
|
|
|
[Hook|Tail] = erl_syntax:application_arguments(Form),
|
|
|
|
case atom_value(Hook, State) of
|
|
|
|
undefined ->
|
|
|
|
State;
|
|
|
|
HookName ->
|
|
|
|
{Module, Fun, Seq} = case Tail of
|
|
|
|
[_Host, M, F, S] ->
|
|
|
|
{M, F, S};
|
|
|
|
[M, F, S] ->
|
|
|
|
{M, F, S}
|
|
|
|
end,
|
|
|
|
ModName = module_name(Module, State),
|
|
|
|
FunName = atom_value(Fun, State),
|
|
|
|
if ModName /= undefined, FunName /= undefined ->
|
|
|
|
Funs = dict:append(
|
|
|
|
HookName,
|
|
|
|
{ModName, FunName, integer_value(Seq, State),
|
|
|
|
{State#state.file, erl_syntax:get_pos(Form)}},
|
|
|
|
State#state.hooked_funs),
|
|
|
|
State#state{hooked_funs = Funs};
|
|
|
|
true ->
|
|
|
|
State
|
|
|
|
end
|
|
|
|
end.
|
|
|
|
|
|
|
|
analyze_iq_handler(Form, State) ->
|
|
|
|
[_Component, _Host, _NS, Module, Function, _IQDisc] =
|
|
|
|
erl_syntax:application_arguments(Form),
|
|
|
|
Mod = module_name(Module, State),
|
|
|
|
Fun = atom_value(Function, State),
|
|
|
|
if Mod /= undefined, Fun /= undefined ->
|
|
|
|
code:ensure_loaded(Mod),
|
|
|
|
case erlang:function_exported(Mod, Fun, 1) of
|
|
|
|
false ->
|
|
|
|
log("~s:~p: Error: function ~s:~s/1 is registered "
|
|
|
|
"as iq handler, but is not exported~n",
|
|
|
|
[State#state.file, erl_syntax:get_pos(Form),
|
|
|
|
Mod, Fun]);
|
|
|
|
true ->
|
|
|
|
ok
|
|
|
|
end;
|
|
|
|
true ->
|
|
|
|
ok
|
|
|
|
end,
|
|
|
|
State.
|
|
|
|
|
|
|
|
analyze_type_spec(Form, State) ->
|
|
|
|
case catch erl_syntax:revert(Form) of
|
|
|
|
{attribute, _, spec, {{F, A}, _}} ->
|
|
|
|
Specs = dict:store({State#state.module, F, A},
|
|
|
|
{Form, State#state.file},
|
|
|
|
State#state.specs),
|
|
|
|
State#state{specs = Specs};
|
|
|
|
_ ->
|
|
|
|
State
|
|
|
|
end.
|
|
|
|
|
|
|
|
build_deps(Hooks, Hooked) ->
|
|
|
|
dict:fold(
|
|
|
|
fun({Hook, Arity}, {_File, _LineNo} = Meta, Deps) ->
|
|
|
|
case dict:find(Hook, Hooked) of
|
|
|
|
{ok, Funs} ->
|
|
|
|
ExportedFuns =
|
|
|
|
lists:flatmap(
|
|
|
|
fun({M, F, Seq, {FunFile, FunLineNo} = FunMeta}) ->
|
|
|
|
code:ensure_loaded(M),
|
|
|
|
case erlang:function_exported(M, F, Arity) of
|
|
|
|
false ->
|
|
|
|
log("~s:~p: Error: function ~s:~s/~p "
|
|
|
|
"is hooked on ~s/~p, but is not "
|
|
|
|
"exported~n",
|
|
|
|
[FunFile, FunLineNo, M, F,
|
|
|
|
Arity, Hook, Arity]),
|
|
|
|
[];
|
|
|
|
true ->
|
|
|
|
[{{M, F, Arity}, Seq, FunMeta}]
|
|
|
|
end
|
|
|
|
end, Funs),
|
|
|
|
dict:append_list({Hook, Arity, Meta}, ExportedFuns, Deps);
|
|
|
|
error ->
|
|
|
|
%% log("~s:~p: Warning: hook ~p/~p is unused~n",
|
|
|
|
%% [_File, _LineNo, Hook, Arity]),
|
|
|
|
dict:append_list({Hook, Arity, Meta}, [], Deps)
|
|
|
|
end
|
|
|
|
end, dict:new(), Hooks).
|
|
|
|
|
|
|
|
report_orphaned_funs(State) ->
|
|
|
|
dict:map(
|
|
|
|
fun(Hook, Funs) ->
|
|
|
|
lists:foreach(
|
|
|
|
fun({M, F, _, {File, Line}}) ->
|
|
|
|
case get_fun_arities(M, F, State) of
|
|
|
|
[] ->
|
|
|
|
log("~s:~p: Error: function ~s:~s is "
|
|
|
|
"hooked on hook ~s, but is not exported~n",
|
|
|
|
[File, Line, M, F, Hook]);
|
|
|
|
Arities ->
|
|
|
|
case lists:any(
|
|
|
|
fun(Arity) ->
|
|
|
|
dict:is_key({Hook, Arity},
|
|
|
|
State#state.run_hooks) orelse
|
|
|
|
dict:is_key({Hook, Arity},
|
|
|
|
State#state.run_fold_hooks);
|
|
|
|
(_) ->
|
|
|
|
false
|
|
|
|
end, Arities) of
|
|
|
|
false ->
|
|
|
|
Arity = hd(Arities),
|
|
|
|
log("~s:~p: Error: function ~s:~s/~p is hooked"
|
|
|
|
" on non-existent hook ~s/~p~n",
|
|
|
|
[File, Line, M, F, Arity, Hook, Arity]);
|
|
|
|
true ->
|
|
|
|
ok
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end, Funs)
|
|
|
|
end, State#state.hooked_funs).
|
|
|
|
|
|
|
|
get_fun_arities(Mod, Fun, _State) ->
|
|
|
|
proplists:get_all_values(Fun, Mod:module_info(exports)).
|
|
|
|
|
|
|
|
module_name(Form, State) ->
|
|
|
|
try
|
|
|
|
Name = erl_syntax:macro_name(Form),
|
|
|
|
'MODULE' = erl_syntax:variable_name(Name),
|
|
|
|
State#state.module
|
|
|
|
catch _:_ ->
|
|
|
|
atom_value(Form, State)
|
|
|
|
end.
|
|
|
|
|
|
|
|
atom_value(Form, State) ->
|
|
|
|
case erl_syntax:type(Form) of
|
|
|
|
atom ->
|
|
|
|
erl_syntax:atom_value(Form);
|
|
|
|
_ ->
|
|
|
|
log("~s:~p: Warning: not an atom: ~s~n",
|
|
|
|
[State#state.file,
|
|
|
|
erl_syntax:get_pos(Form),
|
|
|
|
erl_prettypr:format(Form)]),
|
|
|
|
undefined
|
|
|
|
end.
|
|
|
|
|
|
|
|
integer_value(Form, State) ->
|
|
|
|
case erl_syntax:type(Form) of
|
|
|
|
integer ->
|
|
|
|
erl_syntax:integer_value(Form);
|
|
|
|
_ ->
|
|
|
|
log("~s:~p: Warning: not an integer: ~s~n",
|
|
|
|
[State#state.file,
|
|
|
|
erl_syntax:get_pos(Form),
|
|
|
|
erl_prettypr:format(Form)]),
|
|
|
|
0
|
|
|
|
end.
|
|
|
|
|
|
|
|
emit_module(RunDeps, RunFoldDeps, Specs, Dir, Module) ->
|
|
|
|
File = filename:join([Dir, Module]) ++ ".erl",
|
|
|
|
try
|
|
|
|
{ok, Fd} = file:open(File, [write]),
|
|
|
|
write(Fd, "-module(~s).~n~n", [Module]),
|
|
|
|
emit_export(Fd, RunDeps, "run hooks"),
|
|
|
|
emit_export(Fd, RunFoldDeps, "run_fold hooks"),
|
|
|
|
emit_run_hooks(Fd, RunDeps, Specs),
|
|
|
|
emit_run_fold_hooks(Fd, RunFoldDeps, Specs),
|
|
|
|
write(Fd, "bypass_stop({stop, Acc}) -> Acc;~n"
|
|
|
|
"bypass_stop(Acc) -> Acc.~n", []),
|
|
|
|
file:close(Fd),
|
|
|
|
log("Module written to file ~s~n", [File])
|
|
|
|
catch _:{badmatch, {error, Reason}} ->
|
|
|
|
log("writing to ~s failed: ~s", [File, file:format_error(Reason)])
|
|
|
|
end.
|
|
|
|
|
|
|
|
emit_run_hooks(Fd, Deps, Specs) ->
|
|
|
|
DepsList = lists:sort(dict:to_list(Deps)),
|
|
|
|
lists:foreach(
|
|
|
|
fun({{Hook, Arity, {File, LineNo}}, []}) ->
|
|
|
|
Args = lists:duplicate(Arity, "_"),
|
|
|
|
write(Fd, "%% called at ~s:~p~n", [File, LineNo]),
|
|
|
|
write(Fd, "~s(~s) -> ok.~n~n", [Hook, string:join(Args, ", ")]);
|
|
|
|
({{Hook, Arity, {File, LineNo}}, Funs}) ->
|
|
|
|
emit_specs(Fd, Funs, Specs),
|
|
|
|
write(Fd, "%% called at ~s:~p~n", [File, LineNo]),
|
|
|
|
Args = string:join(
|
|
|
|
[[N] || N <- lists:sublist(lists:seq($A, $Z), Arity)],
|
|
|
|
", "),
|
|
|
|
write(Fd, "~s(~s) ->~n ", [Hook, Args]),
|
|
|
|
Calls = [io_lib:format("~s:~s(~s)", [Mod, Fun, Args])
|
|
|
|
|| {{Mod, Fun, _}, _Seq, _} <- lists:keysort(2, Funs)],
|
|
|
|
write(Fd, "~s.~n~n", [string:join(Calls, ",\n ")])
|
|
|
|
end, DepsList).
|
|
|
|
|
|
|
|
emit_run_fold_hooks(Fd, Deps, Specs) ->
|
|
|
|
DepsList = lists:sort(dict:to_list(Deps)),
|
|
|
|
lists:foreach(
|
|
|
|
fun({{Hook, Arity, {File, LineNo}}, []}) ->
|
|
|
|
write(Fd, "%% called at ~s:~p~n", [File, LineNo]),
|
|
|
|
Args = ["Acc"|lists:duplicate(Arity - 1, "_")],
|
|
|
|
write(Fd, "~s(~s) -> Acc.~n~n", [Hook, string:join(Args, ", ")]);
|
|
|
|
({{Hook, Arity, {File, LineNo}}, Funs}) ->
|
|
|
|
emit_specs(Fd, Funs, Specs),
|
|
|
|
write(Fd, "%% called at ~s:~p~n", [File, LineNo]),
|
|
|
|
Args = [[N] || N <- lists:sublist(lists:seq($A, $Z), Arity - 1)],
|
|
|
|
write(Fd, "~s(~s) ->", [Hook, string:join(["Acc"|Args], ", ")]),
|
|
|
|
FunsCascade = make_funs_cascade(
|
|
|
|
lists:reverse(lists:keysort(2, Funs)),
|
|
|
|
1, Args),
|
|
|
|
write(Fd, "~s.~n~n", [FunsCascade])
|
|
|
|
end, DepsList).
|
|
|
|
|
|
|
|
make_funs_cascade([{{Mod, Fun, _}, _Seq, _}|Funs], N, Args) ->
|
|
|
|
io_lib:format("~n~sbypass_stop(~s:~s(~s))",
|
|
|
|
[lists:duplicate(N, " "),
|
|
|
|
Mod, Fun, string:join([make_funs_cascade(Funs, N+1, Args)|Args], ", ")]);
|
|
|
|
make_funs_cascade([], _N, _Args) ->
|
|
|
|
"Acc".
|
|
|
|
|
|
|
|
emit_export(Fd, Deps, Comment) ->
|
|
|
|
DepsList = lists:sort(dict:to_list(Deps)),
|
|
|
|
Exports = lists:map(
|
|
|
|
fun({{Hook, Arity, _}, _}) ->
|
|
|
|
io_lib:format("~s/~p", [Hook, Arity])
|
|
|
|
end, DepsList),
|
|
|
|
write(Fd, "%% ~s~n-export([~s]).~n~n",
|
|
|
|
[Comment, string:join(Exports, ",\n ")]).
|
|
|
|
|
|
|
|
emit_specs(Fd, Funs, Specs) ->
|
|
|
|
lists:foreach(
|
|
|
|
fun({{M, _, _} = MFA, _, _}) ->
|
|
|
|
case dict:find(MFA, Specs) of
|
|
|
|
{ok, {Form, _File}} ->
|
|
|
|
Lines = string:tokens(erl_syntax:get_ann(Form), "\n"),
|
|
|
|
lists:foreach(
|
|
|
|
fun("%" ++ _) ->
|
|
|
|
ok;
|
|
|
|
("-spec" ++ Spec) ->
|
|
|
|
write(Fd, "%% -spec ~p:~s~n",
|
|
|
|
[M, string:strip(Spec, left)]);
|
|
|
|
(Line) ->
|
|
|
|
write(Fd, "%% ~s~n", [Line])
|
|
|
|
end, Lines);
|
|
|
|
error ->
|
|
|
|
ok
|
|
|
|
end
|
|
|
|
end, lists:keysort(2, Funs)).
|
|
|
|
|
|
|
|
get_forms(Path) ->
|
|
|
|
case file:open(Path, [read]) of
|
|
|
|
{ok, Fd} ->
|
|
|
|
parse(Path, Fd, 1, []);
|
|
|
|
Err ->
|
|
|
|
Err
|
|
|
|
end.
|
|
|
|
|
|
|
|
parse(Path, Fd, Line, Acc) ->
|
|
|
|
{ok, Pos} = file:position(Fd, cur),
|
|
|
|
case epp_dodger:parse_form(Fd, Line) of
|
|
|
|
{ok, Form, NewLine} ->
|
|
|
|
{ok, NewPos} = file:position(Fd, cur),
|
|
|
|
{ok, RawForm} = file:pread(Fd, Pos, NewPos - Pos),
|
|
|
|
file:position(Fd, {bof, NewPos}),
|
|
|
|
AnnForm = erl_syntax:set_ann(Form, RawForm),
|
|
|
|
parse(Path, Fd, NewLine, [AnnForm|Acc]);
|
|
|
|
{eof, _} ->
|
|
|
|
{ok, NewPos} = file:position(Fd, cur),
|
|
|
|
if NewPos > Pos ->
|
|
|
|
{ok, RawForm} = file:pread(Fd, Pos, NewPos - Pos),
|
|
|
|
Form = erl_syntax:text(""),
|
|
|
|
AnnForm = erl_syntax:set_ann(Form, RawForm),
|
|
|
|
{ok, lists:reverse([AnnForm|Acc])};
|
|
|
|
true ->
|
|
|
|
{ok, lists:reverse(Acc)}
|
|
|
|
end;
|
|
|
|
{error, {_, _, ErrDesc}, LineNo} = Err ->
|
|
|
|
log("~s:~p: Error: ~s~n",
|
|
|
|
[Path, LineNo, erl_parse:format_error(ErrDesc)]),
|
|
|
|
Err
|
|
|
|
end.
|
|
|
|
|
|
|
|
log(Format, Args) ->
|
|
|
|
io:format(standard_io, Format, Args).
|
|
|
|
|
|
|
|
write(Fd, Format, Args) ->
|
|
|
|
file:write(Fd, io_lib:format(Format, Args)).
|