Files
gridpilot.gg/apps/website/eslint-rules/ui-element-purity.js
2026-01-13 12:10:15 +01:00

104 lines
2.8 KiB
JavaScript

/**
* 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 {};
let hasStateHooks = false;
let hasEffectHooks = false;
let hasContext = false;
return {
// 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)) {
hasStateHooks = true;
context.report({
node,
messageId: 'noStateInUi',
});
}
// Effect hooks
if (['useEffect', 'useLayoutEffect', 'useInsertionEffect'].includes(hookName)) {
hasEffectHooks = true;
context.report({
node,
messageId: 'noSideEffects',
});
}
// Context (can introduce state)
if (hookName === 'useContext') {
hasContext = true;
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',
});
}
},
};
},
};