Rbtz.CredoChecks.Readability.AwkwardPipe (rbtz_credo_checks v0.3.0)

Copy Markdown View Source

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:

  1. 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/)

  2. Either operand of ++, <>, in, and, or is 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

  3. Pipe into the Kernel. operator form. Every Elixir operator (&&, ||, and, or, !, not, +, -, *, /, ++, --, <>, ==, !=, ===, !==, =~, <, <=, >, >=, in) is reachable as Kernel.<op>/n as 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

  4. Single-step pipe inside a tuple literal element.

    # Bad
    {:ok, socket |> assign(page_title: "Invite")}
    
    # Good
    {:ok, assign(socket, page_title: "Invite")}
  5. Single-step pipe inside string interpolation.

    # Bad "<ol>#{data |> list_items_html()}</ol>"

    # Good "<ol>#{list_items_html(data)}</ol>"

  6. 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))

  7. if / unless / cond condition joins any pipe via && / || / and / or.

    # Bad if x |> something() && other_value do

    # Good if something(x) && other_value do

  8. Single |> inside a single-line fn … -> … end or &(…).

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

  9. 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)}

  10. 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.