✂️

AIコーディング時代のゴミ掃除ツール『Knip』で3000行削除した

に公開

TL;DR

  • AIコーディングツールは便利だが、不要なコードを大量に生む
  • Biomeやtscではプロジェクト横断の未使用exportを検出できない
  • Knipを導入したら30ファイル・3000行の不要コードが見つかった
  • pre-commitフックに組み込むことで、デッドコードの混入を防止できる

AIと開発するほどコードは増える

はじめまして、オランダ出身・東京在住のプロダクトオーナー兼フルスタックエンジニア、ペレスです。今は総合型選抜対策プラットフォーム「アオマル」のプロダクトを担当しています。

Claude CodeやCursor、GitHub Copilotといったツールが登場し、コードを書く速度は劇的に上がりました。自然言語で指示するだけで、数十行のコードが一瞬で生成されます。もはやAIなしの開発に戻れないという方も多いのではないでしょうか。

しかし、AIと一緒に開発を進める中で、ある問題に気づきました。プロジェクトの中に、誰にも使われていないコードがどんどん溜まっていくのです。

僕も普段Claude Codeを使うことが多いですが、新しい機能の最終確認時にプロジェクトを見渡すと、使われていないユーティリティ関数、古い実装の残骸、一度も呼ばれていない型定義が散在していました。また、自分の経験上では「今後必要な関数かもしれないですから、追加しちゃいました」というパターンが特によくあります。手動で探して消すのは現実的ではない量です。そこで出会ったのがKnipというツールでした。

Biomeだけでは不十分な理由

BiomeやTypeScriptコンパイラはファイル単位での未使用変数やimportは検出できます。しかし、ファイルをまたいだ未使用export、つまり「exportされているけどプロジェクト内のどこからもimportされていないコード」は検出できません。AI開発で蓄積するデッドコードの多くはまさにこのパターンです。

AIが生むデッドコードの3つのパターン

AIが不要なコードを生む原因は、大きく3つのパターンに分類できます。いずれも人間が手書きしていた時代には起きにくかった、AI開発特有の問題です。

パターン①:方針転換で旧実装が放置される

開発中に「やっぱり別のアプローチにしよう」や「こうした方がいいんじゃない?」と方針を変えることはよくあります。AIに新しい実装を指示すると、古いコードを削除せずに新しいコードを追加するケースが非常に多いです。人間が手で書き換える場合は、自然と旧コードを意識して消しますが、AIは「追加」が得意な一方で「削除」の判断が苦手です。

結果として、プロジェクト内に「旧バージョン」と「新バージョン」の実装が共存し、どちらが正しいのかわかりにくい状態が生まれます。

パターン②:似たユーティリティ関数が量産される

AIはまだプロジェクト全体のコードを完全に把握しているわけではありません。そのため、既存のユーティリティ関数があるにもかかわらず、似たような関数を別のファイルに新しく生成してしまうことがあります。

例えば、アオマルではユーザーがスマホを使っているか、パソコンを使っているのかを判断するためにisMobileというユーティリティ関数を使っています。ただ、確認したところ、すでにisTouchDeviceという関数があって、内容は違うものの結果と使い方は全く同じでした。

また、自分でも不思議な現象だと思っている例で言うと、新しいユーティリティ関数を作成するときに既存のユーティリティ関数をラップしているだけの関数が見つかりました。

export function validateDomain(domain: string): domain is InsightDomain {
  return isValidDomain(domain);
}

そのほか、formatDateparseResponsevalidateInputなどはAIが重複生成しやすい関数の代表例で、気づいたときにはどれが「正規」の関数なのか判別がつかなくなります。

パターン③:AIが「将来必要そう」と先回りして不要コードを生成する

冒頭でもお伝えした通り、これが一番厄介かもしれません。頼んでいないのに、AIが「拡張性のために」「後で使うかもしれないので」と、ヘルパー関数や型定義、ユーティリティを勝手に追加するパターンです。

実際に、あるエンティティを処理するための関数とデータベースのクエリを生成してもらった時に、勝手にバッチ処理まで作って「今後実装するといいかもしれません」と提案してきました。ただ、このエンティティをバッチで処理するケースはありえませんでした。

また、APIクライアントを作ってもらったら、使う予定のないretryロジックやcacheレイヤーまで実装されていることもよくあります。

一見丁寧に見えますが、実際には一度も使われないまま放置されます。しかもexportされているため、Biomeのno-unused-varsでは検出できません。ファイル単体では「使われているかもしれない」ように見えるのが厄介なところです。

Knipとは何か

https://knip.dev/

Knip(/knɪp/)はオランダ語で「切る」を意味する、JavaScript/TypeScriptプロジェクト向けのコードクリーンアップツールです。一言で言うと、プロジェクト全体を解析して、不要なコードを見つけ出すハサミです。

検出できるものは多岐にわたります。

  • 未使用ファイル:どこからもimportされていないファイル
  • 未使用export:exportされているがプロジェクト内で参照されていない関数・変数・クラス
  • 未使用の依存関係package.jsonに記載されているが使われていないパッケージ
  • 未使用の型定義:exportされた型やインターフェースで使われていないもの

