UNPKG

symref

Version:

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

307 lines 13.3 kB
import { SyntaxKind } from 'ts-morph'; import * as path from 'path'; import { ProjectManager } from './ProjectManager.js'; import { SymbolFinder } from './SymbolFinder.js'; import { NodeUtils } from '../utils/NodeUtils.js'; import { CallGraphAnalyzer } from './CallGraphAnalyzer.js'; import { RuntimeTraceAnalyzer } from './RuntimeTraceAnalyzer.js'; /** * TypeScriptコードのシンボル参照を分析するクラス */ export class SymbolReferenceAnalyzer { /** * コンストラクタ * @param options 設定オプション */ constructor(options) { this.runtimeTraceAnalyzer = null; this.options = options; this.basePath = path.resolve(options.basePath); this.projectManager = new ProjectManager(options); this.symbolFinder = new SymbolFinder(this.projectManager.getProject()); this.nodeUtils = new NodeUtils(); this.callGraphAnalyzer = new CallGraphAnalyzer(this.projectManager.getProject()); if (options.runtime) { this.runtimeTraceAnalyzer = new RuntimeTraceAnalyzer(this.projectManager.getProject(), options); } } /** * シンボルの参照を分析する * @param symbolName 分析対象のシンボル名 * @param options 分析オプション * @returns 参照分析結果 */ analyzeSymbol(symbolName, options = {}) { const definitionNode = this.symbolFinder.findDefinitionNode(symbolName); if (!definitionNode) { throw new Error(`Symbol '${symbolName}' was not found in the codebase. Please check: 1. The symbol name is correct and matches exactly (case-sensitive) 2. The symbol is defined in one of the analyzed files 3. The file containing the symbol is included in the search path`); } const symbolType = this.nodeUtils.determineSymbolType(definitionNode); const references = this.symbolFinder.collectReferences(symbolName, definitionNode, options.includeInternalReferences); const definition = this.symbolFinder.extractDefinitionInfo(definitionNode); return { symbol: symbolName, type: symbolType, definition, references, isReferenced: references.length > 0 }; } /** * ファイル内の未参照シンボルをチェック * @param filePath チェック対象のファイルパス * @returns 他のファイルから参照されていないシンボルのリスト */ checkFile(filePath) { const project = this.projectManager.getProject(); const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(this.basePath, filePath); const sourceFile = project.getSourceFile(absolutePath); if (!sourceFile) { throw new Error(`File not found: ${filePath}`); } const unreferencedSymbols = []; const checkedSymbols = new Set(); // トップレベルのシンボルをチェック this.checkTopLevelSymbols(sourceFile, checkedSymbols, unreferencedSymbols); // クラスメンバーをチェック this.checkClassMembers(sourceFile, checkedSymbols, unreferencedSymbols); return unreferencedSymbols; } /** * トップレベルのシンボルをチェックする * @param sourceFile ソースファイル * @param checkedSymbols チェック済みシンボルのセット * @param unreferencedSymbols 未参照シンボルのリスト */ checkTopLevelSymbols(sourceFile, checkedSymbols, unreferencedSymbols) { // クラス宣言をチェック sourceFile.getClasses().forEach((classDecl) => { const className = classDecl.getName(); if (className && !checkedSymbols.has(className)) { checkedSymbols.add(className); try { const result = this.analyzeSymbol(className); if (!result.isReferenced) { unreferencedSymbols.push({ type: 'class', name: className, context: 'モジュールスコープ' }); } } catch (error) { // シンボルが見つからない場合はスキップ } } }); // インターフェース宣言をチェック sourceFile.getInterfaces().forEach((interfaceDecl) => { const interfaceName = interfaceDecl.getName(); if (interfaceName && !checkedSymbols.has(interfaceName)) { checkedSymbols.add(interfaceName); try { const result = this.analyzeSymbol(interfaceName); if (!result.isReferenced) { unreferencedSymbols.push({ type: 'interface', name: interfaceName, context: 'モジュールスコープ' }); } } catch (error) { // シンボルが見つからない場合はスキップ } } }); // 関数宣言をチェック sourceFile.getFunctions().forEach((funcDecl) => { const funcName = funcDecl.getName(); if (funcName && !checkedSymbols.has(funcName)) { checkedSymbols.add(funcName); try { const result = this.analyzeSymbol(funcName); if (!result.isReferenced) { unreferencedSymbols.push({ type: 'function', name: funcName, context: 'モジュールスコープ' }); } } catch (error) { // シンボルが見つからない場合はスキップ } } }); } /** * クラスメンバーをチェックする * @param sourceFile ソースファイル * @param checkedSymbols チェック済みシンボルのセット * @param unreferencedSymbols 未参照シンボルのリスト */ checkClassMembers(sourceFile, checkedSymbols, unreferencedSymbols) { sourceFile.getClasses().forEach((classDecl) => { const className = classDecl.getName(); if (!className) return; // パブリックメソッドをチェック classDecl.getMethods() .filter((method) => { const modifiers = method.getModifiers(); const isPublic = !modifiers.some((m) => m.getText() === 'private' || m.getText() === 'protected'); const isStatic = modifiers.some((m) => m.getText() === 'static'); return isPublic && !isStatic; }) .forEach((method) => { const methodName = method.getName(); if (methodName && !checkedSymbols.has(`${className}.${methodName}`)) { checkedSymbols.add(`${className}.${methodName}`); // メソッド参照の分析は複雑なため、簡易的なチェックを行う const references = this.findMethodReferences(className, methodName); if (references.length === 0) { unreferencedSymbols.push({ type: 'method', name: methodName, context: `クラス '${className}' 内` }); } } }); // パブリックプロパティをチェック classDecl.getProperties() .filter((prop) => { const modifiers = prop.getModifiers(); const isPublic = !modifiers.some((m) => m.getText() === 'private' || m.getText() === 'protected'); const isStatic = modifiers.some((m) => m.getText() === 'static'); return isPublic && !isStatic; }) .forEach((prop) => { const propName = prop.getName(); if (propName && !checkedSymbols.has(`${className}.${propName}`)) { checkedSymbols.add(`${className}.${propName}`); // プロパティ参照の分析 const references = this.findPropertyReferences(className, propName); if (references.length === 0) { unreferencedSymbols.push({ type: 'property', name: propName, context: `クラス '${className}' 内` }); } } }); }); } /** * メソッド参照を検索する * @param className クラス名 * @param methodName メソッド名 * @returns 参照の配列 */ findMethodReferences(className, methodName) { const project = this.projectManager.getProject(); const references = []; for (const sourceFile of project.getSourceFiles()) { // .d.tsファイルはスキップ if (sourceFile.getFilePath().endsWith('.d.ts')) continue; // プロパティアクセス式を検索 const propAccesses = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression); for (const propAccess of propAccesses) { if (propAccess.getName() === methodName) { const obj = propAccess.getExpression(); // クラスのインスタンスメソッド呼び出しを検出 if (obj.getType().getText().includes(className)) { references.push(propAccess); } } } } return references; } /** * プロパティ参照を検索する * @param className クラス名 * @param propertyName プロパティ名 * @returns 参照の配列 */ findPropertyReferences(className, propertyName) { const project = this.projectManager.getProject(); const references = []; for (const sourceFile of project.getSourceFiles()) { // .d.tsファイルはスキップ if (sourceFile.getFilePath().endsWith('.d.ts')) continue; // プロパティアクセス式を検索 const propAccesses = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression); for (const propAccess of propAccesses) { if (propAccess.getName() === propertyName) { const obj = propAccess.getExpression(); // クラスのインスタンスプロパティアクセスを検出 if (obj.getType().getText().includes(className)) { references.push(propAccess); } } } } return references; } /** * 呼び出しグラフを構築する * @returns 構築されたノード数 */ buildCallGraph() { return this.callGraphAnalyzer.buildCallGraph(); } /** * 2つのシンボル間の呼び出し経路を分析 * @param fromSymbol 開始シンボル * @param toSymbol 終了シンボル * @returns 呼び出し経路の分析結果 */ traceCallPath(fromSymbol, toSymbol) { return this.callGraphAnalyzer.findPathsFromTo(fromSymbol, toSymbol); } /** * シンボルを呼び出すすべての経路を分析 * @param symbol 対象シンボル * @returns 呼び出し経路の分析結果 */ findCallers(symbol) { return this.callGraphAnalyzer.findAllCallers(symbol); } /** * プロジェクトインスタンスを取得する * @returns プロジェクトインスタンス */ getProject() { return this.projectManager.getProject(); } /** * 2つのシンボル間の呼び出し経路を動的に分析 * @param fromSymbol 開始シンボル * @param toSymbol 終了シンボル * @returns 呼び出し経路の分析結果 */ async traceCallPathRuntime(fromSymbol, toSymbol) { if (!this.runtimeTraceAnalyzer) { console.log('注意: 動的トレース機能は実験的な機能です。現在は静的解析にフォールバックします。'); return this.traceCallPath(fromSymbol, toSymbol); } try { return await this.runtimeTraceAnalyzer.traceRuntime(fromSymbol, toSymbol); } catch (error) { console.log('注意: 動的トレースの実行に失敗しました。静的解析を実行します。'); return this.traceCallPath(fromSymbol, toSymbol); } } } //# sourceMappingURL=SymbolReferenceAnalyzer.js.map