UNPKG

symref

Version:

Static code checker for AI code agents (Windsurf, Cline, etc.)

212 lines 10.1 kB
import * as path from 'node:path'; import * as fs from 'node:fs'; import { SymbolReferenceAnalyzer } from '../../analyzer/SymbolReferenceAnalyzer.js'; /** * シンボル間の呼び出し経路を分析するコマンド */ export class TraceCommand { /** * シンボル文字列をパースする * @param input 入力文字列 * @returns パースされたシンボル情報 */ static parseSymbol(input) { const trimmedSymbol = input.trim(); // ドット記法の解析 if (trimmedSymbol.includes('.')) { // 複数のドットに対応するため、最後のドットで分割 const lastDotIndex = trimmedSymbol.lastIndexOf('.'); const containerName = trimmedSymbol.substring(0, lastDotIndex); const memberName = trimmedSymbol.substring(lastDotIndex + 1); return { symbol: trimmedSymbol, containerName, memberName }; } else { return { symbol: trimmedSymbol }; } } /** * コマンドを実行する * @param args 開始シンボルと終了シンボル * @param options コマンドオプション */ static execute(args, options) { try { // 引数の検証 if (!args.from || !args.to) { console.error('エラー: 開始シンボルと終了シンボルの両方を指定してください。'); process.exit(1); } // シンボル名をパース const fromSymbolInfo = this.parseSymbol(args.from); const toSymbolInfo = this.parseSymbol(args.to); const fromSymbol = fromSymbolInfo.symbol; const toSymbol = toSymbolInfo.symbol; // 分析オプションを設定 const analyzerOptions = { basePath: options.dir, tsConfigPath: options.project, includePatterns: options.include ? options.include.split(',') : undefined, excludePatterns: options.exclude ? options.exclude.split(',') : undefined }; // アナライザーを初期化 const analyzer = new SymbolReferenceAnalyzer(analyzerOptions); // 開始シンボルの存在確認 if (fromSymbolInfo.containerName && fromSymbolInfo.memberName) { if (!analyzer.hasSymbol(fromSymbolInfo.containerName)) { process.stderr.write(`エラー: コンテナ '${fromSymbolInfo.containerName}' がコードベース内に見つかりません。\n`); process.exit(1); } } else if (!analyzer.hasSymbol(fromSymbol)) { process.stderr.write(`エラー: シンボル '${fromSymbol}' がコードベース内に見つかりません。\n`); process.exit(1); } // 終了シンボルの存在確認 if (toSymbolInfo.containerName && toSymbolInfo.memberName) { if (!analyzer.hasSymbol(toSymbolInfo.containerName)) { process.stderr.write(`エラー: コンテナ '${toSymbolInfo.containerName}' がコードベース内に見つかりません。\n`); process.exit(1); } } else if (!analyzer.hasSymbol(toSymbol)) { process.stderr.write(`エラー: シンボル '${toSymbol}' がコードベース内に見つかりません。\n`); process.exit(1); } console.log(`\n=== '${fromSymbol}' から '${toSymbol}' への呼び出し経路を分析中... ===\n`); // 呼び出しグラフを構築 const nodeCount = analyzer.buildCallGraph(); console.log(`${nodeCount} 個のシンボルを分析しました。\n`); // 呼び出し経路を分析 const result = analyzer.traceCallPath(fromSymbol, toSymbol); // 結果を表示 TraceCommand.displayResult(result, fromSymbol, toSymbol); // 経路が見つからない場合は、終了コードを1に設定 if (result.paths.length === 0) { console.error(`エラー: '${fromSymbol}' から '${toSymbol}' への呼び出し経路が見つかりませんでした。`); process.exit(1); } // Mermaidファイルを生成(オプション) if (options.mermaid) { TraceCommand.generateMermaidFile(result, options.mermaid); } } catch (error) { console.error(`エラー: ${error.message}`); process.exit(1); } } /** * 分析結果を表示 * @param result 分析結果 * @param fromSymbol 開始シンボル * @param toSymbol 終了シンボル */ static displayResult(result, fromSymbol, toSymbol) { if (result.paths.length === 0) { console.log(`'${fromSymbol}' から '${toSymbol}' への呼び出し経路は見つかりませんでした。`); return; } console.log(`${result.paths.length} 個の呼び出し経路が見つかりました:\n`); // 各経路を表示 result.paths.forEach((path, index) => { console.log(`経路 ${index + 1}:`); // 経路上のノードを表示 for (let i = 0; i < path.nodes.length; i++) { const node = path.nodes[i]; const location = node.location; const locationStr = location.filePath && location.line > 0 ? `${location.filePath}:${location.line}` : '不明な位置'; // 最初のノード if (i === 0) { console.log(`${node.symbol} (${locationStr})`); } // 中間ノードと最後のノード else { // エッジ情報を表示 const edge = path.edges[i - 1]; const edgeLocation = edge === null || edge === void 0 ? void 0 : edge.location; const callLocationStr = (edgeLocation === null || edgeLocation === void 0 ? void 0 : edgeLocation.filePath) && (edgeLocation === null || edgeLocation === void 0 ? void 0 : edgeLocation.line) > 0 ? `${edgeLocation.filePath}:${edgeLocation.line}` : locationStr; console.log(` ↓ calls (${callLocationStr})`); console.log(`${node.symbol} (${locationStr})`); } } console.log('\n'); }); } /** * Mermaidファイルを生成 * @param result 分析結果 * @param outputPath 出力パス */ static generateMermaidFile(result, outputPath) { if (!result.graphMermaidFormat) { console.warn('警告: Mermaidグラフデータを生成できませんでした。'); return; } try { // 出力ディレクトリを.symbolsに変更 const outputDir = '.symbols'; const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const timestamp = `${year}${month}${day}_${hours}${minutes}`; const safeBaseName = outputPath.replace(/[^a-zA-Z0-9]/g, '_'); const fileName = `${safeBaseName}_${timestamp}.md`; const resolvedPath = path.resolve(process.cwd(), outputDir, fileName); // 出力ディレクトリを確保 const dir = path.dirname(resolvedPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(resolvedPath, result.graphMermaidFormat); console.log(`Mermaidグラフファイルを生成しました: ${resolvedPath}`); console.log('可視化するには: GitHubで表示するか、https://mermaid.live で開いてください'); } catch (error) { console.error(`Mermaidファイルの生成中にエラーが発生しました: ${error.message}`); } } formatCallGraph(result) { if (result.paths.length === 0) { return '呼び出し経路は見つかりませんでした。'; } const lines = []; lines.push(`${result.paths.length} 個の呼び出し経路が見つかりました:\n`); result.paths.forEach((path, index) => { lines.push(`経路 ${index + 1}:`); path.nodes.forEach((node, i) => { const location = node.location; const locationStr = location.filePath && location.line > 0 ? `${location.filePath}:${location.line}` : '不明な位置'; if (i === 0) { lines.push(`${node.symbol} (${locationStr})`); } else { const edge = path.edges[i - 1]; const edgeLocation = edge === null || edge === void 0 ? void 0 : edge.location; const callLocationStr = (edgeLocation === null || edgeLocation === void 0 ? void 0 : edgeLocation.filePath) && (edgeLocation === null || edgeLocation === void 0 ? void 0 : edgeLocation.line) > 0 ? `${edgeLocation.filePath}:${edgeLocation.line}` : locationStr; lines.push(` ↓ calls (${callLocationStr})`); lines.push(`${node.symbol} (${locationStr})`); } }); lines.push('\n'); }); return lines.join('\n'); } } //# sourceMappingURL=TraceCommand.js.map