Skip to content

dioxus-autofmt: formatting is not idempotent — oscillates between two states #5508

@micahkepe

Description

@micahkepe

Problem

dioxus-autofmt 0.7.6 (used by dx fmt and custom tooling via try_fmt_file/apply_formats) is not idempotent — running the formatter twice on the same file can produce different output, causing it to oscillate between two states instead of reaching a fixed point.

Root Causes

1. write_attribute_if_chain unconditionally inlines if/else attribute values

In writer.rs, write_attribute_if_chain always writes if cond { val } else { val } on a single line regardless of length. When values are long strings (e.g. Tailwind CSS classes), this produces lines far exceeding the 80-char threshold. On the next formatting pass, write_rsx_block's ShortOptimization logic (which checks formatted.len() <= 80 and attr_len + indent_level * 4 < 80) makes different decisions about line breaking — and the cycle repeats.

2. Spurious blank lines inserted between nested components

The formatter sometimes inserts empty lines between nested component children. For example:

// Before formatting:
Pagination {
    PaginationContent {
        PaginationPrevious { ... }

// After formatting:
Pagination {


    PaginationContent {


        PaginationPrevious { ... }

On the next pass these blank lines cause different formatting decisions.

3. Short single-attribute elements oscillate between oneliner and expanded form

Elements like i { class: "fa-solid fa-check" } get expanded to multiline on one pass, then the next pass sees them as short enough to collapse, and so on.

Minimal Repro (Issue 1)

use dioxus::prelude::*;

#[component]
fn Example() -> Element {
    let is_active = true;
    rsx! {
        button {
            class: if is_active {
                "w-full text-left px-3 py-1 text-xs font-mono text-primary bg-select"
            } else {
                "w-full text-left px-3 py-1 text-xs font-mono text-secondary hover:bg-select hover:text-primary"
            },
            "Click me"
        }
    }
}

Running dx fmt twice produces different output each time — the if/else gets collapsed to one massive line on the first pass, then the formatter tries to break it again on the second.

Minimal Repro (Issue 2)

use dioxus::prelude::*;

#[component]
fn Example() -> Element {
    rsx! {
        Pagination {
            PaginationContent {
                PaginationPrevious { onclick: move |_| on_prev(()), }
                PaginationNext { onclick: move |_| on_next(()), }
            }
        }
    }
}

Environment

  • dioxus-autofmt 0.7.6
  • Also reproduced with dx fmt from dioxus-cli 0.7.5

Workaround

Extract long conditional class strings into constants so they don't exceed the 80-char threshold when inlined.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions