autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
639 lines (638 loc) • 25.5 kB
JavaScript
/**
* @module ProjectGraph
* @description 基于 Tree-sitter 的项目结构图 - v3.0 AI-First Bootstrap 核心组件
*
* 职责:
* 1. 扫描项目源码文件 → 调用 AstAnalyzer 解析
* 2. 构建 类/协议/Category 的查询索引
* 3. 提供查询 API 供 Analyst Agent 工具调用
*
* 生命周期:
* - 在 Bootstrap Phase 1 一次性构建 (ProjectGraph.build())
* - 所有维度共享同一个实例
* - 构建后只读
*/
import fs from 'node:fs';
import path from 'node:path';
import { LanguageService } from '../../shared/LanguageService.js';
import { analyzeFile, isAvailable } from '../AstAnalyzer.js';
// ──────────────────────────────────────────────────────────────────
// 默认配置
// ──────────────────────────────────────────────────────────────────
const DEFAULTS = {
maxFiles: 500,
maxFileSizeBytes: 500_000, // 500KB — 跳过超大文件
excludePatterns: [
// 从 LanguageService 统一跳过目录派生(添加 '/' 后缀以匹配路径片段)
...[...LanguageService.scanSkipDirs].map((d) => `${d}/`),
// ProjectGraph 额外: 测试目录
'__tests__/',
'Tests/',
'test/',
'tests/',
// Glob-style (egg-info)
'*.egg-info/',
],
// 从 LanguageService 派生,仅覆盖 AST 解析需要区分 tsx 的场景
extensionToLang: {
...LanguageService.extToLangMap,
'.tsx': 'tsx', // tree-sitter 需要独立的 tsx 解析器
},
};
// ──────────────────────────────────────────────────────────────────
// ProjectGraph
// ──────────────────────────────────────────────────────────────────
export default class ProjectGraph {
#classes = new Map();
#protocols = new Map();
#categories = new Map();
/** 子类 → 父类 */
#inheritance = new Map();
/** 类 → 遵循的协议集合 */
#conformance = new Map();
/** 文件路径 → 文件级符号 */
#files = new Map();
/** className → 方法列表 (含 impl 中的方法) */
#methodsByClass = new Map();
/** 项目统计缓存 */
#overview = null;
/** 项目根目录 */
#projectRoot;
/** 构建耗时 ms */
#buildTimeMs = 0;
// ── 静态工厂 ──────────────────────────────────────────────────
/**
* 扫描项目并构建 ProjectGraph
* @param projectRoot 项目根目录
* @param [options.extensions] 例如 ['.m', '.h', '.swift']
* @param [options.onProgress] (parsed, total) => void
*/
static async build(projectRoot, options = {}) {
if (!isAvailable()) {
throw new Error('Tree-sitter not available — cannot build ProjectGraph');
}
const startTime = Date.now();
const opts = { ...DEFAULTS, ...options };
// 1. 收集文件列表
const extToLang = opts.extensionToLang || DEFAULTS.extensionToLang;
const extensions = options.extensions ? options.extensions : Object.keys(extToLang);
const files = collectSourceFiles(projectRoot, extensions, opts);
// 2. 逐文件解析
const graph = new ProjectGraph();
graph.#projectRoot = projectRoot;
let parsed = 0;
for (const filePath of files) {
if (opts.timeoutMs && Date.now() - startTime > opts.timeoutMs) {
break; // 超时 — 返回部分结果
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
const ext = path.extname(filePath);
const lang = extToLang[ext];
if (!lang) {
continue;
}
const relativePath = path.relative(projectRoot, filePath);
const summary = analyzeFile(content, lang);
if (!summary) {
continue;
}
graph.#indexFileSummary(relativePath, summary);
parsed++;
opts.onProgress?.(parsed, files.length);
}
catch {
// 单文件解析失败不阻塞
}
}
// 3. 构建反向索引
graph.#buildReverseIndices();
graph.#buildTimeMs = Date.now() - startTime;
return graph;
}
// ── 查询 API ──────────────────────────────────────────────────
/** 获取类的完整信息 */
getClassInfo(className) {
return this.#classes.get(className) || null;
}
/** 获取协议定义 + 所有遵循者 */
getProtocolInfo(protocolName) {
return this.#protocols.get(protocolName) || null;
}
/**
* 获取继承链 (向上到根类)
* @returns [className, parent, grandparent, ...]
*/
getInheritanceChain(className) {
const chain = [];
let current = className;
const visited = new Set();
while (current && !visited.has(current)) {
chain.push(current);
visited.add(current);
current = this.#inheritance.get(current) || null;
}
return chain;
}
/** 获取直接子类 */
getSubclasses(className) {
const subs = [];
for (const [child, parent] of this.#inheritance) {
if (parent === className) {
subs.push(child);
}
}
return subs;
}
/** 递归获取所有后代类 */
getAllDescendants(className) {
const result = [];
const queue = [className];
const visited = new Set();
while (queue.length > 0) {
const current = queue.shift();
if (visited.has(current)) {
continue;
}
visited.add(current);
const subs = this.getSubclasses(current);
result.push(...subs);
queue.push(...subs);
}
return result;
}
/** 获取类的所有 Category 扩展 */
getCategoryExtensions(className) {
return this.#categories.get(className) || [];
}
/**
* 查找覆写了指定方法的所有后代类
* @param methodName 方法名或 selector
*/
getMethodOverrides(className, methodName) {
const descendants = this.getAllDescendants(className);
const overrides = [];
for (const desc of descendants) {
const methods = this.#methodsByClass.get(desc) || [];
const match = methods.find((m) => m.name === methodName || m.selector === methodName);
if (match) {
overrides.push({
className: desc,
method: match,
filePath: this.#classes.get(desc)?.filePath || 'unknown',
});
}
}
return overrides;
}
/** 获取类的所有方法 */
getClassMethods(className) {
return this.#methodsByClass.get(className) || [];
}
/** 获取文件的符号摘要 */
getFileSymbols(relativePath) {
return this.#files.get(relativePath) || null;
}
/**
* 获取所有已解析的文件路径
* @returns 相对路径列表
*/
getAllFilePaths() {
return [...this.#files.keys()];
}
/** 搜索类名 (模糊匹配) */
searchClasses(query, limit = 20) {
const lower = query.toLowerCase();
const results = [];
for (const name of this.#classes.keys()) {
if (name.toLowerCase().includes(lower)) {
results.push(name);
if (results.length >= limit) {
break;
}
}
}
return results;
}
/** 获取项目概览统计 */
getOverview() {
if (this.#overview) {
return this.#overview;
}
// 按模块 (顶层目录) 统计
const classesPerModule = {};
const topModules = new Set();
const entryPoints = [];
for (const [filePath, symbols] of this.#files) {
const parts = filePath.split('/');
const module = parts.length > 1 ? parts[0] : '(root)';
topModules.add(module);
if (!classesPerModule[module]) {
classesPerModule[module] = 0;
}
classesPerModule[module] += symbols.classes.length;
// 入口点检测
const base = path.basename(filePath);
if (/^(AppDelegate|main|SceneDelegate)\.(m|swift)$/.test(base)) {
entryPoints.push(filePath);
}
}
this.#overview = {
totalFiles: this.#files.size,
totalClasses: this.#classes.size,
totalProtocols: this.#protocols.size,
totalCategories: [...this.#categories.values()].reduce((s, arr) => s + arr.length, 0),
totalMethods: [...this.#methodsByClass.values()].reduce((s, arr) => s + arr.length, 0),
topLevelModules: [...topModules].sort(),
entryPoints,
classesPerModule,
buildTimeMs: this.#buildTimeMs,
};
return this.#overview;
}
/** 获取所有类名 */
getAllClassNames() {
return [...this.#classes.keys()];
}
/** 获取所有协议名 */
getAllProtocolNames() {
return [...this.#protocols.keys()];
}
// ── 内部索引构建 ──────────────────────────────────────────────
/** 索引单个文件的解析结果 */
#indexFileSummary(relativePath, summary) {
const fileSymbols = {
path: relativePath,
lang: summary.lang,
classes: [],
protocols: [],
categories: [],
imports: summary.imports || [],
};
// 索引类
for (const cls of summary.classes) {
const classInfo = {
name: cls.name,
filePath: relativePath,
line: cls.line,
endLine: cls.endLine,
superClass: cls.superclass || null,
protocols: cls.protocols || [],
properties: [],
methods: [],
imports: summary.imports || [],
};
// 收集该类的属性
for (const prop of summary.properties || []) {
if (prop.className === cls.name) {
classInfo.properties.push({
name: prop.name,
type: prop.type || 'id',
attributes: prop.attributes || [],
line: prop.line,
});
}
}
// 收集该类的方法 (声明 + 定义去重)
const methodSet = new Set();
for (const m of summary.methods || []) {
if (m.className === cls.name) {
const key = `${m.isClassMethod ? '+' : '-'}${m.name}`;
if (!methodSet.has(key)) {
methodSet.add(key);
classInfo.methods.push({
name: m.name,
selector: m.selector || m.name,
line: m.line,
isClassMethod: m.isClassMethod || false,
returnType: m.returnType || 'void',
paramCount: m.paramCount || 0,
bodyLines: m.bodyLines || 0,
complexity: m.complexity || 1,
});
}
}
}
this.#classes.set(cls.name, classInfo);
// 继承关系
if (cls.superclass) {
this.#inheritance.set(cls.name, cls.superclass);
}
// 协议遵循
if (cls.protocols && cls.protocols.length > 0) {
if (!this.#conformance.has(cls.name)) {
this.#conformance.set(cls.name, new Set());
}
for (const p of cls.protocols) {
this.#conformance.get(cls.name).add(p);
}
}
fileSymbols.classes.push(cls.name);
}
// 索引协议
for (const proto of summary.protocols) {
const protoInfo = {
name: proto.name,
filePath: relativePath,
line: proto.line,
inherits: proto.inherits || [],
requiredMethods: [],
optionalMethods: [],
conformers: [], // 稍后在 buildReverseIndices 中填充
};
for (const m of proto.methods || []) {
const methodInfo = {
name: m.name,
selector: m.selector || m.name,
line: m.line,
isClassMethod: m.isClassMethod || false,
returnType: m.returnType || 'void',
paramCount: m.paramCount || 0,
};
if (m.isOptional) {
protoInfo.optionalMethods.push(methodInfo);
}
else {
protoInfo.requiredMethods.push(methodInfo);
}
}
this.#protocols.set(proto.name, protoInfo);
fileSymbols.protocols.push(proto.name);
}
// 索引 Category
for (const cat of summary.categories) {
const catInfo = {
className: cat.className || cat.name,
categoryName: cat.categoryName || 'ext',
filePath: relativePath,
line: cat.line,
methods: (cat.methods || []).map((m) => ({
name: m.name,
selector: m.selector || m.name,
line: m.line,
isClassMethod: m.isClassMethod || false,
returnType: m.returnType || 'void',
paramCount: m.paramCount || 0,
})),
properties: [],
protocols: cat.protocols || [],
};
const key = catInfo.className;
if (!this.#categories.has(key)) {
this.#categories.set(key, []);
}
this.#categories.get(key).push(catInfo);
// Category 遵循的协议也记录到类的遵循关系
if (catInfo.protocols.length > 0) {
if (!this.#conformance.has(key)) {
this.#conformance.set(key, new Set());
}
for (const p of catInfo.protocols) {
this.#conformance.get(key).add(p);
}
}
fileSymbols.categories.push(`${catInfo.className}(${catInfo.categoryName})`);
}
// 索引方法 (按类名分组)
for (const m of summary.methods || []) {
if (!m.className) {
continue;
}
if (!this.#methodsByClass.has(m.className)) {
this.#methodsByClass.set(m.className, []);
}
this.#methodsByClass.get(m.className).push({
name: m.name,
selector: m.selector || m.name,
line: m.line,
isClassMethod: m.isClassMethod || false,
returnType: m.returnType || 'void',
paramCount: m.paramCount || 0,
bodyLines: m.bodyLines || 0,
complexity: m.complexity || 1,
filePath: relativePath,
});
}
this.#files.set(relativePath, fileSymbols);
}
/** 构建反向索引 — 协议遵循者列表 */
#buildReverseIndices() {
// 填充 protocol.conformers
for (const [className, protos] of this.#conformance) {
for (const protoName of protos) {
const proto = this.#protocols.get(protoName);
if (proto && !proto.conformers.includes(className)) {
proto.conformers.push(className);
}
}
}
// 补充 classInfo 中的 methods (从 methodsByClass 合并)
for (const [className, classInfo] of this.#classes) {
const allMethods = this.#methodsByClass.get(className) || [];
// 只补充 classInfo.methods 中没有的方法
const existingNames = new Set(classInfo.methods.map((m) => `${m.isClassMethod ? '+' : '-'}${m.name}`));
for (const m of allMethods) {
const key = `${m.isClassMethod ? '+' : '-'}${m.name}`;
if (!existingNames.has(key)) {
classInfo.methods.push(m);
existingNames.add(key);
}
}
}
}
// ── 序列化 / 反序列化 ──────────────────────────────────────
/** 序列化为可 JSON.stringify 的纯对象 */
toJSON() {
const mapToObj = (map) => Object.fromEntries(map);
const mapOfSetsToObj = (map) => {
const obj = {};
for (const [k, v] of map) {
obj[k] = [...v];
}
return obj;
};
return {
projectRoot: this.#projectRoot,
buildTimeMs: this.#buildTimeMs,
classes: mapToObj(this.#classes),
protocols: mapToObj(this.#protocols),
categories: mapToObj(this.#categories),
inheritance: mapToObj(this.#inheritance),
conformance: mapOfSetsToObj(this.#conformance),
files: mapToObj(this.#files),
methodsByClass: mapToObj(this.#methodsByClass),
};
}
/**
* 从缓存数据恢复 ProjectGraph 实例
* @param data toJSON() 输出的对象
*/
static fromJSON(data) {
const graph = new ProjectGraph();
graph.#projectRoot = data.projectRoot || '';
graph.#buildTimeMs = data.buildTimeMs || 0;
// 恢复 classes
for (const [name, info] of Object.entries(data.classes || {})) {
graph.#classes.set(name, info);
}
// 恢复 protocols
for (const [name, info] of Object.entries(data.protocols || {})) {
graph.#protocols.set(name, info);
}
// 恢复 categories
for (const [name, arr] of Object.entries(data.categories || {})) {
graph.#categories.set(name, arr);
}
// 恢复 inheritance
for (const [child, parent] of Object.entries(data.inheritance || {})) {
graph.#inheritance.set(child, parent);
}
// 恢复 conformance (Set)
for (const [cls, protos] of Object.entries(data.conformance || {})) {
graph.#conformance.set(cls, new Set(protos));
}
// 恢复 files
for (const [path, symbols] of Object.entries(data.files || {})) {
graph.#files.set(path, symbols);
}
// 恢复 methodsByClass
for (const [cls, methods] of Object.entries(data.methodsByClass || {})) {
graph.#methodsByClass.set(cls, methods);
}
return graph;
}
/**
* 增量更新:仅重新解析变更文件,合并到现有图中
* @param changedPaths 变更文件的绝对路径
* @param deletedPaths 删除文件的相对路径
* @returns >}
*/
async incrementalUpdate(changedPaths, deletedPaths = [], options = {}) {
const { analyzeFile, isAvailable } = await import('../AstAnalyzer.js');
if (!isAvailable()) {
return { added: 0, updated: 0, deleted: 0 };
}
const extToLang = options.extensionToLang || DEFAULTS.extensionToLang;
let added = 0, updated = 0, deleted = 0;
// 1. 删除已移除文件的索引
for (const relPath of deletedPaths) {
if (this.#files.has(relPath)) {
const symbols = this.#files.get(relPath);
// 清除该文件贡献的类、协议、Category
for (const cls of symbols.classes || []) {
this.#classes.delete(cls);
this.#inheritance.delete(cls);
this.#conformance.delete(cls);
this.#methodsByClass.delete(cls);
}
for (const proto of symbols.protocols || []) {
this.#protocols.delete(proto);
}
for (const catKey of symbols.categories || []) {
const className = catKey.split('(')[0];
this.#categories.delete(className);
}
this.#files.delete(relPath);
deleted++;
}
}
// 2. 重新解析变更文件
for (const filePath of changedPaths) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
const ext = path.extname(filePath);
const lang = extToLang[ext];
if (!lang) {
continue;
}
const relativePath = path.relative(this.#projectRoot, filePath);
const isUpdate = this.#files.has(relativePath);
// 先清除旧索引(如果是更新)
if (isUpdate) {
const oldSymbols = this.#files.get(relativePath);
for (const cls of oldSymbols.classes || []) {
this.#classes.delete(cls);
this.#inheritance.delete(cls);
this.#conformance.delete(cls);
this.#methodsByClass.delete(cls);
}
for (const proto of oldSymbols.protocols || []) {
this.#protocols.delete(proto);
}
for (const catKey of oldSymbols.categories || []) {
const className = catKey.split('(')[0];
this.#categories.delete(className);
}
}
const summary = analyzeFile(content, lang);
if (!summary) {
continue;
}
this.#indexFileSummary(relativePath, summary);
isUpdate ? updated++ : added++;
}
catch {
// 单文件解析失败不阻塞
}
}
// 3. 重建反向索引
if (added + updated + deleted > 0) {
this.#buildReverseIndices();
this.#overview = null; // 清除统计缓存
}
return { added, updated, deleted };
}
}
// ──────────────────────────────────────────────────────────────────
// 工具函数 — 文件收集
// ──────────────────────────────────────────────────────────────────
/** 递归收集匹配扩展名的源文件 */
function collectSourceFiles(dir, extensions, opts) {
const results = [];
const extSet = new Set(extensions);
function walk(currentDir) {
if (results.length >= opts.maxFiles) {
return;
}
let entries;
try {
entries = fs.readdirSync(currentDir, { withFileTypes: true });
}
catch {
return;
}
for (const entry of entries) {
if (results.length >= opts.maxFiles) {
return;
}
const fullPath = path.join(currentDir, entry.name);
const relativePath = path.relative(dir, fullPath);
// 排除模式检查
if (opts.excludePatterns.some((p) => relativePath.includes(p))) {
continue;
}
if (entry.isDirectory()) {
walk(fullPath);
}
else if (entry.isFile()) {
const ext = path.extname(entry.name);
if (!extSet.has(ext)) {
continue;
}
// 跳过过大的文件
try {
const stat = fs.statSync(fullPath);
if (stat.size > opts.maxFileSizeBytes) {
continue;
}
}
catch {
continue;
}
results.push(fullPath);
}
}
}
walk(dir);
return results;
}