View Source Styles

Simple (Single Node) Styles

Function Performance & Readability Optimizations

Optimizing for either performance or readability, probably both! These apply to the piped versions as well

Strings to Sigils

Rewrites strings with 4 or more escaped quotes to string sigils with an alternative delimiter. The delimiter will be one of " ( { | [ ' < /, chosen by which would require the fewest escapes, and otherwise preferred in the order listed.

Before

conn
|> put_resp_content_type("application/json")
|> send_resp(403, "{\"errors\":[\"Not Authorized\"]}")
|> halt()

After

conn
|> put_resp_content_type("application/json")
|> send_resp(403, ~s({"errors":["Not Authorized"]})))
|> halt()

Large Base 10 Numbers

Style base 10 numbers with 5 or more digits to have a _ every three digits. Formatter already does this except it doesn't rewrite "typos" like 100_000_0.

If you're concerned that this breaks your team's formatting for things like "cents" (like "$100" being written as 100_00), consider using a library made for denoting currencies rather than raw elixir integers.

Before

10000
1_0_0_0_0 # Elixir's formatter is fine with this
-543213
123456789
55333.22
-123456728.0001

After

10_000
10_000
-543_213
123_456_789
55_333.22
-123_456_728.0001

Enum.into(%{}/Map/Keyword/MapSet.new) -> X.new

While these examples use %{}, the same behaviour occurs for Keyword.new(), MapSet.new() and the empty map %{}.

This is an improvement for the reader, who gets a more natural language expression: "make a new map from a" vs "take a and enumerate it into a new map"

Before

Enum.into(a, %{})
Enum.into(a, %{}, mapping_function)

After

Map.new(a)
Map.new(a, mapping_function)
  • Enum.into(%{}/Map/Keyword/MapSet.new) -> X.new

Map/Keyword.merge w/ single key literal -> X.put

Keyword.merge and Map.merge called with a literal map or keyword argument with a single key are rewritten to the equivalent put, a cognitively simpler function.

Before

foo |> Keyword.merge(%{just_one_key: the_value}) |> bar()

After

foo |> Keyword.put(:just_one_key, the_value) |> bar()

Map/Keyword.drop w/ single key -> X.delete

In the same vein as the merge style above, [Map|Keyword].drop/2 with a single key to drop are rewritten to use delete/2

Before

Map.drop(foo, [key])

After

Map.delete(foo, key)

Enum.reverse(foo) ++ bar -> Enum.reverse(foo, bar)

Enum.reverse/2 optimizes a two-step reverse and concatenation into a single step.

Before

Enum.reverse(foo) ++ bar

baz
|> Enum.reverse()
|> Enum.concat(bop)

After

Enum.reverse(foo, bar)

Enum.reverse(baz, bop)

Timex.now/0 -> DateTime.utc_now/0

Timex certainly has its uses, but knowing what stdlib date/time struct is returned by now/0 is a bit difficult! We prefer calling the actual function rather than its rename in Timex, helping the reader by being more explicit.

Before

Timex.now()

After

DateTime.utc_now()

DateModule.compare(x, y) == :lt/:gt -> DateModule.before?/after?

Again, the goal is readability and maintainability. before?/2 and after?/2 were implemented long after compare/2, so it's not unusual that a codebase needs a lot of refactoring to be brought up to date with these new functions. That's where Styler comes in!

Before

if DateTime.compare(start, end) == :gt,
  do: :error,
  else: :ok

After

if DateTime.after?(start, end),
  do: :error,
  else: :ok

Code Readability

  • put matches on right
  • Credo.Check.Readability.PreferImplicitTry

Consistency

  • def foo() -> def foo

Elixir Deprecation Rewrites

1.15+

  • Logger.warn -> Logger.warning
  • Path.safe_relative_to/2 => Path.safe_relative/2
  • Enum/String.slice/2 w/ ranges -> explicit steps
  • ~R/my_regex/ -> ~r/my_regex/
  • Date.range/2 -> Date.range/3 when decreasing range
  • IO.read/bin_read -> use :eof instead of :all

1.16+

  • File.stream!(file, options, line_or_bytes) => File.stream!(file, line_or_bytes, options)

Function Definitions

  • Shrink multi-line function defs
  • Put assignments on the right

Module Directives (use, import, alias, require, ...)

Mix Configs

Mix Config files have their config stanzas sorted. Similar to the sorting of aliases, this delivers consistency to an otherwise arbitrary world, and can even help catch bugs like configuring the same key multiple times.

A file is considered a config file if

  1. its path matches config/.*\.exs or rel/overlays/.*\.exs
  2. the file imports Mix.Config (import Mix.Config)

Once a file is detected as a mix config, its config/2,3 stanzas are grouped and ordered like so:

  • group config stanzas separated by assignments (x = y) together
  • sort each group according to erlang term sorting
  • move all existing assignments between the config stanzas to above the stanzas (without changing their ordering)

Control Flow Structures (aka "Blocks": case, if, unless, cond, with)

case

  • rewrite to if for true/false, true/_, false/true

with

with great power comes a great responsibility. don't use with when another (simpler!) "Control Flow Structure"

  • single statement with with else clauses is rewritten to case (which can be further rewritten to an if!)
  • move non <- out of the head and into preroll or body
  • fully replace with statement with normal code as
  • drop redundant identity else clause else: (error -> error) (also more complex matches, ala {:error, error} -> {:error, error})
  • Credo.Check.Refactor.RedundantWithClauseResult

cond

  • Credo.Check.Refactor.CondStatements

if/unless

if/unless often looks to see if the root of the statement is a "negator", defined as one of the following operators: :!, :not, :!=, :!==. We always try to rewrite if/unless statements to not be negated, using the inverse construct when appropriate (but we'll never write an unless with an else)

  • repeated negators (!!) are removed
  • negated if/unless without an else are inverted to unless/if (this is done recursively until 0 or 1 negations remain)
  • unless with else are inverted to negated if statements
  • negated if with else have their clauses inverted to remove the negation
  • if/unless with else: nil is dropped as redundant

Pipe Chains

Pipe Start

  • raw value
  • blocks are extracted to variables
  • ecto's from is allowed

Piped function rewrites

  • add parens to function calls |> fun |> => |> fun() |>
  • remove unnecessary then/2: |> then(&f(&1, ...)) -> |> f(...)
  • add then when defining anon funs in pipe |> (& &1).() |> => |> |> then(& &1) |>

Piped function optimizations

Two function calls into one! Tries to fit everything on one line when shrinking.

  • lhs |> Enum.reverse() |> Enum.concat(enum) => lhs |> Enum.reverse(enum) (also Kernel.++)
  • lhs |> Enum.filter(filterer) |> Enum.count() => lhs |> Enum.count(count)
  • lhs |> Enum.map(mapper) |> Enum.join(joiner) => lhs |> Enum.map_join(joiner, mapper)
  • lhs |> Enum.map(mapper) |> Enum.into(empty_map) => lhs |> Map.new(mapper)
  • lhs |> Enum.map(mapper) |> Enum.into(collectable) => lhs |> Enum.into(collectable, mapper)
  • lhs |> Enum.map(mapper) |> Map.new() => lhs |> Map.new(mapper) mapset & keyword also

Unpiping Single Pipes

  • notably, optimizations might turn a 2 pipe into a single pipe
  • doesn't unpipe when we're starting w/ quote
  • pretty straight forward i daresay