eslint-plugin-jsdoc
Version:
JSDoc linting rules for ESLint.
573 lines (490 loc) • 17.1 kB
JavaScript
/**
* @typedef {import('estree').Node|
* import('@typescript-eslint/types').TSESTree.Node} ESTreeOrTypeScriptNode
*/
/**
* Checks if a node is a promise but has no resolve value or an empty value.
* An `undefined` resolve does not count.
* @param {ESTreeOrTypeScriptNode|undefined|null} node
* @returns {boolean|undefined|null}
*/
const isNewPromiseExpression = (node) => {
return node && node.type === 'NewExpression' && node.callee.type === 'Identifier' &&
node.callee.name === 'Promise';
};
/**
* @param {ESTreeOrTypeScriptNode|null|undefined} node
* @returns {boolean}
*/
const isVoidPromise = (node) => {
return /** @type {import('@typescript-eslint/types').TSESTree.TSTypeReference} */ (node)?.typeArguments?.params?.[0]?.type === 'TSVoidKeyword'
/* c8 ignore next 5 */
// eslint-disable-next-line @stylistic/operator-linebreak -- c8
|| /** @type {import('@typescript-eslint/types').TSESTree.TSTypeReference} */ (
node
// @ts-expect-error Ok
)?.typeParameters?.params?.[0]?.type === 'TSVoidKeyword';
};
const undefinedKeywords = new Set([
'TSNeverKeyword', 'TSUndefinedKeyword', 'TSVoidKeyword',
]);
/**
* Checks if a node has a return statement. Void return does not count.
* @param {ESTreeOrTypeScriptNode|undefined|null} node
* @param {boolean} [throwOnNullReturn]
* @param {PromiseFilter} [promFilter]
* @returns {boolean|undefined}
*/
// eslint-disable-next-line complexity
const hasReturnValue = (node, throwOnNullReturn, promFilter) => {
if (!node) {
return false;
}
switch (node.type) {
case 'ArrowFunctionExpression':
case 'FunctionDeclaration':
case 'FunctionExpression': {
return 'expression' in node && node.expression && (!isNewPromiseExpression(
node.body,
) || !isVoidPromise(node.body)) ||
hasReturnValue(node.body, throwOnNullReturn, promFilter);
}
case 'BlockStatement': {
return node.body.some((bodyNode) => {
return bodyNode.type !== 'FunctionDeclaration' && hasReturnValue(bodyNode, throwOnNullReturn, promFilter);
});
}
case 'DoWhileStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'ForStatement':
case 'LabeledStatement':
case 'WhileStatement':
case 'WithStatement': {
return hasReturnValue(node.body, throwOnNullReturn, promFilter);
}
case 'IfStatement': {
return hasReturnValue(node.consequent, throwOnNullReturn, promFilter) ||
hasReturnValue(node.alternate, throwOnNullReturn, promFilter);
}
case 'MethodDefinition':
return hasReturnValue(node.value, throwOnNullReturn, promFilter);
case 'ReturnStatement': {
// void return does not count.
if (node.argument === null) {
if (throwOnNullReturn) {
throw new Error('Null return');
}
return false;
}
if (promFilter && isNewPromiseExpression(node.argument)) {
// Let caller decide how to filter, but this is, at the least,
// a return of sorts and truthy
return promFilter(node.argument);
}
return true;
}
case 'SwitchStatement': {
return node.cases.some(
(someCase) => {
return someCase.consequent.some((nde) => {
return hasReturnValue(nde, throwOnNullReturn, promFilter);
});
},
);
}
case 'TryStatement': {
return hasReturnValue(node.block, throwOnNullReturn, promFilter) ||
hasReturnValue(node.handler && node.handler.body, throwOnNullReturn, promFilter) ||
hasReturnValue(node.finalizer, throwOnNullReturn, promFilter);
}
case 'TSDeclareFunction':
case 'TSFunctionType':
case 'TSMethodSignature': {
const type = node?.returnType?.typeAnnotation?.type;
return type && !undefinedKeywords.has(type);
}
default: {
return false;
}
}
};
/**
* Checks if a node has a return statement. Void return does not count.
* @param {ESTreeOrTypeScriptNode|null|undefined} node
* @param {PromiseFilter} promFilter
* @returns {undefined|boolean|ESTreeOrTypeScriptNode}
*/
// eslint-disable-next-line complexity
const allBrancheshaveReturnValues = (node, promFilter) => {
if (!node) {
return false;
}
switch (node.type) {
// case 'MethodDefinition':
// return allBrancheshaveReturnValues(node.value, promFilter);
case 'ArrowFunctionExpression':
case 'FunctionDeclaration':
case 'FunctionExpression': {
return 'expression' in node && node.expression && (!isNewPromiseExpression(node.body) || !isVoidPromise(node.body)) ||
allBrancheshaveReturnValues(node.body, promFilter) ||
/** @type {import('@typescript-eslint/types').TSESTree.BlockStatement} */
(node.body).body.some((nde) => {
return nde.type === 'ReturnStatement';
});
}
case 'BlockStatement': {
const lastBodyNode = node.body.slice(-1)[0];
return allBrancheshaveReturnValues(lastBodyNode, promFilter);
}
case 'DoWhileStatement':
case 'WhileStatement':
if (
/**
* @type {import('@typescript-eslint/types').TSESTree.Literal}
*/
(node.test).value === true
) {
// If this is an infinite loop, we assume only one branch
// is needed to provide a return
return hasReturnValue(node.body, false, promFilter);
}
// Fallthrough
case 'ForStatement':
if (node.test === null) {
// If this is an infinite loop, we assume only one branch
// is needed to provide a return
return hasReturnValue(node.body, false, promFilter);
}
case 'ForInStatement':
case 'ForOfStatement':
case 'LabeledStatement':
case 'WithStatement': {
return allBrancheshaveReturnValues(node.body, promFilter);
}
case 'IfStatement': {
return allBrancheshaveReturnValues(node.consequent, promFilter) &&
allBrancheshaveReturnValues(node.alternate, promFilter);
}
case 'ReturnStatement': {
// void return does not count.
if (node.argument === null) {
return false;
}
if (promFilter && isNewPromiseExpression(node.argument)) {
// Let caller decide how to filter, but this is, at the least,
// a return of sorts and truthy
return promFilter(node.argument);
}
return true;
}
case 'SwitchStatement': {
return /** @type {import('@typescript-eslint/types').TSESTree.SwitchStatement} */ (node).cases.every(
(someCase) => {
return !someCase.consequent.some((consNode) => {
return consNode.type === 'BreakStatement' ||
consNode.type === 'ReturnStatement' && consNode.argument === null;
});
},
);
}
case 'ThrowStatement': {
return true;
}
case 'TryStatement': {
// If `finally` returns, all return
return node.finalizer && allBrancheshaveReturnValues(node.finalizer, promFilter) ||
// Return in `try`/`catch` may still occur despite `finally`
allBrancheshaveReturnValues(node.block, promFilter) &&
(!node.handler ||
allBrancheshaveReturnValues(node.handler && node.handler.body, promFilter)) &&
(!node.finalizer || (() => {
try {
hasReturnValue(node.finalizer, true, promFilter);
} catch (error) {
if (/** @type {Error} */ (error).message === 'Null return') {
return false;
}
/* c8 ignore next 3 */
// eslint-disable-next-line @stylistic/padding-line-between-statements -- c8
throw error;
}
// As long as not an explicit empty return, then return true
return true;
})());
}
case 'TSDeclareFunction':
case 'TSFunctionType':
case 'TSMethodSignature': {
const type = node?.returnType?.typeAnnotation?.type;
return type && !undefinedKeywords.has(type);
}
default: {
return false;
}
}
};
/**
* @callback PromiseFilter
* @param {ESTreeOrTypeScriptNode|undefined} node
* @returns {boolean}
*/
/**
* Avoids further checking child nodes if a nested function shadows the
* resolver, but otherwise, if name is used (by call or passed in as an
* argument to another function), will be considered as non-empty.
*
* This could check for redeclaration of the resolver, but as such is
* unlikely, we avoid the performance cost of checking everywhere for
* (re)declarations or assignments.
* @param {import('@typescript-eslint/types').TSESTree.Node|null|undefined} node
* @param {string} resolverName
* @returns {boolean}
*/
// eslint-disable-next-line complexity
const hasNonEmptyResolverCall = (node, resolverName) => {
if (!node) {
return false;
}
// Arrow function without block
switch (node.type) {
case 'ArrayExpression':
case 'ArrayPattern':
return node.elements.some((element) => {
return hasNonEmptyResolverCall(element, resolverName);
});
case 'ArrowFunctionExpression':
case 'FunctionDeclaration':
case 'FunctionExpression': {
// Shadowing
if (/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
node.params[0]
)?.name === resolverName) {
return false;
}
return hasNonEmptyResolverCall(node.body, resolverName);
}
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression': {
return hasNonEmptyResolverCall(node.left, resolverName) ||
hasNonEmptyResolverCall(node.right, resolverName);
}
case 'AssignmentPattern':
return hasNonEmptyResolverCall(node.right, resolverName);
case 'AwaitExpression':
case 'SpreadElement':
case 'UnaryExpression':
case 'YieldExpression':
return hasNonEmptyResolverCall(node.argument, resolverName);
case 'BlockStatement':
case 'ClassBody':
return node.body.some((bodyNode) => {
return hasNonEmptyResolverCall(bodyNode, resolverName);
});
/* c8 ignore next 2 -- In Babel? */
case 'CallExpression':
// @ts-expect-error Babel?
case 'OptionalCallExpression':
return /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
node.callee
).name === resolverName && (
// Implicit or explicit undefined
node.arguments.length > 1 || node.arguments[0] !== undefined
) ||
node.arguments.some((nde) => {
// Being passed in to another function (which might invoke it)
return nde.type === 'Identifier' && nde.name === resolverName ||
// Handle nested items
hasNonEmptyResolverCall(nde, resolverName);
});
case 'ChainExpression':
case 'Decorator':
case 'ExpressionStatement':
return hasNonEmptyResolverCall(node.expression, resolverName);
case 'ClassDeclaration':
case 'ClassExpression':
return hasNonEmptyResolverCall(node.body, resolverName);
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'ClassMethod':
case 'MethodDefinition':
return node.decorators && node.decorators.some((decorator) => {
return hasNonEmptyResolverCall(decorator, resolverName);
}) ||
node.computed && hasNonEmptyResolverCall(node.key, resolverName) ||
hasNonEmptyResolverCall(node.value, resolverName);
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'ClassProperty':
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'ObjectProperty':
case 'Property':
case 'PropertyDefinition':
return node.computed && hasNonEmptyResolverCall(node.key, resolverName) ||
hasNonEmptyResolverCall(node.value, resolverName);
case 'ConditionalExpression':
case 'IfStatement': {
return hasNonEmptyResolverCall(node.test, resolverName) ||
hasNonEmptyResolverCall(node.consequent, resolverName) ||
hasNonEmptyResolverCall(node.alternate, resolverName);
}
case 'DoWhileStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'ForStatement':
case 'LabeledStatement':
case 'WhileStatement':
case 'WithStatement': {
return hasNonEmptyResolverCall(node.body, resolverName);
}
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'Import':
case 'ImportExpression':
return hasNonEmptyResolverCall(node.source, resolverName);
// ?.
/* c8 ignore next 2 -- In Babel? */
case 'MemberExpression':
// @ts-expect-error Babel?
case 'OptionalMemberExpression':
return hasNonEmptyResolverCall(node.object, resolverName) ||
hasNonEmptyResolverCall(node.property, resolverName);
case 'ObjectExpression':
case 'ObjectPattern':
return node.properties.some((property) => {
return hasNonEmptyResolverCall(property, resolverName);
});
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'ObjectMethod':
/* c8 ignore next 6 -- In Babel? */
// @ts-expect-error
return node.computed && hasNonEmptyResolverCall(node.key, resolverName) ||
// @ts-expect-error
node.arguments.some((nde) => {
return hasNonEmptyResolverCall(nde, resolverName);
});
case 'ReturnStatement': {
if (node.argument === null) {
return false;
}
return hasNonEmptyResolverCall(node.argument, resolverName);
}
// Comma
case 'SequenceExpression':
case 'TemplateLiteral':
return node.expressions.some((subExpression) => {
return hasNonEmptyResolverCall(subExpression, resolverName);
});
case 'SwitchStatement': {
return node.cases.some(
(someCase) => {
return someCase.consequent.some((nde) => {
return hasNonEmptyResolverCall(nde, resolverName);
});
},
);
}
case 'TaggedTemplateExpression':
return hasNonEmptyResolverCall(node.quasi, resolverName);
case 'TryStatement': {
return hasNonEmptyResolverCall(node.block, resolverName) ||
hasNonEmptyResolverCall(node.handler && node.handler.body, resolverName) ||
hasNonEmptyResolverCall(node.finalizer, resolverName);
}
case 'VariableDeclaration': {
return node.declarations.some((nde) => {
return hasNonEmptyResolverCall(nde, resolverName);
});
}
case 'VariableDeclarator': {
return hasNonEmptyResolverCall(node.id, resolverName) ||
hasNonEmptyResolverCall(node.init, resolverName);
}
/*
// Shouldn't need to parse literals/literal components, etc.
case 'Identifier':
case 'TemplateElement':
case 'Super':
// Exports not relevant in this context
*/
default:
return false;
}
};
/**
* Checks if a Promise executor has no resolve value or an empty value.
* An `undefined` resolve does not count.
* @param {ESTreeOrTypeScriptNode} node
* @param {boolean} anyPromiseAsReturn
* @param {boolean} [allBranches]
* @returns {boolean}
*/
const hasValueOrExecutorHasNonEmptyResolveValue = (node, anyPromiseAsReturn, allBranches) => {
const hasReturnMethod = allBranches ?
/**
* @param {ESTreeOrTypeScriptNode} nde
* @param {PromiseFilter} promiseFilter
* @returns {boolean}
*/
(nde, promiseFilter) => {
let hasReturn;
try {
hasReturn = hasReturnValue(nde, true, promiseFilter);
} catch (error) {
// c8 ignore else
if (/** @type {Error} */ (error).message === 'Null return') {
return false;
}
/* c8 ignore next 3 */
// eslint-disable-next-line @stylistic/padding-line-between-statements -- c8
throw error;
}
// `hasReturn` check needed since `throw` treated as valid return by
// `allBrancheshaveReturnValues`
return Boolean(hasReturn && allBrancheshaveReturnValues(nde, promiseFilter));
} :
/**
* @param {ESTreeOrTypeScriptNode} nde
* @param {PromiseFilter} promiseFilter
* @returns {boolean}
*/
(nde, promiseFilter) => {
return Boolean(hasReturnValue(nde, false, promiseFilter));
};
return hasReturnMethod(node, (prom) => {
if (anyPromiseAsReturn) {
return true;
}
if (isVoidPromise(prom)) {
return false;
}
const {
body,
params,
} =
/**
* @type {import('@typescript-eslint/types').TSESTree.FunctionExpression|
* import('@typescript-eslint/types').TSESTree.ArrowFunctionExpression}
*/ (
/** @type {import('@typescript-eslint/types').TSESTree.NewExpression} */ (
prom
).arguments[0]
) || {};
if (!params?.length) {
return false;
}
const {
name: resolverName,
} = /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
params[0]
);
return hasNonEmptyResolverCall(body, resolverName);
});
};
export {
hasReturnValue,
hasValueOrExecutorHasNonEmptyResolveValue,
};