Skip to content

Commit 2e1edd6

Browse files
guilhermelimakljharb
authored andcommitted
[New] Add no-empty-named-blocks rule
1 parent 4bfe644 commit 2e1edd6

File tree

7 files changed

+237
-1
lines changed

7 files changed

+237
-1
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1616
- [`no-extraneous-dependencies`]: Add `includeTypes` option ([#2543], thanks [@bdwain])
1717
- [`order`]: new `alphabetize.orderImportKind` option to sort imports with same path based on their kind (`type`, `typeof`) ([#2544], thanks [@stropho])
1818
- [`consistent-type-specifier-style`]: add rule ([#2473], thanks [@bradzacher])
19+
- Add [`no-empty-named-blocks`] rule ([#2568], thanks [@guilhermelimak])
1920

2021
### Fixed
2122
- [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311])
@@ -990,6 +991,7 @@ for info on changes for earlier releases.
990991
[`no-deprecated`]: ./docs/rules/no-deprecated.md
991992
[`no-duplicates`]: ./docs/rules/no-duplicates.md
992993
[`no-dynamic-require`]: ./docs/rules/no-dynamic-require.md
994+
[`no-empty-named-blocks`]: ./docs/rules/no-empty-named-blocks.md
993995
[`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md
994996
[`no-import-module-exports`]: ./docs/rules/no-import-module-exports.md
995997
[`no-internal-modules`]: ./docs/rules/no-internal-modules.md
@@ -1016,6 +1018,7 @@ for info on changes for earlier releases.
10161018
[`memo-parser`]: ./memo-parser/README.md
10171019

10181020
[#2570]: https://github.com/import-js/eslint-plugin-import/pull/2570
1021+
[#2568]: https://github.com/import-js/eslint-plugin-import/pull/2568
10191022
[#2546]: https://github.com/import-js/eslint-plugin-import/pull/2546
10201023
[#2541]: https://github.com/import-js/eslint-plugin-import/pull/2541
10211024
[#2531]: https://github.com/import-js/eslint-plugin-import/pull/2531
@@ -1603,13 +1606,14 @@ for info on changes for earlier releases.
16031606
[@futpib]: https://github.com/futpib
16041607
[@gajus]: https://github.com/gajus
16051608
[@gausie]: https://github.com/gausie
1606-
[@georeith]: https://github.com/georeith
16071609
[@gavriguy]: https://github.com/gavriguy
1610+
[@georeith]: https://github.com/georeith
16081611
[@giodamelio]: https://github.com/giodamelio
16091612
[@golopot]: https://github.com/golopot
16101613
[@GoodForOneFare]: https://github.com/GoodForOneFare
16111614
[@graingert]: https://github.com/graingert
16121615
[@grit96]: https://github.com/grit96
1616+
[@guilhermelimak]: https://github.com/guilhermelimak
16131617
[@guillaumewuip]: https://github.com/guillaumewuip
16141618
[@hayes]: https://github.com/hayes
16151619
[@himynameisdave]: https://github.com/himynameisdave

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
5555
* Forbid the use of extraneous packages ([`no-extraneous-dependencies`])
5656
* Forbid the use of mutable exports with `var` or `let`. ([`no-mutable-exports`])
5757
* Report modules without exports, or exports without matching import in another module ([`no-unused-modules`])
58+
* Prevent empty named import blocks ([`no-empty-named-blocks`])
5859

5960
[`export`]: ./docs/rules/export.md
6061
[`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
6364
[`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md
6465
[`no-mutable-exports`]: ./docs/rules/no-mutable-exports.md
6566
[`no-unused-modules`]: ./docs/rules/no-unused-modules.md
67+
[`no-empty-named-blocks`]: ./docs/rules/no-empty-named-blocks.md
6668

6769
### Module systems
6870

docs/rules/no-empty-named-blocks.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# import/no-empty-named-blocks
2+
3+
Reports the use of empty named import blocks.
4+
5+
## Rule Details
6+
7+
### Valid
8+
```js
9+
import { mod } from 'mod'
10+
import Default, { mod } from 'mod'
11+
```
12+
13+
When using typescript
14+
```js
15+
import type { mod } from 'mod'
16+
```
17+
18+
When using flow
19+
```js
20+
import typeof { mod } from 'mod'
21+
```
22+
23+
### Invalid
24+
```js
25+
import {} from 'mod'
26+
import Default, {} from 'mod'
27+
```
28+
29+
When using typescript
30+
```js
31+
import type Default, {} from 'mod'
32+
import type {} from 'mod'
33+
```
34+
35+
When using flow
36+
```js
37+
import typeof {} from 'mod'
38+
import typeof Default, {} from 'mod'
39+
```

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const rules = {
4242
'no-useless-path-segments': require('./rules/no-useless-path-segments'),
4343
'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
4444
'no-import-module-exports': require('./rules/no-import-module-exports'),
45+
'no-empty-named-blocks': require('./rules/no-empty-named-blocks'),
4546

4647
// export
4748
'exports-last': require('./rules/exports-last'),

src/rules/no-empty-named-blocks.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import docsUrl from '../docsUrl';
2+
3+
function getEmptyBlockRange(tokens, index) {
4+
const token = tokens[index];
5+
const nextToken = tokens[index + 1];
6+
const prevToken = tokens[index - 1];
7+
let start = token.range[0];
8+
const end = nextToken.range[1];
9+
10+
// Remove block tokens and the previous comma
11+
if (prevToken.value === ','|| prevToken.value === 'type' || prevToken.value === 'typeof') {
12+
start = prevToken.range[0];
13+
}
14+
15+
return [start, end];
16+
}
17+
18+
module.exports = {
19+
meta: {
20+
type: 'suggestion',
21+
docs: {
22+
url: docsUrl('no-empty-named-blocks'),
23+
},
24+
fixable: 'code',
25+
schema: [],
26+
hasSuggestions: true,
27+
},
28+
29+
create(context) {
30+
return {
31+
Program(node) {
32+
node.tokens.forEach((token, idx) => {
33+
const nextToken = node.tokens[idx + 1];
34+
35+
if (nextToken && token.value === '{' && nextToken.value === '}') {
36+
const hasOtherIdentifiers = node.tokens.some((token) => (
37+
token.type === 'Identifier'
38+
&& token.value !== 'from'
39+
&& token.value !== 'type'
40+
&& token.value !== 'typeof'
41+
));
42+
43+
// If it has no other identifiers it's the only thing in the import, so we can either remove the import
44+
// completely or transform it in a side-effects only import
45+
if (!hasOtherIdentifiers) {
46+
context.report({
47+
node,
48+
message: 'Unexpected empty named import block',
49+
suggest: [
50+
{
51+
desc: 'Remove unused import',
52+
fix(fixer) {
53+
// Remove the whole import
54+
return fixer.remove(node);
55+
},
56+
},
57+
{
58+
desc: 'Remove empty import block',
59+
fix(fixer) {
60+
// Remove the empty block and the 'from' token, leaving the import only for its side
61+
// effects, e.g. `import 'mod'`
62+
const sourceCode = context.getSourceCode();
63+
const fromToken = node.tokens.find(t => t.value === 'from');
64+
const importToken = node.tokens.find(t => t.value === 'import');
65+
const hasSpaceAfterFrom = sourceCode.isSpaceBetween(fromToken, sourceCode.getTokenAfter(fromToken));
66+
const hasSpaceAfterImport = sourceCode.isSpaceBetween(importToken, sourceCode.getTokenAfter(fromToken));
67+
68+
const [start] = getEmptyBlockRange(node.tokens, idx);
69+
const [, end] = fromToken.range;
70+
const range = [start, hasSpaceAfterFrom ? end + 1 : end];
71+
72+
return fixer.replaceTextRange(range, hasSpaceAfterImport ? '' : ' ');
73+
},
74+
},
75+
],
76+
});
77+
} else {
78+
context.report({
79+
node,
80+
message: 'Unexpected empty named import block',
81+
fix(fixer) {
82+
return fixer.removeRange(getEmptyBlockRange(node.tokens, idx));
83+
},
84+
});
85+
}
86+
}
87+
});
88+
},
89+
};
90+
},
91+
};

tests/files/empty-named-blocks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import {} from './bar.js';
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { parsers, test } from '../utils';
2+
3+
import { RuleTester } from 'eslint';
4+
5+
const ruleTester = new RuleTester();
6+
const rule = require('rules/no-empty-named-blocks');
7+
8+
9+
function generateSuggestionsTestCases(cases, parser) {
10+
return cases.map(code => test({
11+
code,
12+
parser,
13+
errors: [{
14+
suggestions: [
15+
{
16+
desc: 'Remove unused import',
17+
output: '',
18+
},
19+
{
20+
desc: 'Remove empty import block',
21+
output: `import 'mod';`,
22+
},
23+
],
24+
}],
25+
}));
26+
}
27+
28+
ruleTester.run('no-empty-named-blocks', rule, {
29+
valid: [].concat(
30+
test({ code: `import 'mod';` }),
31+
test({ code: `import Default from 'mod';` }),
32+
test({ code: `import { Named } from 'mod';` }),
33+
test({ code: `import Default, { Named } from 'mod';` }),
34+
test({ code: `import * as Namespace from 'mod';` }),
35+
36+
// Typescript
37+
parsers.TS_NEW ? [
38+
test({ code: `import type Default from 'mod';`, parser: parsers.TS_NEW }),
39+
test({ code: `import type { Named } from 'mod';`, parser: parsers.TS_NEW }),
40+
test({ code: `import type Default, { Named } from 'mod';`, parser: parsers.TS_NEW }),
41+
test({ code: `import type * as Namespace from 'mod';`, parser: parsers.TS_NEW }),
42+
] : [],
43+
44+
// Flow
45+
test({ code: `import typeof Default from 'mod';`, parser: parsers.BABEL_OLD }),
46+
test({ code: `import typeof { Named } from 'mod';`, parser: parsers.BABEL_OLD }),
47+
test({ code: `import typeof Default, { Named } from 'mod';`, parser: parsers.BABEL_OLD }),
48+
),
49+
invalid: [].concat(
50+
test({
51+
code: `import Default, {} from 'mod';`,
52+
output: `import Default from 'mod';`,
53+
errors: ['Unexpected empty named import block'],
54+
}),
55+
generateSuggestionsTestCases([
56+
`import {} from 'mod';`,
57+
`import{}from'mod';`,
58+
`import {} from'mod';`,
59+
`import {}from 'mod';`,
60+
]),
61+
62+
// Typescript
63+
parsers.TS_NEW ? [].concat(
64+
generateSuggestionsTestCases(
65+
[
66+
`import type {} from 'mod';`,
67+
`import type {}from 'mod';`,
68+
`import type{}from 'mod';`,
69+
`import type {}from'mod';`,
70+
],
71+
parsers.TS_NEW,
72+
),
73+
test({
74+
code: `import type Default, {} from 'mod';`,
75+
output: `import type Default from 'mod';`,
76+
parser: parsers.TS_NEW,
77+
errors: ['Unexpected empty named import block'],
78+
}),
79+
) : [],
80+
81+
// Flow
82+
generateSuggestionsTestCases(
83+
[
84+
`import typeof {} from 'mod';`,
85+
`import typeof {}from 'mod';`,
86+
`import typeof {} from'mod';`,
87+
`import typeof{}from'mod';`,
88+
],
89+
parsers.BABEL_OLD,
90+
),
91+
test({
92+
code: `import typeof Default, {} from 'mod';`,
93+
output: `import typeof Default from 'mod';`,
94+
parser: parsers.BABEL_OLD,
95+
errors: ['Unexpected empty named import block'],
96+
}),
97+
),
98+
});

0 commit comments

Comments
 (0)