diff --git a/src/erlfmt_format.erl b/src/erlfmt_format.erl index dcb1c96..2f7d3b5 100644 --- a/src/erlfmt_format.erl +++ b/src/erlfmt_format.erl @@ -93,6 +93,12 @@ to_algebra({attribute, Meta, {atom, _, record}, [Name, {tuple, TMeta, Values} = Doc = surround(HeadD, <<"">>, expr_to_algebra(Tuple), <<"">>, <<")">>), combine_comments_with_dot(Meta, Doc) end; +to_algebra({attribute, Meta, {atom, _, RawName} = Name, [Value]}) when + RawName =:= doc; RawName =:= moduledoc +-> + ValueD = expr_to_algebra(Value), + Doc = concat([<<"-">>, expr_to_algebra(Name), <<" ">>, ValueD]), + combine_comments_with_dot(Meta, Doc); to_algebra({attribute, Meta, Name, Values}) -> Doc = concat(<<"-">>, attribute_to_algebra(Name), call(Meta, Values, <<"(">>, <<")">>)), combine_comments_with_dot(Meta, Doc); @@ -290,10 +296,8 @@ surround_block(Left, Doc, Right) -> string_to_algebra(Text) -> case string:split(Text, "\n", all) of - ["\"\"\"", Line, "\"\"\""] -> - string_to_algebra(["\"", Line, "\""]); - ["\"\"\"" | _Rest] -> - string(Text); + ["\"\"\"" ++ _ = Quote | Rest] -> + tripple_quoted_to_algebra(Quote, Rest); [Line] -> string(Line); [First, "\""] -> @@ -304,6 +308,26 @@ string_to_algebra(Text) -> concat([force_breaks(), FirstD, line(), LinesD]) end. +%% tripple-quoted string might have actually more than tripple quotes +%% indentation to remove from each line is given from the closing parenteses per documentation +%% see: https://www.erlang.org/doc/system/data_types#string +tripple_quoted_to_algebra(Quote, LinesWithFinalQuote) -> + {Lines, FinalQuote} = splitlast(LinesWithFinalQuote, []), + IndentToRemove = calculate_indentation(Quote, FinalQuote, []), + CleanedLinesD = [string(remove_indentation(Line, IndentToRemove)) || Line <- Lines], + LinesD = fold_doc(fun erlfmt_algebra:line/2, CleanedLinesD), + QuoteD = string(Quote), + line(QuoteD, line(LinesD, QuoteD)). + +splitlast([T], Acc) -> {lists:reverse(Acc), T}; +splitlast([H | T], Acc) -> splitlast(T, [H | Acc]). + +calculate_indentation(Quote, Quote, Acc) -> lists:reverse(Acc); +calculate_indentation(Quote, [I | Rest], Acc) -> calculate_indentation(Quote, Rest, [I | Acc]). + +remove_indentation(Line, []) -> Line; +remove_indentation([H | LineRest], [H | IndentRest]) -> remove_indentation(LineRest, IndentRest). + string_lines_to_algebra([LastLine]) -> string(["\"" | LastLine]); string_lines_to_algebra([Line, "\""]) -> diff --git a/test/erlfmt_SUITE.erl b/test/erlfmt_SUITE.erl index d751c92..a8e0a98 100644 --- a/test/erlfmt_SUITE.erl +++ b/test/erlfmt_SUITE.erl @@ -80,7 +80,6 @@ error_ignore_begin_ignore_old/1, error_ignore_begin_ignore_begin_old/1, error_ignore_end_old/1, - error_ignore_ignore_old/1, simple_comments_range/1, broken_range/1, snapshot_range_whole_comments/1, @@ -99,6 +98,7 @@ snapshot_enclosing_range2/1, snapshot_enclosing_range_no_leak/1, snapshot_range_reinjected/1, + snapshot_tripple_string/1, contains_pragma/1, insert_pragma/1, overlong_warning/1, @@ -116,6 +116,11 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok. +init_per_group(otp_27_snapshot_tests, Config) -> + case erlang:system_info(otp_release) >= "27" of + true -> Config; + false -> {skip, "Skipping tests for features from OTP >= 27"} + end; init_per_group(_GroupName, Config) -> Config. @@ -173,7 +178,11 @@ groups() -> snapshot_ignore_format_old, snapshot_ignore_format_many_old, snapshot_empty, - format_string_unicode + format_string_unicode, + {group, otp_27_snapshot_tests} + ]}, + {otp_27_snapshot_tests, [parallel], [ + snapshot_tripple_string ]}, {error_tests, [parallel], [ error_ignore_begin_ignore, @@ -1100,6 +1109,8 @@ snapshot_empty(Config) -> snapshot_same("empty.erl", Config). snapshot_insert_pragma_with(Config) when is_list(Config) -> snapshot_same("pragma.erl", [{pragma, insert} | Config]). +snapshot_tripple_string(Config) -> snapshot_formatted("tripple_string.erl", Config). + snapshot_same(Module, Config) -> Pragma = proplists:get_value(pragma, Config, ignore), snapshot_match(Module, Module, Config, [{pragma, Pragma}]). diff --git a/test/erlfmt_SUITE_data/tripple_string.erl b/test/erlfmt_SUITE_data/tripple_string.erl new file mode 100644 index 0000000..9a00a8a --- /dev/null +++ b/test/erlfmt_SUITE_data/tripple_string.erl @@ -0,0 +1,38 @@ +-module(triple_string). +-moduledoc """ +abc +""". + +-doc "foobar". +-type my_type :: integer(). + +-doc """ + baz + extra + """. +-opaque otype :: integer(). + +-doc "qux". +foo() -> + """ + foo + """. + +bar() -> +""" +the + long + string +""". + +baz() -> + "foo +". + +four_quotes() -> + """" + """ + the + long + string + """". diff --git a/test/erlfmt_SUITE_data/tripple_string.erl.formatted b/test/erlfmt_SUITE_data/tripple_string.erl.formatted new file mode 100644 index 0000000..615fd75 --- /dev/null +++ b/test/erlfmt_SUITE_data/tripple_string.erl.formatted @@ -0,0 +1,37 @@ +-module(triple_string). +-moduledoc """ +abc +""". + +-doc "foobar". +-type my_type :: integer(). + +-doc """ +baz + extra +""". +-opaque otype :: integer(). + +-doc "qux". +foo() -> + """ + foo + """. + +bar() -> + """ + the + long + string + """. + +baz() -> + "foo\n". + +four_quotes() -> + """" + """ + the + long + string + """". diff --git a/test/erlfmt_format_SUITE.erl b/test/erlfmt_format_SUITE.erl index 4fd2247..7fa0b0f 100644 --- a/test/erlfmt_format_SUITE.erl +++ b/test/erlfmt_format_SUITE.erl @@ -91,8 +91,6 @@ init_per_group(otp_27_features, Config) -> true -> Config; false -> {skip, "Skipping tests for features from OTP >= 27"} end; -init_per_group(_, Config) -> - Config; init_per_group(_GroupName, Config) -> Config. @@ -179,6 +177,7 @@ all() -> [ {group, expressions}, {group, forms}, + {group, otp_27_features}, comment ]. @@ -255,14 +254,28 @@ sigils(Config) when is_list(Config) -> %% The modifier X does not technically exist, but there seems to be no supported %% modifiers yet even though they are correctly parsed. ?assertSame("~b\"abc\\txyz\"x\n"), - ?assertSame("~s\"\"\"\n\\tabc\n\\tdef\n\"\"\"\n"), %% https://www.erlang.org/blog/highlights-otp-27/#triple-quoted-strings ?assertFormat( + "\t\"\"\"\n" + "\t\tTest\n" + "\t\t Test\n" + "\t\"\"\"\n", + "\"\"\"\n" + "\tTest\n" + "\t Test\n" + "\"\"\"\n" + ), + ?assertFormat( + " \"\"\"\n" + " Test\n" + " Test\n" + " \"\"\"\n", "\"\"\"\n" "Test\n" - "\"\"\"\n", - "\"Test\"\n" + " Test\n" + "\"\"\"\n" ), + ?assertSame("~s\"\"\"\n\\tabc\n\\tdef\n\"\"\"\n"), ?assertSame("\"\"\"\nTest\nMultiline\n\"\"\"\n"), ?assertSame("~\"\"\"\nTest\nMultiline\n\"\"\"\n"). @@ -4270,10 +4283,10 @@ comment(Config) when is_list(Config) -> ). doc_attributes(Config) when is_list(Config) -> - ?assertSame("-moduledoc(\"Test\").\n-moduledoc(#{since => <<\"1.0.0\">>}).\n"), - ?assertSame("-moduledoc(\"\"\"\nTest\nMultiline\n\"\"\").\n"), - ?assertSame("-doc(\"Test\").\n-doc(#{since => <<\"1.0.0\">>}).\ntest() -> ok.\n"), - ?assertSame("-doc(\"Test\").\n-doc(#{since => <<\"1.0.0\">>}).\n-type t() :: ok.\n"). + ?assertSame("-moduledoc \"Test\".\n-moduledoc #{since => <<\"1.0.0\">>}.\n"), + ?assertSame("-moduledoc \"\"\"\nTest\nMultiline\n\"\"\".\n"), + ?assertSame("-doc \"Test\".\n-doc #{since => <<\"1.0.0\">>}.\ntest() -> ok.\n"), + ?assertSame("-doc \"Test\".\n-doc #{since => <<\"1.0.0\">>}.\n-type t() :: ok.\n"). doc_macros(Config) when is_list(Config) -> %% Doc Attributes as macros is a common pattern for OTP < 27 compatibility.