@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
316 lines (290 loc) • 11.1 kB
JavaScript
'use strict';
const consoleMethods = new Set([
'log',
'warn',
'error',
'info',
'debug',
'table',
'trace',
'dir',
'dirxml',
'group',
'groupCollapsed',
'groupEnd',
'time',
'timeLog',
'timeEnd',
'clear',
'count',
'countReset',
'assert',
'profile',
'profileEnd',
'timeStamp',
]);
const functionMap = new Map();
module.exports = {
rules: {
'check-return-values': {
meta: {
type: 'problem',
docs: {
description:
'Enforce handling of return values from non-void functions. If the return value is intentionally not used, it should be explicitly ignored.',
category: 'Best Practices',
recommended: false,
},
fixable: null,
schema: [
{
type: 'object',
properties: {
requireExplicitIgnore: {
type: 'boolean',
default: true,
},
},
additionalProperties: false,
},
],
messages: {
returnValueNotHandled:
"Return value of function '{{functionName}}' should be handled or explicitly ignored.",
},
},
create: function (context) {
const sourceCode = context.getSourceCode();
const parserServices = context.parserServices;
const checker = parserServices?.program?.getTypeChecker();
function collectFunctionDeclarations(node) {
if (node.type === 'FunctionDeclaration' && node.id?.name) {
functionMap.set(node.id.name, node);
}
}
function hasReturnWithValue(node, seen = new WeakSet(), depth = 0) {
// Safety checks
if (!node || typeof node !== 'object' || depth > 100) {
return false;
}
// Prevent circular references
if (seen.has(node)) {
return false;
}
seen.add(node);
try {
// Check if current node is a return statement with a value
if (node.type === 'ReturnStatement' && node.argument) {
return true;
}
// Only check properties that are likely to contain AST nodes
const keys = Object.keys(node);
for (const key of keys) {
// Skip certain properties that might cause issues
if (
key === 'parent' ||
key === 'loc' ||
key === 'range' ||
key === 'comments'
) {
continue;
}
const value = node[key];
if (Array.isArray(value)) {
for (const child of value) {
if (
child &&
typeof child === 'object' &&
hasReturnWithValue(child, seen, depth + 1)
) {
return true;
}
}
} else if (value && typeof value === 'object') {
if (hasReturnWithValue(value, seen, depth + 1)) {
return true;
}
}
}
return false;
} finally {
// Clean up to avoid memory leaks
seen.delete(node);
}
}
function getFunctionName(callExprNode) {
const callee = callExprNode.callee;
if (callee.type === 'Identifier') {
return callee.name;
}
if (callee.type === 'MemberExpression') {
return sourceCode.getText(callee);
}
// Avoid overly complex names for IIFEs or deeply nested calls in the message
if (
callee.type === 'FunctionExpression' ||
callee.type === 'ArrowFunctionExpression'
) {
return callee.id ? callee.id.name : '(anonymous function)';
}
return sourceCode.getText(callee).slice(0, 50); // Cap length for very long textual representations
}
function isConsoleCall(node) {
const callee = node.callee;
return (
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === 'console' &&
callee.property.type === 'Identifier' &&
consoleMethods.has(callee.property.name)
);
}
return {
FunctionDeclaration: collectFunctionDeclarations,
CallExpression(node) {
const parent = node.parent;
// First check TypeScript return type if available
if (checker && parserServices) {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const signature = checker.getResolvedSignature(tsNode);
const returnType =
signature && checker.getReturnTypeOfSignature(signature);
if (returnType && checker.typeToString(returnType) === 'void') {
// Skip checks for explicitly void functions
return;
}
}
if (node.callee.type === 'Identifier') {
const fn = functionMap.get(node.callee.name);
if (fn && fn.body) {
// Skip return value checks for functions without return statements
const hasReturns = hasReturnWithValue(fn.body);
if (!hasReturns) {
// If no return statements, treat as void function and skip further checks
return;
}
// Only continue with return value checks if there are actual returns
}
}
// 1. Check if the return value is "used" in common ways
if (
(parent.type === 'VariableDeclarator' && parent.init === node) ||
(parent.type === 'AssignmentExpression' &&
parent.right === node &&
!(
parent.left.type === 'Identifier' && parent.left.name === '_'
)) ||
(parent.type === 'Property' && parent.value === node) ||
(parent.type === 'ReturnStatement' && parent.argument === node) ||
(parent.type === 'ArrowFunctionExpression' &&
parent.body === node) || // Implicit return
(parent.type === 'ConditionalExpression' &&
(parent.test === node ||
parent.consequent === node ||
parent.alternate === node)) ||
(parent.type === 'IfStatement' && parent.test === node) ||
(parent.type === 'SwitchStatement' &&
parent.discriminant === node) ||
(parent.type === 'CallExpression' &&
parent.arguments.includes(node)) ||
(parent.type === 'NewExpression' &&
parent.arguments.includes(node)) ||
(parent.type === 'ArrayExpression' &&
parent.elements.includes(node)) ||
parent.type === 'BinaryExpression' || // e.g., x + foo(), foo() > 0
parent.type === 'LogicalExpression' || // e.g., foo() && bar()
(parent.type === 'UnaryExpression' &&
parent.operator !== 'void') || // e.g., !foo(), await foo() but not void foo()
parent.type === 'UpdateExpression' || // e.g. ++foo()
parent.type === 'YieldExpression' || // e.g. yield foo()
parent.type === 'AwaitExpression' || // e.g. await foo()
parent.type === 'TaggedTemplateExpression' || // e.g. a` ${foo()} `
parent.type === 'SpreadElement' // e.g. [...foo()]
) {
return;
}
// 2. Check for explicit `void` operator
if (
parent.type === 'UnaryExpression' &&
parent.operator === 'void' &&
parent.argument === node
) {
return;
}
// 3. Check for explicit ignore: `_ = foo();` as a statement
if (
parent.type === 'AssignmentExpression' &&
parent.right === node &&
parent.left.type === 'Identifier' &&
parent.left.name === '_' &&
parent.parent.type === 'ExpressionStatement' &&
parent.parent.expression === parent
) {
return;
}
// 4. If we reach here, the CallExpression's value might be discarded.
// This typically happens if it's the main expression in an ExpressionStatement.
if (
parent.type === 'ExpressionStatement' &&
parent.expression === node
) {
// 4a. Exempt console calls
if (isConsoleCall(node)) {
return;
}
// 4c. Check for ignoring comments
let commentIgnored = false;
const statementNode = parent; // The ExpressionStatement
// Check for a comment on the line immediately preceding the statement
const commentsBefore =
sourceCode.getCommentsBefore(statementNode);
for (const comment of commentsBefore) {
if (
comment.value.trim().toLowerCase() ===
'return value intentionally ignored' &&
comment.loc.end.line === statementNode.loc.start.line - 1
) {
commentIgnored = true;
break;
}
}
if (!commentIgnored) {
// Check for a trailing comment on the same line as the CallExpression's statement
const commentsInStatement =
sourceCode.getCommentsInside(statementNode);
const trailingComments =
sourceCode.getCommentsAfter(statementNode);
const allPotentialTrailingComments = [
...commentsInStatement,
...trailingComments,
];
for (const comment of allPotentialTrailingComments) {
// Ensure comment is on the same line the statement ends or CallExpression ends
if (
comment.loc.start.line === statementNode.loc.end.line ||
comment.loc.start.line === node.loc.end.line
) {
if (
comment.value.trim().toLowerCase() ===
'return value intentionally ignored'
) {
commentIgnored = true;
break;
}
}
}
}
if (!commentIgnored) {
context.report({
node: node,
messageId: 'returnValueNotHandled',
data: { functionName: getFunctionName(node) },
});
}
}
},
};
},
},
},
};