KINTO Tech Blog
Flutter

analyzer のパッチ番号 1 違いで止まった Flutter ビルド:retrofit と custom_lint の依存デッドロック解決記

Cover Image for analyzer のパッチ番号 1 違いで止まった Flutter ビルド:retrofit と custom_lint の依存デッドロック解決記

Flutter SDK 3.29 → 3.38 へのアップグレード中に遭遇した retrofit / analyzer / custom_lint の依存衝突を解いた記録です。
「なぜ pub solver が答えを見つけられないのか」から順を追って説明します。


はじめに

はじめまして、KINTOテクノロジーズ(KTC)でモバイルアプリ(Flutter)の開発を担当しているHand-Tomiです。

Flutter SDK のメジャーアップグレードを進めていたある日、dart pub get が突然失敗するようになりました。エラーメッセージを読み解くと、retrofit_generatorcustom_lint がそれぞれ別々の analyzer バージョンを要求していて、両者が要求する analyzer のバージョン差はわずか 1 パッチ。けれど pub solver ではどうやっても解けない デッドロック でした。

本記事では、その原因と解決方法、そしてなぜ dependency_overrides が罠になるのかを順を追って解説します。同じ Flutter プロジェクトで似た衝突に遭遇した方の参考になれば幸いです。


TL;DR

  • Flutter のメジャーアップグレード中に dart pub get が失敗。原因は retrofit_generatorcustom_lint_visitor が同じ analyzer に対して別々のバージョンを要求していたこと。
  • 最終的な解:retrofit: ^4.9.2 + retrofit_generator: ^10.2.1pubspec のピン 2 行ですっきり解決します。
  • dependency_overrides には罠があり、推奨しません。pub get は通っても dart_style が知らぬ間に昇格してビルドが壊れます。
  • この衝突は 構造的な問題 です。analyzer のメジャーが上がるたびに再発します。

1. 始まり — 止まってしまったビルド

Flutter SDK 3.29.2 から 3.38.10 へのメジャーアップグレードを進めていました。flutter_riverpod 2 → 3、freezed 2 → 3、analyzer 6 → 8 といった大きな変更が立て続けに来ていて、いつもなら flutter upgrade のあと dart pub get で済む作業のはずでした。

ところがビルドが止まりました。要点だけ抜き出すと、こういうメッセージです。

And because retrofit_generator >=10.2.4 depends on analyzer >=8.4.1 <13.0.0
and custom_lint_core >=0.7.0 depends on custom_lint_visitor ^1.0.0,
if retrofit_generator >=10.2.4 and custom_lint_core >=0.7.0 then analyzer 9.0.0.

And because custom_lint >=0.8.1 depends on both analyzer ^8.0.0 and custom_lint_core 0.8.1,
custom_lint >=0.8.1 is incompatible with retrofit_generator >=10.2.4.

So, because app depends on both retrofit_generator ^10.2.5 and custom_lint ^0.8.1,
version solving failed.

pub solver が答えを見つけられなかったのです。片方を上げればもう片方が壊れ、下げればまた別のところが壊れる。普通のバージョン衝突ではなく、デッドロック でした。


2. 誰と誰が戦っているのか

主な登場人物は次のとおりです。

  • analyzer — Dart コードの静的解析エンジン(共有資源)
  • retrofit_generator.g.dart を生成するコードジェネレータ
  • custom_lint / custom_lint_core / custom_lint_builder — lint プラグインのランナーと、その builder
  • custom_lint_visitor — analyzer の AST を訪問する visitor 実装

依存関係図 — analyzer を真ん中に置き、retrofit_generator 陣営と custom_lint_visitor 陣営が両側から引っ張り合う構造

問題の核心は、analyzer という共有資源 です。両陣営が同じ analyzer に対して別々のバージョンを要求しています。

  • retrofit_generator 10.2.3+ → 「analyzer 8.4.1 以上が必要」
  • custom_lint_visitor 1.0.0+8.4.0 → 「analyzer 8.4.0 ちょうど」

