Blog Blog

Unwritten Chapters

Geometric JF Fusion

Blog

ESLint で厳格なルールを定義して、AI と一緒にプロジェクト全体のリファクタリングを進める話

ESLint で厳格なルールを定義して、AI と一緒にプロジェクト全体のリファクタリングを進める話

またやりましたよ。そう、コードの整理を。

今回は、プロジェクト全体のリファクタリングを、AI と一緒に進めている話です。

以前の記事では説明が抜けていたんですが、僕が AI を使うときは基本的に Cursor を使っています。ごめんなさいー!YouTube のライブ配信中に質問されて「Cursor 使ってるって書いてなかったわ!」って気づきました。

Cursor では AI の選択ができるんだけど、最近は Gemini 3 Pro を使っています。リファクタリングの話をする前に、まずこの前提を書いておかないとですね。

リファクタリング用のプロンプトを作成した

リファクタリングは、あとからやるのは大変。じゃぁいつやるのって話ですよね。

基本的にはコミットごとにやるべきだと思っています。でも既存のコードを変更するのは注意してね!ちゃんとテストするんだよ!テストコードが準備してあるとなお良いです!

でもたまには、プロジェクト全体を見渡してリファクタリングをしてもらうのがいい。大きく変更しないで。基本的には小さく小さく変更していく。自分が理解できない、理解できても納得できないリファクタリングには手を付けないほうがいい。もしそんなことをしたら、もう自分でメンテナンスできなくなりますよ!

というわけで、リファクタリング用のプロンプトを作成しました。prompts/refactoring.md です。

このプロンプトでは、重要な3つを提案してもらうようにしています。ただ、大きな改変を行うものではなく、小さな変更、定数定義、重複の削除、メソッド抽出、ファイル分割などからやってもらっています。

また、コード標準のドキュメントも作成してあります。これは、ややこしい ESLint のルールを解析して作ってもらったものです。prompts/coding-standards.md として保存しています。

リファクタリングで使っているプロンプト

実際に使っているプロンプトはこれです:

@prompts/refactoring.md プロジェクト全体のリファクタリングを提案して。

これを投げて、対応して、また新しいセッションで同じプロンプトを投げて、というのを繰り返しています。

小さく小さく進めていく。一度に 3つ までの提案に絞ってもらっています。

ESLint で厳格なルールを定義している

リファクタリングするときに使ってるプロンプトを紹介しましたが、実は複雑度、メソッドの行数、ネストの深さ、1ファイルの行数、パラメータ数、これらは ESLint で定義しているんです。

一般的なものよりはかなり厳し目にしています。

コード品質制限値の比較

項目このプロジェクト一般的な値備考
認知複雑度(Cognitive Complexity)410-15sonarjs/cognitive-complexity で設定
サイクロマチック複雑度(Cyclomatic Complexity)510-15complexity ルールで設定
ネストレベル24-5max-depth で設定
関数の行数25行50-100行空行・コメント除外
ファイルの行数300行500-1000行空行・コメント除外
パラメータ数3個4-5個max-params で設定
関数内の文の数10個20-30個max-statements で設定
1行の最大長100文字120-150文字max-len で設定
識別子名の最小長2文字1文字id-length で設定
識別子名の最大長30文字制限なしid-length で設定
連続する空行の最大数1行2行no-multiple-empty-lines で設定

認知複雑度とサイクロマチック複雑度は似ていますが、認知複雑度の方が読みやすさに焦点を当てています。このプロジェクトでは、両方を厳格に設定しています。

なぜこんなに厳しく設定しているのか

めんどくさいんですよね。複雑なコードは。

複雑なコードは読みにくいし、理解するのに時間がかかるし、バグが入り込みやすい。でも、厳格なルールを定義しておけば、ESLint が自動的にチェックしてくれます。

関数が 25 行を超えたら、分割を検討する。ネストが深くなったら、関数に抽出する。複雑度が高くなったら、ロジックを見直す。

