autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
293 lines (292 loc) • 12.9 kB
JavaScript
/**
* @module CallGraphAnalyzer
* @description Phase 5: 顶层编排器 - 协调 Call Graph 分析的全流程
*
* 流水线:
* 1. CallSiteExtractor — 从 AST 提取调用点 (已在 AstAnalyzer 二次遍历中完成)
* 2. SymbolTableBuilder — 构建全局符号表
* 3. ImportPathResolver — 导入路径解析器
* 4. CallEdgeResolver — 调用点 → 调用边
* 5. DataFlowInferrer — 调用边 → 数据流边
*
* 输出:
* { callEdges, dataFlowEdges, stats }
*/
import { CallEdgeResolver } from './CallEdgeResolver.js';
import { DataFlowInferrer } from './DataFlowInferrer.js';
import { ImportPathResolver } from './ImportPathResolver.js';
import { SymbolTableBuilder } from './SymbolTableBuilder.js';
export class CallGraphAnalyzer {
projectRoot;
constructor(projectRoot) {
this.projectRoot = projectRoot;
}
/**
* 执行完整的调用图分析
*
* @param astProjectSummary analyzeProject() 的输出 (需包含 callSites)
*/
async analyze(astProjectSummary, options = {}) {
const t0 = Date.now();
const timeout = options.timeout || 15_000;
const maxCallSitesPerFile = options.maxCallSitesPerFile || 500;
if (!astProjectSummary?.fileSummaries?.length) {
return this._emptyResult(Date.now() - t0);
}
// ── 渐进式超时: 逐文件检查超时,返回 partial result ──
const deadline = t0 + timeout;
const result = await this._doAnalyze(astProjectSummary, maxCallSitesPerFile, deadline);
result.stats.durationMs = Date.now() - t0;
return result;
}
/**
* 增量分析 — 仅重新分析变更文件及其依赖方
*
* @param astProjectSummary analyzeProject() 的全量输出
* @param changedFiles 变更文件的相对路径列表
*/
async analyzeIncremental(astProjectSummary, changedFiles, options = {}) {
const t0 = Date.now();
const timeout = options.timeout || 15_000;
const maxCallSitesPerFile = options.maxCallSitesPerFile || 500;
if (!astProjectSummary?.fileSummaries?.length || !changedFiles?.length) {
return this._emptyResult(Date.now() - t0);
}
// ── 超过 10 个文件变更 → 回退全量分析 ──
if (changedFiles.length > 10) {
return this.analyze(astProjectSummary, options);
}
const deadline = t0 + timeout;
const changedSet = new Set(changedFiles);
// ── Step 1: 构建全局符号表 (始终全量,确保跨文件符号可解析) ──
const symbolTable = SymbolTableBuilder.build(astProjectSummary);
// ── Step 2: 构建 ImportPathResolver ──
const allFiles = astProjectSummary.fileSummaries.map((f) => f.file);
const importResolver = new ImportPathResolver(this.projectRoot, allFiles);
// ── Step 3: 找到依赖变更文件的所有文件 (reverse dependency) ──
const affectedFiles = new Set(changedFiles);
for (const fileSummary of astProjectSummary.fileSummaries) {
const imports = symbolTable.fileImports.get(fileSummary.file) || [];
for (const imp of imports) {
const resolved = importResolver.resolve(String(imp), fileSummary.file);
if (resolved && changedSet.has(resolved)) {
affectedFiles.add(fileSummary.file);
break;
}
}
}
// ── Step 4: 仅对受影响文件解析调用边 ──
const fileCount = astProjectSummary.fileSummaries.length;
const tier = _computeTier(fileCount);
const useCHA = tier === 'full-cha';
const inheritanceGraph = useCHA ? astProjectSummary.inheritanceGraph || [] : [];
const callEdgeResolver = new CallEdgeResolver(symbolTable, importResolver, inheritanceGraph);
const allCallEdges = [];
let totalCallSites = 0;
let processedFiles = 0;
for (const fileSummary of astProjectSummary.fileSummaries) {
if (!affectedFiles.has(fileSummary.file)) {
continue;
}
const callSites = fileSummary.callSites || [];
if (callSites.length === 0) {
continue;
}
// 超时检查
if (Date.now() > deadline) {
return {
callEdges: allCallEdges,
dataFlowEdges: DataFlowInferrer.infer(allCallEdges),
stats: {
totalCallSites,
resolvedCallSites: allCallEdges.length,
resolvedRate: totalCallSites > 0 ? allCallEdges.length / totalCallSites : 0,
totalEdges: allCallEdges.length,
filesProcessed: processedFiles,
symbolCount: symbolTable.declarations.size,
durationMs: Date.now() - t0,
tier,
partial: true,
incremental: true,
processedFiles,
totalFiles: affectedFiles.size,
changedFiles: changedFiles.length,
affectedFiles: affectedFiles.size,
},
};
}
const limitedCallSites = callSites.length > maxCallSitesPerFile
? callSites.slice(0, maxCallSitesPerFile)
: callSites;
totalCallSites += limitedCallSites.length;
const edges = callEdgeResolver.resolveFile(limitedCallSites, fileSummary.file);
allCallEdges.push(...edges);
processedFiles++;
}
// ── Step 5: 推断数据流 ──
const dataFlowEdges = DataFlowInferrer.infer(allCallEdges);
return {
callEdges: allCallEdges,
dataFlowEdges,
stats: {
totalCallSites,
resolvedCallSites: allCallEdges.length,
resolvedRate: totalCallSites > 0 ? allCallEdges.length / totalCallSites : 0,
totalEdges: allCallEdges.length + dataFlowEdges.length,
filesProcessed: processedFiles,
symbolCount: symbolTable.declarations.size,
durationMs: Date.now() - t0,
tier,
incremental: true,
changedFiles: changedFiles.length,
affectedFiles: affectedFiles.size,
},
};
}
/**
* 实际分析逻辑
*
* 分级降级策略 (§5.2):
* - <100 文件 → 完整分析 (含 CHA)
* - 100-500 → 完整分析,禁用 CHA
* - 500-2000 → 抽样分析 (核心目录优先)
* - >2000 → 仅模块级 import graph (跳过调用边解析)
*
* 渐进式超时 (§13 Issue #15):
* 每处理完一个文件检查 deadline,超时时返回已有的 partial result
*
* @param deadline Date.now() + timeout
*/
async _doAnalyze(astProjectSummary, maxCallSitesPerFile, deadline) {
const fileCount = astProjectSummary.fileSummaries.length;
// ── 分级降级 ──
const tier = _computeTier(fileCount);
let fileSummaries = astProjectSummary.fileSummaries;
if (tier === 'import-only') {
// >2000 文件: 仅返回 import graph,不解析调用边
return {
callEdges: [],
dataFlowEdges: [],
stats: {
totalCallSites: 0,
resolvedCallSites: 0,
resolvedRate: 0,
totalEdges: 0,
filesProcessed: fileCount,
symbolCount: 0,
durationMs: 0,
tier: 'import-only',
},
};
}
if (tier === 'sampled') {
// 500-2000 文件: 抽样核心目录 (仅限制 call site 解析范围)
fileSummaries = _sampleCoreFiles(fileSummaries, 500);
}
// ── Step 2: 构建符号表 (始终使用全量文件,确保跨文件符号可解析) ──
const symbolTable = SymbolTableBuilder.build(astProjectSummary);
// ── Step 3: 构建 ImportPathResolver (全量文件索引) ──
const allFiles = astProjectSummary.fileSummaries.map((f) => f.file);
const importResolver = new ImportPathResolver(this.projectRoot, allFiles);
// ── Step 4: 解析调用边 (逐文件 + 超时检查) ──
const useCHA = tier === 'full-cha';
const inheritanceGraph = useCHA ? astProjectSummary.inheritanceGraph || [] : [];
const callEdgeResolver = new CallEdgeResolver(symbolTable, importResolver, inheritanceGraph);
const allCallEdges = [];
let totalCallSites = 0;
let processedFiles = 0;
const totalFiles = fileSummaries.filter((f) => (f.callSites?.length ?? 0) > 0).length;
for (const fileSummary of fileSummaries) {
const callSites = fileSummary.callSites || [];
if (callSites.length === 0) {
continue;
}
// ── 渐进式超时: 每文件检查 deadline ──
if (Date.now() > deadline) {
const dataFlowEdges = DataFlowInferrer.infer(allCallEdges);
return {
callEdges: allCallEdges,
dataFlowEdges,
stats: {
totalCallSites,
resolvedCallSites: allCallEdges.length,
resolvedRate: totalCallSites > 0 ? allCallEdges.length / totalCallSites : 0,
totalEdges: allCallEdges.length + dataFlowEdges.length,
filesProcessed: processedFiles,
symbolCount: symbolTable.declarations.size,
durationMs: 0,
tier,
partial: true,
processedFiles,
totalFiles,
},
};
}
// 防护: 限制每文件调用点数 (防止超大文件)
const limitedCallSites = callSites.length > maxCallSitesPerFile
? callSites.slice(0, maxCallSitesPerFile)
: callSites;
totalCallSites += limitedCallSites.length;
const edges = callEdgeResolver.resolveFile(limitedCallSites, fileSummary.file);
allCallEdges.push(...edges);
processedFiles++;
}
// ── Step 5: 推断数据流 ──
const dataFlowEdges = DataFlowInferrer.infer(allCallEdges);
// ── Stats ──
const stats = {
totalCallSites,
resolvedCallSites: allCallEdges.length,
resolvedRate: totalCallSites > 0 ? allCallEdges.length / totalCallSites : 0,
totalEdges: allCallEdges.length + dataFlowEdges.length,
filesProcessed: processedFiles,
symbolCount: symbolTable.declarations.size,
durationMs: 0, // 由外层填充
tier,
};
return { callEdges: allCallEdges, dataFlowEdges, stats };
}
/** 空结果 */
_emptyResult(durationMs) {
return {
callEdges: [],
dataFlowEdges: [],
stats: {
totalCallSites: 0,
resolvedCallSites: 0,
resolvedRate: 0,
totalEdges: 0,
filesProcessed: 0,
symbolCount: 0,
durationMs,
},
};
}
}
// ── 分级降级辅助 ──────────────────────────────────────────
/** 根据文件数量确定分析层级 */
function _computeTier(fileCount) {
if (fileCount < 100) {
return 'full-cha';
}
if (fileCount <= 500) {
return 'full';
}
if (fileCount <= 2000) {
return 'sampled';
}
return 'import-only';
}
/** 抽样核心文件 — 优先选取 src/、lib/、app/、core/ 等核心目录 */
function _sampleCoreFiles(fileSummaries, limit) {
const CORE_DIRS = /\/(src|lib|app|core|pkg|internal|domain|service|controller|handler|api)\//i;
const scored = fileSummaries.map((f) => ({
f,
score: CORE_DIRS.test(f.file) ? 2 : 1,
callSiteCount: f.callSites?.length || 0,
}));
// 排序: 核心目录优先,有调用点的优先
scored.sort((a, b) => b.score - a.score || b.callSiteCount - a.callSiteCount);
return scored.slice(0, limit).map((s) => s.f);
}
export default CallGraphAnalyzer;