Surgex.Guide.CodeStyle (Surgex v4.15.2) View Source

Basic code style and formatting guidelines.

Link to this section Summary

Functions

Aliases should be preferred over using full module name.

Multi-line calculations should be indented by one level for assignment.

Vertical blocks should be preferred over horizontal blocks.

Indentation blocks must never start or end with blank lines.

Indentation blocks should be padded from surrounding code with single blank line.

There must be no space put before }, ] or ) and after {, [ or ( brackets.

Single space must be put after commas.

Config calls should be placed in alphabetical order, with modules over atoms.

Documentation in @doc and @moduledoc should be written in ExDoc-friendly Markdown.

There must be no blank lines between @doc and the function definition.

Documentation in @doc and @moduledoc should start with an one-line summary sentence.

Keywords in Ecto queries should be indented by one level (and one more for on after join).

Exception modules (and only them) should be named with the Error suffix.

Exceptions should define semantic struct fields and a custom message/1 function.

Functions should be called with parentheses.

Indentation must be done with 2 spaces.

Inline blocks should be preferred for simple code that fits one line.

Lines must not be longer than 100 characters.

Hardcoded word (both string and atom) lists should be written using the ~w sigil.

Macros should be called without parentheses.

Single blank line must be inserted after @moduledoc.

There must be no space put after the ! operator.

Large numbers must be padded with underscores.

Single space must be put around operators.

Pipe chains must be aligned into multiple lines.

Pipe chains must be started with a plain value.

Pipe chains must be used only for multiple function calls.

RESTful actions should be placed in I S N C E U D order in controllers and their tests.

Reuse directives against same module should be grouped with {} syntax and sorted A-Z.

Calls to reuse directives should be placed in use, require, import,alias order.

Reuse directives should be placed on top of modules or functions.

Per-function usage of reuse directives should be preferred over module-wide usage.

Calls to reuse directives should not be separated with blank lines.

; must not be used to separate statements and expressions.

Basic happy case in a test file or scope should be placed on top of other cases.

Files must end with single line break.

Lines must not end with trailing white-space.

Modules referenced in typespecs should be aliased.

Link to this section Functions

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
Link to this function

assignment_indentation()

View Source

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()

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

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

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

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

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

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"

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

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

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
Link to this function

ecto_query_indentation()

View Source

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)

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

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}
Link to this function

function_call_parentheses()

View Source

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

Indentation must be done with 2 spaces.

Reasoning

This is kind of a delicate subject, 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

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)

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

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]
Link to this function

macro_call_parentheses()

View Source

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)

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

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

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

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

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()

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()

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)

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.

Link to this function

reuse_directive_grouping()

View Source

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

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
Link to this function

reuse_directive_placement()

View Source

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

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
Link to this function

reuse_directive_spacing()

View Source

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

; 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()
Link to this function

test_happy_case_placement()

View Source

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

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()

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

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