以下の内容はhttps://blog.inorinrinrin.com/entry/2026/02/24/194907より取得しました。


生成AIでJavaScript/TypeScriptを扱うときに設定しておきたい ESLintルール

このエントリはTSKaigi Mashup Kansai 生成AIでTSを扱うときに考えたい設計&ガードレールでの発表内容です。登壇資料をブログとして読めるように再構成して公開しています。

はじめに

去年あたりは「ジュニアエンジニアレベルと思ってね」と公式各所から言われていたコーディングエージェントも、もはやそのレベルを超えてきました。

今や副操縦席に座っているのは人間の側です。しかし、AIが機長だからといって自由に飛行機を操縦してよいかというとそうではありません。決められた航路や離着陸の手順を守る必要があり、そこを外れないガードレールが必要です。

またこれはAIに限った話でもありません。AIであれ人間であれ、「開発者」はもれなく全員が決められたルールに違反しないようコードを書くべきです。またレビュワーも完璧ではありません。問題に気づかないまま変更を承認してしまうこともあるでしょう。

なので、決められたルールに対する自動検証ツールはいつになっても変わらず必要だと考えています。そのための手段として、ESLintをはじめとする静的解析ツールを適切に設定することはやはり極めて有効な手段です。

そこで今回は自分がガードレールとして設定している以下のESLint(plugin含む)ルールについてお話しします。

プラグイン 主な効果
eslint-plugin-security セキュリティ脆弱性の早期検知
eslint-plugin-unicorn ファイル名規則の統一
@typescript-eslint/naming-convention 命名規則の統一
eslint-plugin-import import順序・依存方向の管理
no-restricted-imports / no-console 意図しないimport・ログの防止
eslint-plugin-jsdoc ドキュメントの品質向上
@vitest/eslint-plugin テストコードの品質向上

eslint-plugin-security

github.com

セキュリティ的にまずい書き方をしていると怒ってくれるルールです。以下のような脆弱性を検知してくれます。

  • detect-eval-with-expression
    • eval(変数)を禁止する
    • 任意コードが実行されるのを防ぐ
  • detect-child-process
    • exec(変数)を禁止する
    • OSコマンドインジェクションを防ぐ
  • detect-possible-timing-attacks
    • == / === によるパスワード・トークン比較を禁止する
    • 文字列比較の速度差から秘密情報が推測されるのを防ぐ
  • detect-unsafe-regex
    • バックトラックが爆発する危険な正規表現を禁止する
    • 悪意ある入力でイベントループが長時間ブロックされるのを防ぐ
  • detect-non-literal-fs-filename
    • fs.readFile(変数) など変数パスのファイル操作
    • ディレクトリトラバーサルを防ぐ

脆弱性診断ツールを使うなどによって脆弱性そのものを検知することは可能です。しかし、ツールを使った診断はプロダクトがある程度形になってから行われるものであり、そのタイミングで脆弱性が検知され修正が発生した場合、修正の規模が大きくなってしまいます。

一方で、開発中から脆弱性を検知することができればそれだけ修正のコストが低くすることができます。

eslint-plugin-unicorn (unicorn/filename-case)

github.com

過去に自分が書いた記事があります。詳細はこちらをご参考に。

zenn.dev

例えば以下のように設定すると、ファイル名がケバブケースに強制されます。

    files: [/** 適用 or 除外したいファイルパターンを書く */],
    rules: {
      'unicorn/filename-case': [
        'error',
        {
          cases: {
            kebabCase: true,
          },
        },
      ],
    },

@typescript-eslint/naming-convention

github.com

