Repository: adobe/elixir-styler
Pull request #118 (2024-01-12 23:23:22)

transform some deprecations added after v1.16
- [x] Please [sign Adobe's CLA](http://opensource.adobe.com/cla.html) if this is your first time contributing to an Adobe open source repo. Thanks!



Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2b462ea..a829961 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,7 @@ jobs:
     name: Ex${{matrix.elixir}}/OTP${{matrix.otp}}
     strategy:
       matrix:
-        elixir: ['1.14.2', '1.15.7']
+        elixir: ['1.14.2', '1.15.7', '1.16.0']
         otp: ['25.1.2']
     steps:
       - uses: actions/checkout@v3
diff --git a/.tool-versions b/.tool-versions
index ff52406..16f4970 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,2 +1,2 @@
 erlang 26.1.2
-elixir 1.15.7-otp-26
+elixir 1.16.0-otp-26
diff --git a/lib/style/deprecations.ex b/lib/style/deprecations.ex
new file mode 100644
index 0000000..67bdab8
--- /dev/null
+++ b/lib/style/deprecations.ex
@@ -0,0 +1,65 @@
+# Copyright 2024 Adobe. All rights reserved.
+# This file is licensed to you under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License. You may obtain a copy
+# of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+# OF ANY KIND, either express or implied. See the License for the specific language
+# governing permissions and limitations under the License.
+
+defmodule Styler.Style.Deprecations do
+  @moduledoc """
+  Transformations to soft or hard deprecations introduced on newer Elixir releases
+  """
+
+  @behaviour Styler.Style
+
+  def run({node, meta}, ctx), do: {:cont, {style(node), meta}, ctx}
+
+  # Logger.warn => Logger.warning
+  # Started to emit warning after Elixir 1.15.0
+  defp style({{:., dm, [{:__aliases__, am, [:Logger]}, :warn]}, funm, args}),
+    do: {{:., dm, [{:__aliases__, am, [:Logger]}, :warning]}, funm, args}
+
+  # 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 style({{:., dm, [{:__aliases__, am, [:Path]}, :safe_relative_to]}, funm, args}),
+    do: {{:., 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 style(
+           {{:., dm, [{:__aliases__, am, [:File]}, :stream!]}, funm,
+            [path, {:__block__, _, [modes]} = options, line_or_bytes]}
+         )
+         when is_list(modes),
+         do: {{:., dm, [{:__aliases__, am, [:File]}, :stream!]}, funm, [path, 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 style({{:., dm, [{:__aliases__, am, [module]}, :slice]}, funm, [_, {:.., _, [_, _]}] = args} = block)
+         when module in [:Enum, :String] do
+      [enumerable, {:.., rm, [first, {_, lm, _} = last]}] = args
+      start = extract_value_from_range(first)
+      stop = 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]}
+        {{:., dm, [{:__aliases__, am, [module]}, :slice]}, funm, [enumerable, range_with_step]}
+      else
+        block
+      end
+    end
+  end
+
+  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 f09e071..2273660 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/lib/style/single_node.ex b/lib/style/single_node.ex
index 3071b4b..0fc51e3 100644
--- a/lib/style/single_node.ex
+++ b/lib/style/single_node.ex
@@ -92,10 +92,6 @@ defmodule Styler.Style.SingleNode do
       do: {{:., dm, [module, :put]}, m, [lhs, key, value]}
   end
 
-  # Logger.warn => Logger.warning
-  defp style({{:., dm, [{:__aliases__, am, [:Logger]}, :warn]}, funm, args}),
-    do: {{:., dm, [{:__aliases__, am, [:Logger]}, :warning]}, funm, args}
-
   # Timex.now() => DateTime.utc_now()
   defp style({{:., dm, [{:__aliases__, am, [:Timex]}, :now]}, funm, []}),
     do: {{:., dm, [{:__aliases__, am, [:DateTime]}, :utc_now]}, funm, []}
diff --git a/lib/styler.ex b/lib/styler.ex
index 1ae10dd..7c894a0 100644
--- a/lib/styler.ex
+++ b/lib/styler.ex
@@ -22,7 +22,8 @@ defmodule Styler do
     Styler.Style.Pipes,
     Styler.Style.SingleNode,
     Styler.Style.Defs,
-    Styler.Style.Blocks
+    Styler.Style.Blocks,
+    Styler.Style.Deprecations
   ]
 
   @doc false


Test patch:
diff --git a/test/style/deprecations_test.exs b/test/style/deprecations_test.exs
new file mode 100644
index 0000000..e2041f9
--- /dev/null
+++ b/test/style/deprecations_test.exs
@@ -0,0 +1,46 @@
+# Copyright 2024 Adobe. All rights reserved.
+# This file is licensed to you under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License. You may obtain a copy
+# of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+# OF ANY KIND, either express or implied. See the License for the specific language
+# governing permissions and limitations under the License.
+
+defmodule Styler.Style.DeprecationsTest do
+  use Styler.StyleCase, async: true
+
+  test "Logger.warn to Logger.warning" do
+    assert_style("Logger.warn(foo)", "Logger.warning(foo)")
+    assert_style("Logger.warn(foo, bar)", "Logger.warning(foo, bar)")
+  end
+
+  test "Path.safe_relative_to/2 to Path.safe_relative/2" do
+    assert_style("Path.safe_relative_to(foo, bar)", "Path.safe_relative(foo, bar)")
+  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(
+        "File.stream!(path, [encoding: :utf8, trim_bom: true], :line)",
+        "File.stream!(path, :line, encoding: :utf8, trim_bom: true)"
+      )
+    end
+
+    test "negative steps with Enum.slice/2" do
+      assert_style("Enum.slice([1, 2, 3, 4], 1..-2)", "Enum.slice([1, 2, 3, 4], 1..-2//1)")
+      assert_style("Enum.slice([1, 2, 3, 4], -1..-2)", "Enum.slice([1, 2, 3, 4], -1..-2//1)")
+      assert_style("Enum.slice([1, 2, 3, 4], 2..1)", "Enum.slice([1, 2, 3, 4], 2..1//1)")
+      assert_style("Enum.slice([1, 2, 3, 4, 5], 1..3)", "Enum.slice([1, 2, 3, 4, 5], 1..3)")
+    end
+
+    test "negative steps with String.slice/2" do
+      assert_style(~s|String.slice("elixir", 2..-1)|, ~s|String.slice("elixir", 2..-1//1)|)
+      assert_style(~s|String.slice("elixir", 1..-2)|, ~s|String.slice("elixir", 1..-2//1)|)
+      assert_style(~s|String.slice("elixir", -4..-1)|)
+      assert_style(~s|String.slice("elixir", ..)|)
+      assert_style(~s|String.slice("elixir", 0..-1//2)|)
+    end
+  end
+end
diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs
index e0f7537..0a00535 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
diff --git a/test/style/single_node_test.exs b/test/style/single_node_test.exs
index fdc970d..2e7a67c 100644
--- a/test/style/single_node_test.exs
+++ b/test/style/single_node_test.exs
@@ -30,11 +30,6 @@ defmodule Styler.Style.SingleNodeTest do
     end
   end
 
-  test "Logger.warn to Logger.warning" do
-    assert_style("Logger.warn(foo)", "Logger.warning(foo)")
-    assert_style("Logger.warn(foo, bar)", "Logger.warning(foo, bar)")
-  end
-
   describe "Timex.now/0,1" do
     test "Timex.now/0 => DateTime.utc_now/0" do
       assert_style("Timex.now()", "DateTime.utc_now()")