Repository: ember-cli/eslint-plugin-ember
Pull request #681 (2020-02-26 21:03:15)

Add new rule `no-legacy-test-waiters`
Implements #680 


Patch:
diff --git a/README.md b/README.md
index 9d547193..6b7c0e54 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,7 @@ Each rule has emojis denoting what configuration it belongs to and/or a :wrench:
 
 |    | Rule ID | Description |
 |:---|:--------|:------------|
+|  | [no-legacy-test-waiters](./docs/rules/no-legacy-test-waiters.md) | disallow the use of the legacy test waiter APIs. |
 |  | [no-pause-test](./docs/rules/no-pause-test.md) | disallow usage of the `pauseTest` helper in tests |
 |  | [no-test-and-then](./docs/rules/no-test-and-then.md) | disallow usage of the `andThen` test wait helper |
 |  | [no-test-import-export](./docs/rules/no-test-import-export.md) | disallow importing of "-test.js" in a test file and exporting from a test file |
diff --git a/lib/index.js b/lib/index.js
index dd348d6d..73398421 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -33,6 +33,7 @@ module.exports = {
     'no-incorrect-calls-with-inline-anonymous-functions': require('./rules/no-incorrect-calls-with-inline-anonymous-functions'),
     'no-invalid-debug-function-arguments': require('./rules/no-invalid-debug-function-arguments'),
     'no-jquery': require('./rules/no-jquery'),
+    'no-legacy-test-waiters': require('./rules/no-legacy-test-waiters'),
     'no-new-mixins': require('./rules/no-new-mixins'),
     'no-observers': require('./rules/no-observers'),
     'no-old-shims': require('./rules/no-old-shims'),
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index 1b717845..78d26e18 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -36,6 +36,7 @@ module.exports = {
   "ember/no-incorrect-calls-with-inline-anonymous-functions": "error",
   "ember/no-invalid-debug-function-arguments": "error",
   "ember/no-jquery": "off",
+  "ember/no-legacy-test-waiters": "off",
   "ember/no-new-mixins": "error",
   "ember/no-observers": "error",
   "ember/no-old-shims": "error",
diff --git a/lib/utils/import.js b/lib/utils/import.js
index 8d921863..3d741a30 100644
--- a/lib/utils/import.js
+++ b/lib/utils/import.js
@@ -11,6 +11,7 @@ const {
 module.exports = {
   getSourceModuleNameForIdentifier,
   getSourceModuleName,
+  getImportIdentifier,
 };
 
 /**
@@ -49,3 +50,33 @@ function getSourceModuleName(node) {
     return undefined;
   }
 }
+
+/**
+ * Gets an import identifier (either imported or local name) from the specified ImportDeclaration.
+ *
+ * @param {node} node the ImportDeclaration to find the import identifier for
+ * @param {string} source the source, or module name string, of the import
+ * @param {string} [namedImportIdentifier=null] the named import identifier to find (will return the alias of the import, of found)
+ * @returns {null} if no import is found with that name
+ */
+function getImportIdentifier(node, source, namedImportIdentifier = null) {
+  assert(
+    isImportDeclaration(node),
+    `getImportIdentifier should be called with a node that's type is 'ImportDeclaration'. You passed '${node.type}'`
+  );
+
+  if (node.source.value !== source) {
+    return null;
+  }
+
+  return node.specifiers
+    .filter(specifier => {
+      return (
+        (specifier.type === 'ImportSpecifier' &&
+          specifier.imported.name === namedImportIdentifier) ||
+        (!namedImportIdentifier && specifier.type === 'ImportDefaultSpecifier')
+      );
+    })
+    .map(specifier => specifier.local.name)
+    .pop();
+}


Test patch:
diff --git a/docs/rules/no-legacy-test-waiters.md b/docs/rules/no-legacy-test-waiters.md
new file mode 100644
index 00000000..eb94e82e
--- /dev/null
+++ b/docs/rules/no-legacy-test-waiters.md
@@ -0,0 +1,84 @@
+# no-legacy-test-waiters
+
+Prevents the use of the legacy test waiter APIs.
+
+## Rule Details
+
+The legacy test waiters API has been superseded by the new [ember-test-waiters](https://github.com/emberjs/ember-test-waiters) addon.
+This new addon provides an enhanced test waiter with robust debugging capabilities. Please use this in favor of the legacy waiter system, as detailed in [RFC 581](https://github.com/emberjs/rfcs/blob/master/text/0581-new-test-waiters.md).
+
+## Examples
+
+Examples of **incorrect** code for this rule:
+
+```js
+import Component from '@ember/component';
+import { registerWaiter } from '@ember/test';
+
+let counter = 0;
+
+if (DEBUG) {
+  registerWaiter(() => {
+    return counter === 0;
+  });
+}
+
+export default Component.extend({
+  init() {
+    counter++;
+    someAsync()
+      .then(() => console.log('hi'))
+      .finally(() => counter--);
+  },
+});
+```
+
+```js
+import Component from '@ember/component';
+import { registerWaiter, unregisterWaiter } from '@ember/test';
+
+let counter = 0;
+let waiter = () => {
+  return counter === 0;
+};
+
+if (DEBUG) {
+  registerWaiter(waiter);
+}
+
+export default Component.extend({
+  init() {
+    counter++;
+    someAsync()
+      .then(() => console.log('hi'))
+      .finally(() => counter--);
+  },
+
+  willDestroy() {
+    unregisterWaiter(waiter);
+  },
+});
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import Component from '@ember/component';
+import { buildWaiter } from 'ember-test-waiters';
+
+let waiter = buildWaiter('my-waiter');
+
+export default Component.extend({
+  init() {
+    let token = waiter.beginAsync();
+
+    someAsync()
+      .then(() => console.log('hi'))
+      .finally(() => waiter.endAsync(token));
+  },
+});
+```
+
+## References
+
+For more information on the new test waiters API, please visit [ember-test-waiters](https://github.com/emberjs/ember-test-waiters).
diff --git a/lib/rules/no-legacy-test-waiters.js b/lib/rules/no-legacy-test-waiters.js
new file mode 100644
index 00000000..08e3334a
--- /dev/null
+++ b/lib/rules/no-legacy-test-waiters.js
@@ -0,0 +1,48 @@
+'use strict';
+const { getImportIdentifier } = require('../utils/import');
+
+const LEGACY_TEST_WAITER_FUNCTIONS = ['registerWaiter', 'unregisterWaiter'];
+const ERROR_MESSAGE =
+  'The use of the legacy test waiters API is not allowed. Please use the new ember-test-waiters (https://github.com/emberjs/ember-test-waiters) APIs instead.';
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow the use of the legacy test waiter APIs.',
+      category: 'Testing',
+      recommended: false,
+      url:
+        'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-legacy-test-waiters.md',
+    },
+    fixable: null,
+    schema: [],
+  },
+
+  ERROR_MESSAGE,
+
+  create(context) {
+    let testWaitersIdentifiers = [];
+
+    return {
+      ImportDeclaration(node) {
+        if (node.source.value !== '@ember/test') {
+          return;
+        }
+
+        testWaitersIdentifiers = LEGACY_TEST_WAITER_FUNCTIONS.map(fn =>
+          getImportIdentifier(node, '@ember/test', fn)
+        );
+      },
+
+      CallExpression(node) {
+        if (
+          testWaitersIdentifiers.length > 0 &&
+          testWaitersIdentifiers.includes(node.callee.name)
+        ) {
+          context.report(node, ERROR_MESSAGE);
+        }
+      },
+    };
+  },
+};
diff --git a/tests/lib/rules/no-legacy-test-waiters.js b/tests/lib/rules/no-legacy-test-waiters.js
new file mode 100644
index 00000000..5dcbcc27
--- /dev/null
+++ b/tests/lib/rules/no-legacy-test-waiters.js
@@ -0,0 +1,84 @@
+const rule = require('../../../lib/rules/no-legacy-test-waiters');
+
+const { ERROR_MESSAGE } = rule;
+const RuleTester = require('eslint').RuleTester;
+
+const ruleTester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 6,
+    sourceType: 'module',
+  },
+});
+
+ruleTester.run('no-legacy-test-waiters', rule, {
+  valid: [
+    `
+      import Component from '@ember/component';
+      import { buildWaiter } from 'ember-test-waiters';
+
+      let waiter = buildWaiter('my-waiter');
+
+      export default Component.extend({
+        init() {
+          let token = waiter.beginAsync();
+
+          someAsync()
+            .finally(() => waiter.endAsync(token));
+        }
+      });`,
+    `
+      import { registerWaiter } from 'table-waiters';
+
+      registerWaiter();
+    `,
+    `
+      import { unregisterWaiter } from 'table-waiters';
+
+      unregisterWaiter();
+    `,
+  ],
+  invalid: [
+    {
+      code: `
+        import { registerWaiter } from '@ember/test';
+
+        registerWaiter(() => {
+          return counter === 0;
+        });
+      `,
+      output: null,
+      errors: [{ message: ERROR_MESSAGE }],
+    },
+    {
+      code: `
+        import { registerWaiter as reg } from '@ember/test';
+
+        reg(() => {
+          return counter === 0;
+        });
+      `,
+      output: null,
+      errors: [{ message: ERROR_MESSAGE }],
+    },
+    {
+      code: `
+        import { registerWaiter, unregisterWaiter } from '@ember/test';
+
+        registerWaiter(waiter);
+        unregisterWaiter(waiter);
+      `,
+      output: null,
+      errors: [{ message: ERROR_MESSAGE }, { message: ERROR_MESSAGE }],
+    },
+    {
+      code: `
+        import { registerWaiter as reg, unregisterWaiter as unreg } from '@ember/test';
+
+        reg(waiter);
+        unreg(waiter);
+      `,
+      output: null,
+      errors: [{ message: ERROR_MESSAGE }, { message: ERROR_MESSAGE }],
+    },
+  ],
+});
diff --git a/tests/lib/utils/import-test.js b/tests/lib/utils/import-test.js
index 048d002c..52ecf6d1 100644
--- a/tests/lib/utils/import-test.js
+++ b/tests/lib/utils/import-test.js
@@ -21,3 +21,52 @@ describe('getSourceModuleName', () => {
     expect(importUtils.getSourceModuleName(node)).toStrictEqual('Model');
   });
 });
