UNPKG

symref

Version:

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

448 lines 17.7 kB
import { SyntaxKind } from 'ts-morph'; import * as path from 'path'; import * as fs from 'fs'; import { NodeUtils } from '../utils/NodeUtils'; /** * 呼び出しグラフの構築と分析を担当するクラス */ export class CallGraphAnalyzer { /** * コンストラクタ * @param project ts-morphのプロジェクトインスタンス * @param outputDir 出力ディレクトリ(オプション) */ constructor(project, outputDir = '.symbols') { this.project = project; this.nodeUtils = new NodeUtils(); this.callGraph = new Map(); this.outputDir = outputDir; this.ensureOutputDir(); } /** * 出力ディレクトリを確保 */ ensureOutputDir() { if (!fs.existsSync(this.outputDir)) { fs.mkdirSync(this.outputDir, { recursive: true }); } // .gitignoreファイルを作成 const gitignorePath = path.join(this.outputDir, '.gitignore'); if (!fs.existsSync(gitignorePath)) { fs.writeFileSync(gitignorePath, '*\n'); } } /** * グラフファイルの出力パスを生成 * @param baseName 基本ファイル名 * @returns 出力パス */ generateOutputPath(baseName) { 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 = baseName.replace(/[^a-zA-Z0-9]/g, '_'); const fileName = `${safeBaseName}_${timestamp}.md`; return path.join(this.outputDir, fileName); } /** * プロジェクト全体の呼び出しグラフを構築 * @returns 構築された呼び出しグラフのノード数 */ buildCallGraph() { // 既存のグラフをクリア this.callGraph.clear(); // プロジェクト内のすべてのソースファイルを処理 for (const sourceFile of this.project.getSourceFiles()) { // .d.tsファイルはスキップ if (sourceFile.getFilePath().endsWith('.d.ts')) continue; // 関数宣言を処理 this.processFunctions(sourceFile); // クラス宣言を処理 this.processClasses(sourceFile); } // 呼び出し関係を構築 this.buildCallRelationships(); return this.callGraph.size; } /** * 関数宣言を処理 * @param sourceFile ソースファイル */ processFunctions(sourceFile) { const functions = sourceFile.getFunctions(); for (const func of functions) { const funcName = func.getName(); if (!funcName) continue; // 無名関数はスキップ // ノードを作成または取得 const node = this.getOrCreateNode(funcName, 'function', func); // 関数本体内の呼び出しを処理 this.processCallExpressions(func, node); } } /** * クラス宣言を処理 * @param sourceFile ソースファイル */ processClasses(sourceFile) { const classes = sourceFile.getClasses(); for (const classDecl of classes) { const className = classDecl.getName(); if (!className) continue; // クラスノードを作成 this.getOrCreateNode(className, 'class', classDecl); // メソッドを処理 const methods = classDecl.getMethods(); for (const method of methods) { const methodName = method.getName(); if (!methodName) continue; const fullMethodName = `${className}.${methodName}`; const methodNode = this.getOrCreateNode(fullMethodName, 'method', method); // メソッド本体内の呼び出しを処理 this.processCallExpressions(method, methodNode); } } } /** * 関数/メソッド本体内の呼び出し式を処理 * @param node 関数/メソッドノード * @param callGraphNode 呼び出しグラフノード */ processCallExpressions(node, callGraphNode) { // 関数/メソッド本体内のすべての呼び出し式を取得 const callExpressions = node.getDescendantsOfKind(SyntaxKind.CallExpression); for (const callExpr of callExpressions) { const expression = callExpr.getExpression(); // 直接呼び出し (例: myFunction()) if (expression.getKind() === SyntaxKind.Identifier) { const calleeName = expression.getText(); // 呼び出し位置情報を含めて記録 this.recordCallRelationship(callGraphNode, calleeName); } // プロパティアクセス呼び出し (例: obj.method()) else if (expression.getKind() === SyntaxKind.PropertyAccessExpression) { const propAccess = expression; const methodName = propAccess.getName(); const objExpr = propAccess.getExpression(); // オブジェクト型を取得して、クラスメソッド呼び出しを検出 const objType = objExpr.getType(); const typeName = objType.getSymbol()?.getName(); if (typeName && methodName) { const fullMethodName = `${typeName}.${methodName}`; // 呼び出し位置情報を含めて記録 this.recordCallRelationship(callGraphNode, fullMethodName); } else { // 型名が取得できない場合は、式のテキストを使用 const objText = objExpr.getText(); if (objText && methodName) { const fullMethodName = `${objText}.${methodName}`; this.recordCallRelationship(callGraphNode, fullMethodName); } } } } } /** * 呼び出し関係を記録 * @param caller 呼び出し元ノード * @param calleeName 呼び出し先シンボル名 */ recordCallRelationship(caller, calleeName) { // 呼び出し先ノードを取得または作成 const calleeNode = this.getOrCreateNode(calleeName, 'unknown', null); // 呼び出し位置情報を取得(将来の拡張のために準備) // let callLocation: SymbolLocation = caller.location; // if (callExpr) { // callLocation = { // filePath: path.relative(process.cwd(), callExpr.getSourceFile().getFilePath()), // line: callExpr.getStartLineNumber(), // column: callExpr.getStartLinePos(), // context: this.nodeUtils.getNodeContext(callExpr) // }; // } // エッジ情報は現在使用していないが、将来の拡張のために準備 // const edge: CallEdge = { // caller, // callee: calleeNode, // location: callLocation // }; // 呼び出し関係を記録 calleeNode.callers.push(caller); caller.callees.push(calleeNode); } /** * 呼び出し関係を構築 */ buildCallRelationships() { // すべてのノードを処理して、呼び出し関係を構築 // この段階では既に基本的な関係は記録されているため、 // 必要に応じて追加の処理を行う } /** * ノードを取得または作成 * @param symbolName シンボル名 * @param type シンボルタイプ * @param node ノード(オプション) * @returns 呼び出しグラフノード */ getOrCreateNode(symbolName, type, node) { if (this.callGraph.has(symbolName)) { const existingNode = this.callGraph.get(symbolName); // ノードが存在するが位置情報がない場合は更新 if (node && (!existingNode.location.filePath || existingNode.location.line === 0)) { existingNode.location = { filePath: path.relative(process.cwd(), node.getSourceFile().getFilePath()), line: node.getStartLineNumber(), column: node.getStartLinePos(), context: this.nodeUtils.getNodeContext(node) }; } return existingNode; } let location = { filePath: '', line: 0, column: 0, context: '' }; if (node) { try { const sourceFile = node.getSourceFile(); location = { filePath: path.relative(process.cwd(), sourceFile.getFilePath()), line: node.getStartLineNumber(), column: node.getStartLinePos(), context: this.nodeUtils.getNodeContext(node) }; } catch (error) { console.warn(`警告: シンボル '${symbolName}' の位置情報を取得できませんでした。`); } } const graphNode = { symbol: symbolName, type, location, callers: [], callees: [] }; this.callGraph.set(symbolName, graphNode); return graphNode; } /** * 2つのシンボル間の呼び出し経路を検索 * @param fromSymbol 開始シンボル * @param toSymbol 終了シンボル * @returns 呼び出し経路の分析結果 */ findPathsFromTo(fromSymbol, toSymbol) { // グラフが構築されていない場合は構築 if (this.callGraph.size === 0) { this.buildCallGraph(); } const startNode = this.callGraph.get(fromSymbol); const endNode = this.callGraph.get(toSymbol); if (!startNode) { throw new Error(`開始シンボル '${fromSymbol}' が見つかりません。`); } if (!endNode) { throw new Error(`終了シンボル '${toSymbol}' が見つかりません。`); } // 深さ優先探索で経路を検索 const paths = []; const visited = new Set(); const currentPath = []; const currentEdges = []; this.dfsSearch(startNode, endNode, visited, currentPath, currentEdges, paths); // グラフを生成 const { content, outputPath } = this.generateMermaidFormat(paths, `trace_${fromSymbol}_to_${toSymbol}`); return { paths, totalPaths: paths.length, graphMermaidFormat: content, outputPath }; } /** * シンボルを呼び出すすべての経路を検索 * @param symbol 対象シンボル * @returns 呼び出し経路の分析結果 */ findAllCallers(symbol) { // グラフが構築されていない場合は構築 if (this.callGraph.size === 0) { this.buildCallGraph(); } const targetNode = this.callGraph.get(symbol); if (!targetNode) { throw new Error(`シンボル '${symbol}' が見つかりません。`); } // 逆方向の深さ優先探索で経路を検索 const paths = []; // すべての呼び出し元を処理 for (const caller of targetNode.callers) { const visited = new Set(); const currentPath = [targetNode]; const currentEdges = []; this.dfsReverseSearch(caller, visited, currentPath, currentEdges, paths); } // グラフを生成 const { content, outputPath } = this.generateMermaidFormat(paths, `callers_${symbol}`); return { paths, totalPaths: paths.length, graphMermaidFormat: content, outputPath }; } /** * 深さ優先探索で経路を検索 * @param current 現在のノード * @param target 目標ノード * @param visited 訪問済みノード * @param path 現在の経路 * @param edges 現在の経路のエッジ * @param results 結果の経路リスト */ dfsSearch(current, target, visited, path, edges, results) { // 現在のノードを経路に追加 path.push(current); visited.add(current.symbol); // 目標に到達した場合 if (current.symbol === target.symbol) { // 経路をコピーして結果に追加 results.push({ nodes: [...path], edges: [...edges], startSymbol: path[0].symbol, endSymbol: current.symbol }); } else { // 呼び出し先を探索 for (const callee of current.callees) { if (!visited.has(callee.symbol)) { // エッジを作成 const edge = { caller: current, callee, location: callee.location }; edges.push(edge); // 再帰的に探索 this.dfsSearch(callee, target, visited, path, edges, results); // バックトラック edges.pop(); } } } // バックトラック path.pop(); visited.delete(current.symbol); } /** * 逆方向の深さ優先探索で経路を検索 * @param current 現在のノード * @param visited 訪問済みノード * @param path 現在の経路 * @param edges 現在の経路のエッジ * @param results 結果の経路リスト */ dfsReverseSearch(current, visited, path, edges, results) { // 現在のノードを経路の先頭に追加 path.unshift(current); visited.add(current.symbol); // エッジを作成 if (path.length > 1) { const edge = { caller: current, callee: path[1], location: current.location }; edges.push(edge); } // 呼び出し元がない場合(エントリーポイント) if (current.callers.length === 0) { // 経路をコピーして結果に追加 results.push({ nodes: [...path], edges: [...edges], startSymbol: current.symbol, endSymbol: path[path.length - 1].symbol }); } else { // 呼び出し元を探索 for (const caller of current.callers) { if (!visited.has(caller.symbol)) { // 再帰的に探索 this.dfsReverseSearch(caller, visited, path, edges, results); } } } // バックトラック path.shift(); visited.delete(current.symbol); if (edges.length > 0) { edges.pop(); } } /** * Mermaid形式のグラフを生成 * @param paths 経路リスト * @param baseName 基本ファイル名 * @returns Mermaid形式の文字列と出力パス */ generateMermaidFormat(paths, baseName) { let mermaid = '```mermaid\n'; mermaid += 'classDiagram\n'; // クラスとメソッドの関係を整理 const classMethods = new Map(); const methodCalls = new Set(); // ノードとエッジを収集 for (const path of paths) { for (const node of path.nodes) { const [className, methodName] = node.symbol.split('.'); if (methodName) { // クラスとメソッドの関係を記録 if (!classMethods.has(className)) { classMethods.set(className, new Set()); } classMethods.get(className).add(methodName); } } for (const edge of path.edges) { const [callerClass, callerMethod] = edge.caller.symbol.split('.'); const [calleeClass, calleeMethod] = edge.callee.symbol.split('.'); if (callerMethod && calleeMethod) { methodCalls.add(`${callerClass}.${callerMethod} --> ${calleeClass}.${calleeMethod}`); } } } // クラスとメソッドを出力 for (const [className, methods] of classMethods) { mermaid += ` class ${className} {\n`; for (const method of methods) { mermaid += ` +${method}()\n`; } mermaid += ' }\n'; } // メソッド間の呼び出し関係を出力 for (const call of methodCalls) { mermaid += ` ${call}\n`; } mermaid += '```\n'; // 出力パスを生成 const outputPath = this.generateOutputPath(baseName); return { content: mermaid, outputPath }; } } //# sourceMappingURL=CallGraphAnalyzer.js.map