Rustfmt: O Formatador Oficial de Código Rust

Guia completo do Rustfmt, o formatador oficial do Rust. Aprenda sobre configuração via rustfmt.toml, integração com editores, CI/CD e melhores práticas de formatação.

O Rustfmt é o formatador oficial de código Rust, responsável por aplicar um estilo consistente a todo o código de forma automática. Ele elimina debates sobre estilo de código em equipes, garante que diffs em pull requests mostrem apenas mudanças reais de lógica, e permite que desenvolvedores foquem na resolução de problemas em vez de formatação.

Assim como o gofmt revolucionou a comunidade Go ao eliminar guerras de estilo, o Rustfmt faz o mesmo pelo ecossistema Rust. A grande maioria dos projetos Rust open-source utiliza o Rustfmt com as configurações padrão, criando uma experiência de leitura consistente em toda a comunidade.

Neste guia, vamos explorar como configurar o Rustfmt, personalizar suas opções, integrá-lo com editores e pipelines de CI/CD, e estabelecer boas práticas para seu projeto.

Instalação

O Rustfmt geralmente já vem instalado com o Rust. Se não estiver:

# Instalar Rustfmt via rustup
rustup component add rustfmt

# Verificar instalação
rustfmt --version
# rustfmt 1.7.0-stable (aedd173a2 2024-03-17)

# Também disponível via cargo
cargo fmt --version

Uso Básico

Formatando Código

# Formatar todo o projeto
cargo fmt

# Formatar todo o workspace
cargo fmt --all

# Verificar se o código está formatado (sem modificar)
cargo fmt -- --check
cargo fmt --all -- --check

# Formatar um arquivo específico
rustfmt src/main.rs
rustfmt src/lib.rs src/utils.rs

# Mostrar diferenças em vez de aplicar
cargo fmt -- --check --color always

# Formatar com output detalhado
cargo fmt -- --verbose

# Formatar e mostrar o que mudou
rustfmt --emit files src/main.rs
rustfmt --emit stdout src/main.rs  # imprime para stdout

Exemplo de Formatação

Antes do Rustfmt:

use std::collections::HashMap;use std::io::{self,Read,Write};
fn main(){let mut mapa=HashMap::new();
mapa.insert("chave",42);mapa.insert("outra",100);
for(k,v) in &mapa{
println!("{}: {}",k,v);
}
if mapa.len()>1{println!("Múltiplas entradas");}else{println!("Uma entrada");}
let resultado=match mapa.get("chave"){Some(v)=>v*2,None=>0,};
println!("Resultado: {}",resultado);
}

Depois do Rustfmt:

use std::collections::HashMap;
use std::io::{self, Read, Write};

fn main() {
    let mut mapa = HashMap::new();
    mapa.insert("chave", 42);
    mapa.insert("outra", 100);
    for (k, v) in &mapa {
        println!("{}: {}", k, v);
    }
    if mapa.len() > 1 {
        println!("Múltiplas entradas");
    } else {
        println!("Uma entrada");
    }
    let resultado = match mapa.get("chave") {
        Some(v) => v * 2,
        None => 0,
    };
    println!("Resultado: {}", resultado);
}

Suprimindo Formatação

Às vezes, a formatação manual é mais legível:

// Pular um bloco de código
#[rustfmt::skip]
fn matriz_transformacao() -> [[f64; 4]; 4] {
    [
        [1.0, 0.0, 0.0, 0.0],
        [0.0, 1.0, 0.0, 0.0],
        [0.0, 0.0, 1.0, 0.0],
        [0.0, 0.0, 0.0, 1.0],
    ]
}

// Pular formatação em um módulo inteiro
#[rustfmt::skip]
mod tabelas_lookup {
    pub const CORES: &[&str] = &[
        "vermelho",  "azul",     "verde",
        "amarelo",   "laranja",  "roxo",
        "branco",    "preto",    "cinza",
    ];
}

// Pular um único item
#[rustfmt::skip]
let flags = FLAG_A | FLAG_B
          | FLAG_C | FLAG_D
          | FLAG_E;

// Pular formatação de macro
#[rustfmt::skip]
macro_rules! minha_macro {
    ($($x:expr),*) => {
        vec![$($x),*]
    };
}

Recursos Avançados

Configuração com rustfmt.toml

Crie um arquivo rustfmt.toml ou .rustfmt.toml na raiz do projeto:

# rustfmt.toml - Configuração completa

# === Indentação e Largura ===

# Largura máxima da linha (padrão: 100)
max_width = 100

# Largura de indentação em espaços (padrão: 4)
tab_spaces = 4

