fix(arch-scan): 修复多语言扫描器并添加 DELETE 清空表

- scanner.ts: 添加 DELETE 清空旧数据语句,避免重复插入
- ts-scanner: 改用 regex 替代 ts-morph 解析(避免解析失败)
- go-scanner/py-scanner/proto-scanner: 完善实现
This commit is contained in:
SpecialX
2026-07-08 12:49:43 +08:00
parent f554011af5
commit 4629de1926
5 changed files with 560 additions and 48 deletions

View File

@@ -1,38 +1,211 @@
import { Project, SyntaxKind } from 'ts-morph';
import path from 'node:path';
import type { Database as DBType } from 'better-sqlite3';
import path from "node:path";
import fs from "node:fs";
import type { Database as DBType } from "better-sqlite3";
interface ScanStats {
modules: number;
symbols: number;
}
export function scanTypeScript(db: DBType, root: string): ScanStats {
const project = new Project({
tsConfigFilePath: undefined,
skipAddingFilesFromTsConfig: true,
compilerOptions: {
allowJs: true,
declaration: false,
resolveJsonModule: true,
},
});
/** 递归收集目录下匹配扩展名的所有文件 */
function walkDir(dir: string, exts: string[]): string[] {
if (!fs.existsSync(dir)) return [];
const results: string[] = [];
const stack: string[] = [dir];
while (stack.length > 0) {
const current = stack.pop()!;
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
// 跳过 node_modules / dist / .git
if (
["node_modules", "dist", ".git", ".next", "build"].includes(
entry.name,
)
)
continue;
stack.push(fullPath);
} else if (entry.isFile()) {
const ext = path.extname(entry.name);
if (exts.includes(ext)) results.push(fullPath);
}
}
}
return results;
}
export function scanTypeScript(db: DBType, root: string): ScanStats {
const insertModule = db.prepare(
"INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)",
);
const insertSymbol = db.prepare(
"INSERT INTO symbols (module_id, name, kind, language, file_path, line_start, line_end, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
);
const getModuleId = db.prepare("SELECT id FROM modules WHERE name = ?") as {
get: (name: string) => { id: number } | undefined;
};
const patterns = ['services/*/src/**/*.ts', 'apps/*/src/**/*.ts', 'packages/*/src/**/*.ts'];
let modules = 0;
let symbols = 0;
const insertModule = db.prepare(
'INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)'
);
const insertSymbol = db.prepare(
'INSERT INTO symbols (module_id, name, kind, language, file_path, line_start, line_end, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
);
// 扫描 services/* 和 apps/* 和 packages/* 下的 TS 服务
const serviceDirs = ["services", "apps"];
const moduleList: { name: string; path: string; service: string }[] = [];
// 简化实现:扫描 services/*/src 目录作为模块
const servicesDir = path.join(root, 'services');
// 实际实现用 fast-glob 模式匹配
// 此处为骨架P1 后期补全
for (const dir of serviceDirs) {
const baseDir = path.join(root, dir);
if (!fs.existsSync(baseDir)) continue;
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const serviceName = entry.name;
const servicePath = path.join(baseDir, serviceName);
// 确认是 TS 服务(有 src 目录或 package.json
const hasSrc = fs.existsSync(path.join(servicePath, "src"));
const hasPkg = fs.existsSync(path.join(servicePath, "package.json"));
if (!hasSrc && !hasPkg) continue;
const moduleName = serviceName;
insertModule.run(moduleName, servicePath, "ts", serviceName, "service");
const row = getModuleId.get(moduleName);
if (row) {
modules++;
moduleList.push({
name: moduleName,
path: servicePath,
service: serviceName,
});
}
}
}
// packages/* 下的 TS 包
const packagesDir = path.join(root, "packages");
if (fs.existsSync(packagesDir)) {
const entries = fs.readdirSync(packagesDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const pkgName = entry.name;
const pkgPath = path.join(packagesDir, pkgName);
const hasSrc = fs.existsSync(path.join(pkgPath, "src"));
const hasPkg = fs.existsSync(path.join(pkgPath, "package.json"));
const hasProto = fs.existsSync(path.join(pkgPath, "proto"));
if (!hasSrc && !hasPkg && !hasProto) continue;
const moduleName = pkgName;
insertModule.run(moduleName, pkgPath, "ts", pkgName, "package");
const row = getModuleId.get(moduleName);
if (row) {
modules++;
moduleList.push({ name: moduleName, path: pkgPath, service: pkgName });
}
}
}
// 收集所有 .ts 文件并用 regex 提取符号ts-morph 对未安装依赖的文件可能解析失败)
const TS_FUNC_RE = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g;
const TS_CLASS_RE = /(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/g;
const TS_INTERFACE_RE = /(?:export\s+)?interface\s+(\w+)/g;
const TS_CONST_RE = /(?:export\s+)?const\s+([A-Z][A-Z0-9_]+)\s*=/g;
for (const mod of moduleList) {
const srcDir = path.join(mod.path, "src");
if (!fs.existsSync(srcDir)) continue;
const tsFiles = walkDir(srcDir, [".ts", ".tsx"]);
const modRow = getModuleId.get(mod.name);
if (!modRow) continue;
for (const filePath of tsFiles) {
let content: string;
try {
content = fs.readFileSync(filePath, "utf-8");
} catch {
continue;
}
// 函数
let match: RegExpExecArray | null;
const funcRe = new RegExp(TS_FUNC_RE);
while ((match = funcRe.exec(content)) !== null) {
const name = match[1];
const lineNum = content.slice(0, match.index).split("\n").length;
const isExported = match[0].includes("export") ? 1 : 0;
insertSymbol.run(
modRow.id,
name,
"function",
"ts",
filePath,
lineNum,
lineNum,
isExported,
);
symbols++;
}
// 类
const classRe = new RegExp(TS_CLASS_RE);
while ((match = classRe.exec(content)) !== null) {
const name = match[1];
const lineNum = content.slice(0, match.index).split("\n").length;
const isExported = match[0].includes("export") ? 1 : 0;
insertSymbol.run(
modRow.id,
name,
"class",
"ts",
filePath,
lineNum,
lineNum,
isExported,
);
symbols++;
}
// 接口
const ifaceRe = new RegExp(TS_INTERFACE_RE);
while ((match = ifaceRe.exec(content)) !== null) {
const name = match[1];
const lineNum = content.slice(0, match.index).split("\n").length;
const isExported = match[0].includes("export") ? 1 : 0;
insertSymbol.run(
modRow.id,
name,
"interface",
"ts",
filePath,
lineNum,
lineNum,
isExported,
);
symbols++;
}
// 常量UPPER_CASE
const constRe = new RegExp(TS_CONST_RE);
while ((match = constRe.exec(content)) !== null) {
const name = match[1];
const lineNum = content.slice(0, match.index).split("\n").length;
const isExported = match[0].includes("export") ? 1 : 0;
insertSymbol.run(
modRow.id,
name,
"const",
"ts",
filePath,
lineNum,
lineNum,
isExported,
);
symbols++;
}
}
}
return { modules, symbols };
}
}