%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2015. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
-module(erl_anno).
-export([new/1, is_anno/1]).
-export([column/1, end_location/1, file/1, generated/1,
line/1, location/1, record/1, text/1]).
-export([set_file/2, set_generated/2, set_line/2, set_location/2,
set_record/2, set_text/2]).
%% To be used when necessary to avoid Dialyzer warnings.
-export([to_term/1, from_term/1]).
-export_type([anno/0, line/0, column/0, location/0, text/0]).
-export_type([anno_term/0]).
-define(LN(L), is_integer(L)).
-define(COL(C), (is_integer(C) andalso C >= 1)).
%% Location.
-define(LCOLUMN(C), ?COL(C)).
-define(LLINE(L), ?LN(L)).
%% Debug: define DEBUG to make sure that annotations are handled as an
%% opaque type. Note that all abstract code need to be compiled with
%% DEBUG=true. See also ./erl_pp.erl.
%-define(DEBUG, true).
-type annotation() :: {'file', filename()}
| {'generated', generated()}
| {'location', location()}
| {'record', record()}
| {'text', string()}.
-type anno() :: location() | [annotation(), ...].
-type anno_term() :: term().
-type column() :: pos_integer().
-type generated() :: boolean().
-type filename() :: file:filename_all().
-type line() :: integer().
-type location() :: line() | {line(), column()}.
-type record() :: boolean().
-type text() :: string().
-ifdef(DEBUG).
%% Anything 'false' accepted by the compiler.
-define(ALINE(A), is_reference(A)).
-define(ACOLUMN(A), is_reference(A)).
-else.
-define(ALINE(L), ?LN(L)).
-define(ACOLUMN(C), ?COL(C)).
-endif.
-spec to_term(Anno) -> anno_term() when
Anno :: anno().
-ifdef(DEBUG).
to_term(Anno) ->
simplify(Anno).
-else.
to_term(Anno) ->
Anno.
-endif.
-spec from_term(Term) -> Anno when
Term :: anno_term(),
Anno :: anno().
-ifdef(DEBUG).
from_term(Term) when is_list(Term) ->
Term;
from_term(Term) ->
[{location, Term}].
-else.
from_term(Term) ->
Term.
-endif.
-spec new(Location) -> anno() when
Location :: location().
new(Line) when ?LLINE(Line) ->
new_location(Line);
new({Line, Column}=Loc) when ?LLINE(Line), ?LCOLUMN(Column) ->
new_location(Loc);
new(Term) ->
erlang:error(badarg, [Term]).
-ifdef(DEBUG).
new_location(Location) ->
[{location, Location}].
-else.
new_location(Location) ->
Location.
-endif.
-spec is_anno(Term) -> boolean() when
Term :: any().
is_anno(Line) when ?ALINE(Line) ->
true;
is_anno({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
true;
is_anno(Anno) ->
(Anno =/= [] andalso
is_anno1(Anno) andalso
lists:keymember(location, 1, Anno)).
is_anno1([{Item, Value}|Anno]) ->
is_anno2(Item, Value) andalso is_anno1(Anno);
is_anno1(A) ->
A =:= [].
is_anno2(location, Line) when ?LN(Line) ->
true;
is_anno2(location, {Line, Column}) when ?LN(Line), ?COL(Column) ->
true;
is_anno2(generated, true) ->
true;
is_anno2(file, Filename) ->
is_filename(Filename);
is_anno2(record, true) ->
true;
is_anno2(text, Text) ->
is_string(Text);
is_anno2(_, _) ->
false.
is_filename(T) ->
is_list(T) orelse is_binary(T).
is_string(T) ->
try lists:all(fun(C) when is_integer(C), C >= 0 -> true end, T)
catch _:_ -> false
end.
-spec column(Anno) -> column() | 'undefined' when
Anno :: anno().
column({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
Column;
column(Line) when ?ALINE(Line) ->
undefined;
column(Anno) ->
case location(Anno) of
{_Line, Column} ->
Column;
_Line ->
undefined
end.
-spec end_location(Anno) -> location() | 'undefined' when
Anno :: anno().
end_location(Anno) ->
case text(Anno) of
undefined ->
undefined;
Text ->
case location(Anno) of
{Line, Column} ->
end_location(Text, Line, Column);
Line ->
end_location(Text, Line)
end
end.
-spec file(Anno) -> filename() | 'undefined' when
Anno :: anno().
file(Line) when ?ALINE(Line) ->
undefined;
file({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
undefined;
file(Anno) ->
anno_info(Anno, file).
-spec generated(Anno) -> generated() when
Anno :: anno().
generated(Line) when ?ALINE(Line) ->
Line =< 0;
generated({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
Line =< 0;
generated(Anno) ->
_ = anno_info(Anno, generated, false),
{location, Location} = lists:keyfind(location, 1, Anno),
case Location of
{Line, _Column} ->
Line =< 0;
Line ->
Line =< 0
end.
-spec line(Anno) -> line() when
Anno :: anno().
line(Anno) ->
case location(Anno) of
{Line, _Column} ->
Line;
Line ->
Line
end.
-spec location(Anno) -> location() when
Anno :: anno().
location(Line) when ?ALINE(Line) ->
abs(Line);
location({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
{abs(Line), Column};
location(Anno) ->
case anno_info(Anno, location) of
Line when Line < 0 ->
-Line;
{Line, Column} when Line < 0 ->
{-Line, Column};
Location ->
Location
end.
-spec record(Anno) -> record() when
Anno :: anno().
record(Line) when ?ALINE(Line) ->
false;
record({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
false;
record(Anno) ->
anno_info(Anno, record, false).
-spec text(Anno) -> text() | 'undefined' when
Anno :: anno().
text(Line) when ?ALINE(Line) ->
undefined;
text({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
undefined;
text(Anno) ->
anno_info(Anno, text).
-spec set_file(File, Anno) -> Anno when
File :: filename(),
Anno :: anno().
set_file(File, Anno) ->
set(file, File, Anno).
-spec set_generated(Generated, Anno) -> Anno when
Generated :: generated(),
Anno :: anno().
set_generated(true, Line) when ?ALINE(Line) ->
-abs(Line);
set_generated(false, Line) when ?ALINE(Line) ->
abs(Line);
set_generated(true, {Line, Column}) when ?ALINE(Line),
?ACOLUMN(Column) ->
{-abs(Line),Column};
set_generated(false, {Line, Column}) when ?ALINE(Line),
?ACOLUMN(Column) ->
{abs(Line),Column};
set_generated(Generated, Anno) ->
_ = set(generated, Generated, Anno),
{location, Location} = lists:keyfind(location, 1, Anno),
NewLocation =
case Location of
{Line, Column} when Generated ->
{-abs(Line), Column};
{Line, Column} when not Generated ->
{abs(Line), Column};
Line when Generated ->
-abs(Line);
Line when not Generated ->
abs(Line)
end,
lists:keyreplace(location, 1, Anno, {location, NewLocation}).
-spec set_line(Line, Anno) -> Anno when
Line :: line(),
Anno :: anno().
set_line(Line, Anno) ->
case location(Anno) of
{_Line, Column} ->
set_location({Line, Column}, Anno);
_Line ->
set_location(Line, Anno)
end.
-spec set_location(Location, Anno) -> Anno when
Location :: location(),
Anno :: anno().
set_location(Line, L) when ?ALINE(L), ?LLINE(Line) ->
new_location(fix_line(Line, L));
set_location(Line, {L, Column}) when ?ALINE(L), ?ACOLUMN(Column),
?LLINE(Line) ->
new_location(fix_line(Line, L));
set_location({L, C}=Loc, Line) when ?ALINE(Line), ?LLINE(L), ?LCOLUMN(C) ->
new_location(fix_location(Loc, Line));
set_location({L, C}=Loc, {Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column),
?LLINE(L), ?LCOLUMN(C) ->
new_location(fix_location(Loc, Line));
set_location(Location, Anno) ->
_ = set(location, Location, Anno),
{location, OldLocation} = lists:keyfind(location, 1, Anno),
NewLocation =
case {Location, OldLocation} of
{{_Line, _Column}=Loc, {L, _C}} ->
fix_location(Loc, L);
{Line, {L, _C}} ->
fix_line(Line, L);
{{_Line, _Column}=Loc, L} ->
fix_location(Loc, L);
{Line, L} ->
fix_line(Line, L)
end,
lists:keyreplace(location, 1, Anno, {location, NewLocation}).
fix_location({Line, Column}, OldLine) ->
{fix_line(Line, OldLine), Column}.
fix_line(Line, OldLine) when OldLine < 0, Line > 0 ->
-Line;
fix_line(Line, _OldLine) ->
Line.
-spec set_record(Record, Anno) -> Anno when
Record :: record(),
Anno :: anno().
set_record(Record, Anno) ->
set(record, Record, Anno).
-spec set_text(Text, Anno) -> Anno when
Text :: text(),
Anno :: anno().
set_text(Text, Anno) ->
set(text, Text, Anno).
set(Item, Value, Anno) ->
case {is_settable(Item, Value), Anno} of
{true, Line} when ?ALINE(Line) ->
set_anno(Item, Value, [{location, Line}]);
{true, {L, C}=Location} when ?ALINE(L), ?ACOLUMN(C) ->
set_anno(Item, Value, [{location, Location}]);
{true, A} when is_list(A), A =/= [] ->
set_anno(Item, Value, Anno);
_ ->
erlang:error(badarg, [Item, Value, Anno])
end.
set_anno(Item, Value, Anno) ->
case default(Item, Value) of
true ->
reset(Anno, Item);
false ->
R = case anno_info(Anno, Item) of
undefined ->
[{Item, Value}|Anno];
_ ->
lists:keyreplace(Item, 1, Anno, {Item, Value})
end,
simplify(R)
end.
reset(Anno, Item) ->
A = lists:keydelete(Item, 1, Anno),
reset_simplify(A).
-ifdef(DEBUG).
reset_simplify(A) ->
A.
-else.
reset_simplify(A) ->
simplify(A).
-endif.
simplify([{location, Location}]) ->
Location;
simplify(Anno) ->
Anno.
anno_info(Anno, Item, Default) ->
try lists:keyfind(Item, 1, Anno) of
false ->
Default;
{Item, Value} ->
Value
catch
_:_ ->
erlang:error(badarg, [Anno])
end.
anno_info(Anno, Item) ->
try lists:keyfind(Item, 1, Anno) of
{Item, Value} ->
Value;
false ->
undefined
catch
_:_ ->
erlang:error(badarg, [Anno])
end.
end_location("", Line, Column) ->
{Line, Column};
end_location([$\n|String], Line, _Column) ->
end_location(String, Line+1, 1);
end_location([_|String], Line, Column) ->
end_location(String, Line, Column+1).
end_location("", Line) ->
Line;
end_location([$\n|String], Line) ->
end_location(String, Line+1);
end_location([_|String], Line) ->
end_location(String, Line).
is_settable(file, File) ->
is_filename(File);
is_settable(generated, Boolean) when Boolean; not Boolean ->
true;
is_settable(location, Line) when ?LLINE(Line) ->
true;
is_settable(location, {Line, Column}) when ?LLINE(Line), ?LCOLUMN(Column) ->
true;
is_settable(record, Boolean) when Boolean; not Boolean ->
true;
is_settable(text, Text) ->
is_string(Text);
is_settable(_, _) ->
false.
default(generated, false) -> true;
default(record, false) -> true;
default(_, _) -> false.