これらを手動でチェックするのはめんどくさい。でも、ESLint が自動でチェックしてくれるからめっちゃ楽ちんなんです。そして、AI さんもそれをちゃんと守って・・・くれないこともあります。

その他の重要なルール

複雑度以外にも、いくつか重要なルールがあります。

名前空間オブジェクトエクスポート(必須)

複数の関数を公開する場合は、必ず名前空間オブジェクトにまとめてエクスポートする必要があります。これは、関数のみに適用されます。定数は個別エクスポートで問題ありません。

例えば、app.ts では、link_label 関数を app という名前空間オブジェクトにまとめています。定数(APPAUTHORURLS など)は個別にエクスポートしています。

これによって、関数は app.link_label() のように使えるし、定数は AUTHOR.NAME のように直接アクセスできます。ドットが2つ以上になると読みにくくなるので、定数は個別エクスポートを維持しています。

マジックナンバーの禁止

01-1 以外の数値リテラルは原則として定数へ抽出する必要があります。繰り返し使用する文字列や識別子も定数化し、意味の分かる名前を付けます。

明示的な型定義が必須

すべての関数には明示的な戻り値の型が必要です。any の使用は完全禁止。未使用変数も禁止。浮動 Promise も禁止。

命名規則

このプロジェクトでは、変数・関数・パラメータは snake_case を使います。標準的な TypeScript の camelCase ではありません。これも ESLint で強制しています。

リファクタリングの実際の流れ

実際のリファクタリングの流れを説明します。

  1. リファクタリング用プロンプトを実行

    • @prompts/refactoring.md プロジェクト全体のリファクタリングを提案して。 を投げる
  2. AI が3つまでの提案をする

    • 優先度順に整理される
    • 実装可能なサンプルコードが提供される
    • 関連ファイルの情報も含まれる
  3. 提案を確認して対応する

    • 自分が納得できる提案だけを採用する
    • 理解できないリファクタリングには手を付けない
    • 小さく小さく進める
  4. 新しいセッションで繰り返す

    • 一度に大量にやらない
    • 定期的にチェックして、小さく改善していく

最近のリファクタリング例

実際にどんなリファクタリングをしたのか、いくつか具体例を紹介します。

TechStack コンポーネントの巨大ファイル分割

まずは、src/lib/components/TechStack.svelte の分割です。

このファイルは、なんと 396行 もありました。ESLint のファイルの最大行数(300行)を超えていたんです。しかも、データ定義と UI のコードが混在していて、読みにくい状態でした。

どうしたかというと、データ定義部分を src/lib/data/tech-stack.ts に移動しました。さらに、tech-stack のデータをカテゴリごとに 12個のファイル に分割しました。

  • programming-languages.ts - プログラミング言語
  • web-technologies.ts - Web 技術
  • databases-and-cloud.ts - データベースとクラウド
  • game-development.ts - ゲーム開発
  • mobile-development.ts - モバイル開発
  • などなど…

結果、TechStack.svelte23行 まで削減されました。373行も削減です!これで、UI のコードだけが残り、すごく読みやすくなりました。

データを追加したいときも、該当するカテゴリのファイルを編集するだけなので、迷いません。

ブログ投稿のパース処理を関数として抽出

次は、src/routes/blog/+page.server.ts です。

このファイルは 90行 もありました。中を見ると、ブログ投稿のパース処理が全部ここに書かれていたんです。型チェック関数、パース関数、すべてが一緒になっていました。

どうしたかというと、パース処理を src/lib/utils/blog-parser.ts に抽出しました。型定義も src/lib/types/blog.ts に移動しました。

結果、+page.server.ts12行 まで削減されました。78行も削減です!

blog-parser.ts では、名前空間オブジェクトエクスポートのルールに従って、blog_parser という名前空間オブジェクトにまとめています。これで、他のファイルからも再利用しやすくなりました。

app.ts の名前空間オブジェクトエクスポート

最後に、src/lib/app.ts のリファクタリングです。

このファイルでは、get_link_label 関数が個別にエクスポートされていました。名前空間オブジェクトエクスポートのルールに従って、app という名前空間オブジェクトにまとめました。

