UNPKG

@addon24/eslint-config

Version:

ESLint configuration rules for WorldOfTextcraft projects - Centralized configuration for all project types

262 lines (226 loc) 8.71 kB
export default { rules: { "enforce-dto-create-pattern": { meta: { type: "problem", docs: { description: "Enforce strict DTO create method pattern: no Object.assign, explicit property assignment, all properties required.", category: "Best Practices", recommended: true, }, fixable: null, schema: [], messages: { noObjectAssignInCreate: "Object.assign is not allowed in create methods. Use explicit property assignment instead.", mustUseNewInstance: "Create methods must use 'const dto = new ClassName();' pattern.", missingPropertyAssignment: "Property '{{propertyName}}' is not assigned in create method. All properties must be explicitly assigned.", invalidCreateMethodStructure: "Create method must follow strict pattern: const dto = new ClassName(); followed by explicit property assignments.", }, }, create(context) { const EXEMPT_DTOS = [ "JwtOptionsDto", ]; function getClassName(context, node) { // Find the class declaration that contains this method let current = node; while (current && current.type !== "ClassDeclaration") { current = current.parent; } return current ? current.id.name : null; } function isExemptDto(className) { return EXEMPT_DTOS.includes(className); } function checkObjectAssignUsage(context, body) { for (const statement of body.body) { if (statement.type === "VariableDeclaration") { for (const declarator of statement.declarations) { if ( declarator.init && declarator.init.type === "CallExpression" && declarator.init.callee && declarator.init.callee.type === "MemberExpression" && declarator.init.callee.object && declarator.init.callee.object.name === "Object" && declarator.init.callee.property && declarator.init.callee.property.name === "assign" ) { context.report({ node: declarator, messageId: "noObjectAssignInCreate", }); } } } } } function checkNewInstancePattern(context, body, className) { let foundNewInstance = false; let foundConstDto = false; for (const statement of body.body) { if (statement.type === "VariableDeclaration") { for (const declarator of statement.declarations) { if ( declarator.id.name === "dto" && declarator.init && declarator.init.type === "NewExpression" && declarator.init.callee && declarator.init.callee.name === className ) { foundNewInstance = true; foundConstDto = true; break; } } } } if (!foundConstDto) { context.report({ node: body, messageId: "mustUseNewInstance", }); } if (!foundNewInstance) { context.report({ node: body, messageId: "invalidCreateMethodStructure", }); } } function checkPropertyAssignments(context, body, className) { // Get all class properties from the file const classProperties = getClassProperties(context, className); if (classProperties.length === 0) { return; } const assignedProperties = new Set(); // Find all property assignments in the create method for (const statement of body.body) { if (statement.type === "ExpressionStatement" && statement.expression.type === "AssignmentExpression") { const assignment = statement.expression; if ( assignment.left.type === "MemberExpression" && assignment.left.object && assignment.left.object.name === "dto" && assignment.left.property && assignment.left.property.type === "Identifier" ) { assignedProperties.add(assignment.left.property.name); } } } // Check for missing property assignments for (const property of classProperties) { if (!assignedProperties.has(property)) { context.report({ node: body, messageId: "missingPropertyAssignment", data: { propertyName: property }, }); } } } function getClassProperties(context, className) { const sourceCode = context.getSourceCode(); const classDeclaration = findClassDeclaration(sourceCode, className); if (!classDeclaration) { return []; } const properties = []; for (const member of classDeclaration.body.body) { if (member.type === "PropertyDefinition" && member.key && member.key.type === "Identifier") { // Skip static methods if (member.static) { continue; } properties.push(member.key.name); } } return properties; } function findClassDeclaration(sourceCode, className) { const ast = sourceCode.ast; const visited = new WeakSet(); function findClass(node) { if (!node || typeof node !== "object" || visited.has(node)) { return null; } visited.add(node); if (node.type === "ClassDeclaration" && node.id && node.id.name === className) { return node; } for (const key in node) { if (node[key] && typeof node[key] === "object") { if (Array.isArray(node[key])) { for (const child of node[key]) { const result = findClass(child); if (result) return result; } } else { const result = findClass(node[key]); if (result) return result; } } } return null; } return findClass(ast); } return { ClassMethod(node) { // Check if this is a static create method if ( node.static && node.key.name === "create" && node.body && node.body.type === "BlockStatement" ) { const className = getClassName(context, node); if (!className || !className.endsWith("Dto")) { return; } // Skip exempt DTOs if (isExemptDto(className)) { return; } const body = node.body; // Check for Object.assign usage checkObjectAssignUsage(context, body); // Check for proper new instance pattern checkNewInstancePattern(context, body, className); // Check for explicit property assignments checkPropertyAssignments(context, body, className); } }, MethodDefinition(node) { // Check if this is a static create method if ( node.static && node.key.name === "create" && node.value && node.value.body && node.value.body.type === "BlockStatement" ) { const className = getClassName(context, node); if (!className || !className.endsWith("Dto")) { return; } // Skip exempt DTOs if (isExemptDto(className)) { return; } const body = node.value.body; // Check for Object.assign usage checkObjectAssignUsage(context, body); // Check for proper new instance pattern checkNewInstancePattern(context, body, className); // Check for explicit property assignments checkPropertyAssignments(context, body, className); } }, }; }, }, }, };