T-CREATOR

ESLint の extends が効かない問題を斬る:Flat Config の files/ignores 落とし穴

ESLint の extends が効かない問題を斬る:Flat Config の files/ignores 落とし穴

ESLint の設定で extends が突然効かなくなった経験はありませんか?特に Flat Config に移行した際、多くの開発者が「なぜ設定が適用されないのか」と頭を抱えています。

実は、この問題の背景には Flat Config における files と ignores の設定方法に関する「落とし穴」が存在します。従来の設定方式とは根本的に異なるアプローチが必要で、単純に設定を移行しただけでは期待通りに動作しないのです。

この記事では、extends が効かない具体的な原因を特定し、確実に動作する解決方法をお示しします。

背景

ESLint Flat Config とは

ESLint Flat Config は、ESLint v8.21.0 で導入された新しい設定方式です。従来の .eslintrc ファイルに代わり、eslint.config.js を使用します。

javascript// 新しい Flat Config の基本構造
export default [
  {
    files: ['**/*.js'],
    rules: {
      'no-unused-vars': 'error',
    },
  },
];

Flat Config では、設定を配列として定義し、各要素が特定のファイルパターンに対するルールセットを表現します。この配列の順序が設定の適用順序を決定する重要な要素となります。

以下の図で、Flat Config の基本構造を理解しましょう。

mermaidflowchart TD
  config[eslint.config.js] --> array[設定配列]
  array --> obj1[設定オブジェクト1]
  array --> obj2[設定オブジェクト2]
  array --> obj3[設定オブジェクト3]
  obj1 --> files1[files: パターン1]
  obj1 --> rules1[rules: ルール1]
  obj2 --> files2[files: パターン2]
  obj2 --> ignores2[ignores: 除外パターン]
  obj2 --> extends2[extends: 拡張設定]
  obj3 --> global[グローバル設定]

補足: 配列の順序が設定の優先順位を決定し、後に定義された設定が前の設定を上書きします。

従来の設定方式との違い

従来の .eslintrc では、extends と overrides を組み合わせて設定を構築していました。

javascript// 従来の .eslintrc.js
module.exports = {
  extends: ['eslint:recommended'],
  overrides: [
    {
      files: ['*.ts'],
      extends: ['@typescript-eslint/recommended'],
    },
  ],
};

一方、Flat Config では設定オブジェクトを配列として並べ、より直線的な構造を採用しています。これにより設定の適用順序が明確になりましたが、同時に新たな複雑さも生まれました。

項目従来設定Flat Config
ファイル名.eslintrc.jseslint.config.js
構造オブジェクト配列
継承方式extends + overrides配列の順序による上書き
ファイル指定overrides.files各設定の files
除外設定.eslintignoreignores プロパティ

extends の役割と期待される動作

extends は、他の設定を継承するための仕組みです。Flat Config では、2025 年の更新により extends が再導入され、より使いやすくなりました。

javascript// extends の基本的な使用方法
import { defineConfig } from 'eslint/config';
import js from '@eslint/js';

export default defineConfig([
  {
    files: ['**/*.js'],
    extends: ['js/recommended'],
    rules: {
      'prefer-const': 'off',
    },
  },
]);

extends は以下の順序で設定を適用します:

  1. 基底設定の読み込み
  2. 継承設定の適用
  3. 個別ルールによる上書き

しかし、files や ignores の設定が不適切だと、この継承チェーンが正しく機能しません。

課題

extends が効かない具体的な症状

多くの開発者が遭遇する典型的な症状をご紹介します。

症状 1: 部分的な設定適用

javascript// 問題のある設定例
export default [
  {
    extends: ['eslint:recommended'],
    rules: {
      'no-console': 'error',
    },
  },
  {
    files: ['src/**/*.js'],
    rules: {
      'prefer-const': 'error',
    },
  },
];

この設定では、eslint:recommended が全ファイルに適用されるはずですが、src​/​**​/​*.js にマッチするファイルでのみ prefer-const が適用されます。

症状 2: TypeScript ファイルでの設定無視

javascript// TypeScript ファイルで設定が適用されない例
export default [
  {
    files: ['**/*.js'],
    extends: ['eslint:recommended'],
  },
];

この設定は .js ファイルのみを対象としているため、.ts ファイルには extends が適用されません。

files と ignores の設定ミス

files と ignores の設定は、extends の適用範囲を決定する重要な要素です。

以下の図で、files と ignores の関係を理解しましょう。

