@addon24/eslint-config
Version:
ESLint configuration rules for WorldOfTextcraft projects - Centralized configuration for all project types
120 lines (99 loc) • 4.07 kB
JavaScript
/**
* @fileoverview Ensure all controller methods have proper Swagger documentation including success and error status codes
*/
;
export default {
meta: {
type: "problem",
docs: {
description: "enforce that all controller methods have proper @ApiResponse documentation",
category: "Best Practices",
recommended: true,
},
fixable: null,
schema: [],
messages: {
missingApiResponse: "Controller method '{{methodName}}' is missing @ApiResponse with status: {{status}}. All controller methods should document potential server errors (BaseController.sendError defaults to 500)",
},
},
create(context) {
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 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 validateApiResponseDecorators(member) {
const httpMethod = getHttpMethodType(member.decorators);
if (!httpMethod) return;
const methodName = member.key.name;
// Check for success status codes based on HTTP method
let successCodes = [200];
if (httpMethod === "post") {
successCodes = [200, 201]; // POST can return 200 or 201
}
const hasSuccessResponse = hasApiResponseDecorator(member.decorators, successCodes);
const has500Response = hasApiResponseDecorator(member.decorators, 500);
if (!hasSuccessResponse) {
const expectedCodes = successCodes.join(" or ");
context.report({
node: member,
message: `Controller method '${methodName}' is missing @ApiResponse with status: ${expectedCodes}. All controller methods should document successful responses.`,
});
}
if (!has500Response) {
context.report({
node: member,
message: `Controller method '${methodName}' is missing @ApiResponse with status: 500. All controller methods should document potential server errors (BaseController.sendError defaults to 500).`,
});
}
}
return {
ClassDeclaration(node) {
if (!isControllerClass(node)) return;
node.body.body.forEach(member => {
if (member.type === "MethodDefinition" &&
member.kind === "method" &&
member.accessibility === "public") {
validateApiResponseDecorators(member);
}
});
},
};
},
};