例えば以下のように設定することで、各種命名規則を矯正することができる。

      '@typescript-eslint/naming-convention': [
        'error',

        // 変数: camelCase or UPPER_CASE(定数)
        {
          selector: 'variable',
          format: ['camelCase', 'UPPER_CASE'],
        },

        // 関数: camelCase
        {
          selector: 'function',
          format: ['camelCase'],
        },

        // パラメータ: camelCase(未使用は _ prefix 許可)
        {
          selector: 'parameter',
          format: ['camelCase'],
          leadingUnderscore: 'allow',
        },

        // boolean 変数: is/has/should/can/will prefix を強制
        {
          selector: 'variable',
          types: ['boolean'],
          format: ['PascalCase'],
          prefix: ['is', 'has', 'should', 'can', 'will'],
        },

        // クラス・interface・type: PascalCase
        {
          selector: 'typeLike',
          format: ['PascalCase'],
        },

        // interface に I prefix を禁止(IUser → User)
        {
          selector: 'interface',
          format: ['PascalCase'],
          custom: {
            regex: '^I[A-Z]',
            match: false,
          },
        },

        // enum メンバー: PascalCase
        {
          selector: 'enumMember',
          format: ['PascalCase'],
        },

        // private メンバー: _ prefix を強制
        {
          selector: 'memberLike',
          modifiers: ['private'],
          format: ['camelCase'],
          leadingUnderscore: 'require',
        },
      ],
    },

eslint-plugin-import

github.com

importを使う場所でのあれこれを防いでくれる。

import/order

グループごとに順序を定義してくれます。

設定例

      'import/order': ['error', {
        groups: [
          'builtin', // Node.js標準モジュール (fs, path など)
          'external', // npm packages
          'internal', // パスエイリアス (@myapp/* など)
          'parent', // 親ディレクトリ (../)
          'sibling', // 同階層 (./)
          'index', // index ファイル
          'type', // type imports
        ],
        'newlines-between': 'always', // グループ間に空行を強制
        alphabetize: { order: 'asc', caseInsensitive: true }, // 各groupはアルファベット順に並ぶ
        pathGroups: [
          {
            pattern: '@myapp/**',
            group: 'internal',
            position: 'before',
          },
        ],
      }]

import/no-duplicates

同一モジュールの重複importを禁止してくれます。

設定例

'import/no-duplicates': 'error'

OK/NG

// NG
import { foo } from './mod';
import { bar } from './mod';

// OK
import { foo, bar } from './mod';

import/newline-after-import

import宣言の後に空行を強制してくれます。

設定例

'import/newline-after-import': 'error'

OK/NG

// NG
import { foo } from './mod';
const x = foo();

// OK
import { foo } from './mod';

const x = foo();

import/no-restricted-paths

モジュール間の依存方向を強制するルール。target(import先)からfrom(import元)へのimportを禁止する。モジュラーモノリスのアーキテクチャ境界の保護に有効です。

構成例

flowchart TD
    subgraph LAYERS["レイヤー構成"]
        batch["src/batch/**"]
        app["src/app/**"]
        domain["src/domain/**"]
        libs["src/libs/**"]
        subgraph INFRA["src/infra/"]
            repo["repository<br/>(公開インターフェース)"]
            db["db.ts<br/>🔒 直接import禁止"]
        end
    end

    %% 許可された依存方向
    app    -->|"✅ 許可"| domain
    domain -->|"✅ 許可"| libs
    app    -->|"✅ 許可<br/>(interface経由)"| repo
    batch  -->|"✅ 許可<br/>(interface経由)"| repo
    domain -->|"✅ 許可<br/>(interface経由)"| repo
    repo   --> db

    %% 禁止された依存方向
    batch  -->|"❌ 禁止<br/>(Rule 1)"| app
    app    -.->|"❌ 禁止<br/>(Rule 1)"| batch
    libs   -.->|"❌ 禁止<br/>(Rule 2)"| domain
    app    -.->|"❌ 禁止<br/>(Rule 3)"| db
    batch  -.->|"❌ 禁止<br/>(Rule 3)"| db
    domain -.->|"❌ 禁止<br/>(Rule 3)"| db
    libs   -.->|"❌ 禁止<br/>(Rule 3)"| db

    %% スタイル
    style db     fill:#fee2e2,stroke:#ef4444,color:#991b1b
    style batch  fill:#dbeafe,stroke:#3b82f6
    style app    fill:#dbeafe,stroke:#3b82f6
    style domain fill:#d1fae5,stroke:#10b981
    style libs   fill:#fef9c3,stroke:#eab308
    style repo   fill:#f3f4f6,stroke:#6b7280

