UNPKG

symref

Version:

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

466 lines 20.8 kB
import { SyntaxKind } from 'ts-morph'; import * as path from 'node:path'; import { ProjectManager } from './ProjectManager.js'; import { SymbolFinder } from './SymbolFinder.js'; import { NodeUtils } from '../utils/NodeUtils.js'; import { CallGraphAnalyzer } from './CallGraphAnalyzer.js'; /** * TypeScriptコードのシンボル参照を分析するクラス */ export class SymbolReferenceAnalyzer { /** * コンストラクタ * @param options 設定オプション * @param dependencies 依存コンポーネント(オプション、テスト用) */ constructor(options, dependencies) { this.basePath = path.resolve(options.basePath); this.projectManager = (dependencies === null || dependencies === void 0 ? void 0 : dependencies.projectManager) || new ProjectManager(options); const project = this.projectManager.getProject(); this.symbolFinder = (dependencies === null || dependencies === void 0 ? void 0 : dependencies.symbolFinder) || new SymbolFinder(project); this.nodeUtils = (dependencies === null || dependencies === void 0 ? void 0 : dependencies.nodeUtils) || new NodeUtils(); this.callGraphAnalyzer = (dependencies === null || dependencies === void 0 ? void 0 : dependencies.callGraphAnalyzer) || new CallGraphAnalyzer(project); } /** * シンボルの参照を分析する * @param symbolName 分析対象のシンボル名 * @param options 分析オプション * @returns 参照分析結果 */ analyzeSymbol(symbolName, options = {}) { // シンボルの存在確認 if (!this.symbolFinder.hasSymbol(symbolName)) { throw new Error(`シンボル '${symbolName}' がコードベース内に見つかりません。以下を確認してください: 1. シンボル名が正確に一致している(大文字小文字を区別) 2. シンボルが分析対象のファイルで定義されている 3. シンボルを含むファイルが検索パスに含まれている`); } // hasSymbolがtrueの場合、definitionNodeは必ず存在する const definitionNode = this.symbolFinder.findDefinitionNode(symbolName); 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); } /** * シンボルが存在するかどうかを確認する * @param symbolName シンボル名 * @returns シンボルが存在する場合はtrue、存在しない場合はfalse */ hasSymbol(symbolName) { return this.symbolFinder.hasSymbol(symbolName); } /** * プロジェクトインスタンスを取得する * @returns プロジェクトインスタンス */ getProject() { return this.projectManager.getProject(); } /** * クラスメンバーの種類を判定する(内部メソッド) * @param className クラス名 * @param memberName メンバー名 * @returns シンボルの種類 */ determineClassMemberType(className, memberName) { const project = this.projectManager.getProject(); for (const sourceFile of project.getSourceFiles()) { // クラス宣言を検索 const classDecl = sourceFile.getClasses() .find(c => c.getName() === className); if (classDecl) { // メソッドをチェック const method = classDecl.getMethods() .find(m => m.getName() === memberName); if (method) return 'method'; // プロパティをチェック const property = classDecl.getProperties() .find(p => p.getName() === memberName); if (property) return 'property'; // getterをチェック const getter = classDecl.getGetAccessors() .find(g => g.getName() === memberName); if (getter) return 'property'; // setterをチェック const setter = classDecl.getSetAccessors() .find(s => s.getName() === memberName); if (setter) return 'property'; } } // デフォルトではmethod扱い return 'method'; } /** * クラスメンバーノードを検索する(内部メソッド) * @param classNameOrNode クラス名またはクラスのノード * @param memberName メンバー名 * @returns メンバーノード */ findClassMemberNode(classNameOrNode, memberName) { const project = this.projectManager.getProject(); // クラス名が文字列の場合 if (typeof classNameOrNode === 'string') { const className = classNameOrNode; for (const sourceFile of project.getSourceFiles()) { // クラス宣言を検索 const classDecl = sourceFile.getClasses() .find(c => c.getName() === className); if (classDecl) { // メソッドをチェック const method = classDecl.getMethods() .find(m => m.getName() === memberName); if (method) { return method.getNameNode(); } // プロパティをチェック const property = classDecl.getProperties() .find(p => p.getName() === memberName); if (property) { return property.getNameNode(); } // getterをチェック const getter = classDecl.getGetAccessors() .find(g => g.getName() === memberName); if (getter) { return getter.getNameNode(); } // setterをチェック const setter = classDecl.getSetAccessors() .find(s => s.getName() === memberName); if (setter) { return setter.getNameNode(); } } } } // コンテナノードが渡された場合 else { const containerNode = classNameOrNode; const parent = containerNode.getParent(); if (!parent) { return undefined; } // クラス宣言の場合 if (parent.getKind() === SyntaxKind.ClassDeclaration || containerNode.getKind() === SyntaxKind.ClassDeclaration) { const classDecl = parent.getKind() === SyntaxKind.ClassDeclaration ? parent : containerNode; // メンバーノードを検索 const members = classDecl.getDescendantsOfKind(SyntaxKind.Identifier) .filter(node => node.getText() === memberName); for (const member of members) { const parent = member.getParent(); if (parent && (parent.getKind() === SyntaxKind.MethodDeclaration || parent.getKind() === SyntaxKind.PropertyDeclaration || parent.getKind() === SyntaxKind.GetAccessor || parent.getKind() === SyntaxKind.SetAccessor)) { return member; } } } } return undefined; } /** * クラスのプロパティまたはメソッドの参照を分析する * @param containerName コンテナ名(クラス/インターフェース名) * @param memberName メンバー名(メソッド/プロパティ名) * @param options 分析オプション * @returns 参照分析結果 */ analyzePropertyOrMethod(containerName, memberName, options = {}) { // コンテナの定義ノードを検索 const containerNode = this.symbolFinder.findDefinitionNode(containerName); if (!containerNode) { throw new Error(`コンテナ '${containerName}' が見つかりません`); } // メンバーノードを検索 const memberNode = this.findClassMemberNode(containerName, memberName); if (!memberNode) { throw new Error(`メンバー '${memberName}' がコンテナ '${containerName}' 内で見つかりません`); } // メンバーの種類を判定 const memberType = this.determineClassMemberType(containerName, memberName); // 参照を収集 let references = []; if (memberType === 'method') { const methodRefs = this.findMethodReferences(containerName, memberName); // extractReferenceInfoはprivateなので、自前で処理 references = methodRefs.map(node => { const pos = node.getSourceFile().getLineAndColumnAtPos(node.getStart()); const filePath = path.relative(process.cwd(), node.getSourceFile().getFilePath()); const context = this.nodeUtils.getNodeContext(node); return { filePath, line: pos.line, column: pos.column, context }; }); } else if (memberType === 'property') { const propRefs = this.findPropertyReferences(containerName, memberName); // extractReferenceInfoはprivateなので、自前で処理 references = propRefs.map(node => { const pos = node.getSourceFile().getLineAndColumnAtPos(node.getStart()); const filePath = path.relative(process.cwd(), node.getSourceFile().getFilePath()); const context = this.nodeUtils.getNodeContext(node); return { filePath, line: pos.line, column: pos.column, context }; }); } // 定義情報 const definition = this.symbolFinder.extractDefinitionInfo(memberNode); return { symbol: `${containerName}.${memberName}`, type: memberType, definition, references, isReferenced: references.length > 0 }; } } //# sourceMappingURL=SymbolReferenceAnalyzer.js.map