@mindfiredigital/eslint-plugin-hub
Version:
eslint-plugin-hub is a powerful, flexible ESLint plugin that provides a curated set of rules to enhance code readability, maintainability, and prevent common errors. Whether you're working with vanilla JavaScript, TypeScript, React, or Angular, eslint-plu
273 lines (255 loc) • 9.99 kB
JavaScript
const knownExpressMethods = new Set([
'get',
'post',
'put',
'delete',
'patch',
'options',
'head',
'all',
'use',
]);
const responseSendMethods = new Set([
'send',
'json',
'end',
'sendStatus',
'redirect',
]);
const defaultStatusCodeByMethod = {
GET: [200],
POST: [201],
PUT: [200, 204],
PATCH: [200, 204],
DELETE: [200, 204],
OPTIONS: [200, 204],
HEAD: [200, 204],
};
module.exports = {
rules: {
'http-status-code': {
meta: {
type: 'problem',
docs: {
description:
'Ensure that Express.js route handlers use appropriate HTTP status codes based on the HTTP method.',
category: 'Best Practices',
recommended: true,
},
fixable: null,
schema: [
{
type: 'object',
properties: {
responseObjectName: {
type: 'string',
description:
'The name of the response object in route handlers.',
default: 'res',
},
validStatusCodesByMethod: {
type: 'object',
description:
'Custom mapping of HTTP methods to arrays of allowed status codes.',
patternProperties: {
'^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|ALL|USE)$': {
type: 'array',
items: { type: 'integer' },
minItems: 1,
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
],
messages: {
invalidStatusCode:
'Expected {{responseObjectName}}.status({{expectedCodes}}) for {{httpMethod}} request, but found {{responseObjectName}}.status({{actualCode}}).',
invalidSendStatusCode:
'Expected {{responseObjectName}}.sendStatus({{expectedCodes}}) for {{httpMethod}} request, but found {{responseObjectName}}.sendStatus({{actualCode}}).',
},
},
create: function (context) {
const options = context.options[0] || {};
const configuredResponseObjectName =
options.responseObjectName || 'res';
const validStatusCodesByMethod = {
...defaultStatusCodeByMethod,
...(options.validStatusCodesByMethod || {}),
};
const routeContextStack = [];
function getStatusCodeFromCall(callExpressionNode) {
if (
callExpressionNode.arguments.length > 0 &&
callExpressionNode.arguments[0].type === 'Literal' &&
typeof callExpressionNode.arguments[0].value === 'number'
) {
return callExpressionNode.arguments[0].value;
}
return null;
}
function getRouteContext() {
return routeContextStack.length > 0
? routeContextStack[routeContextStack.length - 1]
: null;
}
return {
CallExpression(node) {
// Part 1: Identify route definitions and push context to stack
if (node.callee.type === 'MemberExpression') {
const methodName = node.callee.property.name;
if (knownExpressMethods.has(methodName)) {
const handlerArg = node.arguments.find(
arg =>
arg &&
(arg.type === 'FunctionExpression' ||
arg.type === 'ArrowFunctionExpression')
);
if (handlerArg) {
let responseObjectNameInHandler =
configuredResponseObjectName;
if (
handlerArg.params &&
handlerArg.params.length > 1 &&
handlerArg.params[1].type === 'Identifier'
) {
responseObjectNameInHandler = handlerArg.params[1].name;
} else if (
handlerArg.params &&
handlerArg.params.length > 0 &&
handlerArg.params[0].type === 'Identifier' &&
handlerArg.params[0].name === configuredResponseObjectName
) {
responseObjectNameInHandler = handlerArg.params[0].name;
}
routeContextStack.push({
httpMethod: methodName.toUpperCase(),
responseObjectName: responseObjectNameInHandler,
handlerNode: handlerArg,
});
}
}
}
// Part 2: Check for status calls if we are inside a known route handler
const currentRouteContext = getRouteContext();
if (
currentRouteContext &&
node.callee.type === 'MemberExpression'
) {
let inHandlerScope = false;
let tempNode = node;
while (tempNode) {
if (tempNode === currentRouteContext.handlerNode) {
inHandlerScope = true;
break;
}
tempNode = tempNode.parent;
}
if (inHandlerScope) {
let actualCode = null;
let reportNode = node; // The CallExpression node being visited (e.g., .send(), .json(), .sendStatus())
let statusSettingNode = null; // The node that *sets* the status, e.g. .status(XXX) or .sendStatus(XXX)
let messageId = 'invalidStatusCode';
// Case 1: res.sendStatus(code)
if (
node.callee.object.type === 'Identifier' &&
node.callee.object.name ===
currentRouteContext.responseObjectName &&
node.callee.property.name === 'sendStatus'
) {
statusSettingNode = node; // The sendStatus call itself
messageId = 'invalidSendStatusCode';
}
// Case 2: Patterns ending in .send(), .json(), .end() that might be preceded by .status()
else if (
responseSendMethods.has(node.callee.property.name) &&
node.callee.property.name !== 'sendStatus' && // Handled above
node.callee.property.name !== 'redirect'
) {
// Redirects might have different status logic
let objectOfSend = node.callee.object;
// Check if objectOfSend is itself a CallExpression like someFormOf.status(code)
if (
objectOfSend.type === 'CallExpression' &&
objectOfSend.callee.type === 'MemberExpression' &&
objectOfSend.callee.property.name === 'status'
) {
// objectOfSend is the LAST .status(code) in the chain before .send()
// Now, we need to verify its base is the response object
let baseObject = objectOfSend.callee.object;
while (
baseObject.type === 'CallExpression' &&
baseObject.callee.type === 'MemberExpression' &&
baseObject.callee.property.name === 'status'
) {
baseObject = baseObject.callee.object;
}
if (
baseObject.type === 'Identifier' &&
baseObject.name === currentRouteContext.responseObjectName
) {
statusSettingNode = objectOfSend;
} else {
}
} else {
}
}
if (statusSettingNode) {
actualCode = getStatusCodeFromCall(statusSettingNode);
reportNode =
statusSettingNode.arguments.length > 0 &&
statusSettingNode.arguments[0].type === 'Literal'
? statusSettingNode.arguments[0]
: statusSettingNode;
}
if (
actualCode !== null &&
validStatusCodesByMethod[currentRouteContext.httpMethod]
) {
const expectedCodes =
validStatusCodesByMethod[currentRouteContext.httpMethod];
const isErrorCode = actualCode >= 400 && actualCode <= 599;
if (!isErrorCode && !expectedCodes.includes(actualCode)) {
context.report({
node: reportNode,
messageId: messageId,
data: {
responseObjectName:
currentRouteContext.responseObjectName,
httpMethod: currentRouteContext.httpMethod,
actualCode: String(actualCode),
expectedCodes: expectedCodes.join(' or '),
},
});
} else {
}
} else if (actualCode !== null) {
}
}
}
},
'FunctionExpression:exit'(node) {
const currentRouteContext = getRouteContext();
if (
currentRouteContext &&
currentRouteContext.handlerNode === node
) {
// const popped = routeContextStack.pop();
}
},
'ArrowFunctionExpression:exit'(node) {
const currentRouteContext = getRouteContext();
if (
currentRouteContext &&
currentRouteContext.handlerNode === node
) {
// const popped = routeContextStack.pop();
}
},
};
},
},
},
};