設定例

      'import/no-restricted-paths': ['error', {
        zones: [
          // appレイヤーからbatchレイヤーへの依存を禁止
          {
            target: './src/app//*',
            from: './src/batch//',
            message: 'app cannot import from batch.',
          },
          // batchレイヤーからappレイヤーへの依存を禁止
          {
            target: './src/batch//*',
            from: './src/app//',
            message: 'batch cannot import from batch.',
          },
          // 共有ライブラリからドメイン層への依存を禁止
          {
            target: './src/libs/**/',
            from: './src/domain//*',
            message: 'libs cannot depend on domain.',
          },
          // 特定ファイルへの直接importを禁止し、公開インターフェース経由を強制
          {
            target: './src/!(infra)//*',
            from: './src/infra/db.ts',
            message: 'Use repository interface instead of importing db.ts directly.',
          },
        ],
      }]

ESLint本体に組み込まれているコアルール系

no-restricted-imports

特定 export の import を禁止します。ACL層別使い分けなどに有効です。

構成例

flowchart TD
    subgraph LIB["@myapp/logger"]
        appLogger
        adminLogger
        batchLogger
        workerLogger
    end

    subgraph APP["src/app/"]
        A1["アプリケーション層"]
    end

    subgraph ADMIN["src/admin/"]
        A2["管理画面層"]
    end

    subgraph BATCH["src/batch/"]
        A3["バッチ処理層"]
    end

    subgraph WORKER["src/worker/"]
        A4["ワーカー層"]
    end

    appLogger    -->|"✅ 許可"| A1
    adminLogger  -->|"✅ 許可"| A2
    batchLogger  -->|"✅ 許可"| A3
    workerLogger -->|"✅ 許可"| A4

    adminLogger  -.->|"❌ 禁止"| A1
    batchLogger  -.->|"❌ 禁止"| A1
    workerLogger -.->|"❌ 禁止"| A1

    appLogger    -.->|"❌ 禁止"| A2
    batchLogger  -.->|"❌ 禁止"| A2
    workerLogger -.->|"❌ 禁止"| A2

    appLogger    -.->|"❌ 禁止"| A3
    adminLogger  -.->|"❌ 禁止"| A3
    workerLogger -.->|"❌ 禁止"| A3

    appLogger    -.->|"❌ 禁止"| A4
    adminLogger  -.->|"❌ 禁止"| A4
    batchLogger  -.->|"❌ 禁止"| A4

設定例

      // src/app/** → appLogger のみ許可
      files: ['src/app/**/*.{ts,tsx}'],
      rules: {
        'no-restricted-imports': ['error', {
          paths: [{
            name: '@myapp/logger',
            importNames: ['adminLogger', 'batchLogger', 'workerLogger'], // 禁止するものを書く
            message: 'app layer can only import appLogger.',
          }],
        }],
      },

OK/NG

// src/app/service.ts

// OK
import { appLogger } from '@myapp/logger';

// NG: app層でbatchLoggerは使用不可
import { batchLogger } from '@myapp/logger';

no-console

コード中にconsole.xxが残っていると怒られる。allowオプションで特定メソッドのみ許可できる。本番コードでのconsole.log混入防止に有効。

設定例

'no-console': ['error', { allow: ['warn', 'error'] }]

OK/NG

// NG
console.log('debug');
console.debug('debug');

// OK
console.warn('something went wrong');
console.error('fatal error');

eslint-plugin-jsdoc

github.com

関数・クラス・メソッドへのJSDoc付与を促進する。files,ignoresを指定することで、特定のファイルに対してだけ有効にできます*1。warnにしておくと段階的な導入がしやすいです。0->1のプロジェクトであれば最初からerrorでいいと思います。

設定例

{
  plugins: { jsdoc: pluginJsdoc },
  rules: {
        // JSDocコメント自体の存在を要求
        'jsdoc/require-jsdoc': ['warn', {
            require: {
                FunctionDeclaration: true,
                ArrowFunctionExpression: true,
                ClassDeclaration: true,
                MethodDefinition: true,
            },
        }],
        // @param タグの記述を要求
        'jsdoc/require-param': 'warn',
        // @returns タグの記述を要求
        'jsdoc/require-returns': 'warn',
        // description(説明文)の記述を要求
        'jsdoc/require-description': 'warn',
    },
}

