Repository: adobe/elixir-styler
Pull request #1 (2023-03-31 01:16:08)

Add number rewrites in "Simple" style
implements a rule replacing `Credo.Check.Readability.LargeNumbers`


Patch:
diff --git a/README.md b/README.md
index be37d04..b328688 100644
--- a/README.md
+++ b/README.md
@@ -50,19 +50,22 @@ You can find the currently-enabled styles in the `Mix.Tasks.Style` module, insid
 
 ## Credo Rules Styler Replaces
 
-| credo rule                            | style that rewrites to suit          |
+| credo rule                            | Styler Style                         |
 |---------------------------------------|--------------------------------------|
 | `Credo.Check.Readability.AliasOrder`  | `Styler.Style.Aliases`               |
+| `Credo.Check.Readability.LargeNumbers`| `Styler.Style.Simple`                |
 | `Credo.Check.Readability.MultiAlias`  | `Styler.Style.Aliases`               |
-| `Credo.Check.Readability.UnnecessaryAliasExpansion` | `Styler.Style.Aliases` |
 | `Credo.Check.Readability.SinglePipe`  | `Styler.Style.Pipes`                 |
+| `Credo.Check.Readability.UnnecessaryAliasExpansion` | `Styler.Style.Aliases` |
 | `Credo.Check.Refactor.PipeChainStart` | `Styler.Style.Pipes`                 |
 
 ## Writing Styles
 
 Write a new Style by implementing the `Styler.Style` behaviour. See its moduledoc for more.
 
-## Where is Sourceror?
+## Thanks & Inspiration
+
+### Sourceror
 
 This work was inspired by earlier large-scale rewrites of an internal codebase that used the fantastic tool [`Sourceror`](https://github.com/doorgan/sourceror/).
 
@@ -71,4 +74,11 @@ the point that it's no longer an appropriate drop-in for `mix format`.
 
 Still, we're grateful for the inspiration Sourceror provided and the changes to the Elixir AST APIs that it drove.
 
-The AST-Zipper implementation in this project was derived mostly from Sourceror's implementation.
+The AST-Zipper implementation in this project was forked from Sourceror's implementation.
+
+### Credo
+
+Similarly, this project originated from one-off scripts doing large scale rewrites of an enormous codebase as part of an
+effort to enable particular Credo rules for that codebase. Credo's tests and implementations were referenced for implementing
+Styles that took the work the rest of the way. Thanks to Credo & the Elixir community at large for coalescing around
+many of these Elixir style credos.
diff --git a/lib/style/simple.ex b/lib/style/simple.ex
new file mode 100644
index 0000000..d44c84a
--- /dev/null
+++ b/lib/style/simple.ex
@@ -0,0 +1,62 @@
+# Copyright 2023 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.Simple do
+  @moduledoc """
+  Simple 1-1 rewrites all crammed into one module to make for more efficient traversals
+
+  Credo Rules addressed:
+
+  * Credo.Check.Readability.LargeNumbers
+      Formatter handles large number (>5 digits) rewrites, but doesn't rewrite typos like `100_000_0`, so it's worthwhile to have styler do this
+  """
+
+  @behaviour Styler.Style
+
+  alias Styler.Zipper
+
+  # `?-` isn't part of the number node - it's its parent - so all numbers are positive at this point
+  def run({{:__block__, meta, [number]}, _} = zipper) when is_number(number) and number >= 10_000 do
+    # Checking here rather than in the anon function due to compiler bug https://github.com/elixir-lang/elixir/issues/10485
+    integer? = is_integer(number)
+
+    meta =
+      Keyword.update!(meta, :token, fn
+        "0x" <> _ = token ->
+          token
+
+        "0b" <> _ = token ->
+          token
+
+        "0o" <> _ = token ->
+          token
+
+        token when integer? ->
+          delimit(token)
+        # is float
+        token ->
+          [int_token, decimals] = String.split(token, ".")
+          "#{delimit(int_token)}.#{decimals}"
+      end)
+
+    Zipper.replace(zipper, {:__block__, meta, [number]})
+  end
+
+  def run(zipper), do: zipper
+
+  defp delimit(token), do: token |> String.to_charlist() |> remove_underscores([]) |> add_underscores([])
+
+  defp remove_underscores([?_ | rest], acc), do: remove_underscores(rest, acc)
+  defp remove_underscores([digit | rest], acc), do: remove_underscores(rest, [digit | acc])
+  defp remove_underscores([], reversed_list), do: reversed_list
+
+  defp add_underscores([a, b, c, d | rest], acc), do: add_underscores([d | rest], [?_, c, b, a | acc])
+  defp add_underscores(reversed_list, acc), do: reversed_list |> Enum.reverse(acc) |> to_string()
+end


Test patch:
diff --git a/test/style/simple_test.exs b/test/style/simple_test.exs
new file mode 100644
index 0000000..2c7c6ab
--- /dev/null
+++ b/test/style/simple_test.exs
@@ -0,0 +1,48 @@
+# Copyright 2023 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.SimpleTest do
+  use Styler.StyleCase, style: Styler.Style.Simple, async: true
+
+  describe "run/1" do
+    test "styles floats and integers with >4 digits" do
+      assert_style(
+        """
+        10000
+        1_0_0_0_0
+        -543213
+        123456789
+        55333.22
+        -123456728.0001
+        """,
+        """
+        10_000
+        10_000
+        -543_213
+        123_456_789
+        55_333.22
+        -123_456_728.0001
+        """
+      )
+    end
+
+    test "stays away from small numbers, strings and science" do
+      assert_style("""
+      1234
+      9999
+      "10000"
+      0xFFFF
+      0x123456
+      0b1111_1111_1111_1111
+      0o777_7777
+      """)
+    end
+  end
+end