Basics
This check is disabled by default.
Learn how to enable it via .credo.exs.
This check has a base priority of normal and works with any version of Elixir.
Explanation
Flags pipe (|>) usage patterns that hurt readability without providing
chaining benefit — either because a single-step pipe is pure visual noise
in the surrounding context, or because a pipe sits on either side of a
binary operator and obscures precedence.
Ten sub-rules are checked:
Either operand of
&&/||is a pipe. Operator-precedence and visual flow get muddled.# Bad check?(x) || x |> String.match?(~r/\d/)
# Bad (each pipe in a multi-line || chain still flagged) check?(x) ||
x |> String.match?(~r/\d/) || y |> String.match?(~r/\d/)# Good check?(x) || String.match?(x, ~r/\d/)
Either operand of
++,<>,in,and,oris a pipe.# Bad base_url |> String.trim_trailing("/") <> url user |> role() in @admin_roles
# Good String.trim_trailing(base_url, "/") <> url role(user) in @admin_roles
Pipe into the
Kernel.operator form. Every Elixir operator (&&,||,and,or,!,not,+,-,*,/,++,--,<>,==,!=,===,!==,=~,<,<=,>,>=,in) is reachable asKernel.<op>/nas a function — using that form to fit an operator into a pipe chain hides it. Rewrite the chain and use the operator infix.# Bad attrs |> Map.get(:items) |> Kernel.||([]) role |> String.to_atom() |> Kernel.in([:admin, :owner]) list |> length() |> Kernel.-(1)
# Good Map.get(attrs, :items) || [] String.to_atom(role) in [:admin, :owner] length(list) - 1
Single-step pipe inside a tuple literal element.
# Bad {:ok, socket |> assign(page_title: "Invite")} # Good {:ok, assign(socket, page_title: "Invite")}Single-step pipe inside
string interpolation.# Bad "<ol>#{data |> list_items_html()}</ol>"
# Good "<ol>#{list_items_html(data)}</ol>"
Single-step pipe as a non-first argument to a function call.
# Bad map |> Map.put(:slug, name |> String.downcase())
# Good map |> Map.put(:slug, String.downcase(name))
if/unless/condcondition joins any pipe via&&/||/and/or.# Bad if x |> something() && other_value do
# Good if something(x) && other_value do
Single
|>inside a single-linefn … -> … endor&(…).# Bad Enum.map(items, fn x -> x |> String.trim() end) Enum.any?(items, &(text |> String.contains?(&1)))
# Good Enum.map(items, &String.trim/1) Enum.any?(items, &String.contains?(text, &1))
HEEx-only: single
|>on the RHS of<-in:for={…}or<%= for … %>.# Bad :for={{item, idx} <- @items |> Enum.with_index()}
# Good :for={{item, idx} <- Enum.with_index(@items)}
Dot access on a parenthesized single-step pipe. Wrapping a pipe in parens just to access a field or call a function on its result hides the linear flow.
# Bad (string |> URI.parse()).host (string |> Date.from_iso8601!()).year
# Good URI.parse(string).host Date.from_iso8601!(string).year
Multi-step chains (two or more |> in the same expression) are exempt
from rules 4, 5, 6, 8, 9, and 10 — a real chain is doing visible work
and is preferred over nested calls.
Multi-line layout. Rules 1, 2, 3, and 7 fire regardless of how the
expression is laid out — a pipe mixed with ||, &&, and, or,
++, <>, in, or any Kernel. operator form (Kernel.||,
Kernel.&&, Kernel.and, Kernel.or, Kernel.+, Kernel.-,
Kernel.==, Kernel.<, Kernel.in, …) is always awkward, so line
breaks never grant exemption. Rules 4, 5, 6, and 10 fire
only when the pipe operator itself fits on a single source line — a
pipe whose LHS is itself multi-line (e.g. a heredoc
|> String.downcase()) is structurally unavoidable and is exempt.
Rule 8 is intrinsically about single-line anonymous functions. Rule 9
scans HEEx templates line-by-line.
Check-Specific Parameters
There are no specific parameters for this check.
General Parameters
Like with all checks, general params can be applied.
Parameters can be configured via the .credo.exs config file.