OK/NG例

  // NG
  const greet = (name: string): string => {
  return Hello, ${name};
};

// OK
  /**
    挨拶文を返す
    @param name - 対象の名前
    @returns 挨拶文字列
  */
  const greet = (name: string): string => {
  return Hello, ${name};
};

@vitest/eslint-plugin

github.com

過去に自分が書いた記事があります。

zenn.dev

  • consistent-test-it
    • テストの記述をitかtestに統一できる
  • no-conditional-xxx系
    • テストの中でifなどの条件分岐を用いてアサーションを変えたり、実行するテストを変えたりするのを禁止できる
  • prefer-mock-promise-shorthand
    • mockReturnValueとかmockImplementationのなかでPromiseを使わせないようにする
    • vitestが用意してくれているmockRejectedValue, mockResolvedValueを使うよう矯正できる
  • require-to-throw-message
    • toThrowなど例外をアサーションする際には、エラーメッセージも併せて検証するように矯正してくれる
  • require-top-level-describe
    • テストコードにてネスト可能なdescribeの数を制限できる
    • describeが多すぎる場合テストの構成を見直すことにつながる

余談

ガードレールではありませんが、気に入っているESLint Pluginとその考え方を紹介させてください。

@stylistic/eslint-plugin

eslint.style

フォーマッターの役割を果たすplugin。Anthonyの作品。JS/TS環境でフォーマッターといえばPrettierを使ってる人も多いイメージがあります。が、僕は最近はもうこれにフォーマッターの役割を移しています。なんでPrettierを使わないか?という理由を詳しく書くと長くなるのでAnthonyのブログに説明を譲ります。

antfu.me

以下、簡潔にPrettierを使わない理由をまとめます。以下の3点以外にもセキュリティ的にもできるだけ余計なライブラリは入れたくない というのもあります。先に紹介したeslint-plugin-jsdocやeslint-plugin-importなどと同等の役割を果たすツールは存在します。が、やはりESLint一つあれば全て同じルールで設定できるという統一感と、pluginの手数の多さがESLintの魅力だと考えています

1. 設定の柔軟性がない

Prettier は意図的に「Opinionated」なツールとして設計されており、提供するオプションを最小限に絞ることをポリシーとしています。 つまり、「気に入らない挙動があっても設定で直せない」 という状況が発生する。一方でESLint であればルールごとに細かく ON/OFF や挙動を制御できるのに対し、Prettier はそれができません。

2. 行折り返しの問題

PrettierはprintWidth(デフォルト80文字)を超えると自動的に改行が挿入されます。

  // 入力 -- 1行で読みやすい
  foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne());

  // Prettier 出力 -- 不必要に分割される
  foo(
    reallyLongArg(),
    omgSoManyParameters(),
    IShouldRefactorThis(),
    isThereSeriouslyAnotherOne()
  );
  • 引数を1つ追加するだけで、関係のない行まで差分に含まれる。Git diff が汚くなる。
  • printWidth を大きくしても完全には解決しない
  • この動作を完全に無効化できない

3. ESLint との競合

二重管理・二重パースの非効率さがあります。

  • ESLint にもコードスタイルを整形するルールがあり、Prettier と役割が被る
  • eslint-config-prettiereslint-plugin-prettier を導入して競合を解消する必要がある
  • ESLintPrettier がそれぞれ独立してコードをパースするため、同じファイルを2回解析する

おわりに

面白いことにAIが進化してくれたおかげで自分が欲しいESLintのルールをAIに作らせることも以前より簡単になりました。自分はHonoやUnJSのESLint Pluginを自作しています(よかったらStarをください...!!)。

github.com

github.com

みなさんも自チームの状況に合わせて、ぜひESLint Pluginを自作してみてはいかがでしょうか?

*1:例えばStoryBook使いながらtsxのコンポーネント本体にコメントとかいらないので除外するといったユースケースがある。他にも設定ファイルなども除外しておきたい。




以上の内容はhttps://blog.inorinrinrin.com/entry/2026/02/24/194907より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14