211 lines
7.9 KiB
JavaScript
211 lines
7.9 KiB
JavaScript
/**
|
|
* ESLint Rule: Mutation Must Use Builders
|
|
*
|
|
* Ensures mutations use builders to transform DTOs into ViewData
|
|
* or return appropriate simple types (void, string, etc.)
|
|
*/
|
|
|
|
module.exports = {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Ensure mutations use builders or return appropriate types, not DTOs',
|
|
category: 'Mutation',
|
|
recommended: true,
|
|
},
|
|
messages: {
|
|
mustUseBuilder: 'Mutations must use ViewDataBuilder to transform DTOs or return simple types - see apps/website/lib/contracts/builders/ViewDataBuilder.ts',
|
|
noDirectDtoReturn: 'Mutations must not return DTOs directly, use builders to create ViewData or return simple types like void, string, or primitive values',
|
|
},
|
|
schema: [],
|
|
},
|
|
|
|
create(context) {
|
|
const filename = context.getFilename();
|
|
const isMutation = filename.includes('/lib/mutations/') && filename.endsWith('.ts');
|
|
|
|
if (!isMutation) {
|
|
return {};
|
|
}
|
|
|
|
let hasBuilderImport = false;
|
|
const returnStatements = [];
|
|
const methodDefinitions = [];
|
|
|
|
return {
|
|
// Check for builder imports
|
|
ImportDeclaration(node) {
|
|
const importPath = node.source.value;
|
|
if (importPath.includes('/builders/view-data/')) {
|
|
hasBuilderImport = true;
|
|
}
|
|
},
|
|
|
|
// Track method definitions
|
|
MethodDefinition(node) {
|
|
if (node.key.type === 'Identifier' && node.key.name === 'execute') {
|
|
methodDefinitions.push(node);
|
|
}
|
|
},
|
|
|
|
// Track return statements in execute method
|
|
ReturnStatement(node) {
|
|
// Only track returns inside execute method
|
|
let parent = node;
|
|
while (parent) {
|
|
if (parent.type === 'MethodDefinition' &&
|
|
parent.key.type === 'Identifier' &&
|
|
parent.key.name === 'execute') {
|
|
if (node.argument) {
|
|
returnStatements.push(node);
|
|
}
|
|
break;
|
|
}
|
|
parent = parent.parent;
|
|
}
|
|
},
|
|
|
|
'Program:exit'() {
|
|
if (methodDefinitions.length === 0) {
|
|
return; // No execute method found
|
|
}
|
|
|
|
// Check each return statement in execute method
|
|
returnStatements.forEach(returnNode => {
|
|
const returnExpr = returnNode.argument;
|
|
|
|
if (!returnExpr) return;
|
|
|
|
// Check if it's a Result type
|
|
if (returnExpr.type === 'CallExpression') {
|
|
const callee = returnExpr.callee;
|
|
|
|
// Check for Result.ok() or Result.err()
|
|
if (callee.type === 'MemberExpression' &&
|
|
callee.object.type === 'Identifier' &&
|
|
callee.object.name === 'Result' &&
|
|
callee.property.type === 'Identifier' &&
|
|
(callee.property.name === 'ok' || callee.property.name === 'err')) {
|
|
|
|
const resultArg = returnExpr.arguments[0];
|
|
|
|
if (callee.property.name === 'ok') {
|
|
// Check what's being returned in Result.ok()
|
|
|
|
// If it's a builder call, that's good
|
|
if (resultArg && resultArg.type === 'CallExpression') {
|
|
const builderCallee = resultArg.callee;
|
|
if (builderCallee.type === 'MemberExpression' &&
|
|
builderCallee.property.type === 'Identifier' &&
|
|
builderCallee.property.name === 'build') {
|
|
return; // Good: using builder
|
|
}
|
|
}
|
|
|
|
// If it's a method call that might be unwrap() or similar
|
|
if (resultArg && resultArg.type === 'CallExpression') {
|
|
const methodCallee = resultArg.callee;
|
|
if (methodCallee.type === 'MemberExpression' &&
|
|
methodCallee.property.type === 'Identifier') {
|
|
const methodName = methodCallee.property.name;
|
|
// Common DTO extraction methods
|
|
if (['unwrap', 'getValue', 'getData', 'getResult'].includes(methodName)) {
|
|
context.report({
|
|
node: returnNode,
|
|
messageId: 'noDirectDtoReturn',
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If it's a simple type (string, number, boolean, void), that's OK
|
|
if (resultArg && (
|
|
resultArg.type === 'Literal' ||
|
|
resultArg.type === 'Identifier' &&
|
|
['void', 'undefined', 'null'].includes(resultArg.name) ||
|
|
resultArg.type === 'UnaryExpression' // e.g., undefined
|
|
)) {
|
|
return; // Good: simple type
|
|
}
|
|
|
|
// If it's an identifier that might be a DTO
|
|
if (resultArg && resultArg.type === 'Identifier') {
|
|
const varName = resultArg.name;
|
|
// Check if it's likely a DTO (ends with DTO, or common patterns)
|
|
if (varName.match(/(DTO|Result|Response|Data)$/i) &&
|
|
!varName.includes('ViewData') &&
|
|
!varName.includes('ViewModel')) {
|
|
context.report({
|
|
node: returnNode,
|
|
messageId: 'noDirectDtoReturn',
|
|
});
|
|
}
|
|
}
|
|
|
|
// If it's an object literal with DTO-like properties
|
|
if (resultArg && resultArg.type === 'ObjectExpression') {
|
|
const hasDtoLikeProps = resultArg.properties.some(prop => {
|
|
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
const keyName = prop.key.name;
|
|
return keyName.match(/(id|Id|URL|Url|DTO)$/i);
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (hasDtoLikeProps) {
|
|
context.report({
|
|
node: returnNode,
|
|
messageId: 'noDirectDtoReturn',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// If no builder import and we have DTO returns, report
|
|
if (!hasBuilderImport && returnStatements.length > 0) {
|
|
const hasDtoReturn = returnStatements.some(returnNode => {
|
|
const returnExpr = returnNode.argument;
|
|
if (returnExpr && returnExpr.type === 'CallExpression') {
|
|
const callee = returnExpr.callee;
|
|
if (callee.type === 'MemberExpression' &&
|
|
callee.object.type === 'Identifier' &&
|
|
callee.object.name === 'Result' &&
|
|
callee.property.name === 'ok') {
|
|
const arg = returnExpr.arguments[0];
|
|
|
|
// Check for method calls like result.unwrap()
|
|
if (arg && arg.type === 'CallExpression') {
|
|
const methodCallee = arg.callee;
|
|
if (methodCallee.type === 'MemberExpression' &&
|
|
methodCallee.property.type === 'Identifier') {
|
|
const methodName = methodCallee.property.name;
|
|
if (['unwrap', 'getValue', 'getData', 'getResult'].includes(methodName)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for identifier
|
|
if (arg && arg.type === 'Identifier') {
|
|
return arg.name.match(/(DTO|Result|Response|Data)$/i);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (hasDtoReturn) {
|
|
context.report({
|
|
node: methodDefinitions[0],
|
|
messageId: 'mustUseBuilder',
|
|
});
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
}; |