Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TSKaigi2026-静的解析への投資がAI時代のコード品質を支える ── カスタムESLi...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

TSKaigi2026-静的解析への投資がAI時代のコード品質を支える ── カスタムESLintルールの設計と運用

私たちのプロダクトでは、エンジニアだけでなくPMやデザイナー・ビジネスチームもバイブコーディングでPRを出しています。
日々多くのPRが作成されますが、それでもコード品質を維持しレビューコストを抑えられているのは、100以上のカスタムESLintルール等を中心とした静的解析基盤があるからです。

本トークでは、「なぜ標準プラグインでは足りないのか」を整理し、Valibot・Hono・Drizzle・Svelteなど、既存のESLintプラグインではカバーしきれないライブラリ群に対して、プロジェクト固有のガードレールをどのように設計したのかを解説します。
さらに、コードレビューの指摘からESLintルール候補を自動抽出しルール化につなげる仕組みや、CI高速化のための設計、AIによるCIエラー自動修正を試みて見えた限界と教訓についても共有します。

静的解析への投資がAI時代にどう効くのか、カスタムESLintルールの設計からCI運用まで具体的に共有します。

Avatar for HayatoKudou

HayatoKudou

May 21, 2026

More Decks by HayatoKudou

Other Decks in Programming

Transcript

  1. flyle 1 / 42 | 自己紹介 工藤 颯斗 / Hayato

    Kudo 株式会社Flyle Software Engineer リニューアル版 Flyle の開発に従事 TypeScript / Svelte / Hono @metalic_kudo_h
  2. flyle 1 / 42 | FLYLE, INC. 埋もれる VoC、5% しか聞けない品質評価、終わらない後処理

    コンタクトセンターの 対話データ から AI 変革 VoC 分析・応対品質改善・ACW 削減・ リスク検知まで改善する コンタクトセンター・CX の AI 変革 パートナー 多様な業界の大手企業のコンタクトセンター・CX 部門で導入
  3. flyle 1 / 42 | 目次 1 開発生産性の変化 弊社で起きていること 2

    静的解析の必要性 AI時代に静的解析が重要な理由 3 なぜ ESLint カスタムルールを書くのか 外部プラグインで足りる領域と、足りない領域 4 カスタムルールのライフサイクル どう増やし、どう保つか 5 CI運用 高速化とAI自動修正の限界
  4. flyle 1 / 42 | コード生成量・PRの急増 弊社リポジトリの直近半年とその前の半年を比較 指標 (週あたり) 2025/5〜10

    2025/11〜2026/4 変化 追加行数 37,180 111,328 × 2.99 削除行数 15,189 30,630 × 2.02 マージ済みPR数 39.2 92.7 × 2.36
  5. flyle 1 / 42 | エンジニア1人あたりで見ても急増 人数増の影響を除いても、エンジニア1人あたりの生産量が大幅に増えている 指標 (週あたり /

    1人) 2025/5〜10 2025/11〜2026/4 変化 追加行数 3,718 10,121 × 2.72 削除行数 1,519 2,785 × 1.83 マージ済みPR数 3.92 8.43 × 2.15
  6. flyle 1 / 42 | コード生成量・PR数が増えた理由 「1人あたりのコード生成量」と「コードを書ける人」が同時に増えた ① エンジニアのAI活用 Cursor

    / Claude Codeなどを日常的に活用 1人あたりのコード生成量が大幅に向上 ② 非エンジニアのバイブコーディング PM / デザイナー / ビジネスチームがバイブコー ディングでPRを立てるように 「コードを書ける人」が組織で増加
  7. flyle 1 / 42 | 非エンジニアのバイブコーディングの影響 Devin によるバイブコーディングの2025/11〜2026/4の実績 DevinによるPR作成数 586

    全PR 3,168件中 18.5% Devinによるマージ済みPR数 390 全マージPR 2,396件中 16.3% 期間中にDevinを利用したメンバー 25人 エンジニア数の 2倍以上
  8. flyle 1 / 42 | バイブコーディングのフロー PM・デザイナー・ビジネスチーム も Slack経由でAIエージェントに依頼してPRを立てる 軽微な不具合や機能アイデアを見つけたとき、

    起票せずに直接AIエージェントへ指示 → PR化 → エンジニアがレ ビュー ⚠️ 補足 この運用は プロダクトが初期フェーズ で不具合・意見を吸い上げるため に効果的だったが、一方でリリース内容に周囲が追いつけずカオスにな る側面もあり、すべてのプロダクトに有効とは限らない
  9. flyle 1 / 42 | 視覚的に見るコード増減 GitHub Insights (週次の追加・削除コード行数) Devinを導入してから追加コード量が

    安定的に高水準 に ※ 2025/7の大スパイクは外れ値(大規模リファクタリング)。それを除いても以降の増加傾向は明確
  10. flyle 1 / 42 | AIのコード品質にはムラがある 型は通るし動く。ただし プロジェクト固有の制約 が守られないことがある ❌

    例えばこう書かれる 構造化されない・ログレベル制御なし ✅ 自社のルール 自社のロガー経由で構造化ログ・環境別レベル制御・テナント / ユーザーID を自動付与 💡 ポイント コード品質のムラは AI 特有ではない。人間が書く場合も、組織規模の拡大などにより AI 導入による品質低下に近い事象は起こりうる function createUser(input: UserInput) { console.info('creating user', input) const user = db.users.insert(input) console.info('user created', user.id) return user } import logger from '@flyle-lib/logger' function createUser(input: UserInput) { logger.info({ email: input.email }, 'Creating user') const user = db.users.insert(input) logger.info({ userId: user.id }, 'User created') return user }
  11. flyle 1 / 42 | レビューで指摘する運用はスケールしない コード生成量が 約3倍 になった今、全てのPRで人間が指摘・修正していたら レビュアーがボトルネックになり、スケールしない

    ❌ スケールしない理由 同じ指摘を毎PRで繰り返すことに AIに PRレビューで都度伝えても 次PRで再発 レビュアーの時間が制約違反の指摘に消える ✅ 静的解析が担っていること 制約違反は 必ず CIで止まる バイブコーディングはCIを通してからPRを出す ので、レビュー時点で違反は潰れている レビュアーは 設計・意図の議論 に集中できる
  12. flyle 1 / 42 | AIは確率的、静的解析は決定的 🤖 AI 同じプロンプトでも違うコード 「たぶん正しい」しか保証できない

    確率的・非決定的 🛡️ 静的解析 同じASTには同じ結果 違反は 必ず 検出する 決定的・再現可能
  13. flyle 1 / 42 | 外部プラグイン・パーサも積極的に使っている フレームワーク eslint-plugin-svelte eslint-plugin-storybook TypeScript

    typescript-eslint セキュリティ eslint-plugin-security コード品質 eslint-plugin-sonarjs eslint-plugin-unicorn eslint-plugin-import 正規表現 eslint-plugin-regexp Node eslint-plugin-n YAML yaml-eslint-parser SQL postgresql-eslint-parser 💡 車輪の再発明をしない 例: 正規表現の品質・ReDoS 検出 は、何パターンもの研究蓄積と継続的なメンテナンスが必要な専門領域 自前で実装しても最新の脆弱性パターンには追従しきれないので、eslint-plugin-regexp や sonarjs/slow-regex など、長年コミ ュニティで磨かれた資産を活用するのが合理的
  14. flyle 1 / 42 | 外部プラグインとカスタムルールの守備範囲 両方必要なのは、守備範囲が違う から 🧩 外部プラグイン

    「組織を問わず通用する規範」 例: フレームワーク誤用 / 型安全 / セキュリティの一般原則 / コード品質 / 正規表現 ... 🛠️ カスタムルール 「組織ごとに違う方針」 例: フレームワーク利用ルール / 自社のセキュリティ要件 / マ ルチテナント要件 / 型安全ルール ...
  15. flyle 1 / 42 | 利用ルールの例: Honoのリクエスト検証を必須化 ❌ スキーマ検証なしで取得 ✅

    スキーマ検証してから取得 ⚠️ カスタムルールが無いとどう困るか 直接呼ぶと ランタイム検証が行われず、不正な入力も handler に流れ込む 戻り値が検証されていない型で返るため、handler 内で個別に検証・型変換するコードが各所に散らばる app.put('/users/:id', async (c) => { // body: any ( 検証なし) const body = await c.req.json() await updateUser(body) }) app.put( '/users/:id', vValidator('json', UpdateUserBody), async (c) => { // body: UpdateUserBody ( 検証済み・型付き) const body = c.req.valid('json') await updateUser(body) }, )
  16. flyle 1 / 42 | セキュリティ要件の例: Open Redirect 検出 ❌

    ユーザー入力をそのまま渡す ✅ 許可済みの遷移先だけ通す ⚠️ カスタムルールが無いとどう困るか 悪意のある外部URLへ誘導され、信頼ドメインを利用したフィッシングの踏み台などに悪用される可能性 1度マージされてリリースされたら、本番のセキュリティインシデント 直結 app.get( '/redirect/:url', vValidator('param', RedirectParam), (c) => { const { url } = c.req.valid('param') return c.redirect(url) }, ) app.get( '/redirect', vValidator('query', RedirectQuery), (c) => { const { to } = c.req.valid('query') if (to === 'home') return c.redirect('/') return c.redirect('/dashboard') }, )
  17. flyle 1 / 42 | 型ではカバーしきれない領域もカスタムルールで補完 1日目のセッション 権限チェックの一貫性を型で守る ─ TypeScriptによる多層防御

    型で守れる: ビジネスロジック層の権限チェック 型ではカバーしきれない領域: UI レベルの権限制御 SQL レベルの権限制御 これらはカスタムルールで補完
  18. flyle 1 / 42 | 権限要件の例: UI で権限コンポーネントを必須化 ❌権限チェックなしで配置 ✅権限チェックコンポーネントで囲む

    ⚠️ カスタムルールが無いとどう困るか 権限の無いユーザーに UI 上で操作可能に見えてしまい、UX として漏れる サーバー側で権限チェックがあっても、UI 側のチェック漏れは型では検出できない <!-- NG: 権限チェックなしで削除ボタンを置いている --> <Button onclick={deleteRecord}> 削除</Button> <!-- OK: 権限チェックコンポーネントで囲む --> <UserResourcePermission action="TABLE:DELETE" resource={resourcePath} > <Button onclick={deleteRecord}> 削除</Button> </UserResourcePermission>
  19. flyle 1 / 42 | マルチテナント要件の例: SQL の RLS 設定漏れ

    ❌ RLS なし ✅ RLS 強制 ⚠️ カスタムルールが無いとどう困るか 新規テーブルで付け忘れると 他テナントの行がそのまま見える マルチテナントSaaSにとっての致命的なデータ漏洩に繋がる CREATE TABLE invoices ( id uuid PRIMARY KEY, tenant_id uuid NOT NULL ); -- 他テナントの行が見える状態 CREATE TABLE invoices ( ... ); ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
  20. flyle 1 / 42 | カスタムルールはどう増えていくか 個人の気づきと、レビューコメントからの分析の 両面から候補を集める 🚶 ボーイスカウトルール

    同じ指摘を繰り返していると気づいた人が、カス タムルール化を担当 する 🔍 PRコメントから候補抽出 マージ済みPRのレビューコメントを スクリプトで スキャン、ルール候補を抽出する AIレビューと人間レビュー両方が対象
  21. flyle 1 / 42 | 正解が一意なら fixable を定義する 正解が一意なら必ず付与すると、AIが書くコードにも機械的に強制できる ✅

    fixable 定義あり 正解が一意なもの ⚠️ fixable なし 意図の判断が必要なもの // 非破壊ソートの書き方を統一 [...arr].sort() // ❌ arr.toSorted() // ✅ ← fixable で自動修正 try { await dangerousOp() } catch (e) {} // 空のcatch を検出 // 例: fixable で catch ごと削除すると // → 本来は文脈次第で正解が変わるのに、 // 機械的に一律で書き換えられてしまう
  22. flyle 1 / 42 | fixable の実装イメージ meta: { fixable:

    'code' } // ← ① fixable を宣言 CallExpression(node) { // [...arr].sort() の AST 形を判定 const callee = node.callee if (callee.type !== 'MemberExpression') return // foo.bar() の形か if (callee.property.name !== 'sort') return // メソッド名が sort か const target = callee.object if (target.type !== 'ArrayExpression') return // 対象が [...] の配列か if (target.elements[0]?.type !== 'SpreadElement') return // 中身が ... か // [...arr] の arr 部分のテキストを取り出す const arr = context.sourceCode.getText(target.elements[0].argument) context.report({ node, fix(fixer) { // ← ② 修正後のコードを返す return fixer.replaceText(node, `${arr}.toSorted()`) }, }) }
  23. flyle 1 / 42 | rule-tester による宣言的テスト ESLintルールのテストは記述が煩雑になりがちなため、 @typescript-eslint/rule-tester でコード例を並べるだけの宣言的なテストにしている

    入出力を並べるだけでテストが書ける → 高いカバレッジを維持 ruleTester.run('prefer-to-sorted', rule, { valid: [ { code: `arr.toSorted();` }, { code: `arr.sort();` }, // 破壊的は対象外 ], invalid: [{ code: `[...arr].sort();`, output: `arr.toSorted();`, // 修正後を検証 errors: [{ messageId: 'preferToSorted' }], }], })
  24. flyle 1 / 42 | ルール導入時の既存コードへの適用方法 ① fixable 定義あり eslint

    --fix で一括修正 ② 修正が容易な場合 同じPRで違反箇所を一気に対応 ③ 違反件数が多い・修正が複雑な場合 一時的に eslint-disable で仮対応 → 別 PR で再設計や、Devin の cron Workflow で定期解消
  25. flyle 1 / 42 | AIで eslint-disable を定期解消 ① eslint-disable

    の理由記載を必須化 解消可否の判断材料のためeslint-disable には必 ず理由コメントを記載するよう必須化 理由のないeslint-disable は CIで落とす ② 定期的に eslint-disable を解消 Devin の「ESLint ignoreの解決」Workflowを定 期的 にcron実行
  26. flyle 1 / 42 | なぜCI高速化が重要か ESLintワークフローの月別実行回数。直近半年で 約 2.3 倍

    に 📈 PR数の急増 PR数自体が × 2.36 に増え、そのままlint実行回数を押し上げる 🤖 自動修正の再trigger CIでの eslint --fix 結果のbotコミットがさらにCIをtrigger 実行回数を抑えるのは難しいので、1回あたりの高速化 が重要 回数が2倍以上に増えた今、1回あたりの改善が以前より大きく合計コスト・待ち時間に響く
  27. flyle 1 / 42 | 二層lint構成でESLintを速くする Rust製の Oxlint で先回り、ESLint は

    Oxlint で扱えない領域を担う 1段目: Oxlint (Rust製) Rust実装で圧倒的に速い (公式bench: ESLint比 約 50〜100x) 組み込み済みのプラグイン (typescript / unicorn / import 等) 2段目: ESLint 組み込まれていないプラグイン (sonarjs / security 等) 非JS/TSファイル (PostgreSQL / YAML / Svelte 等) 型情報を使うルール 💡 ポイント ESLint は 「Oxlint で拾えないルール」 を走らせる。Oxlint で拾えたものは ESLint 側で重複させない → eslint-plugin-oxlint で Oxlint 側の有効ルールを ESLint 側で自動OFF
  28. flyle 1 / 42 | なぜOxlint一本にできないのか ① 未移植のプラグイン sonarjs /

    security 等、弊社で使うプラグインの一部が Oxlint に現状組み込まれていない ② 非JS/TSファイル PostgreSQL マイグレーション / YAML / Svelte 等は Oxlint 対象外 postgresql-eslint-parser / yaml-eslint-parser 等のカスタムパーサに Oxlint は対応していない ③ 型情報を使うカスタムルール Oxlint の JS plugins は型情報を使わないカスタムルールは書けるが、型情報を使うカスタムルールは未対応 💡 ポイント oxc では Vue / Svelte 等の alt-JS サポートに関する RFC も出されており、② は将来 Oxlint で扱えるかもしれない 🔗 oxc-project/oxc#21936
  29. flyle 1 / 42 | 正解が一意なものはCIで自動修正する 「正解が一意」なものはCIで eslint --fix を実行し、botが自動コミット

    ① PR push で CI起動 → ② CI上で eslint --fix → ③ 差分があれば 自動修正用のbotが commit & push → ④ CIで 結果を再確認 💡 ポイント 対象は正解が一意なものだけで、文脈判断が要るものは自動修正しない 非エンジニアのバイブコーディングなど手元で修正ができないPRにも機械的修正が効き、レビュアーの指摘工数を削減できる
  30. flyle 1 / 42 | 正解が一意でないものはCIで自動修正しない ESLint 違反全般の修正までCI上でAIに自動修正させた時期があったが、問題があり現在は廃止 ⚠️ 試して見えた問題

    正解が一意でないと、 手元修正とCI修正がズレてコンフリクトが頻発 AIは本質的な対処より、 小手先の変更や eslint-disable でエラーを消 すケースが目立つ 💡 教訓 自動修正の境界は 「正解が一意か」 で決める
  31. flyle 1 / 42 | AIが選びがちな小手先の修正例 ❌ 小手先の修正例 このルールは as

    unknown as T での型チェック バイパスを禁止するものだが、eslint-disable コメ ントで通してしまうケースがある ✅ 本質的な修正 値を 実行時に検証して型を保証 する形に直す (設計・文脈の理解が必要) // eslint-disable-next-line ... const user = response.data as unknown as User // 実行時にスキーマ検証して User 型を得る const user = v.parse(UserSchema, response.data)
  32. flyle 1 / 42 | CI エラーの修正はスキルで Claude Code が

    CI 失敗ログを分析して修正するスキルを用意 スキルの起動と最終承認を人が握ることで、AI の修正速度と方針コントロールを両立 ① 発動タイミングを制御可能 bot による常時自動修正ではなく、人がスキルを叩いて起動する → 手元の修正と CI 修正が同時に走る コンフリクトを抑制 ② 修正方針を手元で添削できる スキルはローカルの Claude Code で動くので、修正過程を見ながら push 前に方針を調整 できる → 「eslint-disable で逃げ る」 「型を握りつぶす」など方針逸脱を未然に止められる
  33. flyle 1 / 42 | AIが間違えても 静的解析が止める 静的解析を厚くすることで、AIコーディングの速度に 品質を追従 できる

    AIの確率的な失敗を 決定的に止める 仕組みがあるからこそ、コード量や開発主体が増えても品質を保てる