@@ -372,43 +372,55 @@ defmodule Styler.Style.Pipes do
372372 when mod in @ enum ,
373373 do: { :|> , pm , [ lhs , { count , meta , [ filterer ] } ] }
374374
375- # `lhs |> Enum.filter(f1) |> Enum.filter(f2)` => `lhs |> Enum.filter(fn item -> f1.(item) && f2.(item) end)`
375+ # `lhs |> Enum.filter(f1) |> Enum.filter(f2)` => `lhs |> Enum.filter(fn item -> f1_inlined && f2_inlined end)`
376376 # (Credo.Check.Refactor.FilterFilter)
377377 defp fix_pipe (
378378 pipe_chain (
379379 pm ,
380380 lhs ,
381381 { { :. , _ , [ { _ , _ , [ :Enum ] } , :filter ] } = filter , fm , [ f1 ] } ,
382382 { { :. , _ , [ { _ , _ , [ :Enum ] } , :filter ] } , _ , [ f2 ] }
383- )
384- ) ,
385- do: { :|> , pm , [ lhs , { filter , fm , [ combined_predicate ( f1 , f2 , :&& , fm ) ] } ] }
383+ ) = node
384+ ) do
385+ case combined_predicate ( f1 , f2 , :&& , fm ) do
386+ nil -> node
387+ combined -> { :|> , pm , [ lhs , { filter , fm , [ combined ] } ] }
388+ end
389+ end
386390
387- # `lhs |> Enum.reject(f1) |> Enum.reject(f2)` => `lhs |> Enum.reject(fn item -> f1.(item) || f2.(item) end)`
391+ # `lhs |> Enum.reject(f1) |> Enum.reject(f2)` => `lhs |> Enum.reject(fn item -> f1_inlined || f2_inlined end)`
388392 # (Credo.Check.Refactor.RejectReject)
389393 defp fix_pipe (
390394 pipe_chain (
391395 pm ,
392396 lhs ,
393397 { { :. , _ , [ { _ , _ , [ :Enum ] } , :reject ] } = reject , fm , [ f1 ] } ,
394398 { { :. , _ , [ { _ , _ , [ :Enum ] } , :reject ] } , _ , [ f2 ] }
395- )
396- ) ,
397- do: { :|> , pm , [ lhs , { reject , fm , [ combined_predicate ( f1 , f2 , :|| , fm ) ] } ] }
399+ ) = node
400+ ) do
401+ case combined_predicate ( f1 , f2 , :|| , fm ) do
402+ nil -> node
403+ combined -> { :|> , pm , [ lhs , { reject , fm , [ combined ] } ] }
404+ end
405+ end
398406
399- # `lhs |> Enum.filter(f1) |> Enum.reject(f2)` => `lhs |> Enum.filter(fn item -> f1.(item) && !f2.(item) end)`
407+ # `lhs |> Enum.filter(f1) |> Enum.reject(f2)` => `lhs |> Enum.filter(fn item -> f1_inlined && !f2_inlined end)`
400408 # (Credo.Check.Refactor.FilterReject)
401409 defp fix_pipe (
402410 pipe_chain (
403411 pm ,
404412 lhs ,
405413 { { :. , _ , [ { _ , _ , [ :Enum ] } , :filter ] } = filter , fm , [ f1 ] } ,
406414 { { :. , _ , [ { _ , _ , [ :Enum ] } , :reject ] } , _ , [ f2 ] }
407- )
408- ) ,
409- do: { :|> , pm , [ lhs , { filter , fm , [ combined_predicate ( f1 , f2 , :&& , fm , negate_f2: true ) ] } ] }
415+ ) = node
416+ ) do
417+ case combined_predicate ( f1 , f2 , :&& , fm , negate_f2: true ) do
418+ nil -> node
419+ combined -> { :|> , pm , [ lhs , { filter , fm , [ combined ] } ] }
420+ end
421+ end
410422
411- # `lhs |> Enum.reject(f1) |> Enum.filter(f2)` => `lhs |> Enum.filter(fn item -> !f1.(item) && f2.(item) end)`
423+ # `lhs |> Enum.reject(f1) |> Enum.filter(f2)` => `lhs |> Enum.filter(fn item -> !f1_inlined && f2_inlined end)`
412424 # The merged call collapses to `Enum.filter` (as Credo recommends) — `f1` was the original reject,
413425 # so we negate it; `f2` was the original filter, so it stays.
414426 # (Credo.Check.Refactor.RejectFilter)
@@ -418,9 +430,13 @@ defmodule Styler.Style.Pipes do
418430 lhs ,
419431 { { :. , _ , [ { _ , _ , [ :Enum ] } , :reject ] } , fm , [ f1 ] } ,
420432 { { :. , _ , [ { _ , _ , [ :Enum ] } , :filter ] } = filter , _ , [ f2 ] }
421- )
422- ) ,
423- do: { :|> , pm , [ lhs , { filter , fm , [ combined_predicate ( f1 , f2 , :&& , fm , negate_f1: true ) ] } ] }
433+ ) = node
434+ ) do
435+ case combined_predicate ( f1 , f2 , :&& , fm , negate_f1: true ) do
436+ nil -> node
437+ combined -> { :|> , pm , [ lhs , { filter , fm , [ combined ] } ] }
438+ end
439+ end
424440
425441 # `lhs |> Enum.map(f1) |> Enum.map(f2)` => single `Enum.map` whose body is the inlined nested call. We seed the body
426442 # with a one-step pipe inside f1's slot - Styler's existing `f(pipe, args)` walk then unfolds the f2 call into the
@@ -558,19 +574,52 @@ defmodule Styler.Style.Pipes do
558574 defp valid_pipe_start? ( { fun , _ , _args } ) when is_atom ( fun ) , do: String . match? ( "#{ fun } " , ~r/ ^sigil_[a-zA-Z]$/ )
559575 defp valid_pipe_start? ( _ ) , do: true
560576
561- # Combines two 1-arity predicates into a single anonymous function: `fn item -> f1.(item) <op> f2.(item) end`.
562- # Universal form that's correct regardless of whether each predicate is a capture, an `&(...)` shortform,
563- # or an explicit `fn x -> ... end`. Used by FilterFilter (op: `&&`), RejectReject (op: `||`), and
564- # the mixed FilterReject / RejectFilter rules (op: `&&` with one side wrapped in `!`).
577+ # Combines two 1-arity predicates into a single anonymous function: `fn item -> f1_term <op> f2_term end`.
578+ # Returns nil if either side can't be combined cleanly — the caller skips the rewrite. We inline
579+ # captures and inline-fn predicates whose body is a single expression (so combining with `&&`/`||`
580+ # is sound). Bare references (like `f1`) fall back to `f1.(item)`. Anything else (multi-statement
581+ # `fn` bodies, captures with multiple `&1` uses, nested closures) is left alone — the original
582+ # two-pipe form is more readable than what we'd produce. Used by FilterFilter (op: `&&`),
583+ # RejectReject (op: `||`), and the mixed FilterReject / RejectFilter rules.
565584 defp combined_predicate ( f1 , f2 , op , m , opts \\ [ ] ) do
566585 line = m [ :line ]
567586 item = { :item , [ line: line ] , nil }
568- call_f1 = maybe_negate ( predicate_call ( f1 , item , line ) , opts [ :negate_f1 ] == true , line )
569- call_f2 = maybe_negate ( predicate_call ( f2 , item , line ) , opts [ :negate_f2 ] == true , line )
570- body = { op , [ line: line ] , [ call_f1 , call_f2 ] }
571- { :fn , [ closing: [ line: line ] , line: line ] , [ { :-> , [ line: line ] , [ [ item ] , body ] } ] }
587+
588+ with call_f1 when not is_nil ( call_f1 ) <- predicate_term ( f1 , item , line ) ,
589+ call_f2 when not is_nil ( call_f2 ) <- predicate_term ( f2 , item , line ) do
590+ call_f1 = maybe_negate ( call_f1 , opts [ :negate_f1 ] == true , line )
591+ call_f2 = maybe_negate ( call_f2 , opts [ :negate_f2 ] == true , line )
592+ body = { op , [ line: line ] , [ call_f1 , call_f2 ] }
593+ { :fn , [ closing: [ line: line ] , line: line ] , [ { :-> , [ line: line ] , [ [ item ] , body ] } ] }
594+ end
595+ end
596+
597+ defp predicate_term ( fun , item , line ) do
598+ cond do
599+ inlineable_simple? ( fun ) -> Style . set_line ( inline_capture ( fun , item , line ) , line )
600+ bare_reference? ( fun ) -> predicate_call ( fun , item , line )
601+ true -> nil
602+ end
572603 end
573604
605+ # Inlineable AND the body is a single expression. Multi-statement `fn`/`&` bodies don't compose
606+ # cleanly under `&&`/`||` — short-circuit semantics would change and the formatted output is uglier
607+ # than the original two-pipe chain.
608+ defp inlineable_simple? ( { :& , _ , [ { :/ , _ , _ } ] } = fun ) , do: inlineable? ( fun )
609+
610+ defp inlineable_simple? ( { :& , _ , [ body ] } = fun ) , do: inlineable? ( fun ) and not multi_statement_block? ( body )
611+
612+ defp inlineable_simple? ( { :fn , _ , [ { :-> , _ , [ _ , body ] } ] } = fun ) ,
613+ do: inlineable? ( fun ) and not multi_statement_block? ( body )
614+
615+ defp inlineable_simple? ( _ ) , do: false
616+
617+ defp multi_statement_block? ( { :__block__ , _ , [ _ , _ | _ ] } ) , do: true
618+ defp multi_statement_block? ( _ ) , do: false
619+
620+ defp bare_reference? ( { var , _ , ctx } ) when is_atom ( var ) and is_atom ( ctx ) , do: true
621+ defp bare_reference? ( _ ) , do: false
622+
574623 defp maybe_negate ( call , true , line ) , do: { :! , [ line: line ] , [ call ] }
575624 defp maybe_negate ( call , false , _line ) , do: call
576625
0 commit comments