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