/** * ESLint rule to enforce View Model Builder contract implementation * * View Model Builders in lib/builders/view-models/ must: * 1. Be classes named *ViewModelBuilder * 2. Implement the ViewModelBuilder interface * 3. Have a static build() method */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce View Model Builder contract implementation', category: 'Builders', recommended: true, }, fixable: null, schema: [], messages: { notAClass: 'View Model Builders must be classes named *ViewModelBuilder', missingImplements: 'View Model Builders must implement ViewModelBuilder interface', missingBuildMethod: 'View Model Builders must have a static build() method', }, }, create(context) { const filename = context.getFilename(); const isInViewModelBuilders = filename.includes('/lib/builders/view-models/'); if (!isInViewModelBuilders) return {}; let hasImplements = false; let hasBuildMethod = false; return { // Check class declaration ClassDeclaration(node) { const className = node.id?.name; if (!className || !className.endsWith('ViewModelBuilder')) { context.report({ node, messageId: 'notAClass', }); } // Check if class implements ViewModelBuilder interface if (node.implements && node.implements.length > 0) { for (const impl of node.implements) { // Handle GenericTypeAnnotation for ViewModelBuilder if (impl.expression.type === 'TSInstantiationExpression') { const expr = impl.expression.expression; if (expr.type === 'Identifier' && expr.name === 'ViewModelBuilder') { hasImplements = true; } } else if (impl.expression.type === 'Identifier') { // Handle simple ViewModelBuilder (without generics) if (impl.expression.name === 'ViewModelBuilder') { hasImplements = true; } } } } // Check for static build method const buildMethod = node.body.body.find(member => member.type === 'MethodDefinition' && member.key.type === 'Identifier' && member.key.name === 'build' && member.static === true ); if (buildMethod) { hasBuildMethod = true; } }, 'Program:exit'() { if (!hasImplements) { context.report({ node: context.getSourceCode().ast, messageId: 'missingImplements', }); } if (!hasBuildMethod) { context.report({ node: context.getSourceCode().ast, messageId: 'missingBuildMethod', }); } }, }; }, };