symref
Version:
Static code checker for AI code agents (Windsurf, Cline, etc.)
212 lines • 10.1 kB
JavaScript
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