@@ -23,6 +23,7 @@ defmodule Styler.Style.SingleNode do
2323 * Credo.Check.Refactor.CondStatements
2424 * Credo.Check.Refactor.RedundantWithClauseResult
2525 * Credo.Check.Refactor.WithClauses
26+ * Credo.Check.Warning.ExpensiveEmptyEnumCheck
2627 """
2728
2829 @ behaviour Styler.Style
@@ -204,6 +205,13 @@ defmodule Styler.Style.SingleNode do
204205 { { :. , dm , [ { :__aliases__ , am , [ mod ] } , fun ] } , funm , args }
205206 end
206207
208+ # `length(x) <op> 0|1` => `x == []` or `x != []`. Avoids walking the whole list to check emptiness.
209+ # `Enum.count(x) <op> 0|1` => `Enum.empty?(x)` or `not Enum.empty?(x)` (same reason).
210+ # (Credo.Check.Warning.ExpensiveEmptyEnumCheck)
211+ defp style ( { op , m , [ lhs , rhs ] } = ast ) when op in [ :== , :!= , :=== , :!== , :> , :< , :>= , :<= ] do
212+ rewrite_empty_check ( op , lhs , rhs , m ) || ast
213+ end
214+
207215 # Remove parens from 0 arity funs (Credo.Check.Readability.ParenthesesOnZeroArityDefs)
208216 defp style ( { def , dm , [ { fun , funm , [ ] } | rest ] } ) when def in ~w( def defp) a and is_atom ( fun ) ,
209217 do: style ( { def , dm , [ { fun , Keyword . delete ( funm , :closing ) , nil } | rest ] } )
@@ -337,4 +345,57 @@ defmodule Styler.Style.SingleNode do
337345
338346 defp add_underscores ( [ a , b , c , d | rest ] , acc ) , do: add_underscores ( [ d | rest ] , [ ?_ , c , b , a | acc ] )
339347 defp add_underscores ( reversed_list , acc ) , do: Enum . reverse ( reversed_list , acc )
348+
349+ # ExpensiveEmptyEnumCheck helpers
350+ # Picks out a `length(x)` or `Enum.count(x)` call paired with a literal `0` or `1` and rewrites
351+ # the entire comparison. Returns nil for any shape that isn't a recognized empty-check pattern.
352+ defp rewrite_empty_check ( op , lhs , rhs , m ) do
353+ case { size_call ( lhs ) , int_literal ( rhs ) , size_call ( rhs ) , int_literal ( lhs ) } do
354+ { kind , n , _ , _ } when not is_nil ( kind ) and not is_nil ( n ) -> emit_empty_check ( op , kind , n , m )
355+ { _ , _ , kind , n } when not is_nil ( kind ) and not is_nil ( n ) -> emit_empty_check ( swap_op ( op ) , kind , n , m )
356+ _ -> nil
357+ end
358+ end
359+
360+ defp size_call ( { :length , _ , [ x ] } ) , do: { :length , x }
361+ defp size_call ( { { :. , _ , [ { :__aliases__ , _ , [ :Enum ] } , :count ] } , _ , [ x ] } ) , do: { :enum_count , x }
362+ defp size_call ( _ ) , do: nil
363+
364+ defp int_literal ( { :__block__ , _ , [ n ] } ) when n in [ 0 , 1 ] , do: n
365+ defp int_literal ( _ ) , do: nil
366+
367+ # `length(x) <= 0` is also "empty" because length is non-negative; same for `length(x) >= 0` (tautology, skip).
368+ defp empty_class ( :== , 0 ) , do: :empty
369+ defp empty_class ( :=== , 0 ) , do: :empty
370+ defp empty_class ( :!= , 0 ) , do: :not_empty
371+ defp empty_class ( :!== , 0 ) , do: :not_empty
372+ defp empty_class ( :> , 0 ) , do: :not_empty
373+ defp empty_class ( :<= , 0 ) , do: :empty
374+ defp empty_class ( :>= , 1 ) , do: :not_empty
375+ defp empty_class ( :< , 1 ) , do: :empty
376+ defp empty_class ( _ , _ ) , do: nil
377+
378+ defp swap_op ( :> ) , do: :<
379+ defp swap_op ( :< ) , do: :>
380+ defp swap_op ( :>= ) , do: :<=
381+ defp swap_op ( :<= ) , do: :>=
382+ defp swap_op ( op ) , do: op
383+
384+ defp emit_empty_check ( op , { :length , x } , n , m ) do
385+ case empty_class ( op , n ) do
386+ :empty -> { :== , m , [ x , { :__block__ , [ line: m [ :line ] ] , [ [ ] ] } ] }
387+ :not_empty -> { :!= , m , [ x , { :__block__ , [ line: m [ :line ] ] , [ [ ] ] } ] }
388+ nil -> nil
389+ end
390+ end
391+
392+ defp emit_empty_check ( op , { :enum_count , x } , n , m ) do
393+ empty_call = { { :. , m , [ { :__aliases__ , m , [ :Enum ] } , :empty? ] } , m , [ x ] }
394+
395+ case empty_class ( op , n ) do
396+ :empty -> empty_call
397+ :not_empty -> { :not , m , [ empty_call ] }
398+ nil -> nil
399+ end
400+ end
340401end
0 commit comments