1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust で TOML フォーマッターを作った — ソート・整列・CI チェックモード付き

1
Last updated at Posted at 2026-05-04

きっかけ

TOML ファイルはエントロピーが溜まる。コントリビューターごとにスタイルの好みが違う。キーをソートする人もしない人もいる。= を揃える人もいれば、セクション間に無意味な空行を入れる人もいる。時間が経つと Cargo.tomlpyproject.toml の一貫性が崩れ、diff がノイジーになりレビューが辛くなる。

toml-fmt はこの問題を解決するシングルバイナリの Rust CLI。TOML ファイルをパースし、キーソート・クォート正規化・スペース統一を施して出力する。--check モードはフォーマットが必要な場合に exit code 1 を返すので CI ゲートにぴったり。

🔗 GitHub: https://github.com/sen-ltd/toml-fmt

Screenshot

3つの動作モード

# stdout にフォーマット結果を出力
toml-fmt Cargo.toml

# ファイルを直接書き換え
toml-fmt --in-place Cargo.toml

# CI モード: 変更が必要なら exit 1
toml-fmt --check Cargo.toml

アーキテクチャ

lib.rs に純粋なフォーマットロジック、main.rs に CLI シェル。コア関数のシグネチャ:

pub fn format_toml(input: &str, opts: &Options) -> Result<String, Error>

文字列を受けて文字列を返す。ファイル I/O もサイドエフェクトもない。Options はソートと整列の2つのフラグを持つ。

toml クレートによるパース

toml::Value がパース済みの木構造を提供する。パースは input.parse::<toml::Value>() の1行。マルチライン文字列、インラインテーブル、ドットキー、テーブル配列など TOML スペックのエッジケースはクレートが処理してくれる。

トレードオフとして toml::Value はコメントを保持しない。コメントが重要なファイルには taplotoml_edit を使うべき。toml-fmt は決定論的なソート出力が欲しいプロジェクト向け。

出力アルゴリズム

フォーマッターはパース済みの木を再帰的に走査し、各テーブルレベルで4種類のコンテンツを処理する。

  1. 単純なキーバリューペア — 文字列、整数、配列等
  2. サブテーブル[section.subsection]
  3. テーブル配列[[section]]
  4. インラインテーブル{key = val, ...}

単純ペアが先、次にサブテーブル、最後にテーブル配列。各セクションのキーがヘッダ直後に来る予測可能な出力になる。

キーソート

sort_keys(デフォルト有効)で各テーブル内のキーをアルファベット順にソートする。これが最もインパクトのあるルール。ソートされていれば依存関係の追加が diff の1行で済み、ランダムな位置への挿入にならない。

= の整列

--align フラグでセクション内の = を揃える。パディングはセクションごとにリセットされるので、あるセクションの長いキーが別セクションのパディングに影響しない。

整列前:

[package]
name = "my-app"
version = "0.1.0"
description = "A longer description"
edition = "2021"

整列後:

[package]
description = "A longer description"
edition     = "2021"
name        = "my-app"
version     = "0.1.0"

キーのフォーマット

TOML はベアキー(name)とクォートキー("key with spaces")の2種類を許す。フォーマッターは可能な限りベアキーを使い、A-Za-z0-9_- 以外の文字を含む場合のみクォートする。

値のフォーマット

各型にルールがある。文字列は常にダブルクォート、整数はそのまま、浮動小数点は小数点を保証(11.0)、naninf-inf は TOML のリテラル。配列は1行、インラインテーブルはキーソート済みで出力。

テスト

24件のテスト。基本フォーマット、ソート、整列、ネスト構造、エッジケース(空ドキュメント、空配列、空文字列、クォートキー)、文字列エスケープ、冪等性、エラー処理、実際の Cargo.toml 構造。

冪等性テストはフォーマッターにとって必須:

#[test]
fn idempotent() {
    let input = "[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[dependencies]\nclap = \"4\"\n";
    let first = fmt(input);
    let second = fmt(&first);
    assert_eq!(first, second, "formatting should be idempotent");
}

フォーマット結果を再フォーマットして変わったら壊れている。ユーザーが何回実行しても同じ結果にならなければ、CI チェックが不安定になる。

リリースプロファイル

[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"
panic = "abort"

サイズ最適化でコンパクトなバイナリを生成。Docker イメージや CI 環境に適している。

CI 連携

# GitHub Actions
- name: Check TOML formatting
  run: |
    cargo install --path .
    toml-fmt --check Cargo.toml

exit code の規約(0 = フォーマット済み、1 = 変更が必要、2 = エラー)は rustfmtblack と同じパターン。既存のワークフローに組み込みやすい。

制限とトレードオフ

コメント非保持。 toml クレートの Value 型がパース時にコメントを捨てる。コメント保持が必要なら taplo を使うべき。

インラインテーブルの展開。 dep = { version = "1" } もフルセクションとして出力される。

部分フォーマットなし。 ファイル全体をフォーマットする。

すべて意図的なトレードオフ。小さく、速く、予測可能な出力を目標にした。コメント保持とフォーマット検出を足すとコードベースが約3倍になる。

おわりに

TOML はエッジケースが思った以上に多い。ベアキーとクォートキー、整数と浮動小数点の区別、テーブルとインラインテーブル、テーブル配列。フォーマッターを書くと出力フォーマットを深く理解できる。

toml クレートがパースの重労働をやり、フォーマッターは約200行の出力コード。CI パイプラインに TOML フォーマッターが欲しければ、toml-fmt を試してみてほしい。1バイナリ、設定不要、決定論的な出力。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?