diff --git a/tools/hook_deps.sh b/tools/hook_deps.sh new file mode 100755 index 000000000..39c33ca2e --- /dev/null +++ b/tools/hook_deps.sh @@ -0,0 +1,394 @@ +#!/usr/bin/escript +%% -*- 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)).