JA Development Guides Debugging - hiraishikentaro/rails-factorybot-jump GitHub Wiki

開発ガイド: デバッグ

デバッグ概要

Rails FactoryBot Jump のデバッグには複数のレイヤーが含まれます:VSCode 拡張機能デバッグ、TypeScript ソースデバッグ、ランタイム動作解析。このガイドでは効果的な開発のための包括的なデバッグ戦略をカバーします。

VSCode 拡張機能デバッグ

1. 拡張機能開発ホストデバッグ

デバッグセッション起動:

# プロジェクトディレクトリで
code .
# F5を押すか、実行 > デバッグ開始

デバッグ設定.vscode/launch.json):

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Extension",
      "type": "extensionHost",
      "request": "launch",
      "runtimeExecutable": "${execPath}",
      "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
      "outFiles": ["${workspaceFolder}/out/**/*.js"],
      "preLaunchTask": "${workspaceFolder}/npm: compile"
    }
  ]
}

デバッグワークフロー:

  1. TypeScript ソースファイルにブレークポイントを設定
  2. 拡張機能開発ホストを起動(F5
  3. 開発ホストで Ruby ファイルを開いて拡張機能をトリガー
  4. デバッグコントロールを使用(ステップイン/アウト/オーバー、変数検査)

2. ブレークポイント戦略

戦略的ブレークポイント配置:

// src/extension.ts - 拡張機能アクティベーション
export function activate(context: vscode.ExtensionContext) {
  console.log('拡張機能アクティベート中...') // デバッグログ
  const provider = new FactoryLinkProvider()
  // ここにブレークポイントでアクティベーションをデバッグ
}

// src/providers/factoryLinkProvider.ts - コア機能
provideDocumentLinks(document: vscode.TextDocument): vscode.DocumentLink[] {
  // ここにブレークポイントでリンク生成をデバッグ
  console.log('リンクを提供中:', document.uri.path)

  if (!this.isInitialized) {
    // 初期化問題をデバッグするブレークポイント
    await this.initializeFactoryFiles()
  }
}

条件付きブレークポイント:

// 特定ファイルのみでブレーク
// 条件: document.uri.path.includes('user_spec.rb')

// キャッシュが空の時のみブレーク
// 条件: this.factoryCache.size === 0

// 特定ファクトリ名でブレーク
// 条件: factoryName === 'user'

コンソールデバッグ

1. 戦略的コンソールログ

拡張機能ライフサイクルログ:

// src/extension.ts
export function activate(context: vscode.ExtensionContext) {
  console.log("🚀 Rails FactoryBot Jump: 拡張機能アクティベート");

  const provider = new FactoryLinkProvider();
  context.subscriptions.push(
    vscode.languages.registerDocumentLinkProvider(
      { scheme: "file", language: "ruby" },
      provider
    )
  );
  console.log("📝 ドキュメントリンクプロバイダー登録済み");

  // ファイルウォッチャーログ
  const watcher = vscode.workspace.createFileSystemWatcher(
    "**/factories/**/*.rb"
  );
  watcher.onDidChange(async (uri) => {
    console.log("📁 ファクトリファイル変更:", uri.path);
    await provider.initializeFactoryFiles();
  });
}

キャッシュ管理ログ:

// src/providers/factoryLinkProvider.ts
private async cacheFactoryDefinitions() {
  console.log('🔄 ファクトリキャッシュ構築中...')
  console.log('📊 ファクトリファイル数:', this.factoryFiles.length)

  for (const factoryFile of this.factoryFiles) {
    console.log('📖 処理中:', factoryFile.path)
    const document = await vscode.workspace.openTextDocument(factoryFile)
    const content = document.getText()

    const factoryMatches = content.matchAll(this.factoryPattern)
    for (const match of factoryMatches) {
      console.log('🏭 ファクトリ発見:', match[1], '行:', lineNumber)
    }
  }

  console.log('✅ ファクトリキャッシュ構築完了:', this.factoryCache.size, 'エントリ')
}

リンク生成ログ:

provideDocumentLinks(document: vscode.TextDocument): vscode.DocumentLink[] {
  console.log('🔗 リンク生成中:', document.uri.path)

  const text = document.getText()
  const factoryCallMatches = text.matchAll(this.factoryCallPattern)
  const links: vscode.DocumentLink[] = []

  for (const match of factoryCallMatches) {
    console.log('🎯 ファクトリコール発見:', match[0])
    const factoryName = match[2]?.replace(':', '')

    if (this.factoryCache.has(factoryName)) {
      console.log('✅ ファクトリキャッシュヒット:', factoryName)
      links.push(this.createDocumentLink(match, factoryName))
    } else {
      console.log('❌ ファクトリキャッシュミス:', factoryName)
    }
  }

  console.log('📋 生成したリンク数:', links.length)
  return links
}

2. VSCode 開発者ツール

開発者ツールアクセス:

  1. 拡張機能開発ホストで: ヘルプ > 開発者ツールの切り替え
  2. コンソールタブでログを確認
  3. ファイル操作用ネットワークタブ
  4. ソースマップデバッグ用ソースタブ

コンソールコマンド:

// 開発者ツールコンソールで
// 拡張機能インスタンスを検査
vscode.extensions.getExtension("hir4ken.rails-factorybot-jump");

// ワークスペース設定を取得
vscode.workspace.getConfiguration("rails-factorybot-jump");

// 開いているドキュメントを一覧表示
vscode.workspace.textDocuments.map((d) => d.uri.path);

特定コンポーネントのデバッグ

1. ファクトリ検出問題

正規表現テスト:

// 正規表現パターンを単独でテスト
const testFactory = "user = create(:user, :admin)";
const factoryCallPattern =
  /(?:create|build)\s*(?:\(\s*)?((:[a-zA-Z0-9_]+)(?:\s*,\s*(:[a-zA-Z0-9_]+))*)/g;

console.log("ファクトリ検出テスト:");
const matches = Array.from(testFactory.matchAll(factoryCallPattern));
matches.forEach((match, index) => {
  console.log(`マッチ ${index}:`, match);
  console.log("完全マッチ:", match[0]);
  console.log("ファクトリグループ:", match[1]);
  console.log("ファクトリ名:", match[2]);
});

パターンデバッグヘルパー:

class PatternDebugger {
  static testFactoryCall(text: string) {
    const patterns = [
      /create\s*\(\s*:([a-zA-Z0-9_]+)/g,
      /create\s+:([a-zA-Z0-9_]+)/g,
      /build\s*\(\s*:([a-zA-Z0-9_]+)/g,
      // テストするパターンを追加
    ];

    patterns.forEach((pattern, index) => {
      console.log(`パターン ${index}:`, pattern.source);
      const matches = Array.from(text.matchAll(pattern));
      console.log("マッチ数:", matches.length);
      matches.forEach((match) => console.log(" -", match[0]));
    });
  }
}

// 使用方法
PatternDebugger.testFactoryCall("user = create(:user)");

2. キャッシュ問題

キャッシュ状態検査:

class CacheDebugger {
  static inspectCache(provider: FactoryLinkProvider) {
    console.log("=== キャッシュデバッグ情報 ===");

    const factoryCache = provider.getFactoryCache();
    console.log("ファクトリキャッシュサイズ:", factoryCache.size);
    console.log("ファクトリエントリ:");
    factoryCache.forEach((definition, name) => {
      console.log(
        ` - ${name}: ${definition.uri.path}:${definition.lineNumber}`
      );
    });

    const traitCache = provider.getTraitCache();
    console.log("トレイトキャッシュサイズ:", traitCache.size);
    console.log("トレイトエントリ:");
    traitCache.forEach((definition, key) => {
      console.log(` - ${key}: ${definition.uri.path}:${definition.lineNumber}`);
    });
  }

  static validateCacheConsistency(provider: FactoryLinkProvider) {
    const factoryCache = provider.getFactoryCache();
    const traitCache = provider.getTraitCache();

    // 孤立したトレイトをチェック
    traitCache.forEach((definition, key) => {
      const [factoryName] = key.split(":");
      if (!factoryCache.has(factoryName)) {
        console.warn("🚨 孤立したトレイト発見:", key);
      }
    });

    // 重複ファクトリをチェック
    const uriGroups = new Map();
    factoryCache.forEach((definition, name) => {
      const key = `${definition.uri.path}:${definition.lineNumber}`;
      if (!uriGroups.has(key)) {
        uriGroups.set(key, []);
      }
      uriGroups.get(key).push(name);
    });

    uriGroups.forEach((names, location) => {
      if (names.length > 1) {
        console.warn("🚨 重複ファクトリ:", location, ":", names);
      }
    });
  }
}

3. ファイルシステム問題

ファイル操作デバッグ:

class FileSystemDebugger {
  static async debugFactoryFileDiscovery() {
    console.log("=== ファクトリファイル発見デバッグ ===");

    const config = vscode.workspace.getConfiguration("rails-factorybot-jump");
    const factoryPaths = config.get<string[]>("factoryPaths", [
      "spec/factories/**/*.rb",
    ]);

    console.log("設定されたパス:", factoryPaths);

    for (const pattern of factoryPaths) {
      console.log(`パターン検索中: ${pattern}`);
      try {
        const files = await vscode.workspace.findFiles(pattern);
        console.log(`${files.length}個のファイルが見つかりました:`);
        files.forEach((file) => console.log(` - ${file.path}`));
      } catch (error) {
        console.error("ファイル検索エラー:", error);
      }
    }
  }

  static async debugFileContent(uri: vscode.Uri) {
    console.log("=== ファイル内容デバッグ ===");
    console.log("ファイル:", uri.path);

    try {
      const document = await vscode.workspace.openTextDocument(uri);
      const content = document.getText();

      console.log("ファイルサイズ:", content.length, "文字");
      console.log("行数:", document.lineCount);
      console.log("最初の500文字:");
      console.log(content.substring(0, 500));

      // パターンマッチテスト
      const factoryPattern = /factory\s+:([a-zA-Z0-9_]+)\b/g;
      const matches = Array.from(content.matchAll(factoryPattern));
      console.log("ファクトリマッチ数:", matches.length);
      matches.forEach((match) => {
        const lineNumber =
          content.substring(0, match.index).split("\n").length - 1;
        console.log(` - ${match[1]}${lineNumber}`);
      });
    } catch (error) {
      console.error("ファイル読み込みエラー:", error);
    }
  }
}

パフォーマンスデバッグ

1. タイミング解析

パフォーマンス測定:

class PerformanceDebugger {
  private static timers = new Map<string, number>()

  static startTimer(name: string) {
    this.timers.set(name, Date.now())
    console.log(`⏱️ タイマー開始: ${name}`)
  }

  static endTimer(name: string) {
    const startTime = this.timers.get(name)
    if (startTime) {
      const duration = Date.now() - startTime
      console.log(`⏱️ タイマー ${name}: ${duration}ms`)
      this.timers.delete(name)
      return duration
    }
    console.warn(`タイマー ${name} が見つかりません`)
    return 0
  }

  static async measureAsync<T>(name: string, fn: () => Promise<T>): Promise<T> {
    this.startTimer(name)
    try {
      const result = await fn()
      this.endTimer(name)
      return result
    } catch (error) {
      this.endTimer(name)
      throw error
    }
  }
}

// プロバイダーでの使用方法
async initializeFactoryFiles() {
  await PerformanceDebugger.measureAsync('factoryInitialization', async () => {
    await PerformanceDebugger.measureAsync('fileDiscovery', () =>
      this.discoverFactoryFiles()
    )
    await PerformanceDebugger.measureAsync('cacheBuilding', () =>
      this.buildCaches()
    )
  })
}

2. メモリ使用量解析

メモリ監視:

class MemoryDebugger {
  static logMemoryUsage(label: string) {
    if (typeof process !== "undefined" && process.memoryUsage) {
      const usage = process.memoryUsage();
      console.log(`🧠 メモリ ${label}:`, {
        rss: Math.round(usage.rss / 1024 / 1024) + "MB",
        heapTotal: Math.round(usage.heapTotal / 1024 / 1024) + "MB",
        heapUsed: Math.round(usage.heapUsed / 1024 / 1024) + "MB",
        external: Math.round(usage.external / 1024 / 1024) + "MB",
      });
    }
  }

  static measureCacheSize(provider: FactoryLinkProvider) {
    const factoryCache = provider.getFactoryCache();
    const traitCache = provider.getTraitCache();

    // メモリ使用量推定
    let factorySize = 0;
    factoryCache.forEach((definition, name) => {
      factorySize += name.length * 2; // 文字は2バイト
      factorySize += definition.uri.path.length * 2;
      factorySize += 8; // lineNumber(数値)
    });

    let traitSize = 0;
    traitCache.forEach((definition, key) => {
      traitSize += key.length * 2;
      traitSize += definition.uri.path.length * 2;
      traitSize += definition.factory.length * 2;
      traitSize += 8; // lineNumber
    });

    console.log("📊 キャッシュメモリ推定:", {
      factories: Math.round(factorySize / 1024) + "KB",
      traits: Math.round(traitSize / 1024) + "KB",
      total: Math.round((factorySize + traitSize) / 1024) + "KB",
    });
  }
}

エラーデバッグ

1. エラートラッキング

包括的エラーログ:

class ErrorDebugger {
  static logError(context: string, error: any, additionalInfo?: any) {
    console.error(`❌ ${context}でエラー:`)
    console.error('メッセージ:', error.message)
    console.error('スタック:', error.stack)

    if (additionalInfo) {
      console.error('追加情報:', additionalInfo)
    }

    // VSCodeの出力チャンネルにログ(利用可能な場合)
    if (typeof vscode !== 'undefined') {
      const outputChannel = vscode.window.createOutputChannel('Rails FactoryBot Jump')
      outputChannel.appendLine(`${context}でエラー: ${error.message}`)
      outputChannel.appendLine(error.stack)
    }
  }

  static wrapAsync<T>(context: string, fn: () => Promise<T>): Promise<T> {
    return fn().catch(error => {
      this.logError(context, error)
      throw error
    })
  }

  static wrapSync<T>(context: string, fn: () => T): T {
    try {
      return fn()
    } catch (error) {
      this.logError(context, error)
      throw error
    }
  }
}

// 使用方法
async initializeFactoryFiles() {
  return ErrorDebugger.wrapAsync('ファクトリ初期化', async () => {
    // 実装
  })
}

2. 優雅なエラー回復

エラー回復戦略:

class ErrorRecovery {
  static async safeFileOperation<T>(
    operation: () => Promise<T>,
    fallback: T,
    context: string
  ): Promise<T> {
    try {
      return await operation();
    } catch (error) {
      console.warn(`⚠️ ${context}が失敗、フォールバックを使用:`, error.message);
      return fallback;
    }
  }

  static async retryOperation<T>(
    operation: () => Promise<T>,
    maxAttempts: number = 3,
    delay: number = 1000
  ): Promise<T> {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await operation();
      } catch (error) {
        console.warn(`試行 ${attempt}/${maxAttempts} が失敗:`, error.message);

        if (attempt === maxAttempts) {
          throw error;
        }

        if (delay > 0) {
          await new Promise((resolve) => setTimeout(resolve, delay));
        }
      }
    }

    throw new Error("すべての再試行が失敗しました");
  }
}

テストデバッグサポート

1. テスト固有デバッグ

デバッグテスト実行:

// テストファイルで
suite("デバッグテストスイート", () => {
  let debugMode = process.env.DEBUG_TESTS === "true";

  test("ファクトリ検出をデバッグ", () => {
    if (debugMode) {
      console.log("🔍 デバッグモード: ファクトリ検出テスト");
    }

    const provider = new FactoryLinkProvider();
    const mockDocument = createMockDocument("user = create(:user)");

    if (debugMode) {
      console.log("ドキュメント内容:", mockDocument.getText());
    }

    const links = provider.provideDocumentLinks(mockDocument);

    if (debugMode) {
      console.log("生成されたリンク:", links.length);
      links.forEach((link) => console.log(" -", link.target?.toString()));
    }

    assert.strictEqual(links.length, 1);
  });
});

デバッグモードでテスト実行:

# テスト用デバッグログを有効化
DEBUG_TESTS=true npm test

# 特定テストをデバッグで実行
DEBUG_TESTS=true npm test -- --grep "ファクトリ検出"

一般的なデバッグシナリオ

1. 拡張機能がアクティベートしない

デバッグチェックリスト:

// アクティベーションイベントをチェック
console.log(
  "アクティベーションイベント:",
  vscode.extensions.getExtension("hir4ken.rails-factorybot-jump")?.packageJSON
    .activationEvents
);

// Rubyファイルがアクティベーションをトリガーするかチェック
vscode.workspace.onDidOpenTextDocument((doc) => {
  console.log("ドキュメント開いた:", doc.languageId, doc.uri.path);
});

2. リンクが表示されない

デバッグステップ:

// プロバイダー登録をチェック
console.log('プロバイダー登録言語:',
  vscode.languages.getDocumentLinkProviders().length)

// ドキュメント言語をチェック
provideDocumentLinks(document: vscode.TextDocument) {
  console.log('ドキュメント言語:', document.languageId)
  console.log('ドキュメントスキーム:', document.uri.scheme)
  // ... 実装の残り
}

3. ファクトリファイルが見つからない

ファイル発見デバッグ:

// 設定をテスト
const config = vscode.workspace.getConfiguration("rails-factorybot-jump");
console.log("ファクトリパス設定:", config.get("factoryPaths"));

// ファイルパターンをテスト
for (const pattern of factoryPaths) {
  console.log(`パターンテスト: ${pattern}`);
  const files = await vscode.workspace.findFiles(pattern);
  console.log(`見つかった: ${files.length}個のファイル`);
}

この包括的なデバッグガイドは、Rails FactoryBot Jump 開発中に問題を効果的にトラブルシューティングし解決するために必要なツールとテクニックを提供します。

⚠️ **GitHub.com Fallback** ⚠️