UNPKG

@addon24/eslint-config

Version:

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

341 lines (307 loc) 12.1 kB
/** * ESLint-Regeln für Controller-Architektur * 1. Controller dürfen keine Repositories direkt nutzen, nur Services * 2. Rückgabewerte müssen DTOs sein, nie Entities * 3. Response-Handling muss über sendSuccess/sendError erfolgen */ /** @type {import('eslint').Rule.RuleModule} */ const noDirectRepositoryUseRule = { meta: { type: "problem", docs: { description: "Controller dürfen keine Repositories direkt nutzen, nur Services", category: "Architecture", recommended: true, }, schema: [], messages: { noDirectRepository: "Controller dürfen keine Repositories direkt nutzen. Verwende stattdessen einen Service: '{{usage}}'", noDirectDataSource: "Controller dürfen DataSource nicht direkt nutzen. Verwende stattdessen einen Service: '{{usage}}'", }, }, create(context) { const controllerFiles = /Controller\.ts$/; const filename = context.getFilename(); const isControllerFile = controllerFiles.test(filename) && !/BaseController\.ts$/.test(filename); if (!isControllerFile) return {}; return { // Prüfe auf direkte Repository-Nutzung CallExpression(node) { // dataSource.getRepository() oder appDataSource.getRepository() if ( node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && node.callee.property.name === "getRepository" && node.callee.object.type === "Identifier" && (node.callee.object.name === "dataSource" || node.callee.object.name === "appDataSource" || node.callee.object.name.includes("DataSource")) ) { context.report({ node, messageId: "noDirectRepository", data: { usage: `${node.callee.object.name}.getRepository()`, }, }); } // Repository-Methoden direkt aufrufen if ( node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression" && node.callee.object.callee.type === "MemberExpression" && node.callee.object.callee.property.name === "getRepository" ) { context.report({ node, messageId: "noDirectRepository", data: { usage: "repository method call", }, }); } }, // Prüfe auf direkte DataSource-Imports ImportDeclaration(node) { if (node.source.value === "typeorm" && node.specifiers.some(spec => spec.type === "ImportSpecifier" && spec.imported.name === "DataSource")) { context.report({ node, messageId: "noDirectDataSource", data: { usage: "DataSource import", }, }); } // Prüfe auf appDataSource Import if (node.source.value && node.source.value.includes("ormconfig") || node.source.value.includes("data-source")) { context.report({ node, messageId: "noDirectDataSource", data: { usage: "appDataSource import", }, }); } }, }; }, }; /** @type {import('eslint').Rule.RuleModule} */ const requireDtoResponseRule = { meta: { type: "problem", docs: { description: "Controller müssen DTOs zurückgeben, nie Entities", category: "Architecture", recommended: true, }, schema: [], messages: { returnEntity: "Controller dürfen keine Entities zurückgeben. Verwende DTOs: '{{entityName}}'", serviceReturnsEntity: "Service-Methode '{{methodName}}' gibt wahrscheinlich Entity zurück. Controller müssen DTOs verwenden", }, }, create(context) { const controllerFiles = /Controller\.ts$/; const filename = context.getFilename(); const isControllerFile = controllerFiles.test(filename) && !/BaseController\.ts$/.test(filename); if (!isControllerFile) return {}; // Sammle Service-Imports und erkenne Entity-verdächtige Patterns const serviceImports = new Set(); const entityImports = new Set(); return { // Sammle Imports von Services und Entities ImportDeclaration(node) { if (node.source.value && typeof node.source.value === "string") { const importPath = node.source.value; // Erkenne Entity-Imports (endend mit "Entity" oder aus entity/ Ordnern) if (importPath.includes("/entity/") || node.specifiers.some(spec => spec.type === "ImportDefaultSpecifier" && spec.local.name.endsWith("Entity"))) { node.specifiers.forEach(spec => { if (spec.type === "ImportDefaultSpecifier") { entityImports.add(spec.local.name); } }); } // Erkenne Service-Imports if (importPath.includes("/service/") || node.specifiers.some(spec => spec.type === "ImportDefaultSpecifier" && spec.local.name.endsWith("Service"))) { node.specifiers.forEach(spec => { if (spec.type === "ImportDefaultSpecifier") { serviceImports.add(spec.local.name); } }); } } }, // Prüfe Return-Statements auf Entity-Rückgaben ReturnStatement(node) { if (node.argument && node.argument.type === "CallExpression" && node.argument.callee.type === "MemberExpression" && node.argument.callee.object.type === "Identifier" && node.argument.callee.object.name === "repo") { context.report({ node, messageId: "returnEntity", data: { entityName: "repository result", }, }); } }, // Prüfe Service-Aufrufe in handleGetAll/handleGetById CallExpression(node) { // Prüfe auf this.handleGetAll(..., () => this.serviceMethod(), ...) if (node.callee.type === "MemberExpression" && node.callee.object.type === "ThisExpression" && (node.callee.property.name === "handleGetAll" || node.callee.property.name === "handleGetById")) { // Finde das Service-Callback (zweites Argument bei handleGetAll) const callbackArg = node.arguments[1]; if (callbackArg && callbackArg.type === "ArrowFunctionExpression") { let serviceCall = callbackArg.body; // Handle async functions with BlockStatement body if (serviceCall.type === "BlockStatement" && serviceCall.body.length > 0) { const returnStmt = serviceCall.body.find(stmt => stmt.type === "ReturnStatement"); if (returnStmt && returnStmt.argument) { serviceCall = returnStmt.argument; } } // Check if it's a direct CallExpression if (serviceCall && serviceCall.type === "CallExpression") { if (serviceCall.callee.type === "MemberExpression") { let methodName = ""; // Handle this.serviceProperty.methodName() pattern if (serviceCall.callee.object.type === "MemberExpression" && serviceCall.callee.object.object.type === "ThisExpression" && serviceCall.callee.property.name) { methodName = serviceCall.callee.property.name; } // Handle this.methodName() pattern else if (serviceCall.callee.object.type === "ThisExpression" && serviceCall.callee.property.name) { methodName = serviceCall.callee.property.name; } if (methodName) { // Prüfe nur ob die Service-Klasse auf Entity endet const isEntitySuspicious = serviceCall.callee.object.type === "MemberExpression" && serviceCall.callee.object.object.type === "ThisExpression" && serviceCall.callee.object.property.name.endsWith("Entity"); if (isEntitySuspicious) { context.report({ node: serviceCall, messageId: "serviceReturnsEntity", data: { methodName: methodName, }, }); } } } } } } }, }; }, }; /** @type {import('eslint').Rule.RuleModule} */ const requireProperResponseHandlingRule = { meta: { type: "problem", docs: { description: "Controller müssen sendSuccess/sendError für Responses nutzen", category: "Architecture", recommended: true, }, schema: [], messages: { useBaseControllerMethods: "Verwende this.sendSuccess() oder this.sendError() anstatt direkter Response-Methoden: '{{method}}'", noDirectReturn: "Controller-Methoden dürfen nicht direkt Werte zurückgeben. Verwende this.sendSuccess(): '{{returnType}}'", }, }, create(context) { const controllerFiles = /Controller\.ts$/; const filename = context.getFilename(); const isControllerFile = controllerFiles.test(filename) && !/BaseController\.ts$/.test(filename); if (!isControllerFile) return {}; return { // Prüfe auf direkte res.json/res.status Aufrufe CallExpression(node) { // res.json() direkt if ( node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "res" && node.callee.property.type === "Identifier" && (node.callee.property.name === "json" || node.callee.property.name === "status" || node.callee.property.name === "send") ) { context.report({ node, messageId: "useBaseControllerMethods", data: { method: `res.${node.callee.property.name}()`, }, }); } }, // Prüfe Return-Statements in Controller-Methoden MethodDefinition(node) { // Nur öffentliche Methoden prüfen (nicht private oder protected) if (node.accessibility === "private" || node.accessibility === "protected") { return; } if (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression") { // Durchsuche den Function Body nach Return-Statements const checkReturnStatements = (blockStatement) => { if (!blockStatement || !blockStatement.body) return; for (const stmt of blockStatement.body) { if (stmt.type === "ReturnStatement" && stmt.argument && stmt.argument.type !== "CallExpression") { // Ignoriere "return;" ohne Wert if (stmt.argument.type === "Identifier" && stmt.argument.name === "undefined") continue; context.report({ node: stmt, messageId: "noDirectReturn", data: { returnType: "value", }, }); } } }; if (node.value.body.type === "BlockStatement") { checkReturnStatements(node.value.body); } } }, }; }, }; // Export-Objekt mit allen Regeln const controllerArchitectureRules = { rules: { "controller-architecture": noDirectRepositoryUseRule, "require-dto-response": requireDtoResponseRule, "require-proper-response-handling": requireProperResponseHandlingRule, }, }; export default controllerArchitectureRules;