Repository: ember-cli/eslint-plugin-ember
Pull request #1073 (2021-01-20 21:13:18)

Allow any Ember runloop function in `jquery-ember-run` rule
Any Ember runloop function should be allowed in the jQuery callback, not just `bind`.


Patch:
diff --git a/docs/rules/jquery-ember-run.md b/docs/rules/jquery-ember-run.md
index dc0d8d7e..d9e4ad11 100644
--- a/docs/rules/jquery-ember-run.md
+++ b/docs/rules/jquery-ember-run.md
@@ -4,7 +4,7 @@
 
 Don’t use jQuery without the Ember Run Loop.
 
-Using plain jQuery invokes actions out of the Ember Run Loop. In order to have a control on all operations in Ember, it's good practice to trigger actions in run loop.
+Using plain jQuery invokes actions outside of the Ember Run Loop. In order to have a control on all operations in Ember, it's good practice to trigger actions in a run loop (using one of the [@ember/runloop](https://api.emberjs.com/ember/3.24/classes/@ember%2Frunloop) functions).
 
 ## Examples
 
diff --git a/lib/rules/jquery-ember-run.js b/lib/rules/jquery-ember-run.js
index 81b5e7c3..c3e74f9c 100644
--- a/lib/rules/jquery-ember-run.js
+++ b/lib/rules/jquery-ember-run.js
@@ -12,6 +12,23 @@ const { globalMap, esmMap } = require('../utils/jquery');
 
 const ERROR_MESSAGE = "Don't use jQuery without Ember Run Loop";
 
+// 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',
+];
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -34,7 +51,7 @@ module.exports = {
     };
 
     let importedEmberName;
-    let importedBindName;
+    const importedRunloopFunctions = [];
 
     function checkJqueryCall(node) {
       if (
@@ -57,14 +74,14 @@ module.exports = {
             const isBindCall =
               types.isCallExpression(expression) &&
               expression.callee.type === 'Identifier' &&
-              expression.callee.name === importedBindName;
+              importedRunloopFunctions.includes(expression.callee.name);
 
             // Check for old-style: Ember.run.bind()
             const isEmberBindCall =
               types.isCallExpression(expression) &&
               expression.callee.type === 'MemberExpression' &&
               expression.callee.property.type === 'Identifier' &&
-              expression.callee.property.name === 'bind' &&
+              EMBER_RUNLOOP_FUNCTIONS.includes(expression.callee.property.name) &&
               expression.callee.object.type === 'MemberExpression' &&
               expression.callee.object.property.type === 'Identifier' &&
               expression.callee.object.property.name === 'run' &&
@@ -85,8 +102,11 @@ module.exports = {
           importedEmberName = importedEmberName || getImportIdentifier(node, 'ember');
         }
         if (node.source.value === '@ember/runloop') {
-          importedBindName =
-            importedBindName || getImportIdentifier(node, '@ember/runloop', 'bind');
+          importedRunloopFunctions.push(
+            ...EMBER_RUNLOOP_FUNCTIONS.map((fn) =>
+              getImportIdentifier(node, '@ember/runloop', fn)
+            ).filter((fn) => fn !== undefined)
+          );
         }
       },
 


Test patch:
diff --git a/tests/lib/rules/jquery-ember-run.js b/tests/lib/rules/jquery-ember-run.js
index d07d28ff..eaa047ea 100644
--- a/tests/lib/rules/jquery-ember-run.js
+++ b/tests/lib/rules/jquery-ember-run.js
@@ -19,6 +19,8 @@ eslintTester.run('jquery-ember-run', rule, {
     // jQuery/bind from Ember
     `import Ember from "ember";
      Ember.$("#item").on("click", () => { Ember.run.bind(this, this.handle); });`,
+    `import Ember from "ember";
+     Ember.$("#item").on("click", () => { Ember.run.debounce(this, this.handle); });`,
 
     // jQuery from Ember with destructuring
     `import Ember from "ember";
@@ -37,6 +39,9 @@ eslintTester.run('jquery-ember-run', rule, {
     `import { bind } from "@ember/runloop";
      import $ from "jquery";
      $("#item").on("click", () => { bind(this, this.handle); });`,
+    `import { debounce } from "@ember/runloop";
+     import $ from "jquery";
+     $("#item").on("click", () => { debounce(this, this.handle); });`,
 
     // No callback
     'import $ from "jquery"; $("#item");',
@@ -78,28 +83,35 @@ eslintTester.run('jquery-ember-run', rule, {
       errors: [{ message: ERROR_MESSAGE, type: 'MemberExpression' }],
     },
     {
-      // With function call that isn't `bind`
-      code: 'import $ from "jquery"; $("#item").on("click", () => { notBind(); });',
+      // With function call that isn't a runloop function
+      code: 'import $ from "jquery"; $("#item").on("click", () => { unknownFunction(); });',
+      output: null,
+      errors: [{ message: ERROR_MESSAGE, type: 'Identifier' }],
+    },
+    {
+      // With unknown imported runloop function
+      code:
+        'import { unknownFunction } from "@ember/runloop"; import $ from "jquery"; $("#item").on("click", () => { unknownFunction(); });',
       output: null,
       errors: [{ message: ERROR_MESSAGE, type: 'Identifier' }],
     },
     {
-      // With function call that isn't `Ember`
+      // With not from Ember
       code: 'import $ from "jquery"; $("#item").on("click", () => { notEmber.run.bind(); });',
       output: null,
       errors: [{ message: ERROR_MESSAGE, type: 'MemberExpression' }],
     },
     {
-      // With function call that isn't `run`
+      // With not from Ember.run
       code:
         'import Ember from "ember"; import $ from "jquery"; $("#item").on("click", () => { Ember.notRun.bind(); });',
       output: null,
       errors: [{ message: ERROR_MESSAGE, type: 'MemberExpression' }],
     },
     {
-      // With function call that isn't `bind`
+      // With unknown function call from Ember.run
       code:
-        'import Ember from "ember"; import $ from "jquery"; $("#item").on("click", () => { Ember.run.notBind(); });',
+        'import Ember from "ember"; import $ from "jquery"; $("#item").on("click", () => { Ember.run.unknownFunction(); });',
       output: null,
       errors: [{ message: ERROR_MESSAGE, type: 'MemberExpression' }],
     },