Slide 1

Slide 1 text

flyle 静的解析への投資が AI時代のコード品質を支える カスタムESLintルールの設計と運用 工藤 颯斗 / Flyle TSKaigi 2026

Slide 2

Slide 2 text

flyle 1 / 42 | 自己紹介 工藤 颯斗 / Hayato Kudo 株式会社Flyle Software Engineer リニューアル版 Flyle の開発に従事 TypeScript / Svelte / Hono @metalic_kudo_h

Slide 3

Slide 3 text

flyle 1 / 42 | FLYLE, INC. 埋もれる VoC、5% しか聞けない品質評価、終わらない後処理 コンタクトセンターの 対話データ から AI 変革 VoC 分析・応対品質改善・ACW 削減・ リスク検知まで改善する コンタクトセンター・CX の AI 変革 パートナー 多様な業界の大手企業のコンタクトセンター・CX 部門で導入

Slide 4

Slide 4 text

flyle 1 / 42 | 目次 1 開発生産性の変化 弊社で起きていること 2 静的解析の必要性 AI時代に静的解析が重要な理由 3 なぜ ESLint カスタムルールを書くのか 外部プラグインで足りる領域と、足りない領域 4 カスタムルールのライフサイクル どう増やし、どう保つか 5 CI運用 高速化とAI自動修正の限界

Slide 5

Slide 5 text

CHAPTER 開発生産性の変化 flyle

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

flyle 1 / 42 | コード生成量・PR数が増えた理由 「1人あたりのコード生成量」と「コードを書ける人」が同時に増えた ① エンジニアのAI活用 Cursor / Claude Codeなどを日常的に活用 1人あたりのコード生成量が大幅に向上 ② 非エンジニアのバイブコーディング PM / デザイナー / ビジネスチームがバイブコー ディングでPRを立てるように 「コードを書ける人」が組織で増加

Slide 9

Slide 9 text

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倍以上

Slide 10

Slide 10 text

flyle 1 / 42 | バイブコーディングのフロー PM・デザイナー・ビジネスチーム も Slack経由でAIエージェントに依頼してPRを立てる 軽微な不具合や機能アイデアを見つけたとき、 起票せずに直接AIエージェントへ指示 → PR化 → エンジニアがレ ビュー ⚠️ 補足 この運用は プロダクトが初期フェーズ で不具合・意見を吸い上げるため に効果的だったが、一方でリリース内容に周囲が追いつけずカオスにな る側面もあり、すべてのプロダクトに有効とは限らない

Slide 11

Slide 11 text

flyle 1 / 42 | 視覚的に見るコード増減 GitHub Insights (週次の追加・削除コード行数) Devinを導入してから追加コード量が 安定的に高水準 に ※ 2025/7の大スパイクは外れ値(大規模リファクタリング)。それを除いても以降の増加傾向は明確

Slide 12

Slide 12 text

CHAPTER 静的解析の必要性 AI時代の開発で、なぜ静的解析が重要なのか flyle

Slide 13

Slide 13 text

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 }

Slide 14

Slide 14 text

flyle 1 / 42 | レビューで指摘する運用はスケールしない コード生成量が 約3倍 になった今、全てのPRで人間が指摘・修正していたら レビュアーがボトルネックになり、スケールしない ❌ スケールしない理由 同じ指摘を毎PRで繰り返すことに AIに PRレビューで都度伝えても 次PRで再発 レビュアーの時間が制約違反の指摘に消える ✅ 静的解析が担っていること 制約違反は 必ず CIで止まる バイブコーディングはCIを通してからPRを出す ので、レビュー時点で違反は潰れている レビュアーは 設計・意図の議論 に集中できる

Slide 15

Slide 15 text

flyle 1 / 42 | AIは確率的、静的解析は決定的 🤖 AI 同じプロンプトでも違うコード 「たぶん正しい」しか保証できない 確率的・非決定的 🛡️ 静的解析 同じASTには同じ結果 違反は 必ず 検出する 決定的・再現可能

Slide 16

Slide 16 text

flyle 1 / 42 | 静的解析が大量PRを支えている 元々の 「機械的にできるレビューは自動化する」 カルチャーで、 制約違反を事前に潰せる基盤が育っていたからこそ、PR数増加にマージ数が追従できた PR を立てた数 × 2.75 44.5/週 → 122.5/週 PR マージ数 × 2.36 39.2/週 → 92.7/週

Slide 17

Slide 17 text

CHAPTER なぜ ESLint カスタムルールを書くのか 外部プラグインで足りる領域と、足りない領域の見極め flyle

Slide 18

Slide 18 text

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 など、長年コミ ュニティで磨かれた資産を活用するのが合理的

Slide 19

Slide 19 text

flyle 1 / 42 | それでも 110 個のカスタムルールがある 外部プラグインを積極的に使った上で、自社方針を機械化するために積み上げてきた 採用している外部プラグイン 15 自社カスタムルール 110

Slide 20

Slide 20 text

