Current section

Files

Jump to
kivra_merlin src merlin_annotations.erl
Raw

src/merlin_annotations.erl

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% @doc This module allows for a unified handling of vanilla
%%% {@link erl_anno. annotations} and {@link erl_syntax. user annotations}.
%%%
%%% It maintains the representation of the given syntax node as much as
%%% possible, This means that if you given it a vanilla syntax node and
%%% {@link set/3} an {@link erl_anno} annotation, it will return a vanilla
%%% syntax node with the given annotation.
%%%
%%% However if you either give it an {@link erl_syntax} node or set a
%%% erl_syntax user annotation, it will return an {@link erl_syntax} node.
%%% @end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%_* Module declaration =====================================================
-module(merlin_annotations).
-compile({no_auto_import, [get/1]}).
%%%_* Exports ================================================================
%%%_ * API -------------------------------------------------------------------
-export([
delete/2,
get/1,
get/2,
get/3,
has/2,
merge/2,
set/3
]).
-export([
get_anno/1,
set_anno/2
]).
%%%_* Types ------------------------------------------------------------------
-export_type([
erl_annotation/0,
erl_annotations/0,
erl_annotation_key/0
]).
%%%_* Includes ===============================================================
-include("internal.hrl").
-include("assertions.hrl").
-ifdef(TEST).
-include("merlin_test.hrl").
-endif.
%%%_* Macros =================================================================
%%%_* Types ==================================================================
-type erl_annotation() ::
{line, erl_anno:line()}
| {column, erl_anno:column()}
| {file, file:filename_all()}
| {generated, boolean()}
| {location, erl_anno:location()}
| {record, boolean()}
| {text, string()}.
%% Reprensent a single builtin {@link erl_anno. annotation property}.
-type erl_annotations() :: [erl_annotation()].
%% Represents a list of {@link erl_annotation(). annotations}.
%%
%% This is also the internal format of {@link erl_anno} if some other
%% annotation than {@link erl_anno:line()} and/or
%% {@link erl_anno:line()} has been set.
-type erl_annotation_key() ::
line | column | file | generated | location | record | text.
%% The different types of builtin {@link erl_anno. annotations}.
%%%_* Code ===================================================================
%%%_ * API -------------------------------------------------------------------
%% @doc Returns `true' if the given syntax node has the given annotation,
%% `false' otherwise.
-spec has(Node, Key) -> boolean() when
Node :: merlin:ast(),
Key :: atom().
has(_Node, line) ->
true;
has(Node, column) ->
erl_anno:column(erl_syntax:get_pos(Node)) =/= undefined;
has(Node, file) ->
erl_anno:file(erl_syntax:get_pos(Node)) =/= undefined;
has(Node, generated) ->
erl_anno:generated(erl_syntax:get_pos(Node));
has(_Node, location) ->
true;
has(Node, record) ->
erl_anno:record(erl_syntax:get_pos(Node));
has(Node, text) ->
erl_anno:text(erl_syntax:get_pos(Node)) =/= undefined;
has(Node, Key) ->
Annotations = erl_syntax:get_ann(Node),
case lists:keyfind(Key, 1, Annotations) of
{Key, _Value} -> true;
false -> false
end.
%% @doc Returns all annotations for the given syntax node.
%%
%% This merges {@link erl_anno} annotations with {@link erl_syntax} user
%% annotations. Notably, it always includes both `line' and `location'.
-spec get(merlin:ast()) ->
#{
column => erl_anno:column(),
file => file:filename(),
generated => boolean(),
line := erl_anno:line(),
location := erl_anno:location(),
record => boolean(),
text => string(),
atom() => term()
}.
get(Node) ->
%% erl_anno's internal representation is a two-tuple list if there are
%% any annotations other than `line' and/or `location'.
Anno0 = erl_syntax:get_pos(Node),
AnnoMap1 =
case erl_anno:generated(Anno0) of
true ->
maps:from_list(erl_anno:to_term(Anno0));
false ->
Anno1 = erl_anno:set_generated(true, Anno0),
AnnoMap0 = maps:from_list(erl_anno:to_term(Anno1)),
maps:remove(generated, AnnoMap0)
end,
AnnoMap2 =
case AnnoMap1 of
#{location := {Line, Column}} when
is_integer(Line) andalso is_integer(Column)
->
AnnoMap1#{line => Line, column => Column};
#{location := Line} when is_integer(Line) ->
AnnoMap1#{line => Line}
end,
Annotations = maps:from_list(erl_syntax:get_ann(Node)),
maps:merge(AnnoMap2, Annotations).
%% @doc Get the value of the given annotation from the given syntax node.
-spec get(Node, Annotation) -> term() when
Node :: merlin:ast(),
Annotation :: atom().
get(Node, line) ->
erl_anno:line(erl_syntax:get_pos(Node));
get(Node, column) ->
erl_anno:column(erl_syntax:get_pos(Node));
get(Node, file) ->
erl_anno:file(erl_syntax:get_pos(Node));
get(Node, generated) ->
erl_anno:generated(erl_syntax:get_pos(Node));
get(Node, location) ->
erl_anno:location(erl_syntax:get_pos(Node));
get(Node, record) ->
erl_anno:record(erl_syntax:get_pos(Node));
get(Node, text) ->
erl_anno:text(erl_syntax:get_pos(Node));
get(Node, Annotation) ->
Annotations = erl_syntax:get_ann(Node),
case lists:keyfind(Annotation, 1, Annotations) of
{Annotation, Value} -> Value;
false -> ?raise_error({badkey, Annotation}, [Node, Annotation])
end.
%% @doc Get the value of the given annotation from the given syntax node.
%% If the annotation is not present, return the given default value.
%%
%% @see get/2
-spec get(Node, Key, Default) -> term() when
Node :: merlin:ast(),
Key :: atom(),
Default :: term().
get(Node, Key, Default) ->
case has(Node, Key) of
true -> get(Node, Key);
false -> Default
end.
%% @doc Set the given annotation to the given value on the given syntax node.
%%
%% @see erl_anno
%% @see erl_syntax:set_ann/2
%% @see erl_syntax:set_pos/2
-spec set(Node, Key, Value) -> Node when
Node :: merlin:ast(),
Key :: atom(),
Value :: term().
set(Node, line, Line) when is_integer(Line) ->
Anno0 = erl_syntax:get_pos(Node),
Anno1 = erl_anno:set_line(Line, Anno0),
set_anno(Node, Anno1);
set(Node, column, Column) when is_integer(Column) ->
Anno0 = erl_syntax:get_pos(Node),
Line = erl_anno:line(Anno0),
Anno1 = erl_anno:set_location({Line, Column}, Anno0),
set_anno(Node, Anno1);
set(Node, file, File) when ?is_string(File) ->
Anno0 = erl_syntax:get_pos(Node),
Anno1 = erl_anno:set_file(File, Anno0),
set_anno(Node, Anno1);
set(Node, generated, Generated) when is_boolean(Generated) ->
Anno0 = erl_syntax:get_pos(Node),
Anno1 = erl_anno:set_generated(Generated, Anno0),
set_anno(Node, Anno1);
set(Node, location, Location) ->
Anno0 = erl_syntax:get_pos(Node),
Anno1 = erl_anno:set_location(Location, Anno0),
set_anno(Node, Anno1);
set(Node, record, Record) when is_boolean(Record) ->
Anno0 = erl_syntax:get_pos(Node),
Anno1 = erl_anno:set_record(Record, Anno0),
set_anno(Node, Anno1);
set(Node, text, Text) when is_list(Text) ->
Anno0 = erl_syntax:get_pos(Node),
Anno1 = erl_anno:set_text(Text, Anno0),
set_anno(Node, Anno1);
set(Node, Key, Value) ->
Annotations0 = erl_syntax:get_ann(Node),
Annotations1 = lists:keystore(Key, 1, Annotations0, {Key, Value}),
erl_syntax:set_ann(Node, Annotations1).
%% @doc Returns the given syntax node without the given annotation.
%%
%% You are not allowed to delete `line' or `location'.
-spec delete(Node, Key) -> Node when
Node :: merlin:ast(),
Key :: atom().
delete(Node, line) ->
?raise_error({badkey, line}, [Node, line]);
delete(Node, location) ->
?raise_error({badkey, location}, [Node, location]);
delete(Node, column) ->
Anno0 = erl_syntax:get_pos(Node),
case erl_anno:location(Anno0) of
{Line, Column} when is_integer(Line) andalso is_integer(Column) ->
Anno1 = erl_anno:set_location(Line, Anno0),
set_anno(Node, Anno1);
Line when is_integer(Line) ->
Node
end;
delete(Node, Key) ->
case is_erl_anno(Key) of
true ->
%% erl_anno doesn't provide a way to delete an annotation, so we
%% copy over all _other_ annotations onto a new anno.
Anno0 = erl_syntax:get_pos(Node),
Anno1 = erl_anno_with(
Anno0, ordsets:del_element(Key, [file, generated, record, text])
),
set_anno(Node, Anno1);
false ->
Annotations0 = erl_syntax:get_ann(Node),
%% Delete all annotations with the given key, not just the first
%% one like lists:keydelete/3 would do.
Annotations1 = proplists:delete(Key, Annotations0),
erl_syntax:set_ann(Node, Annotations1)
end.
%% @doc Returns the given syntax node with its existing annotations merged
%% with the given ones.
-spec merge(Node, erl_annotations() | #{atom() => term()}) -> Node when
Node :: merlin:ast().
merge(Node, Annotations) when is_map(Annotations) ->
maps:fold(fun merger/3, Node, Annotations);
merge(Node, Annotations) when is_list(Annotations) ->
lists:foldl(fun merger/2, Node, Annotations).
%% @private
merger({Key, Value}, Node) ->
set(Node, Key, Value).
%% @private
merger(Key, Value, Node) ->
set(Node, Key, Value).
%% @doc Returns the {@link erl_anno. annotation} for the given syntax node.
%%
%% This is just a wrapper around {@link erl_syntax:get_pos/1}, added for
%% completeness.
-spec get_anno(merlin:ast()) -> erl_anno:anno().
get_anno(Node) ->
erl_syntax:get_pos(Node).
%% @doc Returns the given node with the given {@link erl_anno. annotation}.
%%
%% Similar to {@link erl_syntax:set_pos/2}, but preserves the representation
%% of the given syntax node.
-spec set_anno(Node, erl_anno:anno()) -> Node when
Node :: merlin:ast().
set_anno(Wrapper0, Anno) when wrapper =:= element(1, Wrapper0) ->
%% There's a bug in erl_syntax:set_pos/2 where it only sets the
%% pos/erl_anno on the wrapper record and not on the syntax node. This
%% causes erl_syntax:revert/1 to ignore any changes to the pos/erl_anno
%% field.
?assertMatch(
{wrapper, Type, Attr, _Tree} when
is_atom(Type) andalso is_record(Attr, attr, 4),
Wrapper0,
"erl_syntax #wrapper record has changed shape"
),
Wrapper1 = setelement(3, Wrapper0, set_anno(element(3, Wrapper0), Anno)),
Wrapper2 = erl_syntax:set_pos(Wrapper1, Anno),
Wrapper2;
set_anno(Node, Anno) when element(1, Node) =:= tree ->
erl_syntax:set_pos(Node, Anno);
set_anno({error, {_, Module, Reason}}, Anno) ->
%% See erl_syntax:get_pos/1 and erl_parse:form_info()
{error, {Anno, Module, Reason}};
set_anno({warning, {_, Module, Reason}}, Anno) ->
%% See erl_syntax:get_pos/1 and erl_parse:form_info()
{warning, {Anno, Module, Reason}};
set_anno(Node, Anno) ->
setelement(2, Node, Anno).
%%%_* Private ----------------------------------------------------------------
%% @doc Returns `true' if the given annotation is one from {@link erl_anno},
%% `false' otherwise.
-dialyzer({nowarn_function, is_erl_anno/1}).
-spec is_erl_anno
(erl_annotation_key()) -> true;
(atom()) -> false.
is_erl_anno(line) -> true;
is_erl_anno(column) -> true;
is_erl_anno(file) -> true;
is_erl_anno(generated) -> true;
is_erl_anno(location) -> true;
is_erl_anno(record) -> true;
is_erl_anno(text) -> true;
is_erl_anno(_) -> false.
%% @doc Like @{link maps:with/2}, but for {@link erl_anno. annotations}.
-spec erl_anno_with(Anno, Keys) -> Anno when
Anno :: erl_anno:anno(),
Keys :: [file | generated | record | text].
erl_anno_with(Source, Keys) ->
NewAnno0 = erl_anno:new(erl_anno:location(Source)),
lists:foldl(
fun
(file, NewAnno) ->
case erl_anno:file(Source) of
undefined -> NewAnno;
File -> erl_anno:set_file(File, NewAnno)
end;
(generated, NewAnno) ->
case erl_anno:generated(Source) of
false -> NewAnno;
true -> erl_anno:set_generated(true, NewAnno)
end;
(record, NewAnno) ->
case erl_anno:record(Source) of
false -> NewAnno;
true -> erl_anno:set_record(true, NewAnno)
end;
(text, NewAnno) ->
case erl_anno:text(Source) of
undefined -> NewAnno;
Text -> erl_anno:set_text(Text, NewAnno)
end
end,
NewAnno0,
Keys
).
%%%_* Tests ==================================================================
-ifdef(TEST).
-define(ERL_BOOLEAN_ANNOTATIONS, [generated, record]).
-define(ERL_INTEGER_ANNOTATIONS, [line, column]).
-define(ERL_STRING_ANNOTATIONS, [file, text]).
-define(ERL_ANNOTATIONS,
?ERL_INTEGER_ANNOTATIONS ++ [location] ++
?ERL_BOOLEAN_ANNOTATIONS ++
?ERL_STRING_ANNOTATIONS
).
has_test_() ->
maps:to_list(#{
"line" => ?_quickcheck(
annotated_node_type(),
Node,
?assert(has(Node, line))
),
"location" => ?_quickcheck(
annotated_node_type(),
Node,
?assert(has(Node, location))
),
"column" => ?_quickcheck(
annotated_node_type(),
Node,
case erl_anno:column(erl_syntax:get_pos(Node)) of
undefined -> ?assertNot(has(Node, column));
_ -> ?assert(has(Node, column))
end
),
"file" => ?_quickcheck(
annotated_node_type(),
Node,
case erl_anno:file(erl_syntax:get_pos(Node)) of
undefined -> ?assertNot(has(Node, file));
_ -> ?assert(has(Node, file))
end
),
"text" => ?_quickcheck(
annotated_node_type(),
Node,
case erl_anno:text(erl_syntax:get_pos(Node)) of
undefined -> ?assertNot(has(Node, text));
_ -> ?assert(has(Node, text))
end
),
"record" => ?_quickcheck(
annotated_node_type(),
Node,
case erl_anno:record(erl_syntax:get_pos(Node)) of
false -> ?assertNot(has(Node, record));
true -> ?assert(has(Node, record))
end
),
"generated" => ?_quickcheck(
annotated_node_type(),
Node,
case erl_anno:generated(erl_syntax:get_pos(Node)) of
false -> ?assertNot(has(Node, generated));
true -> ?assert(has(Node, generated))
end
),
"existing user annotation" => ?_quickcheck(
annotated_node_type(),
Node0,
begin
Node1 = erl_syntax:add_ann({foo, some_value}, Node0),
?assert(has(Node1, foo))
end
),
"missing user annotation" => ?_quickcheck(
annotated_node_type(),
Node,
?assertNot(has(Node, foo))
)
}).
get_single_test_() ->
LineAnno = erl_anno:new(7),
FileAnno = erl_anno:set_file(?FILE, LineAnno),
GeneratedAnno = erl_anno:set_generated(true, FileAnno),
VariableNode = {var, LineAnno, 'Variable'},
NodeWithGenerated = {var, GeneratedAnno, 'Variable'},
NodeWithFile = {var, FileAnno, 'Variable'},
NodeWithBound = erl_syntax:add_ann({bound, ['Foo']}, VariableNode),
maps:to_list(#{
"get line from form with simplest anno" => fun() ->
?assertEqual(7, get(VariableNode, line))
end,
"get line from form with complex anno" => fun() ->
?assertEqual(7, get(NodeWithFile, line))
end,
"get file from form without file, must return undefined by default" => fun() ->
?assertEqual(undefined, get(VariableNode, file))
end,
"get file from form without file must return the given default" => fun() ->
?assertEqual(123, get(VariableNode, file, 123))
end,
"get file from form with file" => fun() ->
?assertEqual(?FILE, get(NodeWithFile, file))
end,
"get generated returns a boolean" => fun() ->
?assertMatch(
Generated when is_boolean(Generated), get(NodeWithGenerated, generated)
)
end,
"get bound from form with bound user annotation" => fun() ->
?assertEqual(['Foo'], get(NodeWithBound, bound))
end,
"get missing annotation raises badkey" => fun() ->
?assertError({badkey, foo}, get(VariableNode, foo))
end
}) ++
[
{
atom_to_list(Annotation),
?_quickcheck(
node_type(),
Node,
?assertEqual(
erl_anno:Annotation(erl_syntax:get_pos(Node)),
get(Node, Annotation)
)
)
}
|| Annotation <- ?ERL_ANNOTATIONS
].
get_all_test_() ->
LineAnno = erl_anno:new(7),
FileAnno = erl_anno:set_file(?FILE, LineAnno),
ColumnAnno = erl_anno:set_location({7, 23}, LineAnno),
GeneratedAnno = erl_anno:set_generated(true, LineAnno),
Node = {var, FileAnno, 'Variable'},
NodeWithGenerated = {var, GeneratedAnno, 'Variable'},
NodeWithColumn = {var, ColumnAnno, 'Variable'},
NodeWithBound = erl_syntax:add_ann({bound, ['Foo']}, Node),
maps:to_list(#{
"with column" => ?_assertEqual(
#{
line => 7,
location => {7, 23},
column => 23
},
get(NodeWithColumn)
),
"with bound" => ?_assertEqual(
#{
bound => ['Foo'],
file => ?FILE,
line => 7,
location => 7
},
get(NodeWithBound)
),
"with boolean annotation" => ?_assertEqual(
#{
generated => true,
line => 7,
location => 7
},
get(NodeWithGenerated)
)
}).
set_test_() ->
Line = 7,
Column = 23,
Location = {Line, Column},
LineAnno = erl_anno:new(Line),
FileAnno = erl_anno:set_file(?FILE, LineAnno),
LocationAnno = erl_anno:set_location(Location, LineAnno),
VariableNode = {var, LineAnno, 'Variable'},
NodeWithFile = {var, FileAnno, 'Variable'},
NodeWithLocation = {var, LocationAnno, 'Variable'},
NodeWithBound = erl_syntax:add_ann({bound, ['Foo']}, VariableNode),
NodeWithBoth = erl_syntax:add_ann({bound, ['Foo']}, NodeWithFile),
maps:to_list(#{
"set user annotation" => fun() ->
?assertEqual(NodeWithBound, set(VariableNode, bound, ['Foo']))
end,
"setting an erl_anno annotation does not covert the form to erl_syntax AST" => fun() ->
?assertEqual(NodeWithLocation, set(VariableNode, location, Location)),
?assertEqual(NodeWithFile, set(VariableNode, file, ?FILE))
end,
"set erl_anno on erl_syntax AST" => fun() ->
ActualNode = set(NodeWithBound, file, ?FILE),
?assertEqual(get_sorted_pos(NodeWithBoth), get_sorted_pos(ActualNode)),
?assertEqual(get_sorted_ann(NodeWithBoth), get_sorted_ann(ActualNode))
end,
"set user annotation on complex erl_anno" => fun() ->
ActualNode = set(NodeWithFile, bound, ['Foo']),
?assertEqual(get_sorted_pos(NodeWithBoth), get_sorted_pos(ActualNode)),
?assertEqual(get_sorted_ann(NodeWithBoth), get_sorted_ann(ActualNode))
end
}) ++
[
?_quickcheck(
{node_type(), annotation_type()},
{Node, {Annotation, Value}},
begin
ExpectedValue =
case {Annotation, Value} of
{file, ""} -> undefined;
_ -> Value
end,
?assertEqual(
ExpectedValue,
erl_anno:Annotation(
erl_syntax:get_pos(set(Node, Annotation, Value))
)
)
end
)
].
delete_test_() ->
LineAnno = erl_anno:new(?LINE),
ComplexAnno = erl_anno:set_text(
"some text",
erl_anno:set_record(
true,
erl_anno:set_generated(
true,
erl_anno:set_file(
?FILE,
erl_anno:new({?LINE, 23})
)
)
)
),
VariableNode = {var, LineAnno, 'Variable'},
VariableWithComplexAnno = {var, ComplexAnno, 'Variable'},
VariableWithUserAnnotation = erl_syntax:add_ann({bound, ['Foo']}, VariableNode),
maps:to_list(#{
"delete user annotation" => fun() ->
UserAnnotations = erl_syntax:get_ann(
delete(VariableWithUserAnnotation, bound)
),
?assertEqual([], UserAnnotations)
end,
"delete missing annotation does nothing" => maps:to_list(#{
"column" => ?_assertEqual(
VariableNode, delete(VariableNode, column)
),
"generated" => ?_assertEqual(
VariableNode, delete(VariableNode, generated)
),
"user annotation" => ?_assertEqual(
VariableWithUserAnnotation, delete(VariableWithUserAnnotation, foo)
)
}),
"delete line raises badkey" => fun() ->
?assertError({badkey, line}, delete(VariableWithComplexAnno, line))
end,
"delete location raises badkey" => fun() ->
?assertError({badkey, location}, delete(VariableWithComplexAnno, location))
end,
"delete record" => fun() ->
Variable = {var, erl_anno:set_record(true, LineAnno), 'Variable'},
?assertEqual(
false,
erl_anno:record(
erl_syntax:get_pos(delete(Variable, record))
)
)
end
}) ++
[
?_quickcheck(
annotation_type(),
{Annotation, _Value},
case Annotation of
line ->
?assertError(
{badkey, line}, delete(VariableWithComplexAnno, line)
);
location ->
?assertError(
{badkey, location},
delete(VariableWithComplexAnno, location)
);
_ ->
?assertNotEqual(
erl_anno:Annotation(
erl_syntax:get_pos(VariableWithComplexAnno)
),
erl_anno:Annotation(
erl_syntax:get_pos(
delete(VariableWithComplexAnno, Annotation)
)
)
)
end
)
].
merge_test() ->
VariableNode = {var, erl_anno:new(7), 'Variable'},
NodeWithMergedAnnotations0 = merge(VariableNode, #{
column => 23, is_exported => true
}),
NodeWithMergedAnnotations1 = merge(NodeWithMergedAnnotations0, [
{generated, true}, {is_exported, false}
]),
?assertEqual(
[{generated, true}, {location, {7, 23}}],
get_sorted_pos(NodeWithMergedAnnotations1)
),
?assertEqual([{is_exported, false}], get_sorted_ann(NodeWithMergedAnnotations1)).
get_anno_test() ->
VariableNode = {var, erl_anno:new(7), 'Variable'},
?assertEqual(erl_anno:new(7), get_anno(VariableNode)).
set_anno_test_() ->
maps:to_list(#{
"regular node" => fun() ->
VariableNode = {var, erl_anno:new(7), 'Variable'},
?assertEqual(
erl_anno:new(8),
erl_syntax:get_pos(set_anno(VariableNode, erl_anno:new(8)))
)
end,
"error marker" => fun() ->
ErrorMarker = erl_syntax:revert(
erl_syntax:error_marker({erl_anno:new(7), erl_lint, "Reason"})
),
?assertEqual(
erl_anno:new(8),
erl_syntax:get_pos(set_anno(ErrorMarker, erl_anno:new(8)))
)
end,
"warning marker" => fun() ->
WarningMarker = erl_syntax:revert(
erl_syntax:warning_marker({erl_anno:new(7), erl_lint, "Reason"})
),
?assertEqual(
erl_anno:new(8),
erl_syntax:get_pos(set_anno(WarningMarker, erl_anno:new(8)))
)
end
}).
is_erl_anno_test_() ->
maps:to_list(#{
"line" => ?_assert(is_erl_anno(line)),
"column" => ?_assert(is_erl_anno(column)),
"file" => ?_assert(is_erl_anno(file)),
"generated" => ?_assert(is_erl_anno(generated)),
"location" => ?_assert(is_erl_anno(location)),
"record" => ?_assert(is_erl_anno(record)),
"text" => ?_assert(is_erl_anno(text)),
"foo" => ?_assertNot(is_erl_anno(foo))
}).
node_type() ->
?LET(
Forms,
proper_erlang_abstract_code:module(),
oneof(Forms)
).
annotation_type() ->
oneof(annotation_types()).
annotation_types() ->
lists:flatten([
[{Annotation, pos_integer()} || Annotation <- [line, location]],
{column, default(pos_integer(), undefined)},
[{Annotation, string()} || Annotation <- ?ERL_STRING_ANNOTATIONS],
[{Annotation, boolean()} || Annotation <- ?ERL_BOOLEAN_ANNOTATIONS]
]).
annotated_node_type() ->
AnnotationTypes = [default(none, Type) || Type <- annotation_types()],
?LET(
{Node, AnnotationsWithValues},
{node_type(), AnnotationTypes},
begin
Anno0 = erl_syntax:get_pos(Node),
Anno1 = lists:foldl(
fun
(none, Anno) ->
Anno;
({column, undefined}, Anno) ->
erl_anno:set_location(erl_anno:line(Anno), Anno);
({column, Column}, Anno) ->
erl_anno:set_location({erl_anno:line(Anno), Column}, Anno);
({Annotation, Value}, Anno) ->
SetFunction = binary_to_atom(
<<"set_", (atom_to_binary(Annotation))/bytes>>
),
erl_anno:SetFunction(Value, Anno)
end,
Anno0,
AnnotationsWithValues
),
erl_syntax:set_pos(Node, Anno1)
end
).
get_sorted_pos(Node) ->
lists:sort(erl_syntax:get_pos(Node)).
get_sorted_ann(Node) ->
lists:sort(erl_syntax:get_ann(Node)).
-endif.