|
| 1 | +<!-- |
| 2 | +The following rules are sourced from [UsageRules](https://github.com/ash-project/usage_rules), |
| 3 | +with modifications and additions. |
| 4 | +
|
| 5 | +SPDX-FileCopyrightText: 2025 usage_rules contributors <https://github.com/ash-project/usage_rules/graphs.contributors> |
| 6 | +
|
| 7 | +SPDX-License-Identifier: MIT |
| 8 | +--> |
| 9 | +# Elixir Core Usage Rules |
| 10 | + |
| 11 | +## Pattern Matching |
| 12 | + |
| 13 | +- Use pattern matching over conditional logic when possible |
| 14 | +- Prefer to match on function heads instead of using `if`/`else` or `case` in function bodies |
| 15 | +- `%{}` matches ANY map, not just empty maps. Use `map_size(map) == 0` guard to check for truly empty maps |
| 16 | + |
| 17 | +## Error Handling |
| 18 | + |
| 19 | +- Use `{:ok, result}` and `{:error, reason}` tuples for operations that can fail |
| 20 | +- Avoid raising exceptions for control flow |
| 21 | +- Use `with` for chaining operations that return `{:ok, _}` or `{:error, _}` |
| 22 | +- Bang functions (`!`) that explicitly raise exceptions on failure are acceptable (e.g., `File.read!/1`, `String.to_integer!/1`) |
| 23 | +- Avoid rescuing exceptions unless for a very specific case (e.g., cleaning up resources, logging critical errors) |
| 24 | + |
| 25 | +## Common Mistakes to Avoid |
| 26 | + |
| 27 | +- Elixir has no `return` statement, nor early returns. The last expression in a block is always returned. |
| 28 | +- Don't use `Enum` functions on large collections when `Stream` is more appropriate |
| 29 | +- Avoid nested `case` statements - refactor to a single `case`, `with` or separate functions |
| 30 | +- Don't use `String.to_atom/1` on user input (memory leak risk) |
| 31 | +- Lists and enumerables cannot be indexed with brackets. Use pattern matching or `Enum` functions |
| 32 | +- Prefer `Enum` functions like `Enum.reduce` over recursion |
| 33 | +- When recursion is necessary, prefer to use pattern matching in function heads for base case detection |
| 34 | +- Using the process dictionary is typically a sign of unidiomatic code |
| 35 | +- Only use macros if explicitly requested |
| 36 | +- There are many useful standard library functions, prefer to use them where possible |
| 37 | + |
| 38 | +## Function Design |
| 39 | + |
| 40 | +- Use guard clauses: `when is_binary(name) and byte_size(name) > 0` |
| 41 | +- Prefer multiple function clauses over complex conditional logic |
| 42 | +- Name functions descriptively: `calculate_total_price/2` not `calc/2` |
| 43 | +- Predicate function names should not start with `is` and should end in a question mark. |
| 44 | +- Names like `is_thing` should be reserved for guards |
| 45 | + |
| 46 | +## Data Structures |
| 47 | + |
| 48 | +- Use structs over maps when the shape is known: `defstruct [:name, :age]` |
| 49 | +- Prefer keyword lists for options: `[timeout: 5000, retries: 3]` |
| 50 | +- Use maps for dynamic key-value data |
| 51 | +- Prefer to prepend to lists `[new | list]` not `list ++ [new]` |
| 52 | + |
| 53 | +## Mix Tasks |
| 54 | + |
| 55 | +- Use `mix help` to list available mix tasks |
| 56 | +- Use `mix help task_name` to get docs for an individual task |
| 57 | +- Read the docs and options fully before using tasks |
| 58 | + |
| 59 | +## Testing |
| 60 | + |
| 61 | +- Run tests in a specific file with `mix test test/my_test.exs` and a specific test with the line number `mix test path/to/test.exs:123` |
| 62 | +- Limit the number of failed tests with `mix test --max-failures n` |
| 63 | +- Use `@tag` to tag specific tests, and `mix test --only tag` to run only those tests |
| 64 | +- Use `assert_raise` for testing expected exceptions: `assert_raise ArgumentError, fn -> invalid_function() end` |
| 65 | +- Use `mix help test` to for full documentation on running tests |
| 66 | + |
| 67 | +## Debugging |
| 68 | + |
| 69 | +- Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console. |
| 70 | + |
| 71 | +<!-- |
| 72 | +The following rules are sourced from [Phoenix Framework](https://github.com/phoenixframework/phoenix), |
| 73 | +with modifications and additions. |
| 74 | +
|
| 75 | +Copyright (c) 2014 Chris McCord, licensed under the MIT License. |
| 76 | +--> |
| 77 | +## Elixir guidelines |
| 78 | + |
| 79 | +- Elixir lists **do not support index based access via the access syntax** |
| 80 | + |
| 81 | + **Never do this (invalid)**: |
| 82 | + |
| 83 | + i = 0 |
| 84 | + mylist = ["blue", "green"] |
| 85 | + mylist[i] |
| 86 | + |
| 87 | + Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie: |
| 88 | + |
| 89 | + i = 0 |
| 90 | + mylist = ["blue", "green"] |
| 91 | + Enum.at(mylist, i) |
| 92 | + |
| 93 | +- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc |
| 94 | + you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie: |
| 95 | + |
| 96 | + # INVALID: we are rebinding inside the `if` and the result never gets assigned |
| 97 | + if connected?(socket) do |
| 98 | + socket = assign(socket, :val, val) |
| 99 | + end |
| 100 | + |
| 101 | + # VALID: we rebind the result of the `if` to a new variable |
| 102 | + socket = |
| 103 | + if connected?(socket) do |
| 104 | + assign(socket, :val, val) |
| 105 | + end |
| 106 | + |
| 107 | +- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors |
| 108 | +- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets |
| 109 | +- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package) |
| 110 | +- Don't use `String.to_atom/1` on user input (memory leak risk) |
| 111 | +- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards |
| 112 | +- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)` |
| 113 | +- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option |
| 114 | + |
| 115 | +- The `in` operator in guards requires a compile-time known value on the right side (literal list or range) |
| 116 | + |
| 117 | + **Never do this (invalid)**: using a variable which is unknown at compile time |
| 118 | + |
| 119 | + def t(x, y) when x in y, do: {x, y} |
| 120 | + |
| 121 | + This will raise `ArgumentError: invalid right argument for operator "in", it expects a compile-time proper list or compile-time range on the right side when used in guard expressions` |
| 122 | + |
| 123 | + **Valid**: use a known value for the list or range |
| 124 | + |
| 125 | + def t(x, y) when x in [1, 2, 3], do: {x, y} |
| 126 | + def t(x, y) when x in 1..10, do: {x, y} |
| 127 | + |
| 128 | +- In tests, avoid using `assert` with pattern matching when the expected value is fully known. Use direct equality comparison instead for clearer test failures |
| 129 | + |
| 130 | + **Avoid**: |
| 131 | + |
| 132 | + assert {:ok, ^value} = testing() |
| 133 | + assert {:error, :not_found} = fetch() |
| 134 | + |
| 135 | + **Prefer**: |
| 136 | + |
| 137 | + assert testing() == {:ok, value} |
| 138 | + assert fetch() == {:error, :not_found} |
| 139 | + |
| 140 | + **Exception**: Pattern matching is acceptable when you only want to assert part of a complex structure |
| 141 | + |
| 142 | + # OK: asserting only specific fields of a large struct/map |
| 143 | + assert {:ok, %{id: ^id}} = get_order() |
| 144 | + |
| 145 | +- In tests, avoid duplicating test data across multiple tests. Use constants, fixture files, or private fixture functions instead |
| 146 | + |
| 147 | + **Avoid**: Duplicating test data |
| 148 | + |
| 149 | + test "validates user email" do |
| 150 | + assert valid_email?("user@example.com") |
| 151 | + end |
| 152 | + |
| 153 | + test "creates user" do |
| 154 | + assert create_user("user@example.com") |
| 155 | + end |
| 156 | + |
| 157 | + **Prefer**: Use module attributes for constants or fixture functions |
| 158 | + |
| 159 | + @valid_email "user@example.com" |
| 160 | + |
| 161 | + test "validates user email" do |
| 162 | + assert valid_email?(@valid_email) |
| 163 | + end |
| 164 | + |
| 165 | + test "creates user" do |
| 166 | + assert create_user(@valid_email) |
| 167 | + end |
| 168 | + |
| 169 | + For complex data structures, create fixture functions: |
| 170 | + |
| 171 | + defp user_fixture(attrs \\ %{}) do |
| 172 | + %User{ |
| 173 | + name: "John Doe", |
| 174 | + email: "john@example.com", |
| 175 | + age: 30 |
| 176 | + } |
| 177 | + |> Map.merge(attrs) |
| 178 | + end |
| 179 | + |
| 180 | +## Mix guidelines |
| 181 | + |
| 182 | +- Read the docs and options before using tasks (by using `mix help task_name`) |
| 183 | +- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed` |
| 184 | +- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason |
0 commit comments