/** * ESLint rule to enforce View Data Builder contract * * View Data Builders must: * 1. Be classes named *ViewDataBuilder * 2. Have a static build() method * 3. Use 'satisfies ViewDataBuilder<...>' for static enforcement * 4. Accept API DTO as parameter (named 'apiDto') * 5. Return View Data */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce View Data Builder contract', category: 'Builders', recommended: true, }, fixable: null, schema: [], messages: { notAClass: 'View Data Builders must be classes named *ViewDataBuilder', missingStaticBuild: 'View Data Builders must have a static build() method', missingSatisfies: 'View Data Builders must use "satisfies ViewDataBuilder<...>" for static type enforcement', invalidBuildSignature: 'build() method must accept API DTO and return View Data', wrongParameterName: 'Parameter must be named "apiDto", not "pageDto" or other names', }, }, create(context) { const filename = context.getFilename(); const isInViewDataBuilders = filename.includes('/lib/builders/view-data/'); if (!isInViewDataBuilders) return {}; let hasStaticBuild = false; let hasSatisfies = false; let hasCorrectSignature = false; let hasCorrectParameterName = false; return { // Check class declaration ClassDeclaration(node) { const className = node.id?.name; if (!className || !className.endsWith('ViewDataBuilder')) { 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 parameter name const firstParam = staticBuild.value.params[0]; if (firstParam.type === 'Identifier' && firstParam.name === 'apiDto') { hasCorrectParameterName = true; } else if (firstParam.type === 'Identifier' && (firstParam.name === 'pageDto' || firstParam.name === 'dto')) { // Report specific error for wrong names context.report({ node: firstParam, messageId: 'wrongParameterName', }); } } } }, // Check for satisfies expression TSSatisfiesExpression(node) { if (node.typeAnnotation && node.typeAnnotation.type === 'TSTypeReference' && node.typeAnnotation.typeName.name === 'ViewDataBuilder') { 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', }); } }, }; }, };