flyle 1 / 42 | 外部プラグインとカスタムルールの守備範囲 両方必要なのは、守備範囲が違う から 🧩 外部プラグイン 「組織を問わず通用する規範」 例: フレームワーク誤用 / 型安全 / セキュリティの一般原則 / コード品質 / 正規表現 ... 🛠️ カスタムルール 「組織ごとに違う方針」 例: フレームワーク利用ルール / 自社のセキュリティ要件 / マ ルチテナント要件 / 型安全ルール ...

Slide 21

Slide 21 text

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) }, )

Slide 22

Slide 22 text

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') }, )

Slide 23

Slide 23 text

flyle 1 / 42 | 型ではカバーしきれない領域もカスタムルールで補完 1日目のセッション 権限チェックの一貫性を型で守る ─ TypeScriptによる多層防御 型で守れる: ビジネスロジック層の権限チェック 型ではカバーしきれない領域: UI レベルの権限制御 SQL レベルの権限制御 これらはカスタムルールで補完

Slide 24

Slide 24 text

flyle 1 / 42 | 権限要件の例: UI で権限コンポーネントを必須化 ❌権限チェックなしで配置 ✅権限チェックコンポーネントで囲む ⚠️ カスタムルールが無いとどう困るか 権限の無いユーザーに UI 上で操作可能に見えてしまい、UX として漏れる サーバー側で権限チェックがあっても、UI 側のチェック漏れは型では検出できない 削除 削除

Slide 25

Slide 25 text

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;

Slide 26

Slide 26 text

CHAPTER カスタムルールの ライフサイクル flyle

Slide 27

Slide 27 text

flyle 1 / 42 | カスタムルールはどう増えていくか 個人の気づきと、レビューコメントからの分析の 両面から候補を集める 🚶 ボーイスカウトルール 同じ指摘を繰り返していると気づいた人が、カス タムルール化を担当 する 🔍 PRコメントから候補抽出 マージ済みPRのレビューコメントを スクリプトで スキャン、ルール候補を抽出する AIレビューと人間レビュー両方が対象

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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()`) }, }) }

Slide 30

Slide 30 text

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' }], }], })

Slide 31

Slide 31 text

flyle 1 / 42 | ルール導入時の既存コードへの適用方法 ① fixable 定義あり eslint --fix で一括修正 ② 修正が容易な場合 同じPRで違反箇所を一気に対応 ③ 違反件数が多い・修正が複雑な場合 一時的に eslint-disable で仮対応 → 別 PR で再設計や、Devin の cron Workflow で定期解消

Slide 32

Slide 32 text

flyle 1 / 42 | AIで eslint-disable を定期解消 ① eslint-disable の理由記載を必須化 解消可否の判断材料のためeslint-disable には必 ず理由コメントを記載するよう必須化 理由のないeslint-disable は CIで落とす ② 定期的に eslint-disable を解消 Devin の「ESLint ignoreの解決」Workflowを定 期的 にcron実行

Slide 33

Slide 33 text

CHAPTER CI運用での工夫と学び 高速化・機械的な自動修正・人間判断の境界 flyle

Slide 34

Slide 34 text

flyle 1 / 42 | なぜCI高速化が重要か ESLintワークフローの月別実行回数。直近半年で 約 2.3 倍 に 📈 PR数の急増 PR数自体が × 2.36 に増え、そのままlint実行回数を押し上げる 🤖 自動修正の再trigger CIでの eslint --fix 結果のbotコミットがさらにCIをtrigger 実行回数を抑えるのは難しいので、1回あたりの高速化 が重要 回数が2倍以上に増えた今、1回あたりの改善が以前より大きく合計コスト・待ち時間に響く

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

flyle 1 / 42 | 正解が一意なものはCIで自動修正する 「正解が一意」なものはCIで eslint --fix を実行し、botが自動コミット ① PR push で CI起動 → ② CI上で eslint --fix → ③ 差分があれば 自動修正用のbotが commit & push → ④ CIで 結果を再確認 💡 ポイント 対象は正解が一意なものだけで、文脈判断が要るものは自動修正しない 非エンジニアのバイブコーディングなど手元で修正ができないPRにも機械的修正が効き、レビュアーの指摘工数を削減できる

Slide 38

Slide 38 text

flyle 1 / 42 | 正解が一意でないものはCIで自動修正しない ESLint 違反全般の修正までCI上でAIに自動修正させた時期があったが、問題があり現在は廃止 ⚠️ 試して見えた問題 正解が一意でないと、 手元修正とCI修正がズレてコンフリクトが頻発 AIは本質的な対処より、 小手先の変更や eslint-disable でエラーを消 すケースが目立つ 💡 教訓 自動修正の境界は 「正解が一意か」 で決める

Slide 39

Slide 39 text

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)

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

flyle 1 / 42 | AIが間違えても 静的解析が止める 静的解析を厚くすることで、AIコーディングの速度に 品質を追従 できる AIの確率的な失敗を 決定的に止める 仕組みがあるからこそ、コード量や開発主体が増えても品質を保てる

Slide 42

Slide 42 text

flyle 1 / 42 | Thank you! ご清聴ありがとうございました 工藤 颯斗 / Flyle @metalic_kudo_h