差は 8.4.0 と 8.4.1、わずか 1 パッチ。これだけでビルドが止まるのです。

なお、1 節のエラーメッセージ末尾には analyzer 9.0.0 も登場しますが、これは custom_lint_visitor1.0.0+8.4.0 のほかに 1.0.0+9.0.0 ビルドも存在し、custom_lint_visitor: ^1.0.0 を介した solver が両方を順に試した結果です。どちらも analyzer をバージョン固定で要求する点は同じなので、本質的な対立点は変わりません。


3. なぜ 1 パッチ差で壊れるのか

ここで 2 つの事実が噛み合います。

事実 1. analyzer内部 API はパッチリリースでも変わる

SemVer の約束は「パッチリリースでは 公開 API は後方互換 を保つ」です。ところが custom_lint_visitor が使っているのは analyzer の公開 API ではなく、内部 API(AST ノードの型など、パッケージの内部実装に属するもの)です。SemVer の保護範囲外なので、メジャー・パッチを問わず、型が消えたり、シグネチャが変わったりするのは珍しくありません。

後ほど引用するメンテナ自身の言葉を借りれば "some more unique APIs" — SemVer の通常のセーフティネットの外側で扱う必要のある API です。本記事の 5 節で扱う dart_style 3.1.9LabelReference / NamedArgument 欠落も、「内部 API は SemVer 保護外」という同じ構造から生じる事例の 1 つです(こちらは analyzer のメジャー間で起きたケースで、3 節でいうパッチ単位の例ではありません)。

事実 2. custom_lint_visitor はそれゆえ バージョンを完全に固定 する

これを知っているからこそ、custom_lint_visitor のメンテナは意図的に analyzer を完全に固定しています。パッケージ名そのものがその証拠です。

custom_lint_visitor 1.0.0+8.4.0
                          ^^^^^
                          analyzer のバージョン

pubspec.yaml の中でも analyzer: 8.4.0^(caret) なしのバージョン固定)になっています。

これがミスならば PR 一本で解決する話ですが、これは関連する GitHub issue でメンテナ自身が明言した 意図的な方針 です。

"Custom_lint depends on some more unique APIs. I'll probably stick to requiring 8.0 for it."
invertase/dart_custom_lint#345

発言の直接の意図は「メジャー(8.0)単位で範囲を狭めて require する」ですが、その方針が実際のリリースにも反映されており、リリースされる custom_lint_visitor の各バージョンでは analyzer: 8.4.0 のように バージョンが完全に固定 されています(1.0.0+8.4.0analyzer: 8.4.0^(caret) なし)。つまり 意図された決定 の結果としてバージョン固定が生まれており、両者が同じ analyzer バージョンを要求するビルドが揃うまでは pub solver だけでは解けません。


4. 効果のなかった試みリスト

問題が難しく見えると、人は迂回路を探したくなります。しかし直感的に思いつく次の試みはどれも徒労でした。

試み なぜ失敗するのか
retrofit_generator を最新(10.2.5)に上げる analyzer 8.4.1 を要求するため custom_lint_visitor と衝突
retrofit の上限を狭めてみる(<4.9.1 generator 10.2.1 を選びたい動機は 6 節で詳述しますが、retrofit を 4.9.0 系に下げると今度は generator 10.2.1 のソースが retrofit 4.9.2 の新 enum 値(Parser.DartMappable)を参照しているため、generator 自体の AOT コンパイルが Member not found で失敗します
custom_lint_builder のダウングレード analyzer のメジャーが 7.x まで引きずり下ろされ、今度は retrofit_generator(analyzer 8.x 依存)と別の衝突を起こす — freezed / riverpod など analyzer 8 に依存するパッケージがある環境でも同様
analysis_options.yaml の lint を切る dependency_overrides で solver を通した後でも)lint を切って exclude: '**/*.g.dart' を両方適用しても、generator AOT 段階で発生する Member not found 系のコンパイルエラーはそのまま発生する
dependency_overrides で強制固定 pub get は通るがビルド段階で dart_style が壊れる(5 節を参照)

