Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] Add no-empty-named-blocks rule #2568

Merged
merged 1 commit into from Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'],
}),
),
});