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