routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
import {findVariable, getFunctionHeadLocation} from '@eslint-community/eslint-utils';
|
||||
import {isFunction, isMemberExpression, isMethodCall} from './ast/index.js';
|
||||
import {isLogicalExpression} from './utils/index.js';
|
||||
|
||||
const ERROR_PROMISE = 'promise';
|
||||
const ERROR_IIFE = 'iife';
|
||||
const ERROR_IDENTIFIER = 'identifier';
|
||||
const SUGGESTION_ADD_AWAIT = 'add-await';
|
||||
const messages = {
|
||||
[ERROR_PROMISE]: 'Prefer top-level await over using a promise chain.',
|
||||
[ERROR_IIFE]: 'Prefer top-level await over an async IIFE.',
|
||||
[ERROR_IDENTIFIER]: 'Prefer top-level await over an async function `{{name}}` call.',
|
||||
[SUGGESTION_ADD_AWAIT]: 'Insert `await`.',
|
||||
};
|
||||
|
||||
const promisePrototypeMethods = ['then', 'catch', 'finally'];
|
||||
const isTopLevelCallExpression = node => {
|
||||
if (node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
||||
if (
|
||||
isFunction(ancestor)
|
||||
|| ancestor.type === 'ClassDeclaration'
|
||||
|| ancestor.type === 'ClassExpression'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const isPromiseMethodCalleeObject = node =>
|
||||
node.parent.type === 'MemberExpression'
|
||||
&& node.parent.object === node
|
||||
&& !node.parent.computed
|
||||
&& node.parent.property.type === 'Identifier'
|
||||
&& promisePrototypeMethods.includes(node.parent.property.name)
|
||||
&& node.parent.parent.type === 'CallExpression'
|
||||
&& node.parent.parent.callee === node.parent;
|
||||
const isAwaitExpressionArgument = node => {
|
||||
if (node.parent.type === 'ChainExpression') {
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return node.parent.type === 'AwaitExpression' && node.parent.argument === node;
|
||||
};
|
||||
|
||||
const isArrayElementWrapper = node => (
|
||||
(node.parent.type === 'ChainExpression' && node.parent.expression === node)
|
||||
|| (
|
||||
node.parent.type === 'ConditionalExpression'
|
||||
&& (
|
||||
node.parent.consequent === node
|
||||
|| node.parent.alternate === node
|
||||
)
|
||||
)
|
||||
|| (
|
||||
isLogicalExpression(node.parent)
|
||||
&& (
|
||||
node.parent.right === node
|
||||
|| (
|
||||
node.parent.left === node
|
||||
&& node.parent.operator !== '&&'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// `Promise.{all,allSettled,any,race}([foo()])`
|
||||
const isInPromiseMethods = node => {
|
||||
let expression = node;
|
||||
while (isArrayElementWrapper(expression)) {
|
||||
expression = expression.parent;
|
||||
}
|
||||
|
||||
if (
|
||||
expression.parent.type !== 'ArrayExpression'
|
||||
|| !expression.parent.elements.includes(expression)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const arrayExpression = expression.parent;
|
||||
return isMethodCall(arrayExpression.parent, {
|
||||
object: 'Promise',
|
||||
methods: ['all', 'allSettled', 'any', 'race'],
|
||||
argumentsLength: 1,
|
||||
})
|
||||
&& arrayExpression.parent.arguments[0] === arrayExpression;
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
function create(context) {
|
||||
if (context.filename.toLowerCase().endsWith('.cjs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.on('CallExpression', node => {
|
||||
if (
|
||||
!isTopLevelCallExpression(node)
|
||||
|| isPromiseMethodCalleeObject(node)
|
||||
|| isAwaitExpressionArgument(node)
|
||||
|| isInPromiseMethods(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Promises
|
||||
if (isMemberExpression(node.callee, {
|
||||
properties: promisePrototypeMethods,
|
||||
computed: false,
|
||||
})) {
|
||||
return {
|
||||
node: node.callee.property,
|
||||
messageId: ERROR_PROMISE,
|
||||
};
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
|
||||
// IIFE
|
||||
if (
|
||||
(node.callee.type === 'FunctionExpression' || node.callee.type === 'ArrowFunctionExpression')
|
||||
&& node.callee.async
|
||||
&& !node.callee.generator
|
||||
) {
|
||||
return {
|
||||
node,
|
||||
loc: getFunctionHeadLocation(node.callee, sourceCode),
|
||||
messageId: ERROR_IIFE,
|
||||
};
|
||||
}
|
||||
|
||||
// Identifier
|
||||
if (node.callee.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = findVariable(sourceCode.getScope(node), node.callee);
|
||||
if (!variable || variable.defs.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [definition] = variable.defs;
|
||||
const value = definition.type === 'Variable' && definition.kind === 'const'
|
||||
? definition.node.init
|
||||
: definition.node;
|
||||
if (
|
||||
!value
|
||||
|| !(isFunction(value) && !value.generator && value.async)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node,
|
||||
messageId: ERROR_IDENTIFIER,
|
||||
data: {name: node.callee.name},
|
||||
suggest: [
|
||||
{
|
||||
messageId: SUGGESTION_ADD_AWAIT,
|
||||
fix: fixer => fixer.insertTextBefore(node, 'await '),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Prefer top-level await over top-level promises and async function calls.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user