Skip to content

Commit d76c358

Browse files
author
José Valim
committed
Warn if heredoc is outdented, closes #7174
1 parent 6a58127 commit d76c358

File tree

2 files changed

+63
-23
lines changed

2 files changed

+63
-23
lines changed

lib/elixir/src/elixir_tokenizer.erl

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ collect_modifiers(Rest, Buffer) ->
711711
%% Heredocs
712712

713713
extract_heredoc_with_interpolation(Line, Column, Scope, Interpol, T, H) ->
714-
case extract_heredoc(Line, Column, T, H) of
714+
case extract_heredoc(Line, Column, T, H, Scope) of
715715
{ok, NewLine, NewColumn, Body, Rest} ->
716716
case elixir_interpolation:extract(Line + 1, 1, Scope, Interpol, Body, 0) of
717717
{error, Reason} ->
@@ -723,15 +723,15 @@ extract_heredoc_with_interpolation(Line, Column, Scope, Interpol, T, H) ->
723723
Error
724724
end.
725725

726-
extract_heredoc(Line0, Column0, Rest0, Marker) ->
726+
extract_heredoc(Line0, Column0, Rest0, Marker, Scope) ->
727727
case extract_heredoc_header(Rest0) of
728728
{ok, Rest1} ->
729729
%% We prepend a new line so we can transparently remove
730730
%% spaces later. This new line is removed by calling "tl"
731731
%% in the final heredoc body three lines below.
732732
case extract_heredoc_body(Line0, Column0, Marker, [$\n | Rest1], []) of
733733
{ok, Line1, Body, Rest2, Spaces} ->
734-
{ok, Line1, 1, tl(remove_heredoc_spaces(Body, Spaces)), Rest2};
734+
{ok, Line1, 1, tl(remove_heredoc_spaces(Body, Spaces, Marker, Scope)), Rest2};
735735
{error, Reason, ErrorLine} ->
736736
Terminator = [Marker, Marker, Marker],
737737
{Message, Token} = heredoc_error_message(Reason, Line0, Terminator),
@@ -746,23 +746,49 @@ heredoc_error_message(eof, Line, Terminator) ->
746746
{io_lib:format("missing terminator: ~ts (for heredoc starting at line ~B)",
747747
[Terminator, Line]),
748748
[]};
749-
heredoc_error_message(misplacedterminator, _Line, Terminator) ->
749+
heredoc_error_message(badterminator, _Line, Terminator) ->
750750
{"invalid location for heredoc terminator, please escape token or move it to its own line: ",
751751
Terminator}.
752+
752753
%% Remove spaces from heredoc based on the position of the final quotes.
753754

