/** * ESLint Rule: Server Actions Must Return Result Type * * Ensures server actions return Result types instead of arbitrary objects * This enforces the standardized error handling pattern across all server actions */ module.exports = { meta: { type: 'problem', docs: { description: 'Ensure server actions return Result types from lib/contracts/Result.ts', category: 'Server Actions', recommended: true, }, messages: { mustReturnResult: 'Server actions must return Result type. Expected: Promise>', invalidReturnType: 'Return type must be Result or Promise>', missingResultImport: 'Server actions must import Result from @/lib/contracts/Result', }, schema: [], }, create(context) { const filename = context.getFilename(); const isServerActionFile = filename.includes('/app/') && (filename.endsWith('.ts') || filename.endsWith('.tsx')) && !filename.endsWith('.test.ts') && !filename.endsWith('.test.tsx'); if (!isServerActionFile) { return {}; } let hasUseServerDirective = false; let hasResultImport = false; const functionDeclarations = []; return { // Check for 'use server' directive ExpressionStatement(node) { if (node.expression.type === 'Literal' && node.expression.value === 'use server') { hasUseServerDirective = true; } }, // Check for Result import ImportDeclaration(node) { const importPath = node.source.value; if (importPath === '@/lib/contracts/Result' || importPath === '@/lib/contracts/Result.ts') { hasResultImport = true; } }, // Track function declarations and exports FunctionDeclaration(node) { if (node.async && node.id && node.id.name) { functionDeclarations.push({ name: node.id.name, node: node, returnType: node.returnType, body: node.body, }); } }, // Track arrow function exports ExportNamedDeclaration(node) { if (node.declaration && node.declaration.type === 'FunctionDeclaration') { if (node.declaration.async && node.declaration.id) { functionDeclarations.push({ name: node.declaration.id.name, node: node.declaration, returnType: node.declaration.returnType, body: node.declaration.body, }); } } else if (node.declaration && node.declaration.type === 'VariableDeclaration') { node.declaration.declarations.forEach(decl => { if (decl.init && decl.init.type === 'ArrowFunctionExpression' && decl.init.async) { functionDeclarations.push({ name: decl.id.name, node: decl.init, returnType: decl.init.returnType, body: decl.init.body, }); } }); } }, 'Program:exit'() { // Only check files with 'use server' directive if (!hasUseServerDirective) return; // Report if no Result import if (!hasResultImport) { context.report({ node: context.getSourceCode().ast, messageId: 'missingResultImport', }); } // Check each server action function functionDeclarations.forEach(func => { const returnType = func.returnType; // Check if return type annotation exists and is correct if (!returnType) { context.report({ node: func.node, messageId: 'mustReturnResult', }); return; } // Helper function to check if a type is Result function isResultType(typeNode) { if (!typeNode) return false; // Direct Result type if (typeNode.typeName && typeNode.typeName.name === 'Result') { return true; } // Promise> if (typeNode.typeName && typeNode.typeName.name === 'Promise') { const typeParams = typeNode.typeParameters || typeNode.typeArguments; if (typeParams && typeParams.params && typeParams.params[0]) { return isResultType(typeParams.params[0]); } } return false; } // Check if it's a Promise> or Result<...> if (returnType.typeAnnotation) { const typeAnnotation = returnType.typeAnnotation; if (isResultType(typeAnnotation)) { // Valid: Result<...> or Promise> return; } else if (typeAnnotation.type === 'TSUnionType') { // Check if union contains only Result types const hasNonResult = typeAnnotation.types.some(type => !isResultType(type)); if (hasNonResult) { context.report({ node: returnType, messageId: 'invalidReturnType', }); } } else { context.report({ node: returnType, messageId: 'invalidReturnType', }); } } else { context.report({ node: returnType, messageId: 'invalidReturnType', }); } }); }, }; }, };