/** * ESLint rule to enforce Presenter contract * * Enforces that classes ending with "Presenter" must: * 1. Implement Presenter interface * 2. Have a present(input) method * 3. Have 'use client' directive */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce Presenter contract implementation', category: 'Best Practices', recommended: true, }, fixable: null, schema: [], messages: { missingImplements: 'Presenter class must implement Presenter interface', missingPresentMethod: 'Presenter class must have present(input) method', missingUseClient: 'Presenter must have \'use client\' directive at top-level', }, }, create(context) { const sourceCode = context.getSourceCode(); let hasUseClient = false; let presenterClassNode = null; let hasPresentMethod = false; let hasImplements = false; return { // Check for 'use client' directive Program(node) { // Check comments at the top const comments = sourceCode.getAllComments(); if (comments.length > 0) { const firstComment = comments[0]; if (firstComment.type === 'Line' && firstComment.value.trim() === 'use client') { hasUseClient = true; } else if (firstComment.type === 'Block' && firstComment.value.includes('use client')) { hasUseClient = true; } } // Also check for 'use client' string literal as first statement if (node.body.length > 0) { const firstStmt = node.body[0]; if (firstStmt && firstStmt.type === 'ExpressionStatement' && firstStmt.expression.type === 'Literal' && firstStmt.expression.value === 'use client') { hasUseClient = true; } } }, // Find Presenter classes ClassDeclaration(node) { const className = node.id?.name; // Check if this is a Presenter class if (className && className.endsWith('Presenter')) { presenterClassNode = node; // Check if it implements any interface if (node.implements && node.implements.length > 0) { for (const impl of node.implements) { // Handle GenericTypeAnnotation for Presenter if (impl.expression.type === 'TSInstantiationExpression') { const expr = impl.expression.expression; if (expr.type === 'Identifier' && expr.name === 'Presenter') { hasImplements = true; } } else if (impl.expression.type === 'Identifier') { // Handle simple Presenter (without generics) if (impl.expression.name === 'Presenter') { hasImplements = true; } } } } } }, // Check for present method in classes MethodDefinition(node) { if (presenterClassNode && node.key.type === 'Identifier' && node.key.name === 'present' && node.parent === presenterClassNode) { hasPresentMethod = true; } }, // Report violations at the end 'Program:exit'() { if (!presenterClassNode) return; if (!hasImplements) { context.report({ node: presenterClassNode, messageId: 'missingImplements', }); } if (!hasPresentMethod) { context.report({ node: presenterClassNode, messageId: 'missingPresentMethod', }); } if (!hasUseClient) { context.report({ node: presenterClassNode, messageId: 'missingUseClient', }); } }, }; }, };