+
+describe('getImportIdentifier', () => {
+  it('gets null when no import is found', () => {
+    const node = babelEslint.parse("import { later } from '@ember/runloop';").body[0];
+    expect(importUtils.getImportIdentifier(node, '@ember/object', 'action')).toBeNull();
+  });
+
+  it('gets an identifier when found', () => {
+    const node = babelEslint.parse("import { later } from '@ember/runloop';").body[0];
+    const identifier = importUtils.getImportIdentifier(node, '@ember/runloop', 'later');
+
+    expect(identifier).toStrictEqual('later');
+  });
+
+  it('gets an aliased identifier when found', () => {
+    const node = babelEslint.parse("import { later as laterz } from '@ember/runloop';").body[0];
+    const identifier = importUtils.getImportIdentifier(node, '@ember/runloop', 'later');
+
+    expect(identifier).toStrictEqual('laterz');
+  });
+
+  it('returns undefined when no default import is found', () => {
+    const node = babelEslint.parse("import { later } from '@ember/runloop';").body[0];
+    const identifier = importUtils.getImportIdentifier(node, '@ember/runloop');
+
+    expect(identifier).toBeUndefined();
+  });
+
+  it('gets an identifier when found for default imports', () => {
+    const node = babelEslint.parse("import Component from '@ember/component';").body[0];
+    const identifier = importUtils.getImportIdentifier(node, '@ember/component');
+
+    expect(identifier).toStrictEqual('Component');
+  });
+
+  it('gets an named identifier when found with mixed imports', () => {
+    const node = babelEslint.parse("import { later } from '@ember/runloop';").body[0];
+    const identifier = importUtils.getImportIdentifier(node, '@ember/runloop', 'later');
+
+    expect(identifier).toStrictEqual('later');
+  });
+
+  it('gets a default identifier when found with mixed imports', () => {
+    const node = babelEslint.parse("import Component from '@ember/component';").body[0];
+    const identifier = importUtils.getImportIdentifier(node, '@ember/component');
+
+    expect(identifier).toStrictEqual('Component');
+  });
+});