eslint-plugin-typesafe
Version:
ESLint plugin to encourage type-safe practices
229 lines (190 loc) • 7 kB
JavaScript
'use strict'
const utils = require("eslint-utils")
/**
* Recursively finds the function identifier given an AST node.
* The top-level call shoudl be a CallExpression.
* @param {ASTNode | undefined} node
* @returns {String | null}
*/
function getCalledFunctionIdentifierName (node) {
if (!node) return null
switch (node.type) {
case 'Identifier':
return node.name
case 'CallExpression':
return getCalledFunctionIdentifierName(node.callee)
case 'MemberExpression':
return getCalledFunctionIdentifierName(node.object)
case 'ThisExpression':
case 'Super':
return getCalledFunctionIdentifierName(node.parent.property) // try backtracking
default:
return null
}
}
function isIdentifier(node) {
return node && node.type === 'Identifier'
}
function isFunctionDeclaration(node) {
return node && node.type === 'FunctionDeclaration'
}
function isArrowFunctionExpression(node) {
return node && node.type === 'ArrowFunctionExpression'
}
function isVariableDeclarator(node) {
return node && node.type === 'VariableDeclarator'
}
function isNewExpression(node) {
return node && node.type === 'NewExpression'
}
function isCallExpression (node) {
return node && node.type === 'CallExpression'
}
function isMemberExpression (node) {
return node && node.type === 'MemberExpression'
}
/**
* Utility functions to detect async function nodes
* */
function isAsyncFunctionDeclaration (node) {
return isFunctionDeclaration(node) && node.async
}
function isAsyncArrowFunctionExpression (node) {
return isArrowFunctionExpression(node) && node.async
}
function isAsyncVariableDeclarator (node) {
return isVariableDeclarator(node) && isAsyncArrowFunctionExpression(node.init)
}
function isFunctionNodeAsync (node) {
return isAsyncFunctionDeclaration(node) || isAsyncVariableDeclarator(node)
}
function isReturnStatement (node) {
return node && node.type === 'ReturnStatement'
}
/**
* Utility functions to for synchronous function nodes
* */
/**
* Whether an Identifier is a Promise literal
* @param {Identifier} node
*/
function isIdentifierPromiseLiteral (node) {
return isIdentifier(node) && node.name === 'Promise'
}
/**
* Whether a NewExpression is constructing a Promise
* @example return new Promise(...)
* @param {NewExpression} node
*/
function isNewExpressionConstructingPromise (node) {
return isNewExpression(node) && isIdentifierPromiseLiteral(node.callee)
}
/**
* Checks if a MemberExpression evaluates to Promise.resolve() or Promise.reject()
* @param {isMemberExpression} node The ASTNode
*/
function isMemberExpressionReturningPromise (node) {
return isMemberExpression(node) &&
isIdentifierPromiseLiteral(node.object) &&
['resolve','reject'].includes(utils.getPropertyName(node))
}
/**
* Whether a ReturnStatement explicitly returns a promise
* @param {ReturnStatement} node The ASTNode
*/
function isReturnStatementPromise (node) {
return isReturnStatement(node) &&
( // return new Promise(...)
isNewExpressionConstructingPromise(node.argument) ||
// return Promise.resolve() or Promise.reject()
isCallExpression(node.argument) &&
isMemberExpressionReturningPromise(node.argument.callee)
)
}
/**
* Function to detect whether a synchronous function node returns a Promise.
* Currently this only detects cases where a return
* @param {ASTNode | undefined} node
* @returns {Boolean}
*/
function isSyncFunctionReturningPromise (node) {
if (!node) return false
switch (node.type) {
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
case 'FunctionExpression':
return isSyncFunctionReturningPromise(node.body)
case 'ReturnStatement': // base case
return isReturnStatementPromise(node)
case 'BlockStatement':
return node.body.some(x => isSyncFunctionReturningPromise(x))
case 'VariableDeclaration':
return node.declarations.some(x => isSyncFunctionReturningPromise(x))
case 'VariableDeclarator':
return isSyncFunctionReturningPromise(node.init)
case 'IfStatement':
return isSyncFunctionReturningPromise(node.consequent) || isSyncFunctionReturningPromise(node.alternate)
default:
return false // TODO: return Identifier
}
}
function isFunctionReturningPromise (node) {
return isFunctionNodeAsync(node) || isSyncFunctionReturningPromise(node)
}
function isCalleeNotUsingCatchExpression (node) {
return isIdentifier(node) || // e.g. f()
(isMemberExpression(node) && node.property.name !== 'catch')// e.g. f().then(...)
}
/**
* Finds the AST node of the function being called in a CallExpression
* @param {CallExpression} node ASTNode of type CallExpression
* @param {Scope} scope The scope of the node
*/
function findFunctionNodeFromCallExpression (node, scope) {
if (node && node.type !== 'CallExpression') return null
// Find out whether the called function is async
const functionName = getCalledFunctionIdentifierName(node)
// Could not get the function name
if (functionName === null) return null
const functionVariable = utils.findVariable(scope, functionName)
// Could not find the Variable
if (!functionVariable) return null
// Get the AST node of the function declaration that is being called
const functionDefs = functionVariable.defs
const lastFunctionDef = functionDefs[functionDefs.length - 1] // in case of repeated function definitions
return lastFunctionDef ? lastFunctionDef.node : null
}
/**
* Tests whether a call expression should be using a catch statement
* @param {CallExpression} node ASTNode of type CallExpression
* @param {Scope} scope The scope of the CallExpression
* @returns {Boolean | null}
*/
function isCallExpressionNotUsingCatch(node, scope) {
if (!node || node.type !== 'CallExpression' || !scope) return null
// Get the AST node of the function that is being called
const functionNode = findFunctionNodeFromCallExpression(node, scope)
if (!functionNode) return null
// Function being called does not return a Promise
if (!isFunctionReturningPromise(functionNode)) return false
// Async function called without using await
return isCalleeNotUsingCatchExpression(node.callee)
}
module.exports = {
meta: {
type: "suggestion",
},
create (context) {
return {
"ExpressionStatement > CallExpression": function(node) {
const scope = context.getScope()
if (isCallExpressionNotUsingCatch(node, scope)) {
context.report({
node,
message: `Missing catch statement.`
})
}
},
}
}
}