Repository: import-js/eslint-plugin-import (js)

Issue:
New rule: disallow empty named blocks
Spinning this off of our discussion here https://github.com/import-js/eslint-plugin-import/pull/2473#discussion_r912304493

According to [the ES6 spec](https://262.ecma-international.org/6.0/#sec-imports) an empty named import block `import {} from 'mod'` is valid syntax:

```
NamedImports :
    { }
    { ImportsList }
    { ImportsList , }
```

Because it's valid - all ESLint parsers allow it for value imports.

Parsers that handle type-only imports (flow/hermes/typescript/babel) also allow an empty named import block: `import type {} from 'mod'`, (flow only) `import typeof {} from 'mod'`.

I suggest we build a new rule, `no-empty-named-blocks`, which bans these empty import blocks entirely.

# Examples

## ✅ Valid

```js
import 'mod';

import Default from 'mod';
import { Named } from 'mod';
import Default, { Named } from 'mod';
import * as Namespace from 'mod';

import type Default from 'mod';
import type { Named } from 'mod';
import type Default, { Named } from 'mod'; // note - invalid TS but valid Flow
import type * as Namespace from 'mod';     // note - valid TS but invalid Flow

// flow only
import typeof Default from 'mod';
import typeof { Named } from 'mod';
import typeof Default, { Named } from 'mod';
```

## ❌ Invalid

```js
import {} from 'mod';
import Default, {} from 'mod';

import type {} from 'mod';
import type Default, {} from 'mod'; // note - invalid TS but valid Flow

// flow only
import typeof {} from 'mod';
import typeof Default, {} from 'mod';
```

# Fixer

There are 3 cases for us to consider here:

First, an import with a default and an empty named block (either value, type, or typeof).
This can always be safely fixed to remove the empty block:
```diff
- import Default, {} from 'mod';
+ import Default from 'mod';

// flow only
- import type Default, {} from 'mod';
+ import type Default from 'mod';

// flow only
- import typeof Default, {} from 'mod';
+ import typeof Default from 'mod';
```

Second, a type-/typeof-only import with just an empty named block.
This can always be removed in its entirety as type-/typeof-only imports never have side-effects
```diff
- import type {} from 'mod';
+ 
```

Finally a value import with just an empty block.
This cannot be safely deleted because even without any specifiers the import will still trigger side-effects. Instead of an autofixer, we can use a suggestion fixer with two suggestions:
1) Delete the import (fix by deleting the entire statement)
    ```diff
    - import {} from 'mod';
    + 
    ```
2) Convert to a side-effect import (fix by deleting the `{} from`)
    ```diff
    - import {} from 'mod';
    + import 'mod';
    ```


Pull request description:
[New] Add `no-empty-named-blocks` rule
## Description

This PR adds the `no-empty-named-blocks` rule, which forbids empty named blocks on imports.

