eslint-plugin-ember
Version:
ESLint plugin for Ember.js apps
150 lines (137 loc) • 4.89 kB
JavaScript
'use strict';
//------------------------------------------------------------------------------
// General rule - Don’t use runloop functions
//------------------------------------------------------------------------------
/**
* Map of runloop functions to ember-lifeline recommended replacements
*/
const RUNLOOP_TO_LIFELINE_MAP = Object.freeze({
later: 'runTask',
next: 'runTask',
debounce: 'debounceTask',
schedule: 'scheduleTask',
throttle: 'throttleTask',
});
const ERROR_MESSAGE =
"Don't use @ember/runloop functions. Use ember-lifeline, ember-concurrency, or @ember/destroyable instead.";
// https://api.emberjs.com/ember/3.24/classes/@ember%2Frunloop
const EMBER_RUNLOOP_FUNCTIONS = [
'begin',
'bind',
'cancel',
'debounce',
'end',
'join',
'later',
'next',
'once',
'run',
'schedule',
'scheduleOnce',
'throttle',
];
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow usage of `@ember/runloop` functions',
category: 'Miscellaneous',
recommended: true,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-runloop.md',
},
fixable: null,
schema: [
{
type: 'object',
properties: {
allowList: {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
enum: EMBER_RUNLOOP_FUNCTIONS,
minItems: 1,
},
description:
'If you have `@ember/runloop` functions that you wish to allow, you can configure this rule to allow specific methods. The configuration takes an object with the `allowList` property, which is an array of strings where the strings must be names of runloop functions.',
},
},
additionalProperties: false,
},
],
messages: {
main: ERROR_MESSAGE,
lifelineReplacement: `${ERROR_MESSAGE} For this case, you can replace \`{{actualMethodUsed}}\` with \`{{lifelineEquivalent}}\` from ember-lifeline.`,
},
},
create(context) {
// List of allowed runloop functions
const allowList = context.options[0]?.allowList ?? [];
// Maps local names to imported names of imports
const localToImportedNameMap = new Map();
/**
* Reports a node with usage of a disallowed runloop function
* @param {Node} node
* @param {String} [runloopFn] the name of the runloop function that is not allowed
* @param {String} [localName] the locally used name of the runloop function
*/
const report = function (node, runloopFn, localName) {
if (Object.keys(RUNLOOP_TO_LIFELINE_MAP).includes(runloopFn)) {
// If there is a recommended lifeline replacement, include the suggestion
context.report({
node,
messageId: 'lifelineReplacement',
data: {
actualMethodUsed: localName,
lifelineEquivalent: RUNLOOP_TO_LIFELINE_MAP[runloopFn],
},
});
} else {
// Otherwise, show a generic error message
context.report({ node, messageId: 'main' });
}
};
return {
ImportDeclaration(node) {
if (node.source.value === '@ember/runloop') {
for (const spec of node.specifiers) {
if (spec.type === 'ImportSpecifier') {
const importedName = spec.imported.name;
if (EMBER_RUNLOOP_FUNCTIONS.includes(importedName)) {
localToImportedNameMap.set(spec.local.name, importedName);
}
}
}
}
},
CallExpression(node) {
// Examples: run(...), later(...)
if (node.callee.type === 'Identifier') {
const name = node.callee.name;
const runloopFn = localToImportedNameMap.get(name);
const isNotAllowed = runloopFn && !allowList.includes(runloopFn);
if (isNotAllowed) {
report(node, runloopFn, name);
}
}
// runloop functions (aside from run itself) can chain onto `run`, so we need to check for this
// Examples: run.later(...), run.schedule(...)
if (node.callee.type === 'MemberExpression' && node.callee.object?.type === 'Identifier') {
const objectName = node.callee.object.name;
const objectRunloopFn = localToImportedNameMap.get(objectName);
if (objectRunloopFn === 'run' && node.callee.property?.type === 'Identifier') {
const runloopFn = node.callee.property.name;
if (
EMBER_RUNLOOP_FUNCTIONS.includes(runloopFn) &&
runloopFn !== 'run' &&
!allowList.includes(runloopFn)
) {
report(node, runloopFn, `${objectName}.${runloopFn}`);
}
}
}
},
};
},
};