/** * ESLint rule to enforce View Model Builder contract * * View Model Builders must: * 1. Be classes named *ViewModelBuilder * 2. Have a static build() method * 3. Use 'satisfies ViewModelBuilder<...>' for static enforcement * 4. Accept View Data as parameter * 5. Return View Model */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce View Model Builder contract', category: 'Builders', recommended: true, }, fixable: null, schema: [], messages: { notAClass: 'View Model Builders must be classes named *ViewModelBuilder', missingStaticBuild: 'View Model Builders must have a static build() method', missingSatisfies: 'View Model Builders must use "satisfies ViewModelBuilder<...>" for static type enforcement', invalidBuildSignature: 'build() method must accept View Data and return View Model', }, }, create(context) { const filename = context.getFilename(); const isInViewModelBuilders = filename.includes('/lib/builders/view-models/'); if (!isInViewModelBuilders) return {}; let hasStaticBuild = false; let hasSatisfies = false; let hasCorrectSignature = false; return { // Check class declaration ClassDeclaration(node) { const className = node.id?.name; if (!className || !className.endsWith('ViewModelBuilder')) { context.report({ node, messageId: 'notAClass', }); } // Check for static build method const staticBuild = node.body.body.find(member => member.type === 'MethodDefinition' && member.key.type === 'Identifier' && member.key.name === 'build' && member.static === true ); if (staticBuild) { hasStaticBuild = true; // Check signature - should have at least one parameter if (staticBuild.value && staticBuild.value.params && staticBuild.value.params.length > 0) { hasCorrectSignature = true; } } }, // Check for satisfies expression TSSatisfiesExpression(node) { if (node.typeAnnotation && node.typeAnnotation.type === 'TSTypeReference' && node.typeAnnotation.typeName.name === 'ViewModelBuilder') { hasSatisfies = true; } }, 'Program:exit'() { if (!hasStaticBuild) { context.report({ node: context.getSourceCode().ast, messageId: 'missingStaticBuild', }); } if (!hasSatisfies) { context.report({ node: context.getSourceCode().ast, messageId: 'missingSatisfies', }); } if (hasStaticBuild && !hasCorrectSignature) { context.report({ node: context.getSourceCode().ast, messageId: 'invalidBuildSignature', }); } }, }; }, };