export default { meta: { type: 'problem', docs: { description: 'Ensure required props are provided to components', category: 'Possible Errors', recommended: true, }, schema: [], }, create(context) { const sourceCode = context.getSourceCode(); // Collect local identifiers bound to our UI Button component // We scope the rule primarily to Buttons imported from .../ui/Button const buttonLocalNames = new Set(); // Updated regex to catch all Button import patterns including aliases const buttonPathRegex = /(@\/components\/ui\/[Bb]utton|[\\/](ui[\\/])?[Bb]utton)(\.tsx?)?$/i; for (const node of sourceCode.ast.body || []) { if (node.type === 'ImportDeclaration') { const src = node.source && node.source.value; if ( typeof src === 'string' && ( buttonPathRegex.test(src) || src === 'server/src/components/ui/Button' || src === '../../components/ui/Button' || src === '../components/ui/Button' || src === '../ui/Button' || src === './ui/Button' || src === '@/components/ui/button' || src === '@/components/ui/Button' ) ) { for (const spec of node.specifiers || []) { if (spec.type === 'ImportSpecifier' && spec.imported && spec.imported.name === 'Button') { buttonLocalNames.add(spec.local.name); } else if (spec.type === 'ImportDefaultSpecifier') { buttonLocalNames.add(spec.local.name); } } } } } function isTargetButton(openingEl) { if (!openingEl || !openingEl.name) return false; if (openingEl.name.type === 'JSXIdentifier') { const name = openingEl.name.name; // If we detected specific imports, prefer those. Otherwise fall back to literal "Button" return buttonLocalNames.size > 0 ? buttonLocalNames.has(name) : name === 'Button'; } return false; } function hasIdAttribute(openingEl) { return (openingEl.attributes || []).some( (attr) => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'id' ); } function containsIdProperty(objExpr) { if (!objExpr || objExpr.type !== 'ObjectExpression') return false; return (objExpr.properties || []).some((p) => { if (p.type !== 'Property') return false; const key = p.key; return (key.type === 'Identifier' && key.name === 'id') || (key.type === 'Literal' && key.value === 'id'); }); } function hasIdViaSpread(openingEl) { return (openingEl.attributes || []).some((attr) => { if (attr.type !== 'JSXSpreadAttribute') return false; const arg = attr.argument; if (!arg) return false; // Accept {...withDataAutomationId(...)} as satisfying the requirement, // since that utility injects an 'id' (or derives one) consistently. if ( arg.type === 'CallExpression' && arg.callee && arg.callee.type === 'Identifier' && arg.callee.name === 'withDataAutomationId' ) { return true; } // Also accept direct object spreads that contain an 'id' key: {...{ id: '...' }} if (arg.type === 'ObjectExpression' && containsIdProperty(arg)) { return true; } // Accept spreads from variables ending with 'Props' (e.g., filterButtonProps) // These are typically from useAutomationIdAndRegister hook if ( arg.type === 'Identifier' && (arg.name.endsWith('Props') || arg.name.endsWith('ButtonProps')) ) { return true; } return false; }); } return { JSXOpeningElement(node) { if (!isTargetButton(node)) return; const ok = hasIdAttribute(node) || hasIdViaSpread(node); if (!ok) { context.report({ node, message: 'Button component requires an id prop', }); } }, }; }, };