-module(ides_static_parse). -moduledoc """ Static analysis of supervisor `init/1` functions from BEAM abstract code. ## Recognized patterns The parser handles the most common OTP supervisor patterns: - **Literal SupFlags map** — `#{strategy => one_for_one, intensity => 3, period => 10}` - **Variable-bound SupFlags** — `SupFlags = #{...}` followed by `{ok, {SupFlags, Children}}` - **Literal child spec list** — `[#{id => w1, start => {M,F,A}, restart => permanent, type => worker}]` - **Variable-bound child specs** — `ChildSpec = #{...}, {ok, {SupFlags, [ChildSpec]}}` - **Variable-bound children list** — `Children = [...], {ok, {SupFlags, Children}}` ## Unrecognized patterns (return errors) These patterns cannot be analyzed statically and produce `{error, Reason}`: - **Dynamic SupFlags** — strategy/intensity/period derived from function calls or `application:get_env` at runtime - **Dynamic children** — child specs built from function calls, list comprehensions over runtime data, or `ets` lookups - **Conditional children** — `if`/`case` expressions in the return value selecting between different child spec lists - **MFA dispatch** — `apply(M, F, A)` patterns or dynamic module names - **No abstract code** — BEAM file compiled without `debug_info` ## Error handling `parse_init/1` returns `{ok, SupFlags, [ChildSpec]}` on success, or `{error, Reason}` on failure. Callers should handle the error case. """. -export([parse_init/1]). -type sup_flags() :: #{ strategy := ides:supervisor_strategy(), intensity := non_neg_integer(), period := non_neg_integer() }. -type child_spec() :: #{ id := atom(), start := {module(), atom(), [term()]}, restart := ides:child_restart_type(), type := worker | supervisor }. -export_type([sup_flags/0, child_spec/0]). -doc "Parse init/1 function from beam_info, extracting SupFlags and ChildSpecs.". -spec parse_init(ides_static_beam:beam_info()) -> {ok, sup_flags(), [child_spec()]} | {error, term()}. parse_init(#{abstract_code := Abstr}) -> case find_init_function(Abstr) of {ok, Clause} -> parse_init_clause(Clause); error -> {error, no_init_function} end; parse_init(_) -> {error, no_abstract_code}. find_init_function(Abstr) -> case [F || {function, _, init, 1, _} = F <- Abstr] of [Func] -> {function, _, _, _, [Clause | _]} = Func, {ok, Clause}; [] -> error end. parse_init_clause({clause, _Line, _Args, _Guards, Body}) -> ReturnExpr = lists:last(Body), case ReturnExpr of {tuple, _, [{atom, _, ok}, {tuple, _, [SupFlagsExpr, ChildrenExpr]}]} -> SupFlags = resolve_sup_flags(SupFlagsExpr, Body), Children = resolve_children(ChildrenExpr, Body), {ok, SupFlags, Children}; _ -> {error, {unexpected_return, ReturnExpr}} end. resolve_sup_flags({map, _, Fields}, _Body) -> Strategy = extract_atom_field(Fields, strategy), Intensity = extract_integer_field(Fields, intensity), Period = extract_integer_field(Fields, period), #{ strategy => Strategy, intensity => Intensity, period => Period }; resolve_sup_flags({var, _, Name}, Body) -> case find_match(Name, Body) of {ok, Value} -> resolve_sup_flags(Value, Body); error -> error({unbound_variable, Name}) end; resolve_sup_flags(Other, _Body) -> error({dynamic_sup_flags, Other}). resolve_children({cons, _Line, Head, Tail}, Body) -> Resolved = resolve_child_head(Head, Body), [Resolved | resolve_children(Tail, Body)]; resolve_children({nil, _Line}, _Body) -> []; resolve_children({var, _, Name}, Body) -> case find_match(Name, Body) of {ok, Value} -> resolve_children(Value, Body); error -> error({unbound_variable, Name}) end; resolve_children(Other, _Body) -> error({dynamic_children, Other}). resolve_child_head({var, _, Name}, Body) -> case find_match(Name, Body) of {ok, Value} -> parse_child_spec(Value); error -> error({unbound_variable, Name}) end; resolve_child_head(Expr, _Body) -> parse_child_spec(Expr). parse_child_spec({map, _, Fields}) -> Id = extract_atom_field(Fields, id), Start = extract_start(Fields), Restart = extract_restart(Fields), Type = extract_type(Fields), #{ id => Id, start => Start, restart => Restart, type => Type }. extract_start(Fields) -> case [V || {map_field_assoc, _, {atom, _, start}, V} <- Fields] of [{tuple, _, [{atom, _, M}, {atom, _, F}, A]}] -> Args = erl_parse:normalise(A), {M, F, Args}; _ -> {undefined, undefined, []} end. extract_restart(Fields) -> case [V || {map_field_assoc, _, {atom, _, restart}, {atom, _, V}} <- Fields] of [R] when R =:= permanent; R =:= transient; R =:= temporary -> R; _ -> permanent end. extract_type(Fields) -> case [V || {map_field_assoc, _, {atom, _, type}, {atom, _, V}} <- Fields] of [supervisor] -> supervisor; [worker] -> worker; _ -> worker end. extract_atom_field(Fields, Key) -> case [V || {map_field_assoc, _, {atom, _, K}, {atom, _, V}} <- Fields, K =:= Key] of [Val] -> Val; _ -> error({missing_field, Key}) end. extract_integer_field(Fields, Key) -> case [V || {map_field_assoc, _, {atom, _, K}, {integer, _, V}} <- Fields, K =:= Key] of [Val] -> Val; _ -> error({missing_field, Key}) end. find_match(Name, [{match, _, {var, _, Name}, Value} | _]) -> {ok, Value}; find_match(Name, [_ | Rest]) -> find_match(Name, Rest); find_match(_Name, []) -> error.