🔮

Linter 開発こそ Vibe Coding の最も優れた適用例の 1 つである

に公開

はじめに

この記事は Go - Qiita Advent Calendar 2025 (Series 2) の 25 日目の記事です。プログラミング言語のアドベントカレンダーは技術寄りのトピックが多いと思いますが,今回は初めてのアイデア寄りの記事になりそうです。

https://zenn.dev/mpyw/articles/new-relic-go-agent-struggle
https://zenn.dev/mpyw/articles/go-context-feature-flags

1〜2 週間で OSS ライブラリ 5 本,記事 2 本を出してしまいました。

↓ Go テンプレートでカスタマイズ可能な全関数計装コードジェネレータ
https://pkg.go.dev/github.com/mpyw/ctxweaver

↓ Goroutine への context.Context 伝播漏れや New Relic 計装における (*newrelic.Transaction).NewGoroutine() 実行漏れを検出する Linter
https://pkg.go.dev/github.com/mpyw/goroutinectx

zerolog での .Ctx(ctx) 呼び出し漏れを検出する Linter
https://pkg.go.dev/github.com/mpyw/zerologlintctx

*gorm.DB インスタンスの unsafe な再利用 を検出する Linter
https://pkg.go.dev/github.com/mpyw/gormreuse

↓ Feature Flag のボイラープレートをジェネリクスで削減するライブラリ
https://pkg.go.dev/github.com/mpyw/feature

これらの多くは Linter に関するもので,その殆どは Vibe Coding で開発しました。 Go 言語製のライブラリなので,これらについて熱く語る記事を書きたい!と最初は思っていたのですが,「Linter を Vibe Coding で開発した」こと自体が最も語るべき内容では? と考え直して今に至ります。

Vibe Coding と TDD

Vibe Coding とは,AI に雰囲気(vibe)で指示を出してコードを書かせる開発スタイルです。巷では「No Code」や「素人が何でも作れる」みたいな文脈で語られることもありますが,私はそうは思っていません。基礎力を活かして AI のハルシネーションを是正しながら正しい方向に導くスキルは絶対に必要だし,その裏付けとしてこれまで培ってきた経験が腐ることはないと信じています。

そもそも AI の出力は確率的に変化します。モデルの性能が良ければ毎回それっぽい結果に収束するとはいえ,微妙なニュアンスの違いは含まれるし,タスクの難易度によって期待した出力が得られる確率も変動する。AI を使っている時点で「自分がやるにはしんどい物量」「自分が技術的に到達しきれていない領域」を任せることが多く,そのコード出力すべてを理解するには常に困難が付き纏います。

そこで救世主となるのが TDD (Test Driven Development) です。ある入力に対してこういう結果が得られる,というのをあらかじめスタブのテストとして書いておき,後からそれをパスするように作る。高度に一般化して実装するのが人間にとって難しい領域であったとしても,具体例をいくつか挙げることぐらいは人間にとっても着手しやすい。AI のお膳立てのために人間として最低限の仕事をここでしておくのです。

具体例を挙げましょう。 zerologlintctx は「zerolog での .Ctx(ctx) 呼び出し漏れ」を検出する Linter です。以前の記事で解説したように, New Relic の Logs in Context を zerolog で利用するためには,log から始まるメソッドチェーンの中に .Ctx(ctx) が含まれていることが必須です。しかし zerolog の状態遷移は複雑で,.Info(msg).InfoContext(ctx, msg) を出力直前に選ばせるだけの slog に比べると,ルールが複雑になりがちです。

// ❌ Bad: .Ctx(ctx) が呼ばれていない
func handler(ctx context.Context) {
    log.Info().Msg("hello") // want "zerolog call chain missing .Ctx\(ctx\)"
}

// ✅ Good: .Ctx(ctx) がチェーンに含まれている
func handler(ctx context.Context) {
    log.Ctx(ctx).Info().Msg("hello")
}

// ❌ Bad: 再代入を経由しても検出できる
func handler(ctx context.Context) {
    e := log.Info()
    e = e.Str("key", "value")
    e.Msg("hello") // want "zerolog call chain missing .Ctx\(ctx\)"
}

// ❌ Bad: クロージャ経由で参照しても検出できる
func handler(ctx context.Context) {
    e := log.Info()
    func() {
        e.Msg("hello") // want "zerolog call chain missing .Ctx\(ctx\)"
    }()
}

