erlfmt

repository·main·Indexed 19 days ago

https://github.com/whatsapp/erlfmt

An opinionated Erlang code formatter designed to automate styling and reduce team contention. It supports .erl, .hrl, .app, .app.src, .config, and .escript files, requiring Erlang/OTP 21+. erlfmt can be integrated as a rebar3 plugin or used as a standalone escript. It focuses on preserving code semantics, intent, and original representations of strings, atoms, and integers while enforcing a consistent layout and configurable print-width.

Tokens
8.3K
Snippets
24
Records
44
Agent score
66%

What's inside erlfmt

  1. Formatting decision: Commas in lists

    main

    The erlfmt formatter enforces a consistent style for commas in Erlang lists. Based on an analysis of major Erlang codebases (including OTP and WhatsApp), erlfmt uses the suffix pattern, where commas are placed at the end of the expression on each line, rather than as a prefix at the start of the line.

    Suffix Pattern (Enforced):

    [
      x,
      y,
      z
    ]

    Prefix Pattern (Not Enforced):

    [ x
    , y
    , z
    ]
  2. Compare 'Each Element On Their Own Line' vs 'IO Format Tilde P' list styles

    main

    The project evaluated two primary styles for multi-line lists:

    1. Each Element On Their Own Line (Chosen):

      • Minimizes diffs when changing elements or variable names.
      • More familiar to developers from other language backgrounds.
      • Example:
      My_Variable = [
          element_one,
          element_two
      ]
    2. IO Format Tilde P (Rejected):

      • Mimics the compact output of io:format("~p", [List]).
      • Can cause large diffs because changing a variable name requires re-aligning all subsequent lines.
      • Example:
      My_Variable = [element_one, element_two]
  3. How list comprehensions are formatted in erlfmt

    main

    When list comprehensions are broken up over multiple lines (due to long function names, parameters, generators, or filters), erlfmt uses a specific dedented style for the double pipes (||).

    Key characteristics of the chosen format:

    1. The double pipes (||) are dedented by a single space relative to the opening bracket [.
    2. All subsequent parts of the list comprehension (generators, filters, etc.) are aligned using a consistent 4-space indentation.
    3. This ensures that even with multiline expressions (like long function arguments), the structure remains readable and consistent with how lists are formatted.
    [
        function_with_long_name(
            A,
            ALongArgument
        )
     || {A, B} <- Cs, 
        filter(B)
    ]
  4. How erlfmt respects original formatting decisions

    main

    The formatter attempts to preserve your existing layout choices in two specific areas by looking at the presence of newlines in the original code:

    1. Container Layouts

    For containers like lists, tuples, maps, records, and function calls, erlfmt chooses between three layouts:

    • Collapsed: The entire collection is on a single line.
    • Semi-expanded: The enclosing brackets/braces/parentheses are on their own line, but all elements are on a single line.
    • Expanded: Each element is on a separate line.

    How to control it:

    • To force semi-expanded: Place a newline between the opening bracket and the first element.
    • To force expanded: Place a newline between any two elements.
    • To force collapsed: Remove all newlines within the container.

    2. Clause Layouts

    For sequences in functions, case expressions, or receive blocks, the formatter decides if the clause body is printed directly after -> or on a new indented line.

    How to control it:

    • The layout of the entire sequence is determined by the layout of the first clause. If the first clause has a newline after ->, all subsequent clauses will also have their bodies on new lines. If the first clause is on a single line, the whole sequence will be collapsed.
    %% Semi-expanded (newline after opening bracket)
    [
        Foo, Bar
    ]
    
    %% Expanded (newline between elements)
    [
        Foo,
        Bar
    ]
    
    %% Collapsed (no newlines)
    [Foo, Bar]
  5. Understand erlfmt comment formatting behavior

    main

    Currently, erlfmt moves all trailing comments to the line above the code they refer to, ensuring all comments reside on their own dedicated lines.

    Future updates are planned to support directly following comments (comments that share a line with code), but erlfmt will not support aligning comments to a specific column number. This decision was made to prevent large, unnecessary diffs when a single line change forces the re-alignment of all surrounding comments.

  6. How erlfmt formats multi-line lists

    main

    By default, erlfmt formats multi-line lists by placing each element on its own line. This decision was made to minimize diff noise when changing a single line or renaming the variable the list is assigned to, and to align with style guides in other languages (like Python, Java, and JavaScript) to make Erlang more accessible to new developers.

    Note: This formatting assumes that commas are treated as suffixes and that 4 spaces are used for indentation.

    [
        the_first_and,
        second_element_fits_on_one_line,
        but_the_third_cannot_also_fit
    ]
  7. How erlfmt handles comments

    main
    erlfmt preserves comment positioning and avoids the 'floating comment' issue seen in other formatters. It specifically handles trailing comments by moving them to the line above to maintain clarity and prevents reordering comments within complex structures like comprehensions.
  8. How erlfmt handles comment formatting

    main

    erlfmt is an opinionated formatter that makes specific decisions about comment placement to minimize diff noise and maintain consistency. The project supports two primary styles for comments:

    1. Single comments on the same line

    This is the chosen layout for erlfmt. Comments are placed on the same line as the code they describe. This style is popular across many languages and minimizes diffs when new fields are added to structures like maps or records.

    #{
        A => a, % comment
        B => bb, % another comment
        C => ccc % that comment
    }

    2. All comments on a new line

    An alternative style where comments are placed on their own line above the code. While this is common in Erlang, it does not accommodate trailing comments on the same line, which is a standard practice in many other languages.

    #{
        % comment
        A => a,
        % another comment
        B => bb
    }

    Design Decision: Avoiding Fixed Column Alignment

    Unlike some tools (e.g., standard Emacs Erlang mode) that align comments to a fixed column (like 48), erlfmt avoids fixed-column alignment. Fixed alignment causes unnecessary diffs when a line exceeds the chosen column, forcing all subsequent comment lines to be modified. Instead, erlfmt prioritizes minimizing diffs when a single line is changed.

  9. Why erlfmt does not support aligned comments

    main

    Aligning comments to a specific column (e.g., in maps or records) is not supported because it violates the goal of minimizing diffs. If a new field or value is added that is longer than previous ones, every existing line with an aligned comment must be modified to maintain alignment, creating noise in version control.

    Example of problematic alignment:

    #{
        A => a,     % comment
        B => bb,    % another comment
        C => ccc,   % that comment
        Longer => d % this comment
    }

    In the example above, adding Longer => d forces the indentation of the comments on lines A, B, and C to change.

    #{
        A => a,  % comment
        B => bb, % another comment
        C => ccc % that comment
    }
  10. Understand differences between erlfmt_parse and erl_parse ASTs

    main

    When building tools that consume the erlfmt AST, note that erlfmt_parse deviates from the standard Erlang erl_parse (abstract form) in several key ways. The erlfmt parser introduces more explicit nodes for macros, handles guards differently, and uses more structured representations for lists and functions to facilitate formatting.

    Key differences include:

    • Record Names: Always represented as a full atom or macro_call node rather than a raw atom.
    • Guards: Uses new {guard_or, Anno, GuardAndList} and {guard_and, Anno, Exprs} nodes instead of nested lists.
    • Clauses: The clause node structure is {clause, Anno, Head, Guards, Body}. The Head can be a call node, an empty atom (for if), an args node, or a catch node.
    • Macros: Introduces {macro_call, Anno, Name, Args}, {macro_string, Anno, Name} (for ??Name syntax), and a special {op, Anno, 'when', Expr, Guard} node for macro arguments.
    • Functions and Funs: The function node is {function, Anno, Clauses} (a list of clause or macro_call nodes). The fun node uses a new { 'fun', Anno, Value } structure.
    • Lists: Represented as a single list node rather than a chain of cons and nil nodes.
    • Concatenation: Implicit string concatenation (e.g., "foo" "bar") is represented by a {concat, Anno, Concatables} node.
    • Types: The type node is not used; types follow the same representation as values, with new binary operators like |, ::, and ...
  11. How erlfmt handles line length

    main

    erlfmt enforces a maximum line length by parsing and re-printing code. If a line exceeds the limit, the formatter automatically converts it to a vertical style.

    Example of automatic vertical formatting:

    Input:

    scenario(dial_phone_number(),  ring(), hello(mike),hello(joe), hello(robert),   system_working(), seems_to_be())

    Output:

    scenario(
        dial_phone_number(),
        ring(),
        hello(mike),
        hello(joe),
        hello(robert),
        system_working(),
        seems_to_be()
    )

    Note: Line-length enforcement is a best-effort greedy algorithm and may occasionally overrun the selected limit.

    Manual Intervention: Because erlfmt is not allowed to change the AST (Abstract Syntax Tree), it cannot perform semantic refactors like extracting variables to shorten lines. If the automatic vertical layout is undesirable, you must manually refactor the code (e.g., extracting a long expression into a variable) to allow it to fit on a single line.

  12. How erlfmt handles macros

    main
    Unlike many other Erlang formatters that may crash or skip files when encountering complex macros, erlfmt uses a forked Erlang parser designed to handle and format macros safely. This includes handling macro arguments, complex definitions, and preventing syntax errors or loss of parentheses in macro expansions. In cases where a macro cannot be formatted, erlfmt will preserve the exact string to avoid crashing and continue formatting the rest of the file.