#!/usr/bin/env escript %% -*- erlang -*- -compile([nowarn_unused_function]). -record(state, {g_opts = #{} :: map(), m_opts = #{} :: map(), globals = [] :: [atom()], defaults = #{} :: map(), mod_defaults = #{} :: map(), specs = #{} :: map(), mod_specs = #{} :: map()}). main([Mod|Paths]) -> State = fold_beams( fun(File, Form, StateAcc) -> append(Form, File, StateAcc) end, #state{}, Paths), emit_modules(map_to_specs(State#state.m_opts, State#state.mod_defaults, State#state.mod_specs)), emit_config(Mod, map_to_specs(State#state.g_opts, State#state.defaults, State#state.specs), State#state.globals). emit_config(Mod, Specs, Globals) -> File = filename:join("src", Mod ++ ".erl"), case file:open(File, [write]) of {ok, Fd} -> emit_header(Fd, Mod, Specs, Globals), emit_funs(Fd, Mod, Specs, Globals); {error, Reason} -> err("Failed to open file ~s for writing: ~s", [File, file:format_error(Reason)]) end. emit_modules(Specs) -> M = lists:foldl( fun({{Mod, Opt}, Spec}, Acc) -> Opts = maps:get(Mod, Acc, []), Opts1 = [{Opt, Spec}|Opts], maps:put(Mod, Opts1, Acc) end, #{}, Specs), maps:fold( fun(Mod, OptSpecs, _) -> ModS = atom_to_list(Mod) ++ "_opt", File = filename:join("src", ModS ++ ".erl"), case file:open(File, [write]) of {ok, Fd} -> OptSpecs1 = lists:reverse(OptSpecs), emit_header(Fd, ModS, OptSpecs1), emit_funs(Fd, Mod, OptSpecs1); {error, Reason} -> err("Failed to open file ~s for writing: ~s", [File, file:format_error(Reason)]) end end, ok, M). emit_header(Fd, Mod, Specs, Globals) -> log(Fd, comment(), []), log(Fd, "-module(~s).~n", [Mod]), lists:foreach( fun({{_, Opt}, _}) -> case lists:member(Opt, Globals) of true -> log(Fd, "-export([~s/0]).", [Opt]); false -> log(Fd, "-export([~s/0, ~s/1]).", [Opt, Opt]) end end, Specs), log(Fd, "", []). emit_header(Fd, Mod, Specs) -> log(Fd, comment(), []), log(Fd, "-module(~s).~n", [Mod]), lists:foreach( fun({Opt, _}) -> log(Fd, "-export([~s/1]).", [Opt]) end, Specs), log(Fd, "", []). emit_funs(Fd, _Mod, Specs, Globals) -> lists:foreach( fun({{_, Opt}, Type}) -> SType = t_to_string(Type), case lists:member(Opt, Globals) of true -> log(Fd, "-spec ~s() -> ~s.~n" "~s() ->~n" " ejabberd_config:get_option({~s, global}).~n", [Opt, SType, Opt, Opt]); false -> log(Fd, "-spec ~s() -> ~s.~n" "~s() ->~n" " ~s(global).~n" "-spec ~s(global | binary()) -> ~s.~n" "~s(Host) ->~n" " ejabberd_config:get_option({~s, Host}).~n", [Opt, SType, Opt, Opt, Opt, SType, Opt, Opt]) end end, Specs). emit_funs(Fd, Mod, Specs) -> lists:foreach( fun({Opt, Type}) -> log(Fd, "-spec ~s(gen_mod:opts() | global | binary()) -> ~s.~n" "~s(Opts) when is_map(Opts) ->~n" " gen_mod:get_opt(~s, Opts);~n" "~s(Host) ->~n" " gen_mod:get_module_opt(Host, ~s, ~s).~n", [Opt, t_to_string(Type), Opt, Opt, Opt, Mod, Opt]) end, Specs). append({globals, Form}, _File, State) -> [Clause] = erl_syntax:function_clauses(Form), Body = lists:last(erl_syntax:clause_body(Clause)), Gs = lists:map(fun erl_syntax:atom_value/1, erl_syntax:list_elements(Body)), Globals = State#state.globals ++ Gs, State#state{globals = Globals}; append({Index, Form}, File, State) when Index == #state.defaults; Index == #state.mod_defaults -> Mod = module(File), [Clause] = erl_syntax:function_clauses(Form), Body = lists:last(erl_syntax:clause_body(Clause)), case erl_syntax:is_proper_list(Body) of true -> Opts = lists:foldl( fun(E, M) -> try [E1, E2|_] = erl_syntax:tuple_elements(E), Name = erl_syntax:atom_value(E1), Val = erl_syntax:concrete(E2), maps:put({Mod, Name}, Val, M) catch _:_ -> M end end, element(Index, State), erl_syntax:list_elements(Body)), setelement(Index, State, Opts); false -> warn("~s: improper list", [format_file(File, Body)]), State end; append({Index, Form}, File, State) when Index == #state.specs; Index == #state.mod_specs -> Specs = element(Index, State), Mod = module(File), try {type, _, 'fun', Form1} = Form, {type, _, list, Form2} = lists:last(Form1), Tuples = case Form2 of [{type, _, union, Form3}] -> Form3; _ -> Form2 end, Specs1 = lists:foldl( fun({type, _, tuple, [{atom, _, Atom}, Form5]}, Acc) -> maps:put({Mod, Atom}, Form5, Acc); (_, Acc) -> Acc end, Specs, Tuples), setelement(Index, State, Specs1) catch _:_ -> warn("~s: unsupported type spec", [format_file(File, Form)]), State end; append({Type, Form}, File, State) when Type == opt_type; Type == mod_opt_type -> Clauses = erl_syntax:function_clauses(Form), Mod = module(File), lists:foldl( fun(Clause, StateAcc) -> [Arg] = erl_syntax:clause_patterns(Clause), Body = lists:last(erl_syntax:clause_body(Clause)), case erl_syntax:type(Arg) of atom -> Name = erl_syntax:atom_value(Arg), case Type of opt_type -> GOpts = StateAcc#state.g_opts, State#state{ g_opts = append_body({Mod, Name}, Body, GOpts)}; mod_opt_type -> MOpts = StateAcc#state.m_opts, State#state{ m_opts = append_body({Mod, Name}, Body, MOpts)} end; T -> warn("~s: unexpected option name: ~s", [format_file(File, Arg), T]), StateAcc end end, State, Clauses). append_body(Name, Body, Map) -> maps:put(Name, Body, Map). map_to_specs(Map, Defaults, Specs) -> lists:keysort( 1, maps:fold( fun({Mod, Opt} = Key, Val, Acc) -> S1 = type_with_default(Key, Val, Defaults), S2 = case t_is_any(S1) of true -> try maps:get(Key, Specs) catch _:{badkey, _} -> warn("Cannot derive type for ~s->~s", [Mod, Opt]), S1 end; false -> S1 end, [{Key, S2}|Acc] end, [], Map)). type_with_default({Mod, _} = Key, Val, Defaults) -> S = try spec(Mod, Val) catch throw:unknown -> erl_types:t_any() end, case t_is_any(S) of true -> S; false -> try maps:get(Key, Defaults) of T -> erl_types:t_sup( [S, erl_types:t_from_term(T)]) catch _:{badkey, _} -> S end end. spec(Mod, Form) -> case erl_syntax:type(Form) of application -> case erl_syntax_lib:analyze_application(Form) of {M, {Fun, Arity}} when M == econf; M == yconf -> Args = erl_syntax:application_arguments(Form), spec(Fun, Arity, Args, Mod); _ -> t_unknown(Mod) end; _ -> t_unknown(Mod) end. spec(pos_int, 0, _, _) -> erl_types:t_pos_integer(); spec(pos_int, 1, [Inf], _) -> erl_types:t_sup( erl_types:t_pos_integer(), erl_types:t_atom(erl_syntax:atom_value(Inf))); spec(non_neg_int, 0, _, _) -> erl_types:t_non_neg_integer(); spec(non_neg_int, 1, [Inf], _) -> erl_types:t_sup( erl_types:t_non_neg_integer(), erl_types:t_atom(erl_syntax:atom_value(Inf))); spec(int, 0, _, _) -> erl_types:t_integer(); spec(int, 2, [Min, Max], _) -> erl_types:t_from_range( erl_syntax:integer_value(Min), erl_syntax:integer_value(Max)); spec(number, 1, _, _) -> erl_types:t_number(); spec(octal, 0, _, _) -> erl_types:t_non_neg_integer(); spec(binary, A, _, _) when A == 0; A == 1; A == 2 -> erl_types:t_binary(); spec(enum, 1, [L], _) -> try Els = erl_syntax:list_elements(L), Atoms = lists:map( fun(A) -> erl_types:t_atom( erl_syntax:atom_value(A)) end, Els), erl_types:t_sup(Atoms) catch _:_ -> erl_types:t_binary() end; spec(bool, 0, _, _) -> erl_types:t_boolean(); spec(atom, 0, _, _) -> erl_types:t_atom(); spec(string, A, _, _) when A == 0; A == 1; A == 2 -> erl_types:t_string(); spec(any, 0, _, Mod) -> t_unknown(Mod); spec(url, A, _, _) when A == 0; A == 1 -> erl_types:t_binary(); spec(file, A, _, _) when A == 0; A == 1 -> erl_types:t_binary(); spec(directory, A, _, _) when A == 0; A == 1 -> erl_types:t_binary(); spec(ip, 0, _, _) -> t_remote(inet, ip_address); spec(ipv4, 0, _, _) -> t_remote(inet, ip4_address); spec(ipv6, 0, _, _) -> t_remote(inet, ip6_address); spec(ip_mask, 0, _, _) -> erl_types:t_sup( erl_types:t_tuple( [t_remote(inet, ip4_address), erl_types:t_from_range(0, 32)]), erl_types:t_tuple( [t_remote(inet, ip6_address), erl_types:t_from_range(0, 128)])); spec(port, 0, _, _) -> erl_types:t_from_range(1, 65535); spec(re, A, _, _) when A == 0; A == 1 -> t_remote(re, mp); spec(glob, A, _, _) when A == 0; A == 1 -> t_remote(re, mp); spec(path, 0, _, _) -> erl_types:t_binary(); spec(binary_sep, 1, _, _) -> erl_types:t_list(erl_types:t_binary()); spec(beam, A, _, _) when A == 0; A == 1 -> erl_types:t_module(); spec(timeout, 1, _, _) -> erl_types:t_pos_integer(); spec(timeout, 2, [_, Inf], _) -> erl_types:t_sup( erl_types:t_pos_integer(), erl_types:t_atom(erl_syntax:atom_value(Inf))); spec(non_empty, 1, [Form], Mod) -> S = spec(Mod, Form), case erl_types:t_is_list(S) of true -> erl_types:t_nonempty_list( erl_types:t_list_elements(S)); false -> S end; spec(unique, 1, [Form], Mod) -> spec(Mod, Form); spec(acl, 0, _, _) -> t_remote(acl, acl); spec(shaper, 0, _, _) -> erl_types:t_sup( [erl_types:t_atom(), erl_types:t_list(t_remote(ejabberd_shaper, shaper_rule))]); spec(url_or_file, 0, _, _) -> erl_types:t_tuple( [erl_types:t_sup([erl_types:t_atom(file), erl_types:t_atom(url)]), erl_types:t_binary()]); spec(lang, 0, _, _) -> erl_types:t_binary(); spec(pem, 0, _, _) -> erl_types:t_binary(); spec(jid, 0, _, _) -> t_remote(jid, jid); spec(domain, 0, _, _) -> erl_types:t_binary(); spec(db_type, 1, _, _) -> erl_types:t_atom(); spec(queue_type, 0, _, _) -> erl_types:t_sup([erl_types:t_atom(ram), erl_types:t_atom(file)]); spec(ldap_filter, 0, _, _) -> erl_types:t_binary(); spec(sip_uri, 0, _, _) -> t_remote(esip, uri); spec(Fun, A, [Form|_], Mod) when (A == 1 orelse A == 2) andalso (Fun == list orelse Fun == list_or_single) -> erl_types:t_list(spec(Mod, Form)); spec(map, A, [F1, F2|OForm], Mod) when A == 2; A == 3 -> T1 = spec(Mod, F1), T2 = spec(Mod, F2), case options_return_type(OForm) of map -> erl_types:t_map([], T1, T2); dict -> t_remote(dict, dict); _ -> erl_types:t_list(erl_types:t_tuple([T1, T2])) end; spec(either, 2, [F1, F2], Mod) -> Spec1 = case erl_syntax:type(F1) of atom -> erl_types:t_atom(erl_syntax:atom_value(F1)); _ -> spec(Mod, F1) end, Spec2 = spec(Mod, F2), erl_types:t_sup([Spec1, Spec2]); spec(and_then, 2, [_, F], Mod) -> spec(Mod, F); spec(host, 0, _, _) -> erl_types:t_binary(); spec(hosts, 0, _, _) -> erl_types:t_list(erl_types:t_binary()); spec(vcard_temp, 0, _, _) -> erl_types:t_sup([erl_types:t_atom(undefined), erl_types:t_tuple()]); spec(options, A, [Form|OForm], Mod) when A == 1; A == 2 -> case erl_syntax:type(Form) of map_expr -> Fs = erl_syntax:map_expr_fields(Form), Required = options_required(OForm), {Els, {DefK, DefV}} = lists:mapfoldl( fun(F, Acc) -> Name = erl_syntax:map_field_assoc_name(F), Val = erl_syntax:map_field_assoc_value(F), OptType = spec(Mod, Val), case erl_syntax:atom_value(Name) of '_' -> {[], {erl_types:t_atom(), OptType}}; Atom -> Mand = case lists:member(Atom, Required) of true -> mandatory; false -> optional end, {[{erl_types:t_atom(Atom), Mand, OptType}], Acc} end end, {erl_types:t_none(), erl_types:t_none()}, Fs), case options_return_type(OForm) of map -> erl_types:t_map(lists:keysort(1, lists:flatten(Els)), DefK, DefV); dict -> t_remote(dict, dict); _ -> erl_types:t_list( erl_types:t_sup( [erl_types:t_tuple([DefK, DefV])| lists:map( fun({K, _, V}) -> erl_types:t_tuple([K, V]) end, lists:flatten(Els))])) end; _ -> t_unknown(Mod) end; spec(_, _, _, Mod) -> t_unknown(Mod). t_from_form(Spec) -> {T, _} = erl_types:t_from_form( Spec, sets:new(), {type, {mod, foo, 1}}, dict:new(), erl_types:var_table__new(), erl_types:cache__new()), T. t_remote(Mod, Type) -> D = maps:from_list([{{opaque, Type, []}, {{Mod, 1, 2, []}, type}}]), [T] = erl_types:t_opaque_from_records(D), T. t_unknown(_Mod) -> throw(unknown). t_is_any(T) -> T == erl_types:t_any(). t_to_string(T) -> case erl_types:is_erl_type(T) of true -> erl_types:t_to_string(T); false -> erl_types:t_form_to_string(T) end. options_return_type([]) -> list; options_return_type([Form]) -> Opts = erl_syntax:concrete(Form), proplists:get_value(return, Opts, list). options_required([]) -> []; options_required([Form]) -> Opts = erl_syntax:concrete(Form), proplists:get_value(required, Opts, []). format_file(Path, Form) -> filename:rootname(filename:basename(Path)) ++ ".erl:" ++ integer_to_list(erl_syntax:get_pos(Form)). module(Path) -> list_to_atom(filename:rootname(filename:basename(Path))). fold_beams(Fun, State, Paths) -> Paths1 = fold_paths(Paths), Total = length(Paths1), {_, State1} = lists:foldl( fun(File, {I, Acc}) -> io:format("Progress: ~B% (~B/~B)\r", [round(I*100/Total), I, Total]), case is_elixir_beam(File) of true -> {I+1, Acc}; false -> AbsCode = get_code_from_beam(File), Acc2 = case is_behaviour(AbsCode, ejabberd_config) of true -> fold_opt(File, Fun, Acc, AbsCode); false -> fold_mod_opt(File, Fun, Acc, AbsCode) end, {I+1, Acc2} end end, {0, State}, Paths1), State1. fold_opt(File, Fun, Acc, AbsCode) -> lists:foldl( fun(Form, Acc1) -> case erl_syntax_lib:analyze_form(Form) of {function, {opt_type, 1}} -> Fun(File, {opt_type, Form}, Acc1); {function, {globals, 0}} -> Fun(File, {globals, Form}, Acc1); {function, {options, 0}} -> Fun(File, {#state.defaults, Form}, Acc1); {attribute, {spec, {spec, {{options, 0}, Spec}}}} -> Fun(File, {#state.specs, hd(Spec)}, Acc1); _ -> Acc1 end end, Acc, AbsCode). fold_mod_opt(File, Fun, Acc, AbsCode) -> lists:foldl( fun(Form, Acc1) -> case erl_syntax_lib:analyze_form(Form) of {function, {mod_opt_type, 1}} -> Fun(File, {mod_opt_type, Form}, Acc1); {function, {mod_options, 1}} -> Fun(File, {#state.mod_defaults, Form}, Acc1); {attribute, {spec, {spec, {{mod_options, 1}, Spec}}}} -> Fun(File, {#state.mod_specs, hd(Spec)}, Acc1); _ -> Acc1 end end, Acc, AbsCode). fold_paths(Paths) -> lists:flatmap( fun(Path) -> case filelib:is_dir(Path) of true -> Beams = lists:reverse( filelib:fold_files( Path, ".+\.beam\$", false, fun(File, Acc) -> [File|Acc] end, [])), case Beams of [] -> ok; _ -> code:add_path(Path) end, Beams; false -> [Path] end end, Paths). is_behaviour(AbsCode, Mod) -> lists:any( fun(Form) -> case erl_syntax_lib:analyze_form(Form) of {attribute, {Attr, {_, Mod}}} when Attr == behaviour orelse Attr == behavior -> true; _ -> false end end, AbsCode). is_elixir_beam(File) -> case filename:basename(File) of "Elixir" ++ _ -> true; _ -> false end. get_code_from_beam(File) -> try {ok, {_, List}} = beam_lib:chunks(File, [abstract_code]), {_, {raw_abstract_v1, Forms}} = lists:keyfind(abstract_code, 1, List), Forms catch _:{badmatch, _} -> err("no abstract code found in ~s", [File]) end. comment() -> "%% Generated automatically~n" "%% DO NOT EDIT: run `make options` instead~n". log(Format, Args) -> log(standard_io, Format, Args). log(Fd, Format, Args) -> case io:format(Fd, Format ++ "~n", Args) of ok -> ok; {error, Reason} -> err("Failed to write to file: ~s", [file:format_error(Reason)]) end. warn(Format, Args) -> io:format(standard_error, "Warning: " ++ Format ++ "~n", Args). err(Format, Args) -> io:format(standard_error, "Error: " ++ Format ++ "~n", Args), halt(1).