Repository: ember-cli/eslint-plugin-ember (js)

Issue:
New Rule: require-fetch-import
Using `fetch()` without importing it causes the browser to use the native, non-wrapped `window.fetch()`. This is generally fine, but makes testing harder because this non-wrapped version does not have a built-in test waiter. Because of this it is generally better to use [ember-fetch](https://github.com/ember-cli/ember-fetch/) and explicitly `import fetch from 'fetch'`.

The new rule should look for `fetch()` calls and warn about them if there is no corresponding import.

Pull request description:
Implement `require-fetch-import` rule

Resolves #1053 

---

# require-fetch-import

Using `fetch()` without importing it causes the browser to use the native,
non-wrapped `window.fetch()`. This is generally fine, but makes testing harder
because this non-wrapped version does not have a built-in test waiter. Because
of this it is generally better to use [ember-fetch] and explicitly
`import fetch from 'fetch'`.

## Rule Details

The rule looks for `fetch()` calls and reports them as issues if no
corresponding import declaration is found.

## Examples

Examples of **incorrect** code for this rule:

```js
const result = fetch('/something');
```

Examples of **correct** code for this rule:

```js
import fetch from 'fetch';

const result = fetch('/something');
```

## Migration

* Add `import fetch from 'fetch';` to all files that need it

## References

* [@ember/test-waiters](https://github.com/emberjs/ember-test-waiters) addon

[ember-fetch]: https://github.com/ember-cli/ember-fetch/


Patch:
diff --git a/README.md b/README.md
index 8dd0956d..e2c94f6e 100644
--- a/README.md
+++ b/README.md
@@ -155,6 +155,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
 | :white_check_mark: | [no-incorrect-calls-with-inline-anonymous-functions](./docs/rules/no-incorrect-calls-with-inline-anonymous-functions.md) | disallow inline anonymous functions as arguments to `debounce`, `once`, and `scheduleOnce` |
 | :white_check_mark: | [no-invalid-debug-function-arguments](./docs/rules/no-invalid-debug-function-arguments.md) | disallow usages of Ember's `assert()` / `warn()` / `deprecate()` functions that have the arguments passed in the wrong order. |
 |  | [no-restricted-service-injections](./docs/rules/no-restricted-service-injections.md) | disallow injecting certain services under certain paths |
+|  | [require-fetch-import](./docs/rules/require-fetch-import.md) | enforce explicit import for `fetch()` |
 
 ### Routes
 
diff --git a/docs/rules/require-fetch-import.md b/docs/rules/require-fetch-import.md
new file mode 100644
index 00000000..42c49354
--- /dev/null
+++ b/docs/rules/require-fetch-import.md
@@ -0,0 +1,38 @@
+# require-fetch-import
+
+Using `fetch()` without importing it causes the browser to use the native,
+non-wrapped `window.fetch()`. This is generally fine, but makes testing harder
+because this non-wrapped version does not have a built-in test waiter. Because
+of this it is generally better to use [ember-fetch] and explicitly
+`import fetch from 'fetch'`.
+
+## Rule Details
+
+The rule looks for `fetch()` calls and reports them as issues if no
+corresponding import declaration is found.
+
+## Examples
+
+Examples of **incorrect** code for this rule:
+
+```js
+const result = fetch('/something');
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import fetch from 'fetch';
+
+const result = fetch('/something');
+```
+
+## Migration
+
+* Add `import fetch from 'fetch';` to all files that need it
+
+## References
+
+* [@ember/test-waiters](https://github.com/emberjs/ember-test-waiters) addon
+
+[ember-fetch]: https://github.com/ember-cli/ember-fetch/
diff --git a/lib/index.js b/lib/index.js
index 9876af05..25aac65a 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -74,6 +74,7 @@ module.exports = {
     'prefer-ember-test-helpers': require('./rules/prefer-ember-test-helpers'),
     'require-computed-macros': require('./rules/require-computed-macros'),
     'require-computed-property-dependencies': require('./rules/require-computed-property-dependencies'),
+    'require-fetch-import': require('./rules/require-fetch-import'),
     'require-return-from-computed': require('./rules/require-return-from-computed'),
     'require-super-in-lifecycle-hooks': require('./rules/require-super-in-lifecycle-hooks'),
     'require-tagless-components': require('./rules/require-tagless-components'),
diff --git a/lib/rules/require-fetch-import.js b/lib/rules/require-fetch-import.js
new file mode 100644
index 00000000..30501153
--- /dev/null
+++ b/lib/rules/require-fetch-import.js
@@ -0,0 +1,38 @@
+'use strict';
+
+const { ReferenceTracker } = require('eslint-utils');
+
+const ERROR_MESSAGE = 'Explicitly import `fetch` instead of using `window.fetch`';
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce explicit import for `fetch()`',
+      category: 'Miscellaneous',
+      recommended: false,
+      url:
+        'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/require-fetch-import.md',
+    },
+    fixable: null,
+    schema: [],
+  },
+
+  ERROR_MESSAGE,
+
+  create(context) {
+    return {
+      'Program:exit'() {
+        const tracker = new ReferenceTracker(context.getScope());
+
+        const traceMap = {
+          fetch: { [ReferenceTracker.CALL]: true },
+        };
+
+        for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
+          context.report(node, ERROR_MESSAGE);
+        }
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/require-fetch-import.js b/tests/lib/rules/require-fetch-import.js
new file mode 100644
index 00000000..7fbd21b4
--- /dev/null
+++ b/tests/lib/rules/require-fetch-import.js
@@ -0,0 +1,40 @@
+'use strict';
+
+const rule = require('../../../lib/rules/require-fetch-import');
+const RuleTester = require('eslint').RuleTester;
+
+const { ERROR_MESSAGE } = rule;
+
+const ruleTester = new RuleTester({
+  parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+  env: { browser: true },
+});
+
+ruleTester.run('require-fetch-import', rule, {
+  valid: [
+    `
+      import fetch from 'fetch';
+
+      fetch('/something');
+    `,
+    `
+      fetch('/something');
+
+      import fetch from 'fetch';
+    `,
+    `
+      import { default as fetch } from 'fetch';
+
+      fetch('/something');
+    `,
+    "foo('/something');",
+  ],
+
+  invalid: [
+    {
+      code: "fetch('/something');",
+      output: null,
+      errors: [{ message: ERROR_MESSAGE, type: 'CallExpression', line: 1, column: 1 }],
+    },
+  ],
+});