mermaidflowchart LR
  all[全ファイル] --> files{files パターン}
  files -->|マッチ| target[対象ファイル]
  files -->|非マッチ| exclude1[除外ファイル]
  target --> ignores{ignores パターン}
  ignores -->|マッチ| exclude2[除外ファイル]
  ignores -->|非マッチ| final[最終対象ファイル]
  final --> lint[リント実行]

補足: files でフィルタリングされた後、ignores でさらに除外処理が行われます。

よくある間違い 1: グロブパターンの記述ミス

javascript// 間違った files パターン
export default [
  {
    files: ["src/*.js"], // サブディレクトリが含まれない
    extends: ["eslint:recommended"]
  }
];

// 正しい files パターン
export default [
  {
    files: ["src/**/*.js"], // サブディレクトリも含む
    extends: ["eslint:recommended"]
  }
];

よくある間違い 2: ignores の配置順序

javascript// 間違った ignores の配置
export default [
  {
    files: ['**/*.js'],
    ignores: ['dist/**'], // この位置では期待通りに動作しない
    extends: ['eslint:recommended'],
  },
];

設定の優先順位問題

Flat Config では、配列の後方に配置された設定が前方の設定を上書きします。この仕組みを理解せずに設定を記述すると、予期しない動作を引き起こします。

javascript// 設定の上書きが発生する例
export default [
  {
    files: ['**/*.js'],
    extends: ['eslint:recommended'],
    rules: {
      'no-console': 'error',
    },
  },
  {
    files: ['**/*.js'],
    rules: {
      'no-console': 'off', // 前の設定を上書き
    },
  },
];

この場合、2 番目の設定オブジェクトが 1 番目の no-console: "error" を上書きしてしまいます。

図で理解できる要点:

  • 設定の適用順序は配列の順番で決まる
  • files パターンのマッチング精度が重要
  • ignores は適切な位置に配置する必要がある

解決策

正しい files パターンの指定方法

files パターンを正確に指定することで、extends の適用範囲を制御できます。

基本的なパターン記述

javascript// 推奨される files パターンの指定方法
export default [
  {
    // JavaScript ファイル全般
    files: ['**/*.{js,mjs,cjs}'],
    extends: ['eslint:recommended'],
  },
  {
    // TypeScript ファイル全般
    files: ['**/*.{ts,tsx}'],
    extends: ['@typescript-eslint/recommended'],
  },
  {
    // React ファイル
    files: ['**/*.{jsx,tsx}'],
    extends: ['plugin:react/recommended'],
  },
];

特定ディレクトリに限定した設定

javascript// ディレクトリ別の設定適用
export default [
  {
    files: ['src/**/*.js'],
    extends: ['eslint:recommended'],
    rules: {
      'no-console': 'error',
    },
  },
  {
    files: ['tests/**/*.js'],
    extends: ['eslint:recommended'],
    rules: {
      'no-console': 'off', // テストファイルでは console.log を許可
    },
  },
];

複数条件の組み合わせ

javascript// 複雑なファイルパターンの組み合わせ
export default [
  {
    files: [
      'src/**/*.js',
      'lib/**/*.js',
      '!src/**/*.test.js', // テストファイルは除外
    ],
    extends: ['eslint:recommended'],
  },
];

ignores の適切な使い方

ignores の適切な使用方法を理解することで、不要なファイルのリントを回避できます。

グローバル ignores の設定

2025 年の更新により、globalIgnores ヘルパーが導入されました。

javascript// globalIgnores ヘルパーの使用
import { defineConfig, globalIgnores } from 'eslint/config';

export default defineConfig([
  // グローバル ignore は配列の最初に配置
  globalIgnores([
    'dist/**',
    'build/**',
    'node_modules/**',
    'coverage/**',
  ]),
  {
    files: ['**/*.js'],
    extends: ['eslint:recommended'],
  },
]);

条件付き ignores の設定

javascript// 特定の設定でのみ適用される ignores
export default [
  {
    files: ['src/**/*.js'],
    ignores: [
      'src/**/*.min.js', // minify されたファイル
      'src/vendor/**', // サードパーティコード
    ],
    extends: ['eslint:recommended'],
  },
];

ignores の継承を避ける方法

javascript// ignores が他の設定に影響しないようにする
export default [
  {
    // グローバル設定(ignores なし)
    files: ['**/*.js'],
    extends: ['eslint:recommended'],
  },
  {
    // 特定ディレクトリの設定
    files: ['src/legacy/**/*.js'],
    ignores: ['src/legacy/vendor/**'],
    rules: {
      // レガシーコード用の緩い設定
      'no-var': 'off',
    },
  },
];