もちろん妥協して「log の直後は .Ctx(ctx) しか呼べない」単純ルールでゴリ押ししてしまえばいい部分もあるかもしれませんが,色々と副作用も大きそうなのであまり乱暴なことはしたくない。丁寧に作り込む価値は高いです。とはいっても SSA のような専門的知識が自身にあるわけでもない…どうしょう?

しかし,こういった具体的なテストケースを, Linter のコードが書かれる前から 機能要件としていくつも挙げておくことはできます。難しいことはよく分からなくても,

「このコードは NG,このコードは OK」

という具体例を出すのは人間でもできます。TDD の強みはここにあります。更に,人間の思考力で網羅しきれないようなエッジケースは AI に

💬 「他にどんなエッジケースが考えられる?網羅的に列挙して」

と聞けば,人間が与えたヒントを元に AI がいくらでも考え出してくれます。量産は AI の得意分野なので困ったら頼りましょう。といっても投げっぱなしではだめで, AI が投げてきたボールをあなたが打ち返す必要があります。「これは要件に合っているか?」「更に漏れはないか?」を人間がチェックするのです。当然ながら,ここにある程度の基礎力は求められます。

なぜ Linter 開発が Vibe Coding に適しているのか

業務コードとの対比

さて Linter の話に戻りますが,なぜ Linter は Vibe Coding に適していると考えられるのでしょうか?業務コードと比較してみましょう。

