diff --git a/lib/style/deprecations.ex b/lib/style/deprecations.ex index ca6704bc..67bdab8a 100644 --- a/lib/style/deprecations.ex +++ b/lib/style/deprecations.ex @@ -59,6 +59,7 @@ defmodule Styler.Style.Deprecations do defp style(node), do: node + @doc "Extracts the positive or negative integer from the given range block" def extract_value_from_range({:__block__, _, [value]}), do: value def extract_value_from_range({:-, _, [{:__block__, _, [value]}]}), do: -value end diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index f09e0716..22736601 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -27,6 +27,7 @@ defmodule Styler.Style.Pipes do @behaviour Styler.Style alias Styler.Style + alias Styler.Style.Deprecations alias Styler.Zipper def run({{:|>, _, _}, _} = zipper, ctx) do @@ -151,6 +152,47 @@ defmodule Styler.Style.Pipes do defp fix_pipe({:|>, m, [lhs, {{:., dm, [{:__aliases__, am, [:Timex]}, :now]}, funm, []}]}), do: {:|>, m, [lhs, {{:., dm, [{:__aliases__, am, [:DateTime]}, :now!]}, funm, []}]} + # Path.safe_relative_to/2 => Path.safe_relative/2 + # Path.safe_relative/2 is available since v1.14 + # TODO: Remove after Elixir v1.19 + defp fix_pipe({:|>, m, [lhs, {{:., dm, [{:__aliases__, am, [:Path]}, :safe_relative_to]}, funm, args}]}), + do: {:|>, m, [lhs, {{:., dm, [{:__aliases__, am, [:Path]}, :safe_relative]}, funm, args}]} + + if Version.match?(System.version(), ">= 1.16.0-dev") do + # File.stream!(file, options, line_or_bytes) => File.stream!(file, line_or_bytes, options) + defp fix_pipe( + {:|>, m, + [ + lhs, + {{:., dm, [{:__aliases__, am, [:File]}, :stream!]}, funm, + [{:__block__, _, [modes]} = options, line_or_bytes]} + ]} + ) + when is_list(modes), + do: {:|>, m, [lhs, {{:., dm, [{:__aliases__, am, [:File]}, :stream!]}, funm, [line_or_bytes, options]}]} + + # For ranges where `start > stop`, you need to explicitly include the step + # Enum.slice(enumerable, 1..-2) => Enum.slice(enumerable, 1..-2//1) + # String.slice("elixir", 2..-1) => String.slice("elixir", 2..-1//1) + defp fix_pipe( + {:|>, m, [lhs, {{:., dm, [{:__aliases__, am, [module]}, :slice]}, funm, [{:.., _, [_, _]}] = args}]} = block + ) + when module in [:Enum, :String] do + [{:.., rm, [first, {_, lm, _} = last]}] = args + start = Deprecations.extract_value_from_range(first) + stop = Deprecations.extract_value_from_range(last) + + if start > stop do + line = Keyword.fetch!(lm, :line) + step = {:__block__, [token: "1", line: line], [1]} + range_with_step = {:"..//", rm, [first, last, step]} + {:|>, m, [lhs, {{:., dm, [{:__aliases__, am, [module]}, :slice]}, funm, [range_with_step]}]} + else + block + end + end + end + # `lhs |> Enum.reverse() |> Enum.concat(enum)` => `lhs |> Enum.reverse(enum)` defp fix_pipe( pipe_chain( diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index e0f75370..0a005354 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -689,5 +689,67 @@ defmodule Styler.Style.PipesTest do """ ) end + + test "Path.safe_relative_to/2 to Path.safe_relative/2" do + assert_style( + """ + "FOO" + |> String.downcase() + |> Path.safe_relative_to("/") + """, + """ + "FOO" + |> String.downcase() + |> Path.safe_relative("/") + """ + ) + end + + if Version.match?(System.version(), ">= 1.16.0-dev") do + test "File.stream!(path, modes, line_or_bytes) to File.stream!(path, line_or_bytes, modes)" do + assert_style( + "f |> File.stream!([encoding: :utf8, trim_bom: true], :line) |> Enum.take(2)", + "f |> File.stream!(:line, encoding: :utf8, trim_bom: true) |> Enum.take(2)" + ) + end + + test "negative steps with Enum.slice/2" do + assert_style( + "enumerable |> Enum.map(& &1) |> Enum.slice(1..-2)", + "enumerable |> Enum.map(& &1) |> Enum.slice(1..-2//1)" + ) + + assert_style( + "enumerable |> Enum.map(& &1) |> Enum.slice(-1..-2)", + "enumerable |> Enum.map(& &1) |> Enum.slice(-1..-2//1)" + ) + + assert_style( + "enumerable |> Enum.map(& &1) |> Enum.slice(2..1)", + "enumerable |> Enum.map(& &1) |> Enum.slice(2..1//1)" + ) + + assert_style( + "enumerable |> Enum.map(& &1) |> Enum.slice(1..3)", + "enumerable |> Enum.map(& &1) |> Enum.slice(1..3)" + ) + end + + test "negative steps with String.slice/2" do + assert_style( + ~s{"ELIXIR" |> String.downcase() |> String.slice(2..-1)}, + ~s{"ELIXIR" |> String.downcase() |> String.slice(2..-1//1)} + ) + + assert_style( + ~s{"ELIXIR" |> String.downcase() |> String.slice(1..-2)}, + ~s{"ELIXIR" |> String.downcase() |> String.slice(1..-2//1)} + ) + + assert_style(~s{"ELIXIR" |> String.downcase() |> String.slice(-4..-1)}) + assert_style(~s{"ELIXIR" |> String.downcase() |> String.slice(..)}) + assert_style(~s{"ELIXIR" |> String.downcase() |> String.slice(0..-1//2)}) + end + end end end