extends と併用する際の注意点

extends を他の設定要素と併用する際のベストプラクティスをご紹介します。

defineConfig の活用

javascript// defineConfig による型安全な設定
import { defineConfig } from 'eslint/config';
import js from '@eslint/js';
import typescript from '@typescript-eslint/eslint-plugin';

export default defineConfig([
  {
    files: ['**/*.js'],
    plugins: { js },
    extends: ['js/recommended'],
    rules: {
      'prefer-const': 'error',
    },
  },
  {
    files: ['**/*.ts'],
    plugins: { typescript },
    extends: ['typescript/recommended'],
    rules: {
      '@typescript-eslint/no-unused-vars': 'error',
    },
  },
]);

プラグイン設定との組み合わせ

javascript// プラグインと extends の適切な組み合わせ
export default [
  {
    files: ['**/*.js'],
    plugins: {
      js: require('@eslint/js'),
    },
    extends: ['js/recommended'],
  },
  {
    files: ['**/*.jsx'],
    plugins: {
      react: require('eslint-plugin-react'),
    },
    extends: ['js/recommended', 'react/recommended'],
    settings: {
      react: {
        version: 'detect',
      },
    },
  },
];

設定の分割と管理

javascript// 設定の分割による管理しやすい構造
const baseConfig = {
  files: ['**/*.js'],
  extends: ['eslint:recommended'],
  rules: {
    'no-console': 'warn',
  },
};

const reactConfig = {
  files: ['**/*.jsx'],
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
  ],
  rules: {
    'react/prop-types': 'error',
  },
};

const testConfig = {
  files: ['**/*.test.js'],
  extends: ['eslint:recommended'],
  rules: {
    'no-console': 'off',
  },
};

export default [baseConfig, reactConfig, testConfig];

具体例

問題のある設定例

実際のプロジェクトでよく見られる問題のある設定例をご紹介します。

問題例 1: files パターンの不備

javascript// ❌ 問題のある設定
export default [
  {
    // TypeScript ファイルが対象外
    files: ['*.js'],
    extends: ['eslint:recommended'],
  },
  {
    // サブディレクトリが対象外
    files: ['src/*.ts'],
    extends: ['@typescript-eslint/recommended'],
  },
];

この設定の問題点:

  • *.js はルートディレクトリのファイルのみ対象
  • src​/​*.ts はサブディレクトリが含まれない
  • 多くのファイルでリントが実行されない

問題例 2: ignores の配置ミス

javascript// ❌ ignores の配置が不適切
export default [
  {
    files: ['**/*.js'],
    extends: ['eslint:recommended'],
    ignores: ['dist/**'], // この位置では期待通りに動作しない
  },
  {
    ignores: ['node_modules/**'], // 設定対象外のため無効
    rules: {
      'no-console': 'error',
    },
  },
];

この設定の問題点:

  • ignores がファイル指定と同じオブジェクト内にある
  • グローバル ignores が適切に設定されていない

問題例 3: 設定の重複と競合

javascript// ❌ 設定の重複による予期しない動作
export default [
  {
    files: ['**/*.js'],
    extends: ['eslint:recommended'],
    rules: {
      'no-console': 'error',
    },
  },
  {
    // 同じファイルパターンで競合
    files: ['**/*.js'],
    extends: ['plugin:prettier/recommended'],
    rules: {
      'no-console': 'off', // 前の設定を上書き
    },
  },
];

修正後の設定例

上記の問題を解決した適切な設定例をご紹介します。

修正例 1: 適切な files パターン

javascript// ✅ 修正後の設定
import { defineConfig, globalIgnores } from 'eslint/config';

export default defineConfig([
  // グローバル除外設定
  globalIgnores(['dist/**', 'build/**', 'node_modules/**']),
  {
    // JavaScript ファイル全般
    files: ['**/*.{js,mjs,cjs}'],
    extends: ['eslint:recommended'],
    rules: {
      'no-console': 'warn',
    },
  },
  {
    // TypeScript ファイル全般
    files: ['**/*.{ts,tsx}'],
    extends: [
      'eslint:recommended',
      '@typescript-eslint/recommended',
    ],
    rules: {
      '@typescript-eslint/no-unused-vars': 'error',
    },
  },
]);

修正例 2: 階層化された設定