観点 業務コード Linter
技術的難易度 低〜中
業務固有要件 多い(AI に教え込むコスト高) 少ない(AI は最初から理解している)
運用リスク 金銭的損失に繋がりうる ない(//xxx:ignore で済む)
責任 求められる 取る必要がない

業務コードは,技術的な難易度はそれほど高くなくても,業務固有の要件が発生していることが多く,AI にそれらのナレッジを教え込むコストが高いです。「運用でカバー」 でなんとかなっているような部分全てを文書化するまでには時間がかかりますからね。またサービス運用上のリスクとして金銭的な損失に繋がりうるので,バグの許容度が低い。責任の所在は AI には務まりません。こちらは AI に全任せするというよりは,「AI に補助してもらいながら人間が最終責任を取る」 形になるでしょう。

一方 Linter は,技術的な難易度が高い一方で業務固有の要件は少なめ。広く知れ渡った技術的なベストプラクティスが土台になっている ことが多い上,AI は最初からそれを理解しており,自走的なサイクルを回してもらいやすいです。サービス運用上のリスクもない。開発時にバグを踏んでも,一時的に //xxx:ignore TODO: Linter のバグがあるので修正する 等のコメントをつけるだけで済みます。しくじっても誰にも迷惑をかけません。この分野であれば,普段責任を取ることに怯えるポジションであるプロダクトオーナーも,きっと安心して AI に任せてくれることでしょう。

TDD との相性がいい

先ほど TDD の話をしましたが,Linter 開発は TDD との相性が抜群です。

観点 業務コード Linter
入力 ユーザー操作,API リクエスト,DB 状態… ソースコード(テキスト)
出力 画面表示,レスポンス,副作用… 診断結果(位置 + メッセージ)
外部依存 DB,ネットワーク,ファイルシステム… なし
テストの書きやすさ モック・スタブが必要になりがち 極めて高い

Linter のテストは 「このコードを入力したら,この行でこの警告が出る」 という形式に集約されます。外部依存がなく,入出力が明確で,テストケースが書きやすい。特に Go の場合は analysistest パッケージのおかげで,テストコード自体もシンプルに書けます。

一般的に TDD といえば

「まず公開する操作を踏まえた上で interface を定義して,モックを作って,DI して…」

みたいな儀式が必要になりがちですが,Go の Linter テストにはそういう煩わしさが一切ありません。実際業務では 「実装先に書いたほうが早いっしょw」 派な筆者も,Linter に関しては TDD 派閥になりそうです。

正直に言うと, AI が書いたコードの難所の 80% ぐらいは,私は理解できていません。

「明らかにここ if 文でゴリ押しし過ぎだろ, 3 階層以上の再帰に対応できてなくね?」

だとか,そういう経験則的に明らかな観点でのツッコミしかできないと思います。今回 Linter ライブラリは全て SSA をフル活用して実装しましたが,今までそんな概念聞いたこともなかったです。このように 未知の高度な知識を活用してちょっとしたツール開発ができる,それが Vibe Coding の醍醐味ではないでしょうか。

「たかが Linter だし,カバレッジ 8 割以上でテストが通ってりゃいいっしょw」

そういうメンタルでやってます。それでいいんですよ,今後自分が興味を持って勉強するきっかけにもなりますし。

OSS として成果共有しやすいテーマである

付け焼き刃的な Linter なら誰でも作れます。雑に ast.Inspect してパターンマッチングするだけで,ある程度は動くものはできます。しかしそれは業務をその場しのぎで支えるというだけで,今後別の業務で同じような問題に遭遇したときに「また一から作り直し?」となってしまいます。

汎用性が高い Linter であれば,最初から OSS として公開しておけば 他の業務でも再利用できるし,コミュニティからのフィードバックで品質も上がっていきます。そして何より,Vibe Coding の成果物がそのまま自分のポートフォリオになる。会社の業務コードは表に出せないけど,Linter は出せる。 業務のコード品質を守りつつ,自分の GitHub も潤う。そして顔も知らない世界の誰かを救うことになる。一石二鳥どころの話じゃないですね。

💬 「OSS クオリティのお行儀のいい Linter を作るのは難易度が高い」
💬 「いちいち業務時間犠牲にしてそこまで凝り性なことやってらんねーよ」

って? その仕事,AI が大得意な部分なので任せましょうよ。 AI に爆速で作らせて脳汁ドバドバ出して,OSS にしましょう。業務コードだったら絶対オーバーエンジニアリングって怒られるような内容でも,独立した OSS として切り出すなら話は別です。AI のスピード感を活かしてセルフレビューでガンガンマージしていき,「業務コードはシンプルに,Linter はリッチに」 を実現しましょう。

Vibe Coding 流行ってるから活用して何か作りたいけど,まだ人に見せられるレベルのもの作れてないよ…って方,大チャンスですよ?

Linter が持つ好循環

成果物として得られた Linter は,人間が書く業務コードだけでなく…

AI が書くことになる業務コードも,コード品質が下がらないように下支えしてくれます

CLAUDE.md, AGENTS.md に指示書いておけばいいじゃん」 が基本ではありますが,その結果保証は 100% ではなく確率に左右されてしまいます。Linter の出力は安定しており,入力が同じなら毎回出力も同じです。普通書かれないようなコードを除き,Linter の想定の範囲内であればミスの検出率をほぼ 100% にすることも可能であると思います。それはもう Linter が価値を余すことなく発揮してくれる領域です。

…あれ?この構図,どこかで見ませんでしたか?そうです,最初の TDD の話です。

「不安定な出力を安定化する」

という観点ではそっくりですよね。

実際には Linter だけで品質担保ができるわけではなく,並行して実施するテストが主力になることには変わりないですが,Linter がコード品質の下支えをしてくれることは間違いありません。Linter 開発は Vibe Coding の最高の練習台であり,かつ成果物が人間と AI 両方のコード品質を下支えしてくれる最高の好循環を生み出します

実践で役立ったプロンプト集

https://code.claude.com/docs/ja/overview

https://oraios.github.io/serena/01-about/000_intro.html

個人の感想ですが, Claude Code (Opus) + Serena の組み合わせは至上最強 です。連続したタスクから脱線せずに遂行する能力が極めて高いし,とくに Linter 開発においては局所的にも最適解に近いものを安定して出しているようにすら思います。導入方法はググればいくらでも出てくると思うのでそれらは省略し,実際に役立ったプロンプトの紹介に重点を置こうと思います。

5 フェーズの品質改善サイクル

初動はあまり細かく指示出さなくても,ほとんど完璧に自走してプロトタイプまでは一瞬で作ってくれるんですけど,そこから先の機能をリッチにしていくフェーズやコード品質を上げていくフェーズでは,色々とプロンプトテクニックは求められると思います。

私が愛用していたプロンプトに含まれる 5 フェーズの品質改善サイクル を紹介します。実際に goroutinectx の CLAUDE.md に書いてある内容です。もちろんこれは全部人間が書いたのではなく, Claude Code 自身が書いたものがベースになっています。

Phase ペルソナ 役割
1 QA エンジニア 徹底的にエッジケースをつつきまくる。検出できない限界があれば [LIMITATION]: マーカーをコメントに残す
2 実装エンジニア [LIMITATION] を確認し,解決を試みる。実用上の検出精度向上を優先
3 コードスタイルエンジニア リファクタリングレビュー。提案を「Should not do / Either way / Should do」に分類して実施
4 新人エンジニア 素朴な質問で「わかりにくい部分」を炙り出す
5 先生コンビ
(実装エンジニア+コードスタイルエンジニア)
複雑な概念を説明し, CLAUDE.mdARCHITECTURE.md を更新

→ Phase 1 に戻ってサイクルを繰り返す

ポイントは [LIMITATION]: マーカー です。「理想の動作と現状のギャップ」をコード中にコメントとして残しておくことで,次のフェーズで AI が自動的にそれを拾って改善を試みてくれます。 QA エンジニアが発見したエッジケースを QA エンジニア自身が直ちに解決することはしませんが, 「ここを次のフェーズで解決してね」 とコンテキストに残して伝えておけば,実装エンジニアがそれを拾って解決を試みてくれます。

// [LIMITATION]: スライスの要素に代入されるとチェーンを追跡できなくなる

これらのプロンプト指針を伝えて,

💬 「このメンバーでサイクルを 5 回回して。今からお風呂入ってくるからその間勝手にやっといて」

って伝えて数十分後に戻ってくると,あっという間にとんでもない高品質なものが出来上がってたりします。私はこの成果を目の当たりにしたとき,初めて

「人間の IT エンジニアはもう失職寸前なのではないか?」

という危機感すら抱きました。

リファクタリング時の鉄則

もう 1 つ,リファクタリング時に必ず伝える呪文があります。

💬 「本体コードをリファクタリングするときは,テストが絶対落ちないようにして」
💬 「テストコードをリファクタリングするときは,本体カバレッジが絶対落ちないようにして」

これを伝えておかないと,AI がリファクタリングの勢いでテストを壊したり,不要と判断したテストを消してカバレッジが下がったりすることがあります。対策として,片方を固定した上でもう片方を変える。シンプルだけど効果絶大です。

ここまで挙げてきた図の関係性にそれぞれ当てはまりますね。「不安定な出力を安定化する」 という構図は,生成 AI 時代のあらゆる場面で意識すべきベストプラクティスなのかもしれません。

もちろんこれは Linter 文脈に限らず,業務コードのリファクタリングでも同様です。生成 AI 時代のリファクタリングにおける鉄則としてぜひ覚えておいてください。

v2 ディレクトリでのゼロイチ再実装

改修を重ねた結果,コードが汚くなってしまった。 Vibe Coding 開発あるあるではないでしょうか?コードベースが一定以上に大きくなると,既にある設計ミスを含んだ実装に引っ張られ,新たな実装にも歪みが伝播して蓄積していきます。要するに技術的負債ですね。 Vibe Coding は短時間で生産性を出しやすい一方で,技術的負債の蓄積リスクも高いです。

つい最近そのような状況に陥ったときに,うまく状況を打開できた実績のあるプロンプトを紹介します。

💬 「改修を重ねた結果,analyzer ディレクトリ以下が汚くなってしまいました。後方互換性は気にしなくていいので, v2 ディレクトリを切り,完全なゼロイチで,モジュラーアーキテクチャで再実装してもらえませんか?ユニットテストは削除して構いませんが, Linter にとって E2E レベルのテストである analysistest の対象ファイルは一切触らないでください。もしテスト自体にバグがあると疑われる場合,それに関しては私に確認を取ってください。」

現在のファイル群を維持したまま 「きれいなアーキテクチャで再実装してください」 といっても, Claude Code は実装が壊れていないかこまめにテストを実行しながら作業を進めるので,ビッグバンリファクタリングは敬遠される傾向が非常に強いです。そこで段階的な移行と技術的負債の解消を両立する手法として, v2 ディレクトリに部品を少しずつ再実装し,テストが通ることを確認しながら徐々に切り替えていくアプローチを取ります。人間もよくやる手法だと思いますが, AI にとっても有効なようです。

なお v2 実装完了後,更に

💬 「このライブラリには後方互換性不要なので, v2 の中身で元のファイル群を上書きして,これまでのバージョンは無かったことにしてください。バージョニングなんか撤廃」

というと,すんなり言う事聞いてくれます。最初から聞けよ

人間と AI の役割について考える

ここまで「AI に任せる」話をしてきましたが,逆に人間の提案によって AI の提案を改善した事例も紹介しておきましょう。

事例: 設定ファイルのスキーマ設計

例えば ctxweaver の YAML スキーマ設計は,人間と AI が壁打ちしながら一緒に育てた例です。最初はパッケージ指定する方法が, patterns だけのシンプルな設計でした。これは Go ではお馴染みのパッケージパターン記法(./...)ですね。

https://zenn.dev/kanmu_dev/articles/75f227728cad19

patterns: ["./..."]

次に「パッケージ群の中から特定ファイルだけ正規表現で除外したい」と思って exclude_regexps を追加しました。 JSON の string という型名だけでは「何を書けばいいのか」がユーザーに伝わらない。ハンガリアン記法的に,正規表現であることを明示すべきだと考えてこの命名にしました。 今思えば,めっちゃ付け焼き刃感ありますね…

patterns: ["./..."]
exclude_regexps: ["_test\\.go$", "/mock/"]

AI に設計をレビューしてもらうと,

  • 取り込みと除外で指定方式が異なり,対称性が無いのが分かりにくい
  • 語感的に並びとしては exclude があるなら include も無いと分かりにくい
    • とはいえ,この 2 つが並ぶと優先順位がわからない

のようなフィードバックを受けました。完全に正論ですね。加えて更に,

  • match / ignore とかの語彙もありますよ」

という提案も返ってきました。

最初これを採用しようと思ったのですが…やはり既に patterns があるなら正規表現であることを明示しないと誤解を招きそう。だったらもう regexps の中に include / exclude をまとめてしまおう,と考えました。ここから更に, TypeScript の Utility TypesLaravel の Arr ヘルパー の命名を思い出して,

💬 regexps: { only: '...', omit: '...' } なんてどう?」

と提示したところ,AI も「いいですね!!」と賛成してくれて,今の形になりました。これなら Go のパッケージ指定記法と正規表現の差が分かりやすいし,命名もシンプルにできています。

packages:
  patterns: ["./..."]
  regexps:
    only: ["^github\\.com/mpyw/"]    # マッチするものだけ処理
    omit: ["_test\\.go$", "/mock/"]  # マッチするものを除外

ここに示したのは局所的で小さな事例に過ぎませんが,このように 人間が経験で培ってきた,人間らしい細やかな感性に基づいた選択 こそ, AI に人間の必要性を示す瞬間になると思います。

人間に残る仕事とは?

Vibe Coding 全盛期の時代に,人間に求められる仕事は結局何が残るんでしょうか?上の段落でも触れましたが,もう少し言語化してみましょう。

AI はとても優秀ですが,難しいテーマの開発においては AI の知識をもってしても,稀に対応しきれない場面に遭遇することはあります。そんなとき,人間には

「難しそうなのでやっぱり指示撤回します,コミットをここまで戻して」
「もう少しシンプルなアプローチにしましょう」

など,その辺の匙加減を知ったうえで臨機応変に提示する能力が求められそうです。

確かにプロンプト化できちゃいます。でも,徹底的に言語化するのって意外と難しくないですか?

人間の仕事って何が残るの?に対するアンサーとしては,まだ言語化しきれていない「ふわっとした違和感」を敏感に感じ取って臨機応変に対応すること ではないでしょうか。命名の一貫性,抽象度のバランスなどに加えて, 「今撤退しないと大変なことになる」 という第六感…

ただ… Claude Code (Opus) は優秀すぎて,思考の過程で

「ん?これはおかしい。この関数の目的は…だったはずだから,こう改修してしまうと元の目的から脱線してしまう」

と,人間らしさ全開で慎重な思考を重ねていることを観測し,結果として正しいゴールに自力でたどり着く姿を何回も見かけてしまったんですよね。本当にこれは衝撃でした。

さて, AI が人間を必要としてくれるのはいつまででしょうね…?正直これはもう,時間の問題としか思わないです。最後まで残るのは,責任を取ってプルリクエストをマージしてリリースする,要するにハンコを押す仕事ぐらいでしょうか…?

まとめ

  • Vibe Coding は「素人が何でも作れる」ではない。基礎力を活かしてハルシネーションを是正するスキルは必須
  • TDD は AI 時代の救世主。具体例を挙げるのは人間,量産と一般化は AI
  • Linter 開発は Vibe Coding の最高の練習台。業務リスクなし,AI が得意な領域,成果物は人間と AI 両方のコード品質を下支え
  • Claude Code (Opus) + Serena は革命的。自己修正能力まで備えている
  • 人間に最後まで残る仕事は,まだ言語化しきれていない「ふわっとした違和感」を感じ取って臨機応変に対応すること?

Linter 開発,やってみませんか?AI に爆速で作らせて脳汁ドバドバ出して,OSS にしましょう。この記事に触発されて作ってみたよ!という方いらっしゃいましたら,コメント欄で是非シェアしてくださいね。

Go に限らず,どんな言語でも構いません。 PHP であれば Linter として PHPStanPsalm ,コード書き換えツールとして Rector がありますよね。あの辺自作するのものすごく大変ですが, AI の力を借りれば一気にハードルが下がりますよ?少しでも興味を持ったら,迷うよりまず手を動かしてみましょう。

GitHubで編集を提案
ゆめみ

Discussion