Current section
Files
Jump to
Current section
Files
lib/pattern/no_anon_fn_application_in_pipe.ex
defmodule Credence.Pattern.NoAnonFnApplicationInPipe do
@moduledoc """
Readability rule: Detects anonymous functions applied with `.()` inside
a pipeline.
Applying an anonymous function directly in a pipe (e.g.
`|> (fn x -> ... end).()`) is non-idiomatic and hard to read.
Use `then/2` instead, which was added in Elixir 1.12 specifically
for this purpose.
## Bad
list
|> Enum.sort()
|> (fn s -> [1 | s] end).()
## Good
list
|> Enum.sort()
|> then(fn s -> [1 | s] end)
# Or with capture syntax:
|> then(&[1 | &1])
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Match: ... |> (fn ... end).()
{:|>, meta, [_left, {{:., _, [{:fn, _, _}]}, _, []}]} = node, issues ->
issue = %Issue{
rule: :no_anon_fn_application_in_pipe,
message:
"Avoid applying anonymous functions with `.()` inside a pipeline. " <>
"Use `then/2` instead: `|> then(fn x -> ... end)`.",
meta: %{line: Keyword.get(meta, :line)}
}
{node, [issue | issues]}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
@impl true
def fix_patches(ast, _opts) do
# `... |> (fn ... end).()` → `... |> then(fn ... end)`.
#
# We patch the `.()`-application node (`(fn ...).()`) on the pipe's right in
# place rather than rewriting the whole `:|>`, so chained applications each
# get their own non-overlapping patch. Sourceror's range for the application
# starts at the `fn` keyword, EXCLUDING the wrapping `(` that `.()` requires,
# so we extend the range one column to the left to swallow that `(`. Without
# this the patch strands the `(`, producing the uncompilable `|> (then(fn ...end)`.
{_ast, patches} =
Macro.prewalk(ast, [], fn
{:|>, _, [_left, {{:., _, [{:fn, _, _} = fn_node]}, _, []} = dotcall]} = node, acc ->
case Sourceror.get_range(dotcall) do
nil ->
{node, acc}
%Sourceror.Range{start: start, end: stop} ->
start_at_paren = Keyword.update!(start, :column, &(&1 - 1))
range = %Sourceror.Range{start: start_at_paren, end: stop}
change = Credence.RuleHelpers.render_replacement({:then, [], [fn_node]}, range)
{node, [%{range: range, change: change} | acc]}
end
node, acc ->
{node, acc}
end)
Enum.reverse(patches)
end
end