%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% @doc Helpers for working with {@link erl_syntax:syntaxTree/0}. %%% Similar to {@link erl_syntax_lib}, but with a different set of helpers, %%% and a preference for returning maps over proplists. %%% @end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%_* Module declaration ===================================================== -module(merlin_lib). %%%_* Exports ================================================================ %%%_ * API ------------------------------------------------------------------- -export([ export/2, file/1, module/1, module_form/1, update_tree/2, value/1 ]). -export([ format_error/1, format_error/2 ]). -export([ into_error_marker/2, find_source/1 ]). -export([ get_annotation/2, get_annotation/3, get_annotations/1, remove_annotation/2, set_annotation/3, set_annotations/2, update_annotations/2 ]). -export([ get_attribute/3, get_attribute_forms/2, get_attributes/2 ]). -export([ add_binding/2, add_bindings/2, annotate_bindings/1, get_binding_type/2, get_bindings/1, get_bindings_by_type/2, get_bindings_with_type/1 ]). -export([ add_new_variable/1, add_new_variable/2, add_new_variable/3, add_new_variables/2, add_new_variables/3, add_new_variables/4, new_variable/1, new_variable/2, new_variable/3, new_variables/1, new_variables/2, new_variables/3, new_variables/4 ]). -ifdef(TEST). -export([ reset_variable_counter/0 ]). -endif. %%%_* Types ------------------------------------------------------------------ -export_type([ bindings/0, bindings_or_form/0 ]). %%%_* Includes =============================================================== -include("internal.hrl"). %%%_* Macros ================================================================= -define(ERL_ANNO_KEYS, [line, column, file, generated, record, text]). -define(is_binding_type(Type), (Type =:= bound orelse Type =:= env orelse Type =:= free) ). -ifndef(TEST). -define(variable_name_formatting_fun(Prefix, Suffix), fun(N) -> binary_to_atom(iolist_to_binary(io_lib:format("~s~p~s", [Prefix, N, Suffix]))) end). -else. -define(variable_name_formatting_fun(Prefix, Suffix), fun(_N) -> test_variable_formatter(Prefix, Suffix) end). -endif. -define(attribute_filter(Name), fun(Node) -> erl_syntax:type(Node) == attribute andalso value(erl_syntax:attribute_name(Node)) == Name end). %%%_* Types ================================================================== -type variable() :: atom() | merlin:ast(). -type set() :: set(variable()). %% Represents a {@link set()} of {@link variable(). variables}. -type set(T) :: sets:set(T) | ordsets:ordset(T). %% Represents either of the two builtin `set' data structures in OTP. -type bindings() :: #{ env := ordsets:ordset(variable()), bound := ordsets:ordset(variable()), free := ordsets:ordset(variable()) }. %% Represents the current bindings for a form %% @see annotate_bindings/1 -type binding_type() :: env | bound | free. %% Represents the three types of bindings. %%
'' if not found.
-spec module([merlin:ast()]) -> module() | ''.
module(Forms) ->
case module_form(Forms) of
undefined ->
'';
ModuleAttribute ->
[ModuleName | _MaybeParameterizedModuleArgument] =
erl_syntax:attribute_arguments(ModuleAttribute),
erl_syntax:atom_value(ModuleName)
end.
%% @doc Returns the form for the first `-module' attribute in
%% `Forms', or `undefined' if not found.
-spec module_form([merlin:ast()]) -> merlin:ast() | undefined.
module_form(Forms) ->
case get_attribute_forms(Forms, module) of
[ModuleAttribute | _] ->
ModuleAttribute;
_ ->
undefined
end.
%% @doc Updates the given form using the given groups or subtrees of another
%% form.
%%
%% This is a generalisation of {@link erl_syntax:update_tree/2}.
-spec update_tree(merlin:ast(), [[merlin:ast()]] | merlin:ast()) -> merlin:ast().
update_tree(Node, Groups) when is_list(Groups) ->
erl_syntax:update_tree(Node, Groups);
update_tree(Node, Form) when is_tuple(Form) ->
?assertNodeType(Node, ?assertIsForm(Form)),
erl_syntax:update_tree(Node, erl_syntax:subtrees(Form)).
%% @doc Returns the value of the given literal node as an Erlang term.
%%
%% Raises `{badvalue, Node}' if the given `Node' is not an literal node.
-spec value(merlin:ast()) -> term().
value(Node) ->
case erl_syntax:is_literal(Node) of
true ->
case erl_syntax:type(Node) of
atom -> erl_syntax:atom_value(Node);
integer -> erl_syntax:integer_value(Node);
float -> erl_syntax:float_value(Node);
char -> erl_syntax:char_value(Node);
string -> erl_syntax:string_value(Node);
_ -> error({badvalue, Node})
end;
false ->
error({badvalue, Node})
end.
%% @doc Callback for formatting error messages from this module
%%
%% @see erl_parse:format_error/1
-spec format_error(term()) -> string().
format_error(Message0) ->
Message1 =
case io_lib:deep_char_list(Message0) of
true ->
Message0;
_ ->
io_lib:format("~tp", [Message0])
end,
case re:run(Message1, "^\\d+: (.+)$", [{capture, all_but_first, list}]) of
{match, [Message2]} -> Message2;
nomatch -> Message1
end.
%% @doc Callback for formatting error messages from this module.
%%
%% See EEP 54
-spec format_error(Reason, erlang:stacktrace()) -> ErrorInfo when
Reason :: term(),
ErrorInfo :: #{
pos_integer() | general | reason => string()
}.
format_error(badarg, [{?MODULE, remove_annotation, [_Form, line], _Location} | _]) ->
#{2 => "can't remove the line annotation"};
format_error(_, _) ->
#{}.
%% @doc Returns a
%%
%% error info with the given reason and location taken from the second
%% argument. If it is a stacktrace, the latter is taken from the first frame.
%% Otherwise it is assumed to be a {@link merlin:ast(). syntax node} and its
%% location is used.
-spec into_error_marker(Reason, Stacktrace | Node) -> merlin:error_marker() when
Reason :: term(),
Stacktrace :: list({module(), atom(), arity(), [{atom(), term()}]}),
Node :: merlin:ast().
into_error_marker(Reason, [{_Module, _Function, _ArityOrArguments, Location} | _]) ->
File = keyfind(Location, file, none),
Line = keyfind(Location, line, 0),
{error, {File, {Line, ?MODULE, Reason}}};
into_error_marker(Reason, Node) when is_tuple(Node) ->
File = get_annotation(Node, file, none),
Position = erl_syntax:get_pos(Node),
{error, {File, {Position, ?MODULE, Reason}}}.
%% @doc Returns the path to the source for the given module, or `undefined' if
%% it can't be found.
-spec find_source(module()) -> file:filename() | undefined.
find_source(Module) when is_atom(Module) ->
MaybeSource =
case code:get_object_code(Module) of
{Module, _Beam, Beamfile} ->
case filelib:find_source(Beamfile) of
{ok, FoundSource} -> FoundSource;
{error, not_found} -> undefined
end;
error ->
undefined
end,
case MaybeSource of
undefined ->
CompileOptions = Module:module_info(compile),
proplists:get_value(source, CompileOptions);
Source ->
Source
end.
%% @doc Returns the annotation for the given form,
%% or raises `{badkey, Annotation}' if not found.
-spec get_annotation(merlin:ast(), atom()) -> term().
get_annotation(Form, Annotation) when is_atom(Annotation) ->
case is_erl_anno(Annotation) of
true ->
Anno = erl_syntax:get_pos(Form),
get_anno(Annotation, Anno);
false ->
Annotations = get_annotations(Form),
maps:get(Annotation, Annotations)
end.
%% @doc Returns the annotation for the given form,
%% or Default if not found.
-spec get_annotation(merlin:ast(), atom(), Default) -> Default.
get_annotation(Form, Annotation, Default) when is_atom(Annotation) ->
Annotations = get_annotations(Form),
maps:get(Annotation, Annotations, Default).
%% @doc Returns all annotations associated with the given `Form' as a map.
-spec get_annotations(merlin:ast()) -> #{atom() := term()}.
get_annotations(Form) ->
{ErlAnno, ErlSyntax} = get_annotations_internal(Form),
maps:merge(maps:from_list(ErlAnno), ErlSyntax).
%% @doc Returns the given form without the given annotation.
%%
%% You may not remove the `line', as it must always be present.
%% You may remove annotations that are not present, if which case the original
%% form is returned.
-spec remove_annotation(merlin:ast(), atom()) -> merlin:ast().
remove_annotation(Form, line) ->
error(badarg, [Form, line], [{error_info, #{}}]);
remove_annotation(Form, Annotation) when is_atom(Annotation) ->
case is_erl_anno(Annotation) of
true ->
ErlAnnotations0 = erl_syntax:get_pos(Form),
case get_anno(ErlAnnotations0, Annotation) of
undefined ->
%% The annotation to remove is already missing
Form;
_ ->
%% There's no remove annotation in erl_anno,
%% instead we set all _other_ annotations
ErlAnnotations1 = lists:keydelete(Annotation, 1, ErlAnnotations0),
set_erl_anno(Form, ErlAnnotations1)
end;
false ->
ErlSyntax0 = erl_syntax:get_ann(Form),
ErlSyntax1 = lists:keydelete(Annotation, 1, ErlSyntax0),
erl_syntax:set_ann(Form, ErlSyntax1)
end.
%% @doc Returns the given form with the given annotation set to the given
%% value.
%% When given an erl_anno annotation and erl_parse form, it returns a erl_parse
%% form, otherwise an erl_syntax form.
-spec set_annotation(merlin:ast(), atom(), term()) -> merlin:ast().
set_annotation(Form, Annotation, Value) ->
Tuple = {Annotation, Value},
case is_erl_anno(Annotation) of
true ->
Anno0 = erl_syntax:get_pos(Form),
Anno1 = set_anno(Tuple, Anno0),
set_pos(Form, Anno1);
false ->
ErlSyntax0 = erl_syntax:get_ann(Form),
ErlSyntax1 = lists:keystore(Annotation, 1, ErlSyntax0, Tuple),
erl_syntax:set_ann(Form, ErlSyntax1)
end.
%% @doc Returns the given form with the given annotations.
%%
%% These may be {@link erl_parse} annotations, user annotations, or a mix of
%% both. The given annotations overwrite any already present. To preseve
%% existing ones use {@link update_annotations/2} instead.
-spec set_annotations(merlin:ast(), #{atom() := term()}) -> merlin:ast().
set_annotations(Form0, Annotations) when is_map(Annotations) ->
{ErlAnnotations, ErlSyntax} = lists:partition(
fun is_erl_anno/1,
maps:to_list(Annotations)
),
Form1 = set_erl_anno(Form0, ErlAnnotations),
case ErlSyntax of
[] -> Form1;
_ -> erl_syntax:set_ann(Form1, ErlSyntax)
end.
%% @doc Returns the given form with the given annotations merged in.
%% It separates {@link erl_anno} annotations from user once, which means if
%% you set `line' or `file', you update the position/location of the form,
%% else you are setting an erl_syntax user annotation.
%%
%% @see erl_anno
%% @see erl_syntax:get_pos/1
%% @see erl_syntax:get_ann/1
-spec update_annotations(merlin:ast(), #{atom() := term()}) -> erl_syntax_ast().
update_annotations(Form0, NewAnnotations) when is_map(NewAnnotations) ->
{ErlAnnotations, ErlSyntax} = get_annotations_internal(Form0),
{NewErlAnnotations, NewErlSyntax} = lists:partition(
fun is_erl_anno/1,
maps:to_list(NewAnnotations)
),
UpdatedErlSyntax = maps:merge(ErlSyntax, maps:from_list(NewErlSyntax)),
Form1 = set_erl_anno(Form0, ErlAnnotations ++ NewErlAnnotations),
Form2 = erl_syntax:set_ann(Form1, maps:to_list(UpdatedErlSyntax)),
Form2.
%% @doc Returns the argument to the first module attribute with the given
%% name, or Default if not found.
-spec get_attribute(merlin:ast() | [merlin:ast()], atom(), term()) -> term().
get_attribute(Tree, Name, Default) when is_tuple(Tree) ->
get_attribute(lists:flatten(erl_syntax:subtrees(Tree)), Name, Default);
get_attribute(Tree, Name, Default) ->
case lists:search(?attribute_filter(Name), Tree) of
{value, Node} -> erl_syntax:attribute_arguments(Node);
false -> Default
end.
%% @doc Returns the arguments to all attributes with the given name in the
%% given list of forms or subtrees of the given form.
%%
%% Returns the empty list if no such attributes are found.
-spec get_attributes(merlin:ast() | [merlin:ast()], atom()) -> term().
get_attributes(Tree, Name) when is_tuple(Tree) ->
get_attributes(lists:flatten(erl_syntax:subtrees(Tree)), Name);
get_attributes(Tree, Name) ->
lists:map(
fun erl_syntax:attribute_arguments/1,
get_attribute_forms(Tree, Name)
).
%% @doc Returns all attributes with the given name in the given list of forms
%% or subtrees of the given form.
-spec get_attribute_forms(merlin:ast() | [merlin:ast()], atom()) -> merlin:ast().
get_attribute_forms(Tree, Name) when is_tuple(Tree) ->
get_attribute_forms(lists:flatten(erl_syntax:subtrees(Tree)), Name);
get_attribute_forms(Tree, Name) ->
lists:filter(?attribute_filter(Name), Tree).
%% @doc Adds the given binding to the existing ones.
%%
%% @see add_bindings/2
-spec add_binding
(bindings(), variable()) -> bindings();
(merlin:ast(), variable()) -> erl_syntax_ast().
add_binding(BindingsOrForm, NewBinding) ->
add_bindings(BindingsOrForm, [NewBinding]).
%% @doc Adds the given bindings to the existing ones.
%% Accepts the same input as {@link get_bindings/1}.
%%
%% When given a form, it updates the bindings on that form, see
%% {@link merlin:annotate/2} for more info.
%%
%% When given a map of bindings as returned by
%% {@link get_bindings_with_type/1}, it updates the `free' and `bound' fields
%% as appropriate.
-spec add_bindings
(bindings(), set()) -> bindings();
(merlin:ast(), set()) -> erl_syntax_ast().
add_bindings(#{env := _, bound := _, free := _} = Bindings, NewBindings) ->
merge_bindings(Bindings, NewBindings);
add_bindings(Form, NewBindings) when is_tuple(Form) ->
Bindings0 = get_annotations(Form),
Bindings1 = maps:merge(#{env => [], bound => [], free => []}, Bindings0),
Bindings2 = merge_bindings(Bindings1, NewBindings),
update_annotations(Form, Bindings2).
%% @doc Annotates the given form or forms using
%% {@link erl_syntax_lib:annotate_bindings/2}.
%%
%% If given a form, it returns the same with the annotated bindings.
%% If given a list of forms, a {@link erl_syntax:form_list/1. form list} is
%% returned instead.
%%
%% It tries to find the `env' variables from the given form, or first form if
%% given a list of forms. If none can be found it assumes there's no `env'
%% variables.
-spec annotate_bindings(merlin:ast() | [merlin:ast()]) -> merlin:ast().
annotate_bindings(Forms0) when is_list(Forms0) ->
Forms1 = lists:flatten(Forms0),
Env =
case Forms1 of
[Form | _] ->
get_annotation(Form, env, ordsets:new());
_ ->
ordsets:new()
end,
Tree = erl_syntax:form_list(Forms1),
erl_syntax_lib:annotate_bindings(Tree, Env);
annotate_bindings(Form) ->
Env = get_annotation(Form, env, ordsets:new()),
erl_syntax_lib:annotate_bindings(Form, Env).
%% @doc Get the type of the given binding in the given form.
%% Preferring bound over env over free.
%%
%% @see erl_syntax_lib:annotate_bindings/2
-spec get_binding_type(bindings_or_form(), atom()) -> bound | env | free | unknown.
get_binding_type(#{bound := Bound, env := Env, free := Free}, Name) ->
case ordsets:is_element(Name, Bound) of
true ->
bound;
false ->
case ordsets:is_element(Name, Env) of
true ->
env;
false ->
case ordsets:is_element(Name, Free) of
true ->
free;
false ->
unknown
end
end
end;
get_binding_type(Form, Name) when is_tuple(Form) ->
get_binding_type(get_annotations(Form), Name).
%% @doc Get all bindings for the given value.
%% Can be a set of annotations, see {@link get_annotations/1}, or
%% {@link merlin:ast()} form from which those annotations are taken.
-spec get_bindings(bindings_or_form()) -> ordsets:ordset(atom()).
get_bindings(#{bound := Bound, env := Env, free := Free}) ->
lists:map(fun var_name/1, ordsets:union([Env, Bound, Free]));
get_bindings(Form) when is_tuple(Form) ->
get_bindings(get_annotations(Form)).
%% @doc Returns the bindings associated of the given `Type'
-spec get_bindings_by_type(bindings_or_form(), binding_type()) ->
ordsets:ordset(atom()).
get_bindings_by_type(Bindings, Type) when
?is_binding_type(Type) andalso is_map_key(Type, Bindings)
->
lists:map(fun var_name/1, maps:get(Type, Bindings));
get_bindings_by_type(BindingsOrForm, Type) when ?is_binding_type(Type) ->
case sets:is_set(BindingsOrForm) of
true ->
error(badarg);
false ->
get_bindings_by_type(get_annotations(BindingsOrForm), Type)
end.
%% @doc Get all bindings associated with the given `Form'.
%%
%% Returns a map from binding name to kind, preferring bound over env over
%% free.
%%
%% @see erl_syntax_lib:annotate_bindings/2
-spec get_bindings_with_type(bindings_or_form()) -> #{variable() := binding_type()}.
get_bindings_with_type(#{bound := Bound, env := Env, free := Free}) ->
Bindings = ordsets:union([Env, Bound, Free]),
maps:from_list([
case ordsets:is_element(Bound, Binding) of
true ->
{Binding, bound};
false ->
case ordsets:is_element(Env, Binding) of
true ->
{Binding, env};
false ->
case ordsets:is_element(Free, Binding) of
true ->
{Binding, free};
false ->
error({missing_binding, Binding})
end
end
end
|| Binding <- Bindings
]);
get_bindings_with_type(Form) when is_tuple(Form) ->
get_bindings_with_type(get_annotations(Form)).
%% @doc Same as {@link add_new_variable/3} with default prefix and suffix.
%%
%% @see new_variable/3
-spec add_new_variable(BindingsOrForm) -> {variable(), BindingsOrForm} when
BindingsOrForm :: bindings_or_form().
add_new_variable(BindingsOrForm) ->
Var = new_variable(BindingsOrForm),
{Var, add_binding(BindingsOrForm, Var)}.
%% @doc Same as {@link add_new_variable/3} with the given prefix and default
%% suffix.
%%
%% @see new_variable/3
-spec add_new_variable(BindingsOrForm, string()) -> {variable(), BindingsOrForm} when
BindingsOrForm :: bindings_or_form().
add_new_variable(BindingsOrForm, Prefix) ->
Var = new_variable(BindingsOrForm, Prefix),
{Var, add_binding(BindingsOrForm, Var)}.
%% @doc Creates a new variable using {@link new_variable/3} and adds it to the
%% given form or bindings.
%%
%% @see new_variable/3
-spec add_new_variable(BindingsOrForm, string(), string()) ->
{variable(), BindingsOrForm}
when
BindingsOrForm :: bindings_or_form().
add_new_variable(BindingsOrForm, Prefix, Suffix) ->
Var = new_variable(BindingsOrForm, Prefix, Suffix),
{Var, add_binding(BindingsOrForm, Var)}.
%% @doc Same as {@link new_variables/3} with default prefix and suffix.
%%
%% @see new_variable/3
-spec add_new_variables(BindingsOrForm, pos_integer()) ->
{[variable()], BindingsOrForm}
when
BindingsOrForm :: bindings_or_form().
add_new_variables(BindingsOrForm, Total) ->
Vars0 = new_variables(BindingsOrForm, Total),
Vars1 = maybe_form(BindingsOrForm, Vars0),
{Vars1, add_bindings(BindingsOrForm, Vars0)}.
%% @doc Same as {@link new_variable/3} with the given prefix and default
%% suffix.
%%
%% @see new_variable/3
-spec add_new_variables(BindingsOrForm, pos_integer(), string()) ->
{[variable()], BindingsOrForm}
when
BindingsOrForm :: bindings_or_form().
add_new_variables(BindingsOrForm, Total, Prefix) ->
Vars0 = new_variables(BindingsOrForm, Total, Prefix),
Vars1 = maybe_form(BindingsOrForm, Vars0),
{Vars1, add_bindings(BindingsOrForm, Vars0)}.
%% @doc Creates `Total' number of new variables using {@link new_variables/4}
%% and adds it to the given form or bindings.
%%
%% @see new_variable/3
-spec add_new_variables(BindingsOrForm, pos_integer(), string(), string()) ->
{[variable()], BindingsOrForm}
when
BindingsOrForm :: bindings_or_form().
add_new_variables(BindingsOrForm, Total, Prefix, Suffix) ->
Vars0 = new_variables(BindingsOrForm, Total, Prefix, Suffix),
Vars1 = maybe_form(BindingsOrForm, Vars0),
{Vars1, add_bindings(BindingsOrForm, Vars0)}.
%% @doc Same as {@link new_variable/3} with default prefix and suffix.
-spec new_variable(bindings_or_form()) -> variable().
new_variable(BindingsOrForm) ->
new_variable(BindingsOrForm, "__Var", "__").
%% @doc Same as {@link new_variable/3} with the given prefix and default
%% suffix.
-spec new_variable(bindings_or_form(), string()) -> variable().
new_variable(BindingsOrForm, Prefix) ->
new_variable(BindingsOrForm, Prefix, "").
%% @doc Returns a new variable guaranteed not to be in the given bindings, or
%% the bindings associated with the given form.
%%
%% If given a set of existing bindings, it will return an atom, if given a
%% form it will return a new {@link erl_syntax:variable/1. variable}. That
%% variable will have the {@link erl_anno:generated/1. generated} flag set.
%%
%% The resulting variable will be on the format `Prefix