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
- its path matches
config/.*\.exs
orrel/overlays/.*\.exs
- 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
fortrue/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
withelse
clauses is rewritten tocase
(which can be further rewritten to anif
!) - 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
withelse
are inverted to negatedif
statements- negated
if
withelse
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