/** * ESLint rule to enforce UI element purity * * UI elements in ui/ must be: * - Stateless (no useState, useReducer) * - No side effects (no useEffect) * - Pure functions that only render based on props * * Rationale: * - ui/ is for reusable, pure presentation elements * - Stateful logic belongs in components/ or hooks/ * - This ensures maximum reusability and testability */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce UI elements are pure and stateless', category: 'Architecture', recommended: true, }, fixable: null, schema: [], messages: { noStateInUi: 'UI elements in ui/ must be stateless. Use components/ for stateful wrappers.', noHooksInUi: 'UI elements must not use hooks. Use components/ or hooks/ for stateful logic.', noSideEffects: 'UI elements must not have side effects. Use components/ for side effect logic.', }, }, create(context) { const filename = context.getFilename(); const isInUi = filename.includes('/ui/'); if (!isInUi) return {}; return { // Check for 'use client' directive ExpressionStatement(node) { if (node.expression.type === 'Literal' && node.expression.value === 'use client') { context.report({ node, messageId: 'noStateInUi', }); } }, // Check for state hooks CallExpression(node) { if (node.callee.type !== 'Identifier') return; const hookName = node.callee.name; // State management hooks if (['useState', 'useReducer', 'useRef'].includes(hookName)) { context.report({ node, messageId: 'noStateInUi', }); } // Effect hooks if (['useEffect', 'useLayoutEffect', 'useInsertionEffect'].includes(hookName)) { context.report({ node, messageId: 'noSideEffects', }); } // Context (can introduce state) if (hookName === 'useContext') { context.report({ node, messageId: 'noStateInUi', }); } }, // Check for class components with state ClassDeclaration(node) { if (node.superClass && node.superClass.type === 'Identifier' && node.superClass.name === 'Component') { context.report({ node, messageId: 'noStateInUi', }); } }, // Check for direct state assignment (rare but possible) AssignmentExpression(node) { if (node.left.type === 'MemberExpression' && node.left.property.type === 'Identifier' && (node.left.property.name === 'state' || node.left.property.name === 'setState')) { context.report({ node, messageId: 'noStateInUi', }); } }, }; }, };