autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
359 lines (358 loc) • 16.5 kB
JavaScript
/**
* @module CallEdgeResolver
* @description Phase 5: 将调用点 (CallSite) 解析为调用边 (ResolvedEdge)
*
* 解析优先级 (4-priority system):
* 1. this.xxx() — 同类方法调用
* 2. ImportedType.method() / importedFunc() — import-based 解析
* 3. localFunc() — 同文件内函数调用
* 4. globalSearch(name) — 全局唯一匹配 (fallback, 低置信度)
*
* 数据流:
* SymbolTable + ImportPathResolver + CallSite[] → ResolvedEdge[]
*/
export class CallEdgeResolver {
classNames;
fileIndex;
importResolver;
inheritanceGraph;
instantiatedClasses;
nameIndex;
propertyTypes;
symbolTable;
/**
* @param [inheritanceGraph=[]] 继承图边
*/
constructor(symbolTable, importResolver, inheritanceGraph = []) {
this.symbolTable = symbolTable;
this.importResolver = importResolver;
this.inheritanceGraph = inheritanceGraph;
// Phase 5.3: RTA — set of classes that are actually instantiated in the program
this.instantiatedClasses = symbolTable.instantiatedClasses || new Set();
// Phase 5.3: DI — property type annotations: className → (fieldName → typeName)
this.propertyTypes = symbolTable.propertyTypes || new Map();
// 构建反向索引: symbolName → [fqn1, fqn2, ...]
this.nameIndex = new Map();
// 构建文件级索引: file → [{ name, qualifiedName, fqn }] (Issue #14 性能优化)
/** >>} */
this.fileIndex = new Map();
// Phase 5.3: 类名集合索引 (用于 _inferFieldType 优化,避免全表扫描)
this.classNames = new Set();
for (const [fqn, decl] of symbolTable.declarations) {
const names = [decl.name];
const qualifiedName = decl.className ? `${decl.className}.${decl.name}` : decl.name;
if (decl.className) {
names.push(qualifiedName);
}
for (const name of names) {
if (!this.nameIndex.has(name)) {
this.nameIndex.set(name, []);
}
this.nameIndex.get(name)?.push(fqn);
}
// 文件级索引
if (!this.fileIndex.has(decl.file)) {
this.fileIndex.set(decl.file, []);
}
this.fileIndex.get(decl.file)?.push({ name: decl.name, qualifiedName, fqn });
// Phase 5.3: 收集类名用于快速 DI 推断
if (decl.kind === 'class') {
this.classNames.add(decl.name);
}
}
}
/**
* 解析一个文件中的所有调用点为边
*
* @param callSites 来自某个文件的所有调用点
* @param callerFile 调用者文件路径 (相对)
*/
resolveFile(callSites, callerFile) {
const edges = [];
const fileImports = this.symbolTable.fileImports.get(callerFile) || [];
// 构建局部 import 映射: symbolName → { file, namespace }
const importedSymbols = this._buildImportMap(fileImports, callerFile);
// 去重集合: "caller→callee@line" 防止同一调用点产生重复边
const seen = new Set();
for (const cs of callSites) {
const resolved = this._resolveCallSite(cs, callerFile, importedSymbols);
if (resolved) {
const key = `${resolved.caller}→${resolved.callee}@${resolved.line}`;
if (!seen.has(key)) {
seen.add(key);
edges.push(resolved);
}
}
}
return edges;
}
/** 构建局部 import 映射 */
_buildImportMap(fileImports, callerFile) {
/** >} */
const importedSymbols = new Map();
for (const imp of fileImports) {
const targetFile = this.importResolver.resolve(imp.path || String(imp), callerFile);
if (!targetFile) {
continue; // 外部依赖, 跳过
}
if (imp.symbols && imp.symbols.length > 0) {
for (const sym of imp.symbols) {
if (sym === '*' && imp.alias) {
importedSymbols.set(imp.alias, { file: targetFile, namespace: true });
}
else if (sym !== '*') {
// named/default: symbols 已包含本地名 (alias baked-in), 直接使用
importedSymbols.set(sym, { file: targetFile, namespace: false });
}
}
}
else {
// 无结构化信息时,使用路径最后一段作为 namespace hint
const pathParts = String(imp).split('/');
const lastPart = pathParts[pathParts.length - 1]?.replace(/\.\w+$/, '');
if (lastPart) {
importedSymbols.set(lastPart, { file: targetFile, namespace: true });
}
}
}
return importedSymbols;
}
/** 解析单个调用点 */
_resolveCallSite(cs, callerFile, importedSymbols) {
const callerFqn = `${callerFile}::${cs.callerClass ? `${cs.callerClass}.` : ''}${cs.callerMethod}`;
// Priority 0: super.xxx() — 父类方法调用 (CHA 解析,禁止 fallthrough 防止自引用边)
if (cs.callType === 'super' || cs.receiver === 'super' || cs.receiver === 'super()') {
if (cs.callerClass && cs.callee && cs.callee !== 'super') {
const chaResult = this._resolveByCHA(cs.callee, cs.callerClass);
if (chaResult) {
return this._makeEdge(callerFqn, chaResult, 'cha', cs, callerFile);
}
}
// CHA 无法解析时不 fallthrough (避免 local search 匹配到自己产生 self-edge)
return null;
}
// Priority 1: this.xxx() / self.xxx() — 同类方法调用
if (cs.receiver === 'this' || cs.receiver === 'self') {
if (cs.callerClass) {
const candidates = this._findInFile(`${cs.callerClass}.${cs.callee}`, callerFile);
if (candidates.length > 0) {
return this._makeEdge(callerFqn, candidates[0], 'direct', cs, callerFile);
}
// CHA fallback: 在继承链上查找方法
const chaResult = this._resolveByCHA(cs.callee, cs.callerClass);
if (chaResult) {
return this._makeEdge(callerFqn, chaResult, 'cha', cs, callerFile);
}
}
}
// Priority 1.5: this.field.method() — DI 注入字段方法调用
if (cs.receiver && (cs.receiver.startsWith('this.') || cs.receiver.startsWith('self.'))) {
const fieldName = cs.receiver.split('.').slice(1).join('.');
// Phase 5.3: First try explicit type annotation from property declarations (DI-aware)
if (cs.callerClass) {
const classProps = this.propertyTypes.get(cs.callerClass);
if (classProps) {
const fieldType = classProps.get(fieldName);
if (fieldType) {
const typeCandidates = this.nameIndex.get(`${fieldType}.${cs.callee}`) || [];
if (typeCandidates.length > 0) {
return this._makeEdge(callerFqn, typeCandidates[0], 'direct', cs, callerFile);
}
}
}
}
// 尝试从 receiverType 解析 (可能 extractCallSites 已推断)
if (cs.receiverType) {
const typeCandidates = this.nameIndex.get(`${cs.receiverType}.${cs.callee}`) || [];
if (typeCandidates.length > 0) {
return this._makeEdge(callerFqn, typeCandidates[0], 'direct', cs, callerFile);
}
}
// 尝试通过命名约定推断: userRepo → UserRepo, userService → UserService
const inferredType = this._inferFieldType(fieldName);
if (inferredType) {
const typeCandidates = this.nameIndex.get(`${inferredType}.${cs.callee}`) || [];
if (typeCandidates.length > 0) {
return this._makeEdge(callerFqn, typeCandidates[0], 'inferred', cs, callerFile);
}
}
}
// Priority 2: Import-based 解析
const importInfo = importedSymbols.get(cs.receiver || cs.callee);
if (importInfo) {
const targetFile = importInfo.file;
if (importInfo.namespace && cs.receiver) {
// namespace import: M.foo() → 在 targetFile 中查找 foo
const candidates = this._findInFile(cs.callee, targetFile);
if (candidates.length > 0) {
return this._makeEdge(callerFqn, candidates[0], 'direct', cs, callerFile);
}
}
else {
// named import: 查找 import 的符号
const lookupName = cs.receiver ? `${cs.receiver}.${cs.callee}` : cs.callee;
let candidates = this._findInFile(lookupName, targetFile);
if (candidates.length === 0 && cs.receiver) {
// 可能 import 的是类名,方法是类的方法
candidates = this._findInFile(`${cs.receiver}.${cs.callee}`, targetFile);
}
if (candidates.length === 0 && !cs.receiver) {
// 可能是函数名
candidates = this._findInFile(cs.callee, targetFile);
}
if (candidates.length > 0) {
return this._makeEdge(callerFqn, candidates[0], 'direct', cs, callerFile);
}
}
}
// Priority 2.5: Implicit this — OOP 语言中 bare method() 即 this.method()
// 在 Dart/Java/Kotlin/Swift 等语言中, 类内调用 method() 等价于 this.method()
// 先查同类方法, 再 CHA 查父类方法
if (!cs.receiver && cs.callerClass && cs.callType !== 'constructor') {
// 2.5a: 同类方法 (精确匹配 Class.method)
const implicitThisCandidates = this._findInFile(`${cs.callerClass}.${cs.callee}`, callerFile).filter((fqn) => fqn !== callerFqn);
if (implicitThisCandidates.length > 0) {
return this._makeEdge(callerFqn, implicitThisCandidates[0], 'direct', cs, callerFile);
}
// 2.5b: CHA 查父类 (继承链上的方法)
const chaImplicit = this._resolveByCHA(cs.callee, cs.callerClass);
if (chaImplicit) {
return this._makeEdge(callerFqn, chaImplicit, 'cha', cs, callerFile);
}
}
// Priority 3: 同文件内的函数调用
// 过滤 callerFqn 防止同名方法重载(overload)产生假自引用边
const localCandidates = this._findInFile(cs.callee, callerFile).filter((fqn) => fqn !== callerFqn);
if (localCandidates.length > 0) {
return this._makeEdge(callerFqn, localCandidates[0], 'direct', cs, callerFile);
}
// 也尝试 Class.method 格式
if (cs.receiver && !importedSymbols.has(cs.receiver)) {
const qualifiedLocal = this._findInFile(`${cs.receiver}.${cs.callee}`, callerFile).filter((fqn) => fqn !== callerFqn);
if (qualifiedLocal.length > 0) {
return this._makeEdge(callerFqn, qualifiedLocal[0], 'direct', cs, callerFile);
}
}
// Priority 4: 全局搜索 (唯一匹配才采用)
// 过滤 callerFqn 防止全局唯一命名碰撞自己
const globalCandidates = (this.nameIndex.get(cs.callee) || []).filter((fqn) => fqn !== callerFqn);
if (globalCandidates.length === 1) {
return this._makeEdge(callerFqn, globalCandidates[0], 'inferred', cs, callerFile);
}
// Phase 5.3 RTA: 多个全局候选 → 用实例化集合过滤
if (globalCandidates.length > 1 && this.instantiatedClasses.size > 0) {
const rtaFiltered = globalCandidates.filter((fqn) => {
if (fqn === callerFqn) {
return false; // 排除自己
}
const decl = this.symbolTable.declarations.get(fqn);
if (!decl) {
return false;
}
// 非类方法 (顶层函数) 不做 RTA 过滤
if (!decl.className) {
return true;
}
// 类方法 → 仅保留实际实例化的类
return this.instantiatedClasses.has(decl.className);
});
if (rtaFiltered.length === 1) {
return this._makeEdge(callerFqn, rtaFiltered[0], 'rta', cs, callerFile);
}
}
// 无法解析 → 不创建边 (宁缺勿滥)
return null;
}
/**
* CHA (Class Hierarchy Analysis): 沿继承链向上搜索方法
*
* 使用 BFS 遍历 inheritanceGraph,从 className 向上搜索直到找到
* 定义了 methodName 的祖先类。只跟踪 'inherits' 类型的边。
*
* @param methodName 被调用的方法名
* @param className 起始类名
* @returns 找到的 FQN 或 null
*/
_resolveByCHA(methodName, className) {
if (!this.inheritanceGraph || this.inheritanceGraph.length === 0) {
return null;
}
// BFS 向上遍历继承链 (最多 10 层防止循环)
const visited = new Set([className]);
const queue = [className];
const MAX_DEPTH = 10;
let depth = 0;
while (queue.length > 0 && depth < MAX_DEPTH) {
depth++;
const nextQueue = [];
for (const current of queue) {
// 查找 current 的所有父类 (inherits 和 conforms 类型的边)
for (const edge of this.inheritanceGraph) {
if (edge.from === current && !visited.has(edge.to)) {
visited.add(edge.to);
// 在全局符号表中查找 ParentClass.methodName
const qualifiedName = `${edge.to}.${methodName}`;
const candidates = this.nameIndex.get(qualifiedName) || [];
if (candidates.length > 0) {
return candidates[0];
}
nextQueue.push(edge.to);
}
}
}
queue.length = 0;
queue.push(...nextQueue);
}
return null;
}
/**
* 从字段名推断类型(DI/IoC 命名约定推断)
*
* 常见模式:
* - userRepo → UserRepo
* - userRepository → UserRepository
* - userService → UserService
* - _userRepo → UserRepo (Java/Kotlin private field)
*
* 只在符号表中存在匹配类时返回
*/
_inferFieldType(fieldName) {
// 去除前导下划线
const cleaned = fieldName.replace(/^_+/, '');
if (!cleaned) {
return null;
}
// camelCase → PascalCase
const pascalCase = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
// Phase 5.3: 使用 classNames Set 快速查找 (O(1) 替代 O(n) 全表扫描)
return this.classNames.has(pascalCase) ? pascalCase : null;
}
/**
* 在指定文件中查找声明 (使用 fileIndex 优化,避免全表扫描)
* @param name 符号名 (可以是 "ClassName.methodName" 或 "functionName")
* @returns 匹配的 FQN 列表
*/
_findInFile(name, file) {
const fileDecls = this.fileIndex.get(file);
if (!fileDecls) {
return [];
}
return fileDecls
.filter((d) => d.name === name || d.qualifiedName === name)
.map((d) => d.fqn);
}
/** 构建 ResolvedEdge */
_makeEdge(callerFqn, calleeFqn, resolveMethod, cs, callerFile) {
return {
caller: callerFqn,
callee: calleeFqn,
callType: cs.callType,
resolveMethod,
line: cs.line,
file: callerFile,
isAwait: cs.isAwait,
argCount: cs.argCount || 0,
};
}
}
export default CallEdgeResolver;