754-
remove_heredoc_spaces(Body, 0) ->
755-
lists:reverse([0 | Body]);
756-
remove_heredoc_spaces(Body, Spaces) ->
757-
remove_heredoc_spaces([0 | Body], [], Spaces, Spaces).
758-
remove_heredoc_spaces([H, $\n | T], [Backtrack | Buffer], Spaces, Original) when Spaces > 0, ?is_horizontal_space(H) ->
759-
remove_heredoc_spaces([Backtrack, $\n | T], Buffer, Spaces - 1, Original);
760-
remove_heredoc_spaces([$\n=H | T], Buffer, _Spaces, Original) ->
761-
remove_heredoc_spaces(T, [H | Buffer], Original, Original);
762-
remove_heredoc_spaces([H | T], Buffer, Spaces, Original) ->
763-
remove_heredoc_spaces(T, [H | Buffer], Spaces, Original);
764-
remove_heredoc_spaces([], Buffer, _Spaces, _Original) ->
765-
Buffer.
755+
remove_heredoc_spaces(Body, Spaces, Marker, Scope) ->
756+
case trim_spaces(Body, [0], Spaces, false) of
757+
{Acc, false} ->
758+
Acc;
759+
{Acc, Line} ->
760+
Msg = io_lib:format("outdented heredoc line. The contents inside the heredoc should be indented "
761+
"at the same level as the closing ~ts. The following is forbidden:~n~n"
762+
" def text do~n"
763+
" \"\"\"~n"
764+
" contents~n"
765+
" \"\"\"~n"
766+
" end~n~n"
767+
"Instead make sure the contents are indented as much as the heredoc closing:~n~n"
768+
" def text do~n"
769+
" \"\"\"~n"
770+
" contents~n"
771+
" \"\"\"~n"
772+
" end~n~n"
773+
"The current heredoc line is indented too litle", [[Marker, Marker, Marker]]),
774+
elixir_errors:warn(Line, Scope#elixir_tokenizer.file, Msg),
775+
Acc
776+
end.
777+
778+
trim_spaces([{Line, Entry} | Rest], Acc, Spaces, Warned) ->
779+
case trim_space(lists:reverse(Entry), Spaces) of
780+
{Trimmed, true} when Warned == false ->
781+
trim_spaces(Rest, Trimmed ++ Acc, Spaces, Line);
782+
{Trimmed, _} ->
783+
trim_spaces(Rest, Trimmed ++ Acc, Spaces, Warned)
784+
end;
785+
trim_spaces([], Acc, _Spaces, Warned) ->
786+
{Acc, Warned}.
787+
788+
trim_space(Rest, 0) -> {Rest, false};
789+
trim_space([$\n], _) -> {[$\n], false};
790+
trim_space([H | T], Spaces) when ?is_horizontal_space(H) -> trim_space(T, Spaces - 1);
791+
trim_space(Rest, _Spaces) -> {Rest, true}.
766792

767793
%% Extract the heredoc header.
768794

@@ -780,11 +806,11 @@ extract_heredoc_header(_) ->
780806
%% is aligned.
781807

782808
extract_heredoc_body(Line, _Column, Marker, Rest, Buffer) ->
783-
case extract_heredoc_line(Marker, Rest, Buffer, 0) of
784-
{ok, NewBuffer, NewRest} ->
785-
extract_heredoc_body(Line + 1, 1, Marker, NewRest, NewBuffer);
786-
{ok, NewBuffer, NewRest, Spaces} ->
787-
{ok, Line, NewBuffer, NewRest, Spaces};
809+
case extract_heredoc_line(Marker, Rest, [], 0) of
810+
{ok, Entry, NewRest} ->
811+
extract_heredoc_body(Line + 1, 1, Marker, NewRest, [{Line, Entry} | Buffer]);
812+
{done, Entry, NewRest, Spaces} ->
813+
{ok, Line, [{Line, Entry} | Buffer], NewRest, Spaces};
788814
{error, Reason} ->
789815
{error, Reason, Line}
790816
end.
@@ -797,7 +823,7 @@ extract_heredoc_line(Marker, [$\\, $\\ | T], Buffer) ->
797823
extract_heredoc_line(Marker, [$\\, Marker | T], Buffer) ->
798824
extract_heredoc_line(Marker, T, [Marker, $\\ | Buffer]);
799825
extract_heredoc_line(Marker, [Marker, Marker, Marker | _], _) ->
800-
{error, misplacedterminator};
826+
{error, badterminator};
801827
extract_heredoc_line(_, "\r\n" ++ Rest, Buffer) ->
802828
{ok, [$\n | Buffer], Rest};
803829
extract_heredoc_line(_, "\n" ++ Rest, Buffer) ->
@@ -812,7 +838,7 @@ extract_heredoc_line(_, _, _) ->
812838
extract_heredoc_line(Marker, [H | T], Buffer, Counter) when ?is_horizontal_space(H) ->
813839
extract_heredoc_line(Marker, T, [H | Buffer], Counter + 1);
814840
extract_heredoc_line(Marker, [Marker, Marker, Marker | T], Buffer, Counter) ->
815-
{ok, Buffer, T, Counter};
841+
{done, Buffer, T, Counter};
816842
extract_heredoc_line(Marker, Rest, Buffer, _Counter) ->
817843
extract_heredoc_line(Marker, Rest, Buffer).
818844

lib/elixir/test/elixir/kernel/warning_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ defmodule Kernel.WarningTest do
88
capture_io(:stderr, fun)
99
end
1010

11+
test "outdented heredoc" do
12+
output =
13+
capture_err(fn ->
14+
Code.eval_string("""
15+
'''
16+
outdented
17+
'''
18+
""")
19+
end)
20+
21+
assert output =~ "outdented heredoc line"
22+
assert output =~ "nofile:2"
23+
end
24+
1125
test "unused variable" do
1226
output =
1327
capture_err(fn ->

0 commit comments

Comments
 (0)