mirror of
https://github.com/elastic/kibana.git
synced 2025-06-29 03:24:45 -04:00
Updates files outside of x-pack to be triple-licensed under Elastic License 2.0, AGPL 3.0, or SSPL 1.0.
162 lines
5.4 KiB
JavaScript
162 lines
5.4 KiB
JavaScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the "Elastic License
|
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
|
* Public License v 1"; you may not use this file except in compliance with, at
|
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
|
*/
|
|
|
|
const { parseExpression } = require('@babel/parser');
|
|
const { default: generate } = require('@babel/generator');
|
|
const tsEstree = require('@typescript-eslint/typescript-estree');
|
|
const traverse = require('eslint-traverse');
|
|
const esTypes = tsEstree.AST_NODE_TYPES;
|
|
const babelTypes = require('@babel/types');
|
|
|
|
/** @typedef {import("eslint").Rule.RuleModule} Rule */
|
|
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.Expression} Expression */
|
|
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.ArrowFunctionExpression} ArrowFunctionExpression */
|
|
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.FunctionExpression} FunctionExpression */
|
|
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.NewExpression} NewExpression */
|
|
|
|
const ERROR_MSG =
|
|
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections';
|
|
|
|
/**
|
|
* @param {Expression} node
|
|
*/
|
|
const isPromise = (node) => node.type === esTypes.Identifier && node.name === 'Promise';
|
|
|
|
/**
|
|
* @param {Expression} node
|
|
* @returns {node is ArrowFunctionExpression | FunctionExpression}
|
|
*/
|
|
const isFunc = (node) =>
|
|
node.type === esTypes.ArrowFunctionExpression || node.type === esTypes.FunctionExpression;
|
|
|
|
/**
|
|
* @param {any} context
|
|
* @param {ArrowFunctionExpression | FunctionExpression} node
|
|
*/
|
|
const isFuncBodySafe = (context, node) => {
|
|
// if the body isn't wrapped in a blockStatement it can't have a try/catch at the root
|
|
if (node.body.type !== esTypes.BlockStatement) {
|
|
return false;
|
|
}
|
|
|
|
// when the entire body is wrapped in a try/catch it is the only node
|
|
if (node.body.body.length !== 1) {
|
|
return false;
|
|
}
|
|
|
|
const tryNode = node.body.body[0];
|
|
// ensure we have a try node with a handler
|
|
if (tryNode.type !== esTypes.TryStatement || !tryNode.handler) {
|
|
return false;
|
|
}
|
|
|
|
// ensure the handler doesn't throw
|
|
let hasThrow = false;
|
|
traverse(context, tryNode.handler, (path) => {
|
|
if (path.node.type === esTypes.ThrowStatement) {
|
|
hasThrow = true;
|
|
return traverse.STOP;
|
|
}
|
|
});
|
|
return !hasThrow;
|
|
};
|
|
|
|
/**
|
|
* @param {string} code
|
|
*/
|
|
const wrapFunctionInTryCatch = (code) => {
|
|
// parse the code with babel so we can mutate the AST
|
|
const ast = parseExpression(code, {
|
|
plugins: ['typescript', 'jsx'],
|
|
});
|
|
|
|
// validate that the code reperesents an arrow or function expression
|
|
if (!babelTypes.isArrowFunctionExpression(ast) && !babelTypes.isFunctionExpression(ast)) {
|
|
throw new Error('expected function to be an arrow or function expression');
|
|
}
|
|
|
|
// ensure that the function receives the second argument, and capture its name if already defined
|
|
let rejectName = 'reject';
|
|
if (ast.params.length === 0) {
|
|
ast.params.push(babelTypes.identifier('resolve'), babelTypes.identifier(rejectName));
|
|
} else if (ast.params.length === 1) {
|
|
ast.params.push(babelTypes.identifier(rejectName));
|
|
} else if (ast.params.length === 2) {
|
|
if (babelTypes.isIdentifier(ast.params[1])) {
|
|
rejectName = ast.params[1].name;
|
|
} else {
|
|
throw new Error('expected second param of promise definition function to be an identifier');
|
|
}
|
|
}
|
|
|
|
// ensure that the body of the function is a blockStatement
|
|
let block = ast.body;
|
|
if (!babelTypes.isBlockStatement(block)) {
|
|
block = babelTypes.blockStatement([babelTypes.returnStatement(block)]);
|
|
}
|
|
|
|
// redefine the body of the function as a new blockStatement containing a tryStatement
|
|
// which catches errors and forwards them to reject() when caught
|
|
ast.body = babelTypes.blockStatement([
|
|
// try {
|
|
babelTypes.tryStatement(
|
|
block,
|
|
// catch (error) {
|
|
babelTypes.catchClause(
|
|
babelTypes.identifier('error'),
|
|
babelTypes.blockStatement([
|
|
// reject(error)
|
|
babelTypes.expressionStatement(
|
|
babelTypes.callExpression(babelTypes.identifier(rejectName), [
|
|
babelTypes.identifier('error'),
|
|
])
|
|
),
|
|
])
|
|
)
|
|
),
|
|
]);
|
|
|
|
return generate(ast).code;
|
|
};
|
|
|
|
/** @type {Rule} */
|
|
module.exports = {
|
|
meta: {
|
|
fixable: 'code',
|
|
schema: [],
|
|
},
|
|
create: (context) => ({
|
|
NewExpression(_) {
|
|
const node = /** @type {NewExpression} */ (_);
|
|
|
|
// ensure we are newing up a promise with a single argument
|
|
if (!isPromise(node.callee) || node.arguments.length !== 1) {
|
|
return;
|
|
}
|
|
|
|
const func = node.arguments[0];
|
|
// ensure the argument is an arrow or function expression and is async
|
|
if (!isFunc(func) || !func.async) {
|
|
return;
|
|
}
|
|
|
|
// body must be a blockStatement, try/catch can't exist outside of a block
|
|
if (!isFuncBodySafe(context, func)) {
|
|
context.report({
|
|
message: ERROR_MSG,
|
|
loc: func.loc,
|
|
fix(fixer) {
|
|
const source = context.getSourceCode();
|
|
return fixer.replaceText(func, wrapFunctionInTryCatch(source.getText(func)));
|
|
},
|
|
});
|
|
}
|
|
},
|
|
}),
|
|
};
|