Closes #2538


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06d0ebf2..86df0edd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 - [`no-extraneous-dependencies`]: Add `includeTypes` option ([#2543], thanks [@bdwain])
 - [`order`]: new `alphabetize.orderImportKind` option to sort imports with same path based on their kind (`type`, `typeof`) ([#2544], thanks [@stropho])
 - [`consistent-type-specifier-style`]: add rule ([#2473], thanks [@bradzacher])
+- Add [`no-empty-named-blocks`] rule ([#2568], thanks [@guilhermelimak])
 
 ### Fixed
 - [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311])
@@ -990,6 +991,7 @@ for info on changes for earlier releases.
 [`no-deprecated`]: ./docs/rules/no-deprecated.md
 [`no-duplicates`]: ./docs/rules/no-duplicates.md
 [`no-dynamic-require`]: ./docs/rules/no-dynamic-require.md
+[`no-empty-named-blocks`]: ./docs/rules/no-empty-named-blocks.md
 [`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md
 [`no-import-module-exports`]: ./docs/rules/no-import-module-exports.md
 [`no-internal-modules`]: ./docs/rules/no-internal-modules.md
@@ -1016,6 +1018,7 @@ for info on changes for earlier releases.
 [`memo-parser`]: ./memo-parser/README.md
 
 [#2570]: https://github.com/import-js/eslint-plugin-import/pull/2570
+[#2568]: https://github.com/import-js/eslint-plugin-import/pull/2568
 [#2546]: https://github.com/import-js/eslint-plugin-import/pull/2546
 [#2541]: https://github.com/import-js/eslint-plugin-import/pull/2541
 [#2531]: https://github.com/import-js/eslint-plugin-import/pull/2531
@@ -1603,13 +1606,14 @@ for info on changes for earlier releases.
 [@futpib]: https://github.com/futpib
 [@gajus]: https://github.com/gajus
 [@gausie]: https://github.com/gausie
-[@georeith]: https://github.com/georeith
 [@gavriguy]: https://github.com/gavriguy
+[@georeith]: https://github.com/georeith
 [@giodamelio]: https://github.com/giodamelio
 [@golopot]: https://github.com/golopot
 [@GoodForOneFare]: https://github.com/GoodForOneFare
 [@graingert]: https://github.com/graingert
 [@grit96]: https://github.com/grit96
+[@guilhermelimak]: https://github.com/guilhermelimak
 [@guillaumewuip]: https://github.com/guillaumewuip
 [@hayes]: https://github.com/hayes
 [@himynameisdave]: https://github.com/himynameisdave
diff --git a/README.md b/README.md
index 775fc198..0e550f8f 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
 * Forbid the use of extraneous packages ([`no-extraneous-dependencies`])
 * Forbid the use of mutable exports with `var` or `let`. ([`no-mutable-exports`])
 * Report modules without exports, or exports without matching import in another module ([`no-unused-modules`])
+* Prevent empty named import blocks ([`no-empty-named-blocks`])
 
 [`export`]: ./docs/rules/export.md
 [`no-named-as-default`]: ./docs/rules/no-named-as-default.md
@@ -63,6 +64,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
 [`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md
 [`no-mutable-exports`]: ./docs/rules/no-mutable-exports.md
 [`no-unused-modules`]: ./docs/rules/no-unused-modules.md
+[`no-empty-named-blocks`]: ./docs/rules/no-empty-named-blocks.md
 
 ### Module systems
 
diff --git a/docs/rules/no-empty-named-blocks.md b/docs/rules/no-empty-named-blocks.md
new file mode 100644
index 00000000..229d9274
--- /dev/null
+++ b/docs/rules/no-empty-named-blocks.md
@@ -0,0 +1,39 @@
+# import/no-empty-named-blocks
+
+Reports the use of empty named import blocks.
+
+## Rule Details
+
+### Valid
+```js
+import { mod } from 'mod'
+import Default, { mod } from 'mod'
+```
+
+When using typescript
+```js
+import type { mod } from 'mod'
+```
+
+When using flow
+```js
+import typeof { mod } from 'mod'
+```
+
+### Invalid
+```js
+import {} from 'mod'
+import Default, {} from 'mod'
+```
+
+When using typescript
+```js
+import type Default, {} from 'mod'
+import type {} from 'mod'
+```
+
+When using flow
+```js
+import typeof {} from 'mod'
+import typeof Default, {} from 'mod'
+```
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index fd83a4aa..15f98d96 100644
--- a/src/index.js
+++ b/src/index.js
@@ -42,6 +42,7 @@ export const rules = {
   'no-useless-path-segments': require('./rules/no-useless-path-segments'),
   'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
   'no-import-module-exports': require('./rules/no-import-module-exports'),
+  'no-empty-named-blocks': require('./rules/no-empty-named-blocks'),
 
   // export
   'exports-last': require('./rules/exports-last'),
diff --git a/src/rules/no-empty-named-blocks.js b/src/rules/no-empty-named-blocks.js
new file mode 100644
index 00000000..65a8515c
--- /dev/null
+++ b/src/rules/no-empty-named-blocks.js
@@ -0,0 +1,91 @@
+import docsUrl from '../docsUrl';
+
+function getEmptyBlockRange(tokens, index) {
+  const token = tokens[index];
+  const nextToken = tokens[index + 1];
+  const prevToken = tokens[index - 1];
+  let start = token.range[0];
+  const end = nextToken.range[1];
+
+  // Remove block tokens and the previous comma
+  if (prevToken.value === ','|| prevToken.value === 'type' || prevToken.value === 'typeof') {
+    start = prevToken.range[0];
+  }
+
+  return [start, end];
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      url: docsUrl('no-empty-named-blocks'),
+    },
+    fixable: 'code',
+    schema: [],
+    hasSuggestions: true,
+  },
+
+  create(context) {
+    return {
+      Program(node) {
+        node.tokens.forEach((token, idx) => {
+          const nextToken = node.tokens[idx + 1];
+
+          if (nextToken && token.value === '{' && nextToken.value === '}') {
+            const hasOtherIdentifiers = node.tokens.some((token) => (
+              token.type === 'Identifier'
+                && token.value !== 'from'
+                && token.value !== 'type'
+                && token.value !== 'typeof'
+            ));
+
+            // If it has no other identifiers it's the only thing in the import, so we can either remove the import
+            // completely or transform it in a side-effects only import
+            if (!hasOtherIdentifiers) {
+              context.report({
+                node,
+                message: 'Unexpected empty named import block',
+                suggest: [
+                  {
+                    desc: 'Remove unused import',
+                    fix(fixer) {
+                      // Remove the whole import
+                      return fixer.remove(node);
+                    },
+                  },
+                  {
+                    desc: 'Remove empty import block',
+                    fix(fixer) {
+                      // Remove the empty block and the 'from' token, leaving the import only for its side
+                      // effects, e.g. `import 'mod'`
+                      const sourceCode = context.getSourceCode();
+                      const fromToken = node.tokens.find(t => t.value === 'from');
+                      const importToken = node.tokens.find(t => t.value === 'import');
+                      const hasSpaceAfterFrom = sourceCode.isSpaceBetween(fromToken, sourceCode.getTokenAfter(fromToken));
+                      const hasSpaceAfterImport = sourceCode.isSpaceBetween(importToken, sourceCode.getTokenAfter(fromToken));
+
+                      const [start] = getEmptyBlockRange(node.tokens, idx);
+                      const [, end] = fromToken.range;
+                      const range = [start, hasSpaceAfterFrom ? end + 1 : end];
+
+                      return fixer.replaceTextRange(range, hasSpaceAfterImport ? '' : ' ');
+                    },
+                  },
+                ],
+              });
+            } else {
+              context.report({
+                node,
+                message: 'Unexpected empty named import block',
+                fix(fixer) {
+                  return fixer.removeRange(getEmptyBlockRange(node.tokens, idx));
+                },
+              });
+            }
+          }
+        });
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/files/empty-named-blocks.js b/tests/files/empty-named-blocks.js
new file mode 100644
index 00000000..4640c7f8
--- /dev/null
+++ b/tests/files/empty-named-blocks.js
@@ -0,0 +1,1 @@
+import {} from './bar.js';
diff --git a/tests/src/rules/no-empty-named-blocks.js b/tests/src/rules/no-empty-named-blocks.js
new file mode 100644
index 00000000..ee21db34
--- /dev/null
+++ b/tests/src/rules/no-empty-named-blocks.js
@@ -0,0 +1,98 @@
+import { parsers, test } from '../utils';
+
+import { RuleTester } from 'eslint';
+
+const ruleTester = new RuleTester();
+const rule = require('rules/no-empty-named-blocks');
+
+
+function generateSuggestionsTestCases(cases, parser) {
+  return cases.map(code => test({
+    code,
+    parser,
+    errors: [{
+      suggestions: [
+        {
+          desc: 'Remove unused import',
+          output: '',
+        },
+        {
+          desc: 'Remove empty import block',
+          output: `import 'mod';`,
+        },
+      ],
+    }],
+  }));
+}
+
+ruleTester.run('no-empty-named-blocks', rule, {
+  valid: [].concat(
+    test({ code: `import 'mod';` }),
+    test({ code: `import Default from 'mod';` }),
+    test({ code: `import { Named } from 'mod';` }),
+    test({ code: `import Default, { Named } from 'mod';` }),
+    test({ code: `import * as Namespace from 'mod';` }),
+
+    // Typescript
+    parsers.TS_NEW ? [
+      test({ code: `import type Default from 'mod';`, parser: parsers.TS_NEW }),
+      test({ code: `import type { Named } from 'mod';`, parser: parsers.TS_NEW }),
+      test({ code: `import type Default, { Named } from 'mod';`, parser: parsers.TS_NEW }),
+      test({ code: `import type * as Namespace from 'mod';`, parser: parsers.TS_NEW }),
+    ] : [],
+
+    // Flow
+    test({ code: `import typeof Default from 'mod';`, parser: parsers.BABEL_OLD }),
+    test({ code: `import typeof { Named } from 'mod';`, parser: parsers.BABEL_OLD }),
+    test({ code: `import typeof Default, { Named } from 'mod';`, parser: parsers.BABEL_OLD }),
+  ),
+  invalid: [].concat(
+    test({
+      code: `import Default, {} from 'mod';`,
+      output: `import Default from 'mod';`,
+      errors: ['Unexpected empty named import block'],
+    }),
+    generateSuggestionsTestCases([
+      `import {} from 'mod';`,
+      `import{}from'mod';`,
+      `import {} from'mod';`,
+      `import {}from 'mod';`,
+    ]),
+
+    // Typescript
+    parsers.TS_NEW ? [].concat(
+      generateSuggestionsTestCases(
+        [
+          `import type {} from 'mod';`,
+          `import type {}from 'mod';`,
+          `import type{}from 'mod';`,
+          `import type {}from'mod';`,
+        ],
+        parsers.TS_NEW,
+      ),
+      test({
+        code: `import type Default, {} from 'mod';`,
+        output: `import type Default from 'mod';`,
+        parser: parsers.TS_NEW,
+        errors: ['Unexpected empty named import block'],
+      }),
+    ) : [],
+
+    // Flow
+    generateSuggestionsTestCases(
+      [
+        `import typeof {} from 'mod';`,
+        `import typeof {}from 'mod';`,
+        `import typeof {} from'mod';`,
+        `import typeof{}from'mod';`,
+      ],
+      parsers.BABEL_OLD,
+    ),
+    test({
+      code: `import typeof Default, {} from 'mod';`,
+      output: `import typeof Default from 'mod';`,
+      parser: parsers.BABEL_OLD,
+      errors: ['Unexpected empty named import block'],
+    }),
+  ),
+});