Skip to content

Commit

Permalink
[New] Add no-empty-named-blocks rule
Browse files Browse the repository at this point in the history
  • Loading branch information
guilhermelimak authored and ljharb committed Oct 11, 2022
1 parent 4bfe644 commit 2e1edd6
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 1 deletion.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Expand Up @@ -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])
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -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
Expand All @@ -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

Expand Down
39 changes: 39 additions & 0 deletions 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'
```
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -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'),
Expand Down
91 changes: 91 additions & 0 deletions 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));
},
});
}
}
});
},
};
},
};
1 change: 1 addition & 0 deletions tests/files/empty-named-blocks.js
@@ -0,0 +1 @@
import {} from './bar.js';
98 changes: 98 additions & 0 deletions 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'],
}),
),
});

0 comments on commit 2e1edd6

Please sign in to comment.