Skip to content

Commit 5902c29

Browse files
authored
Add :migrate_unless option to formatter (#13844)
1 parent fb065b7 commit 5902c29

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

lib/elixir/lib/code.ex

+5
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,11 @@ defmodule Code do
696696
`'foo'` becomes `~c"foo"`.
697697
Defaults to the value of the `:migrate` option. This option changes the AST.
698698
699+
* `:migrate_unless` (since v1.18.0) - when `true`,
700+
rewrites `unless` expressions using `if` with a negated condition, for example
701+
`unless foo, do:` becomes `if !foo, do:`.
702+
Defaults to the value of the `:migrate` option. This option changes the AST.
703+
699704
## Design principles
700705
701706
The formatter was designed under three principles.

lib/elixir/lib/code/formatter.ex

+66
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ defmodule Code.Formatter do
192192
migrate = Keyword.get(opts, :migrate, false)
193193
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
194194
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
195+
migrate_unless = Keyword.get(opts, :migrate_unless, migrate)
195196
syntax_colors = Keyword.get(opts, :syntax_colors, [])
196197

197198
sigils =
@@ -218,6 +219,7 @@ defmodule Code.Formatter do
218219
file: file,
219220
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
220221
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
222+
migrate_unless: migrate_unless,
221223
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
222224
}
223225
end
@@ -485,6 +487,27 @@ defmodule Code.Formatter do
485487
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
486488
end
487489

490+
# rewrite unless as if!
491+
defp quoted_to_algebra(
492+
{:unless, meta, [condition, block]},
493+
context,
494+
%{migrate_unless: true} = state
495+
) do
496+
quoted_to_algebra({:if, meta, [negate_condition(condition), block]}, context, state)
497+
end
498+
499+
defp quoted_to_algebra(
500+
{:|>, meta1, [condition, {:unless, meta2, [block]}]},
501+
context,
502+
%{migrate_unless: true} = state
503+
) do
504+
quoted_to_algebra(
505+
{:|>, meta1, [negate_condition(condition), {:if, meta2, [block]}]},
506+
context,
507+
state
508+
)
509+
end
510+
488511
# ..
489512
defp quoted_to_algebra({:.., _meta, []}, context, state) do
490513
if context in [:no_parens_arg, :no_parens_one_arg] do
@@ -2450,4 +2473,47 @@ defmodule Code.Formatter do
24502473
defp has_double_quote?(chunk) do
24512474
is_binary(chunk) and chunk =~ @double_quote
24522475
end
2476+
2477+
# Migration rewrites
2478+
2479+
@bool_operators [
2480+
:>,
2481+
:>=,
2482+
:<,
2483+
:<=,
2484+
:in
2485+
]
2486+
@guards [
2487+
:is_atom,
2488+
:is_boolean,
2489+
:is_nil,
2490+
:is_number,
2491+
:is_integer,
2492+
:is_float,
2493+
:is_binary,
2494+
:is_map,
2495+
:is_struct,
2496+
:is_non_struct_map,
2497+
:is_exception,
2498+
:is_list,
2499+
:is_tuple,
2500+
:is_function,
2501+
:is_reference,
2502+
:is_pid,
2503+
:is_port
2504+
]
2505+
2506+
defp negate_condition(condition) do
2507+
case condition do
2508+
{neg, _, [condition]} when neg in [:!, :not] -> condition
2509+
{:|>, _, _} -> {:|>, [], [condition, {{:., [], [Kernel, :!]}, [closing: []], []}]}
2510+
{op, _, [_, _]} when op in @bool_operators -> {:not, [], [condition]}
2511+
{guard, _, [_ | _]} when guard in @guards -> {:not, [], [condition]}
2512+
{:==, meta, [left, right]} -> {:!=, meta, [left, right]}
2513+
{:===, meta, [left, right]} -> {:!==, meta, [left, right]}
2514+
{:!=, meta, [left, right]} -> {:==, meta, [left, right]}
2515+
{:!==, meta, [left, right]} -> {:===, meta, [left, right]}
2516+
_ -> {:!, [], [condition]}
2517+
end
2518+
end
24532519
end

lib/elixir/test/elixir/code_formatter/migration_test.exs

+78
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,76 @@ defmodule Code.Formatter.MigrationTest do
201201
end
202202
end
203203

204+
describe "migrate_unless: true" do
205+
@opts [migrate_unless: true]
206+
207+
test "rewrites unless as an if with negated condition" do
208+
bad = "unless x, do: y"
209+
210+
good = "if !x, do: y"
211+
212+
assert_format bad, good, @opts
213+
214+
bad = """
215+
unless x do
216+
y
217+
else
218+
z
219+
end
220+
"""
221+
222+
good = """
223+
if !x do
224+
y
225+
else
226+
z
227+
end
228+
"""
229+
230+
assert_format bad, good, @opts
231+
end
232+
233+
test "rewrites pipelines with negated condition" do
234+
bad = "x |> unless(do: y)"
235+
236+
good = "!x |> if(do: y)"
237+
238+
assert_format bad, good, @opts
239+
240+
bad = "x |> foo() |> unless(do: y)"
241+
242+
good = "x |> foo() |> Kernel.!() |> if(do: y)"
243+
244+
assert_format bad, good, @opts
245+
end
246+
247+
test "rewrites in as not in" do
248+
assert_format "unless x in y, do: 1", "if x not in y, do: 1", @opts
249+
end
250+
251+
test "rewrites equality operators" do
252+
assert_format "unless x == y, do: 1", "if x != y, do: 1", @opts
253+
assert_format "unless x === y, do: 1", "if x !== y, do: 1", @opts
254+
assert_format "unless x != y, do: 1", "if x == y, do: 1", @opts
255+
assert_format "unless x !== y, do: 1", "if x === y, do: 1", @opts
256+
end
257+
258+
test "rewrites boolean or is_* conditions with not" do
259+
assert_format "unless x > 0, do: 1", "if not (x > 0), do: 1", @opts
260+
assert_format "unless is_atom(x), do: 1", "if not is_atom(x), do: 1", @opts
261+
end
262+
263+
test "removes ! or not in condition" do
264+
assert_format "unless not x, do: 1", "if x, do: 1", @opts
265+
assert_format "unless !x, do: 1", "if x, do: 1", @opts
266+
end
267+
268+
test "does nothing without the migrate_unless option" do
269+
assert_same "unless x, do: y"
270+
assert_same "unless x, do: y, else: z"
271+
end
272+
end
273+
204274
describe "migrate: true" do
205275
test "enables :migrate_bitstring_modifiers" do
206276
assert_format "<<foo::binary()>>", "<<foo::binary>>", migrate: true
@@ -209,5 +279,13 @@ defmodule Code.Formatter.MigrationTest do
209279
test "enables :migrate_charlists_as_sigils" do
210280
assert_format ~S['abc'], ~S[~c"abc"], migrate: true
211281
end
282+
283+
test "enables :migrate_unless" do
284+
bad = "unless x, do: y"
285+
286+
good = "if !x, do: y"
287+
288+
assert_format bad, good, migrate: true
289+
end
212290
end
213291
end

0 commit comments

Comments
 (0)