eslint-plugin-ember
Version:
ESLint plugin for Ember.js apps
167 lines (150 loc) • 5.32 kB
JavaScript
;
//------------------------------------------------------------------------------
// 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();
let namespaceImportName;
const isDisallowed = function (fn) {
return EMBER_RUNLOOP_FUNCTIONS.includes(fn) && !allowList.includes(fn);
};
/**
* 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);
}
} else if (spec.type === 'ImportNamespaceSpecifier') {
namespaceImportName = spec.local.name;
}
}
}
},
CallExpression(node) {
const callee = node.callee;
// Examples: run(...), later(...)
if (callee.type === 'Identifier') {
const localName = callee.name;
const runloopFn = localToImportedNameMap.get(localName);
if (runloopFn && isDisallowed(runloopFn)) {
report(node, runloopFn, localName);
}
return;
}
if (
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.property.type === 'Identifier'
) {
const objectName = callee.object.name;
const methodName = callee.property.name;
// runloop functions (aside from run itself) can chain onto `run`, so we need to check for this
// Examples: run.later(...), run.schedule(...)
if (
localToImportedNameMap.get(objectName) === 'run' &&
isDisallowed(methodName) &&
methodName !== 'run'
) {
report(node, methodName, `${objectName}.${methodName}`);
return;
}
// Example: `import * as runloop from '@ember/runloop'` -> `runloop.later(...)`
if (objectName === namespaceImportName && isDisallowed(methodName)) {
report(node, methodName, `${objectName}.${methodName}`);
}
}
},
};
},
};