# Usar hard tabs em vez de espaços (padrão: false)
hard_tabs = false

# === Imports ===

# Agrupamento de imports (padrão: Preserve)
# StdExternalCrate: separa std, external, crate
# One: agrupa tudo
# Preserve: mantém como está
group_imports = "StdExternalCrate"

# Mesclar imports do mesmo módulo (padrão: Preserve)
imports_granularity = "Crate"

# === Estilo de Código ===

# Edição do Rust (padrão: 2015)
edition = "2021"

# Posição da chave de abertura
# SameLineWhere: mesma linha, exceto com where
# AlwaysNextLine: sempre na próxima linha
brace_style = "SameLineWhere"

# Posição do else
# Same: else na mesma linha do }
# NextLine: else na próxima linha
control_brace_style = "AlwaysSameLine"

# === Comentários ===

# Normalizar comentários (padrão: false)
normalize_comments = false

# Normalizar doc comments (padrão: false)
normalize_doc_attributes = false

# Envolver comentários para caber na largura máxima (padrão: false)
wrap_comments = false

# === Funções e Chamadas ===

# Largura máxima para argumentos de função em uma linha
fn_args_layout = "Tall"

# Chamadas de função em uma única linha se couberem
fn_single_line = false

# === Structs e Enums ===

# Vírgula final em structs (padrão: Always)
trailing_comma = "Vertical"

# Vírgula final em argumentos de macro
# trailing_comma_macro = "Always"

# === Match ===

# Braço de match em uma linha se couber (padrão: true)
match_arm_blocks = true

# Alinhar braços de match com => (instável)
# match_arm_leading_pipes = "Always"

# === Strings ===

# Formatar strings literais (padrão: false)
format_strings = false

# === Macros ===

# Formatar corpo de macros (padrão: true)
format_macro_matchers = false

# Formatar macros declarativas (padrão: true)
format_macro_bodies = true

# === Use declarations ===

# Reordenar imports (padrão: true)
reorder_imports = true

# Reordenar módulos (padrão: true)
reorder_modules = true

# === Misc ===

# Forçar multilinha para where clauses (padrão: false)
where_single_line = false

# Usar field init shorthand (padrão: false)
use_field_init_shorthand = true

# Usar try! vs ? (padrão: false)
use_try_shorthand = true

# Quebrar strings longas
format_strings = false

# Número de espaços antes de : em struct literal
space_before_colon = false
space_after_colon = true

Configurações Populares por Estilo

Estilo Minimalista (menos mudanças):

# rustfmt.toml - Minimalista
max_width = 100
tab_spaces = 4
edition = "2021"

Estilo Detalhado (projeto empresarial):

# rustfmt.toml - Empresarial
max_width = 100
tab_spaces = 4
edition = "2021"
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
reorder_imports = true
reorder_modules = true
use_field_init_shorthand = true
use_try_shorthand = true
trailing_comma = "Vertical"

Estilo Compacto (linhas curtas):

# rustfmt.toml - Compacto
max_width = 80
tab_spaces = 2
edition = "2021"
fn_single_line = true
where_single_line = true

Configurações Instáveis (Nightly)

Algumas opções requerem a toolchain nightly:

# rustfmt.toml com opções instáveis
# Requer: cargo +nightly fmt

# Estabilidade
unstable_features = true

# Estilo de import
imports_granularity = "Module"

# Agrupar e reordenar imports
group_imports = "StdExternalCrate"

# Formatar doc comments
format_code_in_doc_comments = true

# Formatar strings
format_strings = true

# Alinhamento de campos de struct
struct_field_align_threshold = 20

# Estilo de enum
enum_discrim_align_threshold = 20

# Cor na saída
color = "Auto"

# Número máximo de linhas em branco consecutivas
blank_lines_upper_bound = 2
blank_lines_lower_bound = 0

# Estilo de closures
closure_block_indent_threshold = 5

Para usar opções instáveis:

# Formatar com nightly
cargo +nightly fmt

# Verificar com nightly
cargo +nightly fmt -- --check

Integração com Editores

VS Code

// .vscode/settings.json
{
    // Formatar ao salvar
    "editor.formatOnSave": true,

    // Usar rustfmt como formatador
    "[rust]": {
        "editor.defaultFormatter": "rust-lang.rust-analyzer",
        "editor.formatOnSave": true,
        "editor.formatOnPaste": true,
        "editor.formatOnType": true
    },

    // Configurações do rust-analyzer
    "rust-analyzer.rustfmt.extraArgs": [
        "--config",
        "max_width=100"
    ],

    // Usar nightly rustfmt (para opções instáveis)
    "rust-analyzer.rustfmt.overrideCommand": [
        "rustup", "run", "nightly", "--",
        "rustfmt", "--edition", "2021"
    ]
}

