Repository: ember-cli/eslint-plugin-ember
Pull request #450 (2019-07-11 18:06:29)

Add new `use-ember-data-rfc-395-imports` rule
## Description

This PR introduces a new rule to ease ember-data migration to new packages: `use-ember-data-rfc-395-imports`.

Examples of **incorrect** code for this rule:
```js
import Model from "ember-data/model";
import attr from "ember-data/attr";

import DS from "ember-data";
const { Model } = "ember-data";
```

Examples of **correct** code for this rule:

```js
import Model, { attr } from "@ember-data/model";

import Model from "@ember-data/model";
```

## Note

Split into 5 commits that may be reviewed one after the other.

It leverages what have been done in the past with Ember globals and ember-cli-shims.

~~A few discussion are still in progress around https://github.com/ember-data/ember-data-rfc395-data/ (the data set). Hence the WIP!~~

## Links 

[ember-data RFC](https://github.com/ember-data/ember-data-rfc395-data)
[a related codemod](https://github.com/ember-codemods/ember-data-codemod)


_EDIT: I've updated this description to reflect the changes made following https://github.com/ember-data/ember-data-rfc395-data/pull/7_


dcyriller: cc @runspired @rwjblue 
dcyriller: This is ready now.
rwjblue: @bmish 	- Mind doing one more pass on this one before we land / release?
runspired: I dislike being pedantic especially when I feel the name has been changed a few times, but is `no-ember-data-global-imports` really the best name for a rule that:

- lints against older module import locations
- AND lints against an older "single-export-object" style import
- AND lints against global syntax

There was a suggestion to name the rule after the rfc, I think it's probably the way to go.

`use-ember-data-rfc-395-imports`
dcyriller: > There was a suggestion to name the rule after the rfc, I think it's probably the way to go. `use-ember-data-rfc-395-imports`

@rwjblue @bmish if you agree, I'll change that. Let me know! (I want to be sure everyone agrees before changing it.)
rwjblue: Yeah, that seems fine to me. My only real objection was the "new" in the original name, I'm sorry for the run around / bike shedding here.
dcyriller: No worries, I prefer to make it right!

Patch:
diff --git a/.travis.yml b/.travis.yml
index e530d280..f544dfb9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,8 +7,8 @@ node_js:
 cache: yarn
 
 script:
-  - yarn run lint
-  - yarn run test:coverage --runInBand
+  - yarn lint
+  - yarn test:coverage --runInBand
 
 before_deploy:
   - yarn global add auto-dist-tag
diff --git a/README.md b/README.md
index 4f4d2f40..5034174c 100644
--- a/README.md
+++ b/README.md
@@ -87,7 +87,6 @@ The `--fix` option on the command line automatically fixes problems reported by
 | :white_check_mark: | [closure-actions](./docs/rules/closure-actions.md) | Enforces usage of closure actions |
 |  | [named-functions-in-promises](./docs/rules/named-functions-in-promises.md) | Enforces usage of named functions in promises |
 | :white_check_mark: | [new-module-imports](./docs/rules/new-module-imports.md) |  Use "New Module Imports" from Ember RFC #176 |
-|  | [no-empty-attrs](./docs/rules/no-empty-attrs.md) | Prevents usage of empty attributes in ember data models |
 | :white_check_mark: | [no-function-prototype-extensions](./docs/rules/no-function-prototype-extensions.md) | Prevents usage of Ember's `function` prototype extensions |
 |  | [no-get](./docs/rules/no-get.md) | Require ES5 getters instead of Ember's `get` / `getProperties` functions |
 | :white_check_mark: | [no-global-jquery](./docs/rules/no-global-jquery.md) | Prevents usage of global jQuery object |
@@ -137,6 +136,14 @@ The `--fix` option on the command line automatically fixes problems reported by
 |  | [no-proxies](./docs/rules/no-proxies.md) | Disallows using array or object proxies |
 
 
+### Ember Data
+
+|    | Rule ID | Description |
+|:---|:--------|:------------|
+|  | [no-empty-attrs](./docs/rules/no-empty-attrs.md) | Prevents usage of empty attributes in ember data models |
+| :wrench: | [use-ember-data-rfc-395-imports](./docs/rules/use-ember-data-rfc-395-imports.md) | Use "Ember Data Packages" from Ember RFC #395 |
+
+
 ### Testing
 
 |    | Rule ID | Description |
diff --git a/docs/rules/use-ember-data-rfc-395-imports.md b/docs/rules/use-ember-data-rfc-395-imports.md
new file mode 100644
index 00000000..0845fae8
--- /dev/null
+++ b/docs/rules/use-ember-data-rfc-395-imports.md
@@ -0,0 +1,41 @@
+# Use &#34;Ember Data Packages&#34; from Ember RFC #395 (use-ember-data-rfc-395-imports)
+
+The goal of this rule is to ease the migration to the new @ember-data packages.
+
+## Context
+
+ember-data has been split in multiple packages. For instance, its store is now released in "@ember-data/store" package. These packages have been released starting from ember-data version 3.11.
+
+## Rule Details
+
+Examples of **incorrect** code for this rule:
+
+```js
+import Model from "ember-data/model";
+import attr from "ember-data/attr";
+
+import DS from "ember-data";
+const { Model } = "ember-data";
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import Model, { attr } from "@ember-data/model";
+
+import Model from "@ember-data/model";
+```
+
+## How to fix
+
+This rule implements a fix function (or to be more precise: it leverages the `no-old-shims` fix function). If, as a user, you can get rid of all the `import DS from "ember-data";` imports, you'll be able to use this rule fixer to complete the upgrade. 
+
+Note that [a codemod is also available](https://github.com/ember-codemods/ember-data-codemod) to complete the upgrade.
+
+## When Not To Use It
+
+You don't want to use this rule if you're using `ember-data@<3.11`.
+
+## Further Reading
+
+[Ember Data Packages RFC](https://github.com/emberjs/rfcs/blob/master/text/0395-ember-data-packages.md)
diff --git a/lib/index.js b/lib/index.js
index 66e085a2..8d8756ee 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -52,6 +52,7 @@ module.exports = {
     'require-super-in-init': require('./rules/require-super-in-init'),
     'routes-segments-snake-case': require('./rules/routes-segments-snake-case'),
     'use-brace-expansion': require('./rules/use-brace-expansion'),
+    'use-ember-data-rfc-395-imports': require('./rules/use-ember-data-rfc-395-imports'),
     'use-ember-get-and-set': require('./rules/use-ember-get-and-set'),
   },
   configs: {
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index ba07e2a4..669824b6 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -55,5 +55,6 @@ module.exports = {
   "ember/route-path-style": "off",
   "ember/routes-segments-snake-case": "error",
   "ember/use-brace-expansion": "error",
+  "ember/use-ember-data-rfc-395-imports": "off",
   "ember/use-ember-get-and-set": "off"
-}
+}
\ No newline at end of file
diff --git a/lib/rules/new-module-imports.js b/lib/rules/new-module-imports.js
index e9cfd6b1..0a77f4cf 100644
--- a/lib/rules/new-module-imports.js
+++ b/lib/rules/new-module-imports.js
@@ -1,7 +1,7 @@
 'use strict';
 
-const EMBER_NAMESPACES = ['inject.controller', 'inject.service'];
 const MAPPING = require('ember-rfc176-data');
+const { buildMessage, getFullNames, isInitImportedFrom } = require('../utils/new-module');
 
 const GLOBALS = MAPPING.reduce((memo, exportDefinition) => {
   if (exportDefinition.deprecated) {
@@ -33,10 +33,32 @@ module.exports = {
   },
 
   create(context) {
+    function reportNestedProperties(properties, parent) {
+      properties.forEach(item => {
+        const match = GLOBALS[`Ember.${parent}.${item.key.name}`];
+
+        if (match) {
+          const message = buildMessage({
+            node: item,
+            key: item.key.name,
+            match,
+            parent,
+            type: item.type,
+          });
+
+          context.report(item, message);
+        }
+      });
+    }
+
     return {
       VariableDeclarator(node) {
+        if (!node.init) return;
+
         // Filter out non-Ember variable declarations
-        if (!node.init || node.init.name !== 'Ember') return;
+        if (node.init.name !== 'Ember' && !isInitImportedFrom(node, 'ember')) {
+          return;
+        }
 
         const properties = node.id.properties;
         // Iterate through the destructured properties and report them
@@ -49,13 +71,18 @@ module.exports = {
           } else {
             const key = item.key.name;
             const match = GLOBALS[`Ember.${key}`];
-            populateMessage({
-              node: item,
-              customKey: key !== item.value.name ? item.value.name : null,
-              key,
-              match,
-              type: item.type,
-            });
+
+            if (match) {
+              const message = buildMessage({
+                node: item,
+                customKey: key !== item.value.name ? item.value.name : null,
+                key,
+                match,
+                type: item.type,
+              });
+
+              context.report(item, message);
+            }
           }
         });
       },
@@ -67,15 +94,7 @@ module.exports = {
         // 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();
+        const fullNames = getFullNames(fullName, node);
 
         // find a matching expression starting at the end
         for (let i = 0; i < fullNames.length; i++) {
@@ -84,72 +103,15 @@ module.exports = {
           const key = fullName.replace(/^Ember\./, '');
           const match = GLOBALS[fullName];
 
-          const reportedError = populateMessage({ node, fullName, key, match });
-
-          if (reportedError) {
-            // exit the loop after the first match was found
+          // if a given global path does not exist in `mappings.json` there is no
+          // JS module import for it, so do not report the error
+          if (match) {
+            const message = buildMessage({ node, fullName, key, match });
+            context.report(node, message);
             break;
           }
         }
       },
     };
-
-    function reportNestedProperties(properties, parent) {
-      properties.forEach(item => {
-        const match = GLOBALS[`Ember.${parent}.${item.key.name}`];
-
-        populateMessage({
-          node: item,
-          key: item.key.name,
-          match,
-          parent,
-          type: item.type,
-        });
-      });
-    }
-
-    function populateMessage(obj) {
-      // if a given global path does not exist in `globals.json` there is no
-      // JS module import for it, so do not report the error
-      if (!obj.match) {
-        return false;
-      }
-
-      // Both the Controller module and Service module each make available the
-      // "inject" namespace. In order to make it more readable, so it's more
-      // explicit at first glance from which module the "inject" namespace
-      // belongs to or is being imported from, let's check against this so we
-      // can properly populate the import specifier to report the ESLint error.
-      // Ex: import { inject as service } from '@ember/service';
-      const isNamespace = EMBER_NAMESPACES.indexOf(`${obj.parent}.${obj.key}`) !== -1;
-
-      const isNamedExport = obj.match.export !== 'default';
-
-      let importSpecifier;
-      let message;
-
-      if (isNamedExport && !isNamespace) {
-        importSpecifier = obj.match.localName
-          ? `{ ${obj.match.export} as ${obj.match.localName} }`
-          : `{ ${obj.match.export} }`;
-      } else if (isNamedExport && isNamespace) {
-        importSpecifier = `{ ${obj.parent} as ${obj.key} }`;
-      } else {
-        importSpecifier = obj.match.localName || obj.customKey || obj.key;
-      }
-
-      const replacement = `import ${importSpecifier} from '${obj.match.module}';`;
-
-      if (obj.type === 'Property') {
-        message = `Use ${replacement} instead of using Ember destructuring`;
-      } else {
-        message = `Use ${replacement} instead of using ${obj.fullName}`;
-      }
-
-      // report the issue to ESLint
-      context.report(obj.node, message);
-
-      return true;
-    }
   },
 };
diff --git a/lib/rules/no-empty-attrs.js b/lib/rules/no-empty-attrs.js
index 82bdb5c4..809be318 100644
--- a/lib/rules/no-empty-attrs.js
+++ b/lib/rules/no-empty-attrs.js
@@ -10,7 +10,7 @@ module.exports = {
   meta: {
     docs: {
       description: 'Prevents usage of empty attributes in ember data models',
-      category: 'Best Practices',
+      category: 'Ember Data',
       recommended: false,
       url:
         'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-empty-attrs.md',
diff --git a/lib/rules/no-old-shims.js b/lib/rules/no-old-shims.js
index e9818a2d..62788604 100644
--- a/lib/rules/no-old-shims.js
+++ b/lib/rules/no-old-shims.js
@@ -246,6 +246,8 @@ const oldShimsData = {
 // General rule - Don't use import paths from ember-cli-shims
 //------------------------------------------------------------------------------
 
+const { buildFix } = require('../utils/new-module');
+
 module.exports = {
   meta: {
     docs: {
@@ -263,66 +265,11 @@ module.exports = {
 
     return {
       ImportDeclaration(node) {
-        const moduleName = node.source.value;
-        if (!(moduleName in oldShimsData)) {
+        if (!(node.source.value in oldShimsData)) {
           return;
         }
 
-        const moduleMappings = oldShimsData[moduleName];
-
-        const fix = function(fixer) {
-          const newImports = {};
-
-          node.specifiers.forEach(specifier => {
-            const localName = specifier.local.name;
-
-            let importedName;
-            if (specifier.type === 'ImportDefaultSpecifier') {
-              importedName = 'default';
-            } else {
-              importedName = specifier.imported.name;
-            }
-
-            let module;
-            const moduleMapping = moduleMappings[importedName];
-            if (!moduleMapping) {
-              module = moduleName;
-            } else {
-              module = moduleMapping[0];
-              importedName = moduleMapping[1] || 'default';
-            }
-
-            newImports[module] = newImports[module] || [];
-            newImports[module].push({ localName, importedName });
-          });
-
-          const lines = Object.keys(newImports).map(module => {
-            const newModuleImport = newImports[module];
-
-            const defaultImport = newModuleImport
-              .filter(it => it.importedName === 'default')
-              .map(it => it.localName);
-
-            const namedImports = newModuleImport
-              .filter(it => it.importedName !== 'default')
-              .map(it =>
-                it.importedName !== it.localName
-                  ? `${it.importedName} as ${it.localName}`
-                  : it.importedName
-              )
-              .join(', ');
-
-            const specifiers = defaultImport
-              .concat(namedImports ? `{ ${namedImports} }` : '')
-              .filter(Boolean)
-              .join(', ');
-
-            return `import ${specifiers} from '${module}';`;
-          });
-
-          return fixer.replaceText(node, lines.join('\n'));
-        };
-
+        const fix = buildFix(node, oldShimsData);
         context.report({ node, message, fix });
       },
     };
diff --git a/lib/rules/use-ember-data-rfc-395-imports.js b/lib/rules/use-ember-data-rfc-395-imports.js
new file mode 100644
index 00000000..392a918e
--- /dev/null
+++ b/lib/rules/use-ember-data-rfc-395-imports.js
@@ -0,0 +1,170 @@
+'use strict';
+
+const MAPPINGS = require('@ember-data/rfc395-data');
+const { buildFix, buildMessage, getFullNames, isInitImportedFrom } = require('../utils/new-module');
+
+/**
+ * This function returns an object like this:
+ * {
+ *   'ember-data/model': {
+ *     default: ['@ember-data/model'],
+ *   },
+ * }
+ */
+function oldDataImportsReducer(acc, mapping) {
+  let obj = Object.create(null);
+  obj[mapping.export] = [mapping.replacement.module, mapping.replacement.export];
+  acc[mapping.module] = obj; // eslint-disable-line no-param-reassign
+
+  return acc;
+}
+
+const OLD_DATA_IMPORTS = MAPPINGS.reduce(oldDataImportsReducer, Object.create(null));
+
+/**
+ * This function returns an object like this:
+ * {
+ *   "DS.Model": {
+ *     global: "DS.Model",
+ *     export: "default",
+ *     localName: "Model",
+ *     module: "@ember-data/model" <- replacement
+ *   }
+ * }
+ */
+function globalsReducer(acc, mapping) {
+  const { global } = mapping;
+  if (global in acc) {
+    return acc;
+  }
+
+  let obj = {
+    global,
+    export: mapping.replacement.export,
+    localName: mapping.localName,
+    module: mapping.replacement.module,
+  };
+
+  acc[global] = obj;
+
+  return acc;
+}
+
+const GLOBALS = MAPPINGS.reduce(globalsReducer, Object.create(null));
+
+const ERROR_MESSAGE =
+  'Imports from @ember-data packages should be prefered over imports from ember-data';
+
+//------------------------------------------------------------------------------
+// Rule Definition - Use "Ember Data Packages" from Ember RFC #395
+//------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Use "Ember Data Packages" from Ember RFC #395',
+      category: 'Ember Data',
+      recommended: false,
+      url:
+        'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/use-ember-data-rfc-395-imports.md',
+    },
+    fixable: 'code',
+  },
+
+  ERROR_MESSAGE,
+
+  create(context) {
+    //----------------------------------------------------------------------
+    // Variables
+    //----------------------------------------------------------------------
+
+    const message = ERROR_MESSAGE;
+
+    //----------------------------------------------------------------------
+    // Public
+    //----------------------------------------------------------------------
+
+    return {
+      /**
+       * turn `import Model from "ember-data/model"`
+       * into `import Model from "@ember-data/model"`
+       * or
+       * `import attr from "ember-data/attr"`
+       * into `import { attr } from "@ember-data/attr"`
+       */
+      ImportDeclaration(node) {
+        if (node.source.value in OLD_DATA_IMPORTS) {
+          const fix = buildFix(node, OLD_DATA_IMPORTS);
+          context.report({ node, message, fix });
+          return;
+        }
+
+        if (node.source.value.startsWith('ember-data')) {
+          context.report(node, message);
+        }
+      },
+
+      /**
+       * warn against `const { Model } = DS`
+       * and against `const { Model } = ED` (only) if ED is imported from ember-data
+       */
+      VariableDeclarator(node) {
+        if (!node.init) return;
+
+        // Filter out non-DS variable declarations
+        if (node.init.name !== 'DS' && !isInitImportedFrom(node, 'ember-data')) {
+          return;
+        }
+
+        const properties = node.id.properties;
+        // Iterate through the destructured properties and report them
+        properties.forEach(item => {
+          const key = item.key.name;
+          const match = GLOBALS[`DS.${key}`];
+          if (match) {
+            const message = buildMessage({
+              node: item,
+              customKey: key !== item.value.name ? item.value.name : null,
+              key,
+              match,
+              type: item.type,
+            });
+            context.report(item, message);
+          }
+        });
+      },
+
+      /**
+       * warn against DS usage in `name: DS.attr('string')`
+       * taken from
+       * https://github.com/ember-cli/eslint-plugin-ember/blob/v6.7.1/lib/rules/new-module-imports.js#L63
+       */
+      'MemberExpression > Identifier[name=DS]'(node) {
+        // filter out "foo.DS"
+        if (node.parent.object !== node) return;
+
+        // build an array of full expression names
+        // e.g. [DS.Model, DS.JSONSerializer]
+
+        let fullName = 'DS';
+        const fullNames = getFullNames(fullName, node);
+
+        // find a matching expression starting at the end
+        for (let i = 0; i < fullNames.length; i++) {
+          fullName = fullNames[i];
+
+          const key = fullName.replace(/^DS\./, '');
+          const match = GLOBALS[fullName];
+
+          // if a given global path does not exist in `mappings.json` there is no
+          // JS module import for it, so do not report the error
+          if (match) {
+            const message = buildMessage({ node, fullName, key, match });
+            context.report(node, message);
+            break;
+          }
+        }
+      },
+    };
+  },
+};
diff --git a/lib/utils/new-module.js b/lib/utils/new-module.js
new file mode 100644
index 00000000..9dca2bc3
--- /dev/null
+++ b/lib/utils/new-module.js
@@ -0,0 +1,179 @@
+'use strict';
+
+const EMBER_NAMESPACES = ['inject.controller', 'inject.service'];
+
+// Both the Controller module and Service module each make available the
+// "inject" namespace. In order to make it more readable, so it's more
+// explicit at first glance from which module the "inject" namespace
+// belongs to or is being imported from, let's check against this so we
+// can properly populate the import specifier to report the ESLint error.
+// Ex: import { inject as service } from '@ember/service';
+function isNamespace(obj) {
+  return EMBER_NAMESPACES.includes(`${obj.parent}.${obj.key}`);
+}
+
+function isNamedExport(obj) {
+  return obj.match.export !== 'default';
+}
+
+function getReplacementSpecifier(obj) {
+  let importSpecifier;
+
+  if (isNamedExport(obj) && !isNamespace(obj)) {
+    importSpecifier =
+      obj.match.localName && obj.match.localName !== obj.match.export
+        ? `{ ${obj.match.export} as ${obj.match.localName} }`
+        : `{ ${obj.match.export} }`;
+  } else if (isNamedExport(obj) && isNamespace(obj)) {
+    importSpecifier = `{ ${obj.parent} as ${obj.key} }`;
+  } else {
+    importSpecifier = obj.match.localName || obj.customKey || obj.key;
+  }
+
+  return importSpecifier;
+}
+
+function buildMessage(obj) {
+  let message;
+
+  const importSpecifier = getReplacementSpecifier(obj);
+
+  const replacement = `import ${importSpecifier} from '${obj.match.module}';`;
+
+  if (obj.type === 'Property') {
+    const global = obj.match.global.match(/^DS/) ? 'DS' : 'Ember';
+    message = `Use \`${replacement}\` instead of using ${global} destructuring`;
+  } else {
+    message = `Use \`${replacement}\` instead of using ${obj.fullName}`;
+  }
+
+  return message;
+}
+
+/**
+ * This function returns a fix function. The fix function can update the old
+ * imports to the new ones.
+ * @param {Object} node - the AST node related to the import problem
+ * @param {Object} modulesData - the data set that maps old imports to the new
+ * ones. It is an object like this one:
+ * {
+ *   'ember-array': {
+ *     default: ['@ember/array'],
+ *   },
+ *   'ember-array/mutable': {
+ *     default: ['@ember/array/mutable'],
+ *   },
+ *   'ember-array/utils': {
+ *     A: ['@ember/array', 'A'],
+ *     isEmberArray: ['@ember/array', 'isArray'],
+ *     wrap: ['@ember/array', 'makeArray'],
+ *   },
+ * }
+ * @return {function(fixer: Object): function} a function that applies a fix to
+ * update the import
+ */
+function buildFix(node, modulesData) {
+  const moduleName = node.source.value;
+
+  function fix(fixer) {
+    const newImports = buildNewImports(node, modulesData, moduleName);
+
+    const lines = buildLines(newImports);
+
+    return fixer.replaceText(node, lines.join('\n'));
+  }
+
+  return fix;
+}
+
+function buildNewImports(node, modulesData, moduleName) {
+  const newImports = {};
+
+  node.specifiers.forEach(specifier => {
+    const localName = specifier.local.name;
+
+    let importedName;
+    if (specifier.type === 'ImportDefaultSpecifier') {
+      importedName = 'default';
+    } else {
+      importedName = specifier.imported.name;
+    }
+
+    let module;
+    const moduleMapping = modulesData[moduleName][importedName];
+    if (!moduleMapping) {
+      module = moduleName;
+    } else {
+      module = moduleMapping[0];
+      importedName = moduleMapping[1] || 'default';
+    }
+
+    newImports[module] = newImports[module] || [];
+    newImports[module].push({ localName, importedName });
+  });
+
+  return newImports;
+}
+
+function buildLines(newImports) {
+  return Object.keys(newImports).map(module => {
+    const newModuleImport = newImports[module];
+
+    const defaultImport = newModuleImport
+      .filter(it => it.importedName === 'default')
+      .map(it => it.localName);
+
+    const namedImports = newModuleImport
+      .filter(it => it.importedName !== 'default')
+      .map(it =>
+        it.importedName !== it.localName ? `${it.importedName} as ${it.localName}` : it.importedName
+      )
+      .join(', ');
+
+    const specifiers = defaultImport
+      .concat(namedImports ? `{ ${namedImports} }` : '')
+      .filter(Boolean)
+      .join(', ');
+
+    return `import ${specifiers} from '${module}';`;
+  });
+}
+
+function getFullNames(prefix, node) {
+  let fullName = prefix;
+  const fullNames = [];
+
+  let parentNode = node.parent;
+  while (parentNode.type === 'MemberExpression') {
+    fullName += `.${parentNode.property.name}`;
+    fullNames.unshift(fullName);
+    parentNode = parentNode.parent;
+  }
+
+  return fullNames;
+}
+
+function isInitImportedFrom(node, module) {
+  const { name } = node.init;
+  const pp = node.parent.parent;
+  return pp.body.some(n => {
+    function containsInitSpecifier(specifiers) {
+      return specifiers.some(s => {
+        return s.local.name === name;
+      });
+    }
+
+    return (
+      n.type === 'ImportDeclaration' &&
+      n.source.value === module &&
+      containsInitSpecifier(n.specifiers)
+    );
+  });
+}
+
+module.exports = {
+  buildFix,
+  buildMessage,
+  getFullNames,
+  isInitImportedFrom,
+};
diff --git a/package.json b/package.json
index 794b631d..003418e4 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,7 @@
     "prettier": "1.18.2"
   },
   "dependencies": {
+    "@ember-data/rfc395-data": "^0.0.4",
     "ember-rfc176-data": "^0.3.9",
     "snake-case": "^2.1.0"
   },
diff --git a/yarn.lock b/yarn.lock
index 5225e772..0da89726 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -142,6 +142,11 @@
     exec-sh "^0.3.2"
     minimist "^1.2.0"
 
+"@ember-data/rfc395-data@^0.0.4":
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/@ember-data/rfc395-data/-/rfc395-data-0.0.4.tgz#ecb86efdf5d7733a76ff14ea651a1b0ed1f8a843"
+  integrity sha512-tGRdvgC9/QMQSuSuJV45xoyhI0Pzjm7A9o/MVVA3HakXIImJbbzx/k/6dO9CUEQXIyS2y0fW6C1XaYOG7rY0FQ==
+
 "@jest/console@^24.7.1":
   version "24.7.1"
   resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545"


Test patch:
diff --git a/tests/lib/rules/new-module-imports.js b/tests/lib/rules/new-module-imports.js
index d22153f7..b2d849c7 100644
--- a/tests/lib/rules/new-module-imports.js
+++ b/tests/lib/rules/new-module-imports.js
@@ -42,6 +42,13 @@ eslintTester.run('new-module-imports', rule, {
       `,
       parserOptions: { ecmaVersion: 6, sourceType: 'module' },
     },
+    {
+      code: `import LOL from 'who-knows-but-definitely-not-ember';
+
+        const { Controller } = LOL;
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
   ],
   invalid: [
     {
@@ -56,7 +63,22 @@ eslintTester.run('new-module-imports', rule, {
       errors: [
         {
           message:
-            "Use import EmberObject from '@ember/object'; instead of using Ember destructuring",
+            "Use `import EmberObject from '@ember/object';` instead of using Ember destructuring",
+          line: 3,
+        },
+      ],
+    },
+    {
+      code: `import LOL from 'ember';
+
+        const { Controller } = LOL;
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      output: null,
+      errors: [
+        {
+          message:
+            "Use `import Controller from '@ember/controller';` instead of using Ember destructuring",
           line: 3,
         },
       ],
@@ -72,12 +94,12 @@ eslintTester.run('new-module-imports', rule, {
       output: null,
       errors: [
         {
-          message: "Use import $ from 'jquery'; instead of using Ember destructuring",
+          message: "Use `import $ from 'jquery';` instead of using Ember destructuring",
           line: 3,
         },
         {
           message:
-            "Use import Controller from '@ember/controller'; instead of using Ember destructuring",
+            "Use `import Controller from '@ember/controller';` instead of using Ember destructuring",
           line: 3,
         },
       ],
@@ -95,12 +117,12 @@ eslintTester.run('new-module-imports', rule, {
       errors: [
         {
           message:
-            "Use import Component from '@ember/component'; instead of using Ember destructuring",
+            "Use `import Component from '@ember/component';` instead of using Ember destructuring",
           line: 3,
         },
         {
           message:
-            "Use import { htmlSafe } from '@ember/template'; instead of using Ember destructuring",
+            "Use `import { htmlSafe } from '@ember/template';` instead of using Ember destructuring",
           line: 3,
         },
       ],
@@ -119,16 +141,17 @@ eslintTester.run('new-module-imports', rule, {
       errors: [
         {
           message:
-            "Use import { inject as controller } from '@ember/controller'; instead of using Ember destructuring",
+            "Use `import { inject as controller } from '@ember/controller';` instead of using Ember destructuring",
           line: 3,
         },
         {
           message:
-            "Use import { inject as service } from '@ember/service'; instead of using Ember destructuring",
+            "Use `import { inject as service } from '@ember/service';` instead of using Ember destructuring",
           line: 3,
         },
         {
-          message: "Use import Component from '@ember/component'; instead of using Ember.Component",
+          message:
+            "Use `import Component from '@ember/component';` instead of using Ember.Component",
           line: 5,
         },
       ],
@@ -147,12 +170,12 @@ eslintTester.run('new-module-imports', rule, {
       errors: [
         {
           message:
-            "Use import { alias } from '@ember/object/computed'; instead of using Ember destructuring",
+            "Use `import { alias } from '@ember/object/computed';` instead of using Ember destructuring",
           line: 5,
         },
         {
           message:
-            "Use import { uniq } from '@ember/object/computed'; instead of using Ember destructuring",
+            "Use `import { uniq } from '@ember/object/computed';` instead of using Ember destructuring",
           line: 5,
         },
       ],
@@ -163,7 +186,7 @@ eslintTester.run('new-module-imports', rule, {
       output: null,
       errors: [
         {
-          message: "Use import Service from '@ember/service'; instead of using Ember.Service",
+          message: "Use `import Service from '@ember/service';` instead of using Ember.Service",
           line: 1,
         },
       ],
@@ -174,7 +197,7 @@ eslintTester.run('new-module-imports', rule, {
       output: null,
       errors: [
         {
-          message: "Use import Service from '@ember/service'; instead of using Ember.Service",
+          message: "Use `import Service from '@ember/service';` instead of using Ember.Service",
           line: 1,
         },
       ],
@@ -184,7 +207,8 @@ eslintTester.run('new-module-imports', rule, {
       output: null,
       errors: [
         {
-          message: "Use import { computed } from '@ember/object'; instead of using Ember.computed",
+          message:
+            "Use `import { computed } from '@ember/object';` instead of using Ember.computed",
           line: 1,
         },
       ],
@@ -195,7 +219,7 @@ eslintTester.run('new-module-imports', rule, {
       errors: [
         {
           message:
-            "Use import { not } from '@ember/object/computed'; instead of using Ember.computed.not",
+            "Use `import { not } from '@ember/object/computed';` instead of using Ember.computed.not",
           line: 1,
         },
       ],
@@ -206,7 +230,7 @@ eslintTester.run('new-module-imports', rule, {
       errors: [
         {
           message:
-            "Use import { inject } from '@ember/service'; instead of using Ember.inject.service",
+            "Use `import { inject } from '@ember/service';` instead of using Ember.inject.service",
           line: 1,
         },
       ],
@@ -217,7 +241,7 @@ eslintTester.run('new-module-imports', rule, {
       errors: [
         {
           message:
-            "Use import EmberRouter from '@ember/routing/router'; instead of using Ember.Router",
+            "Use `import EmberRouter from '@ember/routing/router';` instead of using Ember.Router",
           line: 1,
         },
       ],
@@ -227,7 +251,7 @@ eslintTester.run('new-module-imports', rule, {
       output: null,
       errors: [
         {
-          message: "Use import $ from 'jquery'; instead of using Ember.$",
+          message: "Use `import $ from 'jquery';` instead of using Ember.$",
           line: 1,
         },
       ],
@@ -237,7 +261,7 @@ eslintTester.run('new-module-imports', rule, {
       output: null,
       errors: [
         {
-          message: "Use import { Promise } from 'rsvp'; instead of using Ember.RSVP.Promise",
+          message: "Use `import { Promise } from 'rsvp';` instead of using Ember.RSVP.Promise",
           line: 1,
         },
       ],
diff --git a/tests/lib/rules/use-ember-data-rfc-395-imports.js b/tests/lib/rules/use-ember-data-rfc-395-imports.js
new file mode 100644
index 00000000..8558c465
--- /dev/null
+++ b/tests/lib/rules/use-ember-data-rfc-395-imports.js
@@ -0,0 +1,185 @@
+'use strict';
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/use-ember-data-rfc-395-imports');
+const RuleTester = require('eslint').RuleTester;
+const parserOptions = { ecmaVersion: 6, sourceType: 'module' };
+
+const { ERROR_MESSAGE: message } = rule;
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions });
+ruleTester.run('use-ember-data-rfc-395-imports', rule, {
+  valid: [
+    "import Model from '@ember-data/model';",
+    `import Model, { attr } from '@ember-data';
+
+     export default Model.extend({
+       name: attr('string')
+     });
+    `,
+    `import LOL from 'who-knows-but-definitely-not-ember-data';
+
+     const { Model } = LOL;
+    `,
+  ],
+
+  invalid: [
+    {
+      code: `import DS from 'ember-data';
+
+        export default DS.Model.extend({
+        });
+      `,
+      errors: [
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+        {
+          message: "Use `import Model from '@ember-data/model';` instead of using DS.Model",
+          type: 'Identifier',
+        },
+      ],
+    },
+    {
+      code: `import DS, { Model } from 'ember-data';
+
+        export default Model.extend({
+          name: DS.attr('string')
+        });
+      `,
+      errors: [
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+        {
+          message: "Use `import { attr } from '@ember-data/model';` instead of using DS.attr",
+          type: 'Identifier',
+        },
+      ],
+    },
+    {
+      code: `import DS from 'ember-data';
+        const { Model, attr } = DS;
+
+        export default Model.extend({
+          name: attr('string')
+        });
+      `,
+      errors: [
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+        {
+          message: "Use `import Model from '@ember-data/model';` instead of using DS destructuring",
+          type: 'Property',
+        },
+        {
+          message:
+            "Use `import { attr } from '@ember-data/model';` instead of using DS destructuring",
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `import Model from 'ember-data/model';
+
+        export default Model.extend({
+        });
+      `,
+      errors: [
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+      ],
+    },
+    {
+      code: `import Model from 'ember-data/model';
+        import attr  from 'ember-data/attr';
+
+        export default Model.extend({
+          name: attr('string')
+        });
+      `,
+      errors: [
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+      ],
+    },
+    {
+      code: `import Model from '@ember-data/model';
+        import attr from 'ember-data/attr';
+
+        export default Model.extend({
+          name: attr('string')
+        });
+      `,
+      errors: [
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+      ],
+    },
+    {
+      code: `import Model from '@ember-data/model';
+        import namedAttr from 'ember-data/attr';
+
+        export default Model.extend({
+          name: namedAttr('string')
+        });
+      `,
+      errors: [
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+      ],
+    },
+    {
+      code: `import { Model } from 'ember-data';
+
+        export default Model.extend({
+        });
+      `,
+      errors: [
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+      ],
+    },
+    {
+      code: `import ED from 'ember-data';
+
+        const { Model } = ED;
+      `,
+      errors: [
+        {
+          message,
+          type: 'ImportDeclaration',
+        },
+        {
+          message: "Use `import Model from '@ember-data/model';` instead of using DS destructuring",
+          type: 'Property',
+        },
+      ],
+    },
+  ],
+});