Files
Edu/scripts/arch-scan/scanners/py-scanner.ts
SpecialX 4629de1926 fix(arch-scan): 修复多语言扫描器并添加 DELETE 清空表
- scanner.ts: 添加 DELETE 清空旧数据语句,避免重复插入
- ts-scanner: 改用 regex 替代 ts-morph 解析(避免解析失败)
- go-scanner/py-scanner/proto-scanner: 完善实现
2026-07-08 12:49:43 +08:00

143 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import path from "node:path";
import fs from "node:fs";
import type { Database as DBType } from "better-sqlite3";
interface ScanStats {
modules: number;
symbols: number;
}
/** 递归收集目录下匹配扩展名的所有文件 */
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()) {
if (
[
"node_modules",
"dist",
".git",
"__pycache__",
".venv",
"venv",
].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;
}
// Python 符号正则
const PY_FUNC_RE = /^(?:async\s+)?def\s+(\w+)\s*\(/gm;
const PY_CLASS_RE = /^class\s+(\w+)\s*[\(:]/gm;
export function scanPython(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;
};
let modules = 0;
let symbols = 0;
// 扫描 services/* 下有 pyproject.toml 的服务
const servicesDir = path.join(root, "services");
if (!fs.existsSync(servicesDir)) return { modules, symbols };
const entries = fs.readdirSync(servicesDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const serviceName = entry.name;
const servicePath = path.join(servicesDir, serviceName);
const hasPyproject = fs.existsSync(
path.join(servicePath, "pyproject.toml"),
);
if (!hasPyproject) continue;
insertModule.run(
serviceName,
servicePath,
"python",
serviceName,
"service",
);
const modRow = getModuleId.get(serviceName);
if (modRow) {
modules++;
// 扫描 .py 文件
const pyFiles = walkDir(servicePath, [".py"]);
for (const filePath of pyFiles) {
let content: string;
try {
content = fs.readFileSync(filePath, "utf-8");
} catch {
continue;
}
// 函数(含 async def
let match: RegExpExecArray | null;
const funcRe = new RegExp(PY_FUNC_RE);
while ((match = funcRe.exec(content)) !== null) {
const name = match[1];
const lineNum = content.slice(0, match.index).split("\n").length;
// Python 中以 _ 开头的为私有
const isExported = !name.startsWith("_") ? 1 : 0;
insertSymbol.run(
modRow.id,
name,
"function",
"python",
filePath,
lineNum,
lineNum,
isExported,
);
symbols++;
}
// 类
const classRe = new RegExp(PY_CLASS_RE);
while ((match = classRe.exec(content)) !== null) {
const name = match[1];
const lineNum = content.slice(0, match.index).split("\n").length;
insertSymbol.run(
modRow.id,
name,
"class",
"python",
filePath,
lineNum,
lineNum,
1,
);
symbols++;
}
}
}
}
return { modules, symbols };
}