特に最後の項目、dependency_overrides は罠が深いので、別途取り上げる価値があります。

上の表の各行は、本記事と同じリポジトリの検証成果物(reports/01-reproduction.mdreports/03-overrides-fallback.md)で実際のコマンド出力として再現されています。


5. dependency_overrides という罠

最初の発想は単純です。「2 つのパッケージが争うなら、こちらで強制的に片方のバージョンを打ち込もう」。

dependency_overrides:
  retrofit: ^4.9.2
  retrofit_generator: ^10.2.5
  analyzer: ^8.4.1

驚くことに dart pub get は通ります。なぜなら dependency_overridesオーバーライドした依存に対する他パッケージからの制約を黙らせ、solver の選択肢を広げるからです。

ところが dart run build_runner build の段階で、突然ビルドが壊れます。

Failed to build build_runner:build_runner:
  .../dart_style-3.1.9/lib/src/front_end/ast_node_visitor.dart:1279:28:
    Error: Type 'LabelReference' not found.

dart_style です。私たちが明示的に依存もしていないパッケージです。

理由を辿ってみると、次のようになっています。

サイレント昇格の因果連鎖 — override が制約を黙らせる → solver が自由 → 最新 dart_style 3.1.9 を自動選択 → analyzer 13 の AST 型を参照 → 強制された  系にないのでコンパイル失敗

  1. dependency_overrides がオーバーライドした依存(retrofitretrofit_generatoranalyzer)に対する他パッケージからの制約を黙らせ、solver の選択肢が広がる
  2. その結果、solver は transitive で dart_style の最新版(3.1.9)を自動的に選ぶ
  3. dart_style 3.1.9analyzer の最新メジャーで導入された AST 型(LabelReferenceNamedArgumentBlockEnumBody など)を参照している
  4. しかし私たちは override で analyzer ^8.4.1(解決範囲は >=8.4.1 <9.0.0)を強制している
  5. → その範囲には存在しない型を参照しようとしてコンパイル失敗

要するに dependency_overrides は制約を黙らせるだけで、互換性を保証しません。 一箇所を押さえるとまた別の場所から噴き出します。これを抑え込もうとすると dart_style もピン、custom_lint_visitor も確認…… と際限なく増えていきます。


6. 結局解けた方法 — シンプルなピン調整 2 行

問題を逆から見ると答えが見えます。

  • 私たちが変えられないもの:custom_lint_visitor 1.0.0+8.4.0analyzer 8.4.0(正確には custom_lint_visitor 自体は 1.0.0+9.0.0 ビルドも存在しますが、それを選ぶと custom_lint 本体が要求する analyzer ^8.0.0 と衝突するため、custom_lint を使う限り 8.4.0 ピン側に寄せるしかありません。1 節のエラーメッセージにも custom_lint >=0.8.1 depends on ... analyzer ^8.0.0 として現れています)
  • 私たちが変えられるもの:retrofit_generator のバージョン

であれば「analyzer 8.4.0 でも動く最新の retrofit_generator」を探せばよいわけです。

retrofit_generator のバージョン別要求を表にまとめると:

retrofit_generator バージョン × analyzer 要求範囲のマトリクス — retrofit 4.9.2 互換 ∩ analyzer 8.4.0 互換の交点は 10.2.1 のみ

retrofit_generator analyzer 要求 logError の呼び出し形式
10.2.0 >=7.7.1 <10.0.0 positional 4 個
10.2.1 >=8.0.0 <10.0.0 named (response: _result)
10.2.3 >=8.4.1 <11.0.0 named
10.2.4 / 10.2.5 >=8.4.1 <13.0.0 named

