eslint-plugin-san
Version:
Official ESLint plugin for San
175 lines (156 loc) • 6.08 kB
JavaScript
/**
* @fileoverview Check if there are no asynchronous actions inside computed properties.
* @author Armano
*/
'use strict';
/* eslint-disable */
const utils = require('../utils');
/**
* @typedef {import('../utils').SanObjectData} SanObjectData
* @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty
*/
const PROMISE_FUNCTIONS = ['then', 'catch', 'finally'];
const PROMISE_METHODS = ['all', 'race', 'reject', 'resolve'];
const TIMED_FUNCTIONS = ['setTimeout', 'setInterval', 'setImmediate', 'requestAnimationFrame'];
/**
* @param {CallExpression} node
*/
function isTimedFunction(node) {
const callee = utils.skipChainExpression(node.callee);
return (
((node.type === 'CallExpression' &&
callee.type === 'Identifier' &&
TIMED_FUNCTIONS.indexOf(callee.name) !== -1) ||
(node.type === 'CallExpression' &&
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === 'window' &&
callee.property.type === 'Identifier' &&
TIMED_FUNCTIONS.indexOf(callee.property.name) !== -1)) &&
node.arguments.length
);
}
/**
* @param {CallExpression} node
*/
function isPromise(node) {
const callee = utils.skipChainExpression(node.callee);
if (node.type === 'CallExpression' && callee.type === 'MemberExpression') {
const flag = (
// hello.PROMISE_FUNCTION()
(callee.property.type === 'Identifier' && PROMISE_FUNCTIONS.indexOf(callee.property.name) !== -1) || // Promise.PROMISE_METHOD()
(callee.object.type === 'Identifier' &&
callee.object.name === 'Promise' &&
callee.property.type === 'Identifier' &&
PROMISE_METHODS.indexOf(callee.property.name) !== -1)
);
return flag;
}
return false;
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow asynchronous actions in computed properties',
categories: ['essential'],
url: 'https://ecomfe.github.io/eslint-plugin-san/rules/no-async-in-computed-properties.html'
},
fixable: null,
schema: []
},
/** @param {RuleContext} context */
create(context) {
/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} upper
* @property {BlockStatement | Expression} body
*/
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
const computedPropertiesMap = new Map();
/** @type {ScopeStack | null} */
let scopeStack = null;
const expressionTypes = {
promise: 'asynchronous action',
await: 'await operator',
async: 'async function declaration',
new: 'Promise object',
timed: 'timed function'
};
/**
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
* @param {SanObjectData} data
*/
function onFunctionEnter(node, {node: sanNode}) {
if (node.async) {
verify(node, node.body, 'async', computedPropertiesMap.get(sanNode));
}
scopeStack = {
upper: scopeStack,
body: node.body
};
}
function onFunctionExit() {
scopeStack = scopeStack && scopeStack.upper;
}
/**
* @param {ESNode} node
* @param {BlockStatement | Expression} targetBody
* @param {keyof expressionTypes} type
* @param {ComponentComputedProperty[]} computedProperties
*/
function verify(node, targetBody, type, computedProperties = []) {
computedProperties.forEach(cp => {
if (
cp.value &&
node.loc.start.line >= cp.value.loc.start.line &&
node.loc.end.line <= cp.value.loc.end.line &&
targetBody === cp.value
) {
context.report({
node,
message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
data: {
expressionName: expressionTypes[type],
propertyName: cp.key || 'unknown'
}
});
}
});
}
return utils.defineSanVisitor(context, {
onSanObjectEnter(node) {
computedPropertiesMap.set(node, utils.getComputedProperties(node));
},
':function': onFunctionEnter,
':function:exit': onFunctionExit,
NewExpression(node, {node: sanNode}) {
if (!scopeStack) {
return;
}
if (node.callee.type === 'Identifier' && node.callee.name === 'Promise') {
verify(node, scopeStack.body, 'new', computedPropertiesMap.get(sanNode));
}
},
CallExpression(node, {node: sanNode}) {
if (!scopeStack) {
return;
}
if (isPromise(node)) {
verify(node, scopeStack.body, 'promise', computedPropertiesMap.get(sanNode));
} else if (isTimedFunction(node)) {
verify(node, scopeStack.body, 'timed', computedPropertiesMap.get(sanNode));
}
},
AwaitExpression(node, {node: sanNode}) {
if (!scopeStack) {
return;
}
verify(node, scopeStack.body, 'await', computedPropertiesMap.get(sanNode));
}
});
}
};