@addon24/eslint-config
Version:
ESLint configuration rules for WorldOfTextcraft projects - Centralized configuration for all project types
341 lines (307 loc) • 12.1 kB
JavaScript
/**
* 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;