diff --git a/.formatter.exs b/.formatter.exs index b09e896884e..84685129091 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -17,6 +17,5 @@ # Float tests float_assert: 1 - ], - normalize_bitstring_modifiers: false + ] ] diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 9c875a89ebf..063a8e36abc 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -654,6 +654,8 @@ defmodule Code do ## Options + Regular options (do not change the AST): + * `:file` - the file which contains the string, used for error reporting @@ -677,28 +679,29 @@ defmodule Code do If you set it to `false` later on, `do`-`end` blocks won't be converted back to keywords. - * `:normalize_bitstring_modifiers` (since v1.14.0) - when `true`, + Migration options (change the AST), see the "Migration formatting" section below: + + * `:migrate` (since v1.18.0) - when `true`, sets all other migration options + to `true` by default. Defaults to `false`. + + * `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`, removes unnecessary parentheses in known bitstring [modifiers](`<<>>/1`), for example `<>` becomes `<>`, or adds parentheses for custom modifiers, where `<>` becomes `<>`. - Defaults to `true`. This option changes the AST. + Defaults to the value of the `:migrate` option. This option changes the AST. - * `:normalize_charlists_as_sigils` (since v1.15.0) - when `true`, + * `:migrate_charlists_as_sigils` (since v1.18.0) - when `true`, formats charlists as [`~c`](`Kernel.sigil_c/2`) sigils, for example `'foo'` becomes `~c"foo"`. - Defaults to `true`. This option changes the AST. + Defaults to the value of the `:migrate` option. This option changes the AST. ## Design principles The formatter was designed under three principles. - First, the formatter never changes the semantics of the code. + First, the formatter never changes the semantics of the code by default. This means the input AST and the output AST are almost always equivalent. - The only cases where the formatter will change the AST is when the input AST - would cause *compiler warnings* and the output AST won't. The cases where - the formatter changes the AST can be disabled through formatting options - if desired. The second principle is to provide as little configuration as possible. This eases the formatter adoption by removing contention points while @@ -986,6 +989,21 @@ defmodule Code do ## Newlines The formatter converts all newlines in code from `\r\n` to `\n`. + + ## Migration formatting + + As part of the Elixir release cycle, deprecations are being introduced, + emitting warnings which might require existing code to be changed. + In order to reduce the burden on developers when upgrading Elixir to the + next version, the formatter exposes some options, disabled by default, + in order to automate this process. + + These options should address most of the typical use cases, but given they + introduce changes to the AST, there is a non-zero risk for meta-programming + heavy projects that relied on a specific AST, or projects that are + re-defining functions from the `Kernel`. In such cases, migrations cannot + be applied blindly and some extra changes might be needed in order to + address the deprecation warnings. """ @doc since: "1.6.0" @spec format_string!(binary, keyword) :: iodata diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index a923f9bde48..1547a6c2973 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -189,8 +189,9 @@ defmodule Code.Formatter do locals_without_parens = Keyword.get(opts, :locals_without_parens, []) file = Keyword.get(opts, :file, nil) sigils = Keyword.get(opts, :sigils, []) - normalize_bitstring_modifiers = Keyword.get(opts, :normalize_bitstring_modifiers, true) - normalize_charlists_as_sigils = Keyword.get(opts, :normalize_charlists_as_sigils, true) + migrate = Keyword.get(opts, :migrate, false) + migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate) + migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate) syntax_colors = Keyword.get(opts, :syntax_colors, []) sigils = @@ -215,8 +216,8 @@ defmodule Code.Formatter do comments: comments, sigils: sigils, file: file, - normalize_bitstring_modifiers: normalize_bitstring_modifiers, - normalize_charlists_as_sigils: normalize_charlists_as_sigils, + migrate_bitstring_modifiers: migrate_bitstring_modifiers, + migrate_charlists_as_sigils: migrate_charlists_as_sigils, inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors} } end @@ -1433,7 +1434,7 @@ defmodule Code.Formatter do {doc, state} = quoted_to_algebra(segment, :parens_arg, state) {spec, state} = - bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers, :"::") + bitstring_spec_to_algebra(spec, state, state.migrate_bitstring_modifiers, :"::") spec = wrap_in_parens_if_inspected_atom(spec) spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec @@ -2431,7 +2432,7 @@ defmodule Code.Formatter do end defp get_charlist_quotes(:heredoc, state) do - if state.normalize_charlists_as_sigils do + if state.migrate_charlists_as_sigils do {@sigil_c_heredoc, @double_heredoc} else {@single_heredoc, @single_heredoc} @@ -2440,7 +2441,7 @@ defmodule Code.Formatter do defp get_charlist_quotes({:regular, chunks}, state) do cond do - !state.normalize_charlists_as_sigils -> {@single_quote, @single_quote} + !state.migrate_charlists_as_sigils -> {@single_quote, @single_quote} Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote} true -> {@sigil_c_double, @double_quote} end diff --git a/lib/elixir/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex index 6b6dfe5e1c0..cf81c378797 100644 --- a/lib/elixir/lib/code/normalizer.ex +++ b/lib/elixir/lib/code/normalizer.ex @@ -288,21 +288,18 @@ defmodule Code.Normalizer do # Lists defp normalize_literal(list, meta, state) when is_list(list) do if list != [] and List.ascii_printable?(list) do - # It's a charlist - list = + # It's a charlist, we normalize it as a ~C sigil + string = if state.escape do - {string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil) - IO.iodata_to_binary(string) |> to_charlist() + {iolist, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil) + IO.iodata_to_binary(iolist) else - list + List.to_string(list) end - meta = - meta - |> Keyword.put_new(:delimiter, "'") - |> patch_meta_line(state.parent_meta) + meta = patch_meta_line([delimiter: "\""], state.parent_meta) - {:__block__, meta, [list]} + {:sigil_c, meta, [{:<<>>, [], [string]}, []]} else meta = if line = state.parent_meta[:line] do diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 6f647f33e51..6210b0c2701 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -1133,7 +1133,9 @@ defmodule Macro do """ @spec to_string(t()) :: String.t() def to_string(tree) do - doc = Inspect.Algebra.format(Code.quoted_to_algebra(tree), 98) + doc = + Inspect.Algebra.format(Code.quoted_to_algebra(tree, migrate_charlists_as_sigils: true), 98) + IO.iodata_to_binary(doc) end diff --git a/lib/elixir/test/elixir/code_formatter/containers_test.exs b/lib/elixir/test/elixir/code_formatter/containers_test.exs index 8e7b5e430d1..940f0b4c8b9 100644 --- a/lib/elixir/test/elixir/code_formatter/containers_test.exs +++ b/lib/elixir/test/elixir/code_formatter/containers_test.exs @@ -293,33 +293,15 @@ defmodule Code.Formatter.ContainersTest do assert_same "<<(<> <- x)>>" end - test "normalizes bitstring modifiers by default" do - assert_format "<>", "<>" + test "keeps parentheses by default" do + assert_same "<>" assert_same "<>" - assert_format "<>", "<>" + assert_same "<>" assert_same "<>" - assert_format "<>", "<>" - assert_same "<>" - assert_same "<<0::size*unit, bytes::binary>>" - assert_format "<<0::size*unit, bytes::custom>>", "<<0::size*unit, bytes::custom()>>" - - assert_format "<<0, 1::2-integer() <- x>>", "<<0, 1::2-integer <- x>>" - assert_same "<<0, 1::2-integer <- x>>" - end - - test "keeps parentheses when normalize_bitstring_modifiers is false" do - opts = [normalize_bitstring_modifiers: false] - - assert_same "<>", opts - assert_same "<>", opts - - assert_same "<>", opts - assert_same "<>", opts - - assert_same "<>", opts - assert_same "<<0, 1::2-integer() <- x>>", opts + assert_same "<>" + assert_same "<<0, 1::2-integer() <- x>>" end test "is flex on line limits" do diff --git a/lib/elixir/test/elixir/code_formatter/literals_test.exs b/lib/elixir/test/elixir/code_formatter/literals_test.exs index 99b3456bde5..27546aa137d 100644 --- a/lib/elixir/test/elixir/code_formatter/literals_test.exs +++ b/lib/elixir/test/elixir/code_formatter/literals_test.exs @@ -6,7 +6,6 @@ defmodule Code.Formatter.LiteralsTest do import CodeFormatterHelpers @short_length [line_length: 10] - @keep_charlists [normalize_charlists_as_sigils: false] describe "integers" do test "in decimal base" do @@ -194,83 +193,49 @@ defmodule Code.Formatter.LiteralsTest do describe "charlists" do test "without escapes" do - assert_format ~S[''], ~S[~c""] - assert_format ~S[' '], ~S[~c" "] - assert_format ~S['foo'], ~S[~c"foo"] - - assert_same ~S[''], @keep_charlists - assert_same ~S[' '], @keep_charlists - assert_same ~S['foo'], @keep_charlists + assert_same ~S[''] + assert_same ~S[' '] + assert_same ~S['foo'] end test "with escapes" do - assert_format ~S['f\a\b\ro'], ~S[~c"f\a\b\ro"] - assert_format ~S['single \' quote'], ~S[~c"single ' quote"] - assert_format ~S['double " quote'], ~S[~c'double " quote'] - assert_format ~S['escaped \" quote'], ~S[~c'escaped \" quote'] - assert_format ~S['\\"'], ~S[~c'\\"'] - - assert_same ~S['f\a\b\ro'], @keep_charlists - assert_same ~S['single \' quote'], @keep_charlists - assert_same ~S['double " quote'], @keep_charlists - assert_same ~S['escaped \" quote'], @keep_charlists - assert_same ~S['\\"'], @keep_charlists + assert_same ~S['f\a\b\ro'] + assert_same ~S['single \' quote'] + assert_same ~S['double " quote'] + assert_same ~S['escaped \" quote'] + assert_same ~S['\\"'] end test "keeps literal new lines" do - assert_format """ - 'fo - o' - """, - """ - ~c"fo - o" - """ - assert_same """ - 'fo - o' - """, - @keep_charlists + 'fo + o' + """ end test "with interpolation" do - assert_format ~S['one #{2} three'], ~S[~c"one #{2} three"] - assert_format ~S['#{1}\n \\ " \"'], ~S[~c'#{1}\n \\ " \"'] - - assert_same ~S['one #{2} three'], @keep_charlists - assert_same ~S['#{1}\n \\ " \"'], @keep_charlists + assert_same ~S['one #{2} three'] + assert_same ~S['#{1}\n \\ " \"'] end test "with escape and interpolation" do - assert_format ~S['one\n\'#{2}\'\nthree'], ~S[~c"one\n'#{2}'\nthree"] - assert_format ~S['one\n"#{2}"\nthree'], ~S[~c'one\n"#{2}"\nthree'] - - assert_same ~S['one\n\'#{2}\'\nthree'], @keep_charlists + assert_same ~S['one\n\'#{2}\'\nthree'] end test "with interpolation on line limit" do - assert_format ~S""" - 'one #{"two"} three' - """, - ~S""" - ~c"one #{"two"} three" - """, - @short_length + assert_same ~S""" + 'one #{"two"} three' + """, + @short_length end test "literal new lines don't count towards line limit" do - assert_format ~S""" - 'one - #{"two"} - three' - """, - ~S""" - ~c"one - #{"two"} - three" - """, - @short_length + assert_same ~S""" + 'one + #{"two"} + three' + """, + @short_length end end @@ -399,149 +364,45 @@ defmodule Code.Formatter.LiteralsTest do describe "charlist heredocs" do test "without escapes" do - assert_format ~S""" - ''' - hello - ''' - """, - ~S''' - ~c""" - hello - """ - ''' - assert_same ~S""" - ''' - hello - ''' - """, - @keep_charlists + ''' + hello + ''' + """ end test "with escapes" do - assert_format ~S""" - ''' - f\a\b\ro - ''' - """, - ~S''' - ~c""" - f\a\b\ro - """ - ''' - - assert_format ~S""" - ''' - multiple "\"" quotes - ''' - """, - ~S''' - ~c""" - multiple "\"" quotes - """ - ''' - assert_same ~S""" - ''' - f\a\b\ro - ''' - """, - @keep_charlists + ''' + f\a\b\ro + ''' + """ assert_same ~S""" - ''' - multiple "\"" quotes - ''' - """, - @keep_charlists + ''' + multiple "\"" quotes + ''' + """ end test "with interpolation" do - assert_format ~S""" - ''' - one - #{2} - three - ''' - """, - ~S''' - ~c""" - one - #{2} - three - """ - ''' - - assert_format ~S""" - ''' - one - " - #{2} - " - three - ''' - """, - ~S''' - ~c""" - one - " - #{2} - " - three - """ - ''' - assert_same ~S""" - ''' - one - #{2} - three - ''' - """, - @keep_charlists + ''' + one + #{2} + three + ''' + """ assert_same ~S""" - ''' - one - " - #{2} - " - three - ''' - """, - @keep_charlists - end - - test "with interpolation on line limit" do - assert_format ~S""" - ''' - one #{"two two"} three - ''' - """, - ~S''' - ~c""" - one #{"two two"} three - """ - ''', - @short_length - end - - test "literal new lines don't count towards line limit" do - assert_format ~S""" - ''' - one - #{"two"} - three - ''' - """, - ~S''' - ~c""" - one - #{"two"} - three - """ - ''', - @short_length + ''' + one + " + #{2} + " + three + ''' + """ end end end diff --git a/lib/elixir/test/elixir/code_formatter/migration_test.exs b/lib/elixir/test/elixir/code_formatter/migration_test.exs new file mode 100644 index 00000000000..96c2621a5af --- /dev/null +++ b/lib/elixir/test/elixir/code_formatter/migration_test.exs @@ -0,0 +1,213 @@ +Code.require_file("../test_helper.exs", __DIR__) + +defmodule Code.Formatter.MigrationTest do + use ExUnit.Case, async: true + + import CodeFormatterHelpers + + @short_length [line_length: 10] + + describe "migrate_bitstring_modifiers: true" do + @opts [migrate_bitstring_modifiers: true] + + test "normalizes bitstring modifiers" do + assert_format "<>", "<>", @opts + assert_same "<>", @opts + + assert_format "<>", "<>", @opts + assert_same "<>", @opts + + assert_format "<>", "<>", @opts + assert_same "<>", @opts + assert_same "<<0::size*unit, bytes::binary>>", @opts + assert_format "<<0::size*unit, bytes::custom>>", "<<0::size*unit, bytes::custom()>>", @opts + + assert_format "<<0, 1::2-integer() <- x>>", "<<0, 1::2-integer <- x>>", @opts + assert_same "<<0, 1::2-integer <- x>>", @opts + end + end + + describe "migrate_charlists_as_sigils: true" do + @opts [migrate_charlists_as_sigils: true] + + test "without escapes" do + assert_format ~S[''], ~S[~c""], @opts + assert_format ~S[' '], ~S[~c" "], @opts + assert_format ~S['foo'], ~S[~c"foo"], @opts + end + + test "with escapes" do + assert_format ~S['f\a\b\ro'], ~S[~c"f\a\b\ro"], @opts + assert_format ~S['single \' quote'], ~S[~c"single ' quote"], @opts + assert_format ~S['double " quote'], ~S[~c'double " quote'], @opts + assert_format ~S['escaped \" quote'], ~S[~c'escaped \" quote'], @opts + assert_format ~S['\\"'], ~S[~c'\\"'], @opts + end + + test "keeps literal new lines" do + assert_format """ + 'fo + o' + """, + """ + ~c"fo + o" + """, + @opts + end + + test "with interpolation" do + assert_format ~S['one #{2} three'], ~S[~c"one #{2} three"], @opts + assert_format ~S['#{1}\n \\ " \"'], ~S[~c'#{1}\n \\ " \"'], @opts + end + + test "with escape and interpolation" do + assert_format ~S['one\n\'#{2}\'\nthree'], ~S[~c"one\n'#{2}'\nthree"], @opts + assert_format ~S['one\n"#{2}"\nthree'], ~S[~c'one\n"#{2}"\nthree'], @opts + end + + test "with interpolation on line limit" do + assert_format ~S""" + 'one #{"two"} three' + """, + ~S""" + ~c"one #{"two"} three" + """, + @short_length ++ @opts + end + + test "literal new lines don't count towards line limit" do + assert_format ~S""" + 'one + #{"two"} + three' + """, + ~S""" + ~c"one + #{"two"} + three" + """, + @short_length ++ @opts + end + + test "heredocs without escapes" do + assert_format ~S""" + ''' + hello + ''' + """, + ~S''' + ~c""" + hello + """ + ''', + @opts + end + + test "heredocs with escapes" do + assert_format ~S""" + ''' + f\a\b\ro + ''' + """, + ~S''' + ~c""" + f\a\b\ro + """ + ''', + @opts + + assert_format ~S""" + ''' + multiple "\"" quotes + ''' + """, + ~S''' + ~c""" + multiple "\"" quotes + """ + ''', + @opts + end + + test "heredocs with interpolation" do + assert_format ~S""" + ''' + one + #{2} + three + ''' + """, + ~S''' + ~c""" + one + #{2} + three + """ + ''', + @opts + + assert_format ~S""" + ''' + one + " + #{2} + " + three + ''' + """, + ~S''' + ~c""" + one + " + #{2} + " + three + """ + ''', + @opts + end + + test "heredocs with interpolation on line limit" do + assert_format ~S""" + ''' + one #{"two two"} three + ''' + """, + ~S''' + ~c""" + one #{"two two"} three + """ + ''', + @short_length ++ @opts + end + + test "heredocs literal new lines don't count towards line limit" do + assert_format ~S""" + ''' + one + #{"two"} + three + ''' + """, + ~S''' + ~c""" + one + #{"two"} + three + """ + ''', + @short_length ++ @opts + end + end + + describe "migrate: true" do + test "enables :migrate_bitstring_modifiers" do + assert_format "<>", "<>", migrate: true + end + + test "enables :migrate_charlists_as_sigils" do + assert_format ~S['abc'], ~S[~c"abc"], migrate: true + end + end +end diff --git a/lib/elixir/test/elixir/code_normalizer/formatted_ast_test.exs b/lib/elixir/test/elixir/code_normalizer/formatted_ast_test.exs index 2d4983c225f..f4167ef8379 100644 --- a/lib/elixir/test/elixir/code_normalizer/formatted_ast_test.exs +++ b/lib/elixir/test/elixir/code_normalizer/formatted_ast_test.exs @@ -173,29 +173,30 @@ defmodule Code.Normalizer.FormatterASTTest do describe "charlists" do test "without escapes" do - assert_same ~S[''] - assert_same ~S[' '] - assert_same ~S['foo'] + assert_same ~S[~c""] + assert_same ~S[~c" "] + assert_same ~S[~c"foo"] end test "with escapes" do - assert_same ~S['f\a\b\ro'] - assert_same ~S['single \' quote'] + assert_same ~S[~c"f\a\b\ro"] + assert_same ~S[~c'single \' quote'] + assert_same ~S[~c"double \" quote"] end test "keeps literal new lines" do assert_same """ - 'fo - o' + ~c"fo + o" """ end test "with interpolation" do - assert_same ~S['one #{2} three'] + assert_same ~S[~c"one #{2} three"] end test "with escape and interpolation" do - assert_same ~S['one\n\'#{2}\'\nthree'] + assert_same ~S[~c'one\n\'#{2}\'\nthree'] end end @@ -297,7 +298,7 @@ defmodule Code.Normalizer.FormatterASTTest do describe "charlist heredocs" do test "without escapes" do assert_same ~S""" - ''' + ~c''' hello ''' """ @@ -305,13 +306,13 @@ defmodule Code.Normalizer.FormatterASTTest do test "with escapes" do assert_same ~S""" - ''' + ~c''' f\a\b\ro ''' """ assert_same ~S""" - ''' + ~c''' multiple "\"" quotes ''' """ @@ -319,7 +320,7 @@ defmodule Code.Normalizer.FormatterASTTest do test "with interpolation" do assert_same ~S""" - ''' + ~c''' one #{2} three @@ -327,7 +328,7 @@ defmodule Code.Normalizer.FormatterASTTest do """ assert_same ~S""" - ''' + ~c''' one " #{2} diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index 9302e5df709..4ae0bf80d71 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -720,6 +720,10 @@ defmodule MacroTest do assert Macro.to_string(-576_460_752_303_423_455) == "-576_460_752_303_423_455" end + test "charlists" do + assert Macro.to_string(~c"foo") == ~s(~c"foo") + end + defmodule HTML do defstruct [:string]