関数は app.link_label() のように使えるようになりました。定数は個別エクスポートを維持しているので、AUTHOR.NAME のように直接アクセスできます。

最初、AI さんは定数も名前空間オブジェクトにまとめようとしていました。でも、app.APP.NAME のようにドットが2つ以上になると読みにくいじゃないですか。そこで、「定数は個別エクスポートでいいよ」って伝えました。そうしたら、プロンプトにも「定数は対象外」って明記してもらいました。

この変更は、src/lib/components/ProjectLinks.svelte での使用箇所も更新する必要がありました。でも、影響範囲が限定的だったので、問題なく進められました。

lint エラーの話

リファクタリングを進めていると、「できました!lint エラーはありません!」と言いながら lint エラーが残る、ということがたまにあります。

たまにじゃないわ、よくあるんですよ!これ、何度もプロンプトを調整して直そうとしてるんですが、なおらない!なぜなんだ!

実際には VSCode 拡張によりファイルを開けばすぐわかります。ただし、npm run lint を実行して、全体的にエラーがないことを確認する。これが重要です。

リファクタリングを実装した後は、必ず lint チェックを実行して、エラーが 0 件であることを確認します。たとえ AI さんが「できました!」と言っても信用できないので大変です。

小さく小さく進めることの大切さ

「ちょっとずつと言っておきながら大量にリファクタリングしてもうた!」ということがあります。

でも、基本的には小さく小さく進めることが大切だと思います。一度に大量にリファクタリングすると、影響範囲が広くなって、予期しない問題が起きる可能性が高くなります。

小さく進めることで、各変更の影響を確認しながら進められます。問題があれば、すぐに気づけます。

プロンプトの改善も継続的に

今回のリファクタリングを通じて、プロンプトの改善も行いました。

名前空間オブジェクトエクスポートのルールについて、「定数は対象外」ということを明確に記載しました。最初の AI さんの提案では、定数も名前空間オブジェクトにまとめようとしていましたが、これは間違いでした。

プロンプトを改善することで、次回からより正確な提案がもらえるようになります。

そして SonarCube さんに怒られた

今回、TechStack のデータ定義をいじっていたら、SonarCube さんに、「重複コードありすぎー!こんなデータ定義ファイルあかんわ!」って怒られました。

色々やってみたんですが、気づいたのは、すべてのバッジ URL が https://img.shields.io/badge/ で始まっているため、これが重複として検出されていたのかな。URL 部分に含まれる文字列が重複してるから、なんとかせーや!という指摘だったのかもです。

そこで、URL の共通部分を定数として抽出し、各バッジではパラメータ(名前、色、ロゴ、ロゴの色)のみを定義し、URL 生成関数で組み立てる方式に変更しました。

これでようやく SonarCube さんからの指摘がなくなりました。SonarCube さんのチェックはめっちゃ厳しいんですよ。だかそれがいい!厳しい先生、最高です。

まとめ

AI と一緒にリファクタリングを進めることで、小さく小さく改善を続けられています。

上記にもっともらしいことを書いていますが、実は、すべてのリファクタリングの提案と、すべてのコードの書き換えは AI さんがぜーんぶやってくれてます!僕が実際にやってるのは、プロンプトで「提案して」と「じゃ、それ対応して」だけです!あ、あと、「lint エラーちゃんと修正してよー」という愚痴ですね。

厳格な ESLint ルールを定義することで、コード品質を保ちながら進められます。リファクタリング用のプロンプトを作成することで、AI に適切な提案をしてもらえます。

でも、最終的には自分が納得できるかどうかが重要です。AI の提案をそのまま採用するのではなく、理解して、納得して、採用する。これが大切だと思います。

みなさんはどうしてますかー?リファクタリングやってます?もっとこうしたほうがいいよーとか、そういう話、ぜひ聞かせてもらえると嬉しいです。

いいなと思ったら応援しよう!

Support チップで応援する

応援してもらえると最高に嬉しいです!