Quokka Elixir Formatter Plugin

repository·main·Indexed 41 days ago

https://github.com/emkguts/quokka

An Elixir formatter plugin that combines mix format and mix credo to automatically rewrite code for style adherence. It provides Credo-inspired rewrites, config-driven autosorting for maps, defstructs, and Ecto schemas, and control flow simplifications for if, unless, and cond statements.

Tokens
14.9K
Snippets
58
Records
76
Agent score
50%

What's inside Quokka

  1. Rewrite Empty Enum Checks

    main

    Quokka automatically converts checks for empty enums (like length() == 0 or Enum.count() > 0) into the more idiomatic and efficient Enum.empty?/1 or Enum.any?/2. This addresses Credo.Check.Warning.ExpensiveEmptyEnumCheck and is not configurable.

    # Given:
    if enum |> MyModule.transform() |> length() == 0, do: "empty"
    # Styled:
    if enum |> MyModule.transform() |> Enum.empty?(), do: "empty"
    
    # Given:
    if Enum.count(enum) > 0, do: "not empty"
    # Styled:
    if not Enum.empty?(enum), do: "not empty"
  2. Compare Config Autosort and # quokka:sort

    main

    Quokka provides two ways to sort values. They use the same implementation and comment rules, but serve different purposes:

    FeatureConfig autosort# quokka:sort
    How to enableautosort: [:map, :defstruct, :schema] in .formatter.exsPlace # quokka:sort on the line above a value
    What it sortsAll matching values in the codebaseOnly values you annotate (lists, sigils, @type maps, etc.)
    Opt out# quokka:skip-sort on a specific value, or exclude: [:autosort]Not applicable — annotated values are always sorted
    ConfigurabilityUses :only / :exclude styleNot configurable — # quokka:sort always runs

    Note for users upgrading from 2.12.x: Previously, both were controlled via :comment_directives. In v2.13.0+, they are separate. Use :autosort in :only or :exclude for config-driven sorting. # quokka:sort is now independent and cannot be disabled via config.

  3. Use additional Quokka style improvements

    main

    Beyond Credo-inspired rewrites, Quokka provides built-in support for:

    • Test Styling: Rewrites tests to be efficient and idiomatic.
    • Unnecessary reduces: Automatically rewrites eligible Enum.reduce/3 calls to more appropriate functions, such as Map.new/2 or other purpose-built functions from the Enum module.
  4. Rewrite Enum.reduce/2,3 to purpose-built functions

    main

    Quokka rewrites Enum.reduce/2,3 calls to more efficient, purpose-built functions like Enum.sum/1, Enum.count/2, or Map.new/2 when the reducer follows specific patterns (e.g., simple addition or map insertion).

    # Before
    Enum.reduce(items, 0, fn item, acc -> acc + item.count end)
    # Styled
    Enum.sum_by(items, fn item -> item.count end)
    
    # Before
    Enum.reduce(items, %{}, fn item, acc -> Map.put(acc, item.id, item) end)
    # Styled
    Map.new(items, fn item -> {item.id, item} end)
  5. Use Implicit Try Syntax

    main

    Quokka rewrites functions where the entire body is a try/do block to use the implicit try syntax. This addresses Credo.Check.Readability.PreferImplicitTry. This behavior is not configurable.

    # Before
    def foo() do
      try do
        uh_oh()
      rescue
        exception -> {:error, exception}
      catch
        :a_throw -> {:error, :threw!}
      else
        try_has_an_else_clause? -> {:did_you_know, try_has_an_else_clause?}
      after
        :done
      end
    end
    
    # After
    def foo() do
      uh_oh()
    rescue
      exception -> {:error, exception}
    catch
      :a_throw -> {:error, :threw!}
    else
      try_has_an_else_clause? -> {:did_you_know, try_has_an_else_clause?}
    after
      :done
    end
  6. Align Variable Matching to the Right

    main

    Quokka rewrites pattern matching to ensure the variable being assigned is on the right-hand side of the = operator. This applies to case statements, with expressions, and function arguments. This addresses Credo.Check.Consistency.ParameterPatternMatching but is not configurable; Quokka always moves matches to the right.

    # Before
    case foo do
      bar = %{baz: baz? = true} -> :baz?
      opts = [[a = %{}] | _] -> a
    end
    # Styled:
    case foo do
      %{baz: true = baz?} = bar -> :baz?
      [[%{} = a] | _] = opts -> a
    end
  7. Expand multi-alias statements

    main

    Quokka automatically expands multi-alias and multi-import statements into explicit, single-line forms. This helps with searchability and addresses Credo readability checks.

    Note: Quokka only expands these statements; it does not compress multiple individual aliases into a single multi-alias statement. The behavior is configurable via your Credo configuration for Credo.Check.Readability.MultiAlias.

    # Before
    import Foo.{Bar, Baz, Bop}
    alias Foo.{Bar, Baz.A, Bop}
    
    # After
    import Foo.Bar
    import Foo.Baz
    import Foo.Bop
    
    alias Foo.Bar
    alias Foo.Baz.A
    alias Foo.Bop
  8. Optimize pipe chains with Piped Function Optimizations

    main

    Quokka automatically optimizes pipe chains by combining multiple function calls into single, more efficient calls. This includes:

    • Enum/Stream combinations:
      • Enum.reverse() |> Enum.concat(enum) $\rightarrow$ Enum.reverse(enum)
      • Enum.filter(fun) |> Enum.count() $\rightarrow$ Enum.count(fun)
      • Enum.drop(n) |> Enum.take(m) $\rightarrow$ Enum.slice(n, m) (only for non-negative integer literals)
      • Enum.map(mapper) |> Enum.join(joiner) $\rightarrow$ Enum.map_join(joiner, mapper)
      • Enum.map(mapper) |> Enum.sum() $\rightarrow$ Enum.sum_by(mapper) (Elixir 1.18+)
      • Enum.map(mapper) |> Enum.product() $\rightarrow$ Enum.product_by(mapper) (Elixir 1.18+)
    • Map/Keyword operations:
      • Consecutive Map.delete or Map.drop calls are merged into a single Map.drop/2.
      • Consecutive Keyword.delete or Keyword.drop calls are merged into a single Keyword.drop/2.
    • Collection Construction:
      • Enum.map(mapper) |> Map.new() $\rightarrow$ Map.new(mapper)
      • Enum.map(mapper) |> Enum.into(%{}) $\rightarrow$ Map.new(mapper)
    • DateTime:
      • DateTime.utc_now() |> DateTime.truncate(precision) $\rightarrow$ DateTime.utc_now(precision) (if Credo.Check.Refactor.UtcNowTruncate is enabled).
  9. Rewrite nested Enum.map/2 to Enum.sum_by/2 or Enum.product_by/2

    main

    On Elixir 1.18+, Quokka collapses nested Enum.map/2 or Stream.map/2 calls consumed by Enum.sum/1 or Enum.product/1 into the purpose-built aggregate functions. This avoids allocating an intermediate list.

    # Before
    Enum.sum(Enum.map(groups, fn group -> length(group.rows) end))
    # Styled
    Enum.sum_by(groups, fn group -> length(group.rows) end)
  10. Configure Credo-inspired rewrites in Quokka

    main

    Quokka utilizes existing Credo opinions to automatically rewrite Elixir code. It supports many Credo checks across several categories.

    Key capabilities include:

    • Configurability: All :controversial Credo checks are configurable. You can also disable specific checks to prevent Quokka from attempting a rewrite.
    • Credo.Check.Consistency: Handles multi-alias/import expansion and parameter pattern matching.
    • Credo.Check.Design: Handles alias lifting (extracting repeated aliases).
    • Credo.Check.Readability: Manages module directive organization (alphabetizing), pipe chain optimization (line length, single pipes, anonymous functions), number formatting, and control flow simplification (e.g., with statements, try expressions).
    • Credo.Check.Refactor: Optimizes boolean expressions, Enum operations (filter/count, map/into, map/join), and with/unless clauses.
    • Credo.Check.Warning: Rewrites slow checks of enum emptiness.
  11. Shrink Function Definitions to One Line

    main

    When possible, Quokka will compress multi-line function definitions into a single line if they fit within the configured line length limit.

    # Before
    def save(
           %Socket{assigns: %{user: user, live_action: :new}} = initial_socket,
           params
         ),
         do: :ok
    
    # Styled
    def save(%Socket{assigns: %{user: user, live_action: :new}} = initial_socket, params), do: :ok
  12. Rewrite Map/Keyword merge, drop, and get

    main

    Quokka simplifies Map and Keyword operations when they involve a single key literal:

    • Map.merge(map, %{key: value}) $\rightarrow$ Map.put(map, :key, value)
    • Map.drop(map, [key]) $\rightarrow$ Map.delete(map, key)
    • Map.get(map, key, nil) $\rightarrow$ Map.get(map, key)
    # Before
    Map.merge(map, %{key: :value})
    # Styled
    Map.put(map, :key, :value)
    
    # Before
    Map.drop(map, [key])
    # Styled
    Map.delete(map, key)