Biomeとの決定的な違いは、プロジェクト全体のモジュールグラフを構築して解析する点です。エントリーポイントから依存関係を辿り、どのファイル・どのexportが実際に使われているかを正確に判定します。

エコシステムとしての勢いも目覚ましく、npm週間ダウンロード数は420万を超え、Next.js、Vite、Biome、Storybook、GitHub Actionsなど100以上のプラグインに対応しています。Vercelでは約30万行の不要コードを削除した実績もあり、「zero config」を目指した設計で導入のハードルが非常に低いのも魅力です。

npm週間ダウンロード数は420万を超え

実際にKnipを導入してみた

セットアップ

導入は驚くほど簡単です。

npm install -D knip typescript
npx knip

zero configを謳っているだけあり、多くのプロジェクトではこれだけで動きます。Next.jsやViteなどを使っている場合、Knipが自動でプラグインを検出して適切に解析してくれます。

プロジェクト固有の設定が必要な場合は、knip.json(またはknip.ts)を作成します。
うちのknip.jsonはこんな感じです。参考になればと思います。

{
  "$schema": "https://unpkg.com/knip@5/schema.json",
  "project": [
    "**/*.{ts,tsx,js,jsx}",
    "!**/*.d.ts",
    "!supabase/migrations/**",
    "!supabase/seeds/**",
    "!coverage/**",
    "!.next/**",
    "!node_modules/**"
  ],
  "ignore": ["**/*.d.ts", "next-env.d.ts"],
  "ignoreDependencies": ["autoprefixer"],
  "ignoreBinaries": ["supabase"],
  "ignoreExportsUsedInFile": true,
  "rules": {
    "files": "error",
    "exports": "error",
    "types": "error",
    "nsTypes": "error",
    "nsExports": "error",
    "classMembers": "warn",
    "duplicates": "error",
    "unlisted": "warn",
    "binaries": "warn",
    "unresolved": "warn",
    "dependencies": "off",
    "devDependencies": "off",
    "optionalPeerDependencies": "off"
  }
}

初回実行の結果

初回実行の結果は正直ショックでした(笑)
未使用の関数がいくつかあるかなくらいに思っていたら、完全に使われていないファイルまで見つかりました。結果として、30ファイルを削除し、全部で3000行も削除できました。
コミットはこんな感じでした。
コミットの詳細

自動修正と注意点

Knipには--fixオプションがあり、検出した不要コードを自動で削除できます。

npx knip --fix                    # 不要なexportやimportを削除
npx knip --fix --allow-remove-files  # 不要なファイルごと削除

ただし、--fixをいきなり本番コードに適用するのは避けた方が無難です。まずは--fixなしで結果を確認し、false positiveがないかチェックしてから適用するのがおすすめです。動的importやリフレクション経由の参照はKnipが追いきれない場合があるため、変更後にテストを通すことも忘れずに。

AI開発ワークフローにKnipを組み込むコツ

Knipの真価は、単発で実行するだけではなく、開発ワークフローに組み込むことで発揮されます。自分の場合はCIではなく、lefthookのpre-commitフックでKnipを実行しています。コミットする前にデッドコードを検知するので、不要コードがリポジトリに入り込むのをそもそも防ぐことができます。

# lefthook.yml
pre-commit:
  commands:
    biome:
      runner: pnpm
      glob: "*.{js,ts,jsx,tsx,json,css}"
      run: pnpm exec biome check --write --staged --no-errors-on-unmatched
    knip:
      runner: pnpm
      run: pnpm knip --production
      fail_text: "デッドコードを検出しました! 'pnpm knip'を実行して詳細を確認してください。"
    typecheck:
      runner: pnpm
      run: npx tsc --noEmit --skipLibCheck

ポイントは--productionフラグです。テスト用のコードやdevDependenciesを除外し、本番コードに絞ってチェックすることで、false positiveを減らしつつ実用的な検出ができます。Biomeによるフォーマット・リント、Knipによるデッドコード検出、TypeScriptの型チェックをpre-commitで一括実行することで、コミット時点でのコード品質を担保しています。

また、2025年12月にはKnipのエディタ拡張とMCPサーバーがリリースされました。これにより、エディタ上で未使用exportにリアルタイムで警告が表示されるほか、AIコーディングエージェントがKnipの情報を活用してよりクリーンなコードを生成できるようになります。AI開発時代にまさにフィットするアップデートです。

https://knip.dev/blog/for-editors-and-agents

まとめ

AIの力で速くコードを書ける時代だからこそ、不要なコードを見つけて削除する仕組みがこれまで以上に重要になっています。Knipは導入コストが極めて低く、それでいてプロジェクト全体を横断して不要コードを確実に検出してくれる、AI時代のコード衛生ツールです。

AIと一緒に開発しているすべてのJavaScript/TypeScriptエンジニアに、ぜひ一度npx knipを試してみてください。きっと、僕と同じように思った以上のゴミが見つかるはずです。

mugendAI テックブログ

Discussion