165 lines
5.1 KiB
TypeScript
165 lines
5.1 KiB
TypeScript
import type Database from "better-sqlite3";
|
||
|
||
export interface SymbolRef {
|
||
caller_name: string;
|
||
caller_path: string;
|
||
caller_line: number;
|
||
is_server_action: boolean;
|
||
depth: number;
|
||
path_chain: string;
|
||
}
|
||
|
||
export function querySymbolRefs(
|
||
db: Database.Database,
|
||
symbolName: string,
|
||
maxDepth = 10
|
||
): SymbolRef[] {
|
||
const sql = `
|
||
WITH RECURSIVE upstream(caller_id, caller_name, caller_path, caller_line, is_sa, depth, path_chain) AS (
|
||
SELECT c.caller_id, s.name, f.path, c.call_line, s.is_server_action, 0, s.name
|
||
FROM calls c
|
||
JOIN symbols s ON c.caller_id = s.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE c.callee_id = (SELECT id FROM symbols WHERE name = ? LIMIT 1)
|
||
UNION
|
||
SELECT c.caller_id, s.name, f.path, c.call_line, s.is_server_action, u.depth + 1,
|
||
u.path_chain || ' → ' || s.name
|
||
FROM upstream u
|
||
JOIN calls c ON c.callee_id = u.caller_id
|
||
JOIN symbols s ON c.caller_id = s.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE u.depth < ?
|
||
)
|
||
SELECT caller_name, caller_path, caller_line, is_sa as is_server_action, depth, path_chain
|
||
FROM upstream ORDER BY depth, caller_path`;
|
||
return db.prepare(sql).all(symbolName, maxDepth) as SymbolRef[];
|
||
}
|
||
|
||
export interface SymbolCall {
|
||
callee_name: string;
|
||
callee_path: string;
|
||
callee_line: number;
|
||
depth: number;
|
||
path_chain: string;
|
||
}
|
||
|
||
export function querySymbolForwardCalls(
|
||
db: Database.Database,
|
||
symbolName: string,
|
||
maxDepth = 10
|
||
): SymbolCall[] {
|
||
const sql = `
|
||
WITH RECURSIVE downstream(callee_id, callee_name, callee_path, callee_line, depth, path_chain) AS (
|
||
SELECT c.callee_id, s.name, f.path, c.call_line, 0, ?
|
||
FROM calls c
|
||
JOIN symbols s ON c.callee_id = s.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE c.caller_id = (SELECT id FROM symbols WHERE name = ? LIMIT 1)
|
||
UNION
|
||
SELECT c.callee_id, s.name, f.path, c.call_line, d.depth + 1,
|
||
d.path_chain || ' → ' || s.name
|
||
FROM downstream d
|
||
JOIN calls c ON c.caller_id = d.callee_id
|
||
JOIN symbols s ON c.callee_id = s.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE d.depth < ?
|
||
)
|
||
SELECT callee_name, callee_path, callee_line, depth, path_chain
|
||
FROM downstream ORDER BY depth, callee_path`;
|
||
return db.prepare(sql).all(symbolName, symbolName, maxDepth) as SymbolCall[];
|
||
}
|
||
|
||
export interface ModuleDep {
|
||
target_module: string;
|
||
dep_type: string;
|
||
}
|
||
|
||
export function queryModuleDeps(
|
||
db: Database.Database,
|
||
moduleName: string,
|
||
_maxDepth = 10
|
||
): ModuleDep[] {
|
||
const sql = `
|
||
SELECT DISTINCT m2.name AS target_module, md.dep_type
|
||
FROM module_deps md
|
||
JOIN modules m ON md.source_module_id = m.id
|
||
JOIN modules m2 ON md.target_module_id = m2.id
|
||
WHERE m.name = ?`;
|
||
return db.prepare(sql).all(moduleName) as ModuleDep[];
|
||
}
|
||
|
||
export interface ModuleReverseDep {
|
||
source_module: string;
|
||
dep_type: string;
|
||
}
|
||
|
||
export function queryModuleReverseDeps(
|
||
db: Database.Database,
|
||
moduleName: string,
|
||
_maxDepth = 10
|
||
): ModuleReverseDep[] {
|
||
const sql = `
|
||
SELECT DISTINCT m.name AS source_module, md.dep_type
|
||
FROM module_deps md
|
||
JOIN modules m ON md.source_module_id = m.id
|
||
JOIN modules m2 ON md.target_module_id = m2.id
|
||
WHERE m2.name = ?`;
|
||
return db.prepare(sql).all(moduleName) as ModuleReverseDep[];
|
||
}
|
||
|
||
export interface TechUsage {
|
||
symbol_name: string;
|
||
file_path: string;
|
||
tag_name: string;
|
||
}
|
||
|
||
export function queryTechUsage(
|
||
db: Database.Database,
|
||
tagName: string
|
||
): TechUsage[] {
|
||
const sql = `
|
||
SELECT s.name AS symbol_name, f.path AS file_path, t.name AS tag_name
|
||
FROM symbols s
|
||
JOIN symbol_tech_tags stt ON s.id = stt.symbol_id
|
||
JOIN tech_tags t ON stt.tag_id = t.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE t.name = ?`;
|
||
return db.prepare(sql).all(tagName) as TechUsage[];
|
||
}
|
||
|
||
export interface Violations {
|
||
long_files: { path: string; lines: number }[];
|
||
server_actions_without_permission: { name: string; path: string }[];
|
||
}
|
||
|
||
export function queryViolations(db: Database.Database): Violations {
|
||
const longFiles = db
|
||
.prepare("SELECT path, lines FROM files WHERE lines > 800 ORDER BY lines DESC")
|
||
.all() as { path: string; lines: number }[];
|
||
// 使用递归 CTE 识别直接或间接调用 requirePermission 的符号
|
||
// 这样可以正确识别通过辅助函数(如 requireAiPermission)间接校验权限的 Action
|
||
const serverActionsWithoutPerm = db
|
||
.prepare(
|
||
`WITH RECURSIVE permission_callers(symbol_id) AS (
|
||
SELECT c.caller_id
|
||
FROM calls c
|
||
JOIN symbols cs ON c.callee_id = cs.id
|
||
WHERE cs.name = 'requirePermission'
|
||
UNION
|
||
SELECT c.caller_id
|
||
FROM calls c
|
||
JOIN permission_callers pc ON c.callee_id = pc.symbol_id
|
||
)
|
||
SELECT s.name, f.path FROM symbols s
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE s.is_server_action = 1
|
||
AND s.is_public = 0
|
||
AND s.id NOT IN (SELECT symbol_id FROM permission_callers)`
|
||
)
|
||
.all() as { name: string; path: string }[];
|
||
return {
|
||
long_files: longFiles,
|
||
server_actions_without_permission: serverActionsWithoutPerm,
|
||
};
|
||
}
|