補足: retrofit.dart は monorepo で、retrofit_generator(タグ v10.x.x)と retrofit(タグ retrofit-vX.Y.Z)を別系統で管理しています。本記事のリンクで prefix が混在するのはそのためです。なお 10.2.2 はリリースが存在しますが、本記事の議論には影響しないため上の表では省略しています。

答えが見えます。10.2.1 です。

  • analyzer 8.4.0 と互換 ✓(>=8.0.0 なので)
  • retrofit 4.9.2{Response? response} named optional シグネチャと互換 ✓
  • dependency_overrides 不要 ✓
pubspec.yaml
dependencies:
  retrofit: ^4.9.2

dev_dependencies:
  retrofit_generator: ^10.2.1

これだけです。^10.2.1 というキャレット範囲を書いても、10.2.3+ は analyzer 8.4.1 を要求してくるので自動的に候補から外れ、実効的に 10.2.1 が選ばれます。

ちなみに retrofit_generator 10.2.110.2.5logError の呼び出しシグネチャは同一 です。generator のソース(lib/src/generator.dart)を直接比較しても、両バージョンとも '$_errorLoggerVar?.logError(e, s, $_optionsVar, response: $_resultVar);' という同一の出力テンプレートを使っています(v10.2.1#L3777 / v10.2.5#L3849)。10.2.5 には Stream<Uint8List> / Stream<String> 処理の検証など別の機能が追加されていますが、本記事が扱う retrofit ↔ analyzer インターフェイスそのものは変更されていません。つまり 10.2.1 に留まることは、コア機能面で損ではありません。


7. それで私たちが学んだこと

この件が片付いたとき、最初に浮かんだ考えは 「次のメジャーアップグレードでまた出くわすだろうな」 でした。

理由は 2 つです。

  1. analyzer の内部 API は今後もパッチで変わり続ける。 それが AST を扱う解析器パッケージの本質です。
  2. custom_lint_visitor は今後もバージョンを完全に固定し続ける。 メンテナが意図的に取っている方針だからです。

つまりこの衝突は 構造的 です。本記事を書いている 2026 年春の時点で、analyzer はすでに 13.0.0 までリリースされており、custom_lint_visitor のピンラインは 1.0.0+9.0.0 までしか追いついていません。

analyzer のメジャーリリースのタイムライン(8 → 9 → 10 → 11 → 12 → 13)の上に custom_lint_visitor のピン(1.0.0+8.4.0、1.0.0+9.0.0)を重ねた図 — custom_lint_visitor がまばらに追従するパターン

custom_lint_visitor がメジャーごとに 1 〜 2 個のビルドだけ追いつくこのまばらなパターンが続く限り、analyzer がさらに一段上がるたびに同じ形で再発します。実際、retrofit.dart の issue tracker を見ると analyzer 10.0 の段階でも同じシグネチャミスマッチが報告されています。

であれば、私たちにできることは:

  • 自然な解決を先に試す。 ピン 1 つの調整で解けるかをまず確認する。シンプルな答えがあるのに dependency_overrides を最初に持ち出さない。
  • dependency_overrides は最後の手段。 黙らせるだけでは解決にならない。一箇所を押さえると別の場所から噴き出す。
  • プレイブックを残す。 次の人(あるいは 6 か月後の自分)が同じ罠にはまらないように。メカニズムと意思決定ツリーを一緒に書き残しておく(本記事自体がそのプレイブックの 1 つです)。

最後に

ここまで読んでいただき、ありがとうございます。

analyzer のような共有依存をめぐる衝突は、一見すると「2 つのパッケージのバグ」に見えますが、実際にはエコシステム側の構造的な制約が背景にあります。同じ罠に出会ったときに「最初に何を疑い、何を試し、どこで止まるか」を整理できれば、次は数時間で解けるはずです。

皆さんの参考になれば幸いです。


参考

Facebook

関連記事 | Related Posts