diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00fb424..838bb53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,9 @@ jobs: - os: ubuntu-22.04 otp-version: 26 rebar3-version: 3.22 + - os: ubuntu-22.04 + otp-version: 27 + rebar3-version: 3.23 runs-on: ${{ matrix.os }} steps: - name: Checkout diff --git a/src/erlfmt_format.erl b/src/erlfmt_format.erl index 74a57da..dcb1c96 100644 --- a/src/erlfmt_format.erl +++ b/src/erlfmt_format.erl @@ -254,6 +254,22 @@ do_expr_to_algebra({clauses, _Meta, Clauses}) -> clauses_to_algebra(Clauses); do_expr_to_algebra({body, _Meta, Exprs}) -> block_to_algebra(Exprs); +do_expr_to_algebra({sigil, _Meta, Prefix, Content, Suffix}) -> + concat( + concat( + concat(<<"~">>, do_expr_to_algebra(Prefix)), + do_expr_to_algebra(Content) + ), + do_expr_to_algebra(Suffix) + ); +do_expr_to_algebra({sigil_prefix, _Meta, ''}) -> + <<"">>; +do_expr_to_algebra({sigil_prefix, _Meta, SigilName}) -> + atom_to_binary(SigilName, utf8); +do_expr_to_algebra({sigil_suffix, _Meta, []}) -> + <<>>; +do_expr_to_algebra({sigil_suffix, _Meta, Modifiers}) -> + list_to_binary(Modifiers); do_expr_to_algebra(Other) -> error(unsupported, [Other]). @@ -274,6 +290,10 @@ surround_block(Left, Doc, Right) -> string_to_algebra(Text) -> case string:split(Text, "\n", all) of + ["\"\"\"", Line, "\"\"\""] -> + string_to_algebra(["\"", Line, "\""]); + ["\"\"\"" | _Rest] -> + string(Text); [Line] -> string(Line); [First, "\""] -> diff --git a/src/erlfmt_parse.yrl b/src/erlfmt_parse.yrl index dfd3a5d..ad9d0fb 100644 --- a/src/erlfmt_parse.yrl +++ b/src/erlfmt_parse.yrl @@ -62,7 +62,8 @@ char integer float atom string var '<<' '>>' '?' '!' '=' '::' '..' '...' spec callback define_expr define_type define_clause standalone_exprs % helper -dot. +dot +sigil_prefix sigil_suffix. %% Conflict comes from optional parens on macro calls. Expect 1. @@ -607,6 +608,7 @@ atomic -> string : '$1'. atomic -> string concatables : {concat, ?range_anno('$1', '$2'), ['$1' | ?val('$2')]}. atomic -> macro_call_none concatables : {concat, ?range_anno('$1', '$2'), ['$1' | ?val('$2')]}. atomic -> macro_string concatables : {concat, ?range_anno('$1', '$2'), ['$1' | ?val('$2')]}. +atomic -> sigil_prefix string sigil_suffix : {sigil, ?range_anno('$1', '$3'), '$1', '$2', '$3'}. concatables_no_initial_call -> concatable_no_call concatables : {['$1' | ?val('$2')], ?anno('$2')}. concatables_no_initial_call -> concatable_no_call : {['$1'], ?anno('$1')}. diff --git a/src/erlfmt_scan.erl b/src/erlfmt_scan.erl index a2f3b41..810721e 100644 --- a/src/erlfmt_scan.erl +++ b/src/erlfmt_scan.erl @@ -315,7 +315,9 @@ atomic_anno([{text, Text}, {location, {Line, Col} = Location}]) -> #{text => Text, location => Location, end_location => end_location(Text, Line, Col)}. token_anno([{text, Text}, {location, {Line, Col} = Location}]) -> - #{location => Location, end_location => end_location(Text, Line, Col)}. + #{location => Location, end_location => end_location(Text, Line, Col)}; +token_anno({_Line, _Col} = Location) -> + #{location => Location, end_location => Location}. comment_anno([{text, _}, {location, Location}], [{text, Text}, {location, {Line, Col}}]) -> #{location => Location, end_location => end_location(Text, Line, Col)}. diff --git a/test/erlfmt_format_SUITE.erl b/test/erlfmt_format_SUITE.erl index 8db527f..4fd2247 100644 --- a/test/erlfmt_format_SUITE.erl +++ b/test/erlfmt_format_SUITE.erl @@ -70,7 +70,10 @@ force_break/1, binary_operator_more/1, binary_operator_equal/1, - update_edgecase/1 + update_edgecase/1, + sigils/1, + doc_attributes/1, + doc_macros/1 ]). suite() -> @@ -83,6 +86,13 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok. +init_per_group(otp_27_features, Config) -> + case erlang:system_info(otp_release) >= "27" of + true -> Config; + false -> {skip, "Skipping tests for features from OTP >= 27"} + end; +init_per_group(_, Config) -> + Config; init_per_group(_GroupName, Config) -> Config. @@ -117,7 +127,8 @@ groups() -> receive_expression, try_expression, if_expression, - macro + macro, + doc_macros ]}, {forms, [parallel], [ function, @@ -157,6 +168,10 @@ groups() -> list_comprehension, map_comprehension, binary_comprehension + ]}, + {otp_27_features, [parallel], [ + sigils, + doc_attributes ]} ]. @@ -232,6 +247,25 @@ literals(Config) when is_list(Config) -> ?assertSame("_Bar\n"), ?assertFormat("$ ", "$\\s\n"). +sigils(Config) when is_list(Config) -> + %% https://www.erlang.org/blog/highlights-otp-27/#sigils + ?assertSame("~b\"abc\\txyz\"\n"), + ?assertSame("~\"abc\\txyz\"\n"), + ?assertSame("~s{\"abc\\txyz\"}\n"), + %% 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( + "\"\"\"\n" + "Test\n" + "\"\"\"\n", + "\"Test\"\n" + ), + ?assertSame("\"\"\"\nTest\nMultiline\n\"\"\"\n"), + ?assertSame("~\"\"\"\nTest\nMultiline\n\"\"\"\n"). + dotted(Config) when is_list(Config) -> ?assertSame("<0.1.2>\n"), ?assertSame("#Ref<0.1.2.3>\n"), @@ -4234,3 +4268,15 @@ comment(Config) when is_list(Config) -> "\"a,\\n\"\n" "\"b\".\n" ). + +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"). + +doc_macros(Config) when is_list(Config) -> + %% Doc Attributes as macros is a common pattern for OTP < 27 compatibility. + ?assertSame("?MODULEDOC(\"Test\").\n?MODULEDOC(#{since => <<\"1.0.0\">>}).\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").