Skip to content

Commit

Permalink
Quote source as part of error messages
Browse files Browse the repository at this point in the history
Can be suppressed with the 'brief' compiler option.
Moves message formatting code to a separate module.
  • Loading branch information
richcarl committed Feb 2, 2021
1 parent e0dc4f9 commit 5170eff
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 32 deletions.
7 changes: 7 additions & 0 deletions lib/compiler/doc/src/compile.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@

<p>Available options:</p>
<taglist>
<tag><c>brief</c></tag>
<item>
<p>Restricts error and warning messages to a single line of output.
As of OTP 24, the compiler will by default also display the part
of the source code that the message refers to.</p>
</item>

<tag><c>basic_validation</c></tag>
<item>
<p>This option is a fast way to test whether a module will
Expand Down
1 change: 1 addition & 0 deletions lib/compiler/src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ MODULES = \
cerl_inline \
cerl_trees \
compile \
compile_messages \
core_lib \
core_lint \
core_parse \
Expand Down
34 changes: 4 additions & 30 deletions lib/compiler/src/compile.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1748,8 +1748,8 @@ write_binary(Name, Bin, St) ->
report_errors(#compile{options=Opts,errors=Errors}) ->
case member(report_errors, Opts) of
true ->
foreach(fun ({{F,_L},Eds}) -> list_errors(F, Eds);
({F,Eds}) -> list_errors(F, Eds) end,
foreach(fun ({{F,_L},Eds}) -> compile_messages:list_errors(F, Eds, Opts);
({F,Eds}) -> compile_messages:list_errors(F, Eds, Opts) end,
Errors);
false -> ok
end.
Expand All @@ -1763,40 +1763,14 @@ report_warnings(#compile{options=Opts,warnings=Ws0}) ->
ReportWerror = Werror andalso member(report_errors, Opts),
case member(report_warnings, Opts) orelse ReportWerror of
true ->
Ws1 = flatmap(fun({{F,_L},Eds}) -> format_message(F, P, Eds);
({F,Eds}) -> format_message(F, P, Eds) end,
Ws1 = flatmap(fun({{F,_L},Eds}) -> compile_messages:format_messages(F, P, Eds, Opts);
({F,Eds}) -> compile_messages:format_messages(F, P, Eds, Opts) end,
Ws0),
Ws = lists:sort(Ws1),
foreach(fun({_,Str}) -> io:put_chars(Str) end, Ws);
false -> ok
end.

format_message(F, P, [{none,Mod,E}|Es]) ->
M = {none,io_lib:format("~ts: ~s~ts\n", [F,P,Mod:format_error(E)])},
[M|format_message(F, P, Es)];
format_message(F, P, [{{Line,Column}=Loc,Mod,E}|Es]) ->
M = {{F,Loc},io_lib:format("~ts:~w:~w: ~s~ts\n",
[F,Line,Column,P,Mod:format_error(E)])},
[M|format_message(F, P, Es)];
format_message(F, P, [{Line,Mod,E}|Es]) ->
M = {{F,{Line,0}},io_lib:format("~ts:~w: ~s~ts\n",
[F,Line,P,Mod:format_error(E)])},
[M|format_message(F, P, Es)];
format_message(_, _, []) -> [].

%% list_errors(File, ErrorDescriptors) -> ok

list_errors(F, [{none,Mod,E}|Es]) ->
io:fwrite("~ts: ~ts\n", [F,Mod:format_error(E)]),
list_errors(F, Es);
list_errors(F, [{{Line,Column},Mod,E}|Es]) ->
io:fwrite("~ts:~w:~w: ~ts\n", [F,Line,Column,Mod:format_error(E)]),
list_errors(F, Es);
list_errors(F, [{Line,Mod,E}|Es]) ->
io:fwrite("~ts:~w: ~ts\n", [F,Line,Mod:format_error(E)]),
list_errors(F, Es);
list_errors(_F, []) -> ok.

%% erlfile(Dir, Base) -> ErlFile
%% outfile(Base, Extension, Options) -> OutputFile
%% objfile(Base, Target, Options) -> ObjFile
Expand Down
214 changes: 214 additions & 0 deletions lib/compiler/src/compile_messages.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2018. All Rights Reserved.
%% Copyright 2020 Facebook, Inc. and its affiliates.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%

-module(compile_messages).

-export([format_messages/4, list_errors/3]).

-type pos() :: integer() | {integer(), integer()}.
-type err_warn_info() :: tuple().
-type message() :: {Where :: none | {File::string(), pos()}, Text :: iolist()}.

-spec format_messages(File::string(), Prefix::string(), [err_warn_info()],
Opts::[term()]) -> [message()].

format_messages(F, P, [{none, Mod, E} | Es], Opts) ->
M = {none, io_lib:format("~ts: ~s~ts\n", [F, P, Mod:format_error(E)])},
[M | format_messages(F, P, Es, Opts)];
format_messages(F, P, [{Loc, Mod, E} | Es], Opts) ->
StartLoc = erl_anno:location(Loc),
EndLoc =
case erl_anno:end_location(Loc) of
undefined -> StartLoc;
Loc2 -> Loc2
end,
Src = quote_source(F, StartLoc, EndLoc, Opts),
Msg = io_lib:format("~ts:~ts: ~s~ts\n~ts", [
F,
fmt_pos(StartLoc),
P,
Mod:format_error(E),
Src
]),
Pos =
if
is_integer(StartLoc) -> {StartLoc, 0};
true -> StartLoc
end,
[{{F, Pos}, Msg} | format_messages(F, P, Es, Opts)];
format_messages(_, _, [], _Opts) ->
[].

-spec list_errors(File::string(), [err_warn_info()], Opts::[term()]) -> ok.

list_errors(F, [{none, Mod, E} | Es], Opts) ->
io:fwrite("~ts: ~ts\n", [F, Mod:format_error(E)]),
list_errors(F, Es, Opts);
list_errors(F, [{{{_, _} = StartLoc, {_, _} = EndLoc}, Mod, E} | Es], Opts) ->
%% this is the location format used in the type analysis pass
Src = quote_source(F, StartLoc, EndLoc, Opts),
io:fwrite("~ts:~ts: ~ts\n~ts", [F, fmt_pos(StartLoc), Mod:format_error(E), Src]),
list_errors(F, Es, Opts);
list_errors(F, [{Loc, Mod, E} | Es], Opts) ->
StartLoc = erl_anno:location(Loc),
EndLoc =
case erl_anno:end_location(Loc) of
undefined -> StartLoc;
Loc2 -> Loc2
end,
list_errors(F, [{{StartLoc, EndLoc}, Mod, E} | Es], Opts);
list_errors(_F, [], _Opts) ->
ok.

fmt_pos({Line, Col}) ->
io_lib:format("~w:~w", [Line, Col]);
fmt_pos(Line) ->
io_lib:format("~w", [Line]).

quote_source(File, StartLoc, EndLoc, Opts) ->
case proplists:get_bool(brief, Opts) of
true -> "";
false -> quote_source_1(File, StartLoc, EndLoc)
end.

quote_source_1(File, Line, Loc2) when is_integer(Line) ->
quote_source_1(File, {Line, 1}, Loc2);
quote_source_1(File, Loc1, Line) when is_integer(Line) ->
quote_source_1(File, Loc1, {Line, -1});
quote_source_1(File, {StartLine, StartCol}, {EndLine, EndCol}) ->
case file:read_file(File) of
{ok, Bin} ->
Ctx =
if
StartLine =:= EndLine -> 0;
true -> 1
end,
case seek_line(Bin, 1, StartLine - Ctx) of
{ok, Bin1} ->
quote_source_2(Bin1, StartLine, StartCol, EndLine, EndCol, Ctx);
error ->
""
end;
{error, _} ->
""
end.

quote_source_2(Bin, StartLine, StartCol, EndLine, EndCol, Ctx) ->
case take_lines(Bin, StartLine - Ctx, EndLine + Ctx) of
[] ->
"";
Lines ->
Lines1 =
case length(Lines) =< (4 + Ctx) of
true ->
Lines;
false ->
%% before = context + start line + following line
%% after = end line + context
%% (total lines: 3 + 1 + context)
Before = lists:sublist(Lines, 2 + Ctx),
After = lists:reverse(
lists:sublist(lists:reverse(Lines), 1 + Ctx)
),
Before ++ [{0, "..."}] ++ After
end,
Lines2 = decorate(Lines1, StartLine, StartCol, EndLine, EndCol),
[[fmt_line(L, Text) || {L, Text} <- Lines2], $\n]
end.

line_prefix() ->
"% ".

fmt_line(L, Text) ->
io_lib:format("~ts~4.ts| ~ts\n", [line_prefix(), line_to_txt(L), Text]).

line_to_txt(0) -> "";
line_to_txt(L) -> integer_to_list(L).

decorate([{Line, Text} = L | Ls], StartLine, StartCol, EndLine, EndCol) when
Line =:= StartLine, EndLine =:= StartLine
->
%% start and end on same line
S = underline(Text, StartCol, EndCol),
decorate(S, L, Ls, StartLine, StartCol, EndLine, EndCol);
decorate([{Line, Text} = L | Ls], StartLine, StartCol, EndLine, EndCol) when Line =:= StartLine ->
%% start with end on separate line
S = underline(Text, StartCol, string:length(Text) + 1),
decorate(S, L, Ls, StartLine, StartCol, EndLine, EndCol);
%% decorate([{Line, Text}=L|Ls], StartLine, StartCol, EndLine, EndCol)
%% when Line =:= EndLine ->
%% S = underline(Text, EndCol,EndCol), % just mark end
%% decorate(S, L, Ls, StartLine, StartCol, EndLine, EndCol);
decorate([{_Line, _Text} = L | Ls], StartLine, StartCol, EndLine, EndCol) ->
[L | decorate(Ls, StartLine, StartCol, EndLine, EndCol)];
decorate([], _StartLine, _StartCol, _EndLine, _EndCol) ->
[].

%% don't produce empty decoration lines
decorate("", L, Ls, StartLine, StartCol, EndLine, EndCol) ->
[L | decorate(Ls, StartLine, StartCol, EndLine, EndCol)];
decorate(Text, L, Ls, StartLine, StartCol, EndLine, EndCol) ->
[L, {0, Text} | decorate(Ls, StartLine, StartCol, EndLine, EndCol)].

%% End typically points to the first position after the actual region.
%% If End = Start, we adjust it to Start+1 to mark at least one character
%% TODO: colorization option
underline(_Text, Start, End) when End < Start ->
% no underlining at all if end column is unknown
"";
underline(Text, Start, Start) ->
underline(Text, Start, Start + 1);
underline(Text, Start, End) ->
underline(Text, Start, End, 1).

underline(<<$\t, Text/binary>>, Start, End, N) when N < Start ->
[$\t | underline(Text, Start, End, N + 1)];
underline(<<_, Text/binary>>, Start, End, N) when N < Start ->
[$\s | underline(Text, Start, End, N + 1)];
underline(_Text, _Start, End, N) ->
underline_1(N, End).

underline_1(N, End) when N < End ->
[$^ | underline_1(N + 1, End)];
underline_1(_N, _End) ->
"".

seek_line(Bin, L, L) -> {ok, Bin};
seek_line(<<$\n, Rest/binary>>, N, L) -> seek_line(Rest, N + 1, L);
seek_line(<<$\r, $\n, Rest/binary>>, N, L) -> seek_line(Rest, N + 1, L);
seek_line(<<_, Rest/binary>>, N, L) -> seek_line(Rest, N, L);
seek_line(<<>>, _, _) -> error.

take_lines(<<>>, _Here, _To) ->
[];
take_lines(Bin, Here, To) when Here =< To ->
{Line, Rest} = take_line(Bin, <<>>),
[{Here, Line} | take_lines(Rest, Here + 1, To)];
take_lines(_Bin, _Here, _To) ->
[].

take_line(<<$\n, Rest/binary>>, Ack) ->
{Ack, Rest};
take_line(<<$\r, $\n, Rest/binary>>, Ack) ->
{Ack, Rest};
take_line(<<B, Rest/binary>>, Ack) ->
take_line(Rest, <<Ack/binary, B>>);
take_line(<<>>, Ack) ->
{Ack, <<>>}.
11 changes: 9 additions & 2 deletions lib/stdlib/src/erl_lint.erl
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,9 @@ format_mna({M, N, A}) when is_integer(A) ->
format_where(L) when is_integer(L) ->
io_lib:format("(line ~p)", [L]);
format_where({L,C}) when is_integer(L), is_integer(C) ->
io_lib:format("(line ~p, column ~p)", [L, C]).
io_lib:format("(line ~p, column ~p)", [L, C]);
format_where(Anno) ->
format_where(erl_anno:location(Anno)).

%% Local functions that are somehow automatically generated.

Expand Down Expand Up @@ -705,7 +707,12 @@ add_lint_warning(W, File, St) ->
St#lint{warnings=[{File,W}|St#lint.warnings]}.

loc(Anno, St) ->
Location = erl_anno:location(Anno),
Location0 = erl_anno:location(Anno),
Location = case erl_anno:end_location(Anno) of
undefined -> Location0;
EndLoc -> [{location, Location0},
{end_location, EndLoc}]
end,
case erl_anno:file(Anno) of
undefined -> {St#lint.file,Location};
File -> {File,Location}
Expand Down

0 comments on commit 5170eff

Please sign in to comment.