--- /dev/null
+#!/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)).