Files
gridpilot.gg/apps/website/eslint-rules/services-implement-contract.js
2026-01-13 02:38:49 +01:00

134 lines
4.3 KiB
JavaScript

/**
* ESLint rule to enforce Services follow clean architecture patterns
*
* Services must:
* 1. Return Result types for type-safe error handling
* 2. Use DomainError types (not strings)
* 3. Be classes named *Service
* 4. Create their own dependencies (API Client, Logger, ErrorReporter)
* 5. Have NO constructor parameters (self-contained)
*
* Note: Method names can vary (execute(), getSomething(), etc.)
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Services follow clean architecture patterns',
category: 'Services',
recommended: true,
},
fixable: null,
schema: [],
messages: {
mustReturnResult: 'Service methods must return Promise<Result<T, DomainError>>',
mustUseDomainError: 'Error types must be DomainError objects, not strings',
noConstructorParams: 'Services must be self-contained. Constructor cannot have parameters. Dependencies should be created inside the constructor.',
},
},
create(context) {
const filename = context.getFilename();
const isInServices = filename.includes('/lib/services/');
if (!isInServices) return {};
let hasResultReturningMethod = false;
let usesStringErrors = false;
let hasConstructorParams = false;
let classNode = null;
return {
ClassDeclaration(node) {
classNode = node;
const className = node.id?.name;
if (!className || !className.endsWith('Service')) {
return; // Not a service class
}
// Check all methods for Result return types
node.body.body.forEach(member => {
if (member.type === 'MethodDefinition' &&
member.key.type === 'Identifier') {
// Check constructor parameters
if (member.kind === 'constructor') {
const params = member.value.params;
if (params && params.length > 0) {
hasConstructorParams = true;
}
}
// Check return types
const returnType = member.value?.returnType;
if (returnType &&
returnType.typeAnnotation &&
returnType.typeAnnotation.typeName &&
returnType.typeAnnotation.typeName.name === 'Promise' &&
returnType.typeAnnotation.typeParameters &&
returnType.typeAnnotation.typeParameters.params.length > 0) {
const resultType = returnType.typeAnnotation.typeParameters.params[0];
// Check for Result<...>
if (resultType.type === 'TSTypeReference' &&
resultType.typeName &&
resultType.typeName.type === 'Identifier' &&
resultType.typeName.name === 'Result') {
hasResultReturningMethod = true;
}
// Check for string error type
if (resultType.type === 'TSTypeReference' &&
resultType.typeParameters &&
resultType.typeParameters.params.length > 1) {
const errorType = resultType.typeParameters.params[1];
// Check if error is string literal or string type
if (errorType.type === 'TSStringKeyword' ||
(errorType.type === 'TSLiteralType' && errorType.literal.type === 'StringLiteral')) {
usesStringErrors = true;
}
}
}
}
});
},
'Program:exit'() {
if (!isInServices || !classNode) return;
const className = classNode.id?.name;
if (!className || !className.endsWith('Service')) return;
// Error if constructor has parameters
if (hasConstructorParams) {
context.report({
node: classNode,
messageId: 'noConstructorParams',
});
}
// Error if no methods return Result
if (!hasResultReturningMethod) {
context.report({
node: classNode,
messageId: 'mustReturnResult',
});
}
// Error if using string errors
if (usesStringErrors) {
context.report({
node: classNode,
messageId: 'mustUseDomainError',
});
}
},
};
},
};