Repository: ember-cli/eslint-plugin-ember
Pull request #116 (2017-08-03 08:41:14)

Add "new-module-imports" rule
Resolves https://github.com/ember-cli/eslint-plugin-ember/issues/61
Resolves https://github.com/ember-cli/eslint-plugin-ember/issues/86

Note that this does unfortunately not support destructuring yet, but IMHO that can be added in another PR.

/cc @kratiahuja @locks @rwjblue 


Patch:
diff --git a/README.md b/README.md
index bf87e68e..e39d4792 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 | :white_check_mark: | [jquery-ember-run](./docs/rules/jquery-ember-run.md) | Prevents usage of jQuery without Ember Run Loop |
 |  | [local-modules](./docs/rules/local-modules.md) | Enforces usage of local modules |
 | :white_check_mark: | [named-functions-in-promises](./docs/rules/named-functions-in-promises.md) | Enforces usage of named functions in promises |
+|  | [new-module-imports](./docs/rules/new-module-imports.md) |  Use "New Module Imports" from Ember RFC #176 |
 |  | [no-attrs-in-components](./docs/rules/no-attrs-in-components.md) | Disallow usage of this.attrs in components |
 | :white_check_mark: | [no-function-prototype-extensions](./docs/rules/no-function-prototype-extensions.md) | Prevents usage of Ember's `function` prototype extensions |
 |  | [no-global-jquery](./docs/rules/no-global-jquery.md) | Prevents usage of global jQuery object |
diff --git a/docs/rules/new-module-imports.md b/docs/rules/new-module-imports.md
new file mode 100644
index 00000000..c6c93ddd
--- /dev/null
+++ b/docs/rules/new-module-imports.md
@@ -0,0 +1,23 @@
+## Use "New Module Imports" from Ember RFC #176
+
+### Rule name: `new-module-imports`
+
+[RFC #176](https://github.com/emberjs/rfcs/pull/176) introduced as new public
+API for Ember.js based on ES6 module imports.
+
+If you use `ember-cli-babel` with version `6.6.0` or above you can start using
+the "New Module Imports" instead of the `Ember` global directly. This will
+enable us to build better tree shaking feature into Ember CLI.
+
+```javascript
+// GOOD
+import Component from '@ember/component'
+import EmberObject, { computed } from '@ember/object'
+import Service, { inject } from '@ember/service'
+
+// BAD
+Ember.Component.extend({});
+Ember.Object.extend({});
+Ember.computed(function() {});
+Ember.inject.service('foo');
+```
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index d03abe55..473319cf 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -10,6 +10,7 @@ module.exports = {
   "ember/jquery-ember-run": "error",
   "ember/local-modules": "off",
   "ember/named-functions-in-promises": "error",
+  "ember/new-module-imports": "off",
   "ember/no-attrs-in-components": "off",
   "ember/no-attrs-snapshot": "error",
   "ember/no-capital-letters-in-routes": "error",
diff --git a/lib/rules/new-module-imports.js b/lib/rules/new-module-imports.js
new file mode 100644
index 00000000..d55f5240
--- /dev/null
+++ b/lib/rules/new-module-imports.js
@@ -0,0 +1,66 @@
+'use strict';
+
+const GLOBALS = require('ember-rfc176-data/globals.json');
+
+//------------------------------------------------------------------------------
+// General rule - Use "New Module Imports" from Ember RFC #176
+//------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: ' Use "New Module Imports" from Ember RFC #176',
+      category: 'General',
+      recommended: false
+    },
+  },
+
+  create(context) {
+    return {
+      'MemberExpression > Identifier[name=Ember]': function (node) {
+        // filter out "foo.Ember"
+        if (node.parent.object !== node) return;
+
+        // build an array of full expression names
+        // e.g. [Ember.computed, Ember.computed.or]
+        let fullName = 'Ember';
+        const fullNames = [];
+        let parentNode = node.parent;
+        while (parentNode.type === 'MemberExpression') {
+          fullName += `.${parentNode.property.name}`;
+          fullNames.push(fullName);
+          parentNode = parentNode.parent;
+        }
+
+        fullNames.reverse();
+
+        // find a matching expression starting at the end
+        for (let i = 0; i < fullNames.length; i++) {
+          fullName = fullNames[i];
+
+          const key = fullName.replace(/^Ember\./, '');
+          const match = GLOBALS[key];
+          if (match) {
+            // build replacement import for the warning message
+            let importSpecifier;
+            if (match[1]) {
+              importSpecifier = match[2] ? `{ ${match[1]} as ${match[2]} }` : `{ ${match[1]} }`;
+            } else {
+              importSpecifier = match[2] || key;
+            }
+
+            const replacement = `import ${importSpecifier} from '${match[0]}';`;
+
+            const message = `Use  ${replacement}  instead of using  ${fullName}`;
+
+            // report the issue to ESLint
+            context.report(node, message);
+
+            // exit the loop after the first match was found
+            break;
+          }
+        }
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/new-module-imports.js b/tests/lib/rules/new-module-imports.js
new file mode 100644
index 00000000..3d10c47e
--- /dev/null
+++ b/tests/lib/rules/new-module-imports.js
@@ -0,0 +1,56 @@
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/new-module-imports');
+const RuleTester = require('eslint').RuleTester;
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const eslintTester = new RuleTester();
+eslintTester.run('new-module-imports', rule, {
+  valid: [
+    { code: 'Ember.onerror = function() {};' },
+    { code: 'Ember.MODEL_FACTORY_INJECTIONS = true;' },
+    { code: 'console.log(Ember.VERSION);' },
+    { code: 'if (Ember.testing) {}' },
+  ],
+  invalid: [
+    {
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      code: 'export default Ember.Service;',
+      errors: [{ message: 'Use  import Service from \'@ember/service\';  instead of using  Ember.Service' }],
+    },
+    {
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      code: 'export default Ember.Service.extend({});',
+      errors: [{ message: 'Use  import Service from \'@ember/service\';  instead of using  Ember.Service' }],
+    },
+    {
+      code: 'Ember.computed();',
+      errors: [{ message: 'Use  import { computed } from \'@ember/object\';  instead of using  Ember.computed' }],
+    },
+    {
+      code: 'Ember.computed.not();',
+      errors: [{ message: 'Use  import { not } from \'@ember/object/computed\';  instead of using  Ember.computed.not' }],
+    },
+    {
+      code: 'Ember.inject.service(\'foo\');',
+      errors: [{ message: 'Use  import { inject } from \'@ember/service\';  instead of using  Ember.inject.service' }],
+    },
+    {
+      code: 'var Router = Ember.Router.extend({});',
+      errors: [{ message: 'Use  import Router from \'@ember/routing/router\';  instead of using  Ember.Router' }],
+    },
+    {
+      code: 'Ember.$(\'.foo\')',
+      errors: [{ message: 'Use  import $ from \'jquery\';  instead of using  Ember.$' }],
+    },
+    {
+      code: 'new Ember.RSVP.Promise();',
+      errors: [{ message: 'Use  import RSVP from \'rsvp\';  instead of using  Ember.RSVP' }],
+    },
+  ],
+});