T-CREATOR

TypeScript でカスタム Lint ルールを作成する方法と運用ポイント

TypeScript でカスタム Lint ルールを作成する方法と運用ポイント

開発チームで TypeScript プロジェクトを進めていると、必ずと言っていいほど直面するのが「コードの一貫性」という課題です。ESLint の標準ルールだけでは対応できない、プロジェクト固有のルールが必要になった経験はありませんか?

この記事では、TypeScript でカスタム Lint ルールを作成し、チーム開発で効果的に運用する方法を実践的なアプローチで解説します。単なる技術的な説明ではなく、実際の開発現場で起こりがちな問題とその解決策に焦点を当てていきます。

TypeScript Lint の基本概念

Lint ツールの役割と重要性

Lint ツールは、コードの品質を自動的にチェックし、潜在的な問題を早期に発見するための重要なツールです。TypeScript プロジェクトでは、主に ESLint と TypeScript ESLint が使用されています。

json// package.json の依存関係例
{
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.0.0"
  }
}

TypeScript ESLint の設定

TypeScript ESLint は、TypeScript の型情報を活用した高度な静的解析を可能にします。

javascript// .eslintrc.js の基本設定
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended',
  ],
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    project: './tsconfig.json',
  },
};

この設定により、TypeScript の型チェックと ESLint の静的解析が連携し、より精度の高いコード品質チェックが実現できます。

カスタム Lint ルールの必要性

標準ルールの限界

ESLint の標準ルールは汎用的で優秀ですが、プロジェクト固有の要件には対応できません。例えば、以下のような場面でカスタムルールが必要になります:

  • プロジェクト固有の命名規則
  • 特定のライブラリの使用パターン
  • セキュリティ要件
  • パフォーマンス最適化

実際の開発現場での課題

開発チームでよくある問題として、以下のようなケースが挙げられます:

typescript// 問題のあるコード例
// 1. 非同期処理でのエラーハンドリング不足
async function fetchUserData(id: string) {
  const response = await fetch(`/api/users/${id}`);
  return response.json(); // エラーハンドリングがない
}

// 2. 型安全性の欠如
function processData(data: any) {
  // any型の使用
  return data.value * 2;
}

これらの問題を自動的に検出し、開発者に適切なガイダンスを提供するカスタムルールを作成することで、コード品質の向上と開発効率の改善を両立できます。

ESLint プラグインの作成方法

開発環境の準備

カスタム Lint ルールを作成するには、まず適切な開発環境を整える必要があります。

bash# プロジェクトの初期化
yarn init -y

# 必要な依存関係のインストール
yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add -D @types/eslint typescript

プラグインの基本構造

ESLint プラグインは、ルールの集合体として機能します。以下のような構造で作成します:

javascript// lib/rules/no-unsafe-fetch.js
module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description:
        '非同期処理でのエラーハンドリングを強制する',
      category: 'Best Practices',
      recommended: true,
    },
    fixable: null,
    schema: [],
  },
  create(context) {
    return {
      // ルールの実装
    };
  },
};

プラグインのエクスポート設定

作成したルールをプラグインとして公開するための設定を行います。

javascript// index.js
module.exports = {
  rules: {
    'no-unsafe-fetch': require('./lib/rules/no-unsafe-fetch'),
    'enforce-error-handling': require('./lib/rules/enforce-error-handling'),
  },
  configs: {
    recommended: {
      rules: {
        'custom/no-unsafe-fetch': 'error',
        'custom/enforce-error-handling': 'warn',
      },
    },
  },
};

この構造により、チーム内で再利用可能なカスタムルールセットを作成できます。

TypeScript 固有のルール実装

AST(抽象構文木)の活用

TypeScript ESLint では、TypeScript の AST を活用して高度な静的解析が可能です。

javascript// TypeScript ASTを活用したルール例
module.exports = {
  create(context) {
    return {
      // 関数宣言の検出
      FunctionDeclaration(node) {
        // 非同期関数のチェック
        if (node.async) {
          const body = node.body;
          // エラーハンドリングの存在確認
          const hasTryCatch = body.body.some(
            (stmt) => stmt.type === 'TryStatement'
          );

          if (!hasTryCatch) {
            context.report({
              node,
              message:
                '非同期関数には適切なエラーハンドリングが必要です',
            });
          }
        }
      },
    };
  },
};

型情報を活用したルール

TypeScript の型情報を活用することで、より精密なチェックが可能になります。

javascript// 型情報を活用したルール例
module.exports = {
  create(context) {
    return {
      VariableDeclarator(node) {
        // any型の使用を検出
        if (
          node.id.typeAnnotation &&
          node.id.typeAnnotation.typeAnnotation.type ===
            'TSAnyKeyword'
        ) {
          context.report({
            node,
            message:
              'any型の使用は避けてください。適切な型を指定してください。',
          });
        }
      },
    };
  },
};

実際のエラーコードと対処法

開発中によく遭遇するエラーとその解決策を紹介します。

bash# よくあるエラー1: パーサーの設定エラー
Error: Failed to load parser '@typescript-eslint/parser' declared in package.json

# 解決策: パーサーの正しい設定
javascript// .eslintrc.js の修正例
module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    project: './tsconfig.json',
    tsconfigRootDir: __dirname,
  },
};
bash# よくあるエラー2: 型情報の取得エラー
Error: Cannot read property 'getTypeAtLocation' of undefined

# 解決策: TypeScript ESLintの正しい設定
javascript// 型情報を活用するルールの正しい実装
const {
  getTypeAtLocation,
} = require('@typescript-eslint/utils');

module.exports = {
  create(context) {
    const parserServices = context.parserServices;

    if (!parserServices || !parserServices.program) {
      return {};
    }

    return {
      // ルールの実装
    };
  },
};

