lib/surgex/guide/code_style.ex
defmodule Surgex.Guide.CodeStyle do
@moduledoc """
Basic code style and formatting guidelines.
"""
@doc """
Indentation must be done with 2 spaces.
## Reasoning
This is [kind of a delicate subject](https://youtu.be/SsoOG6ZeyUI), but seemingly both Elixir and
Ruby communities usually go for spaces, so it's best to stay aligned.
When it comes to linting, the use of specific number of spaces works well with the line length
rule, while tabs can be expanded to arbitrary number of soft spaces in editor, possibly ruining
all the hard work put into staying in line with the column limit.
As to the number of spaces, 2 seems to be optimal to allow unconstrained module, function and
block indentation without sacrificing too many columns.
## Examples
Preferred:
defmodule User do
def blocked?(user) do
!user.confirmed || user.blocked
end
end
Too deep indentation (and usual outcome of using tabs):
defmodule User do
def blocked?(user) do
!user.confirmed || user.blocked
end
end
Missing single space:
defmodule User do
def blocked?(user) do
!user.confirmed || user.blocked
end
end
"""
def indentation, do: nil
@doc """
Lines must not be longer than 100 characters.
## Reasoning
The old-school 70 or 80 column limits seem way limiting for Elixir which is highly based on
indenting blocks. Considering modern screen resolutions, 100 columns should work well for anyone
with something more modern than CGA video card.
Also, 100 column limit plays well with GitHub, CodeClimate, HexDocs and others.
## Examples
Preferred:
defmodule MyProject.Accounts.User do
def build(%{
"first_name" => first_name,
"last_name" => last_name,
"email" => email,
"phone_number" => phone_number
}) do
%__MODULE__{
first_name: first_name,
last_name: last_name,
email: email,
phone_number: phone_number
}
end
end
Missing line breaks before limit:
defmodule MyProject.Accounts.User do
def build(%{"first_name" => first_name, "last_name" => last_name, "email" => email, "phone_number" => phone_number}) do
%__MODULE__{first_name: first_name, last_name: last_name, email: email, phone_number: phone_number}
end
end
"""
def line_length, do: nil
@doc """
Lines must not end with trailing white-space.
## Reasoning
Leaving white-space at the end of lines is a bad programming habit that leads to crazy diffs in
version control once developers that do it get mixed with those that don't.
Most editors can be tuned to automatically trim trailing white-space on save.
## Examples
Preferred:
func()
Hidden white-space (simulated by adding comment at the end of line):
func() # line end
"""
def trailing_whitespace, do: nil
@doc """
Files must end with single line break.
## Reasoning
Many editors and version control systems consider files without final line break invalid. In git,
such last line gets highlighted with an alarming red. Like with trailing white-space, it's a bad
habit to leave such artifacts and ruin diffs for developers who save files correctly.
Reversely, leaving too many line breaks is just sloppy.
Most editors can be tuned to automatically add single trailing line break on save.
## Examples
Preferred:
func()⮐
Missing line break:
func()
Too many line breaks:
func()⮐
⮐
"""
def trailing_newline, do: nil
@doc """
Single space must be put around operators.
## Reasoning
It's a matter of keeping variable names readable and distinct in operator-intensive situations.
There should be no technical problem with such formatting even in long lines, since those can be
easily broken into multiple, properly indented lines.
## Examples
Preferred:
(a + b) / c
Hard to read:
(a+b)/c
"""
def operator_spacing, do: nil
@doc """
Single space must be put after commas.
## Reasoning
It's a convention that passes through many languages - it looks good and so there's no reason to
make an exception for Elixir on this one.
## Examples
Preferred:
fn(arg, %{first: first, second: second}), do: nil
Three creative ways to achieve pure ugliness by omitting comma between arguments, map keys or
before inline `do`:
fn(arg,%{first: first,second: second}),do: nil
"""
def comma_spacing, do: nil
@doc """
There must be no space put before `}`, `]` or `)` and after `{`, `[` or `(` brackets.
## Reasoning
It's often tempting to add inner padding for tuples, maps, lists or function arguments to give
those constructs more space to breathe, but these structures are distinct enough to be readable
without it. They may actually be more readable without the padding, because this rule plays well
with other spacing rules (like comma spacing or operator spacing), making expressions that combine
brackets and operators have a distinct, nicely parse-able "rhythm".
Also, when allowed to pad brackets, developers tend to add such padding inconsistently - even
between opening and ending in single line. This gets even worse once a different developer
modifies such code and has a different approach towards bracket spacing.
Lastly, it keeps pattern matchings more compact and readable, which invites developers to utilize
this wonderful Elixir feature to the fullest.
## Examples
Preferred:
def func(%{first: second}, [head | tail]), do: nil
Everything padded and unreadable (no "rhythm"):
def func( %{ first: second }, [ head | tail ] ), do: nil
Inconsistencies:
def func( %{first: second}, [head | tail]), do: nil
"""
def bracket_spacing, do: nil
@doc """
There must be no space put after the `!` operator.
## Reasoning
Like with brackets, it may be tempting to pad negation to make it more visible, but in general
unary operators tend to be easier to parse when they live close to their argument. Why? Because
they usually have precedence over binary operators and padding them away from their argument makes
this precedence less apparent.
## Examples
Preferred:
!blocked && allowed
Operator precedence mixed up:
! blocked && allowed
"""
def negation_spacing, do: nil
@doc """
`;` must not be used to separate statements and expressions.
## Reasoning
This is the most classical case when it comes to preference of vertical over horizontal alignment.
Let's just keep `;` operator for `iex` sessions and focus on code readability over doing code
minification manually - neither EVM nor GitHub will explode over that additional line break.
> Actually, ", " costs one more byte than an Unix line break but if that would be our biggest
> concern then I suppose we wouldn't prefer spaces over tabs for indentation...
## Examples
Preferred:
func()
other_func()
`iex` session saved to file by mistake:
func(); other_func()
"""
def semicolon_usage, do: nil
@doc """
Indentation blocks must never start or end with blank lines.
## Reasoning
There's no point in adding additional vertical spacing since we already have horizontal padding
increase/decrease on block start/end.
## Examples
Preferred:
def parent do
nil
end
Wasted line:
def parent do
nil
end
"""
def block_inner_spacing, do: nil
@doc """
Indentation blocks should be padded from surrounding code with single blank line.
## Reasoning
There are probably as many approaches to inserting blank lines between regular code as there are
developers, but the common aim usually is to break the heaviest parts into separate "blocks". This
rule tries to highlight one most obvious candidate for such "block" which is... an actual block.
Since blocks are indented on the inside, there's no point in padding them there, but the outer
parts of the block (the line where `do` appears and the line where `end` appears) often include a
key to a reasoning about the whole block and are often the most important parts of the whole
parent scope, so it may be beneficial to make that part distinct.
In case of Elixir it's even more important, since block openings often include non-trivial
destructuring, pattern matching, wrapping things in tuples etc.
## Examples
Preferred (there's blank line before the `Enum.map` block since there's code (`array = [1, 2, 3]`)
in parent block, but there's no blank line after that block since there's no more code after it):
def parent do
array = [1, 2, 3]
Enum.map(array, fn number ->
number + 1
end)
end
Obfuscated block:
def parent do
array = [1, 2, 3]
big_numbers = Enum.map(array, fn number ->
number + 1
end)
big_numbers ++ [5, 6, 7]
end
"""
def block_outer_spacing, do: nil
@doc """
Vertical blocks should be preferred over horizontal blocks.
## Reasoning
There's often more than one way to achieve the same and the difference is in fitting things
horizontally through indentation vs vertically through function composition. This rule is about
preference of the latter over the former in order to avoid crazy indentation, have more smaller
functions, which makes for a code easier to understand and extend.
## Examples
Too much crazy indentation to fit everything in one function:
defp map_array(array) do
array
|> Enum.uniq
|> Enum.map(fn array_item ->
if is_binary(array_item) do
array_item <> " (changed)"
else
array_item + 1
end
end)
end
Preferred refactor of the above:
defp map_array(array) do
array
|> Enum.uniq
|> Enum.map(&map_array_item/1)
end
defp map_array_item(array_item) when is_binary(array_item), do: array_item <> " (changed)"
defp map_array_item(array_item), do: array_item + 1
"""
def block_alignment, do: nil
@doc """
Inline blocks should be preferred for simple code that fits one line.
## Reasoning
In case of simple and small functions, conditions etc, the inline variant of block allows to keep
code more compact and fit biggest piece of the story on the screen without losing readability.
## Examples
Preferred:
def add_two(number), do: number + 2
Wasted vertical space:
def add_two(number) do
number + 2
end
Too long (or too complex) to be inlined:
def add_two_and_multiply_by_the_meaning_of_life_and_more(number),
do: (number + 2) * 42 * get_more_for_this_truly_crazy_computation(number)
"""
def inline_block_usage, do: nil
@doc """
Multi-line calculations should be indented by one level for assignment.
## Reasoning
Horizontal alignment is something especially tempting in Elixir programming as there are many
operators and structures that look cool when it gets applied. In particular, pipe chains only look
good when the pipe "comes out" from the initial value. In order to achieve that in assignment,
vertical alignment is often overused.
The issue is with future-proofness of such alignment. For instance, it may easily get ruined
without developer's attention in typical find-and-replace sessions that touch the name on the left
side of `=` sign.
Hence this rule, which is about inserting a new line after the `=` and indenting the right side
calculation by one level.
## Examples
Preferred:
user =
User
|> build_query()
|> apply_scoping()
|> Repo.one()
Cool yet not so future-proof:
user = User
|> build_query()
|> apply_scoping()
|> Repo.one()
Find-and-replace session result on the above:
authorized_user = User
|> build_query()
|> apply_scoping()
|> Repo.one()
"""
def assignment_indentation, do: nil
@doc """
Keywords in Ecto queries should be indented by one level (and one more for `on` after `join`).
## Reasoning
Horizontal alignment is something especially tempting in Elixir programming as there are many
operators and structures that look cool when it gets applied. In particular, Ecto queries are
often written (and they do look good) when aligned to `:` after `from` macro keywords. In order to
achieve that, vertical alignment is often overused.
The issue is with future-proofness of such alignment. For instance, it'll get ruined when longer
keyword will have to be added, such as `preload` or `select` in queries with only `join` or
`where`.
It's totally possible to adhere to the 2 space indentation rule and yet to write a good looking
and readable Ecto query. In order to make things more readable, additional 2 spaces can be added
for contextual indentation of sub-keywords, like `on` after `join`.
## Examples
Preferred:
from users in User,
join: credit_cards in assoc(users, :credit_card),
on: is_nil(credit_cards.deleted_at),
where: is_nil(users.deleted_at),
select: users.id,
preload: [:credit_card],
Cool yet not so future-proof:
from users in User,
join: credit_cards in assoc(users, :credit_card),
on: is_nil(credit_cards.deleted_at),
where: is_nil(users.deleted_at)
"""
def ecto_query_indentation, do: nil
@doc """
Pipe chains must be used only for multiple function calls.
## Reasoning
The whole point of pipe chain is that... well, it must be a *chain*. As such, single function call
does not qualify. Reversely, nesting multiple calls instead of piping them seriously limits the
readability of the code.
## Examples
Preferred for 2 and more function calls:
arg
|> func()
|> other_func()
Preferred for 1 function call:
yet_another_func(a, b)
Not preferred:
other_func(func(arg))
a |> yet_another_func(b)
"""
def pipe_chain_usage, do: nil
@doc """
Pipe chains must be started with a plain value.
## Reasoning
The whole point of pipe chain is to push some value through the chain, end to end. In order to do
that consistently, it's best to keep away from starting chains with function calls.
This also makes it easier to see if pipe operator should be used at all - since chain with 2 pipes
may get reduced to just 1 pipe when inproperly started with function call, it may falsely look
like a case when pipe should not be used at all.
## Examples
Preferred:
arg
|> func()
|> other_func()
Chain that lost its reason to live:
func(arg)
|> other_func()
"""
def pipe_chain_start, do: nil
@doc """
Large numbers must be padded with underscores.
## Reasoning
They're just more readable that way. It's one of those cases when a minimal effort can lead to
eternal gratitude from other committers.
## Examples
Preferred:
x = 50_000_000
"How many zeros is that" puzzle (hint: not as many as in previous example):
x = 5000000
"""
def number_padding, do: nil
@doc """
Functions should be called with parentheses.
## Reasoning
There's a convention in Elixir universe to make function calls distinct from macro calls by
consistently covering them with parentheses. Function calls often take part in multiple operations
in a single line or inside pipes and as such, it's just safer to mark the precedence via
parentheses.
## Examples
Preferred:
first() && second(arg)
Unreadable and with compiler warning coming up:
first && second arg
"""
def function_call_parentheses, do: nil
@doc """
Macros should be called without parentheses.
## Reasoning
There's a convention in Elixir universe to make function calls distinct from macro calls by
consistently covering them with parentheses. Compared to functions, macros are often used as a
DSL, with one macro invocation per line. As such, they can be safely written (and just look
better) without parentheses.
## Examples
Preferred:
if bool, do: nil
from t in table, select: t.id
Macro call that looks like a function call:
from(t in table, select: t.id)
"""
def macro_call_parentheses, do: nil
@doc """
Single blank line must be inserted after `@moduledoc`.
## Reasoning
`@moduledoc` is a module-wide introduction to the module. It makes sense to give it padding and
separate it from what's coming next. The reverse looks especially bad when followed by a function
that has no `@doc` clause yet.
## Examples
Preferred:
defmodule SuperMod do
@moduledoc \"""
This module is seriously amazing.
\"""
def call, do: nil
end
`@moduledoc` that pretends to be a `@doc`:
defmodule SuperMod do
@moduledoc \"""
This module is seriously amazing.
\"""
def call, do: nil
end
"""
def moduledoc_spacing, do: nil
@doc """
There must be no blank lines between `@doc` and the function definition.
## Reasoning
Compared to moduledoc spacing, the `@doc` clause belongs to the function
definition directly beneath it, so the lack of blank line between the two is there to make this
linkage obvious. If the blank line is there, there's a growing risk of `@doc` clause becoming
completely separated from its owner in the heat of future battles.
## Examples
Preferred:
@doc \"""
This is by far the most complex function in the universe.
\"""
def func, do: nil
Weak linkage:
@doc \"""
This is by far the most complex function in the universe.
\"""
def func, do: nil
Broken linkage:
@doc \"""
This is by far the most complex function in the universe.
\"""
def non_complex_func, do: something_less_complex_than_returning_nil()
def func, do: nil
"""
def doc_spacing, do: nil
@doc """
Aliases should be preferred over using full module name.
## Reasoning
Aliasing modules makes code more compact and easier to read. They're even more beneficial as the
number of uses of aliased module grows.
That's of course assuming they don't override other used modules or ones that may be used in the
future (such as stdlib's `IO` or similar).
## Examples
Preferred:
def create(params)
alias Toolbox.Creator
params
|> Creator.build()
|> Creator.call()
|> Toolbox.IO.write()
end
Not so DRY:
def create(params)
params
|> Toolbox.Creator.build()
|> Toolbox.Creator.call()
|> Toolbox.IO.write()
end
Overriding standard library:
def create(params)
alias Toolbox.IO
params
|> Toolbox.Creator.build()
|> Toolbox.Creator.call()
|> IO.write()
end
"""
def alias_usage, do: nil
@doc """
Reuse directives against same module should be grouped with `{}` syntax and sorted A-Z.
## Reasoning
The fresh new grouping feature for `alias`, `import`, `require` and `use` allows to make multiple
reuses from single module shorter, more declarative and easier to comprehend. It's just a
challenge to use this feature consistently, hence this rule.
Keeping sub-module names in separate lines (even when they could fit a single line) is an
additional investment for the future - to have clean diffs when more modules will get added. It's
also easier to keep them in alphabetical order when they're in separate lines from day one.
## Examples
Preferred:
alias Toolbox.{
Creator,
Deletor,
Other,
}
alias SomeOther.Mod
Short but not so future-proof:
alias Toolbox.{Creator, Deletor, Other}
Classical but inconsistent and not so future-proof:
alias Toolbox.Creator
alias Toolbox.Deletor
alias SomeOther.Mod
alias Toolbox.Other
"""
def reuse_directive_grouping, do: nil
@doc """
Per-function usage of reuse directives should be preferred over module-wide usage.
## Reasoning
If a need for `alias`, `import` or `require` spans only across single function in a module (or
across a small subset of functions in otherwise large module), it should be preferred to declare
it locally on top of that function instead of globally for whole module.
Keeping these declarations local makes them even more descriptive as to what scope is really
affected. They're also more visible, being closer to the place they're used at. The chance for
conflicts is also reduced when they're local.
## Examples
Preferred (`alias` on `Users.User` is used in both `create` and `delete` functions so it's made
global, but `import` on `Ecto.Query` is only used in `delete` function so it's declared only
there):
defmodule Users do
alias Users.User
def create(params)
%User{}
|> User.changeset(params)
|> Repo.insert()
end
def delete(user_id) do
import Ecto.Query
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
Not so DRY (still, this could be OK if there would be more functions in `Users` module that
wouldn't use the `User` sub-module):
defmodule Users do
def create(params)
alias Users.User
%User{}
|> User.changeset(params)
|> Repo.insert()
end
def delete(user_id) do
import Ecto.Query
alias Users.User
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
Everything a bit too public:
defmodule Users do
import Ecto.Query
alias Users.User
def create(params)
%User{}
|> User.changeset(params)
|> Repo.insert()
end
def delete(user_id) do
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
"""
def reuse_directive_scope, do: nil
@doc """
Reuse directives should be placed on top of modules or functions.
## Reasoning
Calls to `alias`, `import`, `require` or `use` should be placed on top of module or function, or
directly below `@moduledoc` in case of modules with documentation.
Just like with the order rule, this is to make finding these directives faster when reading the
code. For that reason, it's more beneficial to have such important key for interpreting code in
obvious place than attempting to have them right above the point where they're needed (which
usually ends up messed up anyway when code gets changed over time).
## Examples
Preferred:
defmodule Users do
alias Users.User
def name(user) do
user["name"] || user.name
end
def delete(user_id) do
import Ecto.Query
user_id = String.to_integer(user_id)
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
Cool yet not so future-proof "lazy" placement:
defmodule Users do
def name(user) do
user["name"] || user.name
end
alias Users.User
def delete(user_id) do
user_id = String.to_integer(user_id)
import Ecto.Query
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
"""
def reuse_directive_placement, do: nil
@doc """
Calls to reuse directives should be placed in `use`, `require`, `import`,`alias` order.
## Reasoning
First of all, having any directive ordering convention definitely beats not having one, since they
are a key to parsing code and so it adds up to better code reading experience when you know
exactly where to look for an alias or import.
This specific order is an attempt to introduce more significant directives before more trivial
ones. It so happens that in case of reuse directives, the reverse alphabetical order does exactly
that, starting with `use` (which can do virtually anything with a target module) and ending with
`alias` (which is only a cosmetic change and doesn't affect the module's behavior).
## Examples
Preferred:
use Helpers.Thing import Helpers.Other alias Helpers.Tool
Out of order:
alias Helpers.Tool
import Helpers.Other
use Helpers.Thing
"""
def reuse_directive_order, do: nil
@doc """
Calls to reuse directives should not be separated with blank lines.
## Reasoning
It may be tempting to separate all aliases from imports with blank line or to separate multi-line
grouped aliases from other aliases, but as long as they're properly placed and ordered, they're
readable enough without such extra efforts. Also, as their number grows, it's more beneficial to
keep them vertically compact than needlessly padded.
## Examples
Preferred:
use Helpers.Thing
import Helpers.Other
alias Helpers.Subhelpers.{
First,
Second
}
alias Helpers.Tool
Too much padding (with actual code starting N screens below):
use Helpers.Thing
import Helpers.Other
alias Helpers.Subhelpers.{
First,
Second
}
alias Helpers.Tool
"""
def reuse_directive_spacing, do: nil
@doc """
RESTful actions should be placed in `I S N C E U D` order in controllers and their tests.
## Reasoning
It's important to establish a consistent order to make it easier to find actions and their tests,
considering that both controller and (especially) controller test files tend to be big at times.
This particular order (`index`, `show`, `new`, `create`, `edit`, `update`, `delete`) comes from
the long-standing convention established by both Phoenix and, earlier, Ruby on Rails generators,
so it should be familiar, predictable and non-surprising to existing developers.
## Examples
Preferred:
defmodule MyProject.Web.UserController do
use MyProject.Web, :controller
def index(_conn, _params), do: raise("Not implemented")
def show(_conn, _params), do: raise("Not implemented")
def new(_conn, _params), do: raise("Not implemented")
def create(_conn, _params), do: raise("Not implemented")
def edit(_conn, _params), do: raise("Not implemented")
def update(_conn, _params), do: raise("Not implemented")
def delete(_conn, _params), do: raise("Not implemented")
end
Different (CRUD-like) order against the convention:
defmodule MyProject.Web.UserController do
use MyProject.Web, :controller
def index(_conn, _params), do: raise("Not implemented")
def new(_conn, _params), do: raise("Not implemented")
def create(_conn, _params), do: raise("Not implemented")
def show(_conn, _params), do: raise("Not implemented")
def edit(_conn, _params), do: raise("Not implemented")
def update(_conn, _params), do: raise("Not implemented")
def delete(_conn, _params), do: raise("Not implemented")
end
> The issue with CRUD order is that `index` action falls between fitting and being kind of "above"
the *Read* section and `new`/`edit` actions fall between *Read* and *Create*/*Update* sections,
respectively.
"""
def restful_action_order, do: nil
@doc """
Documentation in `@doc` and `@moduledoc` should start with an one-line summary sentence.
## Reasoning
This first line is treated specially by ExDoc in that it's taken as a module/function summary for
API summary listings. The period at its end is removed so that it looks good both as a summary
(without the period) and as part of a whole documentation (with a period).
The single-line limit (with up to 100 characters as per line limit rule) is there to avoid mixing
up short and very long summaries on a single listing.
It's also important to fit as precise description as possible in this single line, without
unnecessarily repeating what's already expressed in the module or function name itself.
## Examples
Preferred:
defmodule MyProject.Accounts do
@moduledoc \"""
User account authorization and management system.
\"""
end
Too vague:
defmodule MyProject.Accounts do
@moduledoc \"""
Accounts system.
\"""
end
Missing trailing period:
defmodule MyProject.Accounts do
@moduledoc \"""
Accounts system
\"""
end
Missing trailing blank line:
defmodule MyProject.Accounts do
@moduledoc \"""
User account authorization and management system.
All functions take the `MyProject.Accounts.Input` structure as input argument.
\"""
end
"""
def doc_summary_format, do: nil
@doc """
Documentation in `@doc` and `@moduledoc` should be written in ExDoc-friendly Markdown.
Here's what is considered an ExDoc-friendly Markdown:
- Paragraphs written with full sentences, separated by a blank line
- Headings starting from 2nd level heading (`## Biggest heading`)
- Bullet lists starting with a dash and subsequent lines indented by 2 spaces
- Bullet/ordered list items separated by a blank line
- Elixir code indented by 4 spaces to mark the code block
## Reasoning
This syntax is encouraged in popular Elixir libraries, it's confirmed to generate nicely readable
output and it's just as readable in the code which embeds it as well.
## Examples
Preferred:
defmodule MyProject.Accounts do
@moduledoc \"""
User account authorization and management system.
This module does truly amazing stuff. It's purpose is to take anything you pass its way and
make an user out of that. It can also tell you if specific user can do specific things without
messing the system too much.
Here's what you can expect from this module:
- Nicely written lists with a lot of precious information that
get indented properly in every subsequent line
- And that are well padded as well
And here's an Elixir code example:
defmodule MyProject.Accounts.User do
@defstruct [:name, :email]
end
It's all beautiful, isn't it?
\"""
end
Messed up line breaks, messed up list item indentation and non ExDoc-ish code block:
defmodule MyProject.Accounts do
@moduledoc \"""
User account authorization and management system.
This module does truly amazing stuff. It's purpose is to take anything you pass its way and
make an user out of that. It can also tell you if specific user can do specific things without
messing the system too much.
Here's what you can expect from this module:
- Nicely written lists with a lot of precious information that
get indented properly in every subsequent line
- And that are well padded as well
And here's an Elixir code example:
```
defmodule MyProject.Accounts.User do
@defstruct [:name, :email]
end
```
It's not so beautiful, is it?
\"""
end
"""
def doc_content_format, do: nil
@doc """
Config calls should be placed in alphabetical order, with modules over atoms.
## Reasoning
Provides obvious and predictable placement of specific config calls.
## Examples
Preferred:
config :another_package, key: value
config :my_project, MyProject.A, key: "value"
config :my_project, MyProject.B, key: "value"
config :my_project, :a, key: "value"
config :my_project, :b, key: "value"
config :package, key: "value"
Modules wrongly mixed with atoms and internal props wrongly before external ones:
config :my_project, MyProject.A, key: "value"
config :my_project, :a, key: "value"
config :my_project, MyProject.B, key: "value"
config :my_project, :b, key: "value"
config :another_package, key: value
config :package, key: "value"
"""
def config_order, do: nil
@doc ~S"""
Exceptions should define semantic struct fields and a custom `message/1` function.
## Reasoning
It's possible to define an exception with custom arguments and message by overriding the
`exception/1` function and defining a standard `defexception [:message]` struct, but that yields
to non-semantic exceptions that don't express their arguments in their structure. It also makes
it harder (or at least inconsistent) to define multi-argument exceptions, which is simply a
consequence of not having a struct defined for an actual struct.
Therefore, it's better to define exceptions with a custom set of struct fields instead of a
`message` field and to define a `message/1` function that takes those fields and creates an error
message out of them.
## Examples
Preferred:
defmodule MyError do
defexception [:a, :b]
def message(%__MODULE__{a: a, b: b}) do
"a: #{a}, b: #{b}"
end
end
raise MyError, a: 1, b: 2
Non-semantic error struct with unnamed fields in multi-argument call:
defmodule MyError do
defexception [:message]
def exception({a, b}) do
%__MODULE__{message: "a: #{a}, b: #{b}"}
end
end
raise MyError, {1, 2}
"""
def exception_structure, do: nil
@doc """
Hardcoded word (both string and atom) lists should be written using the `~w` sigil.
## Reasoning
They're simply more compact and easier to read this way. They're also easier to extend. For long
lists, line breaks can be applied without problems.
## Examples
Preferred:
~w(one two three)
~w(one two three)a
Harder to read:
["one", "two", "three"]
[:one, :two, :three]
"""
def list_format, do: nil
@doc """
Exception modules (and only them) should be named with the `Error` suffix.
## Reasoning
Exceptions are a distinct kind of application entities, so it's good to emphasize that in their
naming. Two most popular suffixes are `Exception` and `Error`. The latter was choosen for brevity.
## Examples
Preferred:
defmodule InvalidCredentialsError do
defexception [:one, :other]
end
Invalid suffix:
defmodule InvalidCredentialsException do
defexception [:one, :other]
end
Usage of `Error` suffix for non-exception modules:
defmodule Actions.HandleRegistrationError do
# ...
end
"""
def exception_naming, do: nil
@doc """
Basic happy case in a test file or scope should be placed on top of other cases.
## Reasoning
When using tests to understand how specific unit of code works, it's very handy to have the basic
happy case placed on top of other cases.
## Examples
Preferred:
defmodule MyProject.Web.MyControllerTest do
describe "index/2" do
test "works for valid params" do
# ...
end
test "fails for invalid params" do
# ...
end
end
end
Out of order:
defmodule MyProject.Web.MyControllerTest do
describe "index/2" do
test "fails for invalid params" do
# ...
end
test "works for valid params" do
# ...
end
end
end
"""
def test_happy_case_placement, do: nil
@doc """
Pipe chains must be aligned into multiple lines.
> Check out `Surgex.Guide.CodeStyle.assignment_indentation/0` to see how to assign the output from
properly formatted multi-line chains.
## Reasoning
This comes from general preference of vertical spacing over horizontal spacing, expressed across
this guide by rules such as `Surgex.Guide.CodeStyle.block_alignment/0`. This ensures that the code
is readable and not too condensed. Also, it's easier to modify or extend multi-line chains,
because they don't require re-aligning the whole thing.
By the way, single-line chains look kinda like a code copied from `iex` in a hurry, which is only
fine when the building was on fire during the coding session.
## Examples
Preferred:
user
|> reset_password()
|> send_password_reset_email()
Too condensed:
user |> reset_password() |> send_password_reset_email()
"""
def pipe_chain_alignment, do: nil
@doc """
Modules referenced in typespecs should be aliased.
## Reasoning
When writing typespecs, it is often necessary to reference a module in some nested naming
scheme. One could reference it with the absolute name, e.g. `Application.Accounting.Invoice.t`,
but this makes typespecs rather lengthy.
Using aliased modules makes typespecs easier to read and, as an added benefit, it allows for an
in-front declaration of module dependencies. This way we can easily spot breaches in module
isolation.
## Examples
Preferred:
alias VideoApp.Recommendations.{Rating, Recommendation, User}
@spec calculate_recommendations(User.t, [Rating.t]) :: [Recommendation.t]
def calculate_recommendations(user, ratings) do
# ...
end
Way too long:
@spec calculate_recommendations(
VideoApp.Recommendations.User.t,
[VideoApp.Recommendations.Rating.t]
) :: [VideoApp.Recommendations.Recommendation.t]
def calculate_recommendations(user, ratings) do
# ...
end
"""
def typespec_alias_usage, do: nil
end