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_generator と custom_lint がそれぞれ別々の analyzer バージョンを要求していて、両者が要求する analyzer のバージョン差はわずか 1 パッチ。けれど pub solver ではどうやっても解けない デッドロック でした。
本記事では、その原因と解決方法、そしてなぜ dependency_overrides が罠になるのかを順を追って解説します。同じ Flutter プロジェクトで似た衝突に遭遇した方の参考になれば幸いです。
TL;DR
- Flutter のメジャーアップグレード中に
dart pub getが失敗。原因はretrofit_generatorとcustom_lint_visitorが同じanalyzerに対して別々のバージョンを要求していたこと。 - 最終的な解:
retrofit: ^4.9.2+retrofit_generator: ^10.2.1。pubspecのピン 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 プラグインのランナーと、その buildercustom_lint_visitor— analyzer の AST を訪問する visitor 実装

問題の核心は、analyzer という共有資源 です。両陣営が同じ analyzer に対して別々のバージョンを要求しています。
retrofit_generator 10.2.3+→ 「analyzer8.4.1 以上が必要」custom_lint_visitor 1.0.0+8.4.0→ 「analyzer8.4.0 ちょうど」
差は 8.4.0 と 8.4.1、わずか 1 パッチ。これだけでビルドが止まるのです。
なお、1 節のエラーメッセージ末尾には analyzer 9.0.0 も登場しますが、これは custom_lint_visitor に 1.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.9 の LabelReference / 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.0 → analyzer: 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.md、reports/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 です。私たちが明示的に依存もしていないパッケージです。
理由を辿ってみると、次のようになっています。

dependency_overridesがオーバーライドした依存(retrofit、retrofit_generator、analyzer)に対する他パッケージからの制約を黙らせ、solver の選択肢が広がる- その結果、solver は transitive で
dart_styleの最新版(3.1.9)を自動的に選ぶ dart_style 3.1.9はanalyzerの最新メジャーで導入された AST 型(LabelReference、NamedArgument、BlockEnumBodyなど)を参照している- しかし私たちは override で
analyzer ^8.4.1(解決範囲は>=8.4.1 <9.0.0)を強制している - → その範囲には存在しない型を参照しようとしてコンパイル失敗
要するに dependency_overrides は制約を黙らせるだけで、互換性を保証しません。 一箇所を押さえるとまた別の場所から噴き出します。これを抑え込もうとすると dart_style もピン、custom_lint_visitor も確認…… と際限なく増えていきます。
6. 結局解けた方法 — シンプルなピン調整 2 行
問題を逆から見ると答えが見えます。
- 私たちが変えられないもの:
custom_lint_visitor 1.0.0+8.4.0→analyzer 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 要求 |
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不要 ✓
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.1と10.2.5のlogErrorの呼び出しシグネチャは同一 です。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 つです。
analyzerの内部 API は今後もパッチで変わり続ける。 それが AST を扱う解析器パッケージの本質です。custom_lint_visitorは今後もバージョンを完全に固定し続ける。 メンテナが意図的に取っている方針だからです。
つまりこの衝突は 構造的 です。本記事を書いている 2026 年春の時点で、analyzer はすでに 13.0.0 までリリースされており、custom_lint_visitor のピンラインは 1.0.0+9.0.0 までしか追いついていません。

custom_lint_visitor がメジャーごとに 1 〜 2 個のビルドだけ追いつくこのまばらなパターンが続く限り、analyzer がさらに一段上がるたびに同じ形で再発します。実際、retrofit.dart の issue tracker を見ると analyzer 10.0 の段階でも同じシグネチャミスマッチが報告されています。
であれば、私たちにできることは:
- 自然な解決を先に試す。 ピン 1 つの調整で解けるかをまず確認する。シンプルな答えがあるのに
dependency_overridesを最初に持ち出さない。 dependency_overridesは最後の手段。 黙らせるだけでは解決にならない。一箇所を押さえると別の場所から噴き出す。- プレイブックを残す。 次の人(あるいは 6 か月後の自分)が同じ罠にはまらないように。メカニズムと意思決定ツリーを一緒に書き残しておく(本記事自体がそのプレイブックの 1 つです)。
最後に
ここまで読んでいただき、ありがとうございます。
analyzer のような共有依存をめぐる衝突は、一見すると「2 つのパッケージのバグ」に見えますが、実際にはエコシステム側の構造的な制約が背景にあります。同じ罠に出会ったときに「最初に何を疑い、何を試し、どこで止まるか」を整理できれば、次は数時間で解けるはずです。
皆さんの参考になれば幸いです。
参考
関連記事 | Related Posts

Want to Load a Local JSON in a Flutter Multi-Package? Here’s How!

Flutter Development Efficiency: A Step-by-Step Guide to Automating Web Previews Using GitHub Actions and Firebase Hosting
Flutter Development: Designing a QR Code Border with CustomPaint and Path

Flutterのマルチパッケージの中でローカルのJSONを読み込みたい!

Flutter開発効率化:GitHub ActionsとFirebase Hostingを用いたWebプレビュー自動化の方法をstep-by-stepでご紹介
Flutter開発: CustomPaintとPathでQRコードの枠線をデザインする