テストとデバッグ手法

ルールのテスト作成

カスタムルールの品質を保つため、包括的なテストを作成することが重要です。

javascript// tests/no-unsafe-fetch.test.js
const { RuleTester } = require('eslint');

const ruleTester = new RuleTester({
  parser: require.resolve('@typescript-eslint/parser'),
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
  },
});

ruleTester.run(
  'no-unsafe-fetch',
  require('../../lib/rules/no-unsafe-fetch'),
  {
    valid: [
      {
        code: `
        async function safeFetch() {
          try {
            const response = await fetch('/api/data');
            return response.json();
          } catch (error) {
            console.error('Fetch error:', error);
            throw error;
          }
        }
      `,
      },
    ],
    invalid: [
      {
        code: `
        async function unsafeFetch() {
          const response = await fetch('/api/data');
          return response.json();
        }
      `,
        errors: [
          {
            message:
              '非同期関数には適切なエラーハンドリングが必要です',
          },
        ],
      },
    ],
  }
);

デバッグ手法

ルールの開発中に問題が発生した場合のデバッグ手法を紹介します。

javascript// デバッグ用のログ出力
module.exports = {
  create(context) {
    return {
      FunctionDeclaration(node) {
        console.log('Function name:', node.id?.name);
        console.log('Is async:', node.async);
        console.log('Body type:', node.body.type);

        // ルールの実装
      },
    };
  },
};

パフォーマンスの監視

ルールの実行時間を監視し、パフォーマンスの問題を早期に発見します。

javascript// パフォーマンス監視用のルール
const startTime = Date.now();

module.exports = {
  create(context) {
    return {
      'Program:exit'() {
        const endTime = Date.now();
        console.log(
          `Rule execution time: ${endTime - startTime}ms`
        );
      },
    };
  },
};

チーム開発での運用ポイント

ルールの段階的導入

チーム全体に新しいルールを導入する際は、段階的なアプローチが効果的です。

javascript// .eslintrc.js での段階的導入設定
module.exports = {
  rules: {
    // 第1段階: 警告レベルで導入
    'custom/no-unsafe-fetch': 'warn',

    // 第2段階: エラーレベルに昇格
    'custom/enforce-error-handling': 'error',

    // 第3段階: 自動修正可能なルール
    'custom/prefer-const': ['error', { fixable: true }],
  },
};

チーム内での共有と教育

カスタムルールの目的と効果をチーム全体で理解してもらうことが重要です。

markdown# カスタムルールガイド

# ルールの目的

- コードの一貫性を保つ
- バグの早期発見
- 開発効率の向上

# ルールの説明

各ルールの詳細な説明と使用例を記載

継続的な改善プロセス

ルールは一度作成して終わりではなく、継続的な改善が必要です。

javascript// ルールの使用状況を追跡
module.exports = {
  create(context) {
    return {
      'Program:exit'() {
        // ルールの使用統計を記録
        const stats = context
          .getSourceCode()
          .getAllComments();
        console.log(
          `File analyzed: ${context.getFilename()}`
        );
        console.log(
          `Total lines: ${
            context.getSourceCode().getAllTokens().length
          }`
        );
      },
    };
  },
};

パフォーマンス最適化

ルールの実行効率化

大規模プロジェクトでは、ルールの実行時間が開発効率に直結します。

javascript// 効率的なルール実装例
module.exports = {
  create(context) {
    // 早期リターンで不要な処理を回避
    return {
      FunctionDeclaration(node) {
        // 非同期関数でない場合は早期リターン
        if (!node.async) return;

        // 特定の条件に合致しない場合は早期リターン
        if (!node.id || !node.id.name.startsWith('fetch'))
          return;

        // メインのロジック
        // ...
      },
    };
  },
};

キャッシュの活用

同じファイルの重複解析を避けるため、キャッシュを活用します。

javascript// キャッシュを活用したルール
const cache = new Map();

module.exports = {
  create(context) {
    const filename = context.getFilename();

    return {
      'Program:exit'() {
        if (cache.has(filename)) {
          return; // 既に処理済み
        }

        // 処理を実行
        // ...

        cache.set(filename, true);
      },
    };
  },
};

並列処理の活用

複数のルールを並列で実行することで、全体の処理時間を短縮できます。

javascript// 並列処理を考慮したルール設計
module.exports = {
  create(context) {
    const promises = [];

    return {
      FunctionDeclaration(node) {
        // 非同期で処理を実行
        promises.push(processFunctionAsync(node, context));
      },

      'Program:exit'() {
        // 全ての処理が完了するまで待機
        Promise.all(promises).then((results) => {
          results.forEach((result) => {
            if (result.hasError) {
              context.report(result.error);
            }
          });
        });
      },
    };
  },
};

まとめ

TypeScript でカスタム Lint ルールを作成し、効果的に運用する方法について解説しました。重要なポイントをまとめると:

技術的なポイント

  • TypeScript ESLint の基本概念と設定方法
  • カスタムルールの作成とプラグイン化
  • TypeScript 固有の AST と型情報の活用
  • 包括的なテストとデバッグ手法

運用面でのポイント

  • チーム開発での段階的導入
  • 継続的な改善とメンテナンス
  • パフォーマンス最適化の重要性

心に響く気づき カスタム Lint ルールは、単なるコードチェックツールではありません。チーム全体のコード品質向上と開発効率の改善を実現する、強力な武器です。

最初は小さなルールから始めて、徐々に洗練させていくことで、プロジェクト固有の要件に最適化された開発環境を構築できます。この記事で紹介した手法を活用し、あなたのチームでも効果的なカスタム Lint ルールの運用を始めてみてください。

関連リンク