/** * ESLint rule to enforce Services follow clean architecture patterns * * Services must: * 1. Explicitly implement Service interface * 2. Return Result types for type-safe error handling * 3. Use DomainError types (not strings) * 4. Be classes named *Service * 5. Create their own dependencies (API Client, Logger, ErrorReporter) * 6. Have NO constructor parameters (self-contained) * * Note: Method names can vary (execute(), getSomething(), etc.) */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce Services follow clean architecture patterns', category: 'Services', recommended: true, }, fixable: null, schema: [], messages: { mustReturnResult: 'Service methods must return Promise>', mustUseDomainError: 'Error types must be DomainError objects, not strings', noConstructorParams: 'Services must be self-contained. Constructor cannot have parameters. Dependencies should be created inside the constructor.', mustImplementContract: 'Services must explicitly implement Service interface', }, }, create(context) { const filename = context.getFilename(); const isInServices = filename.includes('/lib/services/'); if (!isInServices) return {}; let hasResultReturningMethod = false; let usesStringErrors = false; let hasConstructorParams = false; let classNode = null; return { ClassDeclaration(node) { classNode = node; const className = node.id?.name; if (!className || !className.endsWith('Service')) { return; // Not a service class } // Check if class implements Service interface // The implements clause can be: // - implements Service // - implements Service // The AST structure is: // impl.expression: Identifier { name: 'Service' } // impl.typeArguments: TSTypeParameterInstantiation (for generic types) const implementsService = node.implements && node.implements.some(impl => { if (impl.expression.type === 'Identifier') { return impl.expression.name === 'Service'; } return false; }); if (!implementsService) { context.report({ node: node.id, messageId: 'mustImplementContract', }); } // Check all methods for Result return types node.body.body.forEach(member => { if (member.type === 'MethodDefinition' && member.key.type === 'Identifier') { // Check constructor parameters if (member.kind === 'constructor') { const params = member.value.params; if (params && params.length > 0) { hasConstructorParams = true; } } // Check return types const returnType = member.value?.returnType; if (returnType && returnType.typeAnnotation && returnType.typeAnnotation.typeName && returnType.typeAnnotation.typeName.name === 'Promise' && returnType.typeAnnotation.typeParameters && returnType.typeAnnotation.typeParameters.params.length > 0) { const resultType = returnType.typeAnnotation.typeParameters.params[0]; // Check for Result<...> if (resultType.type === 'TSTypeReference' && resultType.typeName && resultType.typeName.type === 'Identifier' && resultType.typeName.name === 'Result') { hasResultReturningMethod = true; } // Check for string error type if (resultType.type === 'TSTypeReference' && resultType.typeParameters && resultType.typeParameters.params.length > 1) { const errorType = resultType.typeParameters.params[1]; // Check if error is string literal or string type if (errorType.type === 'TSStringKeyword' || (errorType.type === 'TSLiteralType' && errorType.literal.type === 'StringLiteral')) { usesStringErrors = true; } } } } }); }, 'Program:exit'() { if (!isInServices || !classNode) return; const className = classNode.id?.name; if (!className || !className.endsWith('Service')) return; // Error if constructor has parameters if (hasConstructorParams) { context.report({ node: classNode, messageId: 'noConstructorParams', }); } // Error if no methods return Result if (!hasResultReturningMethod) { context.report({ node: classNode, messageId: 'mustReturnResult', }); } // Error if using string errors if (usesStringErrors) { context.report({ node: classNode, messageId: 'mustUseDomainError', }); } }, }; }, };