]> granicus.if.org Git - ejabberd/commitdiff
Add script to check hook dependencies
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>
Mon, 16 Jan 2017 14:14:33 +0000 (17:14 +0300)
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>
Mon, 16 Jan 2017 14:14:33 +0000 (17:14 +0300)
tools/hook_deps.sh [new file with mode: 0755]

diff --git a/tools/hook_deps.sh b/tools/hook_deps.sh
new file mode 100755 (executable)
index 0000000..39c33ca
--- /dev/null
@@ -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)).