javascript// ✅ プロジェクト構造に合わせた階層化設定
export default defineConfig([
  globalIgnores(['dist/**', 'coverage/**']),

  // ベース設定
  {
    files: ['**/*.{js,ts}'],
    extends: ['eslint:recommended'],
    rules: {
      'no-console': 'warn',
      'prefer-const': 'error',
    },
  },

  // TypeScript 専用設定
  {
    files: ['**/*.ts'],
    extends: ['@typescript-eslint/recommended'],
    rules: {
      '@typescript-eslint/explicit-function-return-type':
        'warn',
    },
  },

  // React 専用設定
  {
    files: ['src/components/**/*.{jsx,tsx}'],
    extends: [
      'plugin:react/recommended',
      'plugin:react-hooks/recommended',
    ],
    rules: {
      'react/prop-types': 'error',
    },
  },

  // テストファイル専用設定
  {
    files: ['**/*.{test,spec}.{js,ts}'],
    extends: ['plugin:jest/recommended'],
    rules: {
      'no-console': 'off',
    },
  },
]);

修正例 3: モノレポ対応設定

javascript// ✅ モノレポプロジェクト向け設定
export default defineConfig([
  globalIgnores([
    '**/node_modules/**',
    '**/dist/**',
    '**/build/**',
  ]),

  // 共通設定
  {
    files: ['packages/*/src/**/*.{js,ts}'],
    extends: ['eslint:recommended'],
    rules: {
      'no-console': 'error',
    },
  },

  // フロントエンドパッケージ
  {
    files: ['packages/frontend/src/**/*.{jsx,tsx}'],
    extends: [
      'eslint:recommended',
      'plugin:react/recommended',
    ],
    rules: {
      'react/react-in-jsx-scope': 'off',
    },
  },

  // バックエンドパッケージ
  {
    files: ['packages/backend/src/**/*.ts'],
    extends: [
      'eslint:recommended',
      '@typescript-eslint/recommended',
    ],
    rules: {
      'no-console': 'off', // サーバーサイドでは console.log を許可
      '@typescript-eslint/no-explicit-any': 'error',
    },
  },
]);

検証方法とデバッグ手順

設定が正しく適用されているかを確認するための手順をご紹介します。

基本的な検証コマンド

bash# 設定の確認
yarn eslint --print-config src/components/Button.tsx

# 特定ファイルのリント実行
yarn eslint src/components/Button.tsx --debug

# 設定ファイル全体の検証
yarn eslint . --debug 2>&1 | grep "Using config"

VS Code での設定確認

VS Code で Flat Config を有効にする設定:

json// .vscode/settings.json
{
  "eslint.experimental.useFlatConfig": true,
  "eslint.debug": true
}

トラブルシューティング用の設定

javascript// デバッグ用の詳細設定
export default defineConfig([
  {
    // デバッグ情報を含む設定
    name: 'debug-base-config',
    files: ['**/*.js'],
    extends: ['eslint:recommended'],
    rules: {
      'no-console': 'warn',
    },
  },
  {
    name: 'debug-typescript-config',
    files: ['**/*.ts'],
    extends: ['@typescript-eslint/recommended'],
    rules: {
      '@typescript-eslint/no-unused-vars': 'error',
    },
  },
]);

設定の動作確認スクリプト

javascript// scripts/verify-eslint-config.js
const { ESLint } = require('eslint');

async function verifyConfig() {
  const eslint = new ESLint();

  // 設定の確認
  const config = await eslint.calculateConfigForFile(
    'src/index.ts'
  );
  console.log(
    'Config for src/index.ts:',
    JSON.stringify(config, null, 2)
  );

  // ファイルの対象確認
  const isIgnored = await eslint.isPathIgnored(
    'src/index.ts'
  );
  console.log('Is src/index.ts ignored:', isIgnored);
}

verifyConfig().catch(console.error);

まとめ

ESLint Flat Config での extends が効かない問題は、主に files と ignores の設定方法に起因します。この記事で紹介した解決策を実践することで、確実に動作する設定を構築できます。

重要なポイントをまとめますと:

  1. globalIgnores の活用: グローバルな除外設定は配列の最初に配置
  2. 適切な files パターン: **​/​*.{js,ts} のような包括的なパターンを使用
  3. defineConfig の採用: 型安全性と最新機能の活用
  4. 設定の階層化: プロジェクト構造に合わせた適切な分割
  5. 検証の徹底: デバッグ機能を活用した動作確認

これらの手法を適用することで、Flat Config への移行をスムーズに進められ、期待通りにリンターが動作するプロジェクト環境を構築できるでしょう。

ESLint の設定は複雑に見えますが、基本原則を理解すれば必ず解決できます。ぜひこの記事を参考に、快適な開発環境を整えてください。

関連リンク