UNPKG

@addon24/eslint-config

Version:

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

310 lines (260 loc) 10.7 kB
/** * @fileoverview Ensure all controller methods have complete Swagger documentation in English */ "use strict"; export default { meta: { type: "problem", docs: { description: "enforce complete English Swagger documentation for all controller methods", category: "Best Practices", recommended: true, }, fixable: null, schema: [], messages: { missingApiOperation: "Controller method '{{methodName}}' is missing @ApiOperation decorator", missingApiResponse: "Controller method '{{methodName}}' is missing @ApiResponse with status: {{status}}", emptyApiOperation: "Controller method '{{methodName}}' has empty @ApiOperation summary or description", germanDocumentation: "Swagger documentation must be in English. Found German text: '{{text}}'", missingSwaggerTag: "Controller class is missing @ApiTags decorator", missingApiBearerAuth: "Controller class is missing @ApiBearerAuth decorator", }, }, create(context) { // Common German words that shouldn't appear in API documentation const germanWords = [ // Eindeutig deutsche Artikel & Pronomen "der", "die", "das", "ein", "eine", "einen", "einem", "einer", "eines", "alle", "aller", "dieser", "diese", "dieses", "jeder", "jede", "jedes", // Eindeutig deutsche Verben "ist", "sind", "wird", "werden", "hat", "haben", "kann", "können", "soll", "sollen", "gibt", "geben", "ruft", "abrufen", "holt", "holen", "erstellt", "erstellen", "aktualisiert", "aktualisieren", "löscht", "löschen", "zurück", "zurückgeben", "erfolgreich", "fehlgeschlagen", "abgerufen", "gelöscht", "erstellt", // Eindeutig deutsche Nomen (keine englischen Homonyme!) "daten", "fehler", "serverfehler", "liste", "gesundheit", "koordinate", "koordinaten", "karten", "karte", "benutzer", "nutzer", "antwort", "anfrage", "informationen", "objekt", "objekte", "wert", "werte", "eigenschaft", "eigenschaften", // Deutsche Adjektive/Adverbien "neue", "neues", "neuer", "neuen", "bestimmten", "bestimmte", "bestimmter", "ungültige", "ungültiger", "ungültiges", "gültige", "gültiger", "gültiges", "vollständige", "vollständiger", "verfügbare", "verfügbarer", // Deutsche Präpositionen & Konjunktionen "mit", "von", "für", "beim", "nach", "oder", "und", "aber", "wenn", "dass", "durch", "über", "unter", "zwischen", "ohne", "während", "seit", // Deutsche Status-/Antwort-Wörter (eindeutig deutsch) "nicht", "gefunden", "vorhanden", "verfügbar", "möglich", "unmöglich", "korrekt", "inkorrekt", // Deutsche Fragewörter "wo", "wie", "was", "wann", "warum", "welche", "welcher", "welches", // Deutsche Zahlwörter "eins", "zwei", "drei", "vier", "fünf", "sechs", "sieben", "acht", "neun", "zehn" ]; function isControllerClass(node) { if (node.type !== "ClassDeclaration") return false; return node.decorators && node.decorators.some(decorator => { return decorator.expression && ((decorator.expression.type === "Identifier" && decorator.expression.name === "Controller") || (decorator.expression.type === "CallExpression" && decorator.expression.callee.name === "Controller")); }); } function containsGermanWords(text) { if (!text || typeof text !== "string") return null; const lowerText = text.toLowerCase(); for (const word of germanWords) { // Use word boundaries to avoid false positives const regex = new RegExp(`\\b${word}\\b`, "i"); if (regex.test(lowerText)) { return word; } } return null; } function checkStringForGerman(stringNode) { if (stringNode && stringNode.type === "Literal" && typeof stringNode.value === "string") { const germanWord = containsGermanWords(stringNode.value); if (germanWord) { context.report({ node: stringNode, messageId: "germanDocumentation", data: { text: stringNode.value.substring(0, 80) + (stringNode.value.length > 80 ? "..." : "") } }); } } } function hasDecorator(decorators, decoratorName) { if (!decorators) return false; return decorators.some(decorator => { const name = decorator.expression?.name || decorator.expression?.callee?.name; return name === decoratorName; }); } function hasApiResponseDecorator(decorators, statuses) { if (!decorators) return false; const statusArray = Array.isArray(statuses) ? statuses : [statuses]; return statusArray.some(status => { return decorators.some(decorator => { if (decorator.expression?.type === "CallExpression" && decorator.expression.callee.name === "ApiResponse") { const args = decorator.expression.arguments; if (args.length > 0 && args[0].type === "ObjectExpression") { const statusProp = args[0].properties.find(prop => prop.key && prop.key.name === "status" ); if (statusProp && statusProp.value) { return statusProp.value.value === status || statusProp.value.raw === status.toString(); } } } return false; }); }); } function getHttpMethodType(decorators) { if (!decorators) return null; const httpMethods = ["Get", "Post", "Put", "Delete", "Patch"]; for (const decorator of decorators) { const decoratorName = decorator.expression?.name || decorator.expression?.callee?.name; if (httpMethods.includes(decoratorName)) { return decoratorName.toLowerCase(); } } return null; } function validateClassDecorators(node) { if (!hasDecorator(node.decorators, "ApiTags")) { context.report({ node: node, messageId: "missingSwaggerTag" }); } if (!hasDecorator(node.decorators, "ApiBearerAuth")) { context.report({ node: node, messageId: "missingApiBearerAuth" }); } } function validateApiOperation(member) { const methodName = member.key.name; if (!hasDecorator(member.decorators, "ApiOperation")) { context.report({ node: member, messageId: "missingApiOperation", data: { methodName } }); return; } // Check ApiOperation content for German text member.decorators.forEach(decorator => { if (decorator.expression?.type === "CallExpression" && decorator.expression.callee.name === "ApiOperation") { const args = decorator.expression.arguments; if (args.length > 0 && args[0].type === "ObjectExpression") { let hasSummary = false; let hasDescription = false; args[0].properties.forEach(prop => { if (prop.key && prop.key.name === "summary") { hasSummary = true; checkStringForGerman(prop.value); } else if (prop.key && prop.key.name === "description") { hasDescription = true; checkStringForGerman(prop.value); } }); if (!hasSummary || !hasDescription) { context.report({ node: member, messageId: "emptyApiOperation", data: { methodName } }); } } } }); } function validateApiResponseDecorators(member) { const httpMethod = getHttpMethodType(member.decorators); if (!httpMethod) return; const methodName = member.key.name; // Check for required status codes let successCodes = [200]; if (httpMethod === "post") { successCodes = [200, 201]; } const hasSuccessResponse = hasApiResponseDecorator(member.decorators, successCodes); const has500Response = hasApiResponseDecorator(member.decorators, 500); if (!hasSuccessResponse) { const expectedCodes = successCodes.join(" or "); context.report({ node: member, messageId: "missingApiResponse", data: { methodName, status: expectedCodes } }); } if (!has500Response) { context.report({ node: member, messageId: "missingApiResponse", data: { methodName, status: "500" } }); } // Check ApiResponse content for German text if (member.decorators) { member.decorators.forEach(decorator => { if (decorator.expression?.type === "CallExpression" && decorator.expression.callee.name === "ApiResponse") { const args = decorator.expression.arguments; if (args.length > 0 && args[0].type === "ObjectExpression") { args[0].properties.forEach(prop => { if (prop.key && prop.key.name === "description") { checkStringForGerman(prop.value); } }); } } }); } } function validateApiParamDecorators(member) { if (!member.decorators) return; member.decorators.forEach(decorator => { if (decorator.expression?.type === "CallExpression" && (decorator.expression.callee.name === "ApiParam" || decorator.expression.callee.name === "ApiQuery" || decorator.expression.callee.name === "ApiBody")) { const args = decorator.expression.arguments; if (args.length > 0 && args[0].type === "ObjectExpression") { args[0].properties.forEach(prop => { if (prop.key && prop.key.name === "description") { checkStringForGerman(prop.value); } }); } } }); } return { ClassDeclaration(node) { if (!isControllerClass(node)) return; validateClassDecorators(node); node.body.body.forEach(member => { if (member.type === "MethodDefinition" && member.kind === "method" && member.accessibility === "public") { const httpMethod = getHttpMethodType(member.decorators); if (httpMethod) { validateApiOperation(member); validateApiResponseDecorators(member); validateApiParamDecorators(member); } } }); }, }; }, };