Neovim (com rust-analyzer)

-- init.lua ou lua/plugins/lsp.lua
local lspconfig = require('lspconfig')

lspconfig.rust_analyzer.setup({
    settings = {
        ['rust-analyzer'] = {
            rustfmt = {
                -- Usar nightly rustfmt
                overrideCommand = {
                    "rustup", "run", "nightly", "--",
                    "rustfmt", "--edition", "2021", "--"
                },
                extraArgs = { "--config", "max_width=100" },
            },
        },
    },
})

-- Formatar ao salvar
vim.api.nvim_create_autocmd("BufWritePre", {
    pattern = "*.rs",
    callback = function()
        vim.lsp.buf.format({ async = false })
    end,
})

Vim (com vim-rustfmt)

" .vimrc
let g:rustfmt_autosave = 1
let g:rustfmt_emit_files = 1
let g:rustfmt_fail_silently = 0
let g:rustfmt_options = '--edition 2021'

" Atalho manual
nnoremap <leader>rf :RustFmt<CR>

IntelliJ / CLion

Configurações > Languages & Frameworks > Rust > Rustfmt
- Ativar "Run rustfmt on save"
- Ativar "Use rustfmt instead of built-in formatter"
- Adicionar caminho para rustfmt se necessário

Helix

# ~/.config/helix/languages.toml
[[language]]
name = "rust"
auto-format = true
formatter = { command = "rustfmt", args = ["--edition", "2021"] }

Boas Práticas

1. Use Configuração Padrão Sempre Que Possível

# rustfmt.toml - O mínimo necessário
edition = "2021"
# A maioria dos projetos Rust open-source usa as configurações padrão.
# Menos configuração = menos surpresas para contribuidores.

2. Configure Formatação no CI

# .github/workflows/fmt.yml
name: Formatação

on: [push, pull_request]

jobs:
  fmt:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - run: cargo fmt --all -- --check

3. Use Pre-commit Hooks

#!/bin/bash
# .git/hooks/pre-commit

echo "Verificando formatação..."
cargo fmt --all -- --check
if [ $? -ne 0 ]; then
    echo ""
    echo "Código não formatado! Execute 'cargo fmt' antes de commitar."
    exit 1
fi

4. Documente Exceções com #[rustfmt::skip]

// Sempre explique por que está pulando formatação
#[rustfmt::skip]
// Manter formatação manual para alinhar colunas da tabela de lookup
const TABELA: &[(u8, &str, f64)] = &[
    (0x00, "nulo",      0.0),
    (0x01, "inicio",    1.0),
    (0x02, "fim",       2.0),
    (0xFF, "especial", 99.9),
];

5. Organize Imports Consistentemente

# rustfmt.toml
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
reorder_imports = true

Resultado:

// Imports da biblioteca padrão
use std::collections::HashMap;
use std::io::{self, Read, Write};

// Imports de crates externas
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;

// Imports do próprio crate
use crate::config::Config;
use crate::models::{Usuario, Produto};

Exemplos Práticos

Exemplo 1: Configuração Completa para Equipe

# rustfmt.toml - Configuração de equipe
edition = "2021"
max_width = 100
tab_spaces = 4
hard_tabs = false

# Imports organizados
reorder_imports = true
reorder_modules = true
group_imports = "StdExternalCrate"
imports_granularity = "Crate"

# Estilo de código
use_field_init_shorthand = true
use_try_shorthand = true
trailing_comma = "Vertical"

# Chaves
brace_style = "SameLineWhere"
control_brace_style = "AlwaysSameLine"

Exemplo 2: Antes e Depois - Código Real

Antes da formatação:

use serde::{Serialize,Deserialize};
use std::collections::HashMap;
use anyhow::Result;
use std::fs;

#[derive(Debug,Serialize,Deserialize)]
struct Config{
    nome:String,
    porta:u16,
    hosts:Vec<String>,
    opcoes:HashMap<String,String>,
}

impl Config{
    fn carregar(caminho:&str)->Result<Self>{
        let conteudo=fs::read_to_string(caminho)?;
        let config:Config=serde_json::from_str(&conteudo)?;
        Ok(config)
    }

    fn validar(&self)->Result<()>{
        if self.nome.is_empty(){anyhow::bail!("Nome vazio")}
        if self.porta==0{anyhow::bail!("Porta inválida")}
        if self.hosts.is_empty(){anyhow::bail!("Sem hosts")}
        Ok(())
    }

    fn hosts_formatados(&self)->String{
        self.hosts.iter().enumerate().map(|(i,h)|format!("{}. {}",i+1,h)).collect::<Vec<_>>().join("\n")
    }
}

Depois do cargo fmt:

use std::collections::HashMap;
use std::fs;

use anyhow::Result;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Config {
    nome: String,
    porta: u16,
    hosts: Vec<String>,
    opcoes: HashMap<String, String>,
}

impl Config {
    fn carregar(caminho: &str) -> Result<Self> {
        let conteudo = fs::read_to_string(caminho)?;
        let config: Config = serde_json::from_str(&conteudo)?;
        Ok(config)
    }

    fn validar(&self) -> Result<()> {
        if self.nome.is_empty() {
            anyhow::bail!("Nome vazio")
        }
        if self.porta == 0 {
            anyhow::bail!("Porta inválida")
        }
        if self.hosts.is_empty() {
            anyhow::bail!("Sem hosts")
        }
        Ok(())
    }

    fn hosts_formatados(&self) -> String {
        self.hosts
            .iter()
            .enumerate()
            .map(|(i, h)| format!("{}. {}", i + 1, h))
            .collect::<Vec<_>>()
            .join("\n")
    }
}

Exemplo 3: Script de Verificação Completo

#!/bin/bash
# scripts/verificar-codigo.sh
# Verificação completa de qualidade de código

set -euo pipefail

echo "=== Verificando formatação ==="
if ! cargo fmt --all -- --check; then
    echo "FALHOU: Código não formatado. Execute 'cargo fmt --all'"
    exit 1
fi
echo "OK: Formatação correta"

echo ""
echo "=== Executando Clippy ==="
if ! cargo clippy --workspace --all-targets --all-features -- -D warnings; then
    echo "FALHOU: Clippy encontrou problemas"
    exit 1
fi
echo "OK: Clippy aprovado"

echo ""
echo "=== Executando testes ==="
if ! cargo test --workspace --all-features; then
    echo "FALHOU: Testes falharam"
    exit 1
fi
echo "OK: Todos os testes passaram"

echo ""
echo "=== Verificação de documentação ==="
if ! RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features; then
    echo "FALHOU: Documentação com problemas"
    exit 1
fi
echo "OK: Documentação gerada com sucesso"

echo ""
echo "=== Todas as verificações passaram! ==="

Exemplo 4: Makefile com Formatação

# Makefile
.PHONY: fmt fmt-check lint test check all

# Formatar código
fmt:
	cargo fmt --all

# Verificar formatação
fmt-check:
	cargo fmt --all -- --check

# Linting
lint:
	cargo clippy --workspace --all-targets --all-features -- -D warnings

# Testes
test:
	cargo test --workspace --all-features

# Verificação rápida (sem testes)
check: fmt-check lint
	cargo check --workspace --all-features

# Verificação completa
all: fmt-check lint test
	@echo "Todas as verificações passaram!"

Comparação com Alternativas

CaracterísticaRustfmtPrettier (JS)Black (Python)gofmt (Go)clang-format (C++)
OficialSimNãoNãoSimSim
ConfigurávelModeradoPoucoMínimoNenhumMuito
OpinionadoModeradoSimMuitoMuitoNão
EstávelSimSimSimSimSim
VelocidadeRápidoMédioRápidoMuito rápidoRápido
Editor supportExcelenteExcelenteExcelenteExcelenteBom
Opções instáveisVia nightlyN/AN/AN/AN/A

O Rustfmt ocupa um meio-termo saudável entre a rigidez total do gofmt (zero opções) e a flexibilidade excessiva do clang-format (centenas de opções). Ele oferece opções suficientes para acomodar preferências de equipe sem permitir configurações que resultem em código inconsistente entre projetos.

Conclusão

O Rustfmt é uma ferramenta essencial que todo projeto Rust deveria adotar desde o início. Ao automatizar a formatação de código, ele elimina discussões improdutivas sobre estilo e permite que a equipe foque no que realmente importa: a lógica e a arquitetura do software.

Pontos-chave para lembrar:

  • Use as configurações padrão sempre que possível para maximizar consistência com a comunidade
  • Configure o CI para verificar formatação com cargo fmt -- --check
  • Integre com seu editor para formatar ao salvar
  • Use #[rustfmt::skip] com parcimônia e sempre documente o motivo
  • group_imports e imports_granularity são as personalizações mais impactantes
  • Pre-commit hooks evitam commits com formatação incorreta

Para referência completa de todas as opções, consulte a documentação oficial do Rustfmt.

No próximo passo, conheça o Serde para serialização e desserialização de dados em Rust.