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();
});
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
JOIN files f ON s.file_id = f.id
WHERE s.is_server_action = 1
AND s.is_public = 0
AND NOT EXISTS (
SELECT 1 FROM calls c
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")
.all() as { id: number; path: string }[];
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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
`INSERT OR IGNORE INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, is_public, signature, start_line, end_line)
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) {
let sourceFile;
try {
@@ -223,6 +238,7 @@ export function scanSymbols(db: Database.Database): void {
const startLine = node.getStartLineNumber();
const endLine = node.getEndLineNumber();
const isServerAction = isServerFile && exported && async;
const isPublic = hasPublicTag(node) ? 1 : 0;
insertSymbol.run(
file.id,
name,
@@ -230,6 +246,7 @@ export function scanSymbols(db: Database.Database): void {
exported ? 1 : 0,
async ? 1 : 0,
isServerAction ? 1 : 0,
isPublic,
signature,
startLine,
endLine
@@ -250,6 +267,7 @@ export function scanSymbols(db: Database.Database): void {
const signature = extractSignature(decl);
const startLine = decl.getStartLineNumber();
const endLine = decl.getEndLineNumber();
const isPublic = hasPublicTag(node) ? 1 : 0;
insertSymbol.run(
file.id,
name,
@@ -257,6 +275,7 @@ export function scanSymbols(db: Database.Database): void {
exported ? 1 : 0,
0,
0,
isPublic,
signature,
startLine,
endLine

View File

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