feat(arch-scan): add @public JSDoc tag exemption mechanism for Server Actions

This commit is contained in:
SpecialX
2026-07-07 19:19:01 +08:00
parent 747344bfe3
commit 692e8ef580
5 changed files with 36 additions and 2 deletions

Binary file not shown.

View File

@@ -103,4 +103,17 @@ describe("queryViolations", () => {
); );
expect(missing).toBeDefined(); expect(missing).toBeDefined();
}); });
it("should exclude @public Server Actions from violations", () => {
const db = setupTestDb();
// 插入一个带 @public 标记的 Server Action豁免权限校验
db.prepare(
"INSERT INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, is_public, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
).run(1, "publicLoginAction", "function", 1, 1, 1, 1, 60, 100);
const violations = queryViolations(db);
const missing = violations.server_actions_without_permission.find(
(v) => v.name === "publicLoginAction"
);
expect(missing).toBeUndefined();
});
}); });

View File

@@ -141,6 +141,7 @@ export function queryViolations(db: Database.Database): Violations {
`SELECT s.name, f.path FROM symbols s `SELECT s.name, f.path FROM symbols s
JOIN files f ON s.file_id = f.id JOIN files f ON s.file_id = f.id
WHERE s.is_server_action = 1 WHERE s.is_server_action = 1
AND s.is_public = 0
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM calls c SELECT 1 FROM calls c
JOIN symbols cs ON c.callee_id = cs.id JOIN symbols cs ON c.callee_id = cs.id

View File

@@ -185,9 +185,24 @@ export function scanSymbols(db: Database.Database): void {
.prepare("SELECT id, path FROM files") .prepare("SELECT id, path FROM files")
.all() as { id: number; path: string }[]; .all() as { id: number; path: string }[];
const insertSymbol = db.prepare( const insertSymbol = db.prepare(
`INSERT OR IGNORE INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, signature, start_line, end_line) `INSERT OR IGNORE INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, is_public, signature, start_line, end_line)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
); );
const hasPublicTag = (node: Node): boolean => {
try {
// ts-morph JSDoc 仅在 JSDocableNode 上可用
const anyNode = node as unknown as {
getJsDocs?: () => Array<{ getTags: () => Array<{ getTagName: () => string }> }>;
};
if (typeof anyNode.getJsDocs !== "function") return false;
const docs = anyNode.getJsDocs();
return docs.some((doc) =>
doc.getTags().some((tag) => tag.getTagName() === "public")
);
} catch {
return false;
}
};
for (const file of files) { for (const file of files) {
let sourceFile; let sourceFile;
try { try {
@@ -223,6 +238,7 @@ export function scanSymbols(db: Database.Database): void {
const startLine = node.getStartLineNumber(); const startLine = node.getStartLineNumber();
const endLine = node.getEndLineNumber(); const endLine = node.getEndLineNumber();
const isServerAction = isServerFile && exported && async; const isServerAction = isServerFile && exported && async;
const isPublic = hasPublicTag(node) ? 1 : 0;
insertSymbol.run( insertSymbol.run(
file.id, file.id,
name, name,
@@ -230,6 +246,7 @@ export function scanSymbols(db: Database.Database): void {
exported ? 1 : 0, exported ? 1 : 0,
async ? 1 : 0, async ? 1 : 0,
isServerAction ? 1 : 0, isServerAction ? 1 : 0,
isPublic,
signature, signature,
startLine, startLine,
endLine endLine
@@ -250,6 +267,7 @@ export function scanSymbols(db: Database.Database): void {
const signature = extractSignature(decl); const signature = extractSignature(decl);
const startLine = decl.getStartLineNumber(); const startLine = decl.getStartLineNumber();
const endLine = decl.getEndLineNumber(); const endLine = decl.getEndLineNumber();
const isPublic = hasPublicTag(node) ? 1 : 0;
insertSymbol.run( insertSymbol.run(
file.id, file.id,
name, name,
@@ -257,6 +275,7 @@ export function scanSymbols(db: Database.Database): void {
exported ? 1 : 0, exported ? 1 : 0,
0, 0,
0, 0,
isPublic,
signature, signature,
startLine, startLine,
endLine endLine

View File

@@ -27,6 +27,7 @@ const DDL_STATEMENTS = [
is_exported INTEGER DEFAULT 0, is_exported INTEGER DEFAULT 0,
is_async INTEGER DEFAULT 0, is_async INTEGER DEFAULT 0,
is_server_action INTEGER DEFAULT 0, is_server_action INTEGER DEFAULT 0,
is_public INTEGER DEFAULT 0,
signature TEXT, signature TEXT,
start_line INTEGER, start_line INTEGER,
